From 9f7cc86a22fbfc3276ff60d4bad25d5b3b7ac3de Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Sat, 10 Jun 2023 18:42:43 -0400 Subject: [PATCH 001/447] Add more details to E722 (bare-except) docs (#5007) ## Summary Note that catching a bare `Exception` is better than catching no specific exception. ## Test Plan Documentation only. --- .../ruff/src/rules/pycodestyle/rules/bare_except.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs b/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs index f2d591e061..42adebdc54 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs @@ -12,7 +12,7 @@ use ruff_python_ast::source_code::Locator; /// A bare `except` catches `BaseException` which includes /// `KeyboardInterrupt`, `SystemExit`, `Exception`, and others. Catching /// `BaseException` can make it hard to interrupt the program (e.g., with -/// Ctrl-C) and disguise other problems. +/// Ctrl-C) and can disguise other problems. /// /// ## Example /// ```python @@ -30,6 +30,17 @@ use ruff_python_ast::source_code::Locator; /// handle_error(e) /// ``` /// +/// If you actually need to catch an unknown error, use `Exception` which will +/// catch regular program errors but not important system exceptions. +/// +/// ```python +/// def run_a_function(some_other_fn): +/// try: +/// some_other_fn() +/// except Exception as e: +/// print(f"How exceptional! {e}") +/// ``` +/// /// ## References /// - [PEP 8](https://www.python.org/dev/peps/pep-0008/#programming-recommendations) /// - [Python: "Exception hierarchy"](https://docs.python.org/3/library/exceptions.html#exception-hierarchy) From ab3c02342bf9d87a6ee9cdbadd3d5626a3fea2ec Mon Sep 17 00:00:00 2001 From: Ryan Yang Date: Sat, 10 Jun 2023 19:17:58 -0700 Subject: [PATCH 002/447] Implement copyright notice detection (#4701) ## Summary Add copyright notice detection to enforce the presence of copyright headers in Python files. Configurable settings include: the relevant regular expression, the author name, and the minimum file size, similar to [flake8-copyright](https://github.com/savoirfairelinux/flake8-copyright). Closes https://github.com/charliermarsh/ruff/issues/3579 --------- Signed-off-by: ryan Co-authored-by: Charlie Marsh --- README.md | 1 + crates/ruff/src/checkers/physical_lines.rs | 8 + crates/ruff/src/codes.rs | 3 + crates/ruff/src/registry.rs | 4 + crates/ruff/src/rules/copyright/mod.rs | 158 ++++++++++++++++++ .../rules/missing_copyright_notice.rs | 59 +++++++ crates/ruff/src/rules/copyright/rules/mod.rs | 3 + crates/ruff/src/rules/copyright/settings.rs | 94 +++++++++++ ...les__copyright__tests__invalid_author.snap | 12 ++ ..._rules__copyright__tests__late_notice.snap | 12 ++ ...ruff__rules__copyright__tests__notice.snap | 4 + ...ules__copyright__tests__notice_with_c.snap | 4 + ...s__copyright__tests__notice_with_caps.snap | 4 + ...__copyright__tests__notice_with_range.snap | 4 + ...__rules__copyright__tests__small_file.snap | 4 + ...rules__copyright__tests__valid_author.snap | 4 + crates/ruff/src/rules/mod.rs | 1 + crates/ruff/src/settings/configuration.rs | 12 +- crates/ruff/src/settings/defaults.rs | 10 +- crates/ruff/src/settings/mod.rs | 11 +- crates/ruff/src/settings/options.rs | 12 +- crates/ruff_macros/src/rule_namespace.rs | 8 +- crates/ruff_wasm/src/lib.rs | 10 +- docs/faq.md | 2 + ruff.schema.json | 44 +++++ scripts/pyproject.toml | 1 + 26 files changed, 465 insertions(+), 24 deletions(-) create mode 100644 crates/ruff/src/rules/copyright/mod.rs create mode 100644 crates/ruff/src/rules/copyright/rules/missing_copyright_notice.rs create mode 100644 crates/ruff/src/rules/copyright/rules/mod.rs create mode 100644 crates/ruff/src/rules/copyright/settings.rs create mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__invalid_author.snap create mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__late_notice.snap create mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice.snap create mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_c.snap create mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_caps.snap create mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_range.snap create mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__small_file.snap create mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__valid_author.snap diff --git a/README.md b/README.md index b23d42d610..86755f5c23 100644 --- a/README.md +++ b/README.md @@ -261,6 +261,7 @@ quality tools, including: - [flake8-builtins](https://pypi.org/project/flake8-builtins/) - [flake8-commas](https://pypi.org/project/flake8-commas/) - [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/) +- [flake8-copyright](https://pypi.org/project/flake8-copyright/) - [flake8-datetimez](https://pypi.org/project/flake8-datetimez/) - [flake8-debugger](https://pypi.org/project/flake8-debugger/) - [flake8-django](https://pypi.org/project/flake8-django/) diff --git a/crates/ruff/src/checkers/physical_lines.rs b/crates/ruff/src/checkers/physical_lines.rs index 3a5345ae58..8934a75762 100644 --- a/crates/ruff/src/checkers/physical_lines.rs +++ b/crates/ruff/src/checkers/physical_lines.rs @@ -8,6 +8,7 @@ use ruff_python_ast::source_code::{Indexer, Locator, Stylist}; use ruff_python_whitespace::UniversalNewlines; use crate::registry::Rule; +use crate::rules::copyright::rules::missing_copyright_notice; use crate::rules::flake8_executable::helpers::{extract_shebang, ShebangDirective}; use crate::rules::flake8_executable::rules::{ shebang_missing, shebang_newline, shebang_not_executable, shebang_python, shebang_whitespace, @@ -49,6 +50,7 @@ pub(crate) fn check_physical_lines( let enforce_blank_line_contains_whitespace = settings.rules.enabled(Rule::BlankLineWithWhitespace); let enforce_tab_indentation = settings.rules.enabled(Rule::TabIndentation); + let enforce_copyright_notice = settings.rules.enabled(Rule::MissingCopyrightNotice); let fix_unnecessary_coding_comment = settings.rules.should_fix(Rule::UTF8EncodingDeclaration); let fix_shebang_whitespace = settings.rules.should_fix(Rule::ShebangLeadingWhitespace); @@ -172,6 +174,12 @@ pub(crate) fn check_physical_lines( } } + if enforce_copyright_notice { + if let Some(diagnostic) = missing_copyright_notice(locator, settings) { + diagnostics.push(diagnostic); + } + } + diagnostics } diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 9e4f05bd97..62dfc0ed77 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -374,6 +374,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Simplify, "401") => (RuleGroup::Unspecified, rules::flake8_simplify::rules::IfElseBlockInsteadOfDictGet), (Flake8Simplify, "910") => (RuleGroup::Unspecified, rules::flake8_simplify::rules::DictGetWithNoneDefault), + // copyright + (Copyright, "001") => (RuleGroup::Unspecified, rules::copyright::rules::MissingCopyrightNotice), + // pyupgrade (Pyupgrade, "001") => (RuleGroup::Unspecified, rules::pyupgrade::rules::UselessMetaclassType), (Pyupgrade, "003") => (RuleGroup::Unspecified, rules::pyupgrade::rules::TypeOfPrimitive), diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 5100ec9384..c89df825dd 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -80,6 +80,9 @@ pub enum Linter { /// [flake8-commas](https://pypi.org/project/flake8-commas/) #[prefix = "COM"] Flake8Commas, + /// Copyright-related rules + #[prefix = "CPY"] + Copyright, /// [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/) #[prefix = "C4"] Flake8Comprehensions, @@ -267,6 +270,7 @@ impl Rule { | Rule::ShebangLeadingWhitespace | Rule::TrailingWhitespace | Rule::TabIndentation + | Rule::MissingCopyrightNotice | Rule::BlankLineWithWhitespace => LintSource::PhysicalLines, Rule::AmbiguousUnicodeCharacterComment | Rule::AmbiguousUnicodeCharacterDocstring diff --git a/crates/ruff/src/rules/copyright/mod.rs b/crates/ruff/src/rules/copyright/mod.rs new file mode 100644 index 0000000000..ceadac5efb --- /dev/null +++ b/crates/ruff/src/rules/copyright/mod.rs @@ -0,0 +1,158 @@ +//! Rules related to copyright notices. +pub(crate) mod rules; + +pub mod settings; + +#[cfg(test)] +mod tests { + use crate::registry::Rule; + use crate::test::test_snippet; + use crate::{assert_messages, settings}; + + #[test] + fn notice() { + let diagnostics = test_snippet( + r#" +# Copyright 2023 + +import os +"# + .trim(), + &settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]), + ); + assert_messages!(diagnostics); + } + + #[test] + fn notice_with_c() { + let diagnostics = test_snippet( + r#" +# Copyright (C) 2023 + +import os +"# + .trim(), + &settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]), + ); + assert_messages!(diagnostics); + } + + #[test] + fn notice_with_caps() { + let diagnostics = test_snippet( + r#" +# COPYRIGHT (C) 2023 + +import os +"# + .trim(), + &settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]), + ); + assert_messages!(diagnostics); + } + + #[test] + fn notice_with_range() { + let diagnostics = test_snippet( + r#" +# Copyright (C) 2021-2023 + +import os +"# + .trim(), + &settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]), + ); + assert_messages!(diagnostics); + } + + #[test] + fn valid_author() { + let diagnostics = test_snippet( + r#" +# Copyright (C) 2023 Ruff + +import os +"# + .trim(), + &settings::Settings { + copyright: super::settings::Settings { + author: Some("Ruff".to_string()), + ..super::settings::Settings::default() + }, + ..settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]) + }, + ); + assert_messages!(diagnostics); + } + + #[test] + fn invalid_author() { + let diagnostics = test_snippet( + r#" +# Copyright (C) 2023 Some Author + +import os +"# + .trim(), + &settings::Settings { + copyright: super::settings::Settings { + author: Some("Ruff".to_string()), + ..super::settings::Settings::default() + }, + ..settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]) + }, + ); + assert_messages!(diagnostics); + } + + #[test] + fn small_file() { + let diagnostics = test_snippet( + r#" +import os +"# + .trim(), + &settings::Settings { + copyright: super::settings::Settings { + min_file_size: 256, + ..super::settings::Settings::default() + }, + ..settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]) + }, + ); + assert_messages!(diagnostics); + } + + #[test] + fn late_notice() { + let diagnostics = test_snippet( + r#" +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content + +# Copyright 2023 +"# + .trim(), + &settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]), + ); + assert_messages!(diagnostics); + } +} diff --git a/crates/ruff/src/rules/copyright/rules/missing_copyright_notice.rs b/crates/ruff/src/rules/copyright/rules/missing_copyright_notice.rs new file mode 100644 index 0000000000..00ceccffee --- /dev/null +++ b/crates/ruff/src/rules/copyright/rules/missing_copyright_notice.rs @@ -0,0 +1,59 @@ +use ruff_text_size::{TextRange, TextSize}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::source_code::Locator; + +use crate::settings::Settings; + +/// ## What it does +/// Checks for the absence of copyright notices within Python files. +/// +/// ## Why is this bad? +/// In some codebases, it's common to have a license header at the top of every +/// file. This rule ensures that the license header is present. +#[violation] +pub struct MissingCopyrightNotice; + +impl Violation for MissingCopyrightNotice { + #[derive_message_formats] + fn message(&self) -> String { + format!("Missing copyright notice at top of file") + } +} + +/// CPY001 +pub(crate) fn missing_copyright_notice( + locator: &Locator, + settings: &Settings, +) -> Option { + // Ignore files that are too small to contain a copyright notice. + if locator.len() < settings.copyright.min_file_size { + return None; + } + + // Only search the first 1024 bytes in the file. + let contents = if locator.len() < 1024 { + locator.contents() + } else { + locator.up_to(TextSize::from(1024)) + }; + + // Locate the copyright notice. + if let Some(match_) = settings.copyright.notice_rgx.find(contents) { + match settings.copyright.author { + Some(ref author) => { + // Ensure that it's immediately followed by the author. + if contents[match_.end()..].trim_start().starts_with(author) { + return None; + } + } + None => return None, + } + } + + Some(Diagnostic::new( + MissingCopyrightNotice, + TextRange::default(), + )) +} diff --git a/crates/ruff/src/rules/copyright/rules/mod.rs b/crates/ruff/src/rules/copyright/rules/mod.rs new file mode 100644 index 0000000000..860448155f --- /dev/null +++ b/crates/ruff/src/rules/copyright/rules/mod.rs @@ -0,0 +1,3 @@ +pub(crate) use missing_copyright_notice::{missing_copyright_notice, MissingCopyrightNotice}; + +mod missing_copyright_notice; diff --git a/crates/ruff/src/rules/copyright/settings.rs b/crates/ruff/src/rules/copyright/settings.rs new file mode 100644 index 0000000000..d3849a8752 --- /dev/null +++ b/crates/ruff/src/rules/copyright/settings.rs @@ -0,0 +1,94 @@ +//! Settings for the `copyright` plugin. + +use once_cell::sync::Lazy; +use regex::Regex; +use serde::{Deserialize, Serialize}; + +use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions}; + +#[derive( + Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions, CombineOptions, +)] +#[serde( + deny_unknown_fields, + rename_all = "kebab-case", + rename = "CopyrightOptions" +)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct Options { + #[option( + default = r#"(?i)Copyright\s+(\(C\)\s+)?\d{4}([-,]\d{4})*"#, + value_type = "str", + example = r#"notice-rgx = "(?i)Copyright \\(C\\) \\d{4}""# + )] + /// The regular expression used to match the copyright notice, compiled + /// with the [`regex`](https://docs.rs/regex/latest/regex/) crate. + /// + /// Defaults to `(?i)Copyright\s+(\(C\)\s+)?\d{4}(-\d{4})*`, which matches + /// the following: + /// - `Copyright 2023` + /// - `Copyright (C) 2023` + /// - `Copyright 2021-2023` + /// - `Copyright (C) 2021-2023` + pub notice_rgx: Option, + #[option(default = "None", value_type = "str", example = r#"author = "Ruff""#)] + /// Author to enforce within the copyright notice. If provided, the + /// author must be present immediately following the copyright notice. + pub author: Option, + #[option( + default = r#"0"#, + value_type = "int", + example = r#" + # Avoid enforcing a header on files smaller than 1024 bytes. + min-file-size = 1024 + "# + )] + /// A minimum file size (in bytes) required for a copyright notice to + /// be enforced. By default, all files are validated. + pub min_file_size: Option, +} + +#[derive(Debug, CacheKey)] +pub struct Settings { + pub notice_rgx: Regex, + pub author: Option, + pub min_file_size: usize, +} + +static COPYRIGHT: Lazy = + Lazy::new(|| Regex::new(r"(?i)Copyright\s+(\(C\)\s+)?\d{4}(-\d{4})*").unwrap()); + +impl Default for Settings { + fn default() -> Self { + Self { + notice_rgx: COPYRIGHT.clone(), + author: None, + min_file_size: 0, + } + } +} + +impl From for Settings { + fn from(options: Options) -> Self { + Self { + notice_rgx: options + .notice_rgx + .map(|pattern| Regex::new(&pattern)) + .transpose() + .expect("Invalid `notice-rgx`") + .unwrap_or_else(|| COPYRIGHT.clone()), + author: options.author, + min_file_size: options.min_file_size.unwrap_or_default(), + } + } +} + +impl From for Options { + fn from(settings: Settings) -> Self { + Self { + notice_rgx: Some(settings.notice_rgx.to_string()), + author: settings.author, + min_file_size: Some(settings.min_file_size), + } + } +} diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__invalid_author.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__invalid_author.snap new file mode 100644 index 0000000000..2b09c76351 --- /dev/null +++ b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__invalid_author.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff/src/rules/copyright/mod.rs +--- +:1:1: CPY001 Missing copyright notice at top of file + | +1 | # Copyright (C) 2023 Some Author + | CPY001 +2 | +3 | import os + | + + diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__late_notice.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__late_notice.snap new file mode 100644 index 0000000000..0bc581e98d --- /dev/null +++ b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__late_notice.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff/src/rules/copyright/mod.rs +--- +:1:1: CPY001 Missing copyright notice at top of file + | +1 | # Content Content Content Content Content Content Content Content Content Content + | CPY001 +2 | # Content Content Content Content Content Content Content Content Content Content +3 | # Content Content Content Content Content Content Content Content Content Content + | + + diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice.snap new file mode 100644 index 0000000000..2a35fab682 --- /dev/null +++ b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_c.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_c.snap new file mode 100644 index 0000000000..2a35fab682 --- /dev/null +++ b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_c.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_caps.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_caps.snap new file mode 100644 index 0000000000..2a35fab682 --- /dev/null +++ b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_caps.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_range.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_range.snap new file mode 100644 index 0000000000..2a35fab682 --- /dev/null +++ b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_range.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__small_file.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__small_file.snap new file mode 100644 index 0000000000..2a35fab682 --- /dev/null +++ b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__small_file.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__valid_author.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__valid_author.snap new file mode 100644 index 0000000000..2a35fab682 --- /dev/null +++ b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__valid_author.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/mod.rs b/crates/ruff/src/rules/mod.rs index 23083cb2ab..cc06dfb91d 100644 --- a/crates/ruff/src/rules/mod.rs +++ b/crates/ruff/src/rules/mod.rs @@ -1,5 +1,6 @@ #![allow(clippy::useless_format)] pub mod airflow; +pub mod copyright; pub mod eradicate; pub mod flake8_2020; pub mod flake8_annotations; diff --git a/crates/ruff/src/settings/configuration.rs b/crates/ruff/src/settings/configuration.rs index 16acabe30a..213128b966 100644 --- a/crates/ruff/src/settings/configuration.rs +++ b/crates/ruff/src/settings/configuration.rs @@ -16,10 +16,11 @@ use crate::fs; use crate::line_width::{LineLength, TabSize}; use crate::rule_selector::RuleSelector; use crate::rules::{ - flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, - 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, + copyright, flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, + flake8_comprehensions, 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, }; use crate::settings::options::Options; use crate::settings::types::{ @@ -74,6 +75,7 @@ pub struct Configuration { pub flake8_bugbear: Option, pub flake8_builtins: Option, pub flake8_comprehensions: Option, + pub copyright: Option, pub flake8_errmsg: Option, pub flake8_implicit_str_concat: Option, pub flake8_import_conventions: Option, @@ -227,6 +229,7 @@ impl Configuration { flake8_bugbear: options.flake8_bugbear, flake8_builtins: options.flake8_builtins, flake8_comprehensions: options.flake8_comprehensions, + copyright: options.copyright, flake8_errmsg: options.flake8_errmsg, flake8_gettext: options.flake8_gettext, flake8_implicit_str_concat: options.flake8_implicit_str_concat, @@ -305,6 +308,7 @@ impl Configuration { flake8_comprehensions: self .flake8_comprehensions .combine(config.flake8_comprehensions), + copyright: self.copyright.combine(config.copyright), flake8_errmsg: self.flake8_errmsg.combine(config.flake8_errmsg), flake8_gettext: self.flake8_gettext.combine(config.flake8_gettext), flake8_implicit_str_concat: self diff --git a/crates/ruff/src/settings/defaults.rs b/crates/ruff/src/settings/defaults.rs index aa5007789d..129c3a16f2 100644 --- a/crates/ruff/src/settings/defaults.rs +++ b/crates/ruff/src/settings/defaults.rs @@ -10,10 +10,11 @@ use crate::line_width::{LineLength, TabSize}; use crate::registry::Linter; use crate::rule_selector::{prefix_to_selector, RuleSelector}; use crate::rules::{ - flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, - 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, + copyright, flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, + flake8_comprehensions, 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, }; use crate::settings::types::FilePatternSet; @@ -95,6 +96,7 @@ impl Default for Settings { flake8_bugbear: flake8_bugbear::settings::Settings::default(), flake8_builtins: flake8_builtins::settings::Settings::default(), flake8_comprehensions: flake8_comprehensions::settings::Settings::default(), + copyright: copyright::settings::Settings::default(), flake8_errmsg: flake8_errmsg::settings::Settings::default(), flake8_implicit_str_concat: flake8_implicit_str_concat::settings::Settings::default(), flake8_import_conventions: flake8_import_conventions::settings::Settings::default(), diff --git a/crates/ruff/src/settings/mod.rs b/crates/ruff/src/settings/mod.rs index f2e2aec2db..c3878d7eda 100644 --- a/crates/ruff/src/settings/mod.rs +++ b/crates/ruff/src/settings/mod.rs @@ -16,10 +16,11 @@ use ruff_macros::CacheKey; use crate::registry::{Rule, RuleNamespace, RuleSet, INCOMPATIBLE_CODES}; use crate::rule_selector::{RuleSelector, Specificity}; use crate::rules::{ - flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, - 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, + copyright, flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, + flake8_comprehensions, 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, }; use crate::settings::configuration::Configuration; use crate::settings::types::{FilePatternSet, PerFileIgnore, PythonVersion, SerializationFormat}; @@ -111,6 +112,7 @@ pub struct Settings { pub flake8_bugbear: flake8_bugbear::settings::Settings, pub flake8_builtins: flake8_builtins::settings::Settings, pub flake8_comprehensions: flake8_comprehensions::settings::Settings, + pub copyright: copyright::settings::Settings, pub flake8_errmsg: flake8_errmsg::settings::Settings, pub flake8_implicit_str_concat: flake8_implicit_str_concat::settings::Settings, pub flake8_import_conventions: flake8_import_conventions::settings::Settings, @@ -199,6 +201,7 @@ impl Settings { .flake8_comprehensions .map(Into::into) .unwrap_or_default(), + copyright: config.copyright.map(Into::into).unwrap_or_default(), flake8_errmsg: config.flake8_errmsg.map(Into::into).unwrap_or_default(), flake8_implicit_str_concat: config .flake8_implicit_str_concat diff --git a/crates/ruff/src/settings/options.rs b/crates/ruff/src/settings/options.rs index 0a7462cc8f..7c99dd8a2c 100644 --- a/crates/ruff/src/settings/options.rs +++ b/crates/ruff/src/settings/options.rs @@ -8,10 +8,11 @@ use ruff_macros::ConfigurationOptions; use crate::line_width::{LineLength, TabSize}; use crate::rule_selector::RuleSelector; use crate::rules::{ - flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, - 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, + copyright, flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, + flake8_comprehensions, 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, }; use crate::settings::types::{PythonVersion, SerializationFormat, Version}; @@ -495,6 +496,9 @@ pub struct Options { /// Options for the `flake8-comprehensions` plugin. pub flake8_comprehensions: Option, #[option_group] + /// Options for the `copyright` plugin. + pub copyright: Option, + #[option_group] /// Options for the `flake8-errmsg` plugin. pub flake8_errmsg: Option, #[option_group] diff --git a/crates/ruff_macros/src/rule_namespace.rs b/crates/ruff_macros/src/rule_namespace.rs index 67dd7c4070..f1bd7f5adf 100644 --- a/crates/ruff_macros/src/rule_namespace.rs +++ b/crates/ruff_macros/src/rule_namespace.rs @@ -15,9 +15,9 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result "Ruff-specific rules", Self::Numpy => "NumPy-specific rules", ); - let mut url_match_arms = quote!(Self::Ruff => None, Self::Numpy => None, ); + let mut name_match_arms = quote!(Self::Ruff => "Ruff-specific rules", Self::Numpy => "NumPy-specific rules", Self::Copyright => "Copyright-related rules", ); + let mut url_match_arms = + quote!(Self::Ruff => None, Self::Numpy => None, Self::Copyright => None, ); let mut all_prefixes = HashSet::new(); @@ -59,7 +59,7 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result #name,}); url_match_arms.extend(quote! {Self::#variant_ident => Some(#url),}); diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index 84566771dd..3b4a66db5e 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -9,10 +9,11 @@ use ruff::line_width::{LineLength, TabSize}; use ruff::linter::{check_path, LinterResult}; use ruff::registry::AsRule; use ruff::rules::{ - flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, - 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, + copyright, flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, + flake8_comprehensions, 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, }; use ruff::settings::configuration::Configuration; use ruff::settings::options::Options; @@ -137,6 +138,7 @@ pub fn defaultSettings() -> Result { flake8_bugbear: Some(flake8_bugbear::settings::Settings::default().into()), flake8_builtins: Some(flake8_builtins::settings::Settings::default().into()), flake8_comprehensions: Some(flake8_comprehensions::settings::Settings::default().into()), + copyright: Some(copyright::settings::Settings::default().into()), flake8_errmsg: Some(flake8_errmsg::settings::Settings::default().into()), flake8_pytest_style: Some(flake8_pytest_style::settings::Settings::default().into()), flake8_quotes: Some(flake8_quotes::settings::Settings::default().into()), diff --git a/docs/faq.md b/docs/faq.md index 3ee9baca50..07167d2829 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -41,6 +41,7 @@ natively, including: - [flake8-builtins](https://pypi.org/project/flake8-builtins/) - [flake8-commas](https://pypi.org/project/flake8-commas/) - [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/) +- [flake8-copyright](https://pypi.org/project/flake8-copyright/) - [flake8-datetimez](https://pypi.org/project/flake8-datetimez/) - [flake8-debugger](https://pypi.org/project/flake8-debugger/) - [flake8-django](https://pypi.org/project/flake8-django/) @@ -143,6 +144,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl - [flake8-builtins](https://pypi.org/project/flake8-builtins/) - [flake8-commas](https://pypi.org/project/flake8-commas/) - [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/) +- [flake8-copyright](https://pypi.org/project/flake8-comprehensions/) - [flake8-datetimez](https://pypi.org/project/flake8-datetimez/) - [flake8-debugger](https://pypi.org/project/flake8-debugger/) - [flake8-django](https://pypi.org/project/flake8-django/) diff --git a/ruff.schema.json b/ruff.schema.json index 69702b9f17..e23fad53f2 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -32,6 +32,17 @@ "null" ] }, + "copyright": { + "description": "Options for the `copyright` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/CopyrightOptions" + }, + { + "type": "null" + } + ] + }, "dummy-variable-rgx": { "description": "A regular expression used to identify \"dummy\" variables, or those which should be ignored when enforcing (e.g.) unused-variable rules. The default expression matches `_`, `__`, and `_var`, but not `_var_`.", "type": [ @@ -620,6 +631,35 @@ } ] }, + "CopyrightOptions": { + "type": "object", + "properties": { + "author": { + "description": "Author to enforce within the copyright notice. If provided, the author must be present immediately following the copyright notice.", + "type": [ + "string", + "null" + ] + }, + "min-file-size": { + "description": "A minimum file size (in bytes) required for a copyright notice to be enforced. By default, all files are validated.", + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 0.0 + }, + "notice-rgx": { + "description": "The regular expression used to match the copyright notice, compiled with the [`regex`](https://docs.rs/regex/latest/regex/) crate.\n\nDefaults to `(?i)Copyright\\s+(\\(C\\)\\s+)?\\d{4}(-\\d{4})*`, which matches the following: - `Copyright 2023` - `Copyright (C) 2023` - `Copyright 2021-2023` - `Copyright (C) 2021-2023`", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, "Flake8AnnotationsOptions": { "type": "object", "properties": { @@ -1679,6 +1719,10 @@ "COM812", "COM818", "COM819", + "CPY", + "CPY0", + "CPY00", + "CPY001", "D", "D1", "D10", diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml index c34ba92a24..ca7d45e508 100644 --- a/scripts/pyproject.toml +++ b/scripts/pyproject.toml @@ -15,6 +15,7 @@ ignore = [ "D", # pydocstyle "PL", # pylint "S", # bandit + "CPY", # copyright "G", # flake8-logging "T", # flake8-print "FBT", # flake8-boolean-trap From 68b6d30c46d60941ac14bd778a58635b72257ecf Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 11 Jun 2023 20:02:40 -0400 Subject: [PATCH 003/447] Use consistent `Cargo.toml` metadata in all crates (#5015) --- Cargo.toml | 1 + crates/flake8_to_ruff/Cargo.toml | 8 ++++++++ crates/ruff/Cargo.toml | 15 ++++++++------- crates/ruff_benchmark/Cargo.toml | 14 ++++++++------ crates/ruff_cache/Cargo.toml | 5 +++++ crates/ruff_cli/Cargo.toml | 11 ++++++----- crates/ruff_dev/Cargo.toml | 5 +++++ crates/ruff_diagnostics/Cargo.toml | 5 +++++ crates/ruff_formatter/Cargo.toml | 5 +++++ crates/ruff_index/Cargo.toml | 5 +++++ crates/ruff_macros/Cargo.toml | 5 +++++ crates/ruff_python_ast/Cargo.toml | 5 +++++ crates/ruff_python_formatter/Cargo.toml | 5 +++++ crates/ruff_python_semantic/Cargo.toml | 5 +++++ crates/ruff_python_stdlib/Cargo.toml | 5 +++++ crates/ruff_python_whitespace/Cargo.toml | 5 +++++ crates/ruff_rustpython/Cargo.toml | 5 +++++ crates/ruff_testing_macros/Cargo.toml | 8 +++++++- crates/ruff_textwrap/Cargo.toml | 5 +++++ crates/ruff_wasm/Cargo.toml | 5 +++++ 20 files changed, 108 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fb9e209e36..29941786c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ homepage = "https://beta.ruff.rs/docs/" documentation = "https://beta.ruff.rs/docs/" repository = "https://github.com/astral-sh/ruff" authors = ["Charlie Marsh "] +license = "MIT" [workspace.dependencies] anyhow = { version = "1.0.69" } diff --git a/crates/flake8_to_ruff/Cargo.toml b/crates/flake8_to_ruff/Cargo.toml index 05109e7495..7cfcfd33a0 100644 --- a/crates/flake8_to_ruff/Cargo.toml +++ b/crates/flake8_to_ruff/Cargo.toml @@ -1,8 +1,16 @@ [package] name = "flake8-to-ruff" version = "0.0.272" +description = """ +Convert Flake8 configuration files to Ruff configuration files. +""" +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [dependencies] ruff = { path = "../ruff", default-features = false } diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index f32364904b..53a1ea9376 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,14 +1,15 @@ [package] name = "ruff" version = "0.0.272" -authors.workspace = true -edition.workspace = true -rust-version.workspace = true -documentation.workspace = true -homepage.workspace = true -repository.workspace = true +publish = false +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } readme = "README.md" -license = "MIT" [lib] name = "ruff" diff --git a/crates/ruff_benchmark/Cargo.toml b/crates/ruff_benchmark/Cargo.toml index f3d63336e0..56617c41d6 100644 --- a/crates/ruff_benchmark/Cargo.toml +++ b/crates/ruff_benchmark/Cargo.toml @@ -1,13 +1,15 @@ [package] name = "ruff_benchmark" version = "0.0.0" -publish = false -edition.workspace = true -authors.workspace = true -homepage.workspace = true -documentation.workspace = true -repository.workspace = true description = "Ruff Micro-benchmarks" +publish = false +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [lib] bench = false diff --git a/crates/ruff_cache/Cargo.toml b/crates/ruff_cache/Cargo.toml index b38e4cba04..89445ed6c8 100644 --- a/crates/ruff_cache/Cargo.toml +++ b/crates/ruff_cache/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_cache" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [dependencies] itertools = { workspace = true } diff --git a/crates/ruff_cli/Cargo.toml b/crates/ruff_cli/Cargo.toml index b45d8557e4..0cdd5b765f 100644 --- a/crates/ruff_cli/Cargo.toml +++ b/crates/ruff_cli/Cargo.toml @@ -1,14 +1,15 @@ [package] name = "ruff_cli" version = "0.0.272" -authors = ["Charlie Marsh "] +publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } -documentation = "https://github.com/astral-sh/ruff" -homepage = "https://github.com/astral-sh/ruff" -repository = "https://github.com/astral-sh/ruff" +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } readme = "../../README.md" -license = "MIT" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/ruff_dev/Cargo.toml b/crates/ruff_dev/Cargo.toml index 4fa0405c9a..376bb09b8e 100644 --- a/crates/ruff_dev/Cargo.toml +++ b/crates/ruff_dev/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_dev" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [dependencies] ruff = { path = "../ruff", features = ["schemars"] } diff --git a/crates/ruff_diagnostics/Cargo.toml b/crates/ruff_diagnostics/Cargo.toml index 980abbbc51..9921f41124 100644 --- a/crates/ruff_diagnostics/Cargo.toml +++ b/crates/ruff_diagnostics/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_diagnostics" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [lib] diff --git a/crates/ruff_formatter/Cargo.toml b/crates/ruff_formatter/Cargo.toml index 593b47e359..17d2a6b549 100644 --- a/crates/ruff_formatter/Cargo.toml +++ b/crates/ruff_formatter/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_formatter" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [dependencies] ruff_text_size = { workspace = true } diff --git a/crates/ruff_index/Cargo.toml b/crates/ruff_index/Cargo.toml index 77e1c3fb56..29e55d60b4 100644 --- a/crates/ruff_index/Cargo.toml +++ b/crates/ruff_index/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_index" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [lib] diff --git a/crates/ruff_macros/Cargo.toml b/crates/ruff_macros/Cargo.toml index f774aee299..6b48c144cd 100644 --- a/crates/ruff_macros/Cargo.toml +++ b/crates/ruff_macros/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_macros" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [lib] proc-macro = true diff --git a/crates/ruff_python_ast/Cargo.toml b/crates/ruff_python_ast/Cargo.toml index 14726020cd..855655b8ee 100644 --- a/crates/ruff_python_ast/Cargo.toml +++ b/crates/ruff_python_ast/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_python_ast" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [lib] diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index bd5526bc43..708b77c8f9 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_python_formatter" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [dependencies] ruff_formatter = { path = "../ruff_formatter" } diff --git a/crates/ruff_python_semantic/Cargo.toml b/crates/ruff_python_semantic/Cargo.toml index 3716d2c103..93c47dae9d 100644 --- a/crates/ruff_python_semantic/Cargo.toml +++ b/crates/ruff_python_semantic/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_python_semantic" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [lib] diff --git a/crates/ruff_python_stdlib/Cargo.toml b/crates/ruff_python_stdlib/Cargo.toml index c698a0adf5..91e43d5377 100644 --- a/crates/ruff_python_stdlib/Cargo.toml +++ b/crates/ruff_python_stdlib/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_python_stdlib" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [lib] diff --git a/crates/ruff_python_whitespace/Cargo.toml b/crates/ruff_python_whitespace/Cargo.toml index 584418405b..cbfc1aea24 100644 --- a/crates/ruff_python_whitespace/Cargo.toml +++ b/crates/ruff_python_whitespace/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_python_whitespace" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [lib] diff --git a/crates/ruff_rustpython/Cargo.toml b/crates/ruff_rustpython/Cargo.toml index e66ba4caa6..e6c0bc0005 100644 --- a/crates/ruff_rustpython/Cargo.toml +++ b/crates/ruff_rustpython/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_rustpython" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [lib] diff --git a/crates/ruff_testing_macros/Cargo.toml b/crates/ruff_testing_macros/Cargo.toml index 215168923c..49daf16a71 100644 --- a/crates/ruff_testing_macros/Cargo.toml +++ b/crates/ruff_testing_macros/Cargo.toml @@ -1,8 +1,14 @@ [package] name = "ruff_testing_macros" -edition = "2021" version = "0.0.0" publish = false +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [lib] proc-macro = true diff --git a/crates/ruff_textwrap/Cargo.toml b/crates/ruff_textwrap/Cargo.toml index 864a259d33..9b721596c6 100644 --- a/crates/ruff_textwrap/Cargo.toml +++ b/crates/ruff_textwrap/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_textwrap" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [dependencies] ruff_python_whitespace = { path = "../ruff_python_whitespace" } diff --git a/crates/ruff_wasm/Cargo.toml b/crates/ruff_wasm/Cargo.toml index 7ad96d646b..5383edfa78 100644 --- a/crates/ruff_wasm/Cargo.toml +++ b/crates/ruff_wasm/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_wasm" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } description = "WebAssembly bindings for Ruff" [lib] From 31067e6ce2bc1b113477f9ad7b578aecdcb63da5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 11 Jun 2023 20:10:37 -0400 Subject: [PATCH 004/447] Update list of crates in `CONTRIBUTING.md` (#5016) --- CONTRIBUTING.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eaeb300f85..0d304b5b72 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,13 +93,27 @@ The vast majority of the code, including all lint rules, lives in the `ruff` cra At time of writing, the repository includes the following crates: - `crates/ruff`: library crate containing all lint rules and the core logic for running them. +- `crates/ruff_benchmark`: binary crate for running micro-benchmarks. +- `crates/ruff_cache`: library crate for caching lint results. - `crates/ruff_cli`: binary crate containing Ruff's command-line interface. - `crates/ruff_dev`: binary crate containing utilities used in the development of Ruff itself (e.g., `cargo dev generate-all`). +- `crates/ruff_diagnostics`: library crate for the lint diagnostics APIs. +- `crates/ruff_formatter`: library crate for generic code formatting logic based on an intermediate + representation. +- `crates/ruff_index`: library crate inspired by `rustc_index`. - `crates/ruff_macros`: library crate containing macros used by Ruff. -- `crates/ruff_python`: library crate implementing Python-specific functionality (e.g., lists of - standard library modules by version). -- `crates/flake8_to_ruff`: binary crate for generating Ruff configuration from Flake8 configuration. +- `crates/ruff_python_ast`: library crate containing Python-specific AST types and utilities. +- `crates/ruff_python_formatter`: library crate containing Python-specific code formatting logic. +- `crates/ruff_python_semantic`: library crate containing Python-specific semantic analysis logic, + including Ruff's semantic model. +- `crates/ruff_python_stdlib`: library crate containing Python-specific standard library data. +- `crates/ruff_python_whitespace`: library crate containing Python-specific whitespace analysis + logic. +- `crates/ruff_rustpython`: library crate containing `RustPython`-specific utilities. +- `crates/ruff_testing_macros`: library crate containing macros used for testing Ruff. +- `crates/ruff_textwrap`: library crate to indent and dedent Python source code. +- `crates/ruff_wasm`: library crate for exposing Ruff as a WebAssembly module. ### Example: Adding a new lint rule From eac3a0cc3daedea71b905caec2d4a12ae996879b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 11 Jun 2023 20:20:59 -0400 Subject: [PATCH 005/447] Update CONTRIBUTING.md guide (#5017) --- CONTRIBUTING.md | 63 +++++++++++-------- crates/ruff/src/checkers/ast/mod.rs | 4 +- crates/ruff/src/codes.rs | 2 +- crates/ruff/src/rules/flake8_bugbear/mod.rs | 2 +- .../src/rules/flake8_bugbear/rules/mod.rs | 4 +- ...nnot_raise_literal.rs => raise_literal.rs} | 17 +++-- 6 files changed, 51 insertions(+), 41 deletions(-) rename crates/ruff/src/rules/flake8_bugbear/rules/{cannot_raise_literal.rs => raise_literal.rs} (54%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0d304b5b72..8ad067e335 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,9 +21,9 @@ Ruff welcomes contributions in the form of Pull Requests. For small changes (e.g., bug fixes), feel free to submit a PR. For larger changes (e.g., new lint rules, new functionality, new configuration options), consider -creating an [**issue**](https://github.com/astral-sh/ruff/issues) outlining your proposed -change. You can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5) to discuss your idea with -the community. +creating an [**issue**](https://github.com/astral-sh/ruff/issues) outlining your proposed change. +You can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5) to discuss your idea with the +community. If you're looking for a place to start, we recommend implementing a new lint rule (see: [_Adding a new lint rule_](#example-adding-a-new-lint-rule), which will allow you to learn from and @@ -31,8 +31,8 @@ pattern-match against the examples in the existing codebase. Many lint rules are existing Python plugins, which can be used as a reference implementation. As a concrete example: consider taking on one of the rules from the [`flake8-pyi`](https://github.com/astral-sh/ruff/issues/848) -plugin, and looking to the originating [Python source](https://github.com/PyCQA/flake8-pyi) -for guidance. +plugin, and looking to the originating [Python source](https://github.com/PyCQA/flake8-pyi) for +guidance. ### Prerequisites @@ -58,8 +58,8 @@ and that it passes both the lint and test validation checks: ```shell cargo fmt # Auto-formatting... -cargo clippy --fix --workspace --all-targets --all-features # Linting... cargo test # Testing... +cargo clippy --workspace --all-targets --all-features -- -D warnings # Linting... ``` These checks will run on GitHub Actions when you open your Pull Request, but running them locally @@ -119,52 +119,63 @@ At time of writing, the repository includes the following crates: At a high level, the steps involved in adding a new lint rule are as follows: -1. Determine a name for the new rule as per our [rule naming convention](#rule-naming-convention). +1. Determine a name for the new rule as per our [rule naming convention](#rule-naming-convention) + (e.g., `AssertFalse`, as in, "allow `assert False`"). -1. Create a file for your rule (e.g., `crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs`). +1. Create a file for your rule (e.g., `crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs`). -1. In that file, define a violation struct. You can grep for `#[violation]` to see examples. +1. In that file, define a violation struct (e.g., `pub struct AssertFalse`). You can grep for + `#[violation]` to see examples. -1. Map the violation struct to a rule code in `crates/ruff/src/codes.rs` (e.g., `E402`). +1. In that file, define a function that adds the violation to the diagnostic list as appropriate + (e.g., `pub(crate) fn assert_false`) based on whatever inputs are required for the rule (e.g., + an `ast::StmtAssert` node). 1. Define the logic for triggering the violation in `crates/ruff/src/checkers/ast/mod.rs` (for AST-based checks), `crates/ruff/src/checkers/tokens.rs` (for token-based checks), `crates/ruff/src/checkers/lines.rs` (for text-based checks), or `crates/ruff/src/checkers/filesystem.rs` (for filesystem-based checks). +1. Map the violation struct to a rule code in `crates/ruff/src/codes.rs` (e.g., `B011`). + 1. Add proper [testing](#rule-testing-fixtures-and-snapshots) for your rule. 1. Update the generated files (documentation and generated code). -To define the violation, start by creating a dedicated file for your rule under the appropriate -rule linter (e.g., `crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs`). That file should -contain a struct defined via `#[violation]`, along with a function that creates the violation -based on any required inputs. - -To trigger the violation, you'll likely want to augment the logic in `crates/ruff/src/checkers/ast.rs`, -which defines the Python AST visitor, responsible for iterating over the abstract syntax tree and -collecting diagnostics as it goes. +To trigger the violation, you'll likely want to augment the logic in `crates/ruff/src/checkers/ast.rs` +to call your new function at the appropriate time and with the appropriate inputs. The `Checker` +defined therein is a Python AST visitor, which iterates over the AST, building up a semantic model, +and calling out to lint rule analyzer functions as it goes. If you need to inspect the AST, you can run `cargo dev print-ast` with a Python file. Grep -for the `Check::new` invocations to understand how other, similar rules are implemented. +for the `Diagnostic::new` invocations to understand how other, similar rules are implemented. Once you're satisfied with your code, add tests for your rule. See [rule testing](#rule-testing-fixtures-and-snapshots) for more details. -Finally, regenerate the documentation and generated code with `cargo dev generate-all`. +Finally, regenerate the documentation and other generated assets (like our JSON Schema) with: +`cargo dev generate-all`. #### Rule naming convention -The rule name should make sense when read as "allow _rule-name_" or "allow _rule-name_ items". +Like Clippy, Ruff's rule names should make grammatical and logical sense when read as "allow +${rule}" or "allow ${rule} items", as in the context of suppression comments. -This implies that rule names: +For example, `AssertFalse` fits this convention: it flags `assert False` statements, and so a +suppression comment would be framed as "allow `assert False`". -- should state the bad thing being checked for +As such, rule names should... -- should not contain instructions on what you should use instead - (these belong in the rule documentation and the `autofix_title` for rules that have autofix) +- Highlight the pattern that is being linted against, rather than the preferred alternative. + For example, `AssertFalse` guards against `assert False` statements. -When re-implementing rules from other linters, this convention is given more importance than +- _Not_ contain instructions on how to fix the violation, which instead belong in the rule + documentation and the `autofix_title`. + +- _Not_ contain a redundant prefix, like `Disallow` or `Banned`, which are already implied by the + convention. + +When re-implementing rules from other linters, we prioritize adhering to this convention over preserving the original rule name. #### Rule testing: fixtures and snapshots diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 481bddf0ce..6334ca31ef 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1356,9 +1356,9 @@ where pyflakes::rules::raise_not_implemented(self, expr); } } - if self.enabled(Rule::CannotRaiseLiteral) { + if self.enabled(Rule::RaiseLiteral) { if let Some(exc) = exc { - flake8_bugbear::rules::cannot_raise_literal(self, exc); + flake8_bugbear::rules::raise_literal(self, exc); } } if self.any_enabled(&[ diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 62dfc0ed77..911fe85ebc 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -232,7 +232,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Bugbear, "013") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::RedundantTupleInExceptionHandler), (Flake8Bugbear, "014") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::DuplicateHandlerException), (Flake8Bugbear, "015") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::UselessComparison), - (Flake8Bugbear, "016") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::CannotRaiseLiteral), + (Flake8Bugbear, "016") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::RaiseLiteral), (Flake8Bugbear, "017") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::AssertRaisesException), (Flake8Bugbear, "018") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::UselessExpression), (Flake8Bugbear, "019") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::CachedInstanceMethod), diff --git a/crates/ruff/src/rules/flake8_bugbear/mod.rs b/crates/ruff/src/rules/flake8_bugbear/mod.rs index 2bf5676306..7e4864a60d 100644 --- a/crates/ruff/src/rules/flake8_bugbear/mod.rs +++ b/crates/ruff/src/rules/flake8_bugbear/mod.rs @@ -19,7 +19,7 @@ mod tests { #[test_case(Rule::AssertRaisesException, Path::new("B017.py"))] #[test_case(Rule::AssignmentToOsEnviron, Path::new("B003.py"))] #[test_case(Rule::CachedInstanceMethod, Path::new("B019.py"))] - #[test_case(Rule::CannotRaiseLiteral, Path::new("B016.py"))] + #[test_case(Rule::RaiseLiteral, Path::new("B016.py"))] #[test_case(Rule::DuplicateHandlerException, Path::new("B014.py"))] #[test_case(Rule::DuplicateTryBlockException, Path::new("B025.py"))] #[test_case(Rule::DuplicateValue, Path::new("B033.py"))] diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs b/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs index f350160a57..f1622fdf4f 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs @@ -6,7 +6,6 @@ pub(crate) use assert_false::{assert_false, AssertFalse}; pub(crate) use assert_raises_exception::{assert_raises_exception, AssertRaisesException}; pub(crate) use assignment_to_os_environ::{assignment_to_os_environ, AssignmentToOsEnviron}; pub(crate) use cached_instance_method::{cached_instance_method, CachedInstanceMethod}; -pub(crate) use cannot_raise_literal::{cannot_raise_literal, CannotRaiseLiteral}; pub(crate) use duplicate_exceptions::{ duplicate_exceptions, DuplicateHandlerException, DuplicateTryBlockException, }; @@ -29,6 +28,7 @@ pub(crate) use loop_variable_overrides_iterator::{ }; pub(crate) use mutable_argument_default::{mutable_argument_default, MutableArgumentDefault}; pub(crate) use no_explicit_stacklevel::{no_explicit_stacklevel, NoExplicitStacklevel}; +pub(crate) use raise_literal::{raise_literal, RaiseLiteral}; pub(crate) use raise_without_from_inside_except::{ raise_without_from_inside_except, RaiseWithoutFromInsideExcept, }; @@ -65,7 +65,6 @@ mod assert_false; mod assert_raises_exception; mod assignment_to_os_environ; mod cached_instance_method; -mod cannot_raise_literal; mod duplicate_exceptions; mod duplicate_value; mod except_with_empty_tuple; @@ -78,6 +77,7 @@ mod jump_statement_in_finally; mod loop_variable_overrides_iterator; mod mutable_argument_default; mod no_explicit_stacklevel; +mod raise_literal; mod raise_without_from_inside_except; mod redundant_tuple_in_exception_handler; mod reuse_of_groupby_generator; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/cannot_raise_literal.rs b/crates/ruff/src/rules/flake8_bugbear/rules/raise_literal.rs similarity index 54% rename from crates/ruff/src/rules/flake8_bugbear/rules/cannot_raise_literal.rs rename to crates/ruff/src/rules/flake8_bugbear/rules/raise_literal.rs index 6b2df20d5b..202d4c1c0c 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/cannot_raise_literal.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/raise_literal.rs @@ -6,9 +6,9 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; #[violation] -pub struct CannotRaiseLiteral; +pub struct RaiseLiteral; -impl Violation for CannotRaiseLiteral { +impl Violation for RaiseLiteral { #[derive_message_formats] fn message(&self) -> String { format!("Cannot raise a literal. Did you intend to return it or raise an Exception?") @@ -16,11 +16,10 @@ impl Violation for CannotRaiseLiteral { } /// B016 -pub(crate) fn cannot_raise_literal(checker: &mut Checker, expr: &Expr) { - let Expr::Constant ( _) = expr else { - return; - }; - checker - .diagnostics - .push(Diagnostic::new(CannotRaiseLiteral, expr.range())); +pub(crate) fn raise_literal(checker: &mut Checker, expr: &Expr) { + if expr.is_constant_expr() { + checker + .diagnostics + .push(Diagnostic::new(RaiseLiteral, expr.range())); + } } From c3d1fa851e3aa9a5c778b98e490043642d72a758 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Mon, 12 Jun 2023 05:51:24 +0530 Subject: [PATCH 006/447] Ignore pyproject.toml for adding noqa directives (#5013) ## Summary Ignore pyproject.toml file for adding noqa directives using `--add-noqa` ## Test Plan `cargo run --bin ruff -- check --add-noqa .` fixes: #5012 --- crates/ruff_cli/src/commands/add_noqa.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/ruff_cli/src/commands/add_noqa.rs b/crates/ruff_cli/src/commands/add_noqa.rs index 54d6417524..0ee7027f97 100644 --- a/crates/ruff_cli/src/commands/add_noqa.rs +++ b/crates/ruff_cli/src/commands/add_noqa.rs @@ -9,6 +9,7 @@ use rayon::prelude::*; use ruff::linter::add_noqa_to_path; use ruff::resolver::PyprojectConfig; use ruff::{packaging, resolver, warn_user_once}; +use ruff_python_stdlib::path::is_project_toml; use crate::args::Overrides; @@ -46,6 +47,9 @@ pub(crate) fn add_noqa( .flatten() .filter_map(|entry| { let path = entry.path(); + if is_project_toml(path) { + return None; + } let package = path .parent() .and_then(|parent| package_roots.get(parent)) From 6a5f3173629cca300782e3c305588ca670068d92 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 11 Jun 2023 20:32:45 -0400 Subject: [PATCH 007/447] Use `use::*` for rule re-exports (#5018) --- crates/ruff/src/rules/airflow/rules/mod.rs | 4 +- crates/ruff/src/rules/copyright/rules/mod.rs | 2 +- crates/ruff/src/rules/eradicate/rules/mod.rs | 2 +- .../ruff/src/rules/flake8_2020/rules/mod.rs | 11 +-- .../src/rules/flake8_annotations/rules/mod.rs | 7 +- .../ruff/src/rules/flake8_async/rules/mod.rs | 8 +- .../ruff/src/rules/flake8_bandit/rules/mod.rs | 69 +++++-------- .../rules/flake8_blind_except/rules/mod.rs | 2 +- .../rules/flake8_boolean_trap/rules/mod.rs | 12 +-- .../src/rules/flake8_bugbear/rules/mod.rs | 93 ++++++------------ .../src/rules/flake8_builtins/rules/mod.rs | 8 +- .../ruff/src/rules/flake8_commas/rules/mod.rs | 4 +- .../rules/flake8_comprehensions/rules/mod.rs | 58 ++++------- .../src/rules/flake8_datetimez/rules/mod.rs | 28 ++---- .../src/rules/flake8_debugger/rules/mod.rs | 2 +- .../ruff/src/rules/flake8_django/rules/mod.rs | 22 ++--- .../ruff/src/rules/flake8_errmsg/rules/mod.rs | 4 +- .../src/rules/flake8_executable/rules/mod.rs | 10 +- .../ruff/src/rules/flake8_fixme/rules/mod.rs | 4 +- .../flake8_future_annotations/rules/mod.rs | 8 +- .../src/rules/flake8_gettext/rules/mod.rs | 12 +-- .../flake8_implicit_str_concat/rules/mod.rs | 6 +- .../flake8_import_conventions/rules/mod.rs | 6 +- .../src/rules/flake8_no_pep420/rules/mod.rs | 2 +- crates/ruff/src/rules/flake8_pie/rules/mod.rs | 16 ++- .../ruff/src/rules/flake8_print/rules/mod.rs | 2 +- crates/ruff/src/rules/flake8_pyi/rules/mod.rs | 67 +++++-------- .../rules/flake8_pytest_style/rules/mod.rs | 34 ++----- .../ruff/src/rules/flake8_quotes/rules/mod.rs | 5 +- .../ruff/src/rules/flake8_raise/rules/mod.rs | 4 +- .../ruff/src/rules/flake8_return/rules/mod.rs | 5 +- .../ruff/src/rules/flake8_self/rules/mod.rs | 2 +- .../src/rules/flake8_simplify/rules/mod.rs | 45 +++------ .../ruff/src/rules/flake8_slots/rules/mod.rs | 8 +- .../rules/flake8_tidy_imports/rules/mod.rs | 6 +- .../ruff/src/rules/flake8_todos/rules/mod.rs | 5 +- .../rules/flake8_type_checking/rules/mod.rs | 11 +-- .../flake8_unused_arguments/rules/mod.rs | 5 +- crates/ruff/src/rules/flynt/rules/mod.rs | 2 +- crates/ruff/src/rules/isort/rules/mod.rs | 4 +- crates/ruff/src/rules/mccabe/rules/mod.rs | 2 +- crates/ruff/src/rules/numpy/rules/mod.rs | 4 +- crates/ruff/src/rules/pandas_vet/rules/mod.rs | 15 ++- .../ruff/src/rules/pep8_naming/rules/mod.rs | 54 +++-------- .../pycodestyle/rules/logical_lines/mod.rs | 56 +++-------- .../ruff/src/rules/pycodestyle/rules/mod.rs | 46 ++++----- crates/ruff/src/rules/pydocstyle/rules/mod.rs | 58 ++++------- crates/ruff/src/rules/pyflakes/rules/mod.rs | 70 +++++-------- .../ruff/src/rules/pygrep_hooks/rules/mod.rs | 12 +-- crates/ruff/src/rules/pylint/rules/mod.rs | 97 ++++++++----------- crates/ruff/src/rules/pyupgrade/rules/mod.rs | 86 +++++++--------- crates/ruff/src/rules/ruff/rules/mod.rs | 28 ++---- .../ruff/src/rules/tryceratops/rules/mod.rs | 22 ++--- scripts/add_rule.py | 2 +- 54 files changed, 399 insertions(+), 758 deletions(-) diff --git a/crates/ruff/src/rules/airflow/rules/mod.rs b/crates/ruff/src/rules/airflow/rules/mod.rs index 0dbd1cf914..f9917250a8 100644 --- a/crates/ruff/src/rules/airflow/rules/mod.rs +++ b/crates/ruff/src/rules/airflow/rules/mod.rs @@ -1,3 +1,3 @@ -mod task_variable_name; +pub(crate) use task_variable_name::*; -pub(crate) use task_variable_name::{variable_name_task_id, AirflowVariableNameTaskIdMismatch}; +mod task_variable_name; diff --git a/crates/ruff/src/rules/copyright/rules/mod.rs b/crates/ruff/src/rules/copyright/rules/mod.rs index 860448155f..0ad5c3a421 100644 --- a/crates/ruff/src/rules/copyright/rules/mod.rs +++ b/crates/ruff/src/rules/copyright/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use missing_copyright_notice::{missing_copyright_notice, MissingCopyrightNotice}; +pub(crate) use missing_copyright_notice::*; mod missing_copyright_notice; diff --git a/crates/ruff/src/rules/eradicate/rules/mod.rs b/crates/ruff/src/rules/eradicate/rules/mod.rs index 8ec37813d9..b4c263a058 100644 --- a/crates/ruff/src/rules/eradicate/rules/mod.rs +++ b/crates/ruff/src/rules/eradicate/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use commented_out_code::{commented_out_code, CommentedOutCode}; +pub(crate) use commented_out_code::*; mod commented_out_code; diff --git a/crates/ruff/src/rules/flake8_2020/rules/mod.rs b/crates/ruff/src/rules/flake8_2020/rules/mod.rs index cb77bcc0dd..9f662e64e3 100644 --- a/crates/ruff/src/rules/flake8_2020/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_2020/rules/mod.rs @@ -1,11 +1,6 @@ -pub(crate) use compare::{ - compare, SysVersionCmpStr10, SysVersionCmpStr3, SysVersionInfo0Eq3, SysVersionInfo1CmpInt, - SysVersionInfoMinorCmpInt, -}; -pub(crate) use name_or_attribute::{name_or_attribute, SixPY3}; -pub(crate) use subscript::{ - subscript, SysVersion0, SysVersion2, SysVersionSlice1, SysVersionSlice3, -}; +pub(crate) use compare::*; +pub(crate) use name_or_attribute::*; +pub(crate) use subscript::*; mod compare; mod name_or_attribute; diff --git a/crates/ruff/src/rules/flake8_annotations/rules/mod.rs b/crates/ruff/src/rules/flake8_annotations/rules/mod.rs index b57c156b18..5b28734c91 100644 --- a/crates/ruff/src/rules/flake8_annotations/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_annotations/rules/mod.rs @@ -1,8 +1,3 @@ -pub(crate) use definition::{ - definition, AnyType, MissingReturnTypeClassMethod, MissingReturnTypePrivateFunction, - MissingReturnTypeSpecialMethod, MissingReturnTypeStaticMethod, - MissingReturnTypeUndocumentedPublicFunction, MissingTypeArgs, MissingTypeCls, - MissingTypeFunctionArgument, MissingTypeKwargs, MissingTypeSelf, -}; +pub(crate) use definition::*; mod definition; diff --git a/crates/ruff/src/rules/flake8_async/rules/mod.rs b/crates/ruff/src/rules/flake8_async/rules/mod.rs index 0f6e8faaca..d2c2472c5b 100644 --- a/crates/ruff/src/rules/flake8_async/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_async/rules/mod.rs @@ -1,8 +1,6 @@ -pub(crate) use blocking_http_call::{blocking_http_call, BlockingHttpCallInAsyncFunction}; -pub(crate) use blocking_os_call::{blocking_os_call, BlockingOsCallInAsyncFunction}; -pub(crate) use open_sleep_or_subprocess_call::{ - open_sleep_or_subprocess_call, OpenSleepOrSubprocessInAsyncFunction, -}; +pub(crate) use blocking_http_call::*; +pub(crate) use blocking_os_call::*; +pub(crate) use open_sleep_or_subprocess_call::*; mod blocking_http_call; mod blocking_os_call; diff --git a/crates/ruff/src/rules/flake8_bandit/rules/mod.rs b/crates/ruff/src/rules/flake8_bandit/rules/mod.rs index 90f1266e42..83cf002caf 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/mod.rs @@ -1,50 +1,25 @@ -pub(crate) use assert_used::{assert_used, Assert}; -pub(crate) use bad_file_permissions::{bad_file_permissions, BadFilePermissions}; -pub(crate) use exec_used::{exec_used, ExecBuiltin}; -pub(crate) use hardcoded_bind_all_interfaces::{ - hardcoded_bind_all_interfaces, HardcodedBindAllInterfaces, -}; -pub(crate) use hardcoded_password_default::{hardcoded_password_default, HardcodedPasswordDefault}; -pub(crate) use hardcoded_password_func_arg::{ - hardcoded_password_func_arg, HardcodedPasswordFuncArg, -}; -pub(crate) use hardcoded_password_string::{ - assign_hardcoded_password_string, compare_to_hardcoded_password_string, HardcodedPasswordString, -}; -pub(crate) use hardcoded_sql_expression::{hardcoded_sql_expression, HardcodedSQLExpression}; -pub(crate) use hardcoded_tmp_directory::{hardcoded_tmp_directory, HardcodedTempFile}; -pub(crate) use hashlib_insecure_hash_functions::{ - hashlib_insecure_hash_functions, HashlibInsecureHashFunction, -}; -pub(crate) use jinja2_autoescape_false::{jinja2_autoescape_false, Jinja2AutoescapeFalse}; -pub(crate) use logging_config_insecure_listen::{ - logging_config_insecure_listen, LoggingConfigInsecureListen, -}; -pub(crate) use paramiko_calls::{paramiko_call, ParamikoCall}; -pub(crate) use request_with_no_cert_validation::{ - request_with_no_cert_validation, RequestWithNoCertValidation, -}; -pub(crate) use request_without_timeout::{request_without_timeout, RequestWithoutTimeout}; -pub(crate) use shell_injection::{ - shell_injection, CallWithShellEqualsTrue, StartProcessWithAShell, StartProcessWithNoShell, - StartProcessWithPartialPath, SubprocessPopenWithShellEqualsTrue, - SubprocessWithoutShellEqualsTrue, UnixCommandWildcardInjection, -}; -pub(crate) use snmp_insecure_version::{snmp_insecure_version, SnmpInsecureVersion}; -pub(crate) use snmp_weak_cryptography::{snmp_weak_cryptography, SnmpWeakCryptography}; -pub(crate) use suspicious_function_call::{ - suspicious_function_call, SuspiciousEvalUsage, SuspiciousFTPLibUsage, - SuspiciousInsecureCipherModeUsage, SuspiciousInsecureCipherUsage, SuspiciousInsecureHashUsage, - SuspiciousMarkSafeUsage, SuspiciousMarshalUsage, SuspiciousMktempUsage, - SuspiciousNonCryptographicRandomUsage, SuspiciousPickleUsage, SuspiciousTelnetUsage, - SuspiciousURLOpenUsage, SuspiciousUnverifiedContextUsage, SuspiciousXMLCElementTreeUsage, - SuspiciousXMLETreeUsage, SuspiciousXMLElementTreeUsage, SuspiciousXMLExpatBuilderUsage, - SuspiciousXMLExpatReaderUsage, SuspiciousXMLMiniDOMUsage, SuspiciousXMLPullDOMUsage, - SuspiciousXMLSaxUsage, -}; -pub(crate) use try_except_continue::{try_except_continue, TryExceptContinue}; -pub(crate) use try_except_pass::{try_except_pass, TryExceptPass}; -pub(crate) use unsafe_yaml_load::{unsafe_yaml_load, UnsafeYAMLLoad}; +pub(crate) use assert_used::*; +pub(crate) use bad_file_permissions::*; +pub(crate) use exec_used::*; +pub(crate) use hardcoded_bind_all_interfaces::*; +pub(crate) use hardcoded_password_default::*; +pub(crate) use hardcoded_password_func_arg::*; +pub(crate) use hardcoded_password_string::*; +pub(crate) use hardcoded_sql_expression::*; +pub(crate) use hardcoded_tmp_directory::*; +pub(crate) use hashlib_insecure_hash_functions::*; +pub(crate) use jinja2_autoescape_false::*; +pub(crate) use logging_config_insecure_listen::*; +pub(crate) use paramiko_calls::*; +pub(crate) use request_with_no_cert_validation::*; +pub(crate) use request_without_timeout::*; +pub(crate) use shell_injection::*; +pub(crate) use snmp_insecure_version::*; +pub(crate) use snmp_weak_cryptography::*; +pub(crate) use suspicious_function_call::*; +pub(crate) use try_except_continue::*; +pub(crate) use try_except_pass::*; +pub(crate) use unsafe_yaml_load::*; mod assert_used; mod bad_file_permissions; diff --git a/crates/ruff/src/rules/flake8_blind_except/rules/mod.rs b/crates/ruff/src/rules/flake8_blind_except/rules/mod.rs index 520b3ece06..555850e95a 100644 --- a/crates/ruff/src/rules/flake8_blind_except/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_blind_except/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use blind_except::{blind_except, BlindExcept}; +pub(crate) use blind_except::*; mod blind_except; diff --git a/crates/ruff/src/rules/flake8_boolean_trap/rules/mod.rs b/crates/ruff/src/rules/flake8_boolean_trap/rules/mod.rs index a0e9b8bd66..b40aa58dc5 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_boolean_trap/rules/mod.rs @@ -1,12 +1,6 @@ -pub(crate) use check_boolean_default_value_in_function_definition::{ - check_boolean_default_value_in_function_definition, BooleanDefaultValueInFunctionDefinition, -}; -pub(crate) use check_boolean_positional_value_in_function_call::{ - check_boolean_positional_value_in_function_call, BooleanPositionalValueInFunctionCall, -}; -pub(crate) use check_positional_boolean_in_def::{ - check_positional_boolean_in_def, BooleanPositionalArgInFunctionDefinition, -}; +pub(crate) use check_boolean_default_value_in_function_definition::*; +pub(crate) use check_boolean_positional_value_in_function_call::*; +pub(crate) use check_positional_boolean_in_def::*; mod check_boolean_default_value_in_function_definition; mod check_boolean_positional_value_in_function_call; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs b/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs index f1622fdf4f..e9f707a0a1 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs @@ -1,64 +1,35 @@ -pub(crate) use abstract_base_class::{ - abstract_base_class, AbstractBaseClassWithoutAbstractMethod, - EmptyMethodWithoutAbstractDecorator, -}; -pub(crate) use assert_false::{assert_false, AssertFalse}; -pub(crate) use assert_raises_exception::{assert_raises_exception, AssertRaisesException}; -pub(crate) use assignment_to_os_environ::{assignment_to_os_environ, AssignmentToOsEnviron}; -pub(crate) use cached_instance_method::{cached_instance_method, CachedInstanceMethod}; -pub(crate) use duplicate_exceptions::{ - duplicate_exceptions, DuplicateHandlerException, DuplicateTryBlockException, -}; -pub(crate) use duplicate_value::{duplicate_value, DuplicateValue}; -pub(crate) use except_with_empty_tuple::{except_with_empty_tuple, ExceptWithEmptyTuple}; -pub(crate) use except_with_non_exception_classes::{ - except_with_non_exception_classes, ExceptWithNonExceptionClasses, -}; -pub(crate) use f_string_docstring::{f_string_docstring, FStringDocstring}; -pub(crate) use function_call_argument_default::{ - function_call_argument_default, FunctionCallInDefaultArgument, -}; -pub(crate) use function_uses_loop_variable::{ - function_uses_loop_variable, FunctionUsesLoopVariable, -}; -pub(crate) use getattr_with_constant::{getattr_with_constant, GetAttrWithConstant}; -pub(crate) use jump_statement_in_finally::{jump_statement_in_finally, JumpStatementInFinally}; -pub(crate) use loop_variable_overrides_iterator::{ - loop_variable_overrides_iterator, LoopVariableOverridesIterator, -}; -pub(crate) use mutable_argument_default::{mutable_argument_default, MutableArgumentDefault}; -pub(crate) use no_explicit_stacklevel::{no_explicit_stacklevel, NoExplicitStacklevel}; -pub(crate) use raise_literal::{raise_literal, RaiseLiteral}; -pub(crate) use raise_without_from_inside_except::{ - raise_without_from_inside_except, RaiseWithoutFromInsideExcept, -}; -pub(crate) use redundant_tuple_in_exception_handler::{ - redundant_tuple_in_exception_handler, RedundantTupleInExceptionHandler, -}; -pub(crate) use reuse_of_groupby_generator::{reuse_of_groupby_generator, ReuseOfGroupbyGenerator}; -pub(crate) use setattr_with_constant::{setattr_with_constant, SetAttrWithConstant}; -pub(crate) use star_arg_unpacking_after_keyword_arg::{ - star_arg_unpacking_after_keyword_arg, StarArgUnpackingAfterKeywordArg, -}; -pub(crate) use strip_with_multi_characters::{ - strip_with_multi_characters, StripWithMultiCharacters, -}; -pub(crate) use unary_prefix_increment::{unary_prefix_increment, UnaryPrefixIncrement}; -pub(crate) use unintentional_type_annotation::{ - unintentional_type_annotation, UnintentionalTypeAnnotation, -}; -pub(crate) use unreliable_callable_check::{unreliable_callable_check, UnreliableCallableCheck}; -pub(crate) use unused_loop_control_variable::{ - unused_loop_control_variable, UnusedLoopControlVariable, -}; -pub(crate) use useless_comparison::{useless_comparison, UselessComparison}; -pub(crate) use useless_contextlib_suppress::{ - useless_contextlib_suppress, UselessContextlibSuppress, -}; -pub(crate) use useless_expression::{useless_expression, UselessExpression}; -pub(crate) use zip_without_explicit_strict::{ - zip_without_explicit_strict, ZipWithoutExplicitStrict, -}; +pub(crate) use abstract_base_class::*; +pub(crate) use assert_false::*; +pub(crate) use assert_raises_exception::*; +pub(crate) use assignment_to_os_environ::*; +pub(crate) use cached_instance_method::*; +pub(crate) use duplicate_exceptions::*; +pub(crate) use duplicate_value::*; +pub(crate) use except_with_empty_tuple::*; +pub(crate) use except_with_non_exception_classes::*; +pub(crate) use f_string_docstring::*; +pub(crate) use function_call_argument_default::*; +pub(crate) use function_uses_loop_variable::*; +pub(crate) use getattr_with_constant::*; +pub(crate) use jump_statement_in_finally::*; +pub(crate) use loop_variable_overrides_iterator::*; +pub(crate) use mutable_argument_default::*; +pub(crate) use no_explicit_stacklevel::*; +pub(crate) use raise_literal::*; +pub(crate) use raise_without_from_inside_except::*; +pub(crate) use redundant_tuple_in_exception_handler::*; +pub(crate) use reuse_of_groupby_generator::*; +pub(crate) use setattr_with_constant::*; +pub(crate) use star_arg_unpacking_after_keyword_arg::*; +pub(crate) use strip_with_multi_characters::*; +pub(crate) use unary_prefix_increment::*; +pub(crate) use unintentional_type_annotation::*; +pub(crate) use unreliable_callable_check::*; +pub(crate) use unused_loop_control_variable::*; +pub(crate) use useless_comparison::*; +pub(crate) use useless_contextlib_suppress::*; +pub(crate) use useless_expression::*; +pub(crate) use zip_without_explicit_strict::*; mod abstract_base_class; mod assert_false; diff --git a/crates/ruff/src/rules/flake8_builtins/rules/mod.rs b/crates/ruff/src/rules/flake8_builtins/rules/mod.rs index f9b8c3c3d7..d81afec0d6 100644 --- a/crates/ruff/src/rules/flake8_builtins/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_builtins/rules/mod.rs @@ -1,8 +1,6 @@ -pub(crate) use builtin_argument_shadowing::{builtin_argument_shadowing, BuiltinArgumentShadowing}; -pub(crate) use builtin_attribute_shadowing::{ - builtin_attribute_shadowing, BuiltinAttributeShadowing, -}; -pub(crate) use builtin_variable_shadowing::{builtin_variable_shadowing, BuiltinVariableShadowing}; +pub(crate) use builtin_argument_shadowing::*; +pub(crate) use builtin_attribute_shadowing::*; +pub(crate) use builtin_variable_shadowing::*; mod builtin_argument_shadowing; mod builtin_attribute_shadowing; diff --git a/crates/ruff/src/rules/flake8_commas/rules/mod.rs b/crates/ruff/src/rules/flake8_commas/rules/mod.rs index 0286278d8c..5dbfe3cb51 100644 --- a/crates/ruff/src/rules/flake8_commas/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_commas/rules/mod.rs @@ -1,5 +1,3 @@ -pub(crate) use trailing_commas::{ - trailing_commas, MissingTrailingComma, ProhibitedTrailingComma, TrailingCommaOnBareTuple, -}; +pub(crate) use trailing_commas::*; mod trailing_commas; diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/mod.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/mod.rs index 0cb1aaacfe..2c24ecfc2f 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/mod.rs @@ -1,43 +1,21 @@ -pub(crate) use unnecessary_call_around_sorted::{ - unnecessary_call_around_sorted, UnnecessaryCallAroundSorted, -}; -pub(crate) use unnecessary_collection_call::{ - unnecessary_collection_call, UnnecessaryCollectionCall, -}; -pub(crate) use unnecessary_comprehension::{ - unnecessary_dict_comprehension, unnecessary_list_set_comprehension, UnnecessaryComprehension, -}; -pub(crate) use unnecessary_comprehension_any_all::{ - unnecessary_comprehension_any_all, UnnecessaryComprehensionAnyAll, -}; -pub(crate) use unnecessary_double_cast_or_process::{ - unnecessary_double_cast_or_process, UnnecessaryDoubleCastOrProcess, -}; -pub(crate) use unnecessary_generator_dict::{unnecessary_generator_dict, UnnecessaryGeneratorDict}; -pub(crate) use unnecessary_generator_list::{unnecessary_generator_list, UnnecessaryGeneratorList}; -pub(crate) use unnecessary_generator_set::{unnecessary_generator_set, UnnecessaryGeneratorSet}; -pub(crate) use unnecessary_list_call::{unnecessary_list_call, UnnecessaryListCall}; -pub(crate) use unnecessary_list_comprehension_dict::{ - unnecessary_list_comprehension_dict, UnnecessaryListComprehensionDict, -}; -pub(crate) use unnecessary_list_comprehension_set::{ - unnecessary_list_comprehension_set, UnnecessaryListComprehensionSet, -}; -pub(crate) use unnecessary_literal_dict::{unnecessary_literal_dict, UnnecessaryLiteralDict}; -pub(crate) use unnecessary_literal_set::{unnecessary_literal_set, UnnecessaryLiteralSet}; -pub(crate) use unnecessary_literal_within_dict_call::{ - unnecessary_literal_within_dict_call, UnnecessaryLiteralWithinDictCall, -}; -pub(crate) use unnecessary_literal_within_list_call::{ - unnecessary_literal_within_list_call, UnnecessaryLiteralWithinListCall, -}; -pub(crate) use unnecessary_literal_within_tuple_call::{ - unnecessary_literal_within_tuple_call, UnnecessaryLiteralWithinTupleCall, -}; -pub(crate) use unnecessary_map::{unnecessary_map, UnnecessaryMap}; -pub(crate) use unnecessary_subscript_reversal::{ - unnecessary_subscript_reversal, UnnecessarySubscriptReversal, -}; +pub(crate) use unnecessary_call_around_sorted::*; +pub(crate) use unnecessary_collection_call::*; +pub(crate) use unnecessary_comprehension::*; +pub(crate) use unnecessary_comprehension_any_all::*; +pub(crate) use unnecessary_double_cast_or_process::*; +pub(crate) use unnecessary_generator_dict::*; +pub(crate) use unnecessary_generator_list::*; +pub(crate) use unnecessary_generator_set::*; +pub(crate) use unnecessary_list_call::*; +pub(crate) use unnecessary_list_comprehension_dict::*; +pub(crate) use unnecessary_list_comprehension_set::*; +pub(crate) use unnecessary_literal_dict::*; +pub(crate) use unnecessary_literal_set::*; +pub(crate) use unnecessary_literal_within_dict_call::*; +pub(crate) use unnecessary_literal_within_list_call::*; +pub(crate) use unnecessary_literal_within_tuple_call::*; +pub(crate) use unnecessary_map::*; +pub(crate) use unnecessary_subscript_reversal::*; mod helpers; mod unnecessary_call_around_sorted; diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/mod.rs b/crates/ruff/src/rules/flake8_datetimez/rules/mod.rs index 8cfa09cdcc..53f734ee94 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/mod.rs @@ -1,22 +1,12 @@ -pub(crate) use call_date_fromtimestamp::{call_date_fromtimestamp, CallDateFromtimestamp}; -pub(crate) use call_date_today::{call_date_today, CallDateToday}; -pub(crate) use call_datetime_fromtimestamp::{ - call_datetime_fromtimestamp, CallDatetimeFromtimestamp, -}; -pub(crate) use call_datetime_now_without_tzinfo::{ - call_datetime_now_without_tzinfo, CallDatetimeNowWithoutTzinfo, -}; -pub(crate) use call_datetime_strptime_without_zone::{ - call_datetime_strptime_without_zone, CallDatetimeStrptimeWithoutZone, -}; -pub(crate) use call_datetime_today::{call_datetime_today, CallDatetimeToday}; -pub(crate) use call_datetime_utcfromtimestamp::{ - call_datetime_utcfromtimestamp, CallDatetimeUtcfromtimestamp, -}; -pub(crate) use call_datetime_utcnow::{call_datetime_utcnow, CallDatetimeUtcnow}; -pub(crate) use call_datetime_without_tzinfo::{ - call_datetime_without_tzinfo, CallDatetimeWithoutTzinfo, -}; +pub(crate) use call_date_fromtimestamp::*; +pub(crate) use call_date_today::*; +pub(crate) use call_datetime_fromtimestamp::*; +pub(crate) use call_datetime_now_without_tzinfo::*; +pub(crate) use call_datetime_strptime_without_zone::*; +pub(crate) use call_datetime_today::*; +pub(crate) use call_datetime_utcfromtimestamp::*; +pub(crate) use call_datetime_utcnow::*; +pub(crate) use call_datetime_without_tzinfo::*; mod call_date_fromtimestamp; mod call_date_today; diff --git a/crates/ruff/src/rules/flake8_debugger/rules/mod.rs b/crates/ruff/src/rules/flake8_debugger/rules/mod.rs index 1dda738e6a..c9e411937a 100644 --- a/crates/ruff/src/rules/flake8_debugger/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_debugger/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use debugger::{debugger_call, debugger_import, Debugger}; +pub(crate) use debugger::*; mod debugger; diff --git a/crates/ruff/src/rules/flake8_django/rules/mod.rs b/crates/ruff/src/rules/flake8_django/rules/mod.rs index 2b5188d71a..145dbe2149 100644 --- a/crates/ruff/src/rules/flake8_django/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_django/rules/mod.rs @@ -1,18 +1,10 @@ -pub(crate) use all_with_model_form::{all_with_model_form, DjangoAllWithModelForm}; -pub(crate) use exclude_with_model_form::{exclude_with_model_form, DjangoExcludeWithModelForm}; -pub(crate) use locals_in_render_function::{ - locals_in_render_function, DjangoLocalsInRenderFunction, -}; -pub(crate) use model_without_dunder_str::{model_without_dunder_str, DjangoModelWithoutDunderStr}; -pub(crate) use non_leading_receiver_decorator::{ - non_leading_receiver_decorator, DjangoNonLeadingReceiverDecorator, -}; -pub(crate) use nullable_model_string_field::{ - nullable_model_string_field, DjangoNullableModelStringField, -}; -pub(crate) use unordered_body_content_in_model::{ - unordered_body_content_in_model, DjangoUnorderedBodyContentInModel, -}; +pub(crate) use all_with_model_form::*; +pub(crate) use exclude_with_model_form::*; +pub(crate) use locals_in_render_function::*; +pub(crate) use model_without_dunder_str::*; +pub(crate) use non_leading_receiver_decorator::*; +pub(crate) use nullable_model_string_field::*; +pub(crate) use unordered_body_content_in_model::*; mod all_with_model_form; mod exclude_with_model_form; diff --git a/crates/ruff/src/rules/flake8_errmsg/rules/mod.rs b/crates/ruff/src/rules/flake8_errmsg/rules/mod.rs index 8740e30113..9fa6564a86 100644 --- a/crates/ruff/src/rules/flake8_errmsg/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_errmsg/rules/mod.rs @@ -1,5 +1,3 @@ -pub(crate) use string_in_exception::{ - string_in_exception, DotFormatInException, FStringInException, RawStringInException, -}; +pub(crate) use string_in_exception::*; mod string_in_exception; diff --git a/crates/ruff/src/rules/flake8_executable/rules/mod.rs b/crates/ruff/src/rules/flake8_executable/rules/mod.rs index 4e300fae44..35b1aa269e 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/mod.rs @@ -1,8 +1,8 @@ -pub(crate) use shebang_missing::{shebang_missing, ShebangMissingExecutableFile}; -pub(crate) use shebang_newline::{shebang_newline, ShebangNotFirstLine}; -pub(crate) use shebang_not_executable::{shebang_not_executable, ShebangNotExecutable}; -pub(crate) use shebang_python::{shebang_python, ShebangMissingPython}; -pub(crate) use shebang_whitespace::{shebang_whitespace, ShebangLeadingWhitespace}; +pub(crate) use shebang_missing::*; +pub(crate) use shebang_newline::*; +pub(crate) use shebang_not_executable::*; +pub(crate) use shebang_python::*; +pub(crate) use shebang_whitespace::*; mod shebang_missing; mod shebang_newline; diff --git a/crates/ruff/src/rules/flake8_fixme/rules/mod.rs b/crates/ruff/src/rules/flake8_fixme/rules/mod.rs index 92e8eae87d..9e5980aeb9 100644 --- a/crates/ruff/src/rules/flake8_fixme/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_fixme/rules/mod.rs @@ -1,5 +1,3 @@ -pub(crate) use todos::{ - todos, LineContainsFixme, LineContainsHack, LineContainsTodo, LineContainsXxx, -}; +pub(crate) use todos::*; mod todos; diff --git a/crates/ruff/src/rules/flake8_future_annotations/rules/mod.rs b/crates/ruff/src/rules/flake8_future_annotations/rules/mod.rs index e5e674a5c8..3af2f506fd 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_future_annotations/rules/mod.rs @@ -1,9 +1,5 @@ -pub(crate) use future_required_type_annotation::{ - future_required_type_annotation, FutureRequiredTypeAnnotation, Reason, -}; -pub(crate) use future_rewritable_type_annotation::{ - future_rewritable_type_annotation, FutureRewritableTypeAnnotation, -}; +pub(crate) use future_required_type_annotation::*; +pub(crate) use future_rewritable_type_annotation::*; mod future_required_type_annotation; mod future_rewritable_type_annotation; diff --git a/crates/ruff/src/rules/flake8_gettext/rules/mod.rs b/crates/ruff/src/rules/flake8_gettext/rules/mod.rs index 1a80938fd1..833309bd21 100644 --- a/crates/ruff/src/rules/flake8_gettext/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_gettext/rules/mod.rs @@ -1,13 +1,7 @@ -pub(crate) use f_string_in_gettext_func_call::{ - f_string_in_gettext_func_call, FStringInGetTextFuncCall, -}; -pub(crate) use format_in_gettext_func_call::{ - format_in_gettext_func_call, FormatInGetTextFuncCall, -}; +pub(crate) use f_string_in_gettext_func_call::*; +pub(crate) use format_in_gettext_func_call::*; pub(crate) use is_gettext_func_call::is_gettext_func_call; -pub(crate) use printf_in_gettext_func_call::{ - printf_in_gettext_func_call, PrintfInGetTextFuncCall, -}; +pub(crate) use printf_in_gettext_func_call::*; mod f_string_in_gettext_func_call; mod format_in_gettext_func_call; diff --git a/crates/ruff/src/rules/flake8_implicit_str_concat/rules/mod.rs b/crates/ruff/src/rules/flake8_implicit_str_concat/rules/mod.rs index 605341dc27..8ec813567d 100644 --- a/crates/ruff/src/rules/flake8_implicit_str_concat/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_implicit_str_concat/rules/mod.rs @@ -1,7 +1,5 @@ -pub(crate) use explicit::{explicit, ExplicitStringConcatenation}; -pub(crate) use implicit::{ - implicit, MultiLineImplicitStringConcatenation, SingleLineImplicitStringConcatenation, -}; +pub(crate) use explicit::*; +pub(crate) use implicit::*; mod explicit; mod implicit; diff --git a/crates/ruff/src/rules/flake8_import_conventions/rules/mod.rs b/crates/ruff/src/rules/flake8_import_conventions/rules/mod.rs index 63f88d0f9a..9d6802ed73 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_import_conventions/rules/mod.rs @@ -1,6 +1,6 @@ -pub(crate) use banned_import_alias::{banned_import_alias, BannedImportAlias}; -pub(crate) use banned_import_from::{banned_import_from, BannedImportFrom}; -pub(crate) use conventional_import_alias::{conventional_import_alias, UnconventionalImportAlias}; +pub(crate) use banned_import_alias::*; +pub(crate) use banned_import_from::*; +pub(crate) use conventional_import_alias::*; mod banned_import_alias; mod banned_import_from; diff --git a/crates/ruff/src/rules/flake8_no_pep420/rules/mod.rs b/crates/ruff/src/rules/flake8_no_pep420/rules/mod.rs index 6b3762cae3..060a6879aa 100644 --- a/crates/ruff/src/rules/flake8_no_pep420/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_no_pep420/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use implicit_namespace_package::{implicit_namespace_package, ImplicitNamespacePackage}; +pub(crate) use implicit_namespace_package::*; mod implicit_namespace_package; diff --git a/crates/ruff/src/rules/flake8_pie/rules/mod.rs b/crates/ruff/src/rules/flake8_pie/rules/mod.rs index 75a94605ee..e86cd7a19d 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/mod.rs @@ -1,12 +1,10 @@ -pub(crate) use duplicate_class_field_definition::{ - duplicate_class_field_definition, DuplicateClassFieldDefinition, -}; -pub(crate) use multiple_starts_ends_with::{multiple_starts_ends_with, MultipleStartsEndsWith}; -pub(crate) use no_unnecessary_pass::{no_unnecessary_pass, UnnecessaryPass}; -pub(crate) use non_unique_enums::{non_unique_enums, NonUniqueEnums}; -pub(crate) use reimplemented_list_builtin::{reimplemented_list_builtin, ReimplementedListBuiltin}; -pub(crate) use unnecessary_dict_kwargs::{unnecessary_dict_kwargs, UnnecessaryDictKwargs}; -pub(crate) use unnecessary_spread::{unnecessary_spread, UnnecessarySpread}; +pub(crate) use duplicate_class_field_definition::*; +pub(crate) use multiple_starts_ends_with::*; +pub(crate) use no_unnecessary_pass::*; +pub(crate) use non_unique_enums::*; +pub(crate) use reimplemented_list_builtin::*; +pub(crate) use unnecessary_dict_kwargs::*; +pub(crate) use unnecessary_spread::*; mod duplicate_class_field_definition; mod multiple_starts_ends_with; diff --git a/crates/ruff/src/rules/flake8_print/rules/mod.rs b/crates/ruff/src/rules/flake8_print/rules/mod.rs index e044fe39e9..954fd75b9e 100644 --- a/crates/ruff/src/rules/flake8_print/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_print/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use print_call::{print_call, PPrint, Print}; +pub(crate) use print_call::*; mod print_call; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index a754a518d2..ea3a26a1ad 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -1,47 +1,26 @@ -pub(crate) use any_eq_ne_annotation::{any_eq_ne_annotation, AnyEqNeAnnotation}; -pub(crate) use bad_version_info_comparison::{ - bad_version_info_comparison, BadVersionInfoComparison, -}; -pub(crate) use collections_named_tuple::{collections_named_tuple, CollectionsNamedTuple}; -pub(crate) use docstring_in_stubs::{docstring_in_stubs, DocstringInStub}; -pub(crate) use duplicate_union_member::{duplicate_union_member, DuplicateUnionMember}; -pub(crate) use ellipsis_in_non_empty_class_body::{ - ellipsis_in_non_empty_class_body, EllipsisInNonEmptyClassBody, -}; -pub(crate) use iter_method_return_iterable::{ - iter_method_return_iterable, IterMethodReturnIterable, -}; -pub(crate) use no_return_argument_annotation::{ - no_return_argument_annotation, NoReturnArgumentAnnotationInStub, -}; -pub(crate) use non_empty_stub_body::{non_empty_stub_body, NonEmptyStubBody}; -pub(crate) use non_self_return_type::{non_self_return_type, NonSelfReturnType}; -pub(crate) use numeric_literal_too_long::{numeric_literal_too_long, NumericLiteralTooLong}; -pub(crate) use pass_in_class_body::{pass_in_class_body, PassInClassBody}; -pub(crate) use pass_statement_stub_body::{pass_statement_stub_body, PassStatementStubBody}; -pub(crate) use prefix_type_params::{prefix_type_params, UnprefixedTypeParam}; -pub(crate) use quoted_annotation_in_stub::{quoted_annotation_in_stub, QuotedAnnotationInStub}; -pub(crate) use simple_defaults::{ - annotated_assignment_default_in_stub, argument_simple_defaults, assignment_default_in_stub, - typed_argument_simple_defaults, unannotated_assignment_in_stub, - unassigned_special_variable_in_stub, ArgumentDefaultInStub, AssignmentDefaultInStub, - TypedArgumentDefaultInStub, UnannotatedAssignmentInStub, UnassignedSpecialVariableInStub, -}; -pub(crate) use str_or_repr_defined_in_stub::{str_or_repr_defined_in_stub, StrOrReprDefinedInStub}; -pub(crate) use string_or_bytes_too_long::{string_or_bytes_too_long, StringOrBytesTooLong}; -pub(crate) use stub_body_multiple_statements::{ - stub_body_multiple_statements, StubBodyMultipleStatements, -}; -pub(crate) use type_alias_naming::{ - snake_case_type_alias, t_suffixed_type_alias, SnakeCaseTypeAlias, TSuffixedTypeAlias, -}; -pub(crate) use type_comment_in_stub::{type_comment_in_stub, TypeCommentInStub}; -pub(crate) use unaliased_collections_abc_set_import::{ - unaliased_collections_abc_set_import, UnaliasedCollectionsAbcSetImport, -}; -pub(crate) use unrecognized_platform::{ - unrecognized_platform, UnrecognizedPlatformCheck, UnrecognizedPlatformName, -}; +pub(crate) use any_eq_ne_annotation::*; +pub(crate) use bad_version_info_comparison::*; +pub(crate) use collections_named_tuple::*; +pub(crate) use docstring_in_stubs::*; +pub(crate) use duplicate_union_member::*; +pub(crate) use ellipsis_in_non_empty_class_body::*; +pub(crate) use iter_method_return_iterable::*; +pub(crate) use no_return_argument_annotation::*; +pub(crate) use non_empty_stub_body::*; +pub(crate) use non_self_return_type::*; +pub(crate) use numeric_literal_too_long::*; +pub(crate) use pass_in_class_body::*; +pub(crate) use pass_statement_stub_body::*; +pub(crate) use prefix_type_params::*; +pub(crate) use quoted_annotation_in_stub::*; +pub(crate) use simple_defaults::*; +pub(crate) use str_or_repr_defined_in_stub::*; +pub(crate) use string_or_bytes_too_long::*; +pub(crate) use stub_body_multiple_statements::*; +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::*; mod any_eq_ne_annotation; mod bad_version_info_comparison; diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/mod.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/mod.rs index dcf1277056..783431e396 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/mod.rs @@ -1,29 +1,11 @@ -pub(crate) use assertion::{ - assert_falsy, assert_in_exception_handler, composite_condition, unittest_assertion, - PytestAssertAlwaysFalse, PytestAssertInExcept, PytestCompositeAssertion, - PytestUnittestAssertion, -}; -pub(crate) use fail::{fail_call, PytestFailWithoutMessage}; -pub(crate) use fixture::{ - fixture, PytestDeprecatedYieldFixture, PytestErroneousUseFixturesOnFixture, - PytestExtraneousScopeFunction, PytestFixtureFinalizerCallback, - PytestFixtureIncorrectParenthesesStyle, PytestFixtureParamWithoutValue, - PytestFixturePositionalArgs, PytestIncorrectFixtureNameUnderscore, - PytestMissingFixtureNameUnderscore, PytestUnnecessaryAsyncioMarkOnFixture, - PytestUselessYieldFixture, -}; -pub(crate) use imports::{import, import_from, PytestIncorrectPytestImport}; -pub(crate) use marks::{ - marks, PytestIncorrectMarkParenthesesStyle, PytestUseFixturesWithoutParameters, -}; -pub(crate) use parametrize::{ - parametrize, PytestParametrizeNamesWrongType, PytestParametrizeValuesWrongType, -}; -pub(crate) use patch::{patch_with_lambda, PytestPatchWithLambda}; -pub(crate) use raises::{ - complex_raises, raises_call, PytestRaisesTooBroad, PytestRaisesWithMultipleStatements, - PytestRaisesWithoutException, -}; +pub(crate) use assertion::*; +pub(crate) use fail::*; +pub(crate) use fixture::*; +pub(crate) use imports::*; +pub(crate) use marks::*; +pub(crate) use parametrize::*; +pub(crate) use patch::*; +pub(crate) use raises::*; mod assertion; mod fail; diff --git a/crates/ruff/src/rules/flake8_quotes/rules/mod.rs b/crates/ruff/src/rules/flake8_quotes/rules/mod.rs index 7f04e8908f..8ad6bad659 100644 --- a/crates/ruff/src/rules/flake8_quotes/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_quotes/rules/mod.rs @@ -1,6 +1,3 @@ -pub(crate) use from_tokens::{ - from_tokens, AvoidableEscapedQuote, BadQuotesDocstring, BadQuotesInlineString, - BadQuotesMultilineString, -}; +pub(crate) use from_tokens::*; mod from_tokens; diff --git a/crates/ruff/src/rules/flake8_raise/rules/mod.rs b/crates/ruff/src/rules/flake8_raise/rules/mod.rs index 12efaed6ba..2df0cf3dd3 100644 --- a/crates/ruff/src/rules/flake8_raise/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_raise/rules/mod.rs @@ -1,5 +1,3 @@ -pub(crate) use unnecessary_paren_on_raise_exception::{ - unnecessary_paren_on_raise_exception, UnnecessaryParenOnRaiseException, -}; +pub(crate) use unnecessary_paren_on_raise_exception::*; mod unnecessary_paren_on_raise_exception; diff --git a/crates/ruff/src/rules/flake8_return/rules/mod.rs b/crates/ruff/src/rules/flake8_return/rules/mod.rs index 7e6a3fbcf7..7435e8a228 100644 --- a/crates/ruff/src/rules/flake8_return/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_return/rules/mod.rs @@ -1,6 +1,3 @@ -pub(crate) use function::{ - function, ImplicitReturn, ImplicitReturnValue, SuperfluousElseBreak, SuperfluousElseContinue, - SuperfluousElseRaise, SuperfluousElseReturn, UnnecessaryAssign, UnnecessaryReturnNone, -}; +pub(crate) use function::*; mod function; diff --git a/crates/ruff/src/rules/flake8_self/rules/mod.rs b/crates/ruff/src/rules/flake8_self/rules/mod.rs index edd1f1a2f7..f2dfbef12a 100644 --- a/crates/ruff/src/rules/flake8_self/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_self/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use private_member_access::{private_member_access, PrivateMemberAccess}; +pub(crate) use private_member_access::*; mod private_member_access; diff --git a/crates/ruff/src/rules/flake8_simplify/rules/mod.rs b/crates/ruff/src/rules/flake8_simplify/rules/mod.rs index 80a9ad752d..0e4b3b4937 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/mod.rs @@ -1,36 +1,15 @@ -pub(crate) use ast_bool_op::{ - compare_with_tuple, duplicate_isinstance_call, expr_and_false, expr_and_not_expr, - expr_or_not_expr, expr_or_true, CompareWithTuple, DuplicateIsinstanceCall, ExprAndFalse, - ExprAndNotExpr, ExprOrNotExpr, ExprOrTrue, -}; -pub(crate) use ast_expr::{ - dict_get_with_none_default, use_capital_environment_variables, DictGetWithNoneDefault, - UncapitalizedEnvironmentVariables, -}; -pub(crate) use ast_if::{ - if_with_same_arms, manual_dict_lookup, needless_bool, nested_if_statements, - use_dict_get_with_default, use_ternary_operator, CollapsibleIf, IfElseBlockInsteadOfDictGet, - IfElseBlockInsteadOfDictLookup, IfElseBlockInsteadOfIfExp, IfWithSameArms, NeedlessBool, -}; -pub(crate) use ast_ifexp::{ - explicit_false_true_in_ifexpr, explicit_true_false_in_ifexpr, twisted_arms_in_ifexpr, - IfExprWithFalseTrue, IfExprWithTrueFalse, IfExprWithTwistedArms, -}; -pub(crate) use ast_unary_op::{ - double_negation, negation_with_equal_op, negation_with_not_equal_op, DoubleNegation, - NegateEqualOp, NegateNotEqualOp, -}; -pub(crate) use ast_with::{multiple_with_statements, MultipleWithStatements}; -pub(crate) use key_in_dict::{key_in_dict_compare, key_in_dict_for, InDictKeys}; -pub(crate) use open_file_with_context_handler::{ - open_file_with_context_handler, OpenFileWithContextHandler, -}; -pub(crate) use reimplemented_builtin::{convert_for_loop_to_any_all, ReimplementedBuiltin}; -pub(crate) use return_in_try_except_finally::{ - return_in_try_except_finally, ReturnInTryExceptFinally, -}; -pub(crate) use suppressible_exception::{suppressible_exception, SuppressibleException}; -pub(crate) use yoda_conditions::{yoda_conditions, YodaConditions}; +pub(crate) use ast_bool_op::*; +pub(crate) use ast_expr::*; +pub(crate) use ast_if::*; +pub(crate) use ast_ifexp::*; +pub(crate) use ast_unary_op::*; +pub(crate) use ast_with::*; +pub(crate) use key_in_dict::*; +pub(crate) use open_file_with_context_handler::*; +pub(crate) use reimplemented_builtin::*; +pub(crate) use return_in_try_except_finally::*; +pub(crate) use suppressible_exception::*; +pub(crate) use yoda_conditions::*; mod ast_bool_op; mod ast_expr; diff --git a/crates/ruff/src/rules/flake8_slots/rules/mod.rs b/crates/ruff/src/rules/flake8_slots/rules/mod.rs index d7a91ce438..abbed6b9e4 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/mod.rs @@ -1,8 +1,6 @@ -pub(crate) use no_slots_in_namedtuple_subclass::{ - no_slots_in_namedtuple_subclass, NoSlotsInNamedtupleSubclass, -}; -pub(crate) use no_slots_in_str_subclass::{no_slots_in_str_subclass, NoSlotsInStrSubclass}; -pub(crate) use no_slots_in_tuple_subclass::{no_slots_in_tuple_subclass, NoSlotsInTupleSubclass}; +pub(crate) use no_slots_in_namedtuple_subclass::*; +pub(crate) use no_slots_in_str_subclass::*; +pub(crate) use no_slots_in_tuple_subclass::*; mod helpers; mod no_slots_in_namedtuple_subclass; diff --git a/crates/ruff/src/rules/flake8_tidy_imports/rules/mod.rs b/crates/ruff/src/rules/flake8_tidy_imports/rules/mod.rs index 8efbdfd6ae..660116d718 100644 --- a/crates/ruff/src/rules/flake8_tidy_imports/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_tidy_imports/rules/mod.rs @@ -1,7 +1,5 @@ -pub(crate) use banned_api::{ - banned_attribute_access, name_is_banned, name_or_parent_is_banned, BannedApi, -}; -pub(crate) use relative_imports::{banned_relative_import, RelativeImports}; +pub(crate) use banned_api::*; +pub(crate) use relative_imports::*; mod banned_api; mod relative_imports; diff --git a/crates/ruff/src/rules/flake8_todos/rules/mod.rs b/crates/ruff/src/rules/flake8_todos/rules/mod.rs index dd10c6bc3b..9e5980aeb9 100644 --- a/crates/ruff/src/rules/flake8_todos/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_todos/rules/mod.rs @@ -1,6 +1,3 @@ -pub(crate) use todos::{ - todos, InvalidTodoCapitalization, InvalidTodoTag, MissingSpaceAfterTodoColon, - MissingTodoAuthor, MissingTodoColon, MissingTodoDescription, MissingTodoLink, -}; +pub(crate) use todos::*; mod todos; diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/mod.rs b/crates/ruff/src/rules/flake8_type_checking/rules/mod.rs index 3873ebd1e7..15ceb3ddf1 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/mod.rs @@ -1,11 +1,6 @@ -pub(crate) use empty_type_checking_block::{empty_type_checking_block, EmptyTypeCheckingBlock}; -pub(crate) use runtime_import_in_type_checking_block::{ - runtime_import_in_type_checking_block, RuntimeImportInTypeCheckingBlock, -}; -pub(crate) use typing_only_runtime_import::{ - typing_only_runtime_import, TypingOnlyFirstPartyImport, TypingOnlyStandardLibraryImport, - TypingOnlyThirdPartyImport, -}; +pub(crate) use empty_type_checking_block::*; +pub(crate) use runtime_import_in_type_checking_block::*; +pub(crate) use typing_only_runtime_import::*; mod empty_type_checking_block; mod runtime_import_in_type_checking_block; diff --git a/crates/ruff/src/rules/flake8_unused_arguments/rules/mod.rs b/crates/ruff/src/rules/flake8_unused_arguments/rules/mod.rs index 5cf115d8bf..0c46f878f4 100644 --- a/crates/ruff/src/rules/flake8_unused_arguments/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_unused_arguments/rules/mod.rs @@ -1,6 +1,3 @@ -pub(crate) use unused_arguments::{ - unused_arguments, UnusedClassMethodArgument, UnusedFunctionArgument, UnusedLambdaArgument, - UnusedMethodArgument, UnusedStaticMethodArgument, -}; +pub(crate) use unused_arguments::*; mod unused_arguments; diff --git a/crates/ruff/src/rules/flynt/rules/mod.rs b/crates/ruff/src/rules/flynt/rules/mod.rs index ce0b9462d8..b57de8364f 100644 --- a/crates/ruff/src/rules/flynt/rules/mod.rs +++ b/crates/ruff/src/rules/flynt/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use static_join_to_fstring::{static_join_to_fstring, StaticJoinToFString}; +pub(crate) use static_join_to_fstring::*; mod static_join_to_fstring; diff --git a/crates/ruff/src/rules/isort/rules/mod.rs b/crates/ruff/src/rules/isort/rules/mod.rs index 0184926441..4db286f410 100644 --- a/crates/ruff/src/rules/isort/rules/mod.rs +++ b/crates/ruff/src/rules/isort/rules/mod.rs @@ -1,5 +1,5 @@ -pub(crate) use add_required_imports::{add_required_imports, MissingRequiredImport}; -pub(crate) use organize_imports::{organize_imports, UnsortedImports}; +pub(crate) use add_required_imports::*; +pub(crate) use organize_imports::*; pub(crate) mod add_required_imports; pub(crate) mod organize_imports; diff --git a/crates/ruff/src/rules/mccabe/rules/mod.rs b/crates/ruff/src/rules/mccabe/rules/mod.rs index b6898b7da2..a23f4cd8d2 100644 --- a/crates/ruff/src/rules/mccabe/rules/mod.rs +++ b/crates/ruff/src/rules/mccabe/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use function_is_too_complex::{function_is_too_complex, ComplexStructure}; +pub(crate) use function_is_too_complex::*; mod function_is_too_complex; diff --git a/crates/ruff/src/rules/numpy/rules/mod.rs b/crates/ruff/src/rules/numpy/rules/mod.rs index c82e79bc1c..25c3f2fb17 100644 --- a/crates/ruff/src/rules/numpy/rules/mod.rs +++ b/crates/ruff/src/rules/numpy/rules/mod.rs @@ -1,5 +1,5 @@ -pub(crate) use deprecated_type_alias::{deprecated_type_alias, NumpyDeprecatedTypeAlias}; -pub(crate) use numpy_legacy_random::{numpy_legacy_random, NumpyLegacyRandom}; +pub(crate) use deprecated_type_alias::*; +pub(crate) use numpy_legacy_random::*; mod deprecated_type_alias; mod numpy_legacy_random; diff --git a/crates/ruff/src/rules/pandas_vet/rules/mod.rs b/crates/ruff/src/rules/pandas_vet/rules/mod.rs index 917c9a2efa..a0c1ef0908 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/mod.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/mod.rs @@ -1,12 +1,9 @@ -pub(crate) use assignment_to_df::{assignment_to_df, PandasDfVariableName}; -pub(crate) use attr::{attr, PandasUseOfDotValues}; -pub(crate) use call::{ - call, PandasUseOfDotIsNull, PandasUseOfDotNotNull, PandasUseOfDotPivotOrUnstack, - PandasUseOfDotReadTable, PandasUseOfDotStack, -}; -pub(crate) use inplace_argument::{inplace_argument, PandasUseOfInplaceArgument}; -pub(crate) use pd_merge::{use_of_pd_merge, PandasUseOfPdMerge}; -pub(crate) use subscript::{subscript, PandasUseOfDotAt, PandasUseOfDotIat, PandasUseOfDotIx}; +pub(crate) use assignment_to_df::*; +pub(crate) use attr::*; +pub(crate) use call::*; +pub(crate) use inplace_argument::*; +pub(crate) use pd_merge::*; +pub(crate) use subscript::*; pub(crate) mod assignment_to_df; pub(crate) mod attr; diff --git a/crates/ruff/src/rules/pep8_naming/rules/mod.rs b/crates/ruff/src/rules/pep8_naming/rules/mod.rs index f842ca40ea..636ae11477 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/mod.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/mod.rs @@ -1,41 +1,19 @@ -pub(crate) use camelcase_imported_as_acronym::{ - camelcase_imported_as_acronym, CamelcaseImportedAsAcronym, -}; -pub(crate) use camelcase_imported_as_constant::{ - camelcase_imported_as_constant, CamelcaseImportedAsConstant, -}; -pub(crate) use camelcase_imported_as_lowercase::{ - camelcase_imported_as_lowercase, CamelcaseImportedAsLowercase, -}; -pub(crate) use constant_imported_as_non_constant::{ - constant_imported_as_non_constant, ConstantImportedAsNonConstant, -}; -pub(crate) use dunder_function_name::{dunder_function_name, DunderFunctionName}; -pub(crate) use error_suffix_on_exception_name::{ - error_suffix_on_exception_name, ErrorSuffixOnExceptionName, -}; -pub(crate) use invalid_argument_name::{invalid_argument_name, InvalidArgumentName}; -pub(crate) use invalid_class_name::{invalid_class_name, InvalidClassName}; -pub(crate) use invalid_first_argument_name_for_class_method::{ - invalid_first_argument_name_for_class_method, InvalidFirstArgumentNameForClassMethod, -}; -pub(crate) use invalid_first_argument_name_for_method::{ - invalid_first_argument_name_for_method, InvalidFirstArgumentNameForMethod, -}; -pub(crate) use invalid_function_name::{invalid_function_name, InvalidFunctionName}; -pub(crate) use invalid_module_name::{invalid_module_name, InvalidModuleName}; -pub(crate) use lowercase_imported_as_non_lowercase::{ - lowercase_imported_as_non_lowercase, LowercaseImportedAsNonLowercase, -}; -pub(crate) use mixed_case_variable_in_class_scope::{ - mixed_case_variable_in_class_scope, MixedCaseVariableInClassScope, -}; -pub(crate) use mixed_case_variable_in_global_scope::{ - mixed_case_variable_in_global_scope, MixedCaseVariableInGlobalScope, -}; -pub(crate) use non_lowercase_variable_in_function::{ - non_lowercase_variable_in_function, NonLowercaseVariableInFunction, -}; +pub(crate) use camelcase_imported_as_acronym::*; +pub(crate) use camelcase_imported_as_constant::*; +pub(crate) use camelcase_imported_as_lowercase::*; +pub(crate) use constant_imported_as_non_constant::*; +pub(crate) use dunder_function_name::*; +pub(crate) use error_suffix_on_exception_name::*; +pub(crate) use invalid_argument_name::*; +pub(crate) use invalid_class_name::*; +pub(crate) use invalid_first_argument_name_for_class_method::*; +pub(crate) use invalid_first_argument_name_for_method::*; +pub(crate) use invalid_function_name::*; +pub(crate) use invalid_module_name::*; +pub(crate) use lowercase_imported_as_non_lowercase::*; +pub(crate) use mixed_case_variable_in_class_scope::*; +pub(crate) use mixed_case_variable_in_global_scope::*; +pub(crate) use non_lowercase_variable_in_function::*; mod camelcase_imported_as_acronym; mod camelcase_imported_as_constant; diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs index ca87e1264a..a00c71e750 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs @@ -1,50 +1,24 @@ -use bitflags::bitflags; -use ruff_text_size::{TextLen, TextRange, TextSize}; -use rustpython_parser::lexer::LexResult; +pub(crate) use extraneous_whitespace::*; +pub(crate) use indentation::*; +pub(crate) use missing_whitespace::*; +pub(crate) use missing_whitespace_after_keyword::*; +pub(crate) use missing_whitespace_around_operator::*; +pub(crate) use space_around_operator::*; +pub(crate) use whitespace_around_keywords::*; +pub(crate) use whitespace_around_named_parameter_equals::*; +pub(crate) use whitespace_before_comment::*; +pub(crate) use whitespace_before_parameters::*; + use std::fmt::{Debug, Formatter}; use std::iter::FusedIterator; +use bitflags::bitflags; +use ruff_text_size::{TextLen, TextRange, TextSize}; +use rustpython_parser::lexer::LexResult; + use ruff_python_ast::source_code::Locator; use ruff_python_ast::token_kind::TokenKind; - -pub(crate) use extraneous_whitespace::{ - extraneous_whitespace, WhitespaceAfterOpenBracket, WhitespaceBeforeCloseBracket, - WhitespaceBeforePunctuation, -}; -pub(crate) use indentation::{ - indentation, IndentationWithInvalidMultiple, IndentationWithInvalidMultipleComment, - NoIndentedBlock, NoIndentedBlockComment, OverIndented, UnexpectedIndentation, - UnexpectedIndentationComment, -}; -pub(crate) use missing_whitespace::{missing_whitespace, MissingWhitespace}; -pub(crate) use missing_whitespace_after_keyword::{ - missing_whitespace_after_keyword, MissingWhitespaceAfterKeyword, -}; -pub(crate) use missing_whitespace_around_operator::{ - missing_whitespace_around_operator, MissingWhitespaceAroundArithmeticOperator, - MissingWhitespaceAroundBitwiseOrShiftOperator, MissingWhitespaceAroundModuloOperator, - MissingWhitespaceAroundOperator, -}; use ruff_python_whitespace::is_python_whitespace; -pub(crate) use space_around_operator::{ - space_around_operator, MultipleSpacesAfterOperator, MultipleSpacesBeforeOperator, - TabAfterOperator, TabBeforeOperator, -}; -pub(crate) use whitespace_around_keywords::{ - whitespace_around_keywords, MultipleSpacesAfterKeyword, MultipleSpacesBeforeKeyword, - TabAfterKeyword, TabBeforeKeyword, -}; -pub(crate) use whitespace_around_named_parameter_equals::{ - whitespace_around_named_parameter_equals, MissingWhitespaceAroundParameterEquals, - UnexpectedSpacesAroundKeywordParameterEquals, -}; -pub(crate) use whitespace_before_comment::{ - whitespace_before_comment, MultipleLeadingHashesForBlockComment, NoSpaceAfterBlockComment, - NoSpaceAfterInlineComment, TooFewSpacesBeforeInlineComment, -}; -pub(crate) use whitespace_before_parameters::{ - whitespace_before_parameters, WhitespaceBeforeParameters, -}; mod extraneous_whitespace; mod indentation; diff --git a/crates/ruff/src/rules/pycodestyle/rules/mod.rs b/crates/ruff/src/rules/pycodestyle/rules/mod.rs index 2d43de0186..c9fceb87a6 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/mod.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/mod.rs @@ -1,33 +1,23 @@ -pub(crate) use ambiguous_class_name::{ambiguous_class_name, AmbiguousClassName}; -pub(crate) use ambiguous_function_name::{ambiguous_function_name, AmbiguousFunctionName}; -pub(crate) use ambiguous_variable_name::{ambiguous_variable_name, AmbiguousVariableName}; -pub(crate) use bare_except::{bare_except, BareExcept}; -pub(crate) use compound_statements::{ - compound_statements, MultipleStatementsOnOneLineColon, MultipleStatementsOnOneLineSemicolon, - UselessSemicolon, -}; -pub(crate) use doc_line_too_long::{doc_line_too_long, DocLineTooLong}; +pub(crate) use ambiguous_class_name::*; +pub(crate) use ambiguous_function_name::*; +pub(crate) use ambiguous_variable_name::*; +pub(crate) use bare_except::*; +pub(crate) use compound_statements::*; +pub(crate) use doc_line_too_long::*; pub use errors::IOError; -pub(crate) use errors::{syntax_error, SyntaxError}; -pub(crate) use imports::{ - module_import_not_at_top_of_file, multiple_imports_on_one_line, ModuleImportNotAtTopOfFile, - MultipleImportsOnOneLine, -}; +pub(crate) use errors::*; +pub(crate) use imports::*; -pub(crate) use invalid_escape_sequence::{invalid_escape_sequence, InvalidEscapeSequence}; -pub(crate) use lambda_assignment::{lambda_assignment, LambdaAssignment}; -pub(crate) use line_too_long::{line_too_long, LineTooLong}; -pub(crate) use literal_comparisons::{literal_comparisons, NoneComparison, TrueFalseComparison}; -pub(crate) use missing_newline_at_end_of_file::{ - no_newline_at_end_of_file, MissingNewlineAtEndOfFile, -}; -pub(crate) use mixed_spaces_and_tabs::{mixed_spaces_and_tabs, MixedSpacesAndTabs}; -pub(crate) use not_tests::{not_tests, NotInTest, NotIsTest}; -pub(crate) use tab_indentation::{tab_indentation, TabIndentation}; -pub(crate) use trailing_whitespace::{ - trailing_whitespace, BlankLineWithWhitespace, TrailingWhitespace, -}; -pub(crate) use type_comparison::{type_comparison, TypeComparison}; +pub(crate) use invalid_escape_sequence::*; +pub(crate) use lambda_assignment::*; +pub(crate) use line_too_long::*; +pub(crate) use literal_comparisons::*; +pub(crate) use missing_newline_at_end_of_file::*; +pub(crate) use mixed_spaces_and_tabs::*; +pub(crate) use not_tests::*; +pub(crate) use tab_indentation::*; +pub(crate) use trailing_whitespace::*; +pub(crate) use type_comparison::*; mod ambiguous_class_name; mod ambiguous_function_name; diff --git a/crates/ruff/src/rules/pydocstyle/rules/mod.rs b/crates/ruff/src/rules/pydocstyle/rules/mod.rs index 2cb62ffe0a..e71d7d08fc 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/mod.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/mod.rs @@ -1,41 +1,23 @@ -pub(crate) use backslashes::{backslashes, EscapeSequenceInDocstring}; -pub(crate) use blank_after_summary::{blank_after_summary, BlankLineAfterSummary}; -pub(crate) use blank_before_after_class::{ - blank_before_after_class, BlankLineBeforeClass, OneBlankLineAfterClass, OneBlankLineBeforeClass, -}; -pub(crate) use blank_before_after_function::{ - blank_before_after_function, NoBlankLineAfterFunction, NoBlankLineBeforeFunction, -}; -pub(crate) use capitalized::{capitalized, FirstLineCapitalized}; -pub(crate) use ends_with_period::{ends_with_period, EndsInPeriod}; -pub(crate) use ends_with_punctuation::{ends_with_punctuation, EndsInPunctuation}; -pub(crate) use if_needed::{if_needed, OverloadWithDocstring}; -pub(crate) use indent::{indent, IndentWithSpaces, OverIndentation, UnderIndentation}; -pub(crate) use multi_line_summary_start::{ - multi_line_summary_start, MultiLineSummaryFirstLine, MultiLineSummarySecondLine, -}; -pub(crate) use newline_after_last_paragraph::{ - newline_after_last_paragraph, NewLineAfterLastParagraph, -}; -pub(crate) use no_signature::{no_signature, NoSignature}; -pub(crate) use no_surrounding_whitespace::{no_surrounding_whitespace, SurroundingWhitespace}; -pub(crate) use non_imperative_mood::{non_imperative_mood, NonImperativeMood}; -pub(crate) use not_empty::{not_empty, EmptyDocstring}; -pub(crate) use not_missing::{ - not_missing, UndocumentedMagicMethod, UndocumentedPublicClass, UndocumentedPublicFunction, - UndocumentedPublicInit, UndocumentedPublicMethod, UndocumentedPublicModule, - UndocumentedPublicNestedClass, UndocumentedPublicPackage, -}; -pub(crate) use one_liner::{one_liner, FitsOnOneLine}; -pub(crate) use sections::{ - sections, BlankLineAfterLastSection, BlankLinesBetweenHeaderAndContent, CapitalizeSectionName, - DashedUnderlineAfterSection, EmptyDocstringSection, NewLineAfterSectionName, - NoBlankLineAfterSection, NoBlankLineBeforeSection, SectionNameEndsInColon, - SectionNotOverIndented, SectionUnderlineAfterName, SectionUnderlineMatchesSectionLength, - SectionUnderlineNotOverIndented, UndocumentedParam, -}; -pub(crate) use starts_with_this::{starts_with_this, DocstringStartsWithThis}; -pub(crate) use triple_quotes::{triple_quotes, TripleSingleQuotes}; +pub(crate) use backslashes::*; +pub(crate) use blank_after_summary::*; +pub(crate) use blank_before_after_class::*; +pub(crate) use blank_before_after_function::*; +pub(crate) use capitalized::*; +pub(crate) use ends_with_period::*; +pub(crate) use ends_with_punctuation::*; +pub(crate) use if_needed::*; +pub(crate) use indent::*; +pub(crate) use multi_line_summary_start::*; +pub(crate) use newline_after_last_paragraph::*; +pub(crate) use no_signature::*; +pub(crate) use no_surrounding_whitespace::*; +pub(crate) use non_imperative_mood::*; +pub(crate) use not_empty::*; +pub(crate) use not_missing::*; +pub(crate) use one_liner::*; +pub(crate) use sections::*; +pub(crate) use starts_with_this::*; +pub(crate) use triple_quotes::*; mod backslashes; mod blank_after_summary; diff --git a/crates/ruff/src/rules/pyflakes/rules/mod.rs b/crates/ruff/src/rules/pyflakes/rules/mod.rs index 3a66bb0e8f..8c21d78d24 100644 --- a/crates/ruff/src/rules/pyflakes/rules/mod.rs +++ b/crates/ruff/src/rules/pyflakes/rules/mod.rs @@ -1,49 +1,27 @@ -pub(crate) use assert_tuple::{assert_tuple, AssertTuple}; -pub(crate) use break_outside_loop::{break_outside_loop, BreakOutsideLoop}; -pub(crate) use continue_outside_loop::{continue_outside_loop, ContinueOutsideLoop}; -pub(crate) use default_except_not_last::{default_except_not_last, DefaultExceptNotLast}; -pub(crate) use f_string_missing_placeholders::{ - f_string_missing_placeholders, FStringMissingPlaceholders, -}; -pub(crate) use forward_annotation_syntax_error::ForwardAnnotationSyntaxError; -pub(crate) use future_feature_not_defined::{future_feature_not_defined, FutureFeatureNotDefined}; -pub(crate) use if_tuple::{if_tuple, IfTuple}; -pub(crate) use imports::{ - ImportShadowedByLoopVar, LateFutureImport, UndefinedLocalWithImportStar, - UndefinedLocalWithImportStarUsage, UndefinedLocalWithNestedImportStarUsage, -}; -pub(crate) use invalid_literal_comparisons::{invalid_literal_comparison, IsLiteral}; -pub(crate) use invalid_print_syntax::{invalid_print_syntax, InvalidPrintSyntax}; -pub(crate) use raise_not_implemented::{raise_not_implemented, RaiseNotImplemented}; -pub(crate) use redefined_while_unused::RedefinedWhileUnused; -pub(crate) use repeated_keys::{ - repeated_keys, MultiValueRepeatedKeyLiteral, MultiValueRepeatedKeyVariable, -}; -pub(crate) use return_outside_function::{return_outside_function, ReturnOutsideFunction}; -pub(crate) use starred_expressions::{ - starred_expressions, ExpressionsInStarAssignment, MultipleStarredExpressions, -}; -pub(crate) use strings::{ - percent_format_expected_mapping, percent_format_expected_sequence, - percent_format_extra_named_arguments, percent_format_missing_arguments, - percent_format_mixed_positional_and_named, percent_format_positional_count_mismatch, - percent_format_star_requires_sequence, string_dot_format_extra_named_arguments, - string_dot_format_extra_positional_arguments, string_dot_format_missing_argument, - string_dot_format_mixing_automatic, PercentFormatExpectedMapping, - PercentFormatExpectedSequence, PercentFormatExtraNamedArguments, PercentFormatInvalidFormat, - PercentFormatMissingArgument, PercentFormatMixedPositionalAndNamed, - PercentFormatPositionalCountMismatch, PercentFormatStarRequiresSequence, - PercentFormatUnsupportedFormatCharacter, StringDotFormatExtraNamedArguments, - StringDotFormatExtraPositionalArguments, StringDotFormatInvalidFormat, - StringDotFormatMissingArguments, StringDotFormatMixingAutomatic, -}; -pub(crate) use undefined_export::{undefined_export, UndefinedExport}; -pub(crate) use undefined_local::{undefined_local, UndefinedLocal}; -pub(crate) use undefined_name::UndefinedName; -pub(crate) use unused_annotation::{unused_annotation, UnusedAnnotation}; -pub(crate) use unused_import::{unused_import, UnusedImport}; -pub(crate) use unused_variable::{unused_variable, UnusedVariable}; -pub(crate) use yield_outside_function::{yield_outside_function, YieldOutsideFunction}; +pub(crate) use assert_tuple::*; +pub(crate) use break_outside_loop::*; +pub(crate) use continue_outside_loop::*; +pub(crate) use default_except_not_last::*; +pub(crate) use f_string_missing_placeholders::*; +pub(crate) use forward_annotation_syntax_error::*; +pub(crate) use future_feature_not_defined::*; +pub(crate) use if_tuple::*; +pub(crate) use imports::*; +pub(crate) use invalid_literal_comparisons::*; +pub(crate) use invalid_print_syntax::*; +pub(crate) use raise_not_implemented::*; +pub(crate) use redefined_while_unused::*; +pub(crate) use repeated_keys::*; +pub(crate) use return_outside_function::*; +pub(crate) use starred_expressions::*; +pub(crate) use strings::*; +pub(crate) use undefined_export::*; +pub(crate) use undefined_local::*; +pub(crate) use undefined_name::*; +pub(crate) use unused_annotation::*; +pub(crate) use unused_import::*; +pub(crate) use unused_variable::*; +pub(crate) use yield_outside_function::*; mod assert_tuple; mod break_outside_loop; diff --git a/crates/ruff/src/rules/pygrep_hooks/rules/mod.rs b/crates/ruff/src/rules/pygrep_hooks/rules/mod.rs index b8fbfb773c..1126d348de 100644 --- a/crates/ruff/src/rules/pygrep_hooks/rules/mod.rs +++ b/crates/ruff/src/rules/pygrep_hooks/rules/mod.rs @@ -1,10 +1,8 @@ -pub(crate) use blanket_noqa::{blanket_noqa, BlanketNOQA}; -pub(crate) use blanket_type_ignore::{blanket_type_ignore, BlanketTypeIgnore}; -pub(crate) use deprecated_log_warn::{deprecated_log_warn, DeprecatedLogWarn}; -pub(crate) use invalid_mock_access::{ - non_existent_mock_method, uncalled_mock_method, InvalidMockAccess, -}; -pub(crate) use no_eval::{no_eval, Eval}; +pub(crate) use blanket_noqa::*; +pub(crate) use blanket_type_ignore::*; +pub(crate) use deprecated_log_warn::*; +pub(crate) use invalid_mock_access::*; +pub(crate) use no_eval::*; mod blanket_noqa; mod blanket_type_ignore; diff --git a/crates/ruff/src/rules/pylint/rules/mod.rs b/crates/ruff/src/rules/pylint/rules/mod.rs index b254b79541..1047bad813 100644 --- a/crates/ruff/src/rules/pylint/rules/mod.rs +++ b/crates/ruff/src/rules/pylint/rules/mod.rs @@ -1,59 +1,48 @@ -pub(crate) use assert_on_string_literal::{assert_on_string_literal, AssertOnStringLiteral}; -pub(crate) use await_outside_async::{await_outside_async, AwaitOutsideAsync}; -pub(crate) use bad_str_strip_call::{bad_str_strip_call, BadStrStripCall}; -pub(crate) use bad_string_format_type::{bad_string_format_type, BadStringFormatType}; -pub(crate) use bidirectional_unicode::{bidirectional_unicode, BidirectionalUnicode}; -pub(crate) use binary_op_exception::{binary_op_exception, BinaryOpException}; -pub(crate) use collapsible_else_if::{collapsible_else_if, CollapsibleElseIf}; -pub(crate) use compare_to_empty_string::{compare_to_empty_string, CompareToEmptyString}; -pub(crate) use comparison_of_constant::{comparison_of_constant, ComparisonOfConstant}; -pub(crate) use comparison_with_itself::{comparison_with_itself, ComparisonWithItself}; -pub(crate) use continue_in_finally::{continue_in_finally, ContinueInFinally}; -pub(crate) use duplicate_bases::{duplicate_bases, DuplicateBases}; -pub(crate) use global_statement::{global_statement, GlobalStatement}; +pub(crate) use assert_on_string_literal::*; +pub(crate) use await_outside_async::*; +pub(crate) use bad_str_strip_call::*; +pub(crate) use bad_string_format_type::*; +pub(crate) use bidirectional_unicode::*; +pub(crate) use binary_op_exception::*; +pub(crate) use collapsible_else_if::*; +pub(crate) use compare_to_empty_string::*; +pub(crate) use comparison_of_constant::*; +pub(crate) use comparison_with_itself::*; +pub(crate) use continue_in_finally::*; +pub(crate) use duplicate_bases::*; +pub(crate) use global_statement::*; pub(crate) use global_variable_not_assigned::GlobalVariableNotAssigned; -pub(crate) use import_self::{import_from_self, import_self, ImportSelf}; -pub(crate) use invalid_all_format::{invalid_all_format, InvalidAllFormat}; -pub(crate) use invalid_all_object::{invalid_all_object, InvalidAllObject}; -pub(crate) use invalid_envvar_default::{invalid_envvar_default, InvalidEnvvarDefault}; -pub(crate) use invalid_envvar_value::{invalid_envvar_value, InvalidEnvvarValue}; -pub(crate) use invalid_str_return::{invalid_str_return, InvalidStrReturnType}; -pub(crate) use invalid_string_characters::{ - invalid_string_characters, InvalidCharacterBackspace, InvalidCharacterEsc, InvalidCharacterNul, - InvalidCharacterSub, InvalidCharacterZeroWidthSpace, -}; -pub(crate) use iteration_over_set::{iteration_over_set, IterationOverSet}; -pub(crate) use load_before_global_declaration::{ - load_before_global_declaration, LoadBeforeGlobalDeclaration, -}; -pub(crate) use logging::{logging_call, LoggingTooFewArgs, LoggingTooManyArgs}; -pub(crate) use magic_value_comparison::{magic_value_comparison, MagicValueComparison}; -pub(crate) use manual_import_from::{manual_from_import, ManualFromImport}; -pub(crate) use named_expr_without_context::{named_expr_without_context, NamedExprWithoutContext}; -pub(crate) use nested_min_max::{nested_min_max, NestedMinMax}; +pub(crate) use import_self::*; +pub(crate) use invalid_all_format::*; +pub(crate) use invalid_all_object::*; +pub(crate) use invalid_envvar_default::*; +pub(crate) use invalid_envvar_value::*; +pub(crate) use invalid_str_return::*; +pub(crate) use invalid_string_characters::*; +pub(crate) use iteration_over_set::*; +pub(crate) use load_before_global_declaration::*; +pub(crate) use logging::*; +pub(crate) use magic_value_comparison::*; +pub(crate) use manual_import_from::*; +pub(crate) use named_expr_without_context::*; +pub(crate) use nested_min_max::*; pub(crate) use nonlocal_without_binding::NonlocalWithoutBinding; -pub(crate) use property_with_parameters::{property_with_parameters, PropertyWithParameters}; -pub(crate) use redefined_loop_name::{redefined_loop_name, RedefinedLoopName}; -pub(crate) use repeated_isinstance_calls::{repeated_isinstance_calls, RepeatedIsinstanceCalls}; -pub(crate) use return_in_init::{return_in_init, ReturnInInit}; -pub(crate) use sys_exit_alias::{sys_exit_alias, SysExitAlias}; -pub(crate) use too_many_arguments::{too_many_arguments, TooManyArguments}; -pub(crate) use too_many_branches::{too_many_branches, TooManyBranches}; -pub(crate) use too_many_return_statements::{too_many_return_statements, TooManyReturnStatements}; -pub(crate) use too_many_statements::{too_many_statements, TooManyStatements}; -pub(crate) use unexpected_special_method_signature::{ - unexpected_special_method_signature, UnexpectedSpecialMethodSignature, -}; -pub(crate) use unnecessary_direct_lambda_call::{ - unnecessary_direct_lambda_call, UnnecessaryDirectLambdaCall, -}; -pub(crate) use useless_else_on_loop::{useless_else_on_loop, UselessElseOnLoop}; -pub(crate) use useless_import_alias::{useless_import_alias, UselessImportAlias}; -pub(crate) use useless_return::{useless_return, UselessReturn}; -pub(crate) use yield_from_in_async_function::{ - yield_from_in_async_function, YieldFromInAsyncFunction, -}; -pub(crate) use yield_in_init::{yield_in_init, YieldInInit}; +pub(crate) use property_with_parameters::*; +pub(crate) use redefined_loop_name::*; +pub(crate) use repeated_isinstance_calls::*; +pub(crate) use return_in_init::*; +pub(crate) use sys_exit_alias::*; +pub(crate) use too_many_arguments::*; +pub(crate) use too_many_branches::*; +pub(crate) use too_many_return_statements::*; +pub(crate) use too_many_statements::*; +pub(crate) use unexpected_special_method_signature::*; +pub(crate) use unnecessary_direct_lambda_call::*; +pub(crate) use useless_else_on_loop::*; +pub(crate) use useless_import_alias::*; +pub(crate) use useless_return::*; +pub(crate) use yield_from_in_async_function::*; +pub(crate) use yield_in_init::*; mod assert_on_string_literal; mod await_outside_async; diff --git a/crates/ruff/src/rules/pyupgrade/rules/mod.rs b/crates/ruff/src/rules/pyupgrade/rules/mod.rs index 2a09b35d65..f8ac097ca7 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/mod.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/mod.rs @@ -1,53 +1,39 @@ -pub(crate) use convert_named_tuple_functional_to_class::{ - convert_named_tuple_functional_to_class, ConvertNamedTupleFunctionalToClass, -}; -pub(crate) use convert_typed_dict_functional_to_class::{ - convert_typed_dict_functional_to_class, ConvertTypedDictFunctionalToClass, -}; -pub(crate) use datetime_utc_alias::{datetime_utc_alias, DatetimeTimezoneUTC}; -pub(crate) use deprecated_c_element_tree::{deprecated_c_element_tree, DeprecatedCElementTree}; -pub(crate) use deprecated_import::{deprecated_import, DeprecatedImport}; -pub(crate) use deprecated_mock_import::{ - deprecated_mock_attribute, deprecated_mock_import, DeprecatedMockImport, -}; -pub(crate) use deprecated_unittest_alias::{deprecated_unittest_alias, DeprecatedUnittestAlias}; -pub(crate) use extraneous_parentheses::{extraneous_parentheses, ExtraneousParentheses}; -pub(crate) use f_strings::{f_strings, FString}; -pub(crate) use format_literals::{format_literals, FormatLiterals}; -pub(crate) use lru_cache_with_maxsize_none::{ - lru_cache_with_maxsize_none, LRUCacheWithMaxsizeNone, -}; -pub(crate) use lru_cache_without_parameters::{ - lru_cache_without_parameters, LRUCacheWithoutParameters, -}; -pub(crate) use native_literals::{native_literals, NativeLiterals}; -pub(crate) use open_alias::{open_alias, OpenAlias}; -pub(crate) use os_error_alias::{ - os_error_alias_call, os_error_alias_handlers, os_error_alias_raise, OSErrorAlias, -}; -pub(crate) use outdated_version_block::{outdated_version_block, OutdatedVersionBlock}; -pub(crate) use printf_string_formatting::{printf_string_formatting, PrintfStringFormatting}; -pub(crate) use quoted_annotation::{quoted_annotation, QuotedAnnotation}; -pub(crate) use redundant_open_modes::{redundant_open_modes, RedundantOpenModes}; -pub(crate) use replace_stdout_stderr::{replace_stdout_stderr, ReplaceStdoutStderr}; -pub(crate) use replace_universal_newlines::{replace_universal_newlines, ReplaceUniversalNewlines}; -pub(crate) use super_call_with_parameters::{super_call_with_parameters, SuperCallWithParameters}; -pub(crate) use type_of_primitive::{type_of_primitive, TypeOfPrimitive}; -pub(crate) use typing_text_str_alias::{typing_text_str_alias, TypingTextStrAlias}; -pub(crate) use unicode_kind_prefix::{unicode_kind_prefix, UnicodeKindPrefix}; -pub(crate) use unnecessary_builtin_import::{unnecessary_builtin_import, UnnecessaryBuiltinImport}; -pub(crate) use unnecessary_coding_comment::{unnecessary_coding_comment, UTF8EncodingDeclaration}; -pub(crate) use unnecessary_encode_utf8::{unnecessary_encode_utf8, UnnecessaryEncodeUTF8}; -pub(crate) use unnecessary_future_import::{unnecessary_future_import, UnnecessaryFutureImport}; -pub(crate) use unpacked_list_comprehension::{ - unpacked_list_comprehension, UnpackedListComprehension, -}; -pub(crate) use use_pep585_annotation::{use_pep585_annotation, NonPEP585Annotation}; -pub(crate) use use_pep604_annotation::{use_pep604_annotation, NonPEP604Annotation}; -pub(crate) use use_pep604_isinstance::{use_pep604_isinstance, NonPEP604Isinstance}; -pub(crate) use useless_metaclass_type::{useless_metaclass_type, UselessMetaclassType}; -pub(crate) use useless_object_inheritance::{useless_object_inheritance, UselessObjectInheritance}; -pub(crate) use yield_in_for_loop::{yield_in_for_loop, YieldInForLoop}; +pub(crate) use convert_named_tuple_functional_to_class::*; +pub(crate) use convert_typed_dict_functional_to_class::*; +pub(crate) use datetime_utc_alias::*; +pub(crate) use deprecated_c_element_tree::*; +pub(crate) use deprecated_import::*; +pub(crate) use deprecated_mock_import::*; +pub(crate) use deprecated_unittest_alias::*; +pub(crate) use extraneous_parentheses::*; +pub(crate) use f_strings::*; +pub(crate) use format_literals::*; +pub(crate) use lru_cache_with_maxsize_none::*; +pub(crate) use lru_cache_without_parameters::*; +pub(crate) use native_literals::*; +pub(crate) use open_alias::*; +pub(crate) use os_error_alias::*; +pub(crate) use outdated_version_block::*; +pub(crate) use printf_string_formatting::*; +pub(crate) use quoted_annotation::*; +pub(crate) use redundant_open_modes::*; +pub(crate) use replace_stdout_stderr::*; +pub(crate) use replace_universal_newlines::*; +pub(crate) use super_call_with_parameters::*; +pub(crate) use type_of_primitive::*; +pub(crate) use typing_text_str_alias::*; +pub(crate) use unicode_kind_prefix::*; +pub(crate) use unnecessary_builtin_import::*; +pub(crate) use unnecessary_coding_comment::*; +pub(crate) use unnecessary_encode_utf8::*; +pub(crate) use unnecessary_future_import::*; +pub(crate) use unpacked_list_comprehension::*; +pub(crate) use use_pep585_annotation::*; +pub(crate) use use_pep604_annotation::*; +pub(crate) use use_pep604_isinstance::*; +pub(crate) use useless_metaclass_type::*; +pub(crate) use useless_object_inheritance::*; +pub(crate) use yield_in_for_loop::*; mod convert_named_tuple_functional_to_class; mod convert_typed_dict_functional_to_class; diff --git a/crates/ruff/src/rules/ruff/rules/mod.rs b/crates/ruff/src/rules/ruff/rules/mod.rs index 4409d034d2..9835966ece 100644 --- a/crates/ruff/src/rules/ruff/rules/mod.rs +++ b/crates/ruff/src/rules/ruff/rules/mod.rs @@ -1,25 +1,13 @@ -pub(crate) use ambiguous_unicode_character::{ - ambiguous_unicode_character, AmbiguousUnicodeCharacterComment, - AmbiguousUnicodeCharacterDocstring, AmbiguousUnicodeCharacterString, -}; -pub(crate) use asyncio_dangling_task::{asyncio_dangling_task, AsyncioDanglingTask}; -pub(crate) use collection_literal_concatenation::{ - collection_literal_concatenation, CollectionLiteralConcatenation, -}; -pub(crate) use explicit_f_string_type_conversion::{ - explicit_f_string_type_conversion, ExplicitFStringTypeConversion, -}; +pub(crate) use ambiguous_unicode_character::*; +pub(crate) use asyncio_dangling_task::*; +pub(crate) use collection_literal_concatenation::*; +pub(crate) use explicit_f_string_type_conversion::*; pub(crate) use invalid_pyproject_toml::InvalidPyprojectToml; -pub(crate) use mutable_defaults_in_dataclass_fields::{ - function_call_in_dataclass_defaults, is_dataclass, mutable_dataclass_default, - FunctionCallInDataclassDefaultArgument, MutableDataclassDefault, -}; -pub(crate) use pairwise_over_zipped::{pairwise_over_zipped, PairwiseOverZipped}; -pub(crate) use unused_noqa::{UnusedCodes, UnusedNOQA}; +pub(crate) use mutable_defaults_in_dataclass_fields::*; +pub(crate) use pairwise_over_zipped::*; +pub(crate) use unused_noqa::*; -pub(crate) use static_key_dict_comprehension::{ - static_key_dict_comprehension, StaticKeyDictComprehension, -}; +pub(crate) use static_key_dict_comprehension::*; mod ambiguous_unicode_character; mod asyncio_dangling_task; diff --git a/crates/ruff/src/rules/tryceratops/rules/mod.rs b/crates/ruff/src/rules/tryceratops/rules/mod.rs index c833503d30..9d004969c0 100644 --- a/crates/ruff/src/rules/tryceratops/rules/mod.rs +++ b/crates/ruff/src/rules/tryceratops/rules/mod.rs @@ -1,15 +1,13 @@ -pub(crate) use error_instead_of_exception::{error_instead_of_exception, ErrorInsteadOfException}; -pub(crate) use raise_vanilla_args::{raise_vanilla_args, RaiseVanillaArgs}; -pub(crate) use raise_vanilla_class::{raise_vanilla_class, RaiseVanillaClass}; -pub(crate) use raise_within_try::{raise_within_try, RaiseWithinTry}; -pub(crate) use reraise_no_cause::{reraise_no_cause, ReraiseNoCause}; -pub(crate) use try_consider_else::{try_consider_else, TryConsiderElse}; -pub(crate) use type_check_without_type_error::{ - type_check_without_type_error, TypeCheckWithoutTypeError, -}; -pub(crate) use useless_try_except::{useless_try_except, UselessTryExcept}; -pub(crate) use verbose_log_message::{verbose_log_message, VerboseLogMessage}; -pub(crate) use verbose_raise::{verbose_raise, VerboseRaise}; +pub(crate) use error_instead_of_exception::*; +pub(crate) use raise_vanilla_args::*; +pub(crate) use raise_vanilla_class::*; +pub(crate) use raise_within_try::*; +pub(crate) use reraise_no_cause::*; +pub(crate) use try_consider_else::*; +pub(crate) use type_check_without_type_error::*; +pub(crate) use useless_try_except::*; +pub(crate) use verbose_log_message::*; +pub(crate) use verbose_raise::*; mod error_instead_of_exception; mod raise_vanilla_args; diff --git a/scripts/add_rule.py b/scripts/add_rule.py index 3d1eb14c33..43bfddfade 100755 --- a/scripts/add_rule.py +++ b/scripts/add_rule.py @@ -70,7 +70,7 @@ def main(*, name: str, prefix: str, code: str, linter: str) -> None: contents = rules_mod.read_text() parts = contents.split("\n\n") - new_pub_use = f"pub(crate) use {rule_name_snake}::{{{rule_name_snake}, {name}}}" + new_pub_use = f"pub(crate) use {rule_name_snake}::*" new_mod = f"mod {rule_name_snake};" if len(parts) == 2: From 81617572292a2c0a1a60ee5135d2f9cfe6b6f336 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Mon, 12 Jun 2023 13:20:16 +0200 Subject: [PATCH 008/447] [flake8-pyi] Implement PYI044 (#5021) ## Summary This implements PYI044. This rule checks if `from __future__ import annotations` is used in stub files as it has no effect in stub files, since type checkers automatically treat stubs as having those semantics. Updates https://github.com/astral-sh/ruff/issues/848 ## Test Plan Added a test case and snapshots. --- .../test/fixtures/flake8_pyi/PYI044.py | 7 ++++ .../test/fixtures/flake8_pyi/PYI044.pyi | 7 ++++ crates/ruff/src/checkers/ast/mod.rs | 3 ++ crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/flake8_pyi/mod.rs | 2 ++ .../rules/future_annotations_in_stub.rs | 33 +++++++++++++++++++ crates/ruff/src/rules/flake8_pyi/rules/mod.rs | 2 ++ ...__flake8_pyi__tests__PYI044_PYI044.py.snap | 4 +++ ..._flake8_pyi__tests__PYI044_PYI044.pyi.snap | 13 ++++++++ ruff.schema.json | 1 + 10 files changed, 73 insertions(+) create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.pyi create mode 100644 crates/ruff/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.py.snap create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.pyi.snap diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.py new file mode 100644 index 0000000000..6439df7f6d --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.py @@ -0,0 +1,7 @@ +# Bad import. +from __future__ import annotations # Not PYI044 (not a stubfile). + +# Good imports. +from __future__ import Something +import sys +from socket import AF_INET diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.pyi new file mode 100644 index 0000000000..18018deee6 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.pyi @@ -0,0 +1,7 @@ +# Bad import. +from __future__ import annotations # PYI044. + +# Good imports. +from __future__ import Something +import sys +from socket import AF_INET diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 6334ca31ef..57c497456d 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1121,6 +1121,9 @@ where if self.enabled(Rule::UnaliasedCollectionsAbcSetImport) { flake8_pyi::rules::unaliased_collections_abc_set_import(self, import_from); } + if self.enabled(Rule::FutureAnnotationsInStub) { + flake8_pyi::rules::from_future_import(self, import_from); + } } for alias in names { if let Some("__future__") = module { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 911fe85ebc..962190fbbd 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -616,6 +616,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Pyi, "035") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnassignedSpecialVariableInStub), (Flake8Pyi, "042") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::SnakeCaseTypeAlias), (Flake8Pyi, "043") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::TSuffixedTypeAlias), + (Flake8Pyi, "044") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::FutureAnnotationsInStub), (Flake8Pyi, "045") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::IterMethodReturnIterable), (Flake8Pyi, "048") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::StubBodyMultipleStatements), (Flake8Pyi, "050") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::NoReturnArgumentAnnotationInStub), diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index 783018fba8..b790c3c77e 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -54,6 +54,8 @@ mod tests { #[test_case(Rule::StubBodyMultipleStatements, Path::new("PYI048.pyi"))] #[test_case(Rule::TSuffixedTypeAlias, Path::new("PYI043.py"))] #[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::TypeCommentInStub, Path::new("PYI033.py"))] #[test_case(Rule::TypeCommentInStub, Path::new("PYI033.pyi"))] #[test_case(Rule::TypedArgumentDefaultInStub, Path::new("PYI011.py"))] diff --git a/crates/ruff/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs new file mode 100644 index 0000000000..2968980034 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs @@ -0,0 +1,33 @@ +use rustpython_parser::ast::StmtImportFrom; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; + +#[violation] +pub struct FutureAnnotationsInStub; + +impl Violation for FutureAnnotationsInStub { + #[derive_message_formats] + fn message(&self) -> String { + format!("`from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics") + } +} + +/// PYI044 +pub(crate) fn from_future_import(checker: &mut Checker, target: &StmtImportFrom) { + if let StmtImportFrom { + range, + module: Some(name), + names, + .. + } = target + { + if name == "__future__" && names.iter().any(|alias| &*alias.name == "annotations") { + checker + .diagnostics + .push(Diagnostic::new(FutureAnnotationsInStub, *range)); + } + } +} diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index ea3a26a1ad..6286ebdca3 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -4,6 +4,7 @@ pub(crate) use collections_named_tuple::*; pub(crate) use docstring_in_stubs::*; pub(crate) use duplicate_union_member::*; pub(crate) use ellipsis_in_non_empty_class_body::*; +pub(crate) use future_annotations_in_stub::*; pub(crate) use iter_method_return_iterable::*; pub(crate) use no_return_argument_annotation::*; pub(crate) use non_empty_stub_body::*; @@ -28,6 +29,7 @@ mod collections_named_tuple; mod docstring_in_stubs; mod duplicate_union_member; mod ellipsis_in_non_empty_class_body; +mod future_annotations_in_stub; mod iter_method_return_iterable; mod no_return_argument_annotation; mod non_empty_stub_body; diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.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__PYI044_PYI044.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.pyi.snap new file mode 100644 index 0000000000..5e52d03969 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.pyi.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI044.pyi:2:1: PYI044 `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics + | +1 | # Bad import. +2 | from __future__ import annotations # PYI044. + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI044 +3 | +4 | # Good imports. + | + + diff --git a/ruff.schema.json b/ruff.schema.json index e23fad53f2..f2190aecf4 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2304,6 +2304,7 @@ "PYI04", "PYI042", "PYI043", + "PYI044", "PYI045", "PYI048", "PYI05", From e586c2759045c17c55e63903e9310472ef97dc4e Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 12 Jun 2023 14:55:47 +0200 Subject: [PATCH 009/447] Format ExprTuple (#4963) This implements formatting ExprTuple, including magic trailing comma. I intentionally didn't change the settings mechanism but just added a dummy global const flag. Besides the snapshots, I added custom breaking/joining tests and a deeply nested test case. The diffs look better than previously, proper black compatibility depends on parentheses handling. --------- Co-authored-by: Micha Reiser --- .../ruff/expression/tuple_expression.py | 59 ++++ .../src/expression/expr_tuple.rs | 149 +++++++++- crates/ruff_python_formatter/src/lib.rs | 4 + ...er__tests__black_test__collections_py.snap | 40 ++- ...tter__tests__black_test__comments6_py.snap | 23 +- ..._test__comments_non_breaking_space_py.snap | 5 +- ...ter__tests__black_test__expression_py.snap | 85 +++--- ...tter__tests__black_test__fmtonoff2_py.snap | 8 +- ...atter__tests__black_test__fmtonoff_py.snap | 22 +- ...atter__tests__black_test__fmtskip3_py.snap | 4 +- ...atter__tests__black_test__fmtskip5_py.snap | 5 +- ...matter__tests__black_test__fmtskip_py.snap | 5 +- ...atter__tests__black_test__function_py.snap | 9 +- ...lack_test__function_trailing_comma_py.snap | 27 +- ...ests__black_test__power_op_spacing_py.snap | 26 +- ...test__prefer_rhs_split_reformatted_py.snap | 26 +- ...__tests__black_test__remove_parens_py.snap | 9 +- ...ck_test__skip_magic_trailing_comma_py.snap | 21 +- ...rmatter__tests__black_test__slices_py.snap | 34 ++- ...tests__black_test__string_prefixes_py.snap | 62 +++- ...matter__tests__black_test__torture_py.snap | 8 +- ...__trailing_commas_in_leading_parts_py.snap | 21 +- ...er__tests__black_test__tupleassign_py.snap | 28 +- ...est__expression__binary_expression_py.snap | 7 +- ...test__expression__tuple_expression_py.snap | 267 ++++++++++++++++++ 25 files changed, 735 insertions(+), 219 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tuple_expression.py create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__tuple_expression_py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tuple_expression.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tuple_expression.py new file mode 100644 index 0000000000..517f5d39e7 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tuple_expression.py @@ -0,0 +1,59 @@ +# Non-wrapping parentheses checks +a1 = 1, 2 +a2 = (1, 2) +a3 = (1, 2), 3 +a4 = ((1, 2), 3) + +# Wrapping parentheses checks +b1 = (("Michael", "Ende"), ("Der", "satanarchäolügenialkohöllische", "Wunschpunsch"), ("Beelzebub", "Irrwitzer"), ("Tyrannja", "Vamperl"),) +b2 = ("akjdshflkjahdslkfjlasfdahjlfds", "ljklsadhflakfdajflahfdlajfhafldkjalfj", "ljklsadhflakfdajflahfdlajfhafldkjalf2",) +b3 = ("The", "Night", "of", "Wishes:", "Or", "the", "Satanarchaeolidealcohellish", "Notion", "Potion",) + +# Nested wrapping parentheses check +c1 = (("cicero"), ("Qui", "autem,", "si", "maxime", "hoc", "placeat,", "moderatius", "tamen", "id", "uolunt", "fieri,", "difficilem", "quandam", "temperantiam", "postulant", "in", "eo,", "quod", "semel", "admissum", "coerceri", "reprimique", "non", "potest,", "ut", "propemodum", "iustioribus", "utamur", "illis,", "qui", "omnino", "auocent", "a", "philosophia,", "quam", "his,", "qui", "rebus", "infinitis", "modum", "constituant", "in", "reque", "eo", "meliore,", "quo", "maior", "sit,", "mediocritatem", "desiderent."), ("de", "finibus", "bonorum", "et", "malorum")) + +# Deeply nested parentheses +d1 = ((("3D",),),) +d2 = (((((((((((((((((((((((((((("¯\_(ツ)_/¯",),),),),),),),),),),),),),),),),),),),),),),),),),),),) + +# Join and magic trailing comma +e1 = ( + 1, + 2 +) +e2 = ( + 1, + 2, +) +e3 = ( + 1, +) +e4 = ( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor", + "incididunt" +) + +# Empty tuples and comments +f1 = ( + # empty +) +f2 = () + +# Comments in other tuples +g1 = ( # a + # b + 1, # c + # d +) # e +g2 = ( # a + # b + 1, # c + # d + 2, # e + # f +) # g + +# Ensure the correct number of parentheses +h1 = ((((1, 2)))) +h2 = ((((1, "qweiurpoiqwurepqiurpqirpuqoiwrupqoirupqoirupqoiurpqiorupwqiourpqurpqurpqurpqurpqurpqurüqurqpuriq")))) +h3 = 1, "qweiurpoiqwurepqiurpqirpuqoiwrupqoirupqoirupqoiurpqiorupwqiourpqurpqurpqurpqurpqurpqurüqurqpuriq" diff --git a/crates/ruff_python_formatter/src/expression/expr_tuple.rs b/crates/ruff_python_formatter/src/expression/expr_tuple.rs index ba96180a15..e7d1ce0865 100644 --- a/crates/ruff_python_formatter/src/expression/expr_tuple.rs +++ b/crates/ruff_python_formatter/src/expression/expr_tuple.rs @@ -1,17 +1,130 @@ -use crate::comments::Comments; +use crate::comments::{dangling_node_comments, Comments}; +use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; -use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::trivia::Token; +use crate::trivia::{first_non_trivia_token, TokenKind}; +use crate::{AsFormat, FormatNodeRule, FormattedIterExt, PyFormatter, USE_MAGIC_TRAILING_COMMA}; +use ruff_formatter::formatter::Formatter; +use ruff_formatter::prelude::{ + block_indent, group, if_group_breaks, soft_block_indent, soft_line_break_or_space, text, +}; +use ruff_formatter::{format_args, write, Buffer, Format, FormatResult}; +use ruff_python_ast::prelude::{Expr, Ranged}; +use ruff_text_size::TextRange; use rustpython_parser::ast::ExprTuple; #[derive(Default)] pub struct FormatExprTuple; impl FormatNodeRule for FormatExprTuple { - fn fmt_fields(&self, _item: &ExprTuple, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented_custom_text("(1, 2)")]) + fn fmt_fields(&self, item: &ExprTuple, f: &mut PyFormatter) -> FormatResult<()> { + let ExprTuple { + range, + elts, + ctx: _, + } = item; + + // Handle the edge cases of an empty tuple and a tuple with one element + let last = match &elts[..] { + [] => { + return write!( + f, + [ + // An empty tuple always needs parentheses, but does not have a comma + &text("("), + block_indent(&dangling_node_comments(item)), + &text(")"), + ] + ); + } + [single] => { + return write!( + f, + [group(&format_args![ + // A single element tuple always needs parentheses and a trailing comma + &text("("), + soft_block_indent(&format_args![single.format(), &text(",")]), + &text(")"), + ])] + ); + } + [.., last] => last, + }; + + let magic_trailing_comma = USE_MAGIC_TRAILING_COMMA + && matches!( + first_non_trivia_token(last.range().end(), f.context().contents()), + Some(Token { + kind: TokenKind::Comma, + .. + }) + ); + + if magic_trailing_comma { + // A magic trailing comma forces us to print in expanded mode since we have more than + // one element + write!( + f, + [ + // An expanded group always needs parentheses + &text("("), + block_indent(&ExprSequence::new(elts)), + &text(")"), + ] + )?; + } else if is_parenthesized(*range, elts, f) { + // If the tuple has parentheses, keep them. Note that unlike other expr parentheses, + // those are actually part of the range + write!( + f, + [group(&format_args![ + // If there were previously parentheses, keep them + &text("("), + soft_block_indent(&ExprSequence::new(elts)), + &text(")"), + ])] + )?; + } else { + write!( + f, + [group(&format_args![ + // If there were previously no parentheses, add them only if the group breaks + if_group_breaks(&text("(")), + soft_block_indent(&ExprSequence::new(elts)), + if_group_breaks(&text(")")), + ])] + )?; + } + + Ok(()) + } + + fn fmt_dangling_comments(&self, _node: &ExprTuple, _f: &mut PyFormatter) -> FormatResult<()> { + // Handled in `fmt_fields` + Ok(()) + } +} + +#[derive(Debug)] +struct ExprSequence<'a> { + elts: &'a [Expr], +} + +impl<'a> ExprSequence<'a> { + const fn new(elts: &'a [Expr]) -> Self { + Self { elts } + } +} + +impl Format> for ExprSequence<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + f.join_with(&format_args!(text(","), soft_line_break_or_space())) + .entries(self.elts.iter().formatted()) + .finish()?; + // Black style has a trailing comma on the last entry of an expanded group + write!(f, [if_group_breaks(&text(","))]) } } @@ -28,3 +141,29 @@ impl NeedsParentheses for ExprTuple { } } } + +/// Check if a tuple has already had parentheses in the input +fn is_parenthesized( + tuple_range: TextRange, + elts: &[Expr], + f: &mut Formatter>, +) -> bool { + let parentheses = '('; + let first_char = &f.context().contents()[usize::from(tuple_range.start())..] + .chars() + .next(); + let Some(first_char) = first_char else { + return false; + }; + if *first_char != parentheses { + return false; + } + + // Consider `a = (1, 2), 3`: The first char of the current expr starts is a parentheses, but + // it's not its own but that of its first tuple child. We know that it belongs to the child + // because if it wouldn't, the child would start (at least) a char later + let Some(first_child) = elts.first() else { + return false; + }; + first_child.range().start() != tuple_range.start() +} diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 51d03c375b..6ad37c5665 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -30,6 +30,10 @@ mod trivia; include!("../../ruff_formatter/shared_traits.rs"); +/// TODO(konstin): hook this up to the settings by replacing `SimpleFormatOptions` with a python +/// specific struct. +pub(crate) const USE_MAGIC_TRAILING_COMMA: bool = true; + /// 'ast is the lifetime of the source code (input), 'buf is the lifetime of the buffer (output) pub(crate) type PyFormatter<'ast, 'buf> = Formatter<'buf, PyFormatContext<'ast>>; diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap index 16be7db816..6a6c2179d6 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap @@ -122,28 +122,25 @@ if True: - 2, - 3, -} --x = (1,) ++c = {1, 2, 3} + x = (1,) -y = (narf(),) -nested = { - (1, 2, 3), - (4, 5, 6), -} --nested_no_trailing_comma = {(1, 2, 3), (4, 5, 6)} -+c = {1, 2, 3} -+x = (1, 2) -+y = (1, 2) -+nested = {(1, 2), (1, 2)} -+nested_no_trailing_comma = {(1, 2), (1, 2)} ++y = (NOT_IMPLEMENTED_call(),) ++nested = {(1, 2, 3), (4, 5, 6)} + nested_no_trailing_comma = {(1, 2, 3), (4, 5, 6)} nested_long_lines = [ - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "cccccccccccccccccccccccccccccccccccccccc", -- (1, 2, 3), ++ "NOT_YET_IMPLEMENTED_STRING", ++ "NOT_YET_IMPLEMENTED_STRING", ++ "NOT_YET_IMPLEMENTED_STRING", + (1, 2, 3), - "dddddddddddddddddddddddddddddddddddddddd", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ (1, 2), + "NOT_YET_IMPLEMENTED_STRING", ] -{ @@ -161,7 +158,7 @@ if True: -) +{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} +{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -+["NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING" % (1, 2)] ++["NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING" % (foo,)] +x = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} +y = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} +NOT_YET_IMPLEMENTED_StmtAssert @@ -181,9 +178,8 @@ if True: -] +[1, 2, 3] --division_result_tuple = (6 / 2,) + division_result_tuple = (6 / 2,) -print("foo %r", (foo.bar,)) -+division_result_tuple = (1, 2) +NOT_IMPLEMENTED_call() if True: @@ -238,20 +234,20 @@ NOT_YET_IMPLEMENTED_StmtImportFrom a = {1, 2, 3} b = {1, 2, 3} c = {1, 2, 3} -x = (1, 2) -y = (1, 2) -nested = {(1, 2), (1, 2)} -nested_no_trailing_comma = {(1, 2), (1, 2)} +x = (1,) +y = (NOT_IMPLEMENTED_call(),) +nested = {(1, 2, 3), (4, 5, 6)} +nested_no_trailing_comma = {(1, 2, 3), (4, 5, 6)} nested_long_lines = [ "NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING", - (1, 2), + (1, 2, 3), "NOT_YET_IMPLEMENTED_STRING", ] {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -["NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING" % (1, 2)] +["NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING" % (foo,)] x = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} y = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} NOT_YET_IMPLEMENTED_StmtAssert @@ -262,7 +258,7 @@ NOT_YET_IMPLEMENTED_StmtFor [1, 2, 3] -division_result_tuple = (1, 2) +division_result_tuple = (6 / 2,) NOT_IMPLEMENTED_call() if True: diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap index 3009fba804..430431fe04 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap @@ -137,7 +137,7 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite def f( -@@ -49,15 +49,10 @@ +@@ -49,10 +49,8 @@ element = 0 # type: int another_element = 1 # type: float another_element_with_long_name = 2 # type: int @@ -148,15 +148,9 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite + another_really_really_long_element_with_a_unnecessarily_long_name_to_describe_what_it_does_enterprise_style = 3 # type: int + an_element_with_a_long_value = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 # type: bool -- tup = ( -- another_element, -- another_really_really_long_element_with_a_unnecessarily_long_name_to_describe_what_it_does_enterprise_style, -- ) # type: Tuple[int, int] -+ tup = (1, 2) # type: Tuple[int, int] - - a = ( - element -@@ -84,35 +79,22 @@ + tup = ( + another_element, +@@ -84,35 +82,22 @@ def func( @@ -203,7 +197,7 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite +NOT_IMPLEMENTED_call() -aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type] -+(1, 2) = NOT_IMPLEMENTED_call() # type: ignore[arg-type] ++aaaaaaaaaaaaa, bbbbbbbbb = NOT_IMPLEMENTED_call() # type: ignore[arg-type] ``` ## Ruff Output @@ -263,7 +257,10 @@ def f( another_really_really_long_element_with_a_unnecessarily_long_name_to_describe_what_it_does_enterprise_style = 3 # type: int an_element_with_a_long_value = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 # type: bool - tup = (1, 2) # type: Tuple[int, int] + tup = ( + another_element, + another_really_really_long_element_with_a_unnecessarily_long_name_to_describe_what_it_does_enterprise_style, + ) # type: Tuple[int, int] a = ( element @@ -308,7 +305,7 @@ AAAAAAAAAAAAA = ( NOT_IMPLEMENTED_call() -(1, 2) = NOT_IMPLEMENTED_call() # type: ignore[arg-type] +aaaaaaaaaaaaa, bbbbbbbbb = NOT_IMPLEMENTED_call() # type: ignore[arg-type] ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap index cddc816cdb..daf0c71385 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap @@ -42,8 +42,7 @@ def function(a:int=42): +NOT_YET_IMPLEMENTED_StmtImportFrom result = 1 # A simple comment --result = (1,) # Another one -+result = (1, 2) # Another one + result = (1,) # Another one result = 1 #  type: ignore result = 1 # This comment is talking about type: ignore @@ -68,7 +67,7 @@ def function(a:int=42): NOT_YET_IMPLEMENTED_StmtImportFrom result = 1 # A simple comment -result = (1, 2) # Another one +result = (1,) # Another one result = 1 #  type: ignore result = 1 # This comment is talking about type: ignore diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap index 5733bbfdd7..b2cc26d4f2 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap @@ -342,8 +342,6 @@ last_call() -{**a, **b, **c} -{"2.7", "3.6", "3.7", "3.8", "3.9", ("4.0" if gilectomy else "3.10")} -({"a": "b"}, (True or False), (+value), "string", b"bytes") or None --() --(1,) +NOT_YET_IMPLEMENTED_ExprUnaryOp +NOT_YET_IMPLEMENTED_ExprUnaryOp +NOT_YET_IMPLEMENTED_ExprUnaryOp @@ -377,11 +375,10 @@ last_call() + (NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false), +} +NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+(1, 2) -+(1, 2) -+(1, 2) + () + (1,) (1, 2) --(1, 2, 3) + (1, 2, 3) [] -[1, 2, 3, 4, 5, 6, 7, 8, 9, (10 or A), (11 or B), (12 or C)] [ @@ -395,11 +392,6 @@ last_call() - *a, 4, 5, --] --[ -- 4, -- *a, -- 5, + 6, + 7, + 8, @@ -408,6 +400,11 @@ last_call() + (NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2), + (NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2), ] +-[ +- 4, +- *a, +- 5, +-] +[1, 2, 3] +[NOT_YET_IMPLEMENTED_ExprStarred] +[NOT_YET_IMPLEMENTED_ExprStarred] @@ -541,6 +538,9 @@ last_call() - int, - float, - dict[str, int], +-] +-very_long_variable_name_filters: t.List[ +- t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], +[ + 1, + 2, @@ -555,9 +555,6 @@ last_call() + NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2, + NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2, ] --very_long_variable_name_filters: t.List[ -- t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], --] -xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore - sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) -) @@ -599,7 +596,7 @@ last_call() -[1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C] (SomeName) SomeName --(Good, Bad, Ugly) + (Good, Bad, Ugly) -(i for i in (1, 2, 3)) -((i**2) for i in (1, 2, 3)) -((i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))) @@ -614,38 +611,25 @@ last_call() - "import_session_id": 1, - **kwargs, -} --a = (1,) --b = (1,) -+(1, 2) +(i for i in []) +(i for i in []) +(i for i in []) +(i for i in []) -+(1, 2) ++(NOT_YET_IMPLEMENTED_ExprStarred,) +{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -+a = (1, 2) -+b = (1, 2) + a = (1,) + b = (1,) c = 1 --d = (1,) + a + (2,) + d = (1,) + a + (2,) -e = (1,).count(1) -f = 1, *range(10) -g = 1, *"ten" -what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set( - vars_to_remove -+d = (1, 2) + a + (1, 2) -+e = NOT_IMPLEMENTED_call() -+f = (1, 2) -+g = (1, 2) -+what_is_up_with_those_new_coord_names = ( -+ (coord_names + NOT_IMPLEMENTED_call()) -+ + NOT_IMPLEMENTED_call() - ) +-) -what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( - vars_to_remove -+what_is_up_with_those_new_coord_names = ( -+ (coord_names | NOT_IMPLEMENTED_call()) -+ - NOT_IMPLEMENTED_call() - ) +-) -result = ( - session.query(models.Customer.id) - .filter( @@ -653,7 +637,13 @@ last_call() - ) - .order_by(models.Customer.id.asc()) - .all() --) ++e = NOT_IMPLEMENTED_call() ++f = 1, NOT_YET_IMPLEMENTED_ExprStarred ++g = 1, NOT_YET_IMPLEMENTED_ExprStarred ++what_is_up_with_those_new_coord_names = ( ++ (coord_names + NOT_IMPLEMENTED_call()) ++ + NOT_IMPLEMENTED_call() + ) -result = ( - session.query(models.Customer.id) - .filter( @@ -663,7 +653,10 @@ last_call() - models.Customer.id.asc(), - ) - .all() --) ++what_is_up_with_those_new_coord_names = ( ++ (coord_names | NOT_IMPLEMENTED_call()) ++ - NOT_IMPLEMENTED_call() + ) -Ø = set() -authors.łukasz.say_thanks() -mapping = { @@ -899,10 +892,10 @@ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false (NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false), } NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 +() +(1,) (1, 2) -(1, 2) -(1, 2) -(1, 2) +(1, 2, 3) [] [ 1, @@ -1020,20 +1013,20 @@ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false ] (SomeName) SomeName -(1, 2) +(Good, Bad, Ugly) (i for i in []) (i for i in []) (i for i in []) (i for i in []) -(1, 2) +(NOT_YET_IMPLEMENTED_ExprStarred,) {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -a = (1, 2) -b = (1, 2) +a = (1,) +b = (1,) c = 1 -d = (1, 2) + a + (1, 2) +d = (1,) + a + (2,) e = NOT_IMPLEMENTED_call() -f = (1, 2) -g = (1, 2) +f = 1, NOT_YET_IMPLEMENTED_ExprStarred +g = 1, NOT_YET_IMPLEMENTED_ExprStarred what_is_up_with_those_new_coord_names = ( (coord_names + NOT_IMPLEMENTED_call()) + NOT_IMPLEMENTED_call() diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap index d4de723ff0..7dc0b255ac 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap @@ -101,8 +101,8 @@ def test_calculate_fades(): # one is zero/none - (0, 4, 0, 0, 10, 0, 0, 6, 10), - (None, 4, 0, 0, 10, 0, 0, 6, 10), -+ (1, 2), -+ (1, 2), ++ (0, 4, 0, 0, 10, 0, 0, 6, 10), ++ (None, 4, 0, 0, 10, 0, 0, 6, 10), ] # fmt: on @@ -144,8 +144,8 @@ def verify_fader(test): def test_calculate_fades(): calcs = [ # one is zero/none - (1, 2), - (1, 2), + (0, 4, 0, 0, 10, 0, 0, 6, 10), + (None, 4, 0, 0, 10, 0, 0, 6, 10), ] # fmt: on diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap index febe1c8ba1..7a3cedbd0b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap @@ -207,10 +207,10 @@ d={'a':1, +NOT_YET_IMPLEMENTED_StmtImport -from third_party import X, Y, Z -- --from library import some_connection, some_decorator +NOT_YET_IMPLEMENTED_StmtImportFrom +-from library import some_connection, some_decorator +- +NOT_YET_IMPLEMENTED_StmtImportFrom # fmt: off -from third_party import (X, @@ -281,7 +281,7 @@ d={'a':1, - assert task._cancel_stack[: len(old_stack)] == old_stack +def spaces( + a=1, -+ b=(1, 2), ++ b=(), + c=[], + d={NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, + e=True, @@ -296,8 +296,7 @@ d={'a':1, def spaces_types( a: int = 1, -- b: tuple = (), -+ b: tuple = (1, 2), + b: tuple = (), c: list = [], - d: dict = {}, + d: dict = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, @@ -351,7 +350,7 @@ d={'a':1, # fmt: off - a , b = *hello - 'unformatted' -+ (1, 2) = NOT_YET_IMPLEMENTED_ExprStarred ++ a, b = NOT_YET_IMPLEMENTED_ExprStarred + "NOT_YET_IMPLEMENTED_STRING" # fmt: on @@ -463,9 +462,8 @@ d={'a':1, def single_literal_yapf_disable(): - """Black does not support this.""" -- BAZ = {(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)} # yapf: disable + "NOT_YET_IMPLEMENTED_STRING" -+ BAZ = {(1, 2), (1, 2), (1, 2)} # yapf: disable + BAZ = {(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)} # yapf: disable -cfg.rule( @@ -561,7 +559,7 @@ def function_signature_stress_test( # fmt: on def spaces( a=1, - b=(1, 2), + b=(), c=[], d={NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, e=True, @@ -576,7 +574,7 @@ def spaces( def spaces_types( a: int = 1, - b: tuple = (1, 2), + b: tuple = (), c: list = [], d: dict = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, e: bool = True, @@ -608,7 +606,7 @@ def import_as_names(): def testlist_star_expr(): # fmt: off - (1, 2) = NOT_YET_IMPLEMENTED_ExprStarred + a, b = NOT_YET_IMPLEMENTED_ExprStarred "NOT_YET_IMPLEMENTED_STRING" # fmt: on @@ -668,7 +666,7 @@ def long_lines(): def single_literal_yapf_disable(): "NOT_YET_IMPLEMENTED_STRING" - BAZ = {(1, 2), (1, 2), (1, 2)} # yapf: disable + BAZ = {(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)} # yapf: disable NOT_IMPLEMENTED_call() diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip3_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip3_py.snap index 1a16430854..4b45e63eaa 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip3_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip3_py.snap @@ -25,7 +25,7 @@ f = ["This is a very long line that should be formatted into a clearer line ", " # fmt: off -b, c = 1, 2 -d = 6 # fmt: skip -+(1, 2) = (1, 2) ++b, c = 1, 2 +d = 6 # fmt: skip e = 5 # fmt: on @@ -41,7 +41,7 @@ f = ["This is a very long line that should be formatted into a clearer line ", " ```py a = 3 # fmt: off -(1, 2) = (1, 2) +b, c = 1, 2 d = 6 # fmt: skip e = 5 # fmt: on diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap index 9f346191a9..5c4a69a412 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap @@ -23,14 +23,13 @@ else: --- Black +++ Ruff @@ -1,9 +1,5 @@ --a, b, c = 3, 4, 5 + a, b, c = 3, 4, 5 -if ( - a == 3 - and b != 9 # fmt: skip - and c is not None -): - print("I'm good!") -+(1, 2) = (1, 2) +if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: + NOT_IMPLEMENTED_call() else: @@ -41,7 +40,7 @@ else: ## Ruff Output ```py -(1, 2) = (1, 2) +a, b, c = 3, 4, 5 if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: NOT_IMPLEMENTED_call() else: diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip_py.snap index 47305ef7ac..c4deeaed9c 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip_py.snap @@ -17,9 +17,8 @@ d = 5 --- Black +++ Ruff @@ -1,3 +1,3 @@ --a, b = 1, 2 + a, b = 1, 2 -c = 6 # fmt: skip -+(1, 2) = (1, 2) +c = 6 # fmt: skip d = 5 ``` @@ -27,7 +26,7 @@ d = 5 ## Ruff Output ```py -(1, 2) = (1, 2) +a, b = 1, 2 c = 6 # fmt: skip d = 5 ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap index 3e8b74e6cf..1ae6f9a66b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap @@ -175,7 +175,7 @@ def __await__(): return (yield) - assert task._cancel_stack[: len(old_stack)] == old_stack +def spaces( + a=1, -+ b=(1, 2), ++ b=(), + c=[], + d={NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, + e=True, @@ -190,8 +190,7 @@ def __await__(): return (yield) def spaces_types( a: int = 1, -- b: tuple = (), -+ b: tuple = (1, 2), + b: tuple = (), c: list = [], - d: dict = {}, + d: dict = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, @@ -348,7 +347,7 @@ def function_signature_stress_test( def spaces( a=1, - b=(1, 2), + b=(), c=[], d={NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, e=True, @@ -363,7 +362,7 @@ def spaces( def spaces_types( a: int = 1, - b: tuple = (1, 2), + b: tuple = (), c: list = [], d: dict = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, e: bool = True, diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap index ecc5c3010f..3e472e8729 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap @@ -74,19 +74,18 @@ some_module.some_function( ```diff --- Black +++ Ruff -@@ -1,69 +1,30 @@ +@@ -1,9 +1,7 @@ def f( a, ): - d = { - "key": "value", - } -- tup = (1,) + d = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -+ tup = (1, 2) + tup = (1,) - def f2( +@@ -11,10 +9,7 @@ a, b, ): @@ -94,14 +93,11 @@ some_module.some_function( - "key": "value", - "key2": "value2", - } -- tup = ( -- 1, -- 2, -- ) + d = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -+ tup = (1, 2) - - + tup = ( + 1, + 2, +@@ -24,46 +19,15 @@ def f( a: int = 1, ): @@ -154,7 +150,7 @@ some_module.some_function( # The type annotation shouldn't get a trailing comma since that would change its type. -@@ -80,35 +41,16 @@ +@@ -80,35 +44,16 @@ pass @@ -203,7 +199,7 @@ def f( a, ): d = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} - tup = (1, 2) + tup = (1,) def f2( @@ -211,7 +207,10 @@ def f2( b, ): d = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} - tup = (1, 2) + tup = ( + 1, + 2, + ) def f( diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap index 7c3bdb253a..4a41269a87 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap @@ -96,11 +96,6 @@ return np.divide( -j = super().name ** 5 -k = [(2**idx, value) for idx, value in pairs] -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 = {(k, k**2): v**2 for k, v in pairs} --q = [10**i for i in range(6)] +a = 5**NOT_YET_IMPLEMENTED_ExprUnaryOp +b = 5 ** NOT_IMPLEMENTED_call() +c = NOT_YET_IMPLEMENTED_ExprUnaryOp @@ -113,7 +108,11 @@ return np.divide( +j = NOT_IMPLEMENTED_call().NOT_IMPLEMENTED_attr ** 5 +k = [i for i in []] +l = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+m = [(1, 2)] + m = [([2**63], [1, 2**63])] +-n = count <= 10**5 +-o = settings(max_examples=10**6) +-p = {(k, k**2): v**2 for k, v in pairs} +-q = [10**i for i in range(6)] +n = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +o = NOT_IMPLEMENTED_call() +p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} @@ -132,11 +131,6 @@ return np.divide( -j = super().name ** 5.0 -k = [(2.0**idx, value) for idx, value in pairs] -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 = {(k, k**2): v**2.0 for k, v in pairs} --q = [10.5**i for i in range(6)] +a = 5.0**NOT_YET_IMPLEMENTED_ExprUnaryOp +b = 5.0 ** NOT_IMPLEMENTED_call() +c = NOT_YET_IMPLEMENTED_ExprUnaryOp @@ -149,7 +143,11 @@ return np.divide( +j = NOT_IMPLEMENTED_call().NOT_IMPLEMENTED_attr ** 5.0 +k = [i for i in []] +l = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+m = [(1, 2)] + m = [([2.0**63.0], [1.0, 2**63.0])] +-n = count <= 10**5.0 +-o = settings(max_examples=10**6.0) +-p = {(k, k**2): v**2.0 for k, v in pairs} +-q = [10.5**i for i in range(6)] +n = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +o = NOT_IMPLEMENTED_call() +p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} @@ -201,7 +199,7 @@ i = NOT_IMPLEMENTED_call() ** 5 j = NOT_IMPLEMENTED_call().NOT_IMPLEMENTED_attr ** 5 k = [i for i in []] l = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -m = [(1, 2)] +m = [([2**63], [1, 2**63])] n = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right o = NOT_IMPLEMENTED_call() p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} @@ -220,7 +218,7 @@ i = NOT_IMPLEMENTED_call() ** 5.0 j = NOT_IMPLEMENTED_call().NOT_IMPLEMENTED_attr ** 5.0 k = [i for i in []] l = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -m = [(1, 2)] +m = [([2.0**63.0], [1.0, 2**63.0])] n = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right o = NOT_IMPLEMENTED_call() p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__prefer_rhs_split_reformatted_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__prefer_rhs_split_reformatted_py.snap index b917eaf3b0..bdc722edb5 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__prefer_rhs_split_reformatted_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__prefer_rhs_split_reformatted_py.snap @@ -25,22 +25,15 @@ xxxxxxxxx_yyy_zzzzzzzz[xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxx ```diff --- Black +++ Ruff -@@ -2,20 +2,8 @@ - - # Left hand side fits in a single line but will still be exploded by the - # magic trailing comma. --( -- first_value, -- ( -- m1, -- m2, -- ), -- third_value, +@@ -9,13 +9,8 @@ + m2, + ), + third_value, -) = xxxxxx_yyyyyy_zzzzzz_wwwwww_uuuuuuu_vvvvvvvvvvv( - arg1, - arg2, -) -+(1, 2) = NOT_IMPLEMENTED_call() ++) = NOT_IMPLEMENTED_call() # Make when when the left side of assignment plus the opening paren "... = (" is # exactly line length limit + 1, it won't be split like that. @@ -57,7 +50,14 @@ xxxxxxxxx_yyy_zzzzzzzz[xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxx # Left hand side fits in a single line but will still be exploded by the # magic trailing comma. -(1, 2) = NOT_IMPLEMENTED_call() +( + first_value, + ( + m1, + m2, + ), + third_value, +) = NOT_IMPLEMENTED_call() # Make when when the left side of assignment plus the opening paren "... = (" is # exactly line length limit + 1, it won't be split like that. diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap index 5e1e16cd2e..edf95a4f94 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap @@ -114,12 +114,7 @@ def example8(): def example4(): -@@ -46,39 +34,15 @@ - - - def example5(): -- return () -+ return (1, 2) +@@ -50,35 +38,11 @@ def example6(): @@ -198,7 +193,7 @@ def example4(): def example5(): - return (1, 2) + return () def example6(): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap index b0626e0c31..2d9a375f59 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap @@ -60,7 +60,7 @@ func( ```diff --- Black +++ Ruff -@@ -1,25 +1,25 @@ +@@ -1,25 +1,30 @@ # We should not remove the trailing comma in a single-element subscript. -a: tuple[int,] -b = tuple[int,] @@ -82,8 +82,7 @@ func( +set_of_types = {NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]} # Except single element tuples --small_tuple = (1,) -+small_tuple = (1, 2) + small_tuple = (1,) # Trailing commas in multiple chained non-nested parens. -zero(one).two(three).four(five) @@ -93,7 +92,12 @@ func( +NOT_IMPLEMENTED_call() -(a, b, c, d) = func1(arg1) and func2(arg2) -+(1, 2) = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 ++( ++ a, ++ b, ++ c, ++ d, ++) = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -func(argument1, (one, two), argument4, argument5, argument6) +NOT_IMPLEMENTED_call() @@ -117,14 +121,19 @@ small_set = {1} set_of_types = {NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]} # Except single element tuples -small_tuple = (1, 2) +small_tuple = (1,) # Trailing commas in multiple chained non-nested parens. NOT_IMPLEMENTED_call() NOT_IMPLEMENTED_call() -(1, 2) = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 +( + a, + b, + c, + d, +) = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 NOT_IMPLEMENTED_call() ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap index 6dc17b505b..1d2e046b96 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap @@ -76,7 +76,7 @@ x[ ```diff --- Black +++ Ruff -@@ -1,59 +1,38 @@ +@@ -1,59 +1,48 @@ -slice[a.b : c.d] -slice[d :: d + 1] -slice[d + 1 :: d] @@ -125,12 +125,22 @@ x[ # These are from PEP-8: -ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:] -ham[lower:upper], ham[lower:upper:], ham[lower::step] -+(1, 2) -+(1, 2) ++( ++ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], ++ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], ++ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], ++ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], ++ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], ++) ++( ++ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], ++ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], ++ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], ++) # ham[lower+offset : upper+offset] -ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] -ham[lower + offset : upper + offset] -+(1, 2) ++NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] +NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -slice[::, ::] @@ -196,10 +206,20 @@ async def f(): # These are from PEP-8: -(1, 2) -(1, 2) +( + NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], + NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], + NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], + NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], + NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], +) +( + NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], + NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], + NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], +) # ham[lower+offset : upper+offset] -(1, 2) +NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap index eb5b0bf26b..9298adaab4 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap @@ -33,7 +33,7 @@ def docstring_multiline(): ```diff --- Black +++ Ruff -@@ -1,20 +1,18 @@ +@@ -1,20 +1,36 @@ #!/usr/bin/env python3 -name = "Łukasz" @@ -42,15 +42,33 @@ def docstring_multiline(): -("", "") -(r"", R"") +name = "NOT_YET_IMPLEMENTED_STRING" -+(1, 2) -+(1, 2) -+(1, 2) -+(1, 2) ++(NOT_YET_IMPLEMENTED_ExprJoinedStr, NOT_YET_IMPLEMENTED_ExprJoinedStr) ++(b"NOT_YET_IMPLEMENTED_BYTE_STRING", b"NOT_YET_IMPLEMENTED_BYTE_STRING") ++("NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING") ++("NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING") -(rf"", rf"", Rf"", Rf"", rf"", rf"", Rf"", Rf"") -(rb"", rb"", Rb"", Rb"", rb"", rb"", Rb"", Rb"") -+(1, 2) -+(1, 2) ++( ++ NOT_YET_IMPLEMENTED_ExprJoinedStr, ++ NOT_YET_IMPLEMENTED_ExprJoinedStr, ++ NOT_YET_IMPLEMENTED_ExprJoinedStr, ++ NOT_YET_IMPLEMENTED_ExprJoinedStr, ++ NOT_YET_IMPLEMENTED_ExprJoinedStr, ++ NOT_YET_IMPLEMENTED_ExprJoinedStr, ++ NOT_YET_IMPLEMENTED_ExprJoinedStr, ++ NOT_YET_IMPLEMENTED_ExprJoinedStr, ++) ++( ++ b"NOT_YET_IMPLEMENTED_BYTE_STRING", ++ b"NOT_YET_IMPLEMENTED_BYTE_STRING", ++ b"NOT_YET_IMPLEMENTED_BYTE_STRING", ++ b"NOT_YET_IMPLEMENTED_BYTE_STRING", ++ b"NOT_YET_IMPLEMENTED_BYTE_STRING", ++ b"NOT_YET_IMPLEMENTED_BYTE_STRING", ++ b"NOT_YET_IMPLEMENTED_BYTE_STRING", ++ b"NOT_YET_IMPLEMENTED_BYTE_STRING", ++) def docstring_singleline(): @@ -71,13 +89,31 @@ def docstring_multiline(): #!/usr/bin/env python3 name = "NOT_YET_IMPLEMENTED_STRING" -(1, 2) -(1, 2) -(1, 2) -(1, 2) +(NOT_YET_IMPLEMENTED_ExprJoinedStr, NOT_YET_IMPLEMENTED_ExprJoinedStr) +(b"NOT_YET_IMPLEMENTED_BYTE_STRING", b"NOT_YET_IMPLEMENTED_BYTE_STRING") +("NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING") +("NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING") -(1, 2) -(1, 2) +( + NOT_YET_IMPLEMENTED_ExprJoinedStr, + NOT_YET_IMPLEMENTED_ExprJoinedStr, + NOT_YET_IMPLEMENTED_ExprJoinedStr, + NOT_YET_IMPLEMENTED_ExprJoinedStr, + NOT_YET_IMPLEMENTED_ExprJoinedStr, + NOT_YET_IMPLEMENTED_ExprJoinedStr, + NOT_YET_IMPLEMENTED_ExprJoinedStr, + NOT_YET_IMPLEMENTED_ExprJoinedStr, +) +( + b"NOT_YET_IMPLEMENTED_BYTE_STRING", + b"NOT_YET_IMPLEMENTED_BYTE_STRING", + b"NOT_YET_IMPLEMENTED_BYTE_STRING", + b"NOT_YET_IMPLEMENTED_BYTE_STRING", + b"NOT_YET_IMPLEMENTED_BYTE_STRING", + b"NOT_YET_IMPLEMENTED_BYTE_STRING", + b"NOT_YET_IMPLEMENTED_BYTE_STRING", + b"NOT_YET_IMPLEMENTED_BYTE_STRING", +) def docstring_singleline(): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap index 0aee58f5ca..2fe7481d4e 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap @@ -42,11 +42,9 @@ assert ( ```diff --- Black +++ Ruff -@@ -1,58 +1,22 @@ - importA +@@ -2,57 +2,21 @@ ( -- () -+ (1, 2) + () << 0 - ** 101234234242352525425252352352525234890264906820496920680926538059059209922523523525 + **101234234242352525425252352352525234890264906820496920680926538059059209922523523525 @@ -114,7 +112,7 @@ assert ( ```py importA ( - (1, 2) + () << 0 **101234234242352525425252352352525234890264906820496920680926538059059209922523523525 ) # diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap index d1a1d68ab9..9999f85e05 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap @@ -46,7 +46,7 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( ```diff --- Black +++ Ruff -@@ -1,50 +1,21 @@ +@@ -1,50 +1,26 @@ -zero( - one, -).two( @@ -67,15 +67,15 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( -func1(arg1).func2(arg1, (one_tuple,)).func3(arg3) +NOT_IMPLEMENTED_call() --( -- a, -- b, -- c, -- d, + ( + a, + b, + c, + d, -) = func1( - arg1 -) and func2(arg2) -+(1, 2) = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 ++) = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 # Example from https://github.com/psf/black/issues/3229 @@ -116,7 +116,12 @@ NOT_IMPLEMENTED_call() # Inner one-element tuple shouldn't explode NOT_IMPLEMENTED_call() -(1, 2) = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 +( + a, + b, + c, + d, +) = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 # Example from https://github.com/psf/black/issues/3229 diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap index 643ac351f9..e335debb8b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap @@ -20,34 +20,36 @@ this_will_be_wrapped_in_parens, = struct.unpack(b"12345678901234567890") ```diff --- Black +++ Ruff -@@ -1,12 +1,7 @@ - # This is a standalone comment. --( -- sdfjklsdfsjldkflkjsf, -- sdfjsdfjlksdljkfsdlkf, -- sdfsdjfklsdfjlksdljkf, -- sdsfsdfjskdflsfsdf, +@@ -4,9 +4,9 @@ + sdfjsdfjlksdljkfsdlkf, + sdfsdjfklsdfjlksdljkf, + sdsfsdfjskdflsfsdf, -) = (1, 2, 3) -+(1, 2) = (1, 2) ++) = 1, 2, 3 # This is as well. -(this_will_be_wrapped_in_parens,) = struct.unpack(b"12345678901234567890") -+(1, 2) = NOT_IMPLEMENTED_call() ++(this_will_be_wrapped_in_parens,) = NOT_IMPLEMENTED_call() -(a,) = call() -+(1, 2) = NOT_IMPLEMENTED_call() ++(a,) = NOT_IMPLEMENTED_call() ``` ## Ruff Output ```py # This is a standalone comment. -(1, 2) = (1, 2) +( + sdfjklsdfsjldkflkjsf, + sdfjsdfjlksdljkfsdlkf, + sdfsdjfklsdfjlksdljkf, + sdsfsdfjskdflsfsdf, +) = 1, 2, 3 # This is as well. -(1, 2) = NOT_IMPLEMENTED_call() +(this_will_be_wrapped_in_parens,) = NOT_IMPLEMENTED_call() -(1, 2) = NOT_IMPLEMENTED_call() +(a,) = NOT_IMPLEMENTED_call() ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_expression_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_expression_py.snap index ab5311bf8a..47374f6ad8 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_expression_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_expression_py.snap @@ -86,7 +86,12 @@ aaaaaaaaaaaaaa + [ dddddddddddddddd, eeeeeee, ] -aaaaaaaaaaaaaa + (1, 2) +aaaaaaaaaaaaaa + ( + bbbbbbbbbbbbbbbbbbbbbb, + ccccccccccccccccccccc, + dddddddddddddddd, + eeeeeee, +) aaaaaaaaaaaaaa + {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} aaaaaaaaaaaaaa + { bbbbbbbbbbbbbbbbbbbbbb, diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__tuple_expression_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__tuple_expression_py.snap new file mode 100644 index 0000000000..70dab6420a --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__tuple_expression_py.snap @@ -0,0 +1,267 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +--- +## Input +```py +# Non-wrapping parentheses checks +a1 = 1, 2 +a2 = (1, 2) +a3 = (1, 2), 3 +a4 = ((1, 2), 3) + +# Wrapping parentheses checks +b1 = (("Michael", "Ende"), ("Der", "satanarchäolügenialkohöllische", "Wunschpunsch"), ("Beelzebub", "Irrwitzer"), ("Tyrannja", "Vamperl"),) +b2 = ("akjdshflkjahdslkfjlasfdahjlfds", "ljklsadhflakfdajflahfdlajfhafldkjalfj", "ljklsadhflakfdajflahfdlajfhafldkjalf2",) +b3 = ("The", "Night", "of", "Wishes:", "Or", "the", "Satanarchaeolidealcohellish", "Notion", "Potion",) + +# Nested wrapping parentheses check +c1 = (("cicero"), ("Qui", "autem,", "si", "maxime", "hoc", "placeat,", "moderatius", "tamen", "id", "uolunt", "fieri,", "difficilem", "quandam", "temperantiam", "postulant", "in", "eo,", "quod", "semel", "admissum", "coerceri", "reprimique", "non", "potest,", "ut", "propemodum", "iustioribus", "utamur", "illis,", "qui", "omnino", "auocent", "a", "philosophia,", "quam", "his,", "qui", "rebus", "infinitis", "modum", "constituant", "in", "reque", "eo", "meliore,", "quo", "maior", "sit,", "mediocritatem", "desiderent."), ("de", "finibus", "bonorum", "et", "malorum")) + +# Deeply nested parentheses +d1 = ((("3D",),),) +d2 = (((((((((((((((((((((((((((("¯\_(ツ)_/¯",),),),),),),),),),),),),),),),),),),),),),),),),),),),) + +# Join and magic trailing comma +e1 = ( + 1, + 2 +) +e2 = ( + 1, + 2, +) +e3 = ( + 1, +) +e4 = ( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor", + "incididunt" +) + +# Empty tuples and comments +f1 = ( + # empty +) +f2 = () + +# Comments in other tuples +g1 = ( # a + # b + 1, # c + # d +) # e +g2 = ( # a + # b + 1, # c + # d + 2, # e + # f +) # g + +# Ensure the correct number of parentheses +h1 = ((((1, 2)))) +h2 = ((((1, "qweiurpoiqwurepqiurpqirpuqoiwrupqoirupqoirupqoiurpqiorupwqiourpqurpqurpqurpqurpqurpqurüqurqpuriq")))) +h3 = 1, "qweiurpoiqwurepqiurpqirpuqoiwrupqoirupqoirupqoiurpqiorupwqiourpqurpqurpqurpqurpqurpqurüqurqpuriq" +``` + + + +## Output +```py +# Non-wrapping parentheses checks +a1 = 1, 2 +a2 = (1, 2) +a3 = (1, 2), 3 +a4 = ((1, 2), 3) + +# Wrapping parentheses checks +b1 = ( + ("NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING"), + ( + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + ), + ("NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING"), + ("NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING"), +) +b2 = ( + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", +) +b3 = ( + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", +) + +# Nested wrapping parentheses check +c1 = ( + ("NOT_YET_IMPLEMENTED_STRING"), + ( + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + ), + ( + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + ), +) + +# Deeply nested parentheses +d1 = ((("NOT_YET_IMPLEMENTED_STRING",),),) +d2 = ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + "NOT_YET_IMPLEMENTED_STRING", + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), +) + +# Join and magic trailing comma +e1 = (1, 2) +e2 = ( + 1, + 2, +) +e3 = (1,) +e4 = ("NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING") + +# Empty tuples and comments +f1 = ( + # empty +) +f2 = () + +# Comments in other tuples +g1 = ( + # a + # b + 1, # c + # d +) # e +g2 = ( + # a + # b + 1, # c + # d + 2, # e + # f +) # g + +# Ensure the correct number of parentheses +h1 = (1, 2) +h2 = (1, "NOT_YET_IMPLEMENTED_STRING") +h3 = 1, "NOT_YET_IMPLEMENTED_STRING" +``` + + From d8f5d2d7673e7934f749254e1a594de2cc5d316b Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Mon, 12 Jun 2023 19:44:15 +0530 Subject: [PATCH 010/447] Add support for auto-fix in Jupyter notebooks (#4665) ## Summary Add support for applying auto-fixes in Jupyter Notebook. ### Solution Cell offsets are the boundaries for each cell in the concatenated source code. They are represented using `TextSize`. It includes the start and end offset as well, thus creating a range for each cell. These offsets are updated using the `SourceMap` markers. ### SourceMap `SourceMap` contains markers constructed from each edits which tracks the original source code position to the transformed positions. The following drawing might make it clear: ![SourceMap visualization](https://github.com/astral-sh/ruff/assets/67177269/3c94e591-70a7-4b57-bd32-0baa91cc7858) The center column where the dotted lines are present are the markers included in the `SourceMap`. The `Notebook` looks at these markers and updates the cell offsets after each linter loop. If you notice closely, the destination takes into account all of the markers before it. The index is constructed only when required as it's only used to render the diagnostics. So, a `OnceCell` is used for this purpose. The cell offsets, cell content and the index will be updated after each iteration of linting in the mentioned order. The order is important here as the content is updated as per the new offsets and index is updated as per the new content. ## Limitations ### 1 Styling rules such as the ones in `pycodestyle` will not be applicable everywhere in Jupyter notebook, especially at the cell boundaries. Let's take an example where a rule suggests to have 2 blank lines before a function and the cells contains the following code: ```python import something # --- def first(): pass def second(): pass ``` (Again, the comment is only to visualize cell boundaries.) In the concatenated source code, the 2 blank lines will be added but it shouldn't actually be added when we look in terms of Jupyter notebook. It's as if the function `first` is at the start of a file. `nbqa` solves this by recording newlines before and after running `autopep8`, then running the tool and restoring the newlines at the end (refer https://github.com/nbQA-dev/nbQA/pull/807). ## Test Plan Three commands were run in order with common flags (`--select=ALL --no-cache --isolated`) to isolate which stage the problem is occurring: 1. Only diagnostics 2. Fix with diff (`--fix --diff`) 3. Fix (`--fix`) ### https://github.com/facebookresearch/segment-anything ``` ------------------------------------------------------------------------------- Jupyter Notebooks 3 0 0 0 0 |- Markdown 3 98 0 94 4 |- Python 3 513 468 4 41 (Total) 611 468 98 45 ------------------------------------------------------------------------------- ``` ```console $ cargo run --all-features --bin ruff -- check --no-cache --isolated --select=ALL /path/to/segment-anything/**/*.ipynb --fix ... Found 180 errors (89 fixed, 91 remaining). ``` ### https://github.com/openai/openai-cookbook ``` ------------------------------------------------------------------------------- Jupyter Notebooks 65 0 0 0 0 |- Markdown 64 3475 12 2507 956 |- Python 65 9700 7362 1101 1237 (Total) 13175 7374 3608 2193 =============================================================================== ``` ```console $ cargo run --all-features --bin ruff -- check --no-cache --isolated --select=ALL /path/to/openai-cookbook/**/*.ipynb --fix error: Failed to parse /path/to/openai-cookbook/examples/vector_databases/Using_vector_databases_for_embeddings_search.ipynb:cell 4:29:18: unexpected token '-' ... Found 4227 errors (2165 fixed, 2062 remaining). ``` ### https://github.com/tensorflow/docs ``` ------------------------------------------------------------------------------- Jupyter Notebooks 150 0 0 0 0 |- Markdown 1 55 0 46 9 |- Python 1 402 289 60 53 (Total) 457 289 106 62 ------------------------------------------------------------------------------- ``` ```console $ cargo run --all-features --bin ruff -- check --no-cache --isolated --select=ALL /path/to/tensorflow-docs/**/*.ipynb --fix error: Failed to parse /path/to/tensorflow-docs/site/en/guide/extension_type.ipynb:cell 80:1:1: unexpected token Indent error: Failed to parse /path/to/tensorflow-docs/site/en/r1/tutorials/eager/custom_layers.ipynb:cell 20:1:1: unexpected token Indent error: Failed to parse /path/to/tensorflow-docs/site/en/guide/data.ipynb:cell 175:5:14: unindent does not match any outer indentation level error: Failed to parse /path/to/tensorflow-docs/site/en/r1/tutorials/representation/unicode.ipynb:cell 30:1:1: unexpected token Indent ... Found 12726 errors (5140 fixed, 7586 remaining). ``` ### https://github.com/tensorflow/models ``` ------------------------------------------------------------------------------- Jupyter Notebooks 46 0 0 0 0 |- Markdown 1 11 0 6 5 |- Python 1 328 249 19 60 (Total) 339 249 25 65 ------------------------------------------------------------------------------- ``` ```console $ cargo run --all-features --bin ruff -- check --no-cache --isolated --select=ALL /path/to/tensorflow-models/**/*.ipynb --fix ... Found 4856 errors (2690 fixed, 2166 remaining). ``` resolves: #1218 fixes: #4556 --- Cargo.lock | 114 +++++- crates/ruff/Cargo.toml | 1 + .../fixtures/jupyter/cell/code_and_magic.json | 5 + .../test/fixtures/jupyter/cell/markdown.json | 5 + .../test/fixtures/jupyter/cell/only_code.json | 5 + .../fixtures/jupyter/cell/only_magic.json | 5 + .../test/fixtures/jupyter/valid.ipynb | 85 +++- crates/ruff/src/autofix/mod.rs | 192 +++++++-- crates/ruff/src/autofix/source_map.rs | 59 +++ crates/ruff/src/checkers/imports.rs | 4 +- crates/ruff/src/jupyter/index.rs | 24 ++ crates/ruff/src/jupyter/mod.rs | 2 + crates/ruff/src/jupyter/notebook.rs | 378 ++++++++++++++---- crates/ruff/src/jupyter/schema.rs | 57 ++- crates/ruff/src/lib.rs | 1 + crates/ruff/src/linter.rs | 27 +- crates/ruff/src/logging.rs | 63 ++- crates/ruff/src/message/grouped.rs | 16 +- crates/ruff/src/message/mod.rs | 22 +- crates/ruff/src/message/text.rs | 45 ++- crates/ruff/src/rules/isort/block.rs | 23 ++ crates/ruff/src/rules/pyflakes/mod.rs | 1 + crates/ruff/src/source_kind.rs | 26 ++ crates/ruff/src/test.rs | 10 +- crates/ruff_cli/src/diagnostics.rs | 81 ++-- crates/ruff_cli/src/printer.rs | 4 +- crates/ruff_wasm/src/lib.rs | 1 + 27 files changed, 1022 insertions(+), 234 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/jupyter/cell/code_and_magic.json create mode 100644 crates/ruff/resources/test/fixtures/jupyter/cell/markdown.json create mode 100644 crates/ruff/resources/test/fixtures/jupyter/cell/only_code.json create mode 100644 crates/ruff/resources/test/fixtures/jupyter/cell/only_magic.json create mode 100644 crates/ruff/src/autofix/source_map.rs create mode 100644 crates/ruff/src/jupyter/index.rs create mode 100644 crates/ruff/src/source_kind.rs diff --git a/Cargo.lock b/Cargo.lock index d0c654d156..50fd9272f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,6 +171,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + [[package]] name = "bincode" version = "1.3.3" @@ -256,7 +262,8 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", - "time", + "serde", + "time 0.1.45", "wasm-bindgen", "winapi", ] @@ -555,6 +562,41 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "darling" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.18", +] + +[[package]] +name = "darling_macro" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.18", +] + [[package]] name = "diff" version = "0.1.13" @@ -814,6 +856,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hexf-parse" version = "0.2.1" @@ -843,6 +891,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.3.0" @@ -1789,6 +1843,7 @@ dependencies = [ "semver", "serde", "serde_json", + "serde_with", "shellexpand", "similar", "smallvec", @@ -2323,6 +2378,34 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f02d8aa6e3c385bf084924f660ce2a3a6bd333ba55b35e8590b321f35d88513" +dependencies = [ + "base64 0.21.2", + "chrono", + "hex", + "indexmap", + "serde", + "serde_json", + "serde_with_macros", + "time 0.3.21", +] + +[[package]] +name = "serde_with_macros" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc7d5d3932fb12ce722ee5e64dd38c504efba37567f0c402f6ca728c3b8b070" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.18", +] + [[package]] name = "shellexpand" version = "3.1.0" @@ -2549,6 +2632,33 @@ dependencies = [ "winapi", ] +[[package]] +name = "time" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +dependencies = [ + "time-core", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -2767,7 +2877,7 @@ version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d" dependencies = [ - "base64", + "base64 0.13.1", "flate2", "log", "once_cell", diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 53a1ea9376..306a689ae6 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -65,6 +65,7 @@ schemars = { workspace = true, optional = true } semver = { version = "1.0.16" } serde = { workspace = true } serde_json = { workspace = true } +serde_with = { version = "3.0.0" } similar = { workspace = true, features = ["inline"] } shellexpand = { workspace = true } smallvec = { workspace = true } diff --git a/crates/ruff/resources/test/fixtures/jupyter/cell/code_and_magic.json b/crates/ruff/resources/test/fixtures/jupyter/cell/code_and_magic.json new file mode 100644 index 0000000000..b1acfb8be0 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/jupyter/cell/code_and_magic.json @@ -0,0 +1,5 @@ +{ + "cell_type": "code", + "metadata": {}, + "source": ["def foo():\n", " pass\n", "\n", "%timeit foo()"] +} diff --git a/crates/ruff/resources/test/fixtures/jupyter/cell/markdown.json b/crates/ruff/resources/test/fixtures/jupyter/cell/markdown.json new file mode 100644 index 0000000000..f6880ebbf0 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/jupyter/cell/markdown.json @@ -0,0 +1,5 @@ +{ + "cell_type": "markdown", + "metadata": {}, + "source": ["This is a markdown cell\n", "Some more content"] +} diff --git a/crates/ruff/resources/test/fixtures/jupyter/cell/only_code.json b/crates/ruff/resources/test/fixtures/jupyter/cell/only_code.json new file mode 100644 index 0000000000..89904fbd93 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/jupyter/cell/only_code.json @@ -0,0 +1,5 @@ +{ + "cell_type": "code", + "metadata": {}, + "source": ["def foo():\n", " pass"] +} diff --git a/crates/ruff/resources/test/fixtures/jupyter/cell/only_magic.json b/crates/ruff/resources/test/fixtures/jupyter/cell/only_magic.json new file mode 100644 index 0000000000..183923bde1 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/jupyter/cell/only_magic.json @@ -0,0 +1,5 @@ +{ + "cell_type": "code", + "metadata": {}, + "source": "%timeit print('hello world')" +} diff --git a/crates/ruff/resources/test/fixtures/jupyter/valid.ipynb b/crates/ruff/resources/test/fixtures/jupyter/valid.ipynb index 5d16c271ee..63ca4467a2 100644 --- a/crates/ruff/resources/test/fixtures/jupyter/valid.ipynb +++ b/crates/ruff/resources/test/fixtures/jupyter/valid.ipynb @@ -3,6 +3,16 @@ { "cell_type": "code", "execution_count": 1, + "metadata": { + "ExecuteTime": { + "end_time": "2023-03-08T23:01:09.782916Z", + "start_time": "2023-03-08T23:01:09.705831Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [ { "name": "stdout", @@ -19,32 +29,26 @@ " print(f\"cell one: {y}\")\n", "\n", "unused_variable()" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-03-08T23:01:09.705831Z", - "end_time": "2023-03-08T23:01:09.782916Z" - } - } + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "Let's do another mistake" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": 2, "metadata": { - "collapsed": true, "ExecuteTime": { - "start_time": "2023-03-08T23:01:09.733809Z", - "end_time": "2023-03-08T23:01:09.915760Z" + "end_time": "2023-03-08T23:01:09.915760Z", + "start_time": "2023-03-08T23:01:09.733809Z" + }, + "collapsed": true, + "jupyter": { + "outputs_hidden": true } }, "outputs": [ @@ -62,27 +66,66 @@ "\n", "mutable_argument()\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's create an empty cell" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Multi-line empty cell!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"after empty cells\")" + ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python (ruff)", "language": "python", - "name": "python3" + "name": "ruff" }, "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" + "pygments_lexer": "ipython3", + "version": "3.11.3" } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 4 } diff --git a/crates/ruff/src/autofix/mod.rs b/crates/ruff/src/autofix/mod.rs index df4fe0e69c..a666171b54 100644 --- a/crates/ruff/src/autofix/mod.rs +++ b/crates/ruff/src/autofix/mod.rs @@ -2,23 +2,31 @@ use std::collections::BTreeSet; use itertools::Itertools; use nohash_hasher::IntSet; -use ruff_text_size::{TextRange, TextSize}; +use ruff_text_size::{TextLen, TextRange, TextSize}; use rustc_hash::FxHashMap; use ruff_diagnostics::{Diagnostic, Edit, Fix, IsolationLevel}; use ruff_python_ast::source_code::Locator; +use crate::autofix::source_map::SourceMap; use crate::linter::FixTable; use crate::registry::{AsRule, Rule}; pub(crate) mod codemods; pub(crate) mod edits; +pub(crate) mod source_map; + +pub(crate) struct FixResult { + /// The resulting source code, after applying all fixes. + pub(crate) code: String, + /// The number of fixes applied for each [`Rule`]. + pub(crate) fixes: FixTable, + /// Source map for the fixed source code. + pub(crate) source_map: SourceMap, +} /// Auto-fix errors in a file, and write the fixed source code to disk. -pub(crate) fn fix_file( - diagnostics: &[Diagnostic], - locator: &Locator, -) -> Option<(String, FixTable)> { +pub(crate) fn fix_file(diagnostics: &[Diagnostic], locator: &Locator) -> Option { let mut with_fixes = diagnostics .iter() .filter(|diag| diag.fix.is_some()) @@ -35,12 +43,13 @@ pub(crate) fn fix_file( fn apply_fixes<'a>( diagnostics: impl Iterator, locator: &'a Locator<'a>, -) -> (String, FixTable) { +) -> FixResult { let mut output = String::with_capacity(locator.len()); let mut last_pos: Option = None; let mut applied: BTreeSet<&Edit> = BTreeSet::default(); let mut isolated: IntSet = IntSet::default(); let mut fixed = FxHashMap::default(); + let mut source_map = SourceMap::default(); for (rule, fix) in diagnostics .filter_map(|diagnostic| { @@ -84,9 +93,15 @@ fn apply_fixes<'a>( let slice = locator.slice(TextRange::new(last_pos.unwrap_or_default(), edit.start())); output.push_str(slice); + // Add the start source marker for the patch. + source_map.push_start_marker(edit, output.text_len()); + // Add the patch itself. output.push_str(edit.content().unwrap_or_default()); + // Add the end source marker for the added patch. + source_map.push_end_marker(edit, output.text_len()); + // Track that the edit was applied. last_pos = Some(edit.end()); applied.insert(edit); @@ -99,7 +114,11 @@ fn apply_fixes<'a>( let slice = locator.after(last_pos.unwrap_or_default()); output.push_str(slice); - (output, fixed) + FixResult { + code: output, + fixes: fixed, + source_map, + } } /// Compare two fixes. @@ -130,7 +149,8 @@ mod tests { use ruff_diagnostics::Fix; use ruff_python_ast::source_code::Locator; - use crate::autofix::apply_fixes; + use crate::autofix::source_map::SourceMarker; + use crate::autofix::{apply_fixes, FixResult}; use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile; #[allow(deprecated)] @@ -150,9 +170,59 @@ mod tests { fn empty_file() { let locator = Locator::new(r#""#); let diagnostics = create_diagnostics([]); - let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator); - assert_eq!(contents, ""); - assert_eq!(fixed.values().sum::(), 0); + let FixResult { + code, + fixes, + source_map, + } = apply_fixes(diagnostics.iter(), &locator); + assert_eq!(code, ""); + assert_eq!(fixes.values().sum::(), 0); + assert!(source_map.markers().is_empty()); + } + + #[test] + fn apply_one_insertion() { + let locator = Locator::new( + r#" +import os + +print("hello world") +"# + .trim(), + ); + let diagnostics = create_diagnostics([Edit::insertion( + "import sys\n".to_string(), + TextSize::new(10), + )]); + let FixResult { + code, + fixes, + source_map, + } = apply_fixes(diagnostics.iter(), &locator); + assert_eq!( + code, + r#" +import os +import sys + +print("hello world") +"# + .trim() + ); + assert_eq!(fixes.values().sum::(), 1); + assert_eq!( + source_map.markers(), + &[ + SourceMarker { + source: 10.into(), + dest: 10.into(), + }, + SourceMarker { + source: 10.into(), + dest: 21.into(), + }, + ] + ); } #[test] @@ -169,16 +239,33 @@ class A(object): TextSize::new(8), TextSize::new(14), )]); - let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator); + let FixResult { + code, + fixes, + source_map, + } = apply_fixes(diagnostics.iter(), &locator); assert_eq!( - contents, + code, r#" class A(Bar): ... "# .trim(), ); - assert_eq!(fixed.values().sum::(), 1); + assert_eq!(fixes.values().sum::(), 1); + assert_eq!( + source_map.markers(), + &[ + SourceMarker { + source: 8.into(), + dest: 8.into(), + }, + SourceMarker { + source: 14.into(), + dest: 11.into(), + }, + ] + ); } #[test] @@ -191,16 +278,33 @@ class A(object): .trim(), ); let diagnostics = create_diagnostics([Edit::deletion(TextSize::new(7), TextSize::new(15))]); - let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator); + let FixResult { + code, + fixes, + source_map, + } = apply_fixes(diagnostics.iter(), &locator); assert_eq!( - contents, + code, r#" class A: ... "# .trim() ); - assert_eq!(fixed.values().sum::(), 1); + assert_eq!(fixes.values().sum::(), 1); + assert_eq!( + source_map.markers(), + &[ + SourceMarker { + source: 7.into(), + dest: 7.into() + }, + SourceMarker { + source: 15.into(), + dest: 7.into() + } + ] + ); } #[test] @@ -216,17 +320,42 @@ class A(object, object, object): Edit::deletion(TextSize::from(8), TextSize::from(16)), Edit::deletion(TextSize::from(22), TextSize::from(30)), ]); - let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator); + let FixResult { + code, + fixes, + source_map, + } = apply_fixes(diagnostics.iter(), &locator); assert_eq!( - contents, + code, r#" class A(object): ... "# .trim() ); - assert_eq!(fixed.values().sum::(), 2); + assert_eq!(fixes.values().sum::(), 2); + assert_eq!( + source_map.markers(), + &[ + SourceMarker { + source: 8.into(), + dest: 8.into() + }, + SourceMarker { + source: 16.into(), + dest: 8.into() + }, + SourceMarker { + source: 22.into(), + dest: 14.into(), + }, + SourceMarker { + source: 30.into(), + dest: 14.into(), + } + ] + ); } #[test] @@ -242,15 +371,32 @@ class A(object): Edit::deletion(TextSize::from(7), TextSize::from(15)), Edit::replacement("ignored".to_string(), TextSize::from(9), TextSize::from(11)), ]); - let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator); + let FixResult { + code, + fixes, + source_map, + } = apply_fixes(diagnostics.iter(), &locator); assert_eq!( - contents, + code, r#" class A: ... "# .trim(), ); - assert_eq!(fixed.values().sum::(), 1); + assert_eq!(fixes.values().sum::(), 1); + assert_eq!( + source_map.markers(), + &[ + SourceMarker { + source: 7.into(), + dest: 7.into(), + }, + SourceMarker { + source: 15.into(), + dest: 7.into(), + } + ] + ); } } diff --git a/crates/ruff/src/autofix/source_map.rs b/crates/ruff/src/autofix/source_map.rs new file mode 100644 index 0000000000..9b6ea21e17 --- /dev/null +++ b/crates/ruff/src/autofix/source_map.rs @@ -0,0 +1,59 @@ +use ruff_text_size::TextSize; + +use ruff_diagnostics::Edit; + +/// Lightweight sourcemap marker representing the source and destination +/// position for an [`Edit`]. +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct SourceMarker { + /// Position of the marker in the original source. + pub(crate) source: TextSize, + /// Position of the marker in the transformed code. + pub(crate) dest: TextSize, +} + +/// A collection of [`SourceMarker`]. +/// +/// Sourcemaps are used to map positions in the original source to positions in +/// the transformed code. Here, only the boundaries of edits are tracked instead +/// of every single character. +#[derive(Default, PartialEq, Eq)] +pub(crate) struct SourceMap(Vec); + +impl SourceMap { + /// Returns a slice of all the markers in the sourcemap in the order they + /// were added. + pub(crate) fn markers(&self) -> &[SourceMarker] { + &self.0 + } + + /// Push the start marker for an [`Edit`]. + /// + /// The `output_length` is the length of the transformed string before the + /// edit is applied. + pub(crate) fn push_start_marker(&mut self, edit: &Edit, output_length: TextSize) { + self.0.push(SourceMarker { + source: edit.start(), + dest: output_length, + }); + } + + /// Push the end marker for an [`Edit`]. + /// + /// The `output_length` is the length of the transformed string after the + /// edit has been applied. + pub(crate) fn push_end_marker(&mut self, edit: &Edit, output_length: TextSize) { + if edit.is_insertion() { + self.0.push(SourceMarker { + source: edit.start(), + dest: output_length, + }); + } else { + // Deletion or replacement + self.0.push(SourceMarker { + source: edit.end(), + dest: output_length, + }); + } + } +} diff --git a/crates/ruff/src/checkers/imports.rs b/crates/ruff/src/checkers/imports.rs index a985c0daaf..0ee66eddaf 100644 --- a/crates/ruff/src/checkers/imports.rs +++ b/crates/ruff/src/checkers/imports.rs @@ -16,6 +16,7 @@ use crate::registry::Rule; use crate::rules::isort; use crate::rules::isort::block::{Block, BlockBuilder}; use crate::settings::Settings; +use crate::source_kind::SourceKind; fn extract_import_map(path: &Path, package: Option<&Path>, blocks: &[&Block]) -> Option { let Some(package) = package else { @@ -83,12 +84,13 @@ pub(crate) fn check_imports( stylist: &Stylist, path: &Path, package: Option<&Path>, + source_kind: Option<&SourceKind>, ) -> (Vec, Option) { let is_stub = is_python_stub_file(path); // Extract all import blocks from the AST. let tracker = { - let mut tracker = BlockBuilder::new(locator, directives, is_stub); + let mut tracker = BlockBuilder::new(locator, directives, is_stub, source_kind); tracker.visit_body(python_ast); tracker }; diff --git a/crates/ruff/src/jupyter/index.rs b/crates/ruff/src/jupyter/index.rs new file mode 100644 index 0000000000..6a46d4da31 --- /dev/null +++ b/crates/ruff/src/jupyter/index.rs @@ -0,0 +1,24 @@ +/// Jupyter Notebook indexing table +/// +/// When we lint a jupyter notebook, we have to translate the row/column based on +/// [`ruff_text_size::TextSize`] to jupyter notebook cell/row/column. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct JupyterIndex { + /// Enter a row (1-based), get back the cell (1-based) + pub(super) row_to_cell: Vec, + /// Enter a row (1-based), get back the row in cell (1-based) + pub(super) row_to_row_in_cell: Vec, +} + +impl JupyterIndex { + /// Returns the cell number (1-based) for the given row (1-based). + pub fn cell(&self, row: usize) -> Option { + self.row_to_cell.get(row).copied() + } + + /// Returns the row number (1-based) in the cell (1-based) for the + /// given row (1-based). + pub fn cell_row(&self, row: usize) -> Option { + self.row_to_row_in_cell.get(row).copied() + } +} diff --git a/crates/ruff/src/jupyter/mod.rs b/crates/ruff/src/jupyter/mod.rs index ce6b9ef3bc..0d0bb5dc0a 100644 --- a/crates/ruff/src/jupyter/mod.rs +++ b/crates/ruff/src/jupyter/mod.rs @@ -1,7 +1,9 @@ //! Utils for reading and writing jupyter notebooks +pub use index::*; pub use notebook::*; pub use schema::*; +mod index; mod notebook; mod schema; diff --git a/crates/ruff/src/jupyter/notebook.rs b/crates/ruff/src/jupyter/notebook.rs index 5f2c545cac..66c4240fe0 100644 --- a/crates/ruff/src/jupyter/notebook.rs +++ b/crates/ruff/src/jupyter/notebook.rs @@ -1,32 +1,27 @@ +use std::cmp::Ordering; use std::fs::File; use std::io::{BufReader, BufWriter}; use std::iter; use std::path::Path; -use ruff_text_size::TextRange; +use itertools::Itertools; +use once_cell::sync::OnceCell; use serde::Serialize; use serde_json::error::Category; use ruff_diagnostics::Diagnostic; +use ruff_python_whitespace::NewlineWithTrailingNewline; +use ruff_text_size::{TextRange, TextSize}; -use crate::jupyter::{CellType, JupyterNotebook, SourceValue}; +use crate::autofix::source_map::{SourceMap, SourceMarker}; +use crate::jupyter::index::JupyterIndex; +use crate::jupyter::{Cell, CellType, RawNotebook, SourceValue}; use crate::rules::pycodestyle::rules::SyntaxError; use crate::IOError; pub const JUPYTER_NOTEBOOK_EXT: &str = "ipynb"; -/// Jupyter Notebook indexing table -/// -/// When we lint a jupyter notebook, we have to translate the row/column based on -/// [`ruff_text_size::TextSize`] -/// to jupyter notebook cell/row/column. -#[derive(Debug, Eq, PartialEq)] -pub struct JupyterIndex { - /// Enter a row (1-based), get back the cell (1-based) - pub row_to_cell: Vec, - /// Enter a row (1-based), get back the cell (1-based) - pub row_to_row_in_cell: Vec, -} +const MAGIC_PREFIX: [&str; 3] = ["%", "!", "?"]; /// Return `true` if the [`Path`] appears to be that of a jupyter notebook file (`.ipynb`). pub fn is_jupyter_notebook(path: &Path) -> bool { @@ -37,7 +32,55 @@ pub fn is_jupyter_notebook(path: &Path) -> bool { && cfg!(feature = "jupyter_notebook") } -impl JupyterNotebook { +impl Cell { + /// Return `true` if it's a valid code cell. + /// + /// A valid code cell is a cell where the type is [`CellType::Code`] and the + /// source doesn't contain a magic, shell or help command. + fn is_valid_code_cell(&self) -> bool { + if self.cell_type != CellType::Code { + return false; + } + // Ignore a cell if it contains a magic command. There could be valid + // Python code as well, but we'll ignore that for now. + // TODO(dhruvmanila): https://github.com/psf/black/blob/main/src/black/handle_ipynb_magics.py + !match &self.source { + SourceValue::String(string) => string.lines().any(|line| { + MAGIC_PREFIX + .iter() + .any(|prefix| line.trim_start().starts_with(prefix)) + }), + SourceValue::StringArray(string_array) => string_array.iter().any(|line| { + MAGIC_PREFIX + .iter() + .any(|prefix| line.trim_start().starts_with(prefix)) + }), + } + } +} + +#[derive(Debug, PartialEq)] +pub struct Notebook { + /// Python source code of the notebook. + /// + /// This is the concatenation of all valid code cells in the notebook + /// separated by a newline and a trailing newline. The trailing newline + /// is added to make sure that each cell ends with a newline which will + /// be removed when updating the cell content. + content: String, + /// The index of the notebook. This is used to map between the concatenated + /// source code and the original notebook. + index: OnceCell, + /// The raw notebook i.e., the deserialized version of JSON string. + raw: RawNotebook, + /// The offsets of each cell in the concatenated source code. This includes + /// the first and last character offsets as well. + cell_offsets: Vec, + /// The cell numbers of all valid code cells in the notebook. + valid_code_cells: Vec, +} + +impl Notebook { /// See also the black implementation /// pub fn read(path: &Path) -> Result> { @@ -49,7 +92,7 @@ impl JupyterNotebook { TextRange::default(), ) })?); - let notebook: JupyterNotebook = match serde_json::from_reader(reader) { + let notebook: RawNotebook = match serde_json::from_reader(reader) { Ok(notebook) => notebook, Err(err) => { // Translate the error into a diagnostic @@ -130,60 +173,201 @@ impl JupyterNotebook { ))); } - Ok(notebook) - } - - /// Concatenates all cells into a single virtual file and builds an index that maps the content - /// to notebook cell locations - pub fn index(&self) -> (String, JupyterIndex) { - let mut jupyter_index = JupyterIndex { - // Enter a line number (1-based), get back the cell (1-based) - // 0 index is just padding - row_to_cell: vec![0], - // Enter a line number (1-based), get back the row number in the cell (1-based) - // 0 index is just padding - row_to_row_in_cell: vec![0], - }; - let size_hint = self - .cells - .iter() - .filter(|cell| cell.cell_type == CellType::Code) - .count(); - - let mut contents = Vec::with_capacity(size_hint); - - for (pos, cell) in self + let valid_code_cells = notebook .cells .iter() .enumerate() - .filter(|(_pos, cell)| cell.cell_type == CellType::Code) - { - let cell_contents = match &cell.source { - SourceValue::String(string) => { - // TODO(konstin): is or isn't there a trailing newline per cell? - // i've only seen these as array and never as string - let line_count = u32::try_from(string.lines().count()).unwrap(); - jupyter_index.row_to_cell.extend( - iter::repeat(u32::try_from(pos + 1).unwrap()).take(line_count as usize), - ); - jupyter_index.row_to_row_in_cell.extend(1..=line_count); - string.clone() - } - SourceValue::StringArray(string_array) => { - jupyter_index.row_to_cell.extend( - iter::repeat(u32::try_from(pos + 1).unwrap()).take(string_array.len()), - ); - jupyter_index - .row_to_row_in_cell - .extend(1..=u32::try_from(string_array.len()).unwrap()); - // lines already end in a newline character - string_array.join("") + .filter(|(_, cell)| cell.is_valid_code_cell()) + .map(|(pos, _)| u32::try_from(pos).unwrap()) + .collect::>(); + + let mut contents = Vec::with_capacity(valid_code_cells.len()); + let mut current_offset = TextSize::from(0); + let mut cell_offsets = Vec::with_capacity(notebook.cells.len()); + cell_offsets.push(TextSize::from(0)); + + for &pos in &valid_code_cells { + let cell_contents = match ¬ebook.cells[pos as usize].source { + SourceValue::String(string) => string.clone(), + SourceValue::StringArray(string_array) => string_array.join(""), + }; + current_offset += TextSize::of(&cell_contents) + TextSize::new(1); + contents.push(cell_contents); + cell_offsets.push(current_offset); + } + + Ok(Self { + raw: notebook, + index: OnceCell::new(), + // The additional newline at the end is to maintain consistency for + // all cells. These newlines will be removed before updating the + // source code with the transformed content. Refer `update_cell_content`. + content: contents.join("\n") + "\n", + cell_offsets, + valid_code_cells, + }) + } + + /// Update the cell offsets as per the given [`SourceMap`]. + fn update_cell_offsets(&mut self, source_map: &SourceMap) { + // When there are multiple cells without any edits, the offsets of those + // cells will be updated using the same marker. So, we can keep track of + // the last marker used to update the offsets and check if it's still + // the closest marker to the current offset. + let mut last_marker: Option<&SourceMarker> = None; + + // The first offset is always going to be at 0, so skip it. + for offset in self.cell_offsets.iter_mut().skip(1).rev() { + let closest_marker = match last_marker { + Some(marker) if marker.source <= *offset => marker, + _ => { + let Some(marker) = source_map + .markers() + .iter() + .rev() + .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 } }; - contents.push(cell_contents); + + match closest_marker.source.cmp(&closest_marker.dest) { + Ordering::Less => *offset += closest_marker.dest - closest_marker.source, + Ordering::Greater => *offset -= closest_marker.source - closest_marker.dest, + Ordering::Equal => (), + } } - // The last line doesn't end in a newline character - (contents.join("\n"), jupyter_index) + } + + /// Update the cell contents with the transformed content. + /// + /// ## Panics + /// + /// Panics if the transformed content is out of bounds for any cell. This + /// can happen only if the cell offsets were not updated before calling + /// this method or the offsets were updated incorrectly. + fn update_cell_content(&mut self, transformed: &str) { + for (&pos, (start, end)) in self + .valid_code_cells + .iter() + .zip(self.cell_offsets.iter().tuple_windows::<(_, _)>()) + { + let cell_content = transformed + .get(start.to_usize()..end.to_usize()) + .unwrap_or_else(|| { + panic!("Transformed content out of bounds ({start:?}..{end:?}) for cell {pos}"); + }); + self.raw.cells[pos as usize].source = SourceValue::String( + cell_content + // We only need to strip the trailing newline which we added + // while concatenating the cell contents. + .strip_suffix('\n') + .unwrap_or(cell_content) + .to_string(), + ); + } + } + + /// Build and return the [`JupyterIndex`]. + /// + /// # Notes + /// + /// Empty cells don't have any newlines, but there's a single visible line + /// in the UI. That single line needs to be accounted for. + /// + /// In case of [`SourceValue::StringArray`], newlines are part of the strings. + /// So, to get the actual count of lines, we need to check for any trailing + /// newline for the last line. + /// + /// For example, consider the following cell: + /// ```python + /// [ + /// "import os\n", + /// "import sys\n", + /// ] + /// ``` + /// + /// Here, the array suggests that there are two lines, but the actual number + /// of lines visible in the UI is three. The same goes for [`SourceValue::String`] + /// where we need to check for the trailing newline. + /// + /// The index building is expensive as it needs to go through the content of + /// every valid code cell. + fn build_index(&self) -> JupyterIndex { + let mut row_to_cell = vec![0]; + let mut row_to_row_in_cell = vec![0]; + + for &pos in &self.valid_code_cells { + let line_count = match &self.raw.cells[pos as usize].source { + SourceValue::String(string) => { + if string.is_empty() { + 1 + } else { + u32::try_from(NewlineWithTrailingNewline::from(string).count()).unwrap() + } + } + SourceValue::StringArray(string_array) => { + if string_array.is_empty() { + 1 + } else { + let trailing_newline = + usize::from(string_array.last().map_or(false, |s| s.ends_with('\n'))); + u32::try_from(string_array.len() + trailing_newline).unwrap() + } + } + }; + row_to_cell.extend(iter::repeat(pos + 1).take(line_count as usize)); + row_to_row_in_cell.extend(1..=line_count); + } + + JupyterIndex { + row_to_cell, + row_to_row_in_cell, + } + } + + /// Return the notebook content. + /// + /// This is the concatenation of all Python code cells. + pub(crate) fn content(&self) -> &str { + &self.content + } + + /// Return the Jupyter notebook index. + /// + /// The index is built only once when required. This is only used to + /// report diagnostics, so by that time all of the autofixes must have + /// been applied if `--fix` was passed. + pub(crate) fn index(&self) -> &JupyterIndex { + self.index.get_or_init(|| self.build_index()) + } + + /// Return the cell offsets for the concatenated source code corresponding + /// the Jupyter notebook. + pub(crate) fn cell_offsets(&self) -> &[TextSize] { + &self.cell_offsets + } + + /// Update the notebook with the given sourcemap and transformed content. + pub(crate) fn update(&mut self, source_map: &SourceMap, transformed: &str) { + // Cell offsets must be updated before updating the cell content as + // it depends on the offsets to extract the cell content. + self.update_cell_offsets(source_map); + self.update_cell_content(transformed); + self.content = transformed.to_string(); + } + + /// Return `true` if the notebook is a Python notebook, `false` otherwise. + pub fn is_python_notebook(&self) -> bool { + self.raw + .metadata + .language_info + .as_ref() + .map_or(true, |language| language.name == "python") } /// Write back with an indent of 1, just like black @@ -192,7 +376,7 @@ impl JupyterNotebook { // https://github.com/psf/black/blob/69ca0a4c7a365c5f5eea519a90980bab72cab764/src/black/__init__.py#LL1041 let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); let mut ser = serde_json::Serializer::with_formatter(&mut writer, formatter); - self.serialize(&mut ser)?; + self.raw.serialize(&mut ser)?; Ok(()) } } @@ -201,28 +385,42 @@ impl JupyterNotebook { mod test { use std::path::Path; + use anyhow::Result; + use test_case::test_case; + + use crate::jupyter::index::JupyterIndex; #[cfg(feature = "jupyter_notebook")] use crate::jupyter::is_jupyter_notebook; - use crate::jupyter::{JupyterIndex, JupyterNotebook}; + use crate::jupyter::schema::Cell; + use crate::jupyter::Notebook; + + use crate::test::test_resource_path; + + /// Read a Jupyter cell from the `resources/test/fixtures/jupyter/cell` directory. + fn read_jupyter_cell(path: impl AsRef) -> Result { + let path = test_resource_path("fixtures/jupyter/cell").join(path); + let contents = std::fs::read_to_string(path)?; + Ok(serde_json::from_str(&contents)?) + } #[test] fn test_valid() { let path = Path::new("resources/test/fixtures/jupyter/valid.ipynb"); - assert!(JupyterNotebook::read(path).is_ok()); + assert!(Notebook::read(path).is_ok()); } #[test] fn test_r() { // We can load this, it will be filtered out later let path = Path::new("resources/test/fixtures/jupyter/R.ipynb"); - assert!(JupyterNotebook::read(path).is_ok()); + assert!(Notebook::read(path).is_ok()); } #[test] fn test_invalid() { let path = Path::new("resources/test/fixtures/jupyter/invalid_extension.ipynb"); assert_eq!( - JupyterNotebook::read(path).unwrap_err().kind.body, + Notebook::read(path).unwrap_err().kind.body, "SyntaxError: Expected a Jupyter Notebook (.ipynb extension), \ which must be internally stored as JSON, \ but found a Python source file: \ @@ -230,19 +428,28 @@ mod test { ); let path = Path::new("resources/test/fixtures/jupyter/not_json.ipynb"); assert_eq!( - JupyterNotebook::read(path).unwrap_err().kind.body, + Notebook::read(path).unwrap_err().kind.body, "SyntaxError: A Jupyter Notebook (.ipynb) must internally be JSON, \ but this file isn't valid JSON: \ expected value at line 1 column 1" ); let path = Path::new("resources/test/fixtures/jupyter/wrong_schema.ipynb"); assert_eq!( - JupyterNotebook::read(path).unwrap_err().kind.body, + Notebook::read(path).unwrap_err().kind.body, "SyntaxError: This file does not match the schema expected of Jupyter Notebooks: \ missing field `cells` at line 1 column 2" ); } + #[test_case(Path::new("markdown.json"), false; "markdown")] + #[test_case(Path::new("only_magic.json"), false; "only_magic")] + #[test_case(Path::new("code_and_magic.json"), false; "code_and_magic")] + #[test_case(Path::new("only_code.json"), true; "only_code")] + fn test_is_valid_code_cell(path: &Path, expected: bool) -> Result<()> { + assert_eq!(read_jupyter_cell(path)?.is_valid_code_cell(), expected); + Ok(()) + } + #[test] #[cfg(feature = "jupyter_notebook")] fn inclusions() { @@ -256,10 +463,9 @@ mod test { #[test] fn test_concat_notebook() { let path = Path::new("resources/test/fixtures/jupyter/valid.ipynb"); - let notebook = JupyterNotebook::read(path).unwrap(); - let (contents, index) = notebook.index(); + let notebook = Notebook::read(path).unwrap(); assert_eq!( - contents, + notebook.content, r#"def unused_variable(): x = 1 y = 2 @@ -270,14 +476,30 @@ def mutable_argument(z=set()): print(f"cell two: {z}") mutable_argument() + + + + +print("after empty cells") "# ); assert_eq!( - index, - JupyterIndex { - row_to_cell: vec![0, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3], - row_to_row_in_cell: vec![0, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4], + notebook.index(), + &JupyterIndex { + row_to_cell: vec![0, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 5, 7, 7, 8], + row_to_row_in_cell: vec![0, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 1, 1, 2, 1], } ); + assert_eq!( + notebook.cell_offsets(), + &[ + 0.into(), + 90.into(), + 168.into(), + 169.into(), + 171.into(), + 198.into() + ] + ); } } diff --git a/crates/ruff/src/jupyter/schema.rs b/crates/ruff/src/jupyter/schema.rs index 33b120f7ee..bc61253e1b 100644 --- a/crates/ruff/src/jupyter/schema.rs +++ b/crates/ruff/src/jupyter/schema.rs @@ -1,26 +1,33 @@ -//! The JSON schema of a Jupyter Notebook, entrypoint is [`JupyterNotebook`] +//! The JSON schema of a Jupyter Notebook, entrypoint is [`RawNotebook`] //! //! Generated by from //! //! Jupyter Notebook v4.5 JSON schema. //! -//! The following changes were made to the generated version: `Cell::id` is optional because it -//! wasn't required ` -//! for `"additionalProperties": true` as preparation for round-trip support. +//! The following changes were made to the generated version: +//! * `Cell::id` is optional because it wasn't required ` for +//! `"additionalProperties": true` as preparation for round-trip support. +//! * `#[serde(skip_serializing_none)]` was added to all structs where one or +//! more fields were optional to avoid serializing `null` values. +//! * `Output::data` & `Cell::attachments` were changed to `Value` because +//! the scheme had `patternProperties`. use std::collections::{BTreeMap, HashMap}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use serde_with::skip_serializing_none; /// The root of the JSON of a Jupyter Notebook /// /// Generated by from /// /// Jupyter Notebook v4.5 JSON schema. -#[derive(Debug, Serialize, Deserialize)] -pub struct JupyterNotebook { +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct RawNotebook { /// Array of cells of the current notebook. pub cells: Vec, /// Notebook root-level metadata. @@ -38,10 +45,11 @@ pub struct JupyterNotebook { /// Notebook markdown cell. /// /// Notebook code cell. -#[derive(Debug, Serialize, Deserialize)] +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct Cell { - pub attachments: Option>>, + pub attachments: Option>>, /// String identifying the type of cell. pub cell_type: CellType, /// Technically, id isn't required (it's not even present) in schema v4.0 through v4.4, but @@ -58,7 +66,8 @@ pub struct Cell { } /// Cell-level metadata. -#[derive(Debug, Serialize, Deserialize)] +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq)] pub struct CellMetadata { /// Raw cell metadata format for nbconvert. pub format: Option, @@ -84,7 +93,8 @@ pub struct CellMetadata { /// Execution time for the code in the cell. This tracks time at which messages are received /// from iopub or shell channels -#[derive(Debug, Serialize, Deserialize)] +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct Execution { /// header.date (in ISO 8601 format) of iopub channel's execute_input message. It indicates @@ -113,10 +123,11 @@ pub struct Execution { /// Stream output from a code cell. /// /// Output of an error that occurred during code cell execution. -#[derive(Debug, Serialize, Deserialize)] +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct Output { - pub data: Option>, + pub data: Option>, /// A result's prompt number. pub execution_count: Option, pub metadata: Option>>, @@ -135,7 +146,8 @@ pub struct Output { } /// Notebook root-level metadata. -#[derive(Debug, Serialize, Deserialize)] +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq)] pub struct JupyterNotebookMetadata { /// The author(s) of the notebook document pub authors: Option>>, @@ -154,7 +166,7 @@ pub struct JupyterNotebookMetadata { } /// Kernel information. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, PartialEq)] pub struct Kernelspec { /// Name to display in UI. pub display_name: String, @@ -166,7 +178,8 @@ pub struct Kernelspec { } /// Kernel information. -#[derive(Debug, Serialize, Deserialize)] +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq)] pub struct LanguageInfo { /// The codemirror mode to use for code in this language. pub codemirror_mode: Option, @@ -189,7 +202,7 @@ pub struct LanguageInfo { /// Contents of the cell, represented as an array of lines. /// /// The stream's text output, represented as an array of strings. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, PartialEq)] #[serde(untagged)] pub enum SourceValue { String(String), @@ -197,7 +210,7 @@ pub enum SourceValue { } /// Whether the cell's output is scrolled, unscrolled, or autoscrolled. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, PartialEq)] #[serde(untagged)] pub enum ScrolledUnion { Bool(bool), @@ -210,7 +223,7 @@ pub enum ScrolledUnion { /// Contents of the cell, represented as an array of lines. /// /// The stream's text output, represented as an array of strings. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, PartialEq)] #[serde(untagged)] pub enum TextUnion { String(String), @@ -218,7 +231,7 @@ pub enum TextUnion { } /// The codemirror mode to use for code in this language. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, PartialEq)] #[serde(untagged)] pub enum CodemirrorMode { AnythingMap(HashMap>), @@ -236,14 +249,14 @@ pub enum CellType { Raw, } -#[derive(Debug, Serialize, Deserialize, Copy, Clone)] +#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq)] pub enum ScrolledEnum { #[serde(rename = "auto")] Auto, } /// Type of cell output. -#[derive(Debug, Serialize, Deserialize, Copy, Clone)] +#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq)] pub enum OutputType { #[serde(rename = "display_data")] DisplayData, diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs index 3ed0cd1d8d..4cba40f704 100644 --- a/crates/ruff/src/lib.rs +++ b/crates/ruff/src/lib.rs @@ -34,6 +34,7 @@ mod rule_redirects; mod rule_selector; pub mod rules; pub mod settings; +pub mod source_kind; #[cfg(any(test, fuzzing))] pub mod test; diff --git a/crates/ruff/src/linter.rs b/crates/ruff/src/linter.rs index bd8f713d87..ebdfded081 100644 --- a/crates/ruff/src/linter.rs +++ b/crates/ruff/src/linter.rs @@ -15,7 +15,7 @@ use ruff_python_ast::imports::ImportMap; use ruff_python_ast::source_code::{Indexer, Locator, SourceFileBuilder, Stylist}; use ruff_python_stdlib::path::is_python_stub_file; -use crate::autofix::fix_file; +use crate::autofix::{fix_file, FixResult}; use crate::checkers::ast::check_ast; use crate::checkers::filesystem::check_file_path; use crate::checkers::imports::check_imports; @@ -30,6 +30,7 @@ use crate::noqa::add_noqa; use crate::registry::{AsRule, Rule}; use crate::rules::pycodestyle; use crate::settings::{flags, Settings}; +use crate::source_kind::SourceKind; use crate::{directives, fs}; const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME"); @@ -77,6 +78,7 @@ pub fn check_path( directives: &Directives, settings: &Settings, noqa: flags::Noqa, + source_kind: Option<&SourceKind>, ) -> LinterResult<(Vec, Option)> { // Aggregate all diagnostics. let mut diagnostics = vec![]; @@ -157,6 +159,7 @@ pub fn check_path( stylist, path, package, + source_kind, ); imports = module_imports; diagnostics.extend(import_diagnostics); @@ -285,11 +288,17 @@ pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings &directives, settings, flags::Noqa::Disabled, + None, ); // Log any parse errors. if let Some(err) = error { - error!("{}", DisplayParseError::new(err, locator.to_source_code())); + // TODO(dhruvmanila): This should use `SourceKind`, update when + // `--add-noqa` is supported for Jupyter notebooks. + error!( + "{}", + DisplayParseError::new(err, locator.to_source_code(), None) + ); } // Add any missing `# noqa` pragmas. @@ -343,6 +352,7 @@ pub fn lint_only( &directives, settings, noqa, + None, ); result.map(|(diagnostics, imports)| { @@ -388,6 +398,7 @@ pub fn lint_fix<'a>( package: Option<&Path>, noqa: flags::Noqa, settings: &Settings, + source_kind: &mut SourceKind, ) -> Result> { let mut transformed = Cow::Borrowed(contents); @@ -433,6 +444,7 @@ pub fn lint_fix<'a>( &directives, settings, noqa, + Some(source_kind), ); if iterations == 0 { @@ -453,13 +465,22 @@ pub fn lint_fix<'a>( } // Apply autofix. - if let Some((fixed_contents, applied)) = fix_file(&result.data.0, &locator) { + if let Some(FixResult { + code: fixed_contents, + fixes: applied, + source_map, + }) = fix_file(&result.data.0, &locator) + { if iterations < MAX_ITERATIONS { // Count the number of fixed errors. for (rule, count) in applied { *fixed.entry(rule).or_default() += count; } + if let SourceKind::Jupyter(notebook) = source_kind { + notebook.update(&source_map, &fixed_contents); + } + // Store the fixed contents. transformed = Cow::Owned(fixed_contents); diff --git a/crates/ruff/src/logging.rs b/crates/ruff/src/logging.rs index ede7c8f43b..e281a35c9a 100644 --- a/crates/ruff/src/logging.rs +++ b/crates/ruff/src/logging.rs @@ -9,9 +9,11 @@ use log::Level; use once_cell::sync::Lazy; use rustpython_parser::{ParseError, ParseErrorType}; -use ruff_python_ast::source_code::SourceCode; +use ruff_python_ast::source_code::{OneIndexed, SourceCode, SourceLocation}; use crate::fs; +use crate::jupyter::Notebook; +use crate::source_kind::SourceKind; pub(crate) static WARNINGS: Lazy>> = Lazy::new(Mutex::default); @@ -137,25 +139,70 @@ pub fn set_up_logging(level: &LogLevel) -> Result<()> { pub struct DisplayParseError<'a> { error: ParseError, source_code: SourceCode<'a, 'a>, + source_kind: Option<&'a SourceKind>, } impl<'a> DisplayParseError<'a> { - pub fn new(error: ParseError, source_code: SourceCode<'a, 'a>) -> Self { - Self { error, source_code } + pub fn new( + error: ParseError, + source_code: SourceCode<'a, 'a>, + source_kind: Option<&'a SourceKind>, + ) -> Self { + Self { + error, + source_code, + source_kind, + } } } impl Display for DisplayParseError<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{header} {path}{colon}", + header = "Failed to parse".bold(), + path = fs::relativize_path(Path::new(&self.error.source_path)).bold(), + colon = ":".cyan(), + )?; + let source_location = self.source_code.source_location(self.error.offset); + // If we're working on a Jupyter notebook, translate the positions + // with respect to the cell and row in the cell. This is the same + // format as the `TextEmitter`. + let error_location = if let Some(jupyter_index) = self + .source_kind + .and_then(SourceKind::notebook) + .map(Notebook::index) + { + write!( + f, + "cell {cell}{colon}", + cell = jupyter_index + .cell(source_location.row.get()) + .unwrap_or_default(), + colon = ":".cyan(), + )?; + + SourceLocation { + row: OneIndexed::new( + jupyter_index + .cell_row(source_location.row.get()) + .unwrap_or(1) as usize, + ) + .unwrap(), + column: source_location.column, + } + } else { + source_location + }; + write!( f, - "{header} {path}{colon}{row}{colon}{column}{colon} {inner}", - header = "Failed to parse".bold(), - path = fs::relativize_path(Path::new(&self.error.source_path)).bold(), - row = source_location.row, - column = source_location.column, + "{row}{colon}{column}{colon} {inner}", + row = error_location.row, + column = error_location.column, colon = ":".cyan(), inner = &DisplayParseErrorType(&self.error.error) ) diff --git a/crates/ruff/src/message/grouped.rs b/crates/ruff/src/message/grouped.rs index 5f8bfa411d..56669f5c67 100644 --- a/crates/ruff/src/message/grouped.rs +++ b/crates/ruff/src/message/grouped.rs @@ -7,12 +7,13 @@ use colored::Colorize; use ruff_python_ast::source_code::OneIndexed; use crate::fs::relativize_path; -use crate::jupyter::JupyterIndex; +use crate::jupyter::{JupyterIndex, Notebook}; use crate::message::diff::calculate_print_width; use crate::message::text::{MessageCodeFrame, RuleCodeAndBody}; use crate::message::{ group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation, }; +use crate::source_kind::SourceKind; #[derive(Default)] pub struct GroupedEmitter { @@ -65,7 +66,10 @@ impl Emitter for GroupedEmitter { writer, "{}", DisplayGroupedMessage { - jupyter_index: context.jupyter_index(message.filename()), + jupyter_index: context + .source_kind(message.filename()) + .and_then(SourceKind::notebook) + .map(Notebook::index), message, show_fix_status: self.show_fix_status, show_source: self.show_source, @@ -114,11 +118,15 @@ impl Display for DisplayGroupedMessage<'_> { write!( f, "cell {cell}{sep}", - cell = jupyter_index.row_to_cell[start_location.row.get()], + cell = jupyter_index + .cell(start_location.row.get()) + .unwrap_or_default(), sep = ":".cyan() )?; ( - jupyter_index.row_to_row_in_cell[start_location.row.get()] as usize, + jupyter_index + .cell_row(start_location.row.get()) + .unwrap_or(1) as usize, start_location.column.get(), ) } else { diff --git a/crates/ruff/src/message/mod.rs b/crates/ruff/src/message/mod.rs index 072bf79fae..be5a03afae 100644 --- a/crates/ruff/src/message/mod.rs +++ b/crates/ruff/src/message/mod.rs @@ -6,6 +6,7 @@ use std::ops::Deref; use ruff_text_size::{TextRange, TextSize}; use rustc_hash::FxHashMap; +use crate::source_kind::SourceKind; pub use azure::AzureEmitter; pub use github::GithubEmitter; pub use gitlab::GitlabEmitter; @@ -17,8 +18,6 @@ use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix}; use ruff_python_ast::source_code::{SourceFile, SourceLocation}; pub use text::TextEmitter; -use crate::jupyter::JupyterIndex; - mod azure; mod diff; mod github; @@ -127,22 +126,23 @@ pub trait Emitter { /// Context passed to [`Emitter`]. pub struct EmitterContext<'a> { - jupyter_indices: &'a FxHashMap, + source_kind: &'a FxHashMap, } impl<'a> EmitterContext<'a> { - pub fn new(jupyter_indices: &'a FxHashMap) -> Self { - Self { jupyter_indices } + pub fn new(source_kind: &'a FxHashMap) -> Self { + Self { source_kind } } /// Tests if the file with `name` is a jupyter notebook. pub fn is_jupyter_notebook(&self, name: &str) -> bool { - self.jupyter_indices.contains_key(name) + self.source_kind + .get(name) + .map_or(false, SourceKind::is_jupyter) } - /// Returns the file's [`JupyterIndex`] if the file `name` is a jupyter notebook. - pub fn jupyter_index(&self, name: &str) -> Option<&JupyterIndex> { - self.jupyter_indices.get(name) + pub fn source_kind(&self, name: &str) -> Option<&SourceKind> { + self.source_kind.get(name) } } @@ -226,8 +226,8 @@ def fibonacci(n): emitter: &mut dyn Emitter, messages: &[Message], ) -> String { - let indices = FxHashMap::default(); - let context = EmitterContext::new(&indices); + let source_kinds = FxHashMap::default(); + let context = EmitterContext::new(&source_kinds); let mut output: Vec = Vec::new(); emitter.emit(&mut output, messages, &context).unwrap(); diff --git a/crates/ruff/src/message/text.rs b/crates/ruff/src/message/text.rs index e15cd3d63a..d82bb47770 100644 --- a/crates/ruff/src/message/text.rs +++ b/crates/ruff/src/message/text.rs @@ -11,10 +11,12 @@ use ruff_text_size::{TextRange, TextSize}; use ruff_python_ast::source_code::{OneIndexed, SourceLocation}; use crate::fs::relativize_path; +use crate::jupyter::Notebook; use crate::line_width::{LineWidth, TabSize}; use crate::message::diff::Diff; use crate::message::{Emitter, EmitterContext, Message}; use crate::registry::AsRule; +use crate::source_kind::SourceKind; bitflags! { #[derive(Default)] @@ -72,25 +74,32 @@ impl Emitter for TextEmitter { let start_location = message.compute_start_location(); // Check if we're working on a jupyter notebook and translate positions with cell accordingly - let diagnostic_location = - if let Some(jupyter_index) = context.jupyter_index(message.filename()) { - write!( - writer, - "cell {cell}{sep}", - cell = jupyter_index.row_to_cell[start_location.row.get()], - sep = ":".cyan(), - )?; + let diagnostic_location = if let Some(jupyter_index) = context + .source_kind(message.filename()) + .and_then(SourceKind::notebook) + .map(Notebook::index) + { + write!( + writer, + "cell {cell}{sep}", + cell = jupyter_index + .cell(start_location.row.get()) + .unwrap_or_default(), + sep = ":".cyan(), + )?; - SourceLocation { - row: OneIndexed::new( - jupyter_index.row_to_row_in_cell[start_location.row.get()] as usize, - ) - .unwrap(), - column: start_location.column, - } - } else { - start_location - }; + SourceLocation { + row: OneIndexed::new( + jupyter_index + .cell_row(start_location.row.get()) + .unwrap_or(1) as usize, + ) + .unwrap(), + column: start_location.column, + } + } else { + start_location + }; writeln!( writer, diff --git a/crates/ruff/src/rules/isort/block.rs b/crates/ruff/src/rules/isort/block.rs index f01511a154..8b8203bd3d 100644 --- a/crates/ruff/src/rules/isort/block.rs +++ b/crates/ruff/src/rules/isort/block.rs @@ -5,7 +5,9 @@ use ruff_python_ast::source_code::Locator; use ruff_python_ast::statement_visitor::StatementVisitor; use crate::directives::IsortDirectives; +use crate::jupyter::Notebook; use crate::rules::isort::helpers; +use crate::source_kind::SourceKind; /// A block of imports within a Python module. #[derive(Debug, Default)] @@ -29,6 +31,7 @@ pub(crate) struct BlockBuilder<'a> { is_stub: bool, blocks: Vec>, splits: &'a [TextSize], + cell_offsets: Option<&'a [TextSize]>, exclusions: &'a [TextRange], nested: bool, } @@ -38,6 +41,7 @@ impl<'a> BlockBuilder<'a> { locator: &'a Locator<'a>, directives: &'a IsortDirectives, is_stub: bool, + source_kind: Option<&'a SourceKind>, ) -> Self { Self { locator, @@ -46,6 +50,9 @@ impl<'a> BlockBuilder<'a> { splits: &directives.splits, exclusions: &directives.exclusions, nested: false, + cell_offsets: source_kind + .and_then(SourceKind::notebook) + .map(Notebook::cell_offsets), } } @@ -129,6 +136,22 @@ where } } + // Track Jupyter notebook cell offsets as splits. This will make sure + // that each cell is considered as an individual block to organize the + // imports in. Thus, not creating an edit which spans across multiple + // cells. + if let Some(cell_offsets) = self.cell_offsets { + for (index, split) in cell_offsets.iter().enumerate() { + if stmt.start() >= *split { + // We don't want any extra newlines between cells. + self.finalize(None); + self.cell_offsets = Some(&cell_offsets[index + 1..]); + } else { + break; + } + } + } + // Test if the statement is in an excluded range let mut is_excluded = false; for (index, exclusion) in self.exclusions.iter().enumerate() { diff --git a/crates/ruff/src/rules/pyflakes/mod.rs b/crates/ruff/src/rules/pyflakes/mod.rs index a27d8c190d..11c6414b7f 100644 --- a/crates/ruff/src/rules/pyflakes/mod.rs +++ b/crates/ruff/src/rules/pyflakes/mod.rs @@ -344,6 +344,7 @@ mod tests { &directives, &settings, flags::Noqa::Enabled, + None, ); diagnostics.sort_by_key(Diagnostic::start); let actual = diagnostics diff --git a/crates/ruff/src/source_kind.rs b/crates/ruff/src/source_kind.rs new file mode 100644 index 0000000000..b266e2df57 --- /dev/null +++ b/crates/ruff/src/source_kind.rs @@ -0,0 +1,26 @@ +use crate::jupyter::Notebook; + +#[derive(Debug, PartialEq, is_macro::Is)] +pub enum SourceKind { + Python(String), + Jupyter(Notebook), +} + +impl SourceKind { + /// Return the source content. + pub fn content(&self) -> &str { + match self { + SourceKind::Python(content) => content, + SourceKind::Jupyter(notebook) => notebook.content(), + } + } + + /// Return the [`Notebook`] if the source kind is [`SourceKind::Jupyter`]. + pub fn notebook(&self) -> Option<&Notebook> { + if let Self::Jupyter(notebook) = self { + Some(notebook) + } else { + None + } + } +} diff --git a/crates/ruff/src/test.rs b/crates/ruff/src/test.rs index 60a4eb9739..e0913256f5 100644 --- a/crates/ruff/src/test.rs +++ b/crates/ruff/src/test.rs @@ -13,7 +13,7 @@ use rustpython_parser::lexer::LexResult; use ruff_diagnostics::{AutofixKind, Diagnostic}; use ruff_python_ast::source_code::{Indexer, Locator, SourceFileBuilder, Stylist}; -use crate::autofix::fix_file; +use crate::autofix::{fix_file, FixResult}; use crate::directives; use crate::linter::{check_path, LinterResult}; use crate::message::{Emitter, EmitterContext, Message, TextEmitter}; @@ -81,6 +81,7 @@ fn test_contents(contents: &str, path: &Path, settings: &Settings) -> Vec Vec Vec, pub(crate) fixed: FxHashMap, pub(crate) imports: ImportMap, - /// Jupyter notebook indexing table for each input file that is a jupyter notebook - /// so we can rewrite the diagnostics in the end - pub(crate) jupyter_index: FxHashMap, + pub(crate) source_kind: FxHashMap, } impl Diagnostics { @@ -42,7 +41,7 @@ impl Diagnostics { messages, fixed: FxHashMap::default(), imports, - jupyter_index: FxHashMap::default(), + source_kind: FxHashMap::default(), } } } @@ -62,20 +61,15 @@ impl AddAssign for Diagnostics { } } } - self.jupyter_index.extend(other.jupyter_index); + self.source_kind.extend(other.source_kind); } } /// Returns either an indexed python jupyter notebook or a diagnostic (which is empty if we skip) -fn load_jupyter_notebook(path: &Path) -> Result<(String, JupyterIndex), Box> { - let notebook = match JupyterNotebook::read(path) { +fn load_jupyter_notebook(path: &Path) -> Result> { + let notebook = match Notebook::read(path) { Ok(notebook) => { - if !notebook - .metadata - .language_info - .as_ref() - .map_or(true, |language| language.name == "python") - { + if !notebook.is_python_notebook() { // Not a python notebook, this could e.g. be an R notebook which we want to just skip debug!( "Skipping {} because it's not a Python notebook", @@ -98,7 +92,7 @@ fn load_jupyter_notebook(path: &Path) -> Result<(String, JupyterIndex), Box (contents, Some(jupyter_index)), - Err(diagnostics) => return Ok(*diagnostics), + Ok(notebook) => SourceKind::Jupyter(notebook), + Err(diagnostic) => return Ok(*diagnostic), } } else { - (std::fs::read_to_string(path)?, None) + SourceKind::Python(std::fs::read_to_string(path)?) }; + let contents = source_kind.content().to_string(); + // Lint the file. let ( LinterResult { @@ -165,11 +161,24 @@ pub(crate) fn lint_path( result, transformed, fixed, - }) = lint_fix(&contents, path, package, noqa, &settings.lib) - { + }) = lint_fix( + &contents, + path, + package, + noqa, + &settings.lib, + &mut source_kind, + ) { if !fixed.is_empty() { if matches!(autofix, flags::FixMode::Apply) { - write(path, transformed.as_bytes())?; + match &source_kind { + SourceKind::Python(_) => { + write(path, transformed.as_bytes())?; + } + SourceKind::Jupyter(notebook) => { + notebook.write(path)?; + } + } } else if matches!(autofix, flags::FixMode::Diff) { let mut stdout = io::stdout().lock(); TextDiff::from_lines(contents.as_str(), &transformed) @@ -200,7 +209,8 @@ pub(crate) fn lint_path( "{}", DisplayParseError::new( err, - SourceCode::new(&contents, &LineIndex::from_source_text(&contents)) + SourceCode::new(&contents, &LineIndex::from_source_text(&contents)), + Some(&source_kind), ) ); @@ -215,25 +225,16 @@ pub(crate) fn lint_path( } } - let jupyter_index = match jupyter_index { - None => FxHashMap::default(), - Some(jupyter_index) => { - let mut index = FxHashMap::default(); - index.insert( - path.to_str() - .ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))? - .to_string(), - jupyter_index, - ); - index - } - }; - Ok(Diagnostics { messages, fixed: FxHashMap::from_iter([(fs::relativize_path(path), fixed)]), imports, - jupyter_index, + source_kind: FxHashMap::from_iter([( + path.to_str() + .ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))? + .to_string(), + source_kind, + )]), }) } @@ -247,6 +248,7 @@ pub(crate) fn lint_stdin( noqa: flags::Noqa, autofix: flags::FixMode, ) -> Result { + let mut source_kind = SourceKind::Python(contents.to_string()); // Lint the inputs. let ( LinterResult { @@ -265,6 +267,7 @@ pub(crate) fn lint_stdin( package, noqa, settings, + &mut source_kind, ) { if matches!(autofix, flags::FixMode::Apply) { // Write the contents to stdout, regardless of whether any errors were fixed. @@ -332,7 +335,7 @@ pub(crate) fn lint_stdin( fixed, )]), imports, - jupyter_index: FxHashMap::default(), + source_kind: FxHashMap::default(), }) } diff --git a/crates/ruff_cli/src/printer.rs b/crates/ruff_cli/src/printer.rs index 81d4c95f4e..34e15fbb11 100644 --- a/crates/ruff_cli/src/printer.rs +++ b/crates/ruff_cli/src/printer.rs @@ -178,7 +178,7 @@ impl Printer { return Ok(()); } - let context = EmitterContext::new(&diagnostics.jupyter_index); + let context = EmitterContext::new(&diagnostics.source_kind); match self.format { SerializationFormat::Json => { @@ -356,7 +356,7 @@ impl Printer { writeln!(stdout)?; } - let context = EmitterContext::new(&diagnostics.jupyter_index); + let context = EmitterContext::new(&diagnostics.source_kind); TextEmitter::default() .with_show_fix_status(show_fix_status(self.autofix_level)) .with_show_source(self.flags.contains(Flags::SHOW_SOURCE)) diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index 3b4a66db5e..dce7b79797 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -204,6 +204,7 @@ pub fn check(contents: &str, options: JsValue) -> Result { &directives, &settings, flags::Noqa::Enabled, + None, ); let source_code = locator.to_source_code(); From d3aa81a474b595b3a3c3f9022a82df9c34737873 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Mon, 12 Jun 2023 16:33:18 +0200 Subject: [PATCH 011/447] Suggest combining async with statements (#5022) ## Summary Previously the rule for SIM117 explicitly ignored `async with` statements as it would incorrectly suggestion to merge `async with` and regular `with` statements as reported in issue #1902. This partially reverts the fix for that (commit 396be5edeac0c5724de87e93c5a885dacf201f05) by enabling the rules for `async with` statements again, but with a check ensuring that the statements are both of the same kind, i.e. both `async with` or both (just) `with` statements. Closes #3025 ## Test Plan Updated and existing test and added a new test case from #3025. --- .../test/fixtures/flake8_simplify/SIM117.py | 29 ++++++++++++-- crates/ruff/src/checkers/ast/mod.rs | 3 +- .../rules/flake8_simplify/rules/ast_with.rs | 39 +++++++++++++++++-- ...ke8_simplify__tests__SIM117_SIM117.py.snap | 39 ++++++++++++++++++- 4 files changed, 99 insertions(+), 11 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_simplify/SIM117.py b/crates/ruff/resources/test/fixtures/flake8_simplify/SIM117.py index 3c99535e43..4053754f02 100644 --- a/crates/ruff/resources/test/fixtures/flake8_simplify/SIM117.py +++ b/crates/ruff/resources/test/fixtures/flake8_simplify/SIM117.py @@ -33,17 +33,17 @@ with A() as a: print("hello") a() -# OK +# OK, can't merge async with and with. async with A() as a: with B() as b: print("hello") -# OK +# OK, can't merge async with and with. with A() as a: async with B() as b: print("hello") -# OK +# SIM117 async with A() as a: async with B() as b: print("hello") @@ -99,4 +99,25 @@ with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as a: # SIM117 (not auto-fixable too long) with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ890") as a: with B("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as b: - print("hello") \ No newline at end of file + print("hello") + +# From issue #3025. +async def main(): + async with A() as a: # SIM117. + async with B() as b: + print("async-inside!") + + return 0 + +# OK. Can't merge across different kinds of with statements. +with a as a2: + async with b as b2: + with c as c2: + async with d as d2: + f(a2, b2, c2, d2) + +# OK. Can't merge across different kinds of with statements. +async with b as b2: + with c as c2: + async with d as d2: + f(b2, c2, d2) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 57c497456d..7381a5f7b7 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1514,7 +1514,8 @@ where pygrep_hooks::rules::non_existent_mock_method(self, test); } } - Stmt::With(ast::StmtWith { items, body, .. }) => { + 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); } diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs index cf88a3ba94..7e1cc3a43d 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs @@ -60,9 +60,14 @@ impl Violation for MultipleWithStatements { } } -fn find_last_with(body: &[Stmt]) -> Option<(&[Withitem], &[Stmt])> { - let [Stmt::With(ast::StmtWith { items, body, .. })] = body else { return None }; - find_last_with(body).or(Some((items, body))) +/// Returns a boolean indicating whether it's an async with statement, the items +/// and body. +fn next_with(body: &[Stmt]) -> Option<(bool, &[Withitem], &[Stmt])> { + match body { + [Stmt::With(ast::StmtWith { items, body, .. })] => Some((false, items, body)), + [Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. })] => Some((true, items, body)), + _ => None, + } } /// SIM117 @@ -72,12 +77,38 @@ pub(crate) fn multiple_with_statements( with_body: &[Stmt], with_parent: Option<&Stmt>, ) { + // Make sure we fix from top to bottom for nested with statements, e.g. for + // ```python + // with A(): + // with B(): + // with C(): + // print("hello") + // ``` + // suggests + // ```python + // with A(), B(): + // with C(): + // print("hello") + // ``` + // but not the following + // ```python + // with A(): + // with B(), C(): + // print("hello") + // ``` if let Some(Stmt::With(ast::StmtWith { body, .. })) = with_parent { if body.len() == 1 { return; } } - if let Some((items, body)) = find_last_with(with_body) { + + if let Some((is_async, items, body)) = next_with(with_body) { + if is_async != with_stmt.is_async_with_stmt() { + // One of the statements is an async with, while the other is not, + // we can't merge those statements. + return; + } + let last_item = items.last().expect("Expected items to be non-empty"); let colon = first_colon_range( TextRange::new( diff --git a/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM117_SIM117.py.snap b/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM117_SIM117.py.snap index c3cf611f84..ceb934c7e9 100644 --- a/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM117_SIM117.py.snap +++ b/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM117_SIM117.py.snap @@ -27,8 +27,8 @@ SIM117.py:7:1: SIM117 [*] Use a single `with` statement with multiple contexts i 6 | # SIM117 7 | / with A(): 8 | | with B(): - 9 | | with C(): - | |_________________^ SIM117 + | |_____________^ SIM117 + 9 | with C(): 10 | print("hello") | = help: Combine `with` statements @@ -85,6 +85,29 @@ SIM117.py:19:1: SIM117 [*] Use a single `with` statement with multiple contexts 24 23 | # OK 25 24 | with A() as a: +SIM117.py:47:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements + | +46 | # SIM117 +47 | / async with A() as a: +48 | | async with B() as b: + | |________________________^ SIM117 +49 | print("hello") + | + = help: Combine `with` statements + +ℹ Suggested fix +44 44 | print("hello") +45 45 | +46 46 | # SIM117 +47 |-async with A() as a: +48 |- async with B() as b: +49 |- print("hello") + 47 |+async with A() as a, B() as b: + 48 |+ print("hello") +50 49 | +51 50 | while True: +52 51 | # SIM117 + SIM117.py:53:5: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements | 51 | while True: @@ -249,4 +272,16 @@ SIM117.py:100:1: SIM117 Use a single `with` statement with multiple contexts ins | = help: Combine `with` statements +SIM117.py:106:5: SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements + | +104 | # From issue #3025. +105 | async def main(): +106 | async with A() as a: # SIM117. + | _____^ +107 | | async with B() as b: + | |____________________________^ SIM117 +108 | print("async-inside!") + | + = help: Combine `with` statements + From 9db622afe144338a8b29e35d06c57622d947d091 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 12 Jun 2023 11:31:50 -0400 Subject: [PATCH 012/447] Allow `Options`-to-`Settings` conversion to use `TryFrom` (#5025) ## Summary This avoids a bad `expect()` call in the `copyright` conversion. ## Test Plan `cargo test` --- crates/ruff/src/rules/copyright/settings.rs | 19 +++-- crates/ruff/src/settings/mod.rs | 92 +++++++++++++++------ 2 files changed, 79 insertions(+), 32 deletions(-) diff --git a/crates/ruff/src/rules/copyright/settings.rs b/crates/ruff/src/rules/copyright/settings.rs index d3849a8752..f3e400ca25 100644 --- a/crates/ruff/src/rules/copyright/settings.rs +++ b/crates/ruff/src/rules/copyright/settings.rs @@ -68,18 +68,19 @@ impl Default for Settings { } } -impl From for Settings { - fn from(options: Options) -> Self { - Self { - notice_rgx: options +impl TryFrom for Settings { + type Error = anyhow::Error; + + fn try_from(value: Options) -> Result { + Ok(Self { + notice_rgx: value .notice_rgx .map(|pattern| Regex::new(&pattern)) - .transpose() - .expect("Invalid `notice-rgx`") + .transpose()? .unwrap_or_else(|| COPYRIGHT.clone()), - author: options.author, - min_file_size: options.min_file_size.unwrap_or_default(), - } + author: value.author, + min_file_size: value.min_file_size.unwrap_or_default(), + }) } } diff --git a/crates/ruff/src/settings/mod.rs b/crates/ruff/src/settings/mod.rs index c3878d7eda..b98c010dd4 100644 --- a/crates/ruff/src/settings/mod.rs +++ b/crates/ruff/src/settings/mod.rs @@ -192,51 +192,97 @@ impl Settings { // Plugins flake8_annotations: config .flake8_annotations - .map(Into::into) + .map(flake8_annotations::settings::Settings::from) + .unwrap_or_default(), + flake8_bandit: config + .flake8_bandit + .map(flake8_bandit::settings::Settings::from) + .unwrap_or_default(), + flake8_bugbear: config + .flake8_bugbear + .map(flake8_bugbear::settings::Settings::from) + .unwrap_or_default(), + flake8_builtins: config + .flake8_builtins + .map(flake8_builtins::settings::Settings::from) .unwrap_or_default(), - flake8_bandit: config.flake8_bandit.map(Into::into).unwrap_or_default(), - flake8_bugbear: config.flake8_bugbear.map(Into::into).unwrap_or_default(), - flake8_builtins: config.flake8_builtins.map(Into::into).unwrap_or_default(), flake8_comprehensions: config .flake8_comprehensions - .map(Into::into) + .map(flake8_comprehensions::settings::Settings::from) + .unwrap_or_default(), + copyright: config + .copyright + .map(copyright::settings::Settings::try_from) + .transpose()? + .unwrap_or_default(), + flake8_errmsg: config + .flake8_errmsg + .map(flake8_errmsg::settings::Settings::from) .unwrap_or_default(), - copyright: config.copyright.map(Into::into).unwrap_or_default(), - flake8_errmsg: config.flake8_errmsg.map(Into::into).unwrap_or_default(), flake8_implicit_str_concat: config .flake8_implicit_str_concat - .map(Into::into) + .map(flake8_implicit_str_concat::settings::Settings::from) .unwrap_or_default(), flake8_import_conventions: config .flake8_import_conventions - .map(Into::into) + .map(flake8_import_conventions::settings::Settings::from) .unwrap_or_default(), flake8_pytest_style: config .flake8_pytest_style - .map(Into::into) + .map(flake8_pytest_style::settings::Settings::from) + .unwrap_or_default(), + flake8_quotes: config + .flake8_quotes + .map(flake8_quotes::settings::Settings::from) + .unwrap_or_default(), + flake8_self: config + .flake8_self + .map(flake8_self::settings::Settings::from) .unwrap_or_default(), - flake8_quotes: config.flake8_quotes.map(Into::into).unwrap_or_default(), - flake8_self: config.flake8_self.map(Into::into).unwrap_or_default(), flake8_tidy_imports: config .flake8_tidy_imports - .map(Into::into) + .map(flake8_tidy_imports::settings::Settings::from) .unwrap_or_default(), flake8_type_checking: config .flake8_type_checking - .map(Into::into) + .map(flake8_type_checking::settings::Settings::from) .unwrap_or_default(), flake8_unused_arguments: config .flake8_unused_arguments - .map(Into::into) + .map(flake8_unused_arguments::settings::Settings::from) + .unwrap_or_default(), + flake8_gettext: config + .flake8_gettext + .map(flake8_gettext::settings::Settings::from) + .unwrap_or_default(), + isort: config + .isort + .map(isort::settings::Settings::from) + .unwrap_or_default(), + mccabe: config + .mccabe + .map(mccabe::settings::Settings::from) + .unwrap_or_default(), + pep8_naming: config + .pep8_naming + .map(pep8_naming::settings::Settings::from) + .unwrap_or_default(), + pycodestyle: config + .pycodestyle + .map(pycodestyle::settings::Settings::from) + .unwrap_or_default(), + pydocstyle: config + .pydocstyle + .map(pydocstyle::settings::Settings::from) + .unwrap_or_default(), + pyflakes: config + .pyflakes + .map(pyflakes::settings::Settings::from) + .unwrap_or_default(), + pylint: config + .pylint + .map(pylint::settings::Settings::from) .unwrap_or_default(), - flake8_gettext: config.flake8_gettext.map(Into::into).unwrap_or_default(), - isort: config.isort.map(Into::into).unwrap_or_default(), - mccabe: config.mccabe.map(Into::into).unwrap_or_default(), - pep8_naming: config.pep8_naming.map(Into::into).unwrap_or_default(), - pycodestyle: config.pycodestyle.map(Into::into).unwrap_or_default(), - pydocstyle: config.pydocstyle.map(Into::into).unwrap_or_default(), - pyflakes: config.pyflakes.map(Into::into).unwrap_or_default(), - pylint: config.pylint.map(Into::into).unwrap_or_default(), }) } From 70e6c212d902fa1f0f3bc989e073cebba4f2f830 Mon Sep 17 00:00:00 2001 From: Addison Crump Date: Mon, 12 Jun 2023 18:10:23 +0200 Subject: [PATCH 013/447] Improve ruff_parse_simple to find UTF-8 violations (#5008) Improves the `ruff_parse_simple` fuzz harness by adding checks for parsed locations to ensure they all lie on UTF-8 character boundaries. This will allow for faster identification of issues like #5004. This also adds additional details for Apple M1 users and clarifies the importance of using `init-fuzzer.sh` (thanks for the feedback, @jasikpark :slightly_smiling_face:). --- crates/ruff_python_ast/src/prelude.rs | 1 + .../src/source_code/generator.rs | 2 +- fuzz/.gitignore | 1 + fuzz/Cargo.lock | 1965 ----------------- fuzz/README.md | 14 +- fuzz/fuzz_targets/ruff_parse_simple.rs | 50 +- 6 files changed, 57 insertions(+), 1976 deletions(-) delete mode 100644 fuzz/Cargo.lock diff --git a/crates/ruff_python_ast/src/prelude.rs b/crates/ruff_python_ast/src/prelude.rs index 76505ecd01..b80837d83c 100644 --- a/crates/ruff_python_ast/src/prelude.rs +++ b/crates/ruff_python_ast/src/prelude.rs @@ -1,2 +1,3 @@ pub use crate::node::AstNode; pub use rustpython_ast::*; +pub use rustpython_parser::*; diff --git a/crates/ruff_python_ast/src/source_code/generator.rs b/crates/ruff_python_ast/src/source_code/generator.rs index fbdedb4bcc..7f7e64047c 100644 --- a/crates/ruff_python_ast/src/source_code/generator.rs +++ b/crates/ruff_python_ast/src/source_code/generator.rs @@ -183,7 +183,7 @@ impl<'a> Generator<'a> { self.buffer } - pub(crate) fn unparse_suite(&mut self, suite: &Suite) { + pub fn unparse_suite(&mut self, suite: &Suite) { for stmt in suite { self.unparse_stmt(stmt); } diff --git a/fuzz/.gitignore b/fuzz/.gitignore index 0ee1bebe7f..0aae9a3e34 100644 --- a/fuzz/.gitignore +++ b/fuzz/.gitignore @@ -1,2 +1,3 @@ artifacts/ corpus/ruff_fix_validity +Cargo.lock diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock deleted file mode 100644 index 677868be0c..0000000000 --- a/fuzz/Cargo.lock +++ /dev/null @@ -1,1965 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" - -[[package]] -name = "aho-corasick" -version = "0.7.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" -dependencies = [ - "memchr", -] - -[[package]] -name = "aho-corasick" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" -dependencies = [ - "memchr", -] - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "annotate-snippets" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7021ce4924a3f25f802b2cccd1af585e39ea1a363a1aa2e72afe54b67a3a7a7" - -[[package]] -name = "annotate-snippets" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b9d411ecbaf79885c6df4d75fff75858d5995ff25385657a28af47e82f9c36" -dependencies = [ - "unicode-width", - "yansi-term", -] - -[[package]] -name = "anstream" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is-terminal", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" - -[[package]] -name = "anstyle-parse" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "anstyle-wincon" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" -dependencies = [ - "anstyle", - "windows-sys", -] - -[[package]] -name = "anyhow" -version = "1.0.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" - -[[package]] -name = "arbitrary" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e" -dependencies = [ - "derive_arbitrary", -] - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84" - -[[package]] -name = "bstr" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a246e68bb43f6cd9db24bea052a53e40405417c5fb372e3d1a8a7f770a564ef5" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "bumpalo" -version = "3.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" - -[[package]] -name = "cc" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" -dependencies = [ - "jobserver", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chic" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5b5db619f3556839cb2223ae86ff3f9a09da2c5013be42bc9af08c9589bf70c" -dependencies = [ - "annotate-snippets 0.6.1", -] - -[[package]] -name = "chrono" -version = "0.4.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "time", - "wasm-bindgen", - "winapi", -] - -[[package]] -name = "clap" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "401a4694d2bf92537b6867d94de48c4842089645fdcdf6c71865b175d836e9c2" -dependencies = [ - "clap_builder", - "clap_derive", - "once_cell", -] - -[[package]] -name = "clap_builder" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72394f3339a76daf211e57d4bcb374410f3965dcc606dd0e03738c7888766980" -dependencies = [ - "anstream", - "anstyle", - "bitflags 1.3.2", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.18", -] - -[[package]] -name = "clap_lex" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" - -[[package]] -name = "colorchoice" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" - -[[package]] -name = "colored" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" -dependencies = [ - "atty", - "lazy_static", - "winapi", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" - -[[package]] -name = "countme" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" - -[[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - -[[package]] -name = "derive_arbitrary" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e0efad4403bfc52dc201159c4b842a246a14b98c64b55dfd0f2d89729dfeb8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.18", -] - -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys", -] - -[[package]] -name = "drop_bomb" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1" - -[[package]] -name = "either" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" - -[[package]] -name = "errno" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "fern" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" -dependencies = [ - "log", -] - -[[package]] -name = "filetime" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "windows-sys", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "getrandom" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - -[[package]] -name = "globset" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" -dependencies = [ - "aho-corasick 0.7.20", - "bstr", - "fnv", - "log", - "regex", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" - -[[package]] -name = "hexf-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" - -[[package]] -name = "iana-time-zone" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "ignore" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" -dependencies = [ - "globset", - "lazy_static", - "log", - "memchr", - "regex", - "same-file", - "thread_local", - "walkdir", - "winapi-util", -] - -[[package]] -name = "imperative" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f92123bf2fe0d9f1b5df1964727b970ca3b2d0203d47cf97fb1f36d856b6398" -dependencies = [ - "phf", - "rust-stemmers", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown", - "serde", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.1", - "libc", - "windows-sys", -] - -[[package]] -name = "is-macro" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7d079e129b77477a49c5c4f1cfe9ce6c2c909ef52520693e8e811a714c7b20" -dependencies = [ - "Inflector", - "pmutil", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "is-terminal" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" -dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", - "rustix", - "windows-sys", -] - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" - -[[package]] -name = "jobserver" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" -dependencies = [ - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "lalrpop-util" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f35c735096c0293d313e8f2a641627472b83d01b937177fe76e5e2708d31e0d" - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "lexical-parse-float" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f" -dependencies = [ - "lexical-parse-integer", - "lexical-util", - "static_assertions", -] - -[[package]] -name = "lexical-parse-integer" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9" -dependencies = [ - "lexical-util", - "static_assertions", -] - -[[package]] -name = "lexical-util" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc" -dependencies = [ - "static_assertions", -] - -[[package]] -name = "libc" -version = "0.2.146" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" - -[[package]] -name = "libcst" -version = "0.1.0" -source = "git+https://github.com/charliermarsh/LibCST?rev=80e4c1399f95e5beb532fdd1e209ad2dbb470438#80e4c1399f95e5beb532fdd1e209ad2dbb470438" -dependencies = [ - "chic", - "itertools", - "libcst_derive", - "once_cell", - "paste", - "peg", - "regex", - "thiserror", -] - -[[package]] -name = "libcst_derive" -version = "0.1.0" -source = "git+https://github.com/charliermarsh/LibCST?rev=80e4c1399f95e5beb532fdd1e209ad2dbb470438#80e4c1399f95e5beb532fdd1e209ad2dbb470438" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "libfuzzer-sys" -version = "0.4.6" -source = "git+https://github.com/rust-fuzz/libfuzzer#1221c356e993b9f82d1ccd152f1c7636468758d2" -dependencies = [ - "arbitrary", - "cc", - "once_cell", -] - -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - -[[package]] -name = "log" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" - -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "natord" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" - -[[package]] -name = "nextest-workspace-hack" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d906846a98739ed9d73d66e62c2641eef8321f1734b7a1156ab045a0248fb2b3" - -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - -[[package]] -name = "num-bigint" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "paste" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" - -[[package]] -name = "path-absolutize" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43eb3595c63a214e1b37b44f44b0a84900ef7ae0b4c5efce59e123d246d7a0de" -dependencies = [ - "path-dedot", -] - -[[package]] -name = "path-dedot" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d55e486337acb9973cdea3ec5638c1b3bcb22e573b2b7b41969e0c744d5a15e" -dependencies = [ - "once_cell", -] - -[[package]] -name = "pathdiff" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" - -[[package]] -name = "peg" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07f2cafdc3babeebc087e499118343442b742cc7c31b4d054682cc598508554" -dependencies = [ - "peg-macros", - "peg-runtime", -] - -[[package]] -name = "peg-macros" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a90084dc05cf0428428e3d12399f39faad19b0909f64fb9170c9fdd6d9cd49b" -dependencies = [ - "peg-runtime", - "proc-macro2", - "quote", -] - -[[package]] -name = "peg-runtime" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa00462b37ead6d11a82c9d568b26682d78e0477dc02d1966c013af80969739" - -[[package]] -name = "pep440_rs" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe1d15693a11422cfa7d401b00dc9ae9fb8edbfbcb711a77130663f4ddf67650" -dependencies = [ - "lazy_static", - "regex", - "serde", - "tracing", - "unicode-width", -] - -[[package]] -name = "pep508_rs" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969679a29dfdc8278a449f75b3dd45edf57e649bd59f7502429c2840751c46d8" -dependencies = [ - "once_cell", - "pep440_rs", - "regex", - "serde", - "thiserror", - "tracing", - "unicode-width", - "url", -] - -[[package]] -name = "percent-encoding" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" - -[[package]] -name = "phf" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_codegen" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_shared" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" - -[[package]] -name = "pmutil" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3894e5d549cccbe44afecf72922f277f603cd4bb0219c8342631ef18fffbe004" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "proc-macro2" -version = "1.0.59" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "pyproject-toml" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04dbbb336bd88583943c7cd973a32fed323578243a7569f40cb0c7da673321b" -dependencies = [ - "indexmap", - "pep440_rs", - "pep508_rs", - "serde", - "toml", -] - -[[package]] -name = "quick-junit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b909fe9bf2abb1e3d6a97c9189a37c8105c61d03dca9ce6aace023e7d682bd" -dependencies = [ - "chrono", - "indexmap", - "nextest-workspace-hack", - "quick-xml", - "thiserror", - "uuid", -] - -[[package]] -name = "quick-xml" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_users" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" -dependencies = [ - "getrandom", - "redox_syscall", - "thiserror", -] - -[[package]] -name = "regex" -version = "1.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" -dependencies = [ - "aho-corasick 1.0.2", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" - -[[package]] -name = "result-like" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccc7ce6435c33898517a30e85578cd204cbb696875efb93dec19a2d31294f810" -dependencies = [ - "result-like-derive", -] - -[[package]] -name = "result-like-derive" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fabf0a2e54f711c68c50d49f648a1a8a37adcb57353f518ac4df374f0788f42" -dependencies = [ - "pmutil", - "proc-macro2", - "quote", - "syn 1.0.109", - "syn-ext", -] - -[[package]] -name = "ruff" -version = "0.0.272" -dependencies = [ - "annotate-snippets 0.9.1", - "anyhow", - "bitflags 2.3.1", - "chrono", - "colored", - "dirs", - "fern", - "glob", - "globset", - "ignore", - "imperative", - "is-macro", - "itertools", - "libcst", - "log", - "natord", - "nohash-hasher", - "num-bigint", - "num-traits", - "once_cell", - "path-absolutize", - "pathdiff", - "pep440_rs", - "pyproject-toml", - "quick-junit", - "regex", - "result-like", - "ruff_cache", - "ruff_diagnostics", - "ruff_macros", - "ruff_python_whitespace", - "ruff_python_ast", - "ruff_python_semantic", - "ruff_python_stdlib", - "ruff_rustpython", - "ruff_text_size", - "ruff_textwrap", - "rustc-hash", - "rustpython-format", - "rustpython-parser", - "semver", - "serde", - "serde_json", - "shellexpand", - "similar", - "smallvec", - "strum", - "strum_macros", - "thiserror", - "toml", - "typed-arena", - "unicode-width", - "unicode_names2", -] - -[[package]] -name = "ruff-fuzz" -version = "0.0.0" -dependencies = [ - "arbitrary", - "libfuzzer-sys", - "ruff", - "ruff_python_ast", - "ruff_python_formatter", - "similar", -] - -[[package]] -name = "ruff_cache" -version = "0.0.0" -dependencies = [ - "filetime", - "globset", - "itertools", - "regex", -] - -[[package]] -name = "ruff_diagnostics" -version = "0.0.0" -dependencies = [ - "anyhow", - "log", - "ruff_text_size", - "serde", -] - -[[package]] -name = "ruff_formatter" -version = "0.0.0" -dependencies = [ - "drop_bomb", - "ruff_text_size", - "rustc-hash", - "static_assertions", - "tracing", - "unicode-width", -] - -[[package]] -name = "ruff_index" -version = "0.0.0" -dependencies = [ - "ruff_macros", -] - -[[package]] -name = "ruff_macros" -version = "0.0.0" -dependencies = [ - "itertools", - "proc-macro2", - "quote", - "ruff_textwrap", - "syn 2.0.18", -] - -[[package]] -name = "ruff_python_whitespace" -version = "0.0.0" -dependencies = [ - "memchr", - "ruff_text_size", -] - -[[package]] -name = "ruff_python_ast" -version = "0.0.0" -dependencies = [ - "anyhow", - "bitflags 2.3.1", - "is-macro", - "itertools", - "log", - "memchr", - "num-bigint", - "num-traits", - "once_cell", - "ruff_python_whitespace", - "ruff_text_size", - "rustc-hash", - "rustpython-ast", - "rustpython-literal", - "rustpython-parser", - "serde", - "smallvec", -] - -[[package]] -name = "ruff_python_formatter" -version = "0.0.0" -dependencies = [ - "anyhow", - "clap", - "countme", - "is-macro", - "itertools", - "once_cell", - "ruff_formatter", - "ruff_python_whitespace", - "ruff_python_ast", - "ruff_text_size", - "rustc-hash", - "rustpython-parser", -] - -[[package]] -name = "ruff_python_semantic" -version = "0.0.0" -dependencies = [ - "bitflags 2.3.1", - "is-macro", - "nohash-hasher", - "num-traits", - "ruff_index", - "ruff_python_ast", - "ruff_python_stdlib", - "ruff_text_size", - "rustc-hash", - "rustpython-parser", - "smallvec", -] - -[[package]] -name = "ruff_python_stdlib" -version = "0.0.0" -dependencies = [ - "once_cell", - "rustc-hash", -] - -[[package]] -name = "ruff_rustpython" -version = "0.0.0" -dependencies = [ - "anyhow", - "rustpython-parser", -] - -[[package]] -name = "ruff_text_size" -version = "0.0.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd" -dependencies = [ - "serde", -] - -[[package]] -name = "ruff_textwrap" -version = "0.0.0" -dependencies = [ - "ruff_python_whitespace", - "ruff_text_size", -] - -[[package]] -name = "rust-stemmers" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" -dependencies = [ - "serde", - "serde_derive", -] - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustix" -version = "0.37.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "rustpython-ast" -version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd" -dependencies = [ - "is-macro", - "num-bigint", - "rustpython-parser-core", - "static_assertions", -] - -[[package]] -name = "rustpython-format" -version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd" -dependencies = [ - "bitflags 2.3.1", - "itertools", - "num-bigint", - "num-traits", - "rustpython-literal", -] - -[[package]] -name = "rustpython-literal" -version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd" -dependencies = [ - "hexf-parse", - "is-macro", - "lexical-parse-float", - "num-traits", - "unic-ucd-category", -] - -[[package]] -name = "rustpython-parser" -version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd" -dependencies = [ - "anyhow", - "is-macro", - "itertools", - "lalrpop-util", - "log", - "num-bigint", - "num-traits", - "phf", - "phf_codegen", - "rustc-hash", - "rustpython-ast", - "rustpython-parser-core", - "tiny-keccak", - "unic-emoji-char", - "unic-ucd-ident", - "unicode_names2", -] - -[[package]] -name = "rustpython-parser-core" -version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd" -dependencies = [ - "is-macro", - "ruff_text_size", -] - -[[package]] -name = "rustversion" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" - -[[package]] -name = "ryu" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "semver" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" - -[[package]] -name = "serde" -version = "1.0.163" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.163" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.18", -] - -[[package]] -name = "serde_json" -version = "1.0.96" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_spanned" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" -dependencies = [ - "serde", -] - -[[package]] -name = "shellexpand" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" -dependencies = [ - "dirs", -] - -[[package]] -name = "similar" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" - -[[package]] -name = "siphasher" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" - -[[package]] -name = "smallvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "strum" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 1.0.109", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn-ext" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b86cb2b68c5b3c078cac02588bc23f3c04bb828c5d3aedd17980876ec6a7be6" -dependencies = [ - "syn 1.0.109", -] - -[[package]] -name = "thiserror" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.18", -] - -[[package]] -name = "thread_local" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "toml" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.19.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - -[[package]] -name = "tracing" -version = "0.1.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" -dependencies = [ - "cfg-if", - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.18", -] - -[[package]] -name = "tracing-core" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "typed-arena" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" - -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-emoji-char" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b07221e68897210270a38bde4babb655869637af0f69407f96053a34f76494d" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-category" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8d4591f5fcfe1bd4453baaf803c40e1b1e69ff8455c47620440b46efef91c0" -dependencies = [ - "matches", - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-ident" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" - -[[package]] -name = "unicode-ident" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" - -[[package]] -name = "unicode-normalization" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - -[[package]] -name = "unicode_names2" -version = "0.6.0" -source = "git+https://github.com/youknowone/unicode_names2.git?rev=4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde#4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" -dependencies = [ - "phf", -] - -[[package]] -name = "url" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" - -[[package]] -name = "uuid" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" - -[[package]] -name = "walkdir" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.18", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.18", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" - -[[package]] -name = "winnow" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" -dependencies = [ - "memchr", -] - -[[package]] -name = "yansi-term" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1" -dependencies = [ - "winapi", -] diff --git a/fuzz/README.md b/fuzz/README.md index 42907fd9f1..406fc4c913 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -12,6 +12,8 @@ To use the fuzzers provided in this directory, start by invoking: This will install [`cargo-fuzz`](https://github.com/rust-fuzz/cargo-fuzz) and optionally download a [dataset](https://zenodo.org/record/3628784) which improves the efficacy of the testing. +**This step is necessary for initialising the corpus directory, as all fuzzers share a common +corpus.** The dataset may take several hours to download and clean, so if you're just looking to try out the fuzzers, skip the dataset download, though be warned that some features simply cannot be tested without it (very unlikely for the fuzzer to generate valid python code from "thin air"). @@ -22,6 +24,8 @@ Once you have initialised the fuzzers, you can then execute any fuzzer with: cargo fuzz run -s none name_of_fuzzer -- -timeout=1 ``` +**Users using Apple M1 devices must use a nightly compiler and omit the `-s none` portion of this +command, as this architecture does not support fuzzing without a sanitizer.** You can view the names of the available fuzzers with `cargo fuzz list`. For specific details about how each fuzzer works, please read this document in its entirety. @@ -74,6 +78,8 @@ itself, each harness is briefly described below. This fuzz harness does not perform any "smart" testing of Ruff; it merely checks that the parsing and unparsing of a particular input (what would normally be a source code file) does not crash. +It also attempts to verify that the locations of tokens and errors identified do not fall in the +middle of a UTF-8 code point, which may cause downstream panics. While this is unlikely to find any issues on its own, it executes very quickly and covers a large and diverse code region that may speed up the generation of inputs and therefore make a more valuable corpus quickly. @@ -95,11 +101,3 @@ This fuzz harness checks that fixes applied by Ruff do not introduce new errors [`ruff::test::test_snippet`](../crates/ruff/src/test.rs) testing utility. It currently is only configured to use default settings, but may be extended in future versions to test non-default linter settings. - -## Experimental settings - -You can optionally use `--no-default-features --features libafl` to use the libafl fuzzer instead of -libfuzzer. -This fuzzer has experimental support, but can vastly improve fuzzer performance. -If you are not already familiar with [LibAFL](https://github.com/AFLplusplus/LibAFL), this mode is -not currently recommended. diff --git a/fuzz/fuzz_targets/ruff_parse_simple.rs b/fuzz/fuzz_targets/ruff_parse_simple.rs index e685f73857..d649746dcf 100644 --- a/fuzz/fuzz_targets/ruff_parse_simple.rs +++ b/fuzz/fuzz_targets/ruff_parse_simple.rs @@ -4,13 +4,59 @@ #![no_main] use libfuzzer_sys::{fuzz_target, Corpus}; -use ruff_python_ast::source_code::round_trip; +use ruff_python_ast::prelude::{lexer, Mode, Parse, ParseError, Suite}; +use ruff_python_ast::source_code::{Generator, Locator, Stylist}; fn do_fuzz(case: &[u8]) -> Corpus { let Ok(code) = std::str::from_utf8(case) else { return Corpus::Reject; }; // just round-trip it once to trigger both parse and unparse - let _ = round_trip(code, "fuzzed-source.py"); + let locator = Locator::new(code); + let python_ast = match Suite::parse(code, "fuzzed-source.py") { + Ok(stmts) => stmts, + Err(ParseError { offset, .. }) => { + let offset = offset.to_usize(); + assert!( + code.is_char_boundary(offset), + "Invalid error location {} (not at char boundary)", + offset + ); + return Corpus::Keep; + } + }; + + let tokens: Vec<_> = lexer::lex(code, Mode::Module).collect(); + + for maybe_token in tokens.iter() { + match maybe_token.as_ref() { + Ok((_, range)) => { + let start = range.start().to_usize(); + let end = range.end().to_usize(); + assert!( + code.is_char_boundary(start), + "Invalid start position {} (not at char boundary)", + start + ); + assert!( + code.is_char_boundary(end), + "Invalid end position {} (not at char boundary)", + end + ); + } + Err(err) => { + let offset = err.location.to_usize(); + assert!( + code.is_char_boundary(offset), + "Invalid error location {} (not at char boundary)", + offset + ); + } + } + } + + let stylist = Stylist::from_tokens(&tokens, &locator); + let mut generator: Generator = (&stylist).into(); + generator.unparse_suite(&python_ast); Corpus::Keep } From 638c18f007dc1bed6102958f6c5f2effe18f5d2e Mon Sep 17 00:00:00 2001 From: Adam Pauls Date: Mon, 12 Jun 2023 09:54:27 -0700 Subject: [PATCH 014/447] Expand RUF008 to all classes, but to a new code (RUF012) (#4390) AFAIK, there is no reason to limit RUF008 to just dataclasses -- mutable defaults have the same problems for regular classes. Partially addresses https://github.com/charliermarsh/ruff/issues/4053 and broken out from https://github.com/charliermarsh/ruff/pull/4096. --------- Co-authored-by: Micha Reiser Co-authored-by: Charlie Marsh --- .../resources/test/fixtures/ruff/RUF012.py | 22 ++++++ crates/ruff/src/checkers/ast/mod.rs | 13 ++-- crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/ruff/mod.rs | 13 +++- crates/ruff/src/rules/ruff/rules/mod.rs | 4 +- ...rs => mutable_defaults_in_class_fields.rs} | 78 +++++++++++++------ ..._rules__ruff__tests__RUF012_RUF012.py.snap | 42 ++++++++++ ruff.schema.json | 1 + 8 files changed, 141 insertions(+), 33 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/ruff/RUF012.py rename crates/ruff/src/rules/ruff/rules/{mutable_defaults_in_dataclass_fields.rs => mutable_defaults_in_class_fields.rs} (80%) create mode 100644 crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF012.py b/crates/ruff/resources/test/fixtures/ruff/RUF012.py new file mode 100644 index 0000000000..4b4c6df0bf --- /dev/null +++ b/crates/ruff/resources/test/fixtures/ruff/RUF012.py @@ -0,0 +1,22 @@ +import typing +from typing import ClassVar, Sequence + +KNOWINGLY_MUTABLE_DEFAULT = [] + + +class A: + mutable_default: list[int] = [] + immutable_annotation: typing.Sequence[int] = [] + without_annotation = [] + ignored_via_comment: list[int] = [] # noqa: RUF012 + correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT + class_variable: typing.ClassVar[list[int]] = [] + + +class B: + mutable_default: list[int] = [] + immutable_annotation: Sequence[int] = [] + without_annotation = [] + ignored_via_comment: list[int] = [] # noqa: RUF012 + correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT + class_variable: ClassVar[list[int]] = [] diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 7381a5f7b7..40263c7f08 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -794,13 +794,16 @@ where if self.any_enabled(&[ Rule::MutableDataclassDefault, Rule::FunctionCallInDataclassDefaultArgument, - ]) && ruff::rules::is_dataclass(&self.semantic_model, decorator_list) - { - if self.enabled(Rule::MutableDataclassDefault) { - ruff::rules::mutable_dataclass_default(self, body); + Rule::MutableClassDefault, + ]) { + let is_dataclass = + ruff::rules::is_dataclass(&self.semantic_model, decorator_list); + if self.any_enabled(&[Rule::MutableDataclassDefault, Rule::MutableClassDefault]) + { + ruff::rules::mutable_class_default(self, body, is_dataclass); } - if self.enabled(Rule::FunctionCallInDataclassDefaultArgument) { + if is_dataclass && self.enabled(Rule::FunctionCallInDataclassDefaultArgument) { ruff::rules::function_call_in_dataclass_defaults(self, body); } } diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 962190fbbd..07a8826f1b 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -752,6 +752,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "009") => (RuleGroup::Unspecified, rules::ruff::rules::FunctionCallInDataclassDefaultArgument), (Ruff, "010") => (RuleGroup::Unspecified, rules::ruff::rules::ExplicitFStringTypeConversion), (Ruff, "011") => (RuleGroup::Unspecified, rules::ruff::rules::StaticKeyDictComprehension), + (Ruff, "012") => (RuleGroup::Unspecified, rules::ruff::rules::MutableClassDefault), (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 66948c9d3e..646842686f 100644 --- a/crates/ruff/src/rules/ruff/mod.rs +++ b/crates/ruff/src/rules/ruff/mod.rs @@ -170,7 +170,18 @@ mod tests { #[test_case(Rule::MutableDataclassDefault, Path::new("RUF008.py"))] #[test_case(Rule::FunctionCallInDataclassDefaultArgument, Path::new("RUF009.py"))] - fn mutable_defaults(rule_code: Rule, path: &Path) -> Result<()> { + fn mutable_dataclass_defaults(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); + let diagnostics = test_path( + Path::new("ruff").join(path).as_path(), + &settings::Settings::for_rule(rule_code), + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } + + #[test_case(Rule::MutableClassDefault, Path::new("RUF012.py"))] + fn mutable_class_defaults(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( Path::new("ruff").join(path).as_path(), diff --git a/crates/ruff/src/rules/ruff/rules/mod.rs b/crates/ruff/src/rules/ruff/rules/mod.rs index 9835966ece..368570cd20 100644 --- a/crates/ruff/src/rules/ruff/rules/mod.rs +++ b/crates/ruff/src/rules/ruff/rules/mod.rs @@ -3,7 +3,7 @@ pub(crate) use asyncio_dangling_task::*; pub(crate) use collection_literal_concatenation::*; pub(crate) use explicit_f_string_type_conversion::*; pub(crate) use invalid_pyproject_toml::InvalidPyprojectToml; -pub(crate) use mutable_defaults_in_dataclass_fields::*; +pub(crate) use mutable_defaults_in_class_fields::*; pub(crate) use pairwise_over_zipped::*; pub(crate) use unused_noqa::*; @@ -15,7 +15,7 @@ mod collection_literal_concatenation; mod confusables; mod explicit_f_string_type_conversion; mod invalid_pyproject_toml; -mod mutable_defaults_in_dataclass_fields; +mod mutable_defaults_in_class_fields; mod pairwise_over_zipped; mod static_key_dict_comprehension; mod unused_noqa; diff --git a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs similarity index 80% rename from crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs rename to crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs index 1f300b303b..ea1ab50cd1 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs @@ -12,8 +12,7 @@ use ruff_python_semantic::{ use crate::checkers::ast::Checker; /// ## What it does -/// Checks for mutable default values in dataclasses without the use of -/// `dataclasses.field`. +/// Checks for mutable default values in dataclasses. /// /// ## Why is this bad? /// Mutable default values share state across all instances of the dataclass, @@ -21,7 +20,10 @@ use crate::checkers::ast::Checker; /// changed in one instance, as those changes will unexpectedly affect all /// other instances. /// -/// ## Examples: +/// Instead of sharing mutable defaults, use the `field(default_factory=...)` +/// pattern. +/// +/// ## Examples /// ```python /// from dataclasses import dataclass /// @@ -40,19 +42,6 @@ use crate::checkers::ast::Checker; /// class A: /// mutable_default: list[int] = field(default_factory=list) /// ``` -/// -/// Alternatively, if you _want_ shared behaviour, make it more obvious -/// by assigning to a module-level variable: -/// ```python -/// from dataclasses import dataclass -/// -/// I_KNOW_THIS_IS_SHARED_STATE = [1, 2, 3, 4] -/// -/// -/// @dataclass -/// class A: -/// mutable_default: list[int] = I_KNOW_THIS_IS_SHARED_STATE -/// ``` #[violation] pub struct MutableDataclassDefault; @@ -63,6 +52,42 @@ impl Violation for MutableDataclassDefault { } } +/// ## What it does +/// Checks for mutable default values in class attributes. +/// +/// ## Why is this bad? +/// Mutable default values share state across all instances of the class, +/// while not being obvious. This can lead to bugs when the attributes are +/// changed in one instance, as those changes will unexpectedly affect all +/// other instances. +/// +/// When mutable value are intended, they should be annotated with +/// `typing.ClassVar`. +/// +/// ## Examples +/// ```python +/// class A: +/// mutable_default: list[int] = [] +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import ClassVar +/// +/// +/// class A: +/// mutable_default: ClassVar[list[int]] = [] +/// ``` +#[violation] +pub struct MutableClassDefault; + +impl Violation for MutableClassDefault { + #[derive_message_formats] + fn message(&self) -> String { + format!("Do not use mutable default values for class attributes") + } +} + /// ## What it does /// Checks for function calls in dataclass defaults. /// @@ -73,7 +98,7 @@ impl Violation for MutableDataclassDefault { /// ## Options /// - `flake8-bugbear.extend-immutable-calls` /// -/// ## Examples: +/// ## Examples /// ```python /// from dataclasses import dataclass /// @@ -214,8 +239,15 @@ pub(crate) fn function_call_in_dataclass_defaults(checker: &mut Checker, body: & } } -/// RUF008 -pub(crate) fn mutable_dataclass_default(checker: &mut Checker, body: &[Stmt]) { +/// RUF008/RUF012 +pub(crate) fn mutable_class_default(checker: &mut Checker, body: &[Stmt], is_dataclass: bool) { + fn diagnostic(is_dataclass: bool, value: &Expr) -> Diagnostic { + if is_dataclass { + Diagnostic::new(MutableDataclassDefault, value.range()) + } else { + Diagnostic::new(MutableClassDefault, value.range()) + } + } for statement in body { match statement { Stmt::AnnAssign(ast::StmtAnnAssign { @@ -227,16 +259,12 @@ pub(crate) fn mutable_dataclass_default(checker: &mut Checker, body: &[Stmt]) { && !is_immutable_annotation(checker.semantic_model(), annotation) && is_mutable_expr(value) { - checker - .diagnostics - .push(Diagnostic::new(MutableDataclassDefault, value.range())); + checker.diagnostics.push(diagnostic(is_dataclass, value)); } } Stmt::Assign(ast::StmtAssign { value, .. }) => { if is_mutable_expr(value) { - checker - .diagnostics - .push(Diagnostic::new(MutableDataclassDefault, value.range())); + checker.diagnostics.push(diagnostic(is_dataclass, value)); } } _ => (), diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap new file mode 100644 index 0000000000..4f12483f56 --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap @@ -0,0 +1,42 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF012.py:8:34: RUF012 Do not use mutable default values for class attributes + | + 7 | class A: + 8 | mutable_default: list[int] = [] + | ^^ RUF012 + 9 | immutable_annotation: typing.Sequence[int] = [] +10 | without_annotation = [] + | + +RUF012.py:10:26: RUF012 Do not use mutable default values for class attributes + | + 8 | mutable_default: list[int] = [] + 9 | immutable_annotation: typing.Sequence[int] = [] +10 | without_annotation = [] + | ^^ RUF012 +11 | ignored_via_comment: list[int] = [] # noqa: RUF012 +12 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT + | + +RUF012.py:17:34: RUF012 Do not use mutable default values for class attributes + | +16 | class B: +17 | mutable_default: list[int] = [] + | ^^ RUF012 +18 | immutable_annotation: Sequence[int] = [] +19 | without_annotation = [] + | + +RUF012.py:19:26: RUF012 Do not use mutable default values for class attributes + | +17 | mutable_default: list[int] = [] +18 | immutable_annotation: Sequence[int] = [] +19 | without_annotation = [] + | ^^ RUF012 +20 | ignored_via_comment: list[int] = [] # noqa: RUF012 +21 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT + | + + diff --git a/ruff.schema.json b/ruff.schema.json index f2190aecf4..ba2220c10c 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2348,6 +2348,7 @@ "RUF01", "RUF010", "RUF011", + "RUF012", "RUF1", "RUF10", "RUF100", From a77d2df93482ef67361dc0f7222cbd4a42c75848 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 12 Jun 2023 13:21:28 -0400 Subject: [PATCH 015/447] Split mutable-class-defaults rules into separate modules (#5031) --- crates/ruff/src/checkers/ast/mod.rs | 23 +- .../function_call_in_dataclass_default.rs | 115 +++++++ crates/ruff/src/rules/ruff/rules/helpers.rs | 45 +++ crates/ruff/src/rules/ruff/rules/mod.rs | 12 +- .../rules/ruff/rules/mutable_class_default.rs | 78 +++++ .../ruff/rules/mutable_dataclass_default.rs | 83 +++++ .../rules/mutable_defaults_in_class_fields.rs | 283 ------------------ ..._rules__ruff__tests__RUF012_RUF012.py.snap | 8 +- 8 files changed, 342 insertions(+), 305 deletions(-) create mode 100644 crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs create mode 100644 crates/ruff/src/rules/ruff/rules/helpers.rs create mode 100644 crates/ruff/src/rules/ruff/rules/mutable_class_default.rs create mode 100644 crates/ruff/src/rules/ruff/rules/mutable_dataclass_default.rs delete mode 100644 crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 40263c7f08..cc5b0d8388 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -791,21 +791,16 @@ where flake8_pie::rules::non_unique_enums(self, stmt, body); } - if self.any_enabled(&[ - Rule::MutableDataclassDefault, - Rule::FunctionCallInDataclassDefaultArgument, - Rule::MutableClassDefault, - ]) { - let is_dataclass = - ruff::rules::is_dataclass(&self.semantic_model, decorator_list); - if self.any_enabled(&[Rule::MutableDataclassDefault, Rule::MutableClassDefault]) - { - ruff::rules::mutable_class_default(self, body, is_dataclass); - } + if self.enabled(Rule::MutableClassDefault) { + ruff::rules::mutable_class_default(self, class_def); + } - if is_dataclass && self.enabled(Rule::FunctionCallInDataclassDefaultArgument) { - ruff::rules::function_call_in_dataclass_defaults(self, body); - } + if self.enabled(Rule::MutableDataclassDefault) { + ruff::rules::mutable_dataclass_default(self, class_def); + } + + if self.enabled(Rule::FunctionCallInDataclassDefaultArgument) { + ruff::rules::function_call_in_dataclass_default(self, class_def); } if self.enabled(Rule::FStringDocstring) { diff --git a/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs b/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs new file mode 100644 index 0000000000..373d37a724 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs @@ -0,0 +1,115 @@ +use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::call_path::compose_call_path; +use ruff_python_ast::call_path::{from_qualified_name, CallPath}; +use ruff_python_semantic::analyze::typing::is_immutable_func; + +use crate::checkers::ast::Checker; +use crate::rules::ruff::rules::helpers::{ + is_allowed_dataclass_function, is_class_var_annotation, is_dataclass, +}; + +/// ## What it does +/// Checks for function calls in dataclass attribute defaults. +/// +/// ## Why is this bad? +/// Function calls are only performed once, at definition time. The returned +/// value is then reused by all instances of the dataclass. This can lead to +/// unexpected behavior when the function call returns a mutable object, as +/// changes to the object will be shared across all instances. +/// +/// If a field needs to be initialized with a mutable object, use the +/// `field(default_factory=...)` pattern. +/// +/// ## Options +/// - `flake8-bugbear.extend-immutable-calls` +/// +/// ## Examples +/// ```python +/// from dataclasses import dataclass +/// +/// +/// def simple_list() -> list[int]: +/// return [1, 2, 3, 4] +/// +/// +/// @dataclass +/// class A: +/// mutable_default: list[int] = simple_list() +/// ``` +/// +/// Use instead: +/// ```python +/// from dataclasses import dataclass, field +/// +/// +/// def creating_list() -> list[int]: +/// return [1, 2, 3, 4] +/// +/// +/// @dataclass +/// class A: +/// mutable_default: list[int] = field(default_factory=creating_list) +/// ``` +#[violation] +pub struct FunctionCallInDataclassDefaultArgument { + pub name: Option, +} + +impl Violation for FunctionCallInDataclassDefaultArgument { + #[derive_message_formats] + fn message(&self) -> String { + let FunctionCallInDataclassDefaultArgument { name } = self; + if let Some(name) = name { + format!("Do not perform function call `{name}` in dataclass defaults") + } else { + format!("Do not perform function call in dataclass defaults") + } + } +} + +/// RUF009 +pub(crate) fn function_call_in_dataclass_default( + checker: &mut Checker, + class_def: &ast::StmtClassDef, +) { + if !is_dataclass(checker.semantic_model(), class_def) { + return; + } + + let extend_immutable_calls: Vec = checker + .settings + .flake8_bugbear + .extend_immutable_calls + .iter() + .map(|target| from_qualified_name(target)) + .collect(); + + for statement in &class_def.body { + if let Stmt::AnnAssign(ast::StmtAnnAssign { + annotation, + value: Some(expr), + .. + }) = statement + { + if let Expr::Call(ast::ExprCall { func, .. }) = expr.as_ref() { + if is_class_var_annotation(checker.semantic_model(), annotation) { + continue; + } + + if !is_immutable_func(checker.semantic_model(), func, &extend_immutable_calls) + && !is_allowed_dataclass_function(checker.semantic_model(), func) + { + checker.diagnostics.push(Diagnostic::new( + FunctionCallInDataclassDefaultArgument { + name: compose_call_path(func), + }, + expr.range(), + )); + } + } + } + } +} diff --git a/crates/ruff/src/rules/ruff/rules/helpers.rs b/crates/ruff/src/rules/ruff/rules/helpers.rs new file mode 100644 index 0000000000..fca12bfd2a --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/helpers.rs @@ -0,0 +1,45 @@ +use ruff_python_ast::helpers::map_callable; +use rustpython_parser::ast::{self, Expr}; + +use ruff_python_semantic::model::SemanticModel; + +pub(super) fn is_mutable_expr(expr: &Expr) -> bool { + matches!( + expr, + Expr::List(_) + | Expr::Dict(_) + | Expr::Set(_) + | Expr::ListComp(_) + | Expr::DictComp(_) + | Expr::SetComp(_) + ) +} + +const ALLOWED_DATACLASS_SPECIFIC_FUNCTIONS: &[&[&str]] = &[&["dataclasses", "field"]]; + +pub(super) fn is_allowed_dataclass_function(model: &SemanticModel, func: &Expr) -> bool { + model.resolve_call_path(func).map_or(false, |call_path| { + ALLOWED_DATACLASS_SPECIFIC_FUNCTIONS + .iter() + .any(|target| call_path.as_slice() == *target) + }) +} + +/// Returns `true` if the given [`Expr`] is a `typing.ClassVar` annotation. +pub(super) fn is_class_var_annotation(model: &SemanticModel, annotation: &Expr) -> bool { + let Expr::Subscript(ast::ExprSubscript { value, .. }) = &annotation else { + return false; + }; + model.match_typing_expr(value, "ClassVar") +} + +/// Returns `true` if the given class is a dataclass. +pub(super) fn is_dataclass(model: &SemanticModel, class_def: &ast::StmtClassDef) -> bool { + class_def.decorator_list.iter().any(|decorator| { + model + .resolve_call_path(map_callable(&decorator.expression)) + .map_or(false, |call_path| { + call_path.as_slice() == ["dataclasses", "dataclass"] + }) + }) +} diff --git a/crates/ruff/src/rules/ruff/rules/mod.rs b/crates/ruff/src/rules/ruff/rules/mod.rs index 368570cd20..7800b3d74a 100644 --- a/crates/ruff/src/rules/ruff/rules/mod.rs +++ b/crates/ruff/src/rules/ruff/rules/mod.rs @@ -2,20 +2,24 @@ pub(crate) use ambiguous_unicode_character::*; pub(crate) use asyncio_dangling_task::*; pub(crate) use collection_literal_concatenation::*; pub(crate) use explicit_f_string_type_conversion::*; +pub(crate) use function_call_in_dataclass_default::*; pub(crate) use invalid_pyproject_toml::InvalidPyprojectToml; -pub(crate) use mutable_defaults_in_class_fields::*; +pub(crate) use mutable_class_default::*; +pub(crate) use mutable_dataclass_default::*; pub(crate) use pairwise_over_zipped::*; -pub(crate) use unused_noqa::*; - pub(crate) use static_key_dict_comprehension::*; +pub(crate) use unused_noqa::*; mod ambiguous_unicode_character; mod asyncio_dangling_task; mod collection_literal_concatenation; mod confusables; mod explicit_f_string_type_conversion; +mod function_call_in_dataclass_default; +mod helpers; mod invalid_pyproject_toml; -mod mutable_defaults_in_class_fields; +mod mutable_class_default; +mod mutable_dataclass_default; mod pairwise_over_zipped; mod static_key_dict_comprehension; mod unused_noqa; diff --git a/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs b/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs new file mode 100644 index 0000000000..9bf066ecad --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs @@ -0,0 +1,78 @@ +use rustpython_parser::ast::{self, Ranged, Stmt}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_semantic::analyze::typing::is_immutable_annotation; + +use crate::checkers::ast::Checker; +use crate::rules::ruff::rules::helpers::{is_class_var_annotation, is_dataclass, is_mutable_expr}; + +/// ## What it does +/// Checks for mutable default values in class attributes. +/// +/// ## Why is this bad? +/// Mutable default values share state across all instances of the class, +/// while not being obvious. This can lead to bugs when the attributes are +/// changed in one instance, as those changes will unexpectedly affect all +/// other instances. +/// +/// When mutable value are intended, they should be annotated with +/// `typing.ClassVar`. +/// +/// ## Examples +/// ```python +/// class A: +/// mutable_default: list[int] = [] +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import ClassVar +/// +/// +/// class A: +/// mutable_default: ClassVar[list[int]] = [] +/// ``` +#[violation] +pub struct MutableClassDefault; + +impl Violation for MutableClassDefault { + #[derive_message_formats] + fn message(&self) -> String { + format!("Mutable class attributes should be annotated with `typing.ClassVar`") + } +} + +/// RUF012 +pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::StmtClassDef) { + if is_dataclass(checker.semantic_model(), class_def) { + return; + } + + for statement in &class_def.body { + match statement { + Stmt::AnnAssign(ast::StmtAnnAssign { + annotation, + value: Some(value), + .. + }) => { + if is_mutable_expr(value) + && !is_class_var_annotation(checker.semantic_model(), annotation) + && !is_immutable_annotation(checker.semantic_model(), annotation) + { + checker + .diagnostics + .push(Diagnostic::new(MutableClassDefault, value.range())); + } + } + Stmt::Assign(ast::StmtAssign { value, .. }) => { + if is_mutable_expr(value) { + checker + .diagnostics + .push(Diagnostic::new(MutableClassDefault, value.range())); + } + } + _ => (), + } + } +} diff --git a/crates/ruff/src/rules/ruff/rules/mutable_dataclass_default.rs b/crates/ruff/src/rules/ruff/rules/mutable_dataclass_default.rs new file mode 100644 index 0000000000..6c51fc2443 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/mutable_dataclass_default.rs @@ -0,0 +1,83 @@ +use rustpython_parser::ast::{self, Ranged, Stmt}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_semantic::analyze::typing::is_immutable_annotation; + +use crate::checkers::ast::Checker; +use crate::rules::ruff::rules::helpers::{is_class_var_annotation, is_dataclass, is_mutable_expr}; + +/// ## What it does +/// Checks for mutable default values in dataclasses. +/// +/// ## Why is this bad? +/// Mutable default values share state across all instances of the dataclass, +/// while not being obvious. This can lead to bugs when the attributes are +/// changed in one instance, as those changes will unexpectedly affect all +/// other instances. +/// +/// Instead of sharing mutable defaults, use the `field(default_factory=...)` +/// pattern. +/// +/// ## Examples +/// ```python +/// from dataclasses import dataclass +/// +/// +/// @dataclass +/// class A: +/// mutable_default: list[int] = [] +/// ``` +/// +/// Use instead: +/// ```python +/// from dataclasses import dataclass, field +/// +/// +/// @dataclass +/// class A: +/// mutable_default: list[int] = field(default_factory=list) +/// ``` +#[violation] +pub struct MutableDataclassDefault; + +impl Violation for MutableDataclassDefault { + #[derive_message_formats] + fn message(&self) -> String { + format!("Do not use mutable default values for dataclass attributes") + } +} + +/// RUF008 +pub(crate) fn mutable_dataclass_default(checker: &mut Checker, class_def: &ast::StmtClassDef) { + if !is_dataclass(checker.semantic_model(), class_def) { + return; + } + + for statement in &class_def.body { + match statement { + Stmt::AnnAssign(ast::StmtAnnAssign { + annotation, + value: Some(value), + .. + }) => { + if is_mutable_expr(value) + && !is_class_var_annotation(checker.semantic_model(), annotation) + && !is_immutable_annotation(checker.semantic_model(), annotation) + { + checker + .diagnostics + .push(Diagnostic::new(MutableDataclassDefault, value.range())); + } + } + Stmt::Assign(ast::StmtAssign { value, .. }) => { + if is_mutable_expr(value) { + checker + .diagnostics + .push(Diagnostic::new(MutableDataclassDefault, value.range())); + } + } + _ => (), + } + } +} diff --git a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs deleted file mode 100644 index ea1ab50cd1..0000000000 --- a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs +++ /dev/null @@ -1,283 +0,0 @@ -use rustpython_parser::ast::{self, Decorator, Expr, Ranged, Stmt}; - -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::{from_qualified_name, CallPath}; -use ruff_python_ast::{call_path::compose_call_path, helpers::map_callable}; -use ruff_python_semantic::{ - analyze::typing::{is_immutable_annotation, is_immutable_func}, - model::SemanticModel, -}; - -use crate::checkers::ast::Checker; - -/// ## What it does -/// Checks for mutable default values in dataclasses. -/// -/// ## Why is this bad? -/// Mutable default values share state across all instances of the dataclass, -/// while not being obvious. This can lead to bugs when the attributes are -/// changed in one instance, as those changes will unexpectedly affect all -/// other instances. -/// -/// Instead of sharing mutable defaults, use the `field(default_factory=...)` -/// pattern. -/// -/// ## Examples -/// ```python -/// from dataclasses import dataclass -/// -/// -/// @dataclass -/// class A: -/// mutable_default: list[int] = [] -/// ``` -/// -/// Use instead: -/// ```python -/// from dataclasses import dataclass, field -/// -/// -/// @dataclass -/// class A: -/// mutable_default: list[int] = field(default_factory=list) -/// ``` -#[violation] -pub struct MutableDataclassDefault; - -impl Violation for MutableDataclassDefault { - #[derive_message_formats] - fn message(&self) -> String { - format!("Do not use mutable default values for dataclass attributes") - } -} - -/// ## What it does -/// Checks for mutable default values in class attributes. -/// -/// ## Why is this bad? -/// Mutable default values share state across all instances of the class, -/// while not being obvious. This can lead to bugs when the attributes are -/// changed in one instance, as those changes will unexpectedly affect all -/// other instances. -/// -/// When mutable value are intended, they should be annotated with -/// `typing.ClassVar`. -/// -/// ## Examples -/// ```python -/// class A: -/// mutable_default: list[int] = [] -/// ``` -/// -/// Use instead: -/// ```python -/// from typing import ClassVar -/// -/// -/// class A: -/// mutable_default: ClassVar[list[int]] = [] -/// ``` -#[violation] -pub struct MutableClassDefault; - -impl Violation for MutableClassDefault { - #[derive_message_formats] - fn message(&self) -> String { - format!("Do not use mutable default values for class attributes") - } -} - -/// ## What it does -/// Checks for function calls in dataclass defaults. -/// -/// ## Why is this bad? -/// Function calls are only performed once, at definition time. The returned -/// value is then reused by all instances of the dataclass. -/// -/// ## Options -/// - `flake8-bugbear.extend-immutable-calls` -/// -/// ## Examples -/// ```python -/// from dataclasses import dataclass -/// -/// -/// def creating_list() -> list[int]: -/// return [1, 2, 3, 4] -/// -/// -/// @dataclass -/// class A: -/// mutable_default: list[int] = creating_list() -/// -/// -/// # also: -/// -/// -/// @dataclass -/// class B: -/// also_mutable_default_but_sneakier: A = A() -/// ``` -/// -/// Use instead: -/// ```python -/// from dataclasses import dataclass, field -/// -/// -/// def creating_list() -> list[int]: -/// return [1, 2, 3, 4] -/// -/// -/// @dataclass -/// class A: -/// mutable_default: list[int] = field(default_factory=creating_list) -/// -/// -/// @dataclass -/// class B: -/// also_mutable_default_but_sneakier: A = field(default_factory=A) -/// ``` -/// -/// Alternatively, if you _want_ the shared behaviour, make it more obvious -/// by assigning it to a module-level variable: -/// ```python -/// from dataclasses import dataclass -/// -/// -/// def creating_list() -> list[int]: -/// return [1, 2, 3, 4] -/// -/// -/// I_KNOW_THIS_IS_SHARED_STATE = creating_list() -/// -/// -/// @dataclass -/// class A: -/// mutable_default: list[int] = I_KNOW_THIS_IS_SHARED_STATE -/// ``` -#[violation] -pub struct FunctionCallInDataclassDefaultArgument { - pub name: Option, -} - -impl Violation for FunctionCallInDataclassDefaultArgument { - #[derive_message_formats] - fn message(&self) -> String { - let FunctionCallInDataclassDefaultArgument { name } = self; - if let Some(name) = name { - format!("Do not perform function call `{name}` in dataclass defaults") - } else { - format!("Do not perform function call in dataclass defaults") - } - } -} - -fn is_mutable_expr(expr: &Expr) -> bool { - matches!( - expr, - Expr::List(_) - | Expr::Dict(_) - | Expr::Set(_) - | Expr::ListComp(_) - | Expr::DictComp(_) - | Expr::SetComp(_) - ) -} - -const ALLOWED_DATACLASS_SPECIFIC_FUNCTIONS: &[&[&str]] = &[&["dataclasses", "field"]]; - -fn is_allowed_dataclass_function(model: &SemanticModel, func: &Expr) -> bool { - model.resolve_call_path(func).map_or(false, |call_path| { - ALLOWED_DATACLASS_SPECIFIC_FUNCTIONS - .iter() - .any(|target| call_path.as_slice() == *target) - }) -} - -/// Returns `true` if the given [`Expr`] is a `typing.ClassVar` annotation. -fn is_class_var_annotation(model: &SemanticModel, annotation: &Expr) -> bool { - let Expr::Subscript(ast::ExprSubscript { value, .. }) = &annotation else { - return false; - }; - model.match_typing_expr(value, "ClassVar") -} - -/// RUF009 -pub(crate) fn function_call_in_dataclass_defaults(checker: &mut Checker, body: &[Stmt]) { - let extend_immutable_calls: Vec = checker - .settings - .flake8_bugbear - .extend_immutable_calls - .iter() - .map(|target| from_qualified_name(target)) - .collect(); - - for statement in body { - if let Stmt::AnnAssign(ast::StmtAnnAssign { - annotation, - value: Some(expr), - .. - }) = statement - { - if is_class_var_annotation(checker.semantic_model(), annotation) { - continue; - } - if let Expr::Call(ast::ExprCall { func, .. }) = expr.as_ref() { - if !is_immutable_func(checker.semantic_model(), func, &extend_immutable_calls) - && !is_allowed_dataclass_function(checker.semantic_model(), func) - { - checker.diagnostics.push(Diagnostic::new( - FunctionCallInDataclassDefaultArgument { - name: compose_call_path(func), - }, - expr.range(), - )); - } - } - } - } -} - -/// RUF008/RUF012 -pub(crate) fn mutable_class_default(checker: &mut Checker, body: &[Stmt], is_dataclass: bool) { - fn diagnostic(is_dataclass: bool, value: &Expr) -> Diagnostic { - if is_dataclass { - Diagnostic::new(MutableDataclassDefault, value.range()) - } else { - Diagnostic::new(MutableClassDefault, value.range()) - } - } - for statement in body { - match statement { - Stmt::AnnAssign(ast::StmtAnnAssign { - annotation, - value: Some(value), - .. - }) => { - if !is_class_var_annotation(checker.semantic_model(), annotation) - && !is_immutable_annotation(checker.semantic_model(), annotation) - && is_mutable_expr(value) - { - checker.diagnostics.push(diagnostic(is_dataclass, value)); - } - } - Stmt::Assign(ast::StmtAssign { value, .. }) => { - if is_mutable_expr(value) { - checker.diagnostics.push(diagnostic(is_dataclass, value)); - } - } - _ => (), - } - } -} - -pub(crate) fn is_dataclass(model: &SemanticModel, decorator_list: &[Decorator]) -> bool { - decorator_list.iter().any(|decorator| { - model - .resolve_call_path(map_callable(&decorator.expression)) - .map_or(false, |call_path| { - call_path.as_slice() == ["dataclasses", "dataclass"] - }) - }) -} diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap index 4f12483f56..02a09e70f4 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/ruff/mod.rs --- -RUF012.py:8:34: RUF012 Do not use mutable default values for class attributes +RUF012.py:8:34: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` | 7 | class A: 8 | mutable_default: list[int] = [] @@ -10,7 +10,7 @@ RUF012.py:8:34: RUF012 Do not use mutable default values for class attributes 10 | without_annotation = [] | -RUF012.py:10:26: RUF012 Do not use mutable default values for class attributes +RUF012.py:10:26: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` | 8 | mutable_default: list[int] = [] 9 | immutable_annotation: typing.Sequence[int] = [] @@ -20,7 +20,7 @@ RUF012.py:10:26: RUF012 Do not use mutable default values for class attributes 12 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT | -RUF012.py:17:34: RUF012 Do not use mutable default values for class attributes +RUF012.py:17:34: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` | 16 | class B: 17 | mutable_default: list[int] = [] @@ -29,7 +29,7 @@ RUF012.py:17:34: RUF012 Do not use mutable default values for class attributes 19 | without_annotation = [] | -RUF012.py:19:26: RUF012 Do not use mutable default values for class attributes +RUF012.py:19:26: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` | 17 | mutable_default: list[int] = [] 18 | immutable_annotation: Sequence[int] = [] From cb4f086cbf26530c609e51c699792fd7de8f39bf Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Mon, 12 Jun 2023 23:27:45 +0530 Subject: [PATCH 016/447] Add roundtrip support for Jupyter notebook (#5028) ## Summary Add roundtrip support for Jupyter notebook. 1. Read the notebook 2. Extract out the source code content 3. Use it to update the notebook itself (should be exactly the same [^1]) 4. Serialize into JSON and print it to stdout ## Test Plan `cargo run --all-features --bin ruff_dev --package ruff_dev -- round-trip `
Example output:

``` { "cells": [ { "cell_type": "markdown", "id": "f3c286e9-fa52-4440-816f-4449232f199a", "metadata": {}, "source": [ "# Ruff Test" ] }, { "cell_type": "markdown", "id": "a2b7bc6c-778a-4b07-86ae-dde5a2d9511e", "metadata": {}, "source": [ "Markdown block before the first import" ] }, { "cell_type": "code", "id": "5e3ef98e-224c-450a-80e6-be442ad50907", "metadata": { "tags": [] }, "source": "", "execution_count": 1, "outputs": [] }, { "cell_type": "code", "id": "6bced3f8-e0a4-450c-ae7c-f60ad5671ee9", "metadata": {}, "source": "import contextlib\n\nwith contextlib.suppress(ValueError):\n print()\n", "outputs": [] }, { "cell_type": "code", "id": "d7102cfd-5bb5-4f5b-a3b8-07a7b8cca34c", "metadata": {}, "source": "import random\n\nrandom.randint(10, 20)", "outputs": [] }, { "cell_type": "code", "id": "88471d1c-7429-4967-898f-b0088fcb4c53", "metadata": {}, "source": "foo = 1\nif foo < 2:\n msg = f\"Invalid foo: {foo}\"\n raise ValueError(msg)", "outputs": [] } ], "metadata": { "kernelspec": { "display_name": "Python (ruff-playground)", "name": "ruff-playground", "language": "python" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "pygments_lexer": "ipython3", "nbconvert_exporter": "python", "version": "3.11.3" } }, "nbformat": 4, "nbformat_minor": 5 } ```

[^1]: The type in JSON might be different (https://github.com/astral-sh/ruff/pull/4665#discussion_r1212663495) Part of #1218 --- crates/ruff/src/jupyter/notebook.rs | 31 ++++++++++++++++++++++++----- crates/ruff_dev/src/round_trip.rs | 14 +++++++++---- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/crates/ruff/src/jupyter/notebook.rs b/crates/ruff/src/jupyter/notebook.rs index 66c4240fe0..70420e89a7 100644 --- a/crates/ruff/src/jupyter/notebook.rs +++ b/crates/ruff/src/jupyter/notebook.rs @@ -1,6 +1,6 @@ use std::cmp::Ordering; use std::fs::File; -use std::io::{BufReader, BufWriter}; +use std::io::{BufReader, BufWriter, Cursor, Write}; use std::iter; use std::path::Path; @@ -23,6 +23,22 @@ pub const JUPYTER_NOTEBOOK_EXT: &str = "ipynb"; const MAGIC_PREFIX: [&str; 3] = ["%", "!", "?"]; +/// Run round-trip source code generation on a given Jupyter notebook file path. +pub fn round_trip(path: &Path) -> anyhow::Result { + let mut notebook = Notebook::read(path).map_err(|err| { + anyhow::anyhow!( + "Failed to read notebook file `{}`: {:?}", + path.display(), + err + ) + })?; + let code = notebook.content().to_string(); + notebook.update_cell_content(&code); + let mut buffer = Cursor::new(Vec::new()); + notebook.write_inner(&mut buffer)?; + Ok(String::from_utf8(buffer.into_inner())?) +} + /// Return `true` if the [`Path`] appears to be that of a jupyter notebook file (`.ipynb`). pub fn is_jupyter_notebook(path: &Path) -> bool { path.extension() @@ -370,13 +386,18 @@ impl Notebook { .map_or(true, |language| language.name == "python") } + fn write_inner(&self, writer: &mut impl Write) -> anyhow::Result<()> { + // https://github.com/psf/black/blob/69ca0a4c7a365c5f5eea519a90980bab72cab764/src/black/__init__.py#LL1041 + let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); + let mut ser = serde_json::Serializer::with_formatter(writer, formatter); + self.raw.serialize(&mut ser)?; + Ok(()) + } + /// Write back with an indent of 1, just like black pub fn write(&self, path: &Path) -> anyhow::Result<()> { let mut writer = BufWriter::new(File::create(path)?); - // https://github.com/psf/black/blob/69ca0a4c7a365c5f5eea519a90980bab72cab764/src/black/__init__.py#LL1041 - let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); - let mut ser = serde_json::Serializer::with_formatter(&mut writer, formatter); - self.raw.serialize(&mut ser)?; + self.write_inner(&mut writer)?; Ok(()) } } diff --git a/crates/ruff_dev/src/round_trip.rs b/crates/ruff_dev/src/round_trip.rs index 4cc4f36c59..75d1cf59e4 100644 --- a/crates/ruff_dev/src/round_trip.rs +++ b/crates/ruff_dev/src/round_trip.rs @@ -1,4 +1,4 @@ -//! Run round-trip source code generation on a given Python file. +//! Run round-trip source code generation on a given Python or Jupyter notebook file. #![allow(clippy::print_stdout, clippy::print_stderr)] use std::fs; @@ -6,17 +6,23 @@ use std::path::PathBuf; use anyhow::Result; +use ruff::jupyter; use ruff::round_trip; #[derive(clap::Args)] pub(crate) struct Args { - /// Python file to round-trip. + /// Python or Jupyter notebook file to round-trip. #[arg(required = true)] file: PathBuf, } pub(crate) fn main(args: &Args) -> Result<()> { - let contents = fs::read_to_string(&args.file)?; - println!("{}", round_trip(&contents, &args.file.to_string_lossy())?); + let path = args.file.as_path(); + if jupyter::is_jupyter_notebook(path) { + println!("{}", jupyter::round_trip(path)?); + } else { + let contents = fs::read_to_string(&args.file)?; + println!("{}", round_trip(&contents, &args.file.to_string_lossy())?); + } Ok(()) } From 3470dee7d4d1e0a1d2cfdd7a84102b3c91ff879d Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Mon, 12 Jun 2023 23:42:10 +0530 Subject: [PATCH 017/447] Add rule to disallow implicit optional with autofix (#4831) ## Summary Add rule to disallow implicit optional with autofix. Currently, I've added it under `RUF` category. ### Limitation Type aliases could result in false positive: ```python from typing import Optional StrOptional = Optional[str] def foo(arg: StrOptional = None): pass ``` ## Test Plan `cargo test` resolves: #1983 --------- Co-authored-by: Micha Reiser Co-authored-by: Charlie Marsh --- .../resources/test/fixtures/ruff/RUF013_0.py | 187 ++++++++++ .../resources/test/fixtures/ruff/RUF013_1.py | 5 + crates/ruff/src/checkers/ast/mod.rs | 4 + crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/ruff/mod.rs | 27 +- .../src/rules/ruff/rules/implicit_optional.rs | 343 ++++++++++++++++++ crates/ruff/src/rules/ruff/rules/mod.rs | 2 + ..._ruff__tests__PY39_RUF013_RUF013_0.py.snap | 316 ++++++++++++++++ ..._ruff__tests__PY39_RUF013_RUF013_1.py.snap | 21 ++ ...ules__ruff__tests__RUF013_RUF013_0.py.snap | 316 ++++++++++++++++ ...ules__ruff__tests__RUF013_RUF013_1.py.snap | 20 + ruff.schema.json | 1 + 12 files changed, 1240 insertions(+), 3 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/ruff/RUF013_0.py create mode 100644 crates/ruff/resources/test/fixtures/ruff/RUF013_1.py create mode 100644 crates/ruff/src/rules/ruff/rules/implicit_optional.rs create mode 100644 crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap create mode 100644 crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_1.py.snap create mode 100644 crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap create mode 100644 crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_1.py.snap diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py b/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py new file mode 100644 index 0000000000..90252f46a1 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py @@ -0,0 +1,187 @@ +import typing +from typing import Annotated, Any, Literal, Optional, Tuple, Union + + +def f(arg: int): + pass + + +def f(arg=None): + pass + + +def f(arg: Any = None): + pass + + +def f(arg: object = None): + pass + + +def f(arg: int = None): # RUF011 + pass + + +def f(arg: str = None): # RUF011 + pass + + +def f(arg: typing.List[str] = None): # RUF011 + pass + + +def f(arg: Tuple[str] = None): # RUF011 + pass + + +# Optional + + +def f(arg: Optional[int] = None): + pass + + +def f(arg: typing.Optional[int] = None): + pass + + +# Union + + +def f(arg: Union[None, int] = None): + pass + + +def f(arg: Union[str, None] = None): + pass + + +def f(arg: typing.Union[int, str, None] = None): + pass + + +def f(arg: Union[int, str, Any] = None): + pass + + +def f(arg: Union = None): # RUF011 + pass + + +def f(arg: Union[int, str] = None): # RUF011 + pass + + +def f(arg: typing.Union[int, str] = None): # RUF011 + pass + + +# PEP 604 Union + + +def f(arg: None | int = None): + pass + + +def f(arg: int | None = None): + pass + + +def f(arg: int | float | str | None = None): + pass + + +def f(arg: int | float = None): # RUF011 + pass + + +def f(arg: int | float | str | bytes = None): # RUF011 + pass + + +# Literal + + +def f(arg: None = None): + pass + + +def f(arg: Literal[1, 2, None, 3] = None): + pass + + +def f(arg: Literal[1, "foo"] = None): # RUF011 + pass + + +def f(arg: typing.Literal[1, "foo", True] = None): # RUF011 + pass + + +# Annotated + + +def f(arg: Annotated[Optional[int], ...] = None): + pass + + +def f(arg: Annotated[Union[int, None], ...] = None): + pass + + +def f(arg: Annotated[Any, ...] = None): + pass + + +def f(arg: Annotated[int, ...] = None): # RUF011 + pass + + +def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF011 + pass + + +# Multiple arguments + + +def f( + arg1: Optional[int] = None, + arg2: Union[int, None] = None, + arg3: Literal[1, 2, None, 3] = None, +): + pass + + +def f( + arg1: int = None, # RUF011 + arg2: Union[int, float] = None, # RUF011 + arg3: Literal[1, 2, 3] = None, # RUF011 +): + pass + + +# Nested + + +def f(arg: Literal[1, "foo", Literal[True, None]] = None): + pass + + +def f(arg: Union[int, Union[float, Union[str, None]]] = None): + pass + + +def f(arg: Union[int, Union[float, Optional[str]]] = None): + pass + + +def f(arg: Union[int, Literal[True, None]] = None): + pass + + +def f(arg: Union[Annotated[int, ...], Annotated[Optional[float], ...]] = None): + pass + + +def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF011 + pass diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF013_1.py b/crates/ruff/resources/test/fixtures/ruff/RUF013_1.py new file mode 100644 index 0000000000..e270aaf3d8 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/ruff/RUF013_1.py @@ -0,0 +1,5 @@ +# No `typing.Optional` import + + +def f(arg: int = None): # RUF011 + pass diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index cc5b0d8388..bb448c2140 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -4148,6 +4148,10 @@ where } } + if self.settings.rules.enabled(Rule::ImplicitOptional) { + ruff::rules::implicit_optional(self, arguments); + } + // Bind, but intentionally avoid walking default expressions, as we handle them // upstream. for arg in &arguments.posonlyargs { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 07a8826f1b..6d3a3d5b8f 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -753,6 +753,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "010") => (RuleGroup::Unspecified, rules::ruff::rules::ExplicitFStringTypeConversion), (Ruff, "011") => (RuleGroup::Unspecified, rules::ruff::rules::StaticKeyDictComprehension), (Ruff, "012") => (RuleGroup::Unspecified, rules::ruff::rules::MutableClassDefault), + (Ruff, "013") => (RuleGroup::Unspecified, rules::ruff::rules::ImplicitOptional), (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 646842686f..8d966395b8 100644 --- a/crates/ruff/src/rules/ruff/mod.rs +++ b/crates/ruff/src/rules/ruff/mod.rs @@ -16,14 +16,16 @@ mod tests { use crate::pyproject_toml::lint_pyproject_toml; use crate::registry::Rule; use crate::settings::resolve_per_file_ignores; - use crate::settings::types::PerFileIgnore; + use crate::settings::types::{PerFileIgnore, PythonVersion}; use crate::test::{test_path, test_resource_path}; use crate::{assert_messages, settings}; - #[test_case(Rule::StaticKeyDictComprehension, Path::new("RUF011.py"))] - #[test_case(Rule::ExplicitFStringTypeConversion, Path::new("RUF010.py"))] #[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005.py"))] #[test_case(Rule::AsyncioDanglingTask, Path::new("RUF006.py"))] + #[test_case(Rule::ExplicitFStringTypeConversion, Path::new("RUF010.py"))] + #[test_case(Rule::StaticKeyDictComprehension, Path::new("RUF011.py"))] + #[test_case(Rule::ImplicitOptional, Path::new("RUF013_0.py"))] + #[test_case(Rule::ImplicitOptional, Path::new("RUF013_1.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( @@ -34,6 +36,25 @@ mod tests { Ok(()) } + #[test_case(Path::new("RUF013_0.py"))] + #[test_case(Path::new("RUF013_1.py"))] + fn implicit_optional(path: &Path) -> Result<()> { + let snapshot = format!( + "PY39_{}_{}", + Rule::ImplicitOptional.noqa_code(), + path.to_string_lossy() + ); + let diagnostics = test_path( + Path::new("ruff").join(path).as_path(), + &settings::Settings { + target_version: PythonVersion::Py39, + ..settings::Settings::for_rule(Rule::ImplicitOptional) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } + #[test] fn confusables() -> Result<()> { let diagnostics = test_path( diff --git a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs new file mode 100644 index 0000000000..166a032ba3 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs @@ -0,0 +1,343 @@ +use std::fmt; + +use anyhow::Result; +use rustpython_parser::ast::{self, Arguments, Constant, Expr, Operator, Ranged}; + +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_semantic::model::SemanticModel; +use ruff_text_size::TextRange; + +use crate::checkers::ast::Checker; +use crate::importer::ImportRequest; +use crate::registry::AsRule; +use crate::settings::types::PythonVersion; + +/// ## What it does +/// Checks for the use of implicit `Optional` in type annotations when the +/// default parameter value is `None`. +/// +/// ## Why is this bad? +/// Implicit `Optional` is prohibited by [PEP 484]. It is confusing and +/// inconsistent with the rest of the type system. +/// +/// It's recommended to use `Optional[T]` instead. For Python 3.10 and later, +/// you can also use `T | None`. +/// +/// ## Example +/// ```python +/// def foo(arg: int = None): +/// pass +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import Optional +/// +/// +/// def foo(arg: Optional[int] = None): +/// pass +/// ``` +/// +/// Or, for Python 3.10 and later: +/// ```python +/// def foo(arg: int | None = None): +/// pass +/// ``` +/// +/// If you want to use the `|` operator in Python 3.9 and earlier, you can +/// use future imports: +/// ```python +/// from __future__ import annotations +/// +/// +/// def foo(arg: int | None = None): +/// pass +/// ``` +/// +/// [PEP 484]: https://peps.python.org/pep-0484/#union-types +#[violation] +pub struct ImplicitOptional { + conversion_type: ConversionType, +} + +impl AlwaysAutofixableViolation for ImplicitOptional { + #[derive_message_formats] + fn message(&self) -> String { + format!("PEP 484 prohibits implicit `Optional`") + } + + fn autofix_title(&self) -> String { + format!("Convert to `{}`", self.conversion_type) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ConversionType { + /// Conversion using the `|` operator e.g., `str | None` + BinOpOr, + /// Conversion using the `typing.Optional` type e.g., `typing.Optional[str]` + Optional, +} + +impl fmt::Display for ConversionType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::BinOpOr => f.write_str("T | None"), + Self::Optional => f.write_str("Optional[T]"), + } + } +} + +impl From for ConversionType { + fn from(target_version: PythonVersion) -> Self { + if target_version >= PythonVersion::Py310 { + Self::BinOpOr + } else { + Self::Optional + } + } +} + +/// Custom iterator to collect all the `|` separated expressions in a PEP 604 +/// union type. +struct PEP604UnionIterator<'a> { + stack: Vec<&'a Expr>, +} + +impl<'a> PEP604UnionIterator<'a> { + fn new(expr: &'a Expr) -> Self { + Self { stack: vec![expr] } + } +} + +impl<'a> Iterator for PEP604UnionIterator<'a> { + type Item = &'a Expr; + + fn next(&mut self) -> Option { + while let Some(expr) = self.stack.pop() { + match expr { + Expr::BinOp(ast::ExprBinOp { + left, + op: Operator::BitOr, + right, + .. + }) => { + self.stack.push(left); + self.stack.push(right); + } + _ => return Some(expr), + } + } + None + } +} + +#[derive(Debug)] +enum TypingTarget<'a> { + None, + Any, + Object, + Optional, + Union(Vec<&'a Expr>), + Literal(Vec<&'a Expr>), + Annotated(&'a Expr), +} + +impl<'a> TypingTarget<'a> { + fn try_from_expr(model: &SemanticModel, expr: &'a Expr) -> Option { + match expr { + Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { + if model.match_typing_expr(value, "Optional") { + return Some(TypingTarget::Optional); + } + let Expr::Tuple(ast::ExprTuple { elts: elements, .. }) = slice.as_ref() else{ + return None; + }; + if model.match_typing_expr(value, "Literal") { + Some(TypingTarget::Literal(elements.iter().collect())) + } else if model.match_typing_expr(value, "Union") { + Some(TypingTarget::Union(elements.iter().collect())) + } else if model.match_typing_expr(value, "Annotated") { + elements.first().map(TypingTarget::Annotated) + } else { + None + } + } + Expr::BinOp(..) => Some(TypingTarget::Union( + PEP604UnionIterator::new(expr).collect(), + )), + Expr::Constant(ast::ExprConstant { + value: Constant::None, + .. + }) => Some(TypingTarget::None), + _ => model.resolve_call_path(expr).and_then(|call_path| { + if model.match_typing_call_path(&call_path, "Any") { + Some(TypingTarget::Any) + } else if matches!(call_path.as_slice(), ["" | "builtins", "object"]) { + Some(TypingTarget::Object) + } else { + None + } + }), + } + } + + /// Check if the [`TypingTarget`] explicitly allows `None`. + fn contains_none(&self, model: &SemanticModel) -> bool { + match self { + TypingTarget::None + | TypingTarget::Optional + | TypingTarget::Any + | TypingTarget::Object => true, + TypingTarget::Literal(elements) => elements.iter().any(|element| { + let Some(new_target) = TypingTarget::try_from_expr(model, element) 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(model), + _ => false, + } + }), + TypingTarget::Union(elements) => elements.iter().any(|element| { + let Some(new_target) = TypingTarget::try_from_expr(model, element) else { + return false; + }; + match new_target { + TypingTarget::None => true, + _ => new_target.contains_none(model), + } + }), + TypingTarget::Annotated(element) => { + let Some(new_target) = TypingTarget::try_from_expr(model, element) else { + return false; + }; + match new_target { + TypingTarget::None => true, + _ => new_target.contains_none(model), + } + } + } + } +} + +/// Check if the given annotation [`Expr`] explicitly allows `None`. +/// +/// This function will return `None` if the annotation explicitly allows `None` +/// otherwise it will return the annotation itself. If it's a `Annotated` type, +/// then the inner type will be checked. +/// +/// This function assumes that the annotation is a valid typing annotation expression. +fn type_hint_explicitly_allows_none<'a>( + model: &SemanticModel, + annotation: &'a Expr, +) -> Option<&'a Expr> { + let Some(target) = TypingTarget::try_from_expr(model, annotation) else { + return Some(annotation); + }; + match target { + // Short circuit on top level `None`, `Any` or `Optional` + TypingTarget::None | TypingTarget::Optional | TypingTarget::Any => None, + // Top-level `Annotated` node should check for the inner type and + // return the inner type if it doesn't allow `None`. If `Annotated` + // is found nested inside another type, then the outer type should + // be returned. + TypingTarget::Annotated(expr) => type_hint_explicitly_allows_none(model, expr), + _ => { + if target.contains_none(model) { + None + } else { + Some(annotation) + } + } + } +} + +/// Generate a [`Fix`] for the given [`Expr`] as per the [`ConversionType`]. +fn generate_fix(checker: &Checker, conversion_type: ConversionType, expr: &Expr) -> Result { + match conversion_type { + ConversionType::BinOpOr => { + let new_expr = Expr::BinOp(ast::ExprBinOp { + left: Box::new(expr.clone()), + op: Operator::BitOr, + right: Box::new(Expr::Constant(ast::ExprConstant { + value: Constant::None, + kind: None, + range: TextRange::default(), + })), + range: TextRange::default(), + }); + let content = checker.generator().expr(&new_expr); + Ok(Fix::suggested(Edit::range_replacement( + content, + expr.range(), + ))) + } + ConversionType::Optional => { + let (import_edit, binding) = checker.importer.get_or_import_symbol( + &ImportRequest::import_from("typing", "Optional"), + expr.start(), + checker.semantic_model(), + )?; + let new_expr = Expr::Subscript(ast::ExprSubscript { + range: TextRange::default(), + value: Box::new(Expr::Name(ast::ExprName { + id: binding.into(), + ctx: ast::ExprContext::Store, + range: TextRange::default(), + })), + slice: Box::new(expr.clone()), + ctx: ast::ExprContext::Load, + }); + let content = checker.generator().expr(&new_expr); + Ok(Fix::suggested_edits( + Edit::range_replacement(content, expr.range()), + [import_edit], + )) + } + } +} + +/// RUF011 +pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) { + let arguments_with_defaults = arguments + .kwonlyargs + .iter() + .rev() + .zip(arguments.kw_defaults.iter().rev()) + .chain( + arguments + .args + .iter() + .rev() + .chain(arguments.posonlyargs.iter().rev()) + .zip(arguments.defaults.iter().rev()), + ); + for (arg, default) in arguments_with_defaults { + if !matches!( + default, + Expr::Constant(ast::ExprConstant { + value: Constant::None, + .. + }), + ) { + continue; + } + let Some(annotation) = &arg.annotation else { + continue + }; + let Some(expr) = type_hint_explicitly_allows_none(checker.semantic_model(), annotation) else { + continue; + }; + let conversion_type = checker.settings.target_version.into(); + + let mut diagnostic = Diagnostic::new(ImplicitOptional { conversion_type }, expr.range()); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.try_set_fix(|| generate_fix(checker, conversion_type, expr)); + } + checker.diagnostics.push(diagnostic); + } +} diff --git a/crates/ruff/src/rules/ruff/rules/mod.rs b/crates/ruff/src/rules/ruff/rules/mod.rs index 7800b3d74a..c75237d500 100644 --- a/crates/ruff/src/rules/ruff/rules/mod.rs +++ b/crates/ruff/src/rules/ruff/rules/mod.rs @@ -3,6 +3,7 @@ pub(crate) use asyncio_dangling_task::*; pub(crate) use collection_literal_concatenation::*; pub(crate) use explicit_f_string_type_conversion::*; pub(crate) use function_call_in_dataclass_default::*; +pub(crate) use implicit_optional::*; pub(crate) use invalid_pyproject_toml::InvalidPyprojectToml; pub(crate) use mutable_class_default::*; pub(crate) use mutable_dataclass_default::*; @@ -17,6 +18,7 @@ mod confusables; mod explicit_f_string_type_conversion; mod function_call_in_dataclass_default; mod helpers; +mod implicit_optional; mod invalid_pyproject_toml; mod mutable_class_default; mod mutable_dataclass_default; diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap new file mode 100644 index 0000000000..956b055d44 --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap @@ -0,0 +1,316 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF013_0.py:21:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +21 | def f(arg: int = None): # RUF011 + | ^^^ RUF013 +22 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +18 18 | pass +19 19 | +20 20 | +21 |-def f(arg: int = None): # RUF011 + 21 |+def f(arg: Optional[int] = None): # RUF011 +22 22 | pass +23 23 | +24 24 | + +RUF013_0.py:25:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +25 | def f(arg: str = None): # RUF011 + | ^^^ RUF013 +26 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +22 22 | pass +23 23 | +24 24 | +25 |-def f(arg: str = None): # RUF011 + 25 |+def f(arg: Optional[str] = None): # RUF011 +26 26 | pass +27 27 | +28 28 | + +RUF013_0.py:29:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +29 | def f(arg: typing.List[str] = None): # RUF011 + | ^^^^^^^^^^^^^^^^ RUF013 +30 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +26 26 | pass +27 27 | +28 28 | +29 |-def f(arg: typing.List[str] = None): # RUF011 + 29 |+def f(arg: Optional[typing.List[str]] = None): # RUF011 +30 30 | pass +31 31 | +32 32 | + +RUF013_0.py:33:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +33 | def f(arg: Tuple[str] = None): # RUF011 + | ^^^^^^^^^^ RUF013 +34 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +30 30 | pass +31 31 | +32 32 | +33 |-def f(arg: Tuple[str] = None): # RUF011 + 33 |+def f(arg: Optional[Tuple[str]] = None): # RUF011 +34 34 | pass +35 35 | +36 36 | + +RUF013_0.py:67:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +67 | def f(arg: Union = None): # RUF011 + | ^^^^^ RUF013 +68 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +64 64 | pass +65 65 | +66 66 | +67 |-def f(arg: Union = None): # RUF011 + 67 |+def f(arg: Optional[Union] = None): # RUF011 +68 68 | pass +69 69 | +70 70 | + +RUF013_0.py:71:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +71 | def f(arg: Union[int, str] = None): # RUF011 + | ^^^^^^^^^^^^^^^ RUF013 +72 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +68 68 | pass +69 69 | +70 70 | +71 |-def f(arg: Union[int, str] = None): # RUF011 + 71 |+def f(arg: Optional[Union[int, str]] = None): # RUF011 +72 72 | pass +73 73 | +74 74 | + +RUF013_0.py:75:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +75 | def f(arg: typing.Union[int, str] = None): # RUF011 + | ^^^^^^^^^^^^^^^^^^^^^^ RUF013 +76 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +72 72 | pass +73 73 | +74 74 | +75 |-def f(arg: typing.Union[int, str] = None): # RUF011 + 75 |+def f(arg: Optional[typing.Union[int, str]] = None): # RUF011 +76 76 | pass +77 77 | +78 78 | + +RUF013_0.py:94:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +94 | def f(arg: int | float = None): # RUF011 + | ^^^^^^^^^^^ RUF013 +95 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +91 91 | pass +92 92 | +93 93 | +94 |-def f(arg: int | float = None): # RUF011 + 94 |+def f(arg: Optional[int | float] = None): # RUF011 +95 95 | pass +96 96 | +97 97 | + +RUF013_0.py:98:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +98 | def f(arg: int | float | str | bytes = None): # RUF011 + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013 +99 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +95 95 | pass +96 96 | +97 97 | +98 |-def f(arg: int | float | str | bytes = None): # RUF011 + 98 |+def f(arg: Optional[int | float | str | bytes] = None): # RUF011 +99 99 | pass +100 100 | +101 101 | + +RUF013_0.py:113:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +113 | def f(arg: Literal[1, "foo"] = None): # RUF011 + | ^^^^^^^^^^^^^^^^^ RUF013 +114 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +110 110 | pass +111 111 | +112 112 | +113 |-def f(arg: Literal[1, "foo"] = None): # RUF011 + 113 |+def f(arg: Optional[Literal[1, "foo"]] = None): # RUF011 +114 114 | pass +115 115 | +116 116 | + +RUF013_0.py:117:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +117 | def f(arg: typing.Literal[1, "foo", True] = None): # RUF011 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013 +118 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +114 114 | pass +115 115 | +116 116 | +117 |-def f(arg: typing.Literal[1, "foo", True] = None): # RUF011 + 117 |+def f(arg: Optional[typing.Literal[1, "foo", True]] = None): # RUF011 +118 118 | pass +119 119 | +120 120 | + +RUF013_0.py:136:22: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +136 | def f(arg: Annotated[int, ...] = None): # RUF011 + | ^^^ RUF013 +137 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +133 133 | pass +134 134 | +135 135 | +136 |-def f(arg: Annotated[int, ...] = None): # RUF011 + 136 |+def f(arg: Annotated[Optional[int], ...] = None): # RUF011 +137 137 | pass +138 138 | +139 139 | + +RUF013_0.py:140:32: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +140 | def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF011 + | ^^^^^^^^^ RUF013 +141 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +137 137 | pass +138 138 | +139 139 | +140 |-def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF011 + 140 |+def f(arg: Annotated[Annotated[Optional[int | str], ...], ...] = None): # RUF011 +141 141 | pass +142 142 | +143 143 | + +RUF013_0.py:156:11: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +155 | def f( +156 | arg1: int = None, # RUF011 + | ^^^ RUF013 +157 | arg2: Union[int, float] = None, # RUF011 +158 | arg3: Literal[1, 2, 3] = None, # RUF011 + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +153 153 | +154 154 | +155 155 | def f( +156 |- arg1: int = None, # RUF011 + 156 |+ arg1: Optional[int] = None, # RUF011 +157 157 | arg2: Union[int, float] = None, # RUF011 +158 158 | arg3: Literal[1, 2, 3] = None, # RUF011 +159 159 | ): + +RUF013_0.py:157:11: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +155 | def f( +156 | arg1: int = None, # RUF011 +157 | arg2: Union[int, float] = None, # RUF011 + | ^^^^^^^^^^^^^^^^^ RUF013 +158 | arg3: Literal[1, 2, 3] = None, # RUF011 +159 | ): + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +154 154 | +155 155 | def f( +156 156 | arg1: int = None, # RUF011 +157 |- arg2: Union[int, float] = None, # RUF011 + 157 |+ arg2: Optional[Union[int, float]] = None, # RUF011 +158 158 | arg3: Literal[1, 2, 3] = None, # RUF011 +159 159 | ): +160 160 | pass + +RUF013_0.py:158:11: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +156 | arg1: int = None, # RUF011 +157 | arg2: Union[int, float] = None, # RUF011 +158 | arg3: Literal[1, 2, 3] = None, # RUF011 + | ^^^^^^^^^^^^^^^^ RUF013 +159 | ): +160 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +155 155 | def f( +156 156 | arg1: int = None, # RUF011 +157 157 | arg2: Union[int, float] = None, # RUF011 +158 |- arg3: Literal[1, 2, 3] = None, # RUF011 + 158 |+ arg3: Optional[Literal[1, 2, 3]] = None, # RUF011 +159 159 | ): +160 160 | pass +161 161 | + +RUF013_0.py:186:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +186 | def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF011 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013 +187 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +183 183 | pass +184 184 | +185 185 | +186 |-def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF011 + 186 |+def f(arg: Optional[Union[Annotated[int, ...], Union[str, bytes]]] = None): # RUF011 +187 187 | pass + + diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_1.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_1.py.snap new file mode 100644 index 0000000000..ef30934200 --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_1.py.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF013_1.py:4:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +4 | def f(arg: int = None): # RUF011 + | ^^^ RUF013 +5 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +1 1 | # No `typing.Optional` import + 2 |+from typing import Optional +2 3 | +3 4 | +4 |-def f(arg: int = None): # RUF011 + 5 |+def f(arg: Optional[int] = None): # RUF011 +5 6 | pass + + diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap new file mode 100644 index 0000000000..23ff31a6b6 --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap @@ -0,0 +1,316 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF013_0.py:21:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +21 | def f(arg: int = None): # RUF011 + | ^^^ RUF013 +22 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +18 18 | pass +19 19 | +20 20 | +21 |-def f(arg: int = None): # RUF011 + 21 |+def f(arg: int | None = None): # RUF011 +22 22 | pass +23 23 | +24 24 | + +RUF013_0.py:25:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +25 | def f(arg: str = None): # RUF011 + | ^^^ RUF013 +26 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +22 22 | pass +23 23 | +24 24 | +25 |-def f(arg: str = None): # RUF011 + 25 |+def f(arg: str | None = None): # RUF011 +26 26 | pass +27 27 | +28 28 | + +RUF013_0.py:29:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +29 | def f(arg: typing.List[str] = None): # RUF011 + | ^^^^^^^^^^^^^^^^ RUF013 +30 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +26 26 | pass +27 27 | +28 28 | +29 |-def f(arg: typing.List[str] = None): # RUF011 + 29 |+def f(arg: typing.List[str] | None = None): # RUF011 +30 30 | pass +31 31 | +32 32 | + +RUF013_0.py:33:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +33 | def f(arg: Tuple[str] = None): # RUF011 + | ^^^^^^^^^^ RUF013 +34 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +30 30 | pass +31 31 | +32 32 | +33 |-def f(arg: Tuple[str] = None): # RUF011 + 33 |+def f(arg: Tuple[str] | None = None): # RUF011 +34 34 | pass +35 35 | +36 36 | + +RUF013_0.py:67:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +67 | def f(arg: Union = None): # RUF011 + | ^^^^^ RUF013 +68 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +64 64 | pass +65 65 | +66 66 | +67 |-def f(arg: Union = None): # RUF011 + 67 |+def f(arg: Union | None = None): # RUF011 +68 68 | pass +69 69 | +70 70 | + +RUF013_0.py:71:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +71 | def f(arg: Union[int, str] = None): # RUF011 + | ^^^^^^^^^^^^^^^ RUF013 +72 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +68 68 | pass +69 69 | +70 70 | +71 |-def f(arg: Union[int, str] = None): # RUF011 + 71 |+def f(arg: Union[int, str] | None = None): # RUF011 +72 72 | pass +73 73 | +74 74 | + +RUF013_0.py:75:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +75 | def f(arg: typing.Union[int, str] = None): # RUF011 + | ^^^^^^^^^^^^^^^^^^^^^^ RUF013 +76 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +72 72 | pass +73 73 | +74 74 | +75 |-def f(arg: typing.Union[int, str] = None): # RUF011 + 75 |+def f(arg: typing.Union[int, str] | None = None): # RUF011 +76 76 | pass +77 77 | +78 78 | + +RUF013_0.py:94:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +94 | def f(arg: int | float = None): # RUF011 + | ^^^^^^^^^^^ RUF013 +95 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +91 91 | pass +92 92 | +93 93 | +94 |-def f(arg: int | float = None): # RUF011 + 94 |+def f(arg: int | float | None = None): # RUF011 +95 95 | pass +96 96 | +97 97 | + +RUF013_0.py:98:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +98 | def f(arg: int | float | str | bytes = None): # RUF011 + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013 +99 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +95 95 | pass +96 96 | +97 97 | +98 |-def f(arg: int | float | str | bytes = None): # RUF011 + 98 |+def f(arg: int | float | str | bytes | None = None): # RUF011 +99 99 | pass +100 100 | +101 101 | + +RUF013_0.py:113:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +113 | def f(arg: Literal[1, "foo"] = None): # RUF011 + | ^^^^^^^^^^^^^^^^^ RUF013 +114 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +110 110 | pass +111 111 | +112 112 | +113 |-def f(arg: Literal[1, "foo"] = None): # RUF011 + 113 |+def f(arg: Literal[1, "foo"] | None = None): # RUF011 +114 114 | pass +115 115 | +116 116 | + +RUF013_0.py:117:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +117 | def f(arg: typing.Literal[1, "foo", True] = None): # RUF011 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013 +118 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +114 114 | pass +115 115 | +116 116 | +117 |-def f(arg: typing.Literal[1, "foo", True] = None): # RUF011 + 117 |+def f(arg: typing.Literal[1, "foo", True] | None = None): # RUF011 +118 118 | pass +119 119 | +120 120 | + +RUF013_0.py:136:22: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +136 | def f(arg: Annotated[int, ...] = None): # RUF011 + | ^^^ RUF013 +137 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +133 133 | pass +134 134 | +135 135 | +136 |-def f(arg: Annotated[int, ...] = None): # RUF011 + 136 |+def f(arg: Annotated[int | None, ...] = None): # RUF011 +137 137 | pass +138 138 | +139 139 | + +RUF013_0.py:140:32: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +140 | def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF011 + | ^^^^^^^^^ RUF013 +141 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +137 137 | pass +138 138 | +139 139 | +140 |-def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF011 + 140 |+def f(arg: Annotated[Annotated[int | str | None, ...], ...] = None): # RUF011 +141 141 | pass +142 142 | +143 143 | + +RUF013_0.py:156:11: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +155 | def f( +156 | arg1: int = None, # RUF011 + | ^^^ RUF013 +157 | arg2: Union[int, float] = None, # RUF011 +158 | arg3: Literal[1, 2, 3] = None, # RUF011 + | + = help: Convert to `T | None` + +ℹ Suggested fix +153 153 | +154 154 | +155 155 | def f( +156 |- arg1: int = None, # RUF011 + 156 |+ arg1: int | None = None, # RUF011 +157 157 | arg2: Union[int, float] = None, # RUF011 +158 158 | arg3: Literal[1, 2, 3] = None, # RUF011 +159 159 | ): + +RUF013_0.py:157:11: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +155 | def f( +156 | arg1: int = None, # RUF011 +157 | arg2: Union[int, float] = None, # RUF011 + | ^^^^^^^^^^^^^^^^^ RUF013 +158 | arg3: Literal[1, 2, 3] = None, # RUF011 +159 | ): + | + = help: Convert to `T | None` + +ℹ Suggested fix +154 154 | +155 155 | def f( +156 156 | arg1: int = None, # RUF011 +157 |- arg2: Union[int, float] = None, # RUF011 + 157 |+ arg2: Union[int, float] | None = None, # RUF011 +158 158 | arg3: Literal[1, 2, 3] = None, # RUF011 +159 159 | ): +160 160 | pass + +RUF013_0.py:158:11: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +156 | arg1: int = None, # RUF011 +157 | arg2: Union[int, float] = None, # RUF011 +158 | arg3: Literal[1, 2, 3] = None, # RUF011 + | ^^^^^^^^^^^^^^^^ RUF013 +159 | ): +160 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +155 155 | def f( +156 156 | arg1: int = None, # RUF011 +157 157 | arg2: Union[int, float] = None, # RUF011 +158 |- arg3: Literal[1, 2, 3] = None, # RUF011 + 158 |+ arg3: Literal[1, 2, 3] | None = None, # RUF011 +159 159 | ): +160 160 | pass +161 161 | + +RUF013_0.py:186:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +186 | def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF011 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013 +187 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +183 183 | pass +184 184 | +185 185 | +186 |-def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF011 + 186 |+def f(arg: Union[Annotated[int, ...], Union[str, bytes]] | None = None): # RUF011 +187 187 | pass + + diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_1.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_1.py.snap new file mode 100644 index 0000000000..65568b66a3 --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_1.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF013_1.py:4:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +4 | def f(arg: int = None): # RUF011 + | ^^^ RUF013 +5 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +1 1 | # No `typing.Optional` import +2 2 | +3 3 | +4 |-def f(arg: int = None): # RUF011 + 4 |+def f(arg: int | None = None): # RUF011 +5 5 | pass + + diff --git a/ruff.schema.json b/ruff.schema.json index ba2220c10c..5bc038143a 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2349,6 +2349,7 @@ "RUF010", "RUF011", "RUF012", + "RUF013", "RUF1", "RUF10", "RUF100", From 54e103fc99fa59948bd7ea5c4c40fdc41adacb10 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 12 Jun 2023 14:43:06 -0400 Subject: [PATCH 018/447] Add a rule to remove unnecessary parentheses in class definitions (#5032) Closes #2409. --- .../test/fixtures/pyupgrade/UP039.py | 26 +++++ crates/ruff/src/checkers/ast/mod.rs | 3 + crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/pyupgrade/mod.rs | 81 ++++++++-------- crates/ruff/src/rules/pyupgrade/rules/mod.rs | 2 + .../rules/unnecessary_class_parentheses.rs | 96 +++++++++++++++++++ ...ff__rules__pyupgrade__tests__UP039.py.snap | 59 ++++++++++++ ruff.schema.json | 1 + scripts/check_docs_formatted.py | 1 + 9 files changed, 230 insertions(+), 40 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/pyupgrade/UP039.py create mode 100644 crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs create mode 100644 crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP039.py.snap diff --git a/crates/ruff/resources/test/fixtures/pyupgrade/UP039.py b/crates/ruff/resources/test/fixtures/pyupgrade/UP039.py new file mode 100644 index 0000000000..a4da32bc32 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pyupgrade/UP039.py @@ -0,0 +1,26 @@ +# Errors +class A(): + pass + + +class A() \ + : + pass + + +class A \ + (): + pass + + +# OK +class A: + pass + + +class A(A): + pass + + +class A(metaclass=type): + pass diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index bb448c2140..0f176b6921 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -729,6 +729,9 @@ where if self.enabled(Rule::UselessObjectInheritance) { pyupgrade::rules::useless_object_inheritance(self, stmt, name, bases, keywords); } + if self.enabled(Rule::UnnecessaryClassParentheses) { + pyupgrade::rules::unnecessary_class_parentheses(self, class_def); + } if self.enabled(Rule::AmbiguousClassName) { if let Some(diagnostic) = pycodestyle::rules::ambiguous_class_name(name, || { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 6d3a3d5b8f..6085e64b5a 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -414,6 +414,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pyupgrade, "036") => (RuleGroup::Unspecified, rules::pyupgrade::rules::OutdatedVersionBlock), (Pyupgrade, "037") => (RuleGroup::Unspecified, rules::pyupgrade::rules::QuotedAnnotation), (Pyupgrade, "038") => (RuleGroup::Unspecified, rules::pyupgrade::rules::NonPEP604Isinstance), + (Pyupgrade, "039") => (RuleGroup::Unspecified, rules::pyupgrade::rules::UnnecessaryClassParentheses), // pydocstyle (Pydocstyle, "100") => (RuleGroup::Unspecified, rules::pydocstyle::rules::UndocumentedPublicModule), diff --git a/crates/ruff/src/rules/pyupgrade/mod.rs b/crates/ruff/src/rules/pyupgrade/mod.rs index 331194a03d..260ee6af1c 100644 --- a/crates/ruff/src/rules/pyupgrade/mod.rs +++ b/crates/ruff/src/rules/pyupgrade/mod.rs @@ -16,64 +16,65 @@ mod tests { use crate::test::test_path; use crate::{assert_messages, settings}; - #[test_case(Rule::UselessMetaclassType, Path::new("UP001.py"))] - #[test_case(Rule::TypeOfPrimitive, Path::new("UP003.py"))] - #[test_case(Rule::UselessObjectInheritance, Path::new("UP004.py"))] + #[test_case(Rule::ConvertNamedTupleFunctionalToClass, Path::new("UP014.py"))] + #[test_case(Rule::ConvertTypedDictFunctionalToClass, Path::new("UP013.py"))] + #[test_case(Rule::DeprecatedCElementTree, Path::new("UP023.py"))] + #[test_case(Rule::DeprecatedImport, Path::new("UP035.py"))] + #[test_case(Rule::DeprecatedMockImport, Path::new("UP026.py"))] #[test_case(Rule::DeprecatedUnittestAlias, Path::new("UP005.py"))] + #[test_case(Rule::ExtraneousParentheses, Path::new("UP034.py"))] + #[test_case(Rule::FString, Path::new("UP032_0.py"))] + #[test_case(Rule::FString, Path::new("UP032_1.py"))] + #[test_case(Rule::FString, Path::new("UP032_2.py"))] + #[test_case(Rule::FormatLiterals, Path::new("UP030_0.py"))] + #[test_case(Rule::FormatLiterals, Path::new("UP030_1.py"))] + #[test_case(Rule::FormatLiterals, Path::new("UP030_2.py"))] + #[test_case(Rule::LRUCacheWithMaxsizeNone, Path::new("UP033_0.py"))] + #[test_case(Rule::LRUCacheWithMaxsizeNone, Path::new("UP033_1.py"))] + #[test_case(Rule::LRUCacheWithoutParameters, Path::new("UP011.py"))] + #[test_case(Rule::NativeLiterals, Path::new("UP018.py"))] #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_0.py"))] #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_1.py"))] #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_2.py"))] #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_3.py"))] #[test_case(Rule::NonPEP604Annotation, Path::new("UP007.py"))] - #[test_case(Rule::SuperCallWithParameters, Path::new("UP008.py"))] - #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_0.py"))] - #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_1.py"))] - #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_2.py"))] - #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_3.py"))] - #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_4.py"))] - #[test_case(Rule::UnnecessaryFutureImport, Path::new("UP010.py"))] - #[test_case(Rule::LRUCacheWithoutParameters, Path::new("UP011.py"))] - #[test_case(Rule::UnnecessaryEncodeUTF8, Path::new("UP012.py"))] - #[test_case(Rule::ConvertTypedDictFunctionalToClass, Path::new("UP013.py"))] - #[test_case(Rule::ConvertNamedTupleFunctionalToClass, Path::new("UP014.py"))] - #[test_case(Rule::RedundantOpenModes, Path::new("UP015.py"))] - #[test_case(Rule::NativeLiterals, Path::new("UP018.py"))] - #[test_case(Rule::TypingTextStrAlias, Path::new("UP019.py"))] - #[test_case(Rule::OpenAlias, Path::new("UP020.py"))] - #[test_case(Rule::ReplaceUniversalNewlines, Path::new("UP021.py"))] - #[test_case(Rule::ReplaceStdoutStderr, Path::new("UP022.py"))] - #[test_case(Rule::DeprecatedCElementTree, Path::new("UP023.py"))] + #[test_case(Rule::NonPEP604Isinstance, Path::new("UP038.py"))] #[test_case(Rule::OSErrorAlias, Path::new("UP024_0.py"))] #[test_case(Rule::OSErrorAlias, Path::new("UP024_1.py"))] #[test_case(Rule::OSErrorAlias, Path::new("UP024_2.py"))] #[test_case(Rule::OSErrorAlias, Path::new("UP024_3.py"))] #[test_case(Rule::OSErrorAlias, Path::new("UP024_4.py"))] - #[test_case(Rule::UnicodeKindPrefix, Path::new("UP025.py"))] - #[test_case(Rule::DeprecatedMockImport, Path::new("UP026.py"))] - #[test_case(Rule::UnpackedListComprehension, Path::new("UP027.py"))] - #[test_case(Rule::YieldInForLoop, Path::new("UP028_0.py"))] - #[test_case(Rule::YieldInForLoop, Path::new("UP028_1.py"))] - #[test_case(Rule::UnnecessaryBuiltinImport, Path::new("UP029.py"))] - #[test_case(Rule::FormatLiterals, Path::new("UP030_0.py"))] - #[test_case(Rule::FormatLiterals, Path::new("UP030_1.py"))] - #[test_case(Rule::FormatLiterals, Path::new("UP030_2.py"))] - #[test_case(Rule::PrintfStringFormatting, Path::new("UP031_0.py"))] - #[test_case(Rule::PrintfStringFormatting, Path::new("UP031_1.py"))] - #[test_case(Rule::FString, Path::new("UP032_0.py"))] - #[test_case(Rule::FString, Path::new("UP032_1.py"))] - #[test_case(Rule::FString, Path::new("UP032_2.py"))] - #[test_case(Rule::LRUCacheWithMaxsizeNone, Path::new("UP033_0.py"))] - #[test_case(Rule::LRUCacheWithMaxsizeNone, Path::new("UP033_1.py"))] - #[test_case(Rule::ExtraneousParentheses, Path::new("UP034.py"))] - #[test_case(Rule::DeprecatedImport, Path::new("UP035.py"))] + #[test_case(Rule::OpenAlias, Path::new("UP020.py"))] #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_0.py"))] #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_1.py"))] #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_2.py"))] #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_3.py"))] #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_4.py"))] #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_5.py"))] + #[test_case(Rule::PrintfStringFormatting, Path::new("UP031_0.py"))] + #[test_case(Rule::PrintfStringFormatting, Path::new("UP031_1.py"))] #[test_case(Rule::QuotedAnnotation, Path::new("UP037.py"))] - #[test_case(Rule::NonPEP604Isinstance, Path::new("UP038.py"))] + #[test_case(Rule::RedundantOpenModes, Path::new("UP015.py"))] + #[test_case(Rule::ReplaceStdoutStderr, Path::new("UP022.py"))] + #[test_case(Rule::ReplaceUniversalNewlines, Path::new("UP021.py"))] + #[test_case(Rule::SuperCallWithParameters, Path::new("UP008.py"))] + #[test_case(Rule::TypeOfPrimitive, Path::new("UP003.py"))] + #[test_case(Rule::TypingTextStrAlias, Path::new("UP019.py"))] + #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_0.py"))] + #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_1.py"))] + #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_2.py"))] + #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_3.py"))] + #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_4.py"))] + #[test_case(Rule::UnicodeKindPrefix, Path::new("UP025.py"))] + #[test_case(Rule::UnnecessaryBuiltinImport, Path::new("UP029.py"))] + #[test_case(Rule::UnnecessaryClassParentheses, Path::new("UP039.py"))] + #[test_case(Rule::UnnecessaryEncodeUTF8, Path::new("UP012.py"))] + #[test_case(Rule::UnnecessaryFutureImport, Path::new("UP010.py"))] + #[test_case(Rule::UnpackedListComprehension, Path::new("UP027.py"))] + #[test_case(Rule::UselessMetaclassType, Path::new("UP001.py"))] + #[test_case(Rule::UselessObjectInheritance, Path::new("UP004.py"))] + #[test_case(Rule::YieldInForLoop, Path::new("UP028_0.py"))] + #[test_case(Rule::YieldInForLoop, Path::new("UP028_1.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = path.to_string_lossy().to_string(); let diagnostics = test_path( diff --git a/crates/ruff/src/rules/pyupgrade/rules/mod.rs b/crates/ruff/src/rules/pyupgrade/rules/mod.rs index f8ac097ca7..16ca52a57b 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/mod.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/mod.rs @@ -24,6 +24,7 @@ pub(crate) use type_of_primitive::*; pub(crate) use typing_text_str_alias::*; pub(crate) use unicode_kind_prefix::*; pub(crate) use unnecessary_builtin_import::*; +pub(crate) use unnecessary_class_parentheses::*; pub(crate) use unnecessary_coding_comment::*; pub(crate) use unnecessary_encode_utf8::*; pub(crate) use unnecessary_future_import::*; @@ -61,6 +62,7 @@ mod type_of_primitive; mod typing_text_str_alias; mod unicode_kind_prefix; mod unnecessary_builtin_import; +mod unnecessary_class_parentheses; mod unnecessary_coding_comment; mod unnecessary_encode_utf8; mod unnecessary_future_import; diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs new file mode 100644 index 0000000000..f6e0120a3a --- /dev/null +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs @@ -0,0 +1,96 @@ +use std::ops::Add; + +use ruff_text_size::{TextRange, TextSize}; +use rustpython_parser::ast::{self}; + +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; +use crate::registry::AsRule; + +/// ## What it does +/// Checks for class definitions that include unnecessary parentheses after +/// the class name. +/// +/// ## Why is this bad? +/// If a class definition doesn't have any bases, the parentheses are +/// unnecessary. +/// +/// ## Examples +/// ```python +/// class Foo(): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// class Foo: +/// ... +/// ``` +#[violation] +pub struct UnnecessaryClassParentheses; + +impl AlwaysAutofixableViolation for UnnecessaryClassParentheses { + #[derive_message_formats] + fn message(&self) -> String { + format!("Unnecessary parentheses after class definition") + } + + fn autofix_title(&self) -> String { + "Remove parentheses".to_string() + } +} + +/// UP039 +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 contents = checker.locator.slice(class_def.range); + + // Find the open and closing parentheses between the class name and the colon, if they exist. + let mut depth = 0u32; + let mut start = None; + let mut end = None; + for (i, c) in contents.char_indices() { + match c { + '(' => { + if depth == 0 { + start = Some(i); + } + depth = depth.saturating_add(1); + } + ')' => { + depth = depth.saturating_sub(1); + if depth == 0 { + end = Some(i + c.len_utf8()); + } + } + ':' => { + if depth == 0 { + break; + } + } + _ => {} + } + } + let (Some(start), Some(end)) = (start, end) else { + return; + }; + + // Convert to `TextSize`. + let start = TextSize::try_from(start).unwrap(); + let end = TextSize::try_from(end).unwrap(); + + // Add initial offset. + let start = class_def.range.start().add(start); + let end = class_def.range.start().add(end); + + let mut diagnostic = Diagnostic::new(UnnecessaryClassParentheses, TextRange::new(start, end)); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.set_fix(Fix::automatic(Edit::deletion(start, end))); + } + checker.diagnostics.push(diagnostic); +} diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP039.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP039.py.snap new file mode 100644 index 0000000000..7cbeae5b2f --- /dev/null +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP039.py.snap @@ -0,0 +1,59 @@ +--- +source: crates/ruff/src/rules/pyupgrade/mod.rs +--- +UP039.py:2:8: UP039 [*] Unnecessary parentheses after class definition + | +1 | # Errors +2 | class A(): + | ^^ UP039 +3 | pass + | + = help: Remove parentheses + +ℹ Fix +1 1 | # Errors +2 |-class A(): + 2 |+class A: +3 3 | pass +4 4 | +5 5 | + +UP039.py:6:8: UP039 [*] Unnecessary parentheses after class definition + | +6 | class A() \ + | ^^ UP039 +7 | : +8 | pass + | + = help: Remove parentheses + +ℹ Fix +3 3 | pass +4 4 | +5 5 | +6 |-class A() \ + 6 |+class A \ +7 7 | : +8 8 | pass +9 9 | + +UP039.py:12:9: UP039 [*] Unnecessary parentheses after class definition + | +11 | class A \ +12 | (): + | ^^ UP039 +13 | pass + | + = help: Remove parentheses + +ℹ Fix +9 9 | +10 10 | +11 11 | class A \ +12 |- (): + 12 |+ : +13 13 | pass +14 14 | +15 15 | + + diff --git a/ruff.schema.json b/ruff.schema.json index 5bc038143a..54023b1ece 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2562,6 +2562,7 @@ "UP036", "UP037", "UP038", + "UP039", "W", "W1", "W19", diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index 34068a2e80..81752ac742 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -47,6 +47,7 @@ KNOWN_FORMATTING_VIOLATIONS = [ "too-few-spaces-before-inline-comment", "trailing-comma-on-bare-tuple", "unexpected-indentation-comment", + "unnecessary-class-parentheses", "useless-semicolon", "whitespace-after-open-bracket", "whitespace-before-close-bracket", From 6d861743c87a4471d0c12edf72a515a4ae700b9d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 12 Jun 2023 14:54:04 -0400 Subject: [PATCH 019/447] Remove custom tests in `rules/ruff/mod.rs` (#5033) --- crates/ruff/src/rules/ruff/mod.rs | 43 +++---------------- ...rules__ruff__tests__RUF007_RUF007.py.snap} | 0 2 files changed, 7 insertions(+), 36 deletions(-) rename crates/ruff/src/rules/ruff/snapshots/{ruff__rules__ruff__tests__ruff_pairwise_over_zipped.snap => ruff__rules__ruff__tests__RUF007_RUF007.py.snap} (100%) diff --git a/crates/ruff/src/rules/ruff/mod.rs b/crates/ruff/src/rules/ruff/mod.rs index 8d966395b8..3d5c312ce4 100644 --- a/crates/ruff/src/rules/ruff/mod.rs +++ b/crates/ruff/src/rules/ruff/mod.rs @@ -20,12 +20,16 @@ mod tests { use crate::test::{test_path, test_resource_path}; use crate::{assert_messages, settings}; - #[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005.py"))] #[test_case(Rule::AsyncioDanglingTask, Path::new("RUF006.py"))] + #[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005.py"))] #[test_case(Rule::ExplicitFStringTypeConversion, Path::new("RUF010.py"))] - #[test_case(Rule::StaticKeyDictComprehension, Path::new("RUF011.py"))] + #[test_case(Rule::FunctionCallInDataclassDefaultArgument, Path::new("RUF009.py"))] #[test_case(Rule::ImplicitOptional, Path::new("RUF013_0.py"))] #[test_case(Rule::ImplicitOptional, Path::new("RUF013_1.py"))] + #[test_case(Rule::MutableClassDefault, Path::new("RUF012.py"))] + #[test_case(Rule::MutableDataclassDefault, Path::new("RUF008.py"))] + #[test_case(Rule::PairwiseOverZipped, Path::new("RUF007.py"))] + #[test_case(Rule::StaticKeyDictComprehension, Path::new("RUF011.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( @@ -38,7 +42,7 @@ mod tests { #[test_case(Path::new("RUF013_0.py"))] #[test_case(Path::new("RUF013_1.py"))] - fn implicit_optional(path: &Path) -> Result<()> { + fn implicit_optional_py39(path: &Path) -> Result<()> { let snapshot = format!( "PY39_{}_{}", Rule::ImplicitOptional.noqa_code(), @@ -179,39 +183,6 @@ mod tests { Ok(()) } - #[test] - fn ruff_pairwise_over_zipped() -> Result<()> { - let diagnostics = test_path( - Path::new("ruff/RUF007.py"), - &settings::Settings::for_rules(vec![Rule::PairwiseOverZipped]), - )?; - assert_messages!(diagnostics); - Ok(()) - } - - #[test_case(Rule::MutableDataclassDefault, Path::new("RUF008.py"))] - #[test_case(Rule::FunctionCallInDataclassDefaultArgument, Path::new("RUF009.py"))] - fn mutable_dataclass_defaults(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); - let diagnostics = test_path( - Path::new("ruff").join(path).as_path(), - &settings::Settings::for_rule(rule_code), - )?; - assert_messages!(snapshot, diagnostics); - Ok(()) - } - - #[test_case(Rule::MutableClassDefault, Path::new("RUF012.py"))] - fn mutable_class_defaults(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); - let diagnostics = test_path( - Path::new("ruff").join(path).as_path(), - &settings::Settings::for_rule(rule_code), - )?; - assert_messages!(snapshot, diagnostics); - Ok(()) - } - #[test_case(Rule::InvalidPyprojectToml, Path::new("bleach"))] #[test_case(Rule::InvalidPyprojectToml, Path::new("invalid_author"))] #[test_case(Rule::InvalidPyprojectToml, Path::new("maturin"))] diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruff_pairwise_over_zipped.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF007_RUF007.py.snap similarity index 100% rename from crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruff_pairwise_over_zipped.snap rename to crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF007_RUF007.py.snap From 4080f36850f58d51462f6724462dff09ead4eae6 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 12 Jun 2023 15:19:59 -0400 Subject: [PATCH 020/447] Handle decorators in class-parenthesis-modifying rules (#5034) ## Summary A few of our rules look at the parentheses that follow a class definition (e.g., `class Foo(object):`) and attempt to modify those parentheses. Neither of those rules were behaving properly in the presence of decorators, which were recently added to the statement range. ## Test Plan `cargo test` with a variety of new fixture tests. --- .../test/fixtures/pyupgrade/UP004.py | 13 ++ .../test/fixtures/pyupgrade/UP039.py | 17 +++ crates/ruff/src/checkers/ast/mod.rs | 4 +- .../rules/unnecessary_class_parentheses.rs | 16 ++- .../rules/useless_object_inheritance.rs | 69 +++------ ...ff__rules__pyupgrade__tests__UP004.py.snap | 131 +++++++++++++----- ...ff__rules__pyupgrade__tests__UP039.py.snap | 38 +++++ 7 files changed, 197 insertions(+), 91 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pyupgrade/UP004.py b/crates/ruff/resources/test/fixtures/pyupgrade/UP004.py index a114f15017..723733f938 100644 --- a/crates/ruff/resources/test/fixtures/pyupgrade/UP004.py +++ b/crates/ruff/resources/test/fixtures/pyupgrade/UP004.py @@ -134,6 +134,19 @@ class A( ... +class A(object, object): + ... + + +@decorator() +class A(object): + ... + +@decorator() # class A(object): +class A(object): + ... + + object = A diff --git a/crates/ruff/resources/test/fixtures/pyupgrade/UP039.py b/crates/ruff/resources/test/fixtures/pyupgrade/UP039.py index a4da32bc32..ba44e1deb9 100644 --- a/crates/ruff/resources/test/fixtures/pyupgrade/UP039.py +++ b/crates/ruff/resources/test/fixtures/pyupgrade/UP039.py @@ -13,6 +13,14 @@ class A \ pass +@decorator() +class A(): + pass + +@decorator +class A(): + pass + # OK class A: pass @@ -24,3 +32,12 @@ class A(A): class A(metaclass=type): pass + + +@decorator() +class A: + pass + +@decorator +class A: + pass diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 0f176b6921..e166718bdb 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -727,10 +727,10 @@ where pylint::rules::global_statement(self, name); } if self.enabled(Rule::UselessObjectInheritance) { - pyupgrade::rules::useless_object_inheritance(self, stmt, name, bases, keywords); + pyupgrade::rules::useless_object_inheritance(self, class_def, stmt); } if self.enabled(Rule::UnnecessaryClassParentheses) { - pyupgrade::rules::unnecessary_class_parentheses(self, class_def); + pyupgrade::rules::unnecessary_class_parentheses(self, class_def, stmt); } if self.enabled(Rule::AmbiguousClassName) { 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 f6e0120a3a..faa8f49fbd 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs @@ -1,10 +1,11 @@ use std::ops::Add; use ruff_text_size::{TextRange, TextSize}; -use rustpython_parser::ast::{self}; +use rustpython_parser::ast::{self, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::identifier_range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -43,12 +44,17 @@ impl AlwaysAutofixableViolation for UnnecessaryClassParentheses { } /// UP039 -pub(crate) fn unnecessary_class_parentheses(checker: &mut Checker, class_def: &ast::StmtClassDef) { +pub(crate) fn unnecessary_class_parentheses( + checker: &mut Checker, + class_def: &ast::StmtClassDef, + stmt: &Stmt, +) { if !class_def.bases.is_empty() || !class_def.keywords.is_empty() { return; } - let contents = checker.locator.slice(class_def.range); + let offset = identifier_range(stmt, checker.locator).start(); + let contents = checker.locator.after(offset); // Find the open and closing parentheses between the class name and the colon, if they exist. let mut depth = 0u32; @@ -85,8 +91,8 @@ pub(crate) fn unnecessary_class_parentheses(checker: &mut Checker, class_def: &a let end = TextSize::try_from(end).unwrap(); // Add initial offset. - let start = class_def.range.start().add(start); - let end = class_def.range.start().add(end); + let start = offset.add(start); + let end = offset.add(end); let mut diagnostic = Diagnostic::new(UnnecessaryClassParentheses, TextRange::new(start, end)); if checker.patch(diagnostic.kind.rule()) { 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 8a3bc3839d..6fa11e3a04 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs @@ -1,9 +1,8 @@ -use rustpython_parser::ast::{self, Expr, Keyword, Ranged, Stmt}; +use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::binding::{Binding, BindingKind, Bindings}; -use ruff_python_semantic::scope::Scope; +use ruff_python_ast::helpers::identifier_range; use crate::autofix::edits::remove_argument; use crate::checkers::ast::Checker; @@ -26,62 +25,40 @@ impl AlwaysAutofixableViolation for UselessObjectInheritance { } } -fn rule(name: &str, bases: &[Expr], scope: &Scope, bindings: &Bindings) -> Option { - for expr in bases { +/// UP004 +pub(crate) fn useless_object_inheritance( + checker: &mut Checker, + class_def: &ast::StmtClassDef, + stmt: &Stmt, +) { + for expr in &class_def.bases { let Expr::Name(ast::ExprName { id, .. }) = expr else { continue; }; if id != "object" { continue; } - if !matches!( - scope - .get(id.as_str()) - .map(|binding_id| &bindings[binding_id]), - None | Some(Binding { - kind: BindingKind::Builtin, - .. - }) - ) { + if !checker.semantic_model().is_builtin("object") { continue; } - return Some(Diagnostic::new( + + let mut diagnostic = Diagnostic::new( UselessObjectInheritance { - name: name.to_string(), + name: class_def.name.to_string(), }, expr.range(), - )); - } - - None -} - -/// UP004 -pub(crate) fn useless_object_inheritance( - checker: &mut Checker, - stmt: &Stmt, - name: &str, - bases: &[Expr], - keywords: &[Keyword], -) { - if let Some(mut diagnostic) = rule( - name, - bases, - checker.semantic_model().scope(), - &checker.semantic_model().bindings, - ) { + ); if checker.patch(diagnostic.kind.rule()) { - let expr_range = diagnostic.range(); - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { - remove_argument( + diagnostic.try_set_fix(|| { + let edit = remove_argument( checker.locator, - stmt.start(), - expr_range, - bases, - keywords, + identifier_range(stmt, checker.locator).start(), + expr.range(), + &class_def.bases, + &class_def.keywords, true, - ) + )?; + Ok(Fix::automatic(edit)) }); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP004.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP004.py.snap index f480d6b0df..e106675943 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP004.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP004.py.snap @@ -9,7 +9,7 @@ UP004.py:5:9: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 2 2 | ... 3 3 | 4 4 | @@ -29,7 +29,7 @@ UP004.py:10:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 6 6 | ... 7 7 | 8 8 | @@ -51,7 +51,7 @@ UP004.py:16:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 12 12 | ... 13 13 | 14 14 | @@ -75,7 +75,7 @@ UP004.py:24:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 19 19 | ... 20 20 | 21 21 | @@ -99,7 +99,7 @@ UP004.py:31:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 26 26 | ... 27 27 | 28 28 | @@ -122,7 +122,7 @@ UP004.py:37:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 33 33 | ... 34 34 | 35 35 | @@ -146,7 +146,7 @@ UP004.py:45:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 40 40 | ... 41 41 | 42 42 | @@ -171,7 +171,7 @@ UP004.py:53:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 48 48 | ... 49 49 | 50 50 | @@ -196,7 +196,7 @@ UP004.py:61:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 56 56 | ... 57 57 | 58 58 | @@ -221,7 +221,7 @@ UP004.py:69:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 64 64 | ... 65 65 | 66 66 | @@ -243,7 +243,7 @@ UP004.py:75:12: UP004 [*] Class `B` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 72 72 | ... 73 73 | 74 74 | @@ -261,7 +261,7 @@ UP004.py:79:9: UP004 [*] Class `B` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 76 76 | ... 77 77 | 78 78 | @@ -281,7 +281,7 @@ UP004.py:84:5: UP004 [*] Class `B` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 81 81 | 82 82 | 83 83 | class B( @@ -301,7 +301,7 @@ UP004.py:92:5: UP004 [*] Class `B` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 89 89 | 90 90 | class B( 91 91 | A, @@ -320,7 +320,7 @@ UP004.py:98:5: UP004 [*] Class `B` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 95 95 | 96 96 | 97 97 | class B( @@ -340,7 +340,7 @@ UP004.py:108:5: UP004 [*] Class `B` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 105 105 | class B( 106 106 | # Comment on A. 107 107 | A, @@ -349,25 +349,6 @@ UP004.py:108:5: UP004 [*] Class `B` inherits from `object` 110 109 | ... 111 110 | -UP004.py:114:13: UP004 [*] Class `A` inherits from `object` - | -113 | def f(): -114 | class A(object): - | ^^^^^^ UP004 -115 | ... - | - = help: Remove `object` inheritance - -ℹ Suggested fix -111 111 | -112 112 | -113 113 | def f(): -114 |- class A(object): - 114 |+ class A: -115 115 | ... -116 116 | -117 117 | - UP004.py:119:5: UP004 [*] Class `A` inherits from `object` | 118 | class A( @@ -378,7 +359,7 @@ UP004.py:119:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 115 115 | ... 116 116 | 117 117 | @@ -400,7 +381,7 @@ UP004.py:125:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 121 121 | ... 122 122 | 123 123 | @@ -422,7 +403,7 @@ UP004.py:131:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 127 127 | ... 128 128 | 129 129 | @@ -435,4 +416,78 @@ UP004.py:131:5: UP004 [*] Class `A` inherits from `object` 135 132 | 136 133 | +UP004.py:137:9: UP004 [*] Class `A` inherits from `object` + | +137 | class A(object, object): + | ^^^^^^ UP004 +138 | ... + | + = help: Remove `object` inheritance + +ℹ Fix +134 134 | ... +135 135 | +136 136 | +137 |-class A(object, object): + 137 |+class A(object): +138 138 | ... +139 139 | +140 140 | + +UP004.py:137:17: UP004 [*] Class `A` inherits from `object` + | +137 | class A(object, object): + | ^^^^^^ UP004 +138 | ... + | + = help: Remove `object` inheritance + +ℹ Fix +134 134 | ... +135 135 | +136 136 | +137 |-class A(object, object): + 137 |+class A(object): +138 138 | ... +139 139 | +140 140 | + +UP004.py:142:9: UP004 [*] Class `A` inherits from `object` + | +141 | @decorator() +142 | class A(object): + | ^^^^^^ UP004 +143 | ... + | + = help: Remove `object` inheritance + +ℹ Fix +139 139 | +140 140 | +141 141 | @decorator() +142 |-class A(object): + 142 |+class A: +143 143 | ... +144 144 | +145 145 | @decorator() # class A(object): + +UP004.py:146:9: UP004 [*] Class `A` inherits from `object` + | +145 | @decorator() # class A(object): +146 | class A(object): + | ^^^^^^ UP004 +147 | ... + | + = help: Remove `object` inheritance + +ℹ Fix +143 143 | ... +144 144 | +145 145 | @decorator() # class A(object): +146 |-class A(object): + 146 |+class A: +147 147 | ... +148 148 | +149 149 | + diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP039.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP039.py.snap index 7cbeae5b2f..4d1a0bb04f 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP039.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP039.py.snap @@ -56,4 +56,42 @@ UP039.py:12:9: UP039 [*] Unnecessary parentheses after class definition 14 14 | 15 15 | +UP039.py:17:8: UP039 [*] Unnecessary parentheses after class definition + | +16 | @decorator() +17 | class A(): + | ^^ UP039 +18 | pass + | + = help: Remove parentheses + +ℹ Fix +14 14 | +15 15 | +16 16 | @decorator() +17 |-class A(): + 17 |+class A: +18 18 | pass +19 19 | +20 20 | @decorator + +UP039.py:21:8: UP039 [*] Unnecessary parentheses after class definition + | +20 | @decorator +21 | class A(): + | ^^ UP039 +22 | pass + | + = help: Remove parentheses + +ℹ Fix +18 18 | pass +19 19 | +20 20 | @decorator +21 |-class A(): + 21 |+class A: +22 22 | pass +23 23 | +24 24 | # OK + From ab11dd08dfcbe9045fc334b65dee8a5217953196 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 12 Jun 2023 17:23:39 -0400 Subject: [PATCH 021/447] Improve `TypedDict` conversion logic for shadowed builtins and dunder methods (#5038) ## Summary This PR (1) avoids flagging `TypedDict` and `NamedTuple` conversions when attributes are dunder methods, like `__dict__`, and (2) avoids flagging the `A003` shadowed-attribute rule for `TypedDict` classes at all, where it doesn't really apply (since those attributes are only accessed via subscripting anyway). Closes #5027. --- .../test/fixtures/flake8_builtins/A003.py | 9 ++- crates/ruff/src/checkers/ast/mod.rs | 6 +- .../ruff/src/rules/flake8_builtins/helpers.rs | 2 +- .../rules/builtin_attribute_shadowing.rs | 12 ++++ ..._flake8_builtins__tests__A003_A003.py.snap | 6 +- ...sts__A003_A003.py_builtins_ignorelist.snap | 2 +- ...convert_named_tuple_functional_to_class.rs | 33 ++++++----- .../convert_typed_dict_functional_to_class.rs | 55 ++++++++++--------- crates/ruff_python_ast/src/helpers.rs | 2 +- 9 files changed, 78 insertions(+), 49 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_builtins/A003.py b/crates/ruff/resources/test/fixtures/flake8_builtins/A003.py index 0337a34e95..b971974ef2 100644 --- a/crates/ruff/resources/test/fixtures/flake8_builtins/A003.py +++ b/crates/ruff/resources/test/fixtures/flake8_builtins/A003.py @@ -1,6 +1,6 @@ class MyClass: ImportError = 4 - id = 5 + id: int dir = "/" def __init__(self): @@ -10,3 +10,10 @@ class MyClass: def str(self): pass + + +from typing import TypedDict + + +class MyClass(TypedDict): + id: int diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index e166718bdb..80a5fb86e3 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -656,10 +656,11 @@ where pyupgrade::rules::yield_in_for_loop(self, stmt); } - if self.semantic_model.scope().kind.is_class() { + if let ScopeKind::Class(class_def) = self.semantic_model.scope().kind { if self.enabled(Rule::BuiltinAttributeShadowing) { flake8_builtins::rules::builtin_attribute_shadowing( self, + class_def, name, AnyShadowing::from(stmt), ); @@ -2369,10 +2370,11 @@ where } } - if self.semantic_model.scope().kind.is_class() { + if let ScopeKind::Class(class_def) = self.semantic_model.scope().kind { if self.enabled(Rule::BuiltinAttributeShadowing) { flake8_builtins::rules::builtin_attribute_shadowing( self, + class_def, id, AnyShadowing::from(expr), ); diff --git a/crates/ruff/src/rules/flake8_builtins/helpers.rs b/crates/ruff/src/rules/flake8_builtins/helpers.rs index 1f1eb0f3ba..d350c11447 100644 --- a/crates/ruff/src/rules/flake8_builtins/helpers.rs +++ b/crates/ruff/src/rules/flake8_builtins/helpers.rs @@ -1,9 +1,9 @@ +use ruff_text_size::TextRange; use rustpython_parser::ast::{Excepthandler, Expr, Ranged, Stmt}; use ruff_python_ast::helpers::identifier_range; use ruff_python_ast::source_code::Locator; use ruff_python_stdlib::builtins::BUILTINS; -use ruff_text_size::TextRange; pub(super) fn shadows_builtin(name: &str, ignorelist: &[String]) -> bool { BUILTINS.contains(&name) && ignorelist.iter().all(|ignore| ignore != name) diff --git a/crates/ruff/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs b/crates/ruff/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs index 701fa4edd3..ec7e9fb4e6 100644 --- a/crates/ruff/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs +++ b/crates/ruff/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs @@ -1,6 +1,7 @@ use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; +use rustpython_parser::ast; use crate::checkers::ast::Checker; @@ -64,10 +65,21 @@ impl Violation for BuiltinAttributeShadowing { /// A003 pub(crate) fn builtin_attribute_shadowing( checker: &mut Checker, + class_def: &ast::StmtClassDef, name: &str, shadowing: AnyShadowing, ) { if shadows_builtin(name, &checker.settings.flake8_builtins.builtins_ignorelist) { + // Ignore shadowing within `TypedDict` definitions, since these are only accessible through + // subscripting and not through attribute access. + if class_def.bases.iter().any(|base| { + checker + .semantic_model() + .match_typing_expr(base, "TypedDict") + }) { + return; + } + checker.diagnostics.push(Diagnostic::new( BuiltinAttributeShadowing { name: name.to_string(), diff --git a/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A003_A003.py.snap b/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A003_A003.py.snap index 8c8a70b623..48c9dfa217 100644 --- a/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A003_A003.py.snap +++ b/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A003_A003.py.snap @@ -6,7 +6,7 @@ A003.py:2:5: A003 Class attribute `ImportError` is shadowing a Python builtin 1 | class MyClass: 2 | ImportError = 4 | ^^^^^^^^^^^ A003 -3 | id = 5 +3 | id: int 4 | dir = "/" | @@ -14,7 +14,7 @@ A003.py:3:5: A003 Class attribute `id` is shadowing a Python builtin | 1 | class MyClass: 2 | ImportError = 4 -3 | id = 5 +3 | id: int | ^^ A003 4 | dir = "/" | @@ -22,7 +22,7 @@ A003.py:3:5: A003 Class attribute `id` is shadowing a Python builtin A003.py:4:5: A003 Class attribute `dir` is shadowing a Python builtin | 2 | ImportError = 4 -3 | id = 5 +3 | id: int 4 | dir = "/" | ^^^ A003 5 | diff --git a/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A003_A003.py_builtins_ignorelist.snap b/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A003_A003.py_builtins_ignorelist.snap index 3c509b2fb8..342f14a9a1 100644 --- a/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A003_A003.py_builtins_ignorelist.snap +++ b/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A003_A003.py_builtins_ignorelist.snap @@ -6,7 +6,7 @@ A003.py:2:5: A003 Class attribute `ImportError` is shadowing a Python builtin 1 | class MyClass: 2 | ImportError = 4 | ^^^^^^^^^^^ A003 -3 | id = 5 +3 | id: int 4 | dir = "/" | 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 d2de8fb2b8..f9f86618f8 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 @@ -5,6 +5,7 @@ use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Keyword, Ranged, use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_dunder; use ruff_python_ast::source_code::Generator; use ruff_python_semantic::model::SemanticModel; use ruff_python_stdlib::identifiers::is_identifier; @@ -66,19 +67,21 @@ fn create_property_assignment_stmt( annotation: &Expr, value: Option<&Expr>, ) -> Stmt { - let node = ast::ExprName { - id: property.into(), - ctx: ExprContext::Load, - range: TextRange::default(), - }; - let node1 = ast::StmtAnnAssign { - target: Box::new(node.into()), + ast::StmtAnnAssign { + target: Box::new( + ast::ExprName { + id: property.into(), + ctx: ExprContext::Load, + range: TextRange::default(), + } + .into(), + ), annotation: Box::new(annotation.clone()), value: value.map(|value| Box::new(value.clone())), simple: true, range: TextRange::default(), - }; - node1.into() + } + .into() } /// Match the `defaults` keyword in a `NamedTuple(...)` call. @@ -140,6 +143,9 @@ fn create_properties_from_args(args: &[Expr], defaults: &[Expr]) -> Result Result, base_class: &Expr) -> Stmt { - let node = ast::StmtClassDef { + ast::StmtClassDef { name: typename.into(), bases: vec![base_class.clone()], keywords: vec![], body, decorator_list: vec![], range: TextRange::default(), - }; - node.into() + } + .into() } /// Generate a `Fix` to convert a `NamedTuple` assignment to a class definition. @@ -169,8 +175,7 @@ fn convert_to_class( base_class: &Expr, generator: Generator, ) -> Fix { - #[allow(deprecated)] - Fix::unspecified(Edit::range_replacement( + Fix::suggested(Edit::range_replacement( generator.stmt(&create_class_def_stmt(typename, body, base_class)), stmt.range(), )) diff --git a/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs b/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs index e1906fa880..50ae7304f4 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs @@ -5,6 +5,7 @@ use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Keyword, Ranged, use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_dunder; use ruff_python_ast::source_code::Generator; use ruff_python_semantic::model::SemanticModel; use ruff_python_stdlib::identifiers::is_identifier; @@ -62,20 +63,21 @@ fn match_typed_dict_assign<'a>( /// Generate a `Stmt::AnnAssign` representing the provided property /// definition. fn create_property_assignment_stmt(property: &str, annotation: &Expr) -> Stmt { - let node = annotation.clone(); - let node1 = ast::ExprName { - id: property.into(), - ctx: ExprContext::Load, - range: TextRange::default(), - }; - let node2 = ast::StmtAnnAssign { - target: Box::new(node1.into()), - annotation: Box::new(node), + ast::StmtAnnAssign { + target: Box::new( + ast::ExprName { + id: property.into(), + ctx: ExprContext::Load, + range: TextRange::default(), + } + .into(), + ), + annotation: Box::new(annotation.clone()), value: None, simple: true, range: TextRange::default(), - }; - node2.into() + } + .into() } /// Generate a `StmtKind:ClassDef` statement based on the provided body, @@ -90,15 +92,15 @@ fn create_class_def_stmt( Some(keyword) => vec![keyword.clone()], None => vec![], }; - let node = ast::StmtClassDef { + ast::StmtClassDef { name: class_name.into(), bases: vec![base_class.clone()], keywords, body, decorator_list: vec![], range: TextRange::default(), - }; - node.into() + } + .into() } fn properties_from_dict_literal(keys: &[Option], values: &[Expr]) -> Result> { @@ -116,11 +118,13 @@ fn properties_from_dict_literal(keys: &[Option], values: &[Expr]) -> Resul value: Constant::Str(property), .. })) => { - if is_identifier(property) { - Ok(create_property_assignment_stmt(property, value)) - } else { - bail!("Property name is not valid identifier: {}", property) + if !is_identifier(property) { + bail!("Invalid property name: {}", property) } + if is_dunder(property) { + bail!("Cannot use dunder property name: {}", property) + } + Ok(create_property_assignment_stmt(property, value)) } _ => bail!("Expected `key` to be `Constant::Str`"), }) @@ -170,12 +174,12 @@ fn properties_from_keywords(keywords: &[Keyword]) -> Result> { // TypedDict('name', {'a': int}, total=True) // ``` fn match_total_from_only_keyword(keywords: &[Keyword]) -> Option<&Keyword> { - let keyword = keywords.get(0)?; - let arg = &keyword.arg.as_ref()?; - match arg.as_str() { - "total" => Some(keyword), - _ => None, - } + keywords.iter().find(|keyword| { + let Some(arg) = &keyword.arg else { + return false + }; + arg.as_str() == "total" + }) } fn match_properties_and_total<'a>( @@ -219,8 +223,7 @@ fn convert_to_class( base_class: &Expr, generator: Generator, ) -> Fix { - #[allow(deprecated)] - Fix::unspecified(Edit::range_replacement( + Fix::suggested(Edit::range_replacement( generator.stmt(&create_class_def_stmt( class_name, body, diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 0f9ee007db..c9dce0160c 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -546,7 +546,7 @@ where body.iter().any(|stmt| any_over_stmt(stmt, func)) } -fn is_dunder(id: &str) -> bool { +pub fn is_dunder(id: &str) -> bool { id.starts_with("__") && id.ends_with("__") } From 7e37d8916c759f8c5a7d4e9d73cf6350b5a907da Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 12 Jun 2023 18:06:03 -0400 Subject: [PATCH 022/447] Remove lexer dependency from identifier_range (#5036) ## Summary We run this quite a bit -- the new version is zero-allocation, though it's not quite as nice as the lexer we have in the formatter. --- crates/ruff_python_ast/src/helpers.rs | 98 ++++++++++++++++++++++++--- 1 file changed, 88 insertions(+), 10 deletions(-) diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index c9dce0160c..a205e027e1 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -1,5 +1,5 @@ use std::borrow::Cow; -use std::ops::Sub; +use std::ops::{Add, Sub}; use std::path::Path; use itertools::Itertools; @@ -1071,6 +1071,21 @@ pub fn match_parens(start: TextSize, locator: &Locator) -> Option { } } +/// Return `true` if the given character is a valid identifier character. +fn is_identifier(c: char) -> bool { + c.is_alphanumeric() || c == '_' +} + +#[derive(Debug)] +enum IdentifierState { + /// We're in a comment, awaiting the identifier at the given index. + InComment { index: usize }, + /// We're looking for the identifier at the given index. + AwaitingIdentifier { index: usize }, + /// We're in the identifier at the given index, starting at the given character. + InIdentifier { index: usize, start: TextSize }, +} + /// Return the appropriate visual `Range` for any message that spans a `Stmt`. /// Specifically, this method returns the range of a function or class name, /// rather than that of the entire function or class body. @@ -1095,17 +1110,59 @@ pub fn identifier_range(stmt: &Stmt, locator: &Locator) -> TextRange { TextRange::new(last_decorator.end(), range.end()) }); - let contents = locator.slice(header_range); + // If the statement is an async function, we're looking for the third + // keyword-or-identifier (`foo` in `async def foo()`). Otherwise, it's the + // second keyword-or-identifier (`foo` in `def foo()` or `Foo` in `class Foo`). + let name_index = if stmt.is_async_function_def_stmt() { + 2 + } else { + 1 + }; - let mut tokens = - lexer::lex_starts_at(contents, Mode::Module, header_range.start()).flatten(); - tokens - .find_map(|(t, range)| t.is_name().then_some(range)) - .unwrap_or_else(|| { - error!("Failed to find identifier for {:?}", stmt); + let mut state = IdentifierState::AwaitingIdentifier { index: 0 }; + for (char_index, char) in locator.slice(header_range).char_indices() { + match state { + IdentifierState::InComment { index } => match char { + // Read until the end of the comment. + '\r' | '\n' => { + state = IdentifierState::AwaitingIdentifier { index }; + } + _ => {} + }, + IdentifierState::AwaitingIdentifier { index } => match char { + // Read until we hit an identifier. + '#' => { + state = IdentifierState::InComment { index }; + } + c if is_identifier(c) => { + state = IdentifierState::InIdentifier { + index, + start: TextSize::try_from(char_index).unwrap(), + }; + } + _ => {} + }, + IdentifierState::InIdentifier { index, start } => { + // We've reached the end of the identifier. + if !is_identifier(char) { + if index == name_index { + // We've found the identifier we're looking for. + let end = TextSize::try_from(char_index).unwrap(); + return TextRange::new( + header_range.start().add(start), + header_range.start().add(end), + ); + } - header_range - }) + // We're looking for a different identifier. + state = IdentifierState::AwaitingIdentifier { index: index + 1 }; + } + } + } + } + + error!("Failed to find identifier for {:?}", stmt); + header_range } _ => stmt.range(), } @@ -1681,6 +1738,14 @@ y = 2 TextRange::new(TextSize::from(4), TextSize::from(5)) ); + let contents = "async def f(): pass".trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + assert_eq!( + identifier_range(&stmt, &locator), + TextRange::new(TextSize::from(10), TextSize::from(11)) + ); + let contents = r#" def \ f(): @@ -1723,6 +1788,19 @@ class Class(): TextRange::new(TextSize::from(19), TextSize::from(24)) ); + let contents = r#" +@decorator() # Comment +class Class(): + pass +"# + .trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + assert_eq!( + identifier_range(&stmt, &locator), + TextRange::new(TextSize::from(30), TextSize::from(35)) + ); + let contents = r#"x = y + 1"#.trim(); let stmt = Stmt::parse(contents, "")?; let locator = Locator::new(contents); From 780336db0a993d77a91c43d7a6ee1ad8ed5695b8 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 12 Jun 2023 18:25:47 -0400 Subject: [PATCH 023/447] Include f-string prefixes in quote-stripping utilities (#5039) Mentioned here: https://github.com/astral-sh/ruff/pull/4853#discussion_r1217560348. Generated with this hacky script: https://gist.github.com/charliermarsh/8ecc4e55bc87d51dc27340402f33b348. --- crates/ruff_python_ast/src/str.rs | 133 ++++++++++++++++++++++++++++-- 1 file changed, 125 insertions(+), 8 deletions(-) diff --git a/crates/ruff_python_ast/src/str.rs b/crates/ruff_python_ast/src/str.rs index 7da1af24fe..bc484d8569 100644 --- a/crates/ruff_python_ast/src/str.rs +++ b/crates/ruff_python_ast/src/str.rs @@ -1,23 +1,140 @@ use ruff_text_size::{TextLen, TextRange}; +/// Includes all permutations of `r`, `u`, `f`, and `fr` (`ur` is invalid, as is `uf`). This +/// includes all possible orders, and all possible casings, for both single and triple quotes. +/// /// See: +#[rustfmt::skip] const TRIPLE_QUOTE_STR_PREFIXES: &[&str] = &[ - "u\"\"\"", "u'''", "r\"\"\"", "r'''", "U\"\"\"", "U'''", "R\"\"\"", "R'''", "\"\"\"", "'''", + "FR\"\"\"", + "Fr\"\"\"", + "fR\"\"\"", + "fr\"\"\"", + "RF\"\"\"", + "Rf\"\"\"", + "rF\"\"\"", + "rf\"\"\"", + "FR'''", + "Fr'''", + "fR'''", + "fr'''", + "RF'''", + "Rf'''", + "rF'''", + "rf'''", + "R\"\"\"", + "r\"\"\"", + "R'''", + "r'''", + "F\"\"\"", + "f\"\"\"", + "F'''", + "f'''", + "U\"\"\"", + "u\"\"\"", + "U'''", + "u'''", + "\"\"\"", + "'''", ]; + +#[rustfmt::skip] const SINGLE_QUOTE_STR_PREFIXES: &[&str] = &[ - "u\"", "u'", "r\"", "r'", "U\"", "U'", "R\"", "R'", "\"", "'", + "FR\"", + "Fr\"", + "fR\"", + "fr\"", + "RF\"", + "Rf\"", + "rF\"", + "rf\"", + "FR'", + "Fr'", + "fR'", + "fr'", + "RF'", + "Rf'", + "rF'", + "rf'", + "R\"", + "r\"", + "R'", + "r'", + "F\"", + "f\"", + "F'", + "f'", + "U\"", + "u\"", + "U'", + "u'", + "\"", + "'", ]; + +/// Includes all permutations of `b` and `rb`. This includes all possible orders, and all possible +/// casings, for both single and triple quotes. +/// +/// See: +#[rustfmt::skip] pub const TRIPLE_QUOTE_BYTE_PREFIXES: &[&str] = &[ - "br'''", "rb'''", "bR'''", "Rb'''", "Br'''", "rB'''", "RB'''", "BR'''", "b'''", "br\"\"\"", - "rb\"\"\"", "bR\"\"\"", "Rb\"\"\"", "Br\"\"\"", "rB\"\"\"", "RB\"\"\"", "BR\"\"\"", "b\"\"\"", + "BR\"\"\"", + "Br\"\"\"", + "bR\"\"\"", + "br\"\"\"", + "RB\"\"\"", + "Rb\"\"\"", + "rB\"\"\"", + "rb\"\"\"", + "BR'''", + "Br'''", + "bR'''", + "br'''", + "RB'''", + "Rb'''", + "rB'''", + "rb'''", "B\"\"\"", + "b\"\"\"", + "B'''", + "b'''", ]; + +#[rustfmt::skip] pub const SINGLE_QUOTE_BYTE_PREFIXES: &[&str] = &[ - "br'", "rb'", "bR'", "Rb'", "Br'", "rB'", "RB'", "BR'", "b'", "br\"", "rb\"", "bR\"", "Rb\"", - "Br\"", "rB\"", "RB\"", "BR\"", "b\"", "B\"", + "BR\"", + "Br\"", + "bR\"", + "br\"", + "RB\"", + "Rb\"", + "rB\"", + "rb\"", + "BR'", + "Br'", + "bR'", + "br'", + "RB'", + "Rb'", + "rB'", + "rb'", + "B\"", + "b\"", + "B'", + "b'", +]; + +#[rustfmt::skip] +const TRIPLE_QUOTE_SUFFIXES: &[&str] = &[ + "\"\"\"", + "'''", +]; + +#[rustfmt::skip] +const SINGLE_QUOTE_SUFFIXES: &[&str] = &[ + "\"", + "'", ]; -const TRIPLE_QUOTE_SUFFIXES: &[&str] = &["\"\"\"", "'''"]; -const SINGLE_QUOTE_SUFFIXES: &[&str] = &["\"", "'"]; /// Strip the leading and trailing quotes from a string. /// Assumes that the string is a valid string literal, but does not verify that the string From e2130707f58765d8caade71c6dabb94bbb9dbfba Mon Sep 17 00:00:00 2001 From: Timofei Kukushkin Date: Tue, 13 Jun 2023 03:28:57 +0400 Subject: [PATCH 024/447] Autofixer for ISC001 (#4853) ## Summary This PR adds autofixer for rule ISC001 in cases where both string literals are of the same kind and with same quotes (double / single). Fixes #4829 ## Test Plan I added testcases with different combinations of string literals. --- .../flake8_implicit_str_concat/ISC.py | 16 +++ .../rules/implicit.rs | 55 ++++++- ...icit_str_concat__tests__ISC001_ISC.py.snap | 135 +++++++++++++++++- ...oncat__tests__multiline_ISC001_ISC.py.snap | 135 +++++++++++++++++- 4 files changed, 332 insertions(+), 9 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_implicit_str_concat/ISC.py b/crates/ruff/resources/test/fixtures/flake8_implicit_str_concat/ISC.py index a88e8e5573..633e075731 100644 --- a/crates/ruff/resources/test/fixtures/flake8_implicit_str_concat/ISC.py +++ b/crates/ruff/resources/test/fixtures/flake8_implicit_str_concat/ISC.py @@ -34,3 +34,19 @@ _ = ( b"abc" b"def" ) + +_ = """a""" """b""" + +_ = """a +b""" """c +d""" + +_ = f"""a""" f"""b""" + +_ = f"a" "b" + +_ = """a""" "b" + +_ = 'a' "b" + +_ = rf"a" rf"b" diff --git a/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs b/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs index 43dac7b3e5..63e107e3bb 100644 --- a/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs +++ b/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs @@ -3,9 +3,10 @@ use ruff_text_size::TextRange; use rustpython_parser::lexer::LexResult; use rustpython_parser::Tok; -use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::Locator; +use ruff_python_ast::str::{leading_quote, trailing_quote}; use crate::rules::flake8_implicit_str_concat::settings::Settings; @@ -34,10 +35,16 @@ use crate::rules::flake8_implicit_str_concat::settings::Settings; pub struct SingleLineImplicitStringConcatenation; impl Violation for SingleLineImplicitStringConcatenation { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { format!("Implicitly concatenated string literals on one line") } + + fn autofix_title(&self) -> Option { + Some("Combine string literals".to_string()) + } } /// ## What it does @@ -106,12 +113,50 @@ pub(crate) fn implicit( TextRange::new(a_range.start(), b_range.end()), )); } else { - diagnostics.push(Diagnostic::new( + let mut diagnostic = Diagnostic::new( SingleLineImplicitStringConcatenation, TextRange::new(a_range.start(), b_range.end()), - )); - } - } + ); + + if let Some(fix) = concatenate_strings(*a_range, *b_range, locator) { + diagnostic.set_fix(fix); + } + + diagnostics.push(diagnostic); + }; + }; } diagnostics } + +fn concatenate_strings(a_range: TextRange, b_range: TextRange, locator: &Locator) -> Option { + let a_text = &locator.contents()[a_range]; + let b_text = &locator.contents()[b_range]; + + let a_leading_quote = leading_quote(a_text)?; + let b_leading_quote = leading_quote(b_text)?; + + // Require, for now, that the leading quotes are the same. + if a_leading_quote != b_leading_quote { + return None; + } + + let a_trailing_quote = trailing_quote(a_text)?; + let b_trailing_quote = trailing_quote(b_text)?; + + // Require, for now, that the trailing quotes are the same. + if a_trailing_quote != b_trailing_quote { + return None; + } + + let a_body = &a_text[a_leading_quote.len()..a_text.len() - a_trailing_quote.len()]; + let b_body = &b_text[b_leading_quote.len()..b_text.len() - b_trailing_quote.len()]; + + let concatenation = format!("{a_leading_quote}{a_body}{b_body}{a_trailing_quote}"); + let range = TextRange::new(a_range.start(), b_range.end()); + + Some(Fix::automatic(Edit::range_replacement( + concatenation, + range, + ))) +} diff --git a/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__ISC001_ISC.py.snap b/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__ISC001_ISC.py.snap index 6770789051..afeafc7660 100644 --- a/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__ISC001_ISC.py.snap +++ b/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__ISC001_ISC.py.snap @@ -1,20 +1,151 @@ --- source: crates/ruff/src/rules/flake8_implicit_str_concat/mod.rs --- -ISC.py:1:5: ISC001 Implicitly concatenated string literals on one line +ISC.py:1:5: ISC001 [*] Implicitly concatenated string literals on one line | 1 | _ = "a" "b" "c" | ^^^^^^^ ISC001 2 | 3 | _ = "abc" + "def" | + = help: Combine string literals -ISC.py:1:9: ISC001 Implicitly concatenated string literals on one line +ℹ Fix +1 |-_ = "a" "b" "c" + 1 |+_ = "ab" "c" +2 2 | +3 3 | _ = "abc" + "def" +4 4 | + +ISC.py:1:9: ISC001 [*] Implicitly concatenated string literals on one line | 1 | _ = "a" "b" "c" | ^^^^^^^ ISC001 2 | 3 | _ = "abc" + "def" | + = help: Combine string literals + +ℹ Fix +1 |-_ = "a" "b" "c" + 1 |+_ = "a" "bc" +2 2 | +3 3 | _ = "abc" + "def" +4 4 | + +ISC.py:38:5: ISC001 [*] Implicitly concatenated string literals on one line + | +36 | ) +37 | +38 | _ = """a""" """b""" + | ^^^^^^^^^^^^^^^ ISC001 +39 | +40 | _ = """a + | + = help: Combine string literals + +ℹ Fix +35 35 | b"def" +36 36 | ) +37 37 | +38 |-_ = """a""" """b""" + 38 |+_ = """ab""" +39 39 | +40 40 | _ = """a +41 41 | b""" """c + +ISC.py:40:5: ISC001 [*] Implicitly concatenated string literals on one line + | +38 | _ = """a""" """b""" +39 | +40 | _ = """a + | _____^ +41 | | b""" """c +42 | | d""" + | |____^ ISC001 +43 | +44 | _ = f"""a""" f"""b""" + | + = help: Combine string literals + +ℹ Fix +38 38 | _ = """a""" """b""" +39 39 | +40 40 | _ = """a +41 |-b""" """c + 41 |+bc +42 42 | d""" +43 43 | +44 44 | _ = f"""a""" f"""b""" + +ISC.py:44:5: ISC001 [*] Implicitly concatenated string literals on one line + | +42 | d""" +43 | +44 | _ = f"""a""" f"""b""" + | ^^^^^^^^^^^^^^^^^ ISC001 +45 | +46 | _ = f"a" "b" + | + = help: Combine string literals + +ℹ Fix +41 41 | b""" """c +42 42 | d""" +43 43 | +44 |-_ = f"""a""" f"""b""" + 44 |+_ = f"""ab""" +45 45 | +46 46 | _ = f"a" "b" +47 47 | + +ISC.py:46:5: ISC001 Implicitly concatenated string literals on one line + | +44 | _ = f"""a""" f"""b""" +45 | +46 | _ = f"a" "b" + | ^^^^^^^^ ISC001 +47 | +48 | _ = """a""" "b" + | + = help: Combine string literals + +ISC.py:48:5: ISC001 Implicitly concatenated string literals on one line + | +46 | _ = f"a" "b" +47 | +48 | _ = """a""" "b" + | ^^^^^^^^^^^ ISC001 +49 | +50 | _ = 'a' "b" + | + = help: Combine string literals + +ISC.py:50:5: ISC001 Implicitly concatenated string literals on one line + | +48 | _ = """a""" "b" +49 | +50 | _ = 'a' "b" + | ^^^^^^^ ISC001 +51 | +52 | _ = rf"a" rf"b" + | + = help: Combine string literals + +ISC.py:52:5: ISC001 [*] Implicitly concatenated string literals on one line + | +50 | _ = 'a' "b" +51 | +52 | _ = rf"a" rf"b" + | ^^^^^^^^^^^ ISC001 + | + = help: Combine string literals + +ℹ Fix +49 49 | +50 50 | _ = 'a' "b" +51 51 | +52 |-_ = rf"a" rf"b" + 52 |+_ = rf"ab" diff --git a/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__multiline_ISC001_ISC.py.snap b/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__multiline_ISC001_ISC.py.snap index 6770789051..afeafc7660 100644 --- a/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__multiline_ISC001_ISC.py.snap +++ b/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__multiline_ISC001_ISC.py.snap @@ -1,20 +1,151 @@ --- source: crates/ruff/src/rules/flake8_implicit_str_concat/mod.rs --- -ISC.py:1:5: ISC001 Implicitly concatenated string literals on one line +ISC.py:1:5: ISC001 [*] Implicitly concatenated string literals on one line | 1 | _ = "a" "b" "c" | ^^^^^^^ ISC001 2 | 3 | _ = "abc" + "def" | + = help: Combine string literals -ISC.py:1:9: ISC001 Implicitly concatenated string literals on one line +ℹ Fix +1 |-_ = "a" "b" "c" + 1 |+_ = "ab" "c" +2 2 | +3 3 | _ = "abc" + "def" +4 4 | + +ISC.py:1:9: ISC001 [*] Implicitly concatenated string literals on one line | 1 | _ = "a" "b" "c" | ^^^^^^^ ISC001 2 | 3 | _ = "abc" + "def" | + = help: Combine string literals + +ℹ Fix +1 |-_ = "a" "b" "c" + 1 |+_ = "a" "bc" +2 2 | +3 3 | _ = "abc" + "def" +4 4 | + +ISC.py:38:5: ISC001 [*] Implicitly concatenated string literals on one line + | +36 | ) +37 | +38 | _ = """a""" """b""" + | ^^^^^^^^^^^^^^^ ISC001 +39 | +40 | _ = """a + | + = help: Combine string literals + +ℹ Fix +35 35 | b"def" +36 36 | ) +37 37 | +38 |-_ = """a""" """b""" + 38 |+_ = """ab""" +39 39 | +40 40 | _ = """a +41 41 | b""" """c + +ISC.py:40:5: ISC001 [*] Implicitly concatenated string literals on one line + | +38 | _ = """a""" """b""" +39 | +40 | _ = """a + | _____^ +41 | | b""" """c +42 | | d""" + | |____^ ISC001 +43 | +44 | _ = f"""a""" f"""b""" + | + = help: Combine string literals + +ℹ Fix +38 38 | _ = """a""" """b""" +39 39 | +40 40 | _ = """a +41 |-b""" """c + 41 |+bc +42 42 | d""" +43 43 | +44 44 | _ = f"""a""" f"""b""" + +ISC.py:44:5: ISC001 [*] Implicitly concatenated string literals on one line + | +42 | d""" +43 | +44 | _ = f"""a""" f"""b""" + | ^^^^^^^^^^^^^^^^^ ISC001 +45 | +46 | _ = f"a" "b" + | + = help: Combine string literals + +ℹ Fix +41 41 | b""" """c +42 42 | d""" +43 43 | +44 |-_ = f"""a""" f"""b""" + 44 |+_ = f"""ab""" +45 45 | +46 46 | _ = f"a" "b" +47 47 | + +ISC.py:46:5: ISC001 Implicitly concatenated string literals on one line + | +44 | _ = f"""a""" f"""b""" +45 | +46 | _ = f"a" "b" + | ^^^^^^^^ ISC001 +47 | +48 | _ = """a""" "b" + | + = help: Combine string literals + +ISC.py:48:5: ISC001 Implicitly concatenated string literals on one line + | +46 | _ = f"a" "b" +47 | +48 | _ = """a""" "b" + | ^^^^^^^^^^^ ISC001 +49 | +50 | _ = 'a' "b" + | + = help: Combine string literals + +ISC.py:50:5: ISC001 Implicitly concatenated string literals on one line + | +48 | _ = """a""" "b" +49 | +50 | _ = 'a' "b" + | ^^^^^^^ ISC001 +51 | +52 | _ = rf"a" rf"b" + | + = help: Combine string literals + +ISC.py:52:5: ISC001 [*] Implicitly concatenated string literals on one line + | +50 | _ = 'a' "b" +51 | +52 | _ = rf"a" rf"b" + | ^^^^^^^^^^^ ISC001 + | + = help: Combine string literals + +ℹ Fix +49 49 | +50 50 | _ = 'a' "b" +51 51 | +52 |-_ = rf"a" rf"b" + 52 |+_ = rf"ab" From cbd4c10fddd33d3afb735d7aed1b77d8ece2ed88 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 12 Jun 2023 20:54:07 -0400 Subject: [PATCH 025/447] Support 'reason' argument to `pytest.fail` (#5040) ## Summary Per the [API reference](https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest.fail), `reason` was added in version 7, and is equivalent to `msg` (but preferred going forward). I also grepped for `msg` usages in `flake8_pytest_style`, but found no others (apart from those that reference `unittest` APIs.) Closes #3387. --- .../fixtures/flake8_pytest_style/PT016.py | 18 +++-- .../rules/flake8_pytest_style/rules/fail.rs | 7 +- .../rules/unittest_assert.rs | 20 +++-- ...es__flake8_pytest_style__tests__PT016.snap | 73 ++++++++++++------- 4 files changed, 75 insertions(+), 43 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT016.py b/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT016.py index 4f06451450..288d5d9be6 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT016.py +++ b/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT016.py @@ -1,17 +1,25 @@ import pytest -def test_xxx(): - pytest.fail("this is a failure") # Test OK arg +# OK +def f(): + pytest.fail("this is a failure") -def test_xxx(): - pytest.fail(msg="this is a failure") # Test OK kwarg +def f(): + pytest.fail(msg="this is a failure") -def test_xxx(): # Error +def f(): + pytest.fail(reason="this is a failure") + + +# Errors +def f(): pytest.fail() pytest.fail("") pytest.fail(f"") pytest.fail(msg="") pytest.fail(msg=f"") + pytest.fail(reason="") + pytest.fail(reason=f"") diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/fail.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/fail.rs index 6faa0915f4..5f11d6f2f6 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/fail.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/fail.rs @@ -21,7 +21,12 @@ impl Violation for PytestFailWithoutMessage { pub(crate) fn fail_call(checker: &mut Checker, func: &Expr, args: &[Expr], keywords: &[Keyword]) { if is_pytest_fail(checker.semantic_model(), func) { let call_args = SimpleCallArgs::new(args, keywords); - let msg = call_args.argument("msg", 0); + + // Allow either `pytest.fail(reason="...")` (introduced in pytest 7.0) or + // `pytest.fail(msg="...")` (deprecated in pytest 7.0) + let msg = call_args + .argument("reason", 0) + .or_else(|| call_args.argument("msg", 0)); if let Some(msg) = msg { if is_empty_or_null_string(msg) { diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/unittest_assert.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/unittest_assert.rs index 81b07d166a..8c70114e67 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/unittest_assert.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/unittest_assert.rs @@ -206,9 +206,7 @@ impl UnittestAssert { keywords: &'a [Keyword], ) -> Result> { // If we have variable-length arguments, abort. - if args.iter().any(|arg| matches!(arg, Expr::Starred(_))) - || keywords.iter().any(|kw| kw.arg.is_none()) - { + if args.iter().any(Expr::is_starred_expr) || keywords.iter().any(|kw| kw.arg.is_none()) { bail!("Variable-length arguments are not supported"); } @@ -263,14 +261,14 @@ impl UnittestAssert { .ok_or_else(|| anyhow!("Missing argument `expr`"))?; let msg = args.get("msg").copied(); Ok(if matches!(self, UnittestAssert::False) { - let node = expr.clone(); - let node1 = Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::Not, - operand: Box::new(node), - range: TextRange::default(), - }); - let unary_expr = node1; - assert(&unary_expr, msg) + assert( + &Expr::UnaryOp(ast::ExprUnaryOp { + op: Unaryop::Not, + operand: Box::new(expr.clone()), + range: TextRange::default(), + }), + msg, + ) } else { assert(expr, msg) }) diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT016.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT016.snap index 5223b12eed..945f989567 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT016.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT016.snap @@ -1,49 +1,70 @@ --- source: crates/ruff/src/rules/flake8_pytest_style/mod.rs --- -PT016.py:13:5: PT016 No message passed to `pytest.fail()` +PT016.py:19:5: PT016 No message passed to `pytest.fail()` | -12 | def test_xxx(): # Error -13 | pytest.fail() +17 | # Errors +18 | def f(): +19 | pytest.fail() | ^^^^^^^^^^^ PT016 -14 | pytest.fail("") -15 | pytest.fail(f"") +20 | pytest.fail("") +21 | pytest.fail(f"") | -PT016.py:14:5: PT016 No message passed to `pytest.fail()` +PT016.py:20:5: PT016 No message passed to `pytest.fail()` | -12 | def test_xxx(): # Error -13 | pytest.fail() -14 | pytest.fail("") +18 | def f(): +19 | pytest.fail() +20 | pytest.fail("") | ^^^^^^^^^^^ PT016 -15 | pytest.fail(f"") -16 | pytest.fail(msg="") +21 | pytest.fail(f"") +22 | pytest.fail(msg="") | -PT016.py:15:5: PT016 No message passed to `pytest.fail()` +PT016.py:21:5: PT016 No message passed to `pytest.fail()` | -13 | pytest.fail() -14 | pytest.fail("") -15 | pytest.fail(f"") +19 | pytest.fail() +20 | pytest.fail("") +21 | pytest.fail(f"") | ^^^^^^^^^^^ PT016 -16 | pytest.fail(msg="") -17 | pytest.fail(msg=f"") +22 | pytest.fail(msg="") +23 | pytest.fail(msg=f"") | -PT016.py:16:5: PT016 No message passed to `pytest.fail()` +PT016.py:22:5: PT016 No message passed to `pytest.fail()` | -14 | pytest.fail("") -15 | pytest.fail(f"") -16 | pytest.fail(msg="") +20 | pytest.fail("") +21 | pytest.fail(f"") +22 | pytest.fail(msg="") | ^^^^^^^^^^^ PT016 -17 | pytest.fail(msg=f"") +23 | pytest.fail(msg=f"") +24 | pytest.fail(reason="") | -PT016.py:17:5: PT016 No message passed to `pytest.fail()` +PT016.py:23:5: PT016 No message passed to `pytest.fail()` | -15 | pytest.fail(f"") -16 | pytest.fail(msg="") -17 | pytest.fail(msg=f"") +21 | pytest.fail(f"") +22 | pytest.fail(msg="") +23 | pytest.fail(msg=f"") + | ^^^^^^^^^^^ PT016 +24 | pytest.fail(reason="") +25 | pytest.fail(reason=f"") + | + +PT016.py:24:5: PT016 No message passed to `pytest.fail()` + | +22 | pytest.fail(msg="") +23 | pytest.fail(msg=f"") +24 | pytest.fail(reason="") + | ^^^^^^^^^^^ PT016 +25 | pytest.fail(reason=f"") + | + +PT016.py:25:5: PT016 No message passed to `pytest.fail()` + | +23 | pytest.fail(msg=f"") +24 | pytest.fail(reason="") +25 | pytest.fail(reason=f"") | ^^^^^^^^^^^ PT016 | From be2fa6d2172a3b19c93d645f65858820b5e43cbd Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 12 Jun 2023 21:08:23 -0400 Subject: [PATCH 026/447] Increase density of `Checker` arms (#5041) --- crates/ruff/src/checkers/ast/mod.rs | 181 +++------------------------- 1 file changed, 16 insertions(+), 165 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 80a5fb86e3..dd778171c6 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -268,7 +268,6 @@ where scope.add(name, binding_id); } } - if self.enabled(Rule::AmbiguousVariableName) { self.diagnostics .extend(names.iter().zip(ranges.iter()).filter_map(|(name, range)| { @@ -329,7 +328,6 @@ where } } } - if self.enabled(Rule::AmbiguousVariableName) { self.diagnostics .extend(names.iter().zip(ranges.iter()).filter_map(|(name, range)| { @@ -390,11 +388,9 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::InvalidStrReturnType) { pylint::rules::invalid_str_return(self, name, body); } - if self.enabled(Rule::InvalidFunctionName) { if let Some(diagnostic) = pep8_naming::rules::invalid_function_name( stmt, @@ -407,7 +403,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::InvalidFirstArgumentNameForClassMethod) { if let Some(diagnostic) = pep8_naming::rules::invalid_first_argument_name_for_class_method( @@ -421,7 +416,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::InvalidFirstArgumentNameForMethod) { if let Some(diagnostic) = pep8_naming::rules::invalid_first_argument_name_for_method( @@ -467,7 +461,6 @@ where flake8_pyi::rules::no_return_argument_annotation(self, args); } } - if self.enabled(Rule::DunderFunctionName) { if let Some(diagnostic) = pep8_naming::rules::dunder_function_name( self.semantic_model.scope(), @@ -478,11 +471,9 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::GlobalStatement) { pylint::rules::global_statement(self, name); } - if self.enabled(Rule::LRUCacheWithoutParameters) && self.settings.target_version >= PythonVersion::Py38 { @@ -493,11 +484,9 @@ where { pyupgrade::rules::lru_cache_with_maxsize_none(self, decorator_list); } - if self.enabled(Rule::CachedInstanceMethod) { flake8_bugbear::rules::cached_instance_method(self, decorator_list); } - if self.any_enabled(&[ Rule::UnnecessaryReturnNone, Rule::ImplicitReturnValue, @@ -514,7 +503,6 @@ where returns.as_ref().map(|expr| &**expr), ); } - if self.enabled(Rule::UselessReturn) { pylint::rules::useless_return( self, @@ -523,7 +511,6 @@ where returns.as_ref().map(|expr| &**expr), ); } - if self.enabled(Rule::ComplexStructure) { if let Some(diagnostic) = mccabe::rules::function_is_too_complex( stmt, @@ -535,20 +522,16 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::HardcodedPasswordDefault) { self.diagnostics .extend(flake8_bandit::rules::hardcoded_password_default(args)); } - if self.enabled(Rule::PropertyWithParameters) { pylint::rules::property_with_parameters(self, stmt, decorator_list, args); } - if self.enabled(Rule::TooManyArguments) { pylint::rules::too_many_arguments(self, args, stmt); } - if self.enabled(Rule::TooManyReturnStatements) { if let Some(diagnostic) = pylint::rules::too_many_return_statements( stmt, @@ -559,7 +542,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::TooManyBranches) { if let Some(diagnostic) = pylint::rules::too_many_branches( stmt, @@ -570,7 +552,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::TooManyStatements) { if let Some(diagnostic) = pylint::rules::too_many_statements( stmt, @@ -581,7 +562,6 @@ where self.diagnostics.push(diagnostic); } } - if self.any_enabled(&[ Rule::PytestFixtureIncorrectParenthesesStyle, Rule::PytestFixturePositionalArgs, @@ -604,21 +584,18 @@ where body, ); } - if self.any_enabled(&[ Rule::PytestParametrizeNamesWrongType, Rule::PytestParametrizeValuesWrongType, ]) { flake8_pytest_style::rules::parametrize(self, decorator_list); } - if self.any_enabled(&[ Rule::PytestIncorrectMarkParenthesesStyle, Rule::PytestUseFixturesWithoutParameters, ]) { flake8_pytest_style::rules::marks(self, decorator_list); } - if self.enabled(Rule::BooleanPositionalArgInFunctionDefinition) { flake8_boolean_trap::rules::check_positional_boolean_in_def( self, @@ -627,7 +604,6 @@ where args, ); } - if self.enabled(Rule::BooleanDefaultValueInFunctionDefinition) { flake8_boolean_trap::rules::check_boolean_default_value_in_function_definition( self, @@ -636,7 +612,6 @@ where args, ); } - if self.enabled(Rule::UnexpectedSpecialMethodSignature) { pylint::rules::unexpected_special_method_signature( self, @@ -647,15 +622,12 @@ where self.locator, ); } - if self.enabled(Rule::FStringDocstring) { flake8_bugbear::rules::f_string_docstring(self, body); } - if self.enabled(Rule::YieldInForLoop) { pyupgrade::rules::yield_in_for_loop(self, stmt); } - if let ScopeKind::Class(class_def) = self.semantic_model.scope().kind { if self.enabled(Rule::BuiltinAttributeShadowing) { flake8_builtins::rules::builtin_attribute_shadowing( @@ -699,7 +671,6 @@ where self, body, )); } - if self.enabled(Rule::DjangoExcludeWithModelForm) { if let Some(diagnostic) = flake8_django::rules::exclude_with_model_form(self, bases, body) @@ -733,7 +704,6 @@ where if self.enabled(Rule::UnnecessaryClassParentheses) { pyupgrade::rules::unnecessary_class_parentheses(self, class_def, stmt); } - if self.enabled(Rule::AmbiguousClassName) { if let Some(diagnostic) = pycodestyle::rules::ambiguous_class_name(name, || { helpers::identifier_range(stmt, self.locator) @@ -741,7 +711,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::InvalidClassName) { if let Some(diagnostic) = pep8_naming::rules::invalid_class_name(stmt, name, self.locator) @@ -749,7 +718,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::ErrorSuffixOnExceptionName) { if let Some(diagnostic) = pep8_naming::rules::error_suffix_on_exception_name( stmt, @@ -760,7 +728,6 @@ where self.diagnostics.push(diagnostic); } } - if !self.is_stub { if self.any_enabled(&[ Rule::AbstractBaseClassWithoutAbstractMethod, @@ -782,35 +749,27 @@ where flake8_pyi::rules::ellipsis_in_non_empty_class_body(self, stmt, body); } } - if self.enabled(Rule::PytestIncorrectMarkParenthesesStyle) { flake8_pytest_style::rules::marks(self, decorator_list); } - if self.enabled(Rule::DuplicateClassFieldDefinition) { flake8_pie::rules::duplicate_class_field_definition(self, stmt, body); } - if self.enabled(Rule::NonUniqueEnums) { flake8_pie::rules::non_unique_enums(self, stmt, body); } - if self.enabled(Rule::MutableClassDefault) { ruff::rules::mutable_class_default(self, class_def); } - if self.enabled(Rule::MutableDataclassDefault) { ruff::rules::mutable_dataclass_default(self, class_def); } - if self.enabled(Rule::FunctionCallInDataclassDefaultArgument) { ruff::rules::function_call_in_dataclass_default(self, class_def); } - if self.enabled(Rule::FStringDocstring) { flake8_bugbear::rules::f_string_docstring(self, body); } - if self.enabled(Rule::BuiltinVariableShadowing) { flake8_builtins::rules::builtin_variable_shadowing( self, @@ -818,19 +777,15 @@ where AnyShadowing::from(stmt), ); } - if self.enabled(Rule::DuplicateBases) { pylint::rules::duplicate_bases(self, name, bases); } - if self.enabled(Rule::NoSlotsInStrSubclass) { flake8_slots::rules::no_slots_in_str_subclass(self, stmt, class_def); } - if self.enabled(Rule::NoSlotsInTupleSubclass) { flake8_slots::rules::no_slots_in_tuple_subclass(self, stmt, class_def); } - if self.enabled(Rule::NoSlotsInNamedtupleSubclass) { flake8_slots::rules::no_slots_in_namedtuple_subclass(self, stmt, class_def); } @@ -842,7 +797,6 @@ where if self.enabled(Rule::ModuleImportNotAtTopOfFile) { pycodestyle::rules::module_import_not_at_top_of_file(self, stmt, self.locator); } - if self.enabled(Rule::GlobalStatement) { for name in names.iter() { if let Some(asname) = name.asname.as_ref() { @@ -852,7 +806,6 @@ where } } } - if self.enabled(Rule::DeprecatedCElementTree) { pyupgrade::rules::deprecated_c_element_tree(self, stmt); } @@ -919,8 +872,6 @@ where } } } - - // flake8-debugger if self.enabled(Rule::Debugger) { if let Some(diagnostic) = flake8_debugger::rules::debugger_import(stmt, None, &alias.name) @@ -928,8 +879,6 @@ where self.diagnostics.push(diagnostic); } } - - // flake8_tidy_imports if self.enabled(Rule::BannedApi) { flake8_tidy_imports::rules::name_or_parent_is_banned( self, @@ -937,8 +886,6 @@ where alias, ); } - - // pylint if !self.is_stub { if self.enabled(Rule::UselessImportAlias) { pylint::rules::useless_import_alias(self, alias); @@ -1007,7 +954,6 @@ where } } } - if self.enabled(Rule::UnconventionalImportAlias) { if let Some(diagnostic) = flake8_import_conventions::rules::conventional_import_alias( @@ -1020,7 +966,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::BannedImportAlias) { if let Some(asname) = &alias.asname { if let Some(diagnostic) = @@ -1035,7 +980,6 @@ where } } } - if self.enabled(Rule::PytestIncorrectPytestImport) { if let Some(diagnostic) = flake8_pytest_style::rules::import( stmt, @@ -1060,7 +1004,6 @@ where if self.enabled(Rule::ModuleImportNotAtTopOfFile) { pycodestyle::rules::module_import_not_at_top_of_file(self, stmt, self.locator); } - if self.enabled(Rule::GlobalStatement) { for name in names.iter() { if let Some(asname) = name.asname.as_ref() { @@ -1070,7 +1013,6 @@ where } } } - if self.enabled(Rule::UnnecessaryFutureImport) && self.settings.target_version >= PythonVersion::Py37 { @@ -1110,7 +1052,6 @@ where } } } - if self.enabled(Rule::PytestIncorrectPytestImport) { if let Some(diagnostic) = flake8_pytest_style::rules::import_from(stmt, module, level) @@ -1207,7 +1148,6 @@ where }, ); } - if self.enabled(Rule::RelativeImports) { if let Some(diagnostic) = flake8_tidy_imports::rules::banned_relative_import( self, @@ -1220,8 +1160,6 @@ where self.diagnostics.push(diagnostic); } } - - // flake8-debugger if self.enabled(Rule::Debugger) { if let Some(diagnostic) = flake8_debugger::rules::debugger_import(stmt, module, &alias.name) @@ -1229,7 +1167,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::UnconventionalImportAlias) { let qualified_name = helpers::format_import_from_member(level, module, &alias.name); @@ -1244,7 +1181,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::BannedImportAlias) { if let Some(asname) = &alias.asname { let qualified_name = @@ -1336,7 +1272,6 @@ where } } } - if self.enabled(Rule::ImportSelf) { if let Some(diagnostic) = pylint::rules::import_from_self(level, module, names, self.module_path) @@ -1344,7 +1279,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::BannedImportFrom) { if let Some(diagnostic) = flake8_import_conventions::rules::banned_import_from( stmt, @@ -2023,7 +1957,6 @@ where if self.enabled(Rule::JumpStatementInFinally) { flake8_bugbear::rules::jump_statement_in_finally(self, finalbody); } - if self.enabled(Rule::ContinueInFinally) { if self.settings.target_version <= PythonVersion::Py38 { pylint::rules::continue_in_finally(self, finalbody); @@ -2126,7 +2059,6 @@ where if self.semantic_model.at_top_level() { self.importer.visit_type_checking_block(stmt); } - if self.enabled(Rule::EmptyTypeCheckingBlock) { flake8_type_checking::rules::empty_type_checking_block(self, stmt_if); } @@ -2268,7 +2200,6 @@ where if self.semantic_model.match_typing_expr(value, "Literal") { self.semantic_model.flags |= SemanticModelFlags::LITERAL; } - if self.any_enabled(&[ Rule::SysVersionSlice3, Rule::SysVersion2, @@ -2277,7 +2208,6 @@ where ]) { flake8_2020::rules::subscript(self, value, slice); } - if self.enabled(Rule::UncapitalizedEnvironmentVariables) { flake8_simplify::rules::use_capital_environment_variables(self, expr); } @@ -2393,11 +2323,9 @@ where } ExprContext::Del => self.handle_node_delete(expr), } - if self.enabled(Rule::SixPY3) { flake8_2020::rules::name_or_attribute(self, expr); } - if self.enabled(Rule::LoadBeforeGlobalDeclaration) { pylint::rules::load_before_global_declaration(self, id, expr); } @@ -2469,6 +2397,13 @@ where keywords, range: _, }) => { + if let Expr::Name(ast::ExprName { id, ctx, range: _ }) = func.as_ref() { + if id == "locals" && matches!(ctx, ExprContext::Load) { + let scope = self.semantic_model.scope_mut(); + scope.set_uses_locals(); + } + } + if self.any_enabled(&[ // pyflakes Rule::StringDotFormatInvalidFormat, @@ -2550,7 +2485,6 @@ where } } - // pyupgrade if self.enabled(Rule::TypeOfPrimitive) { pyupgrade::rules::type_of_primitive(self, expr, func, args); } @@ -2586,8 +2520,6 @@ where { pyupgrade::rules::use_pep604_isinstance(self, expr, func, args); } - - // flake8-async if self.enabled(Rule::BlockingHttpCallInAsyncFunction) { flake8_async::rules::blocking_http_call(self, expr); } @@ -2597,13 +2529,9 @@ where if self.enabled(Rule::BlockingOsCallInAsyncFunction) { flake8_async::rules::blocking_os_call(self, expr); } - - // flake8-print if self.any_enabled(&[Rule::Print, Rule::PPrint]) { flake8_print::rules::print_call(self, func, keywords); } - - // flake8-bandit if self.any_enabled(&[ Rule::SuspiciousPickleUsage, Rule::SuspiciousMarshalUsage, @@ -2629,8 +2557,6 @@ where ]) { flake8_bandit::rules::suspicious_function_call(self, expr); } - - // flake8-bugbear if self.enabled(Rule::UnreliableCallableCheck) { flake8_bugbear::rules::unreliable_callable_check(self, expr, func, args); } @@ -2651,23 +2577,19 @@ where self, args, keywords, ); } - if self.enabled(Rule::ZipWithoutExplicitStrict) - && self.settings.target_version >= PythonVersion::Py310 - { - flake8_bugbear::rules::zip_without_explicit_strict( - self, expr, func, args, keywords, - ); + if self.enabled(Rule::ZipWithoutExplicitStrict) { + if self.settings.target_version >= PythonVersion::Py310 { + flake8_bugbear::rules::zip_without_explicit_strict( + self, expr, func, args, keywords, + ); + } } if self.enabled(Rule::NoExplicitStacklevel) { flake8_bugbear::rules::no_explicit_stacklevel(self, func, args, keywords); } - - // flake8-pie if self.enabled(Rule::UnnecessaryDictKwargs) { flake8_pie::rules::unnecessary_dict_kwargs(self, expr, keywords); } - - // flake8-bandit if self.enabled(Rule::ExecBuiltin) { if let Some(diagnostic) = flake8_bandit::rules::exec_used(expr, func) { self.diagnostics.push(diagnostic); @@ -2727,8 +2649,6 @@ where ]) { flake8_bandit::rules::shell_injection(self, func, args, keywords); } - - // flake8-comprehensions if self.enabled(Rule::UnnecessaryGeneratorList) { flake8_comprehensions::rules::unnecessary_generator_list( self, expr, func, args, keywords, @@ -2821,26 +2741,14 @@ where self, expr, func, args, keywords, ); } - - // flake8-boolean-trap if self.enabled(Rule::BooleanPositionalValueInFunctionCall) { flake8_boolean_trap::rules::check_boolean_positional_value_in_function_call( self, args, func, ); } - if let Expr::Name(ast::ExprName { id, ctx, range: _ }) = func.as_ref() { - if id == "locals" && matches!(ctx, ExprContext::Load) { - let scope = self.semantic_model.scope_mut(); - scope.set_uses_locals(); - } - } - - // flake8-debugger if self.enabled(Rule::Debugger) { flake8_debugger::rules::debugger_call(self, expr, func); } - - // pandas-vet if self.enabled(Rule::PandasUseOfInplaceArgument) { self.diagnostics.extend( pandas_vet::rules::inplace_argument(self, expr, func, args, keywords) @@ -2854,8 +2762,6 @@ where self.diagnostics.push(diagnostic); }; } - - // flake8-datetimez if self.enabled(Rule::CallDatetimeWithoutTzinfo) { flake8_datetimez::rules::call_datetime_without_tzinfo( self, @@ -2910,16 +2816,12 @@ where if self.enabled(Rule::CallDateFromtimestamp) { flake8_datetimez::rules::call_date_fromtimestamp(self, func, expr.range()); } - - // pygrep-hooks if self.enabled(Rule::Eval) { pygrep_hooks::rules::no_eval(self, func); } if self.enabled(Rule::DeprecatedLogWarn) { pygrep_hooks::rules::deprecated_log_warn(self, func); } - - // pylint if self.enabled(Rule::UnnecessaryDirectLambdaCall) { pylint::rules::unnecessary_direct_lambda_call(self, expr, func); } @@ -2938,8 +2840,6 @@ where if self.enabled(Rule::NestedMinMax) { pylint::rules::nested_min_max(self, expr, func, args, keywords); } - - // flake8-pytest-style if self.enabled(Rule::PytestPatchWithLambda) { if let Some(diagnostic) = flake8_pytest_style::rules::patch_with_lambda(func, args, keywords) @@ -2954,25 +2854,20 @@ where self.diagnostics.push(diagnostic); } } - if self.any_enabled(&[ Rule::PytestRaisesWithoutException, Rule::PytestRaisesTooBroad, ]) { flake8_pytest_style::rules::raises_call(self, func, args, keywords); } - if self.enabled(Rule::PytestFailWithoutMessage) { flake8_pytest_style::rules::fail_call(self, func, args, keywords); } - if self.enabled(Rule::PairwiseOverZipped) { if self.settings.target_version >= PythonVersion::Py310 { ruff::rules::pairwise_over_zipped(self, func, args); } } - - // flake8-gettext if self.any_enabled(&[ Rule::FStringInGetTextFuncCall, Rule::FormatInGetTextFuncCall, @@ -2994,21 +2889,15 @@ where .extend(flake8_gettext::rules::printf_in_gettext_func_call(args)); } } - - // flake8-simplify if self.enabled(Rule::UncapitalizedEnvironmentVariables) { flake8_simplify::rules::use_capital_environment_variables(self, expr); } - if self.enabled(Rule::OpenFileWithContextHandler) { flake8_simplify::rules::open_file_with_context_handler(self, func); } - if self.enabled(Rule::DictGetWithNoneDefault) { flake8_simplify::rules::dict_get_with_none_default(self, expr); } - - // flake8-use-pathlib if self.any_enabled(&[ Rule::OsPathAbspath, Rule::OsChmod, @@ -3037,13 +2926,9 @@ where ]) { flake8_use_pathlib::rules::replaceable_by_pathlib(self, func); } - - // numpy if self.enabled(Rule::NumpyLegacyRandom) { numpy::rules::numpy_legacy_random(self, func); } - - // flake8-logging-format if self.any_enabled(&[ Rule::LoggingStringFormat, Rule::LoggingPercentFormat, @@ -3056,13 +2941,9 @@ where ]) { flake8_logging_format::rules::logging_call(self, func, args, keywords); } - - // pylint logging checker if self.any_enabled(&[Rule::LoggingTooFewArgs, Rule::LoggingTooManyArgs]) { pylint::rules::logging_call(self, func, args, keywords); } - - // flake8-django if self.enabled(Rule::DjangoLocalsInRenderFunction) { flake8_django::rules::locals_in_render_function(self, func, args, keywords); } @@ -3078,7 +2959,6 @@ where ]) { pyflakes::rules::repeated_keys(self, keys, values); } - if self.enabled(Rule::UnnecessarySpread) { flake8_pie::rules::unnecessary_spread(self, keys, values); } @@ -3221,7 +3101,6 @@ where } } } - if self.enabled(Rule::PrintfStringFormatting) { pyupgrade::rules::printf_string_formatting(self, expr, right, self.locator); } @@ -3300,11 +3179,9 @@ where check_not_is, ); } - if self.enabled(Rule::UnaryPrefixIncrement) { flake8_bugbear::rules::unary_prefix_increment(self, expr, *op, operand); } - if self.enabled(Rule::NegateEqualOp) { flake8_simplify::rules::negation_with_equal_op(self, expr, *op, operand); } @@ -3334,15 +3211,12 @@ where check_true_false_comparisons, ); } - if self.enabled(Rule::IsLiteral) { pyflakes::rules::invalid_literal_comparison(self, left, ops, comparators, expr); } - if self.enabled(Rule::TypeComparison) { pycodestyle::rules::type_comparison(self, expr, ops, comparators); } - if self.any_enabled(&[ Rule::SysVersionCmpStr3, Rule::SysVersionInfo0Eq3, @@ -3352,7 +3226,6 @@ 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( @@ -3361,31 +3234,24 @@ where ), ); } - if self.enabled(Rule::ComparisonWithItself) { pylint::rules::comparison_with_itself(self, left, ops, comparators); } - if self.enabled(Rule::ComparisonOfConstant) { pylint::rules::comparison_of_constant(self, left, ops, comparators); } - if self.enabled(Rule::CompareToEmptyString) { pylint::rules::compare_to_empty_string(self, left, ops, comparators); } - if self.enabled(Rule::MagicValueComparison) { pylint::rules::magic_value_comparison(self, left, comparators); } - if self.enabled(Rule::InDictKeys) { flake8_simplify::rules::key_in_dict_compare(self, expr, left, ops, comparators); } - 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, @@ -3399,7 +3265,6 @@ where comparators, ); } - if self.enabled(Rule::BadVersionInfoComparison) { flake8_pyi::rules::bad_version_info_comparison( self, @@ -4038,7 +3903,6 @@ where if self.enabled(Rule::ReraiseNoCause) { tryceratops::rules::reraise_no_cause(self, body); } - if self.enabled(Rule::BinaryOpException) { pylint::rules::binary_op_exception(self, excepthandler); } @@ -4141,22 +4005,18 @@ where if self.enabled(Rule::FunctionCallInDefaultArgument) { flake8_bugbear::rules::function_call_argument_default(self, arguments); } - + if self.settings.rules.enabled(Rule::ImplicitOptional) { + ruff::rules::implicit_optional(self, arguments); + } if self.is_stub { if self.enabled(Rule::TypedArgumentDefaultInStub) { flake8_pyi::rules::typed_argument_simple_defaults(self, arguments); } - } - if self.is_stub { if self.enabled(Rule::ArgumentDefaultInStub) { flake8_pyi::rules::argument_simple_defaults(self, arguments); } } - if self.settings.rules.enabled(Rule::ImplicitOptional) { - ruff::rules::implicit_optional(self, arguments); - } - // Bind, but intentionally avoid walking default expressions, as we handle them // upstream. for arg in &arguments.posonlyargs { @@ -4193,7 +4053,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::InvalidArgumentName) { if let Some(diagnostic) = pep8_naming::rules::invalid_argument_name( &arg.arg, @@ -4203,7 +4062,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::BuiltinArgumentShadowing) { flake8_builtins::rules::builtin_argument_shadowing(self, arg); } @@ -4573,7 +4431,6 @@ impl<'a> Checker<'a> { if self.enabled(Rule::UndefinedLocal) { pyflakes::rules::undefined_local(self, id); } - if self.enabled(Rule::NonLowercaseVariableInFunction) { if self.semantic_model.scope().kind.is_any_function() { // Ignore globals. @@ -4589,7 +4446,6 @@ impl<'a> Checker<'a> { } } } - if self.enabled(Rule::MixedCaseVariableInClassScope) { if let ScopeKind::Class(ast::StmtClassDef { bases, .. }) = &self.semantic_model.scope().kind @@ -4599,7 +4455,6 @@ impl<'a> Checker<'a> { ); } } - if self.enabled(Rule::MixedCaseVariableInGlobalScope) { if matches!(self.semantic_model.scope().kind, ScopeKind::Module) { pep8_naming::rules::mixed_case_variable_in_global_scope(self, expr, parent, id); @@ -4846,16 +4701,13 @@ impl<'a> Checker<'a> { for snapshot in assignments { self.semantic_model.restore(snapshot); - // pyflakes if self.enabled(Rule::UnusedVariable) { pyflakes::rules::unused_variable(self, self.semantic_model.scope_id); } if self.enabled(Rule::UnusedAnnotation) { pyflakes::rules::unused_annotation(self, self.semantic_model.scope_id); } - if !self.is_stub { - // flake8-unused-arguments if self.any_enabled(&[ Rule::UnusedFunctionArgument, Rule::UnusedMethodArgument, @@ -5285,7 +5137,6 @@ impl<'a> Checker<'a> { if !pydocstyle::rules::not_empty(self, &docstring) { continue; } - if self.enabled(Rule::FitsOnOneLine) { pydocstyle::rules::one_liner(self, &docstring); } From a477720f4ee5e6d6b52e710f78db132e9316a87d Mon Sep 17 00:00:00 2001 From: qdegraaf <34540841+qdegraaf@users.noreply.github.com> Date: Tue, 13 Jun 2023 03:54:44 +0200 Subject: [PATCH 027/447] [`perflint`] Add `perflint` plugin, add first rule `PERF102` (#4821) ## Summary Adds boilerplate for implementing the [perflint](https://github.com/tonybaloney/perflint/) plugin, plus a first rule. ## Test Plan Fixture added for PER8102 ## Issue link Refers: https://github.com/charliermarsh/ruff/issues/4789 --- LICENSE | 25 +++ .../test/fixtures/perflint/PERF102.py | 71 ++++++++ crates/ruff/src/checkers/ast/mod.rs | 6 +- crates/ruff/src/codes.rs | 3 + crates/ruff/src/registry.rs | 3 + crates/ruff/src/rules/mod.rs | 1 + crates/ruff/src/rules/perflint/mod.rs | 26 +++ .../perflint/rules/incorrect_dict_iterator.rs | 170 ++++++++++++++++++ crates/ruff/src/rules/perflint/rules/mod.rs | 3 + ...__perflint__tests__PERF102_PERF102.py.snap | 167 +++++++++++++++++ docs/faq.md | 2 + ruff.schema.json | 4 + 12 files changed, 480 insertions(+), 1 deletion(-) create mode 100644 crates/ruff/resources/test/fixtures/perflint/PERF102.py create mode 100644 crates/ruff/src/rules/perflint/mod.rs create mode 100644 crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs create mode 100644 crates/ruff/src/rules/perflint/rules/mod.rs create mode 100644 crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF102_PERF102.py.snap diff --git a/LICENSE b/LICENSE index 932ce42b6b..68a9d4958c 100644 --- a/LICENSE +++ b/LICENSE @@ -1199,6 +1199,31 @@ are: - flake8-django, licensed under the GPL license. +- perflint, licensed as follows: + """ + MIT License + + Copyright (c) 2022 Anthony Shaw + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + - rust-analyzer/text-size, licensed under the MIT license: """ Permission is hereby granted, free of charge, to any diff --git a/crates/ruff/resources/test/fixtures/perflint/PERF102.py b/crates/ruff/resources/test/fixtures/perflint/PERF102.py new file mode 100644 index 0000000000..8167138ca6 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/perflint/PERF102.py @@ -0,0 +1,71 @@ +some_dict = {"a": 12, "b": 32, "c": 44} + +for _, value in some_dict.items(): # PERF102 + print(value) + + +for key, _ in some_dict.items(): # PERF102 + print(key) + + +for weird_arg_name, _ in some_dict.items(): # PERF102 + print(weird_arg_name) + + +for name, (_, _) in some_dict.items(): # PERF102 + pass + + +for name, (value1, _) in some_dict.items(): # OK + pass + + +for (key1, _), (_, _) in some_dict.items(): # PERF102 + pass + + +for (_, (_, _)), (value, _) in some_dict.items(): # PERF102 + pass + + +for (_, key2), (value1, _) in some_dict.items(): # OK + pass + + +for ((_, key2), (value1, _)) in some_dict.items(): # OK + pass + + +for ((_, key2), (_, _)) in some_dict.items(): # PERF102 + pass + + +for (_, _, _, variants), (r_language, _, _, _) in some_dict.items(): # OK + pass + + +for (_, _, (_, variants)), (_, (_, (r_language, _))) in some_dict.items(): # OK + pass + + +for key, value in some_dict.items(): # OK + print(key, value) + + +for _, value in some_dict.items(12): # OK + print(value) + + +for key in some_dict.keys(): # OK + print(key) + + +for value in some_dict.values(): # OK + print(value) + + +for name, (_, _) in (some_function()).items(): # PERF102 + pass + +for name, (_, _) in (some_function().some_attribute).items(): # PERF102 + pass diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index dd778171c6..f269b58f6a 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -50,7 +50,8 @@ use crate::rules::{ flake8_print, flake8_pyi, flake8_pytest_style, flake8_raise, flake8_return, flake8_self, flake8_simplify, flake8_slots, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, flake8_use_pathlib, flynt, mccabe, numpy, pandas_vet, pep8_naming, - pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, tryceratops, + perflint, pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, + tryceratops, }; use crate::settings::types::PythonVersion; use crate::settings::{flags, Settings}; @@ -1525,6 +1526,9 @@ where flake8_simplify::rules::key_in_dict_for(self, target, iter); } } + if self.enabled(Rule::IncorrectDictIterator) { + perflint::rules::incorrect_dict_iterator(self, target, iter); + } } Stmt::Try(ast::StmtTry { body, diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 6085e64b5a..57985dd762 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -783,6 +783,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { // airflow (Airflow, "001") => (RuleGroup::Unspecified, rules::airflow::rules::AirflowVariableNameTaskIdMismatch), + // perflint + (Perflint, "102") => (RuleGroup::Unspecified, rules::perflint::rules::IncorrectDictIterator), + // flake8-fixme (Flake8Fixme, "001") => (RuleGroup::Unspecified, rules::flake8_fixme::rules::LineContainsFixme), (Flake8Fixme, "002") => (RuleGroup::Unspecified, rules::flake8_fixme::rules::LineContainsTodo), diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index c89df825dd..7e0fff6b37 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -191,6 +191,9 @@ pub enum Linter { /// [Airflow](https://pypi.org/project/apache-airflow/) #[prefix = "AIR"] Airflow, + /// [Perflint](https://pypi.org/project/perflint/) + #[prefix = "PERF"] + Perflint, /// Ruff-specific rules #[prefix = "RUF"] Ruff, diff --git a/crates/ruff/src/rules/mod.rs b/crates/ruff/src/rules/mod.rs index cc06dfb91d..4a12fb0319 100644 --- a/crates/ruff/src/rules/mod.rs +++ b/crates/ruff/src/rules/mod.rs @@ -45,6 +45,7 @@ pub mod mccabe; pub mod numpy; pub mod pandas_vet; pub mod pep8_naming; +pub mod perflint; pub mod pycodestyle; pub mod pydocstyle; pub mod pyflakes; diff --git a/crates/ruff/src/rules/perflint/mod.rs b/crates/ruff/src/rules/perflint/mod.rs new file mode 100644 index 0000000000..c0b0dd894d --- /dev/null +++ b/crates/ruff/src/rules/perflint/mod.rs @@ -0,0 +1,26 @@ +//! Rules from [perflint](https://pypi.org/project/perflint/). +pub(crate) mod rules; + +#[cfg(test)] +mod tests { + use std::path::Path; + + use anyhow::Result; + use test_case::test_case; + + use crate::assert_messages; + use crate::registry::Rule; + use crate::settings::Settings; + use crate::test::test_path; + + #[test_case(Rule::IncorrectDictIterator, Path::new("PERF102.py"))] + fn rules(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); + let diagnostics = test_path( + Path::new("perflint").join(path).as_path(), + &Settings::for_rule(rule_code), + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } +} diff --git a/crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs b/crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs new file mode 100644 index 0000000000..842c207e45 --- /dev/null +++ b/crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs @@ -0,0 +1,170 @@ +use std::fmt; + +use ruff_text_size::{TextRange, TextSize}; +use rustpython_parser::ast::Expr; +use rustpython_parser::{ast, lexer, Mode, Tok}; + +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::prelude::Ranged; +use ruff_python_ast::source_code::Locator; + +use crate::checkers::ast::Checker; +use crate::registry::AsRule; + +/// ## What it does +/// Checks for uses of `dict.items()` that discard either the key or the value +/// when iterating over the dictionary. +/// +/// ## Why is this bad? +/// If you only need the keys or values of a dictionary, you should use +/// `dict.keys()` or `dict.values()` respectively, instead of `dict.items()`. +/// These specialized methods are more efficient than `dict.items()`, as they +/// avoid allocating tuples for every item in the dictionary. They also +/// communicate the intent of the code more clearly. +/// +/// ## Example +/// ```python +/// some_dict = {"a": 1, "b": 2} +/// for _, val in some_dict.items(): +/// print(val) +/// ``` +/// +/// Use instead: +/// ```python +/// some_dict = {"a": 1, "b": 2} +/// for val in some_dict.values(): +/// print(val) +/// ``` +#[violation] +pub struct IncorrectDictIterator { + subset: DictSubset, +} + +impl AlwaysAutofixableViolation for IncorrectDictIterator { + #[derive_message_formats] + fn message(&self) -> String { + let IncorrectDictIterator { subset } = self; + format!("When using only the {subset} of a dict use the `{subset}()` method") + } + + fn autofix_title(&self) -> String { + let IncorrectDictIterator { subset } = self; + format!("Replace `.items()` with `.{subset}()`") + } +} + +/// PERF102 +pub(crate) fn incorrect_dict_iterator(checker: &mut Checker, target: &Expr, iter: &Expr) { + let Expr::Tuple(ast::ExprTuple { + elts, + .. + }) = target + else { + return + }; + if elts.len() != 2 { + return; + } + let Expr::Call(ast::ExprCall { func, args, .. }) = iter else { + return; + }; + if !args.is_empty() { + return; + } + let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func.as_ref() else { + return; + }; + if attr != "items" { + return; + } + + let unused_key = is_ignored_tuple_or_name(&elts[0]); + let unused_value = is_ignored_tuple_or_name(&elts[1]); + + match (unused_key, unused_value) { + (true, true) => { + // Both the key and the value are unused. + } + (false, false) => { + // Neither the key nor the value are unused. + } + (true, false) => { + // The key is unused, so replace with `dict.values()`. + let mut diagnostic = Diagnostic::new( + IncorrectDictIterator { + subset: DictSubset::Values, + }, + func.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + if let Some(range) = attribute_range(value.end(), checker.locator) { + let replace_attribute = Edit::range_replacement("values".to_string(), range); + let replace_target = Edit::range_replacement( + checker.locator.slice(elts[1].range()).to_string(), + target.range(), + ); + diagnostic.set_fix(Fix::suggested_edits(replace_attribute, [replace_target])); + } + } + checker.diagnostics.push(diagnostic); + } + (false, true) => { + // The value is unused, so replace with `dict.keys()`. + let mut diagnostic = Diagnostic::new( + IncorrectDictIterator { + subset: DictSubset::Keys, + }, + func.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + if let Some(range) = attribute_range(value.end(), checker.locator) { + let replace_attribute = Edit::range_replacement("keys".to_string(), range); + let replace_target = Edit::range_replacement( + checker.locator.slice(elts[0].range()).to_string(), + target.range(), + ); + diagnostic.set_fix(Fix::suggested_edits(replace_attribute, [replace_target])); + } + } + checker.diagnostics.push(diagnostic); + } + } +} + +#[derive(Debug, PartialEq, Eq)] +enum DictSubset { + Keys, + Values, +} + +impl fmt::Display for DictSubset { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + DictSubset::Keys => fmt.write_str("keys"), + DictSubset::Values => fmt.write_str("values"), + } + } +} + +/// Returns `true` if the given expression is either an ignored value or a tuple of ignored values. +fn is_ignored_tuple_or_name(expr: &Expr) -> bool { + match expr { + Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().all(is_ignored_tuple_or_name), + Expr::Name(ast::ExprName { id, .. }) => id == "_", + _ => false, + } +} + +/// Returns the range of the attribute identifier after the given location, if any. +fn attribute_range(at: TextSize, locator: &Locator) -> Option { + lexer::lex_starts_at(locator.after(at), Mode::Expression, at) + .flatten() + .find_map(|(tok, range)| { + if matches!(tok, Tok::Name { .. }) { + Some(range) + } else { + None + } + }) +} diff --git a/crates/ruff/src/rules/perflint/rules/mod.rs b/crates/ruff/src/rules/perflint/rules/mod.rs new file mode 100644 index 0000000000..a092bb73f8 --- /dev/null +++ b/crates/ruff/src/rules/perflint/rules/mod.rs @@ -0,0 +1,3 @@ +pub(crate) use incorrect_dict_iterator::{incorrect_dict_iterator, IncorrectDictIterator}; + +mod incorrect_dict_iterator; diff --git a/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF102_PERF102.py.snap b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF102_PERF102.py.snap new file mode 100644 index 0000000000..c62a538e0b --- /dev/null +++ b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF102_PERF102.py.snap @@ -0,0 +1,167 @@ +--- +source: crates/ruff/src/rules/perflint/mod.rs +--- +PERF102.py:3:17: PERF102 [*] When using only the values of a dict use the `values()` method + | +1 | some_dict = {"a": 12, "b": 32, "c": 44} +2 | +3 | for _, value in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ PERF102 +4 | print(value) + | + = help: Replace `.items()` with `.values()` + +ℹ Suggested fix +1 1 | some_dict = {"a": 12, "b": 32, "c": 44} +2 2 | +3 |-for _, value in some_dict.items(): # PERF102 + 3 |+for value in some_dict.values(): # PERF102 +4 4 | print(value) +5 5 | +6 6 | + +PERF102.py:7:15: PERF102 [*] When using only the keys of a dict use the `keys()` method + | +7 | for key, _ in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ PERF102 +8 | print(key) + | + = help: Replace `.items()` with `.keys()` + +ℹ Suggested fix +4 4 | print(value) +5 5 | +6 6 | +7 |-for key, _ in some_dict.items(): # PERF102 + 7 |+for key in some_dict.keys(): # PERF102 +8 8 | print(key) +9 9 | +10 10 | + +PERF102.py:11:26: PERF102 [*] When using only the keys of a dict use the `keys()` method + | +11 | for weird_arg_name, _ in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ PERF102 +12 | print(weird_arg_name) + | + = help: Replace `.items()` with `.keys()` + +ℹ Suggested fix +8 8 | print(key) +9 9 | +10 10 | +11 |-for weird_arg_name, _ in some_dict.items(): # PERF102 + 11 |+for weird_arg_name in some_dict.keys(): # PERF102 +12 12 | print(weird_arg_name) +13 13 | +14 14 | + +PERF102.py:15:21: PERF102 [*] When using only the keys of a dict use the `keys()` method + | +15 | for name, (_, _) in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ PERF102 +16 | pass + | + = help: Replace `.items()` with `.keys()` + +ℹ Suggested fix +12 12 | print(weird_arg_name) +13 13 | +14 14 | +15 |-for name, (_, _) in some_dict.items(): # PERF102 + 15 |+for name in some_dict.keys(): # PERF102 +16 16 | pass +17 17 | +18 18 | + +PERF102.py:23:26: PERF102 [*] When using only the keys of a dict use the `keys()` method + | +23 | for (key1, _), (_, _) in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ PERF102 +24 | pass + | + = help: Replace `.items()` with `.keys()` + +ℹ Suggested fix +20 20 | pass +21 21 | +22 22 | +23 |-for (key1, _), (_, _) in some_dict.items(): # PERF102 + 23 |+for (key1, _) in some_dict.keys(): # PERF102 +24 24 | pass +25 25 | +26 26 | + +PERF102.py:27:32: PERF102 [*] When using only the values of a dict use the `values()` method + | +27 | for (_, (_, _)), (value, _) in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ PERF102 +28 | pass + | + = help: Replace `.items()` with `.values()` + +ℹ Suggested fix +24 24 | pass +25 25 | +26 26 | +27 |-for (_, (_, _)), (value, _) in some_dict.items(): # PERF102 + 27 |+for (value, _) in some_dict.values(): # PERF102 +28 28 | pass +29 29 | +30 30 | + +PERF102.py:39:28: PERF102 [*] When using only the keys of a dict use the `keys()` method + | +39 | for ((_, key2), (_, _)) in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ PERF102 +40 | pass + | + = help: Replace `.items()` with `.keys()` + +ℹ Suggested fix +36 36 | pass +37 37 | +38 38 | +39 |-for ((_, key2), (_, _)) in some_dict.items(): # PERF102 + 39 |+for (_, key2) in some_dict.keys(): # PERF102 +40 40 | pass +41 41 | +42 42 | + +PERF102.py:67:21: PERF102 [*] When using only the keys of a dict use the `keys()` method + | +67 | for name, (_, _) in (some_function()).items(): # PERF102 + | ^^^^^^^^^^^^^^^^^^^^^^^ PERF102 +68 | pass + | + = help: Replace `.items()` with `.keys()` + +ℹ Suggested fix +64 64 | print(value) +65 65 | +66 66 | +67 |-for name, (_, _) in (some_function()).items(): # PERF102 + 67 |+for name in (some_function()).keys(): # PERF102 +68 68 | pass +69 69 | +70 70 | for name, (_, _) in (some_function().some_attribute).items(): # PERF102 + +PERF102.py:70:21: PERF102 [*] When using only the keys of a dict use the `keys()` method + | +68 | pass +69 | +70 | for name, (_, _) in (some_function().some_attribute).items(): # PERF102 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PERF102 +71 | pass + | + = help: Replace `.items()` with `.keys()` + +ℹ Suggested fix +67 67 | for name, (_, _) in (some_function()).items(): # PERF102 +68 68 | pass +69 69 | +70 |-for name, (_, _) in (some_function().some_attribute).items(): # PERF102 + 70 |+for name in (some_function().some_attribute).keys(): # PERF102 +71 71 | pass + + diff --git a/docs/faq.md b/docs/faq.md index 07167d2829..e0fe13380a 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -74,6 +74,7 @@ natively, including: - [mccabe](https://pypi.org/project/mccabe/) - [pandas-vet](https://pypi.org/project/pandas-vet/) - [pep8-naming](https://pypi.org/project/pep8-naming/) +- [perflint](https://pypi.org/project/perflint/) ([#4789](https://github.com/astral-sh/ruff/issues/4789)) - [pydocstyle](https://pypi.org/project/pydocstyle/) - [pygrep-hooks](https://github.com/pre-commit/pygrep-hooks) - [pyupgrade](https://pypi.org/project/pyupgrade/) @@ -175,6 +176,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl - [mccabe](https://pypi.org/project/mccabe/) - [pandas-vet](https://pypi.org/project/pandas-vet/) - [pep8-naming](https://pypi.org/project/pep8-naming/) +- [perflint](https://pypi.org/project/perflint/) ([#4789](https://github.com/astral-sh/ruff/issues/4789)) - [pydocstyle](https://pypi.org/project/pydocstyle/) - [tryceratops](https://pypi.org/project/tryceratops/) diff --git a/ruff.schema.json b/ruff.schema.json index 54023b1ece..9cd7a4b16a 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2061,6 +2061,10 @@ "PD9", "PD90", "PD901", + "PERF", + "PERF1", + "PERF10", + "PERF102", "PGH", "PGH0", "PGH00", From cc44349401e5193dec878f56ed11c3803a3e2636 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 12 Jun 2023 23:57:34 -0400 Subject: [PATCH 028/447] Use dedicated structs in `comparable.rs` (#5042) ## Summary Updating to match the updated AST structure, for consistency. --- crates/ruff_python_ast/src/comparable.rs | 1271 ++++++++++------- .../src/pattern/pattern_match_value.rs | 6 +- 2 files changed, 731 insertions(+), 546 deletions(-) diff --git a/crates/ruff_python_ast/src/comparable.rs b/crates/ruff_python_ast/src/comparable.rs index 6477644477..94e902814b 100644 --- a/crates/ruff_python_ast/src/comparable.rs +++ b/crates/ruff_python_ast/src/comparable.rs @@ -1,13 +1,8 @@ -//! An equivalent object hierarchy to the [`Expr`] hierarchy, but with the +//! An equivalent object hierarchy to the `RustPython` AST hierarchy, but with the //! ability to compare expressions for equality (via [`Eq`] and [`Hash`]). use num_bigint::BigInt; -use rustpython_ast::Decorator; -use rustpython_parser::ast::{ - self, Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, ConversionFlag, - Excepthandler, Expr, ExprContext, Identifier, Int, Keyword, MatchCase, Operator, Pattern, Stmt, - Unaryop, Withitem, -}; +use rustpython_parser::ast; #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] pub enum ComparableExprContext { @@ -16,12 +11,12 @@ pub enum ComparableExprContext { Del, } -impl From<&ExprContext> for ComparableExprContext { - fn from(ctx: &ExprContext) -> Self { +impl From<&ast::ExprContext> for ComparableExprContext { + fn from(ctx: &ast::ExprContext) -> Self { match ctx { - ExprContext::Load => Self::Load, - ExprContext::Store => Self::Store, - ExprContext::Del => Self::Del, + ast::ExprContext::Load => Self::Load, + ast::ExprContext::Store => Self::Store, + ast::ExprContext::Del => Self::Del, } } } @@ -32,11 +27,11 @@ pub enum ComparableBoolop { Or, } -impl From<&Boolop> for ComparableBoolop { - fn from(op: &Boolop) -> Self { +impl From<&ast::Boolop> for ComparableBoolop { + fn from(op: &ast::Boolop) -> Self { match op { - Boolop::And => Self::And, - Boolop::Or => Self::Or, + ast::Boolop::And => Self::And, + ast::Boolop::Or => Self::Or, } } } @@ -58,22 +53,22 @@ pub enum ComparableOperator { FloorDiv, } -impl From<&Operator> for ComparableOperator { - fn from(op: &Operator) -> Self { +impl From<&ast::Operator> for ComparableOperator { + fn from(op: &ast::Operator) -> Self { match op { - Operator::Add => Self::Add, - Operator::Sub => Self::Sub, - Operator::Mult => Self::Mult, - Operator::MatMult => Self::MatMult, - Operator::Div => Self::Div, - Operator::Mod => Self::Mod, - Operator::Pow => Self::Pow, - Operator::LShift => Self::LShift, - Operator::RShift => Self::RShift, - Operator::BitOr => Self::BitOr, - Operator::BitXor => Self::BitXor, - Operator::BitAnd => Self::BitAnd, - Operator::FloorDiv => Self::FloorDiv, + ast::Operator::Add => Self::Add, + ast::Operator::Sub => Self::Sub, + ast::Operator::Mult => Self::Mult, + ast::Operator::MatMult => Self::MatMult, + ast::Operator::Div => Self::Div, + ast::Operator::Mod => Self::Mod, + ast::Operator::Pow => Self::Pow, + ast::Operator::LShift => Self::LShift, + ast::Operator::RShift => Self::RShift, + ast::Operator::BitOr => Self::BitOr, + ast::Operator::BitXor => Self::BitXor, + ast::Operator::BitAnd => Self::BitAnd, + ast::Operator::FloorDiv => Self::FloorDiv, } } } @@ -86,13 +81,13 @@ pub enum ComparableUnaryop { USub, } -impl From<&Unaryop> for ComparableUnaryop { - fn from(op: &Unaryop) -> Self { +impl From<&ast::Unaryop> for ComparableUnaryop { + fn from(op: &ast::Unaryop) -> Self { match op { - Unaryop::Invert => Self::Invert, - Unaryop::Not => Self::Not, - Unaryop::UAdd => Self::UAdd, - Unaryop::USub => Self::USub, + ast::Unaryop::Invert => Self::Invert, + ast::Unaryop::Not => Self::Not, + ast::Unaryop::UAdd => Self::UAdd, + ast::Unaryop::USub => Self::USub, } } } @@ -111,19 +106,19 @@ pub enum ComparableCmpop { NotIn, } -impl From<&Cmpop> for ComparableCmpop { - fn from(op: &Cmpop) -> Self { +impl From<&ast::Cmpop> for ComparableCmpop { + fn from(op: &ast::Cmpop) -> Self { match op { - Cmpop::Eq => Self::Eq, - Cmpop::NotEq => Self::NotEq, - Cmpop::Lt => Self::Lt, - Cmpop::LtE => Self::LtE, - Cmpop::Gt => Self::Gt, - Cmpop::GtE => Self::GtE, - Cmpop::Is => Self::Is, - Cmpop::IsNot => Self::IsNot, - Cmpop::In => Self::In, - Cmpop::NotIn => Self::NotIn, + ast::Cmpop::Eq => Self::Eq, + ast::Cmpop::NotEq => Self::NotEq, + ast::Cmpop::Lt => Self::Lt, + ast::Cmpop::LtE => Self::LtE, + ast::Cmpop::Gt => Self::Gt, + ast::Cmpop::GtE => Self::GtE, + ast::Cmpop::Is => Self::Is, + ast::Cmpop::IsNot => Self::IsNot, + ast::Cmpop::In => Self::In, + ast::Cmpop::NotIn => Self::NotIn, } } } @@ -134,8 +129,8 @@ pub struct ComparableAlias<'a> { pub asname: Option<&'a str>, } -impl<'a> From<&'a Alias> for ComparableAlias<'a> { - fn from(alias: &'a Alias) -> Self { +impl<'a> From<&'a ast::Alias> for ComparableAlias<'a> { + fn from(alias: &'a ast::Alias) -> Self { Self { name: alias.name.as_str(), asname: alias.asname.as_deref(), @@ -149,8 +144,8 @@ pub struct ComparableWithitem<'a> { pub optional_vars: Option>, } -impl<'a> From<&'a Withitem> for ComparableWithitem<'a> { - fn from(withitem: &'a Withitem) -> Self { +impl<'a> From<&'a ast::Withitem> for ComparableWithitem<'a> { + fn from(withitem: &'a ast::Withitem) -> Self { Self { context_expr: (&withitem.context_expr).into(), optional_vars: withitem.optional_vars.as_ref().map(Into::into), @@ -158,95 +153,127 @@ impl<'a> From<&'a Withitem> for ComparableWithitem<'a> { } } +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct PatternMatchValue<'a> { + value: ComparableExpr<'a>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct PatternMatchSingleton<'a> { + value: ComparableConstant<'a>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct PatternMatchSequence<'a> { + patterns: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct PatternMatchMapping<'a> { + keys: Vec>, + patterns: Vec>, + rest: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct PatternMatchClass<'a> { + cls: ComparableExpr<'a>, + patterns: Vec>, + kwd_attrs: Vec<&'a str>, + kwd_patterns: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct PatternMatchStar<'a> { + name: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct PatternMatchAs<'a> { + pattern: Option>>, + name: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct PatternMatchOr<'a> { + patterns: Vec>, +} + #[allow(clippy::enum_variant_names)] #[derive(Debug, PartialEq, Eq, Hash)] pub enum ComparablePattern<'a> { - MatchValue { - value: ComparableExpr<'a>, - }, - MatchSingleton { - value: ComparableConstant<'a>, - }, - MatchSequence { - patterns: Vec>, - }, - MatchMapping { - keys: Vec>, - patterns: Vec>, - rest: Option<&'a str>, - }, - MatchClass { - cls: ComparableExpr<'a>, - patterns: Vec>, - kwd_attrs: Vec<&'a str>, - kwd_patterns: Vec>, - }, - MatchStar { - name: Option<&'a str>, - }, - MatchAs { - pattern: Option>>, - name: Option<&'a str>, - }, - MatchOr { - patterns: Vec>, - }, + MatchValue(PatternMatchValue<'a>), + MatchSingleton(PatternMatchSingleton<'a>), + MatchSequence(PatternMatchSequence<'a>), + MatchMapping(PatternMatchMapping<'a>), + MatchClass(PatternMatchClass<'a>), + MatchStar(PatternMatchStar<'a>), + MatchAs(PatternMatchAs<'a>), + MatchOr(PatternMatchOr<'a>), } -impl<'a> From<&'a Pattern> for ComparablePattern<'a> { - fn from(pattern: &'a Pattern) -> Self { +impl<'a> From<&'a ast::Pattern> for ComparablePattern<'a> { + fn from(pattern: &'a ast::Pattern) -> Self { match pattern { - Pattern::MatchValue(ast::PatternMatchValue { value, .. }) => Self::MatchValue { - value: value.into(), - }, - Pattern::MatchSingleton(ast::PatternMatchSingleton { value, .. }) => { - Self::MatchSingleton { + ast::Pattern::MatchValue(ast::PatternMatchValue { value, .. }) => { + Self::MatchValue(PatternMatchValue { value: value.into(), - } + }) } - Pattern::MatchSequence(ast::PatternMatchSequence { patterns, .. }) => { - Self::MatchSequence { + ast::Pattern::MatchSingleton(ast::PatternMatchSingleton { value, .. }) => { + Self::MatchSingleton(PatternMatchSingleton { + value: value.into(), + }) + } + ast::Pattern::MatchSequence(ast::PatternMatchSequence { patterns, .. }) => { + Self::MatchSequence(PatternMatchSequence { patterns: patterns.iter().map(Into::into).collect(), - } + }) } - Pattern::MatchMapping(ast::PatternMatchMapping { + ast::Pattern::MatchMapping(ast::PatternMatchMapping { keys, patterns, rest, .. - }) => Self::MatchMapping { + }) => Self::MatchMapping(PatternMatchMapping { keys: keys.iter().map(Into::into).collect(), patterns: patterns.iter().map(Into::into).collect(), rest: rest.as_deref(), - }, - Pattern::MatchClass(ast::PatternMatchClass { + }), + ast::Pattern::MatchClass(ast::PatternMatchClass { cls, patterns, kwd_attrs, kwd_patterns, .. - }) => Self::MatchClass { + }) => Self::MatchClass(PatternMatchClass { cls: cls.into(), patterns: patterns.iter().map(Into::into).collect(), - kwd_attrs: kwd_attrs.iter().map(Identifier::as_str).collect(), + kwd_attrs: kwd_attrs.iter().map(ast::Identifier::as_str).collect(), kwd_patterns: kwd_patterns.iter().map(Into::into).collect(), - }, - Pattern::MatchStar(ast::PatternMatchStar { name, .. }) => Self::MatchStar { - name: name.as_deref(), - }, - Pattern::MatchAs(ast::PatternMatchAs { pattern, name, .. }) => Self::MatchAs { - pattern: pattern.as_ref().map(Into::into), - name: name.as_deref(), - }, - Pattern::MatchOr(ast::PatternMatchOr { patterns, .. }) => Self::MatchOr { - patterns: patterns.iter().map(Into::into).collect(), - }, + }), + ast::Pattern::MatchStar(ast::PatternMatchStar { name, .. }) => { + Self::MatchStar(PatternMatchStar { + name: name.as_deref(), + }) + } + ast::Pattern::MatchAs(ast::PatternMatchAs { pattern, name, .. }) => { + Self::MatchAs(PatternMatchAs { + pattern: pattern.as_ref().map(Into::into), + name: name.as_deref(), + }) + } + ast::Pattern::MatchOr(ast::PatternMatchOr { patterns, .. }) => { + Self::MatchOr(PatternMatchOr { + patterns: patterns.iter().map(Into::into).collect(), + }) + } } } } -impl<'a> From<&'a Box> for Box> { - fn from(pattern: &'a Box) -> Self { +impl<'a> From<&'a Box> for Box> { + fn from(pattern: &'a Box) -> Self { Box::new((&**pattern).into()) } } @@ -258,8 +285,8 @@ pub struct ComparableMatchCase<'a> { pub body: Vec>, } -impl<'a> From<&'a MatchCase> for ComparableMatchCase<'a> { - fn from(match_case: &'a MatchCase) -> Self { +impl<'a> From<&'a ast::MatchCase> for ComparableMatchCase<'a> { + fn from(match_case: &'a ast::MatchCase) -> Self { Self { pattern: (&match_case.pattern).into(), guard: match_case.guard.as_ref().map(Into::into), @@ -273,8 +300,8 @@ pub struct ComparableDecorator<'a> { pub expression: ComparableExpr<'a>, } -impl<'a> From<&'a Decorator> for ComparableDecorator<'a> { - fn from(decorator: &'a Decorator) -> Self { +impl<'a> From<&'a ast::Decorator> for ComparableDecorator<'a> { + fn from(decorator: &'a ast::Decorator) -> Self { Self { expression: (&decorator.expression).into(), } @@ -294,21 +321,21 @@ pub enum ComparableConstant<'a> { Ellipsis, } -impl<'a> From<&'a Constant> for ComparableConstant<'a> { - fn from(constant: &'a Constant) -> Self { +impl<'a> From<&'a ast::Constant> for ComparableConstant<'a> { + fn from(constant: &'a ast::Constant) -> Self { match constant { - Constant::None => Self::None, - Constant::Bool(value) => Self::Bool(value), - Constant::Str(value) => Self::Str(value), - Constant::Bytes(value) => Self::Bytes(value), - Constant::Int(value) => Self::Int(value), - Constant::Tuple(value) => Self::Tuple(value.iter().map(Into::into).collect()), - Constant::Float(value) => Self::Float(value.to_bits()), - Constant::Complex { real, imag } => Self::Complex { + ast::Constant::None => Self::None, + ast::Constant::Bool(value) => Self::Bool(value), + ast::Constant::Str(value) => Self::Str(value), + ast::Constant::Bytes(value) => Self::Bytes(value), + ast::Constant::Int(value) => Self::Int(value), + ast::Constant::Tuple(value) => Self::Tuple(value.iter().map(Into::into).collect()), + ast::Constant::Float(value) => Self::Float(value.to_bits()), + ast::Constant::Complex { real, imag } => Self::Complex { real: real.to_bits(), imag: imag.to_bits(), }, - Constant::Ellipsis => Self::Ellipsis, + ast::Constant::Ellipsis => Self::Ellipsis, } } } @@ -324,8 +351,8 @@ pub struct ComparableArguments<'a> { pub defaults: Vec>, } -impl<'a> From<&'a Arguments> for ComparableArguments<'a> { - fn from(arguments: &'a Arguments) -> Self { +impl<'a> From<&'a ast::Arguments> for ComparableArguments<'a> { + fn from(arguments: &'a ast::Arguments) -> Self { Self { posonlyargs: arguments.posonlyargs.iter().map(Into::into).collect(), args: arguments.args.iter().map(Into::into).collect(), @@ -338,14 +365,14 @@ impl<'a> From<&'a Arguments> for ComparableArguments<'a> { } } -impl<'a> From<&'a Box> for ComparableArguments<'a> { - fn from(arguments: &'a Box) -> Self { +impl<'a> From<&'a Box> for ComparableArguments<'a> { + fn from(arguments: &'a Box) -> Self { (&**arguments).into() } } -impl<'a> From<&'a Box> for ComparableArg<'a> { - fn from(arg: &'a Box) -> Self { +impl<'a> From<&'a Box> for ComparableArg<'a> { + fn from(arg: &'a Box) -> Self { (&**arg).into() } } @@ -357,8 +384,8 @@ pub struct ComparableArg<'a> { pub type_comment: Option<&'a str>, } -impl<'a> From<&'a Arg> for ComparableArg<'a> { - fn from(arg: &'a Arg) -> Self { +impl<'a> From<&'a ast::Arg> for ComparableArg<'a> { + fn from(arg: &'a ast::Arg) -> Self { Self { arg: arg.arg.as_str(), annotation: arg.annotation.as_ref().map(Into::into), @@ -373,10 +400,10 @@ pub struct ComparableKeyword<'a> { pub value: ComparableExpr<'a>, } -impl<'a> From<&'a Keyword> for ComparableKeyword<'a> { - fn from(keyword: &'a Keyword) -> Self { +impl<'a> From<&'a ast::Keyword> for ComparableKeyword<'a> { + fn from(keyword: &'a ast::Keyword) -> Self { Self { - arg: keyword.arg.as_ref().map(Identifier::as_str), + arg: keyword.arg.as_ref().map(ast::Identifier::as_str), value: (&keyword.value).into(), } } @@ -390,8 +417,8 @@ pub struct ComparableComprehension<'a> { pub is_async: bool, } -impl<'a> From<&'a Comprehension> for ComparableComprehension<'a> { - fn from(comprehension: &'a Comprehension) -> Self { +impl<'a> From<&'a ast::Comprehension> for ComparableComprehension<'a> { + fn from(comprehension: &'a ast::Comprehension) -> Self { Self { target: (&comprehension.target).into(), iter: (&comprehension.iter).into(), @@ -410,10 +437,13 @@ pub enum ComparableExcepthandler<'a> { }, } -impl<'a> From<&'a Excepthandler> for ComparableExcepthandler<'a> { - fn from(excepthandler: &'a Excepthandler) -> Self { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { - type_, name, body, .. +impl<'a> From<&'a ast::Excepthandler> for ComparableExcepthandler<'a> { + fn from(excepthandler: &'a ast::Excepthandler) -> Self { + let ast::Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { + type_, + name, + body, + .. }) = excepthandler; Self::ExceptHandler { type_: type_.as_ref().map(Into::into), @@ -424,498 +454,651 @@ impl<'a> From<&'a Excepthandler> for ComparableExcepthandler<'a> { } #[derive(Debug, PartialEq, Eq, Hash)] -pub enum ComparableExpr<'a> { - BoolOp { - op: ComparableBoolop, - values: Vec>, - }, - NamedExpr { - target: Box>, - value: Box>, - }, - BinOp { - left: Box>, - op: ComparableOperator, - right: Box>, - }, - UnaryOp { - op: ComparableUnaryop, - operand: Box>, - }, - Lambda { - args: ComparableArguments<'a>, - body: Box>, - }, - IfExp { - test: Box>, - body: Box>, - orelse: Box>, - }, - Dict { - keys: Vec>>, - values: Vec>, - }, - Set { - elts: Vec>, - }, - ListComp { - elt: Box>, - generators: Vec>, - }, - SetComp { - elt: Box>, - generators: Vec>, - }, - DictComp { - key: Box>, - value: Box>, - generators: Vec>, - }, - GeneratorExp { - elt: Box>, - generators: Vec>, - }, - Await { - value: Box>, - }, - Yield { - value: Option>>, - }, - YieldFrom { - value: Box>, - }, - Compare { - left: Box>, - ops: Vec, - comparators: Vec>, - }, - Call { - func: Box>, - args: Vec>, - keywords: Vec>, - }, - FormattedValue { - value: Box>, - conversion: ConversionFlag, - format_spec: Option>>, - }, - JoinedStr { - values: Vec>, - }, - Constant { - value: ComparableConstant<'a>, - kind: Option<&'a str>, - }, - Attribute { - value: Box>, - attr: &'a str, - ctx: ComparableExprContext, - }, - Subscript { - value: Box>, - slice: Box>, - ctx: ComparableExprContext, - }, - Starred { - value: Box>, - ctx: ComparableExprContext, - }, - Name { - id: &'a str, - ctx: ComparableExprContext, - }, - List { - elts: Vec>, - ctx: ComparableExprContext, - }, - Tuple { - elts: Vec>, - ctx: ComparableExprContext, - }, - Slice { - lower: Option>>, - upper: Option>>, - step: Option>>, - }, +pub struct ExprBoolOp<'a> { + op: ComparableBoolop, + values: Vec>, } -impl<'a> From<&'a Box> for Box> { - fn from(expr: &'a Box) -> Self { +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprNamedExpr<'a> { + target: Box>, + value: Box>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprBinOp<'a> { + left: Box>, + op: ComparableOperator, + right: Box>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprUnaryOp<'a> { + op: ComparableUnaryop, + operand: Box>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprLambda<'a> { + args: ComparableArguments<'a>, + body: Box>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprIfExp<'a> { + test: Box>, + body: Box>, + orelse: Box>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprDict<'a> { + keys: Vec>>, + values: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprSet<'a> { + elts: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprListComp<'a> { + elt: Box>, + generators: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprSetComp<'a> { + elt: Box>, + generators: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprDictComp<'a> { + key: Box>, + value: Box>, + generators: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprGeneratorExp<'a> { + elt: Box>, + generators: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprAwait<'a> { + value: Box>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprYield<'a> { + value: Option>>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprYieldFrom<'a> { + value: Box>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprCompare<'a> { + left: Box>, + ops: Vec, + comparators: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprCall<'a> { + func: Box>, + args: Vec>, + keywords: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprFormattedValue<'a> { + value: Box>, + conversion: ast::ConversionFlag, + format_spec: Option>>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprJoinedStr<'a> { + values: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprConstant<'a> { + value: ComparableConstant<'a>, + kind: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprAttribute<'a> { + value: Box>, + attr: &'a str, + ctx: ComparableExprContext, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprSubscript<'a> { + value: Box>, + slice: Box>, + ctx: ComparableExprContext, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprStarred<'a> { + value: Box>, + ctx: ComparableExprContext, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprName<'a> { + id: &'a str, + ctx: ComparableExprContext, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprList<'a> { + elts: Vec>, + ctx: ComparableExprContext, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprTuple<'a> { + elts: Vec>, + ctx: ComparableExprContext, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprSlice<'a> { + lower: Option>>, + upper: Option>>, + step: Option>>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub enum ComparableExpr<'a> { + BoolOp(ExprBoolOp<'a>), + NamedExpr(ExprNamedExpr<'a>), + BinOp(ExprBinOp<'a>), + UnaryOp(ExprUnaryOp<'a>), + Lambda(ExprLambda<'a>), + IfExp(ExprIfExp<'a>), + Dict(ExprDict<'a>), + Set(ExprSet<'a>), + ListComp(ExprListComp<'a>), + SetComp(ExprSetComp<'a>), + DictComp(ExprDictComp<'a>), + GeneratorExp(ExprGeneratorExp<'a>), + Await(ExprAwait<'a>), + Yield(ExprYield<'a>), + YieldFrom(ExprYieldFrom<'a>), + Compare(ExprCompare<'a>), + Call(ExprCall<'a>), + FormattedValue(ExprFormattedValue<'a>), + JoinedStr(ExprJoinedStr<'a>), + Constant(ExprConstant<'a>), + Attribute(ExprAttribute<'a>), + Subscript(ExprSubscript<'a>), + Starred(ExprStarred<'a>), + Name(ExprName<'a>), + List(ExprList<'a>), + Tuple(ExprTuple<'a>), + Slice(ExprSlice<'a>), +} + +impl<'a> From<&'a Box> for Box> { + fn from(expr: &'a Box) -> Self { Box::new((&**expr).into()) } } -impl<'a> From<&'a Box> for ComparableExpr<'a> { - fn from(expr: &'a Box) -> Self { +impl<'a> From<&'a Box> for ComparableExpr<'a> { + fn from(expr: &'a Box) -> Self { (&**expr).into() } } -impl<'a> From<&'a Expr> for ComparableExpr<'a> { - fn from(expr: &'a Expr) -> Self { +impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { + fn from(expr: &'a ast::Expr) -> Self { match expr { - Expr::BoolOp(ast::ExprBoolOp { + ast::Expr::BoolOp(ast::ExprBoolOp { op, values, range: _range, - }) => Self::BoolOp { + }) => Self::BoolOp(ExprBoolOp { op: op.into(), values: values.iter().map(Into::into).collect(), - }, - Expr::NamedExpr(ast::ExprNamedExpr { + }), + ast::Expr::NamedExpr(ast::ExprNamedExpr { target, value, range: _range, - }) => Self::NamedExpr { + }) => Self::NamedExpr(ExprNamedExpr { target: target.into(), value: value.into(), - }, - Expr::BinOp(ast::ExprBinOp { + }), + ast::Expr::BinOp(ast::ExprBinOp { left, op, right, range: _range, - }) => Self::BinOp { + }) => Self::BinOp(ExprBinOp { left: left.into(), op: op.into(), right: right.into(), - }, - Expr::UnaryOp(ast::ExprUnaryOp { + }), + ast::Expr::UnaryOp(ast::ExprUnaryOp { op, operand, range: _range, - }) => Self::UnaryOp { + }) => Self::UnaryOp(ExprUnaryOp { op: op.into(), operand: operand.into(), - }, - Expr::Lambda(ast::ExprLambda { + }), + ast::Expr::Lambda(ast::ExprLambda { args, body, range: _range, - }) => Self::Lambda { + }) => Self::Lambda(ExprLambda { args: (&**args).into(), body: body.into(), - }, - Expr::IfExp(ast::ExprIfExp { + }), + ast::Expr::IfExp(ast::ExprIfExp { test, body, orelse, range: _range, - }) => Self::IfExp { + }) => Self::IfExp(ExprIfExp { test: test.into(), body: body.into(), orelse: orelse.into(), - }, - Expr::Dict(ast::ExprDict { + }), + ast::Expr::Dict(ast::ExprDict { keys, values, range: _range, - }) => Self::Dict { + }) => Self::Dict(ExprDict { keys: keys .iter() .map(|expr| expr.as_ref().map(Into::into)) .collect(), values: values.iter().map(Into::into).collect(), - }, - Expr::Set(ast::ExprSet { + }), + ast::Expr::Set(ast::ExprSet { elts, range: _range, - }) => Self::Set { + }) => Self::Set(ExprSet { elts: elts.iter().map(Into::into).collect(), - }, - Expr::ListComp(ast::ExprListComp { + }), + ast::Expr::ListComp(ast::ExprListComp { elt, generators, range: _range, - }) => Self::ListComp { + }) => Self::ListComp(ExprListComp { elt: elt.into(), generators: generators.iter().map(Into::into).collect(), - }, - Expr::SetComp(ast::ExprSetComp { + }), + ast::Expr::SetComp(ast::ExprSetComp { elt, generators, range: _range, - }) => Self::SetComp { + }) => Self::SetComp(ExprSetComp { elt: elt.into(), generators: generators.iter().map(Into::into).collect(), - }, - Expr::DictComp(ast::ExprDictComp { + }), + ast::Expr::DictComp(ast::ExprDictComp { key, value, generators, range: _range, - }) => Self::DictComp { + }) => Self::DictComp(ExprDictComp { key: key.into(), value: value.into(), generators: generators.iter().map(Into::into).collect(), - }, - Expr::GeneratorExp(ast::ExprGeneratorExp { + }), + ast::Expr::GeneratorExp(ast::ExprGeneratorExp { elt, generators, range: _range, - }) => Self::GeneratorExp { + }) => Self::GeneratorExp(ExprGeneratorExp { elt: elt.into(), generators: generators.iter().map(Into::into).collect(), - }, - Expr::Await(ast::ExprAwait { + }), + ast::Expr::Await(ast::ExprAwait { value, range: _range, - }) => Self::Await { + }) => Self::Await(ExprAwait { value: value.into(), - }, - Expr::Yield(ast::ExprYield { + }), + ast::Expr::Yield(ast::ExprYield { value, range: _range, - }) => Self::Yield { + }) => Self::Yield(ExprYield { value: value.as_ref().map(Into::into), - }, - Expr::YieldFrom(ast::ExprYieldFrom { + }), + ast::Expr::YieldFrom(ast::ExprYieldFrom { value, range: _range, - }) => Self::YieldFrom { + }) => Self::YieldFrom(ExprYieldFrom { value: value.into(), - }, - Expr::Compare(ast::ExprCompare { + }), + ast::Expr::Compare(ast::ExprCompare { left, ops, comparators, range: _range, - }) => Self::Compare { + }) => Self::Compare(ExprCompare { left: left.into(), ops: ops.iter().map(Into::into).collect(), comparators: comparators.iter().map(Into::into).collect(), - }, - Expr::Call(ast::ExprCall { + }), + ast::Expr::Call(ast::ExprCall { func, args, keywords, range: _range, - }) => Self::Call { + }) => Self::Call(ExprCall { func: func.into(), args: args.iter().map(Into::into).collect(), keywords: keywords.iter().map(Into::into).collect(), - }, - Expr::FormattedValue(ast::ExprFormattedValue { + }), + ast::Expr::FormattedValue(ast::ExprFormattedValue { value, conversion, format_spec, range: _range, - }) => Self::FormattedValue { + }) => Self::FormattedValue(ExprFormattedValue { value: value.into(), conversion: *conversion, format_spec: format_spec.as_ref().map(Into::into), - }, - Expr::JoinedStr(ast::ExprJoinedStr { + }), + ast::Expr::JoinedStr(ast::ExprJoinedStr { values, range: _range, - }) => Self::JoinedStr { + }) => Self::JoinedStr(ExprJoinedStr { values: values.iter().map(Into::into).collect(), - }, - Expr::Constant(ast::ExprConstant { + }), + ast::Expr::Constant(ast::ExprConstant { value, kind, range: _range, - }) => Self::Constant { + }) => Self::Constant(ExprConstant { value: value.into(), kind: kind.as_ref().map(String::as_str), - }, - Expr::Attribute(ast::ExprAttribute { + }), + ast::Expr::Attribute(ast::ExprAttribute { value, attr, ctx, range: _range, - }) => Self::Attribute { + }) => Self::Attribute(ExprAttribute { value: value.into(), attr: attr.as_str(), ctx: ctx.into(), - }, - Expr::Subscript(ast::ExprSubscript { + }), + ast::Expr::Subscript(ast::ExprSubscript { value, slice, ctx, range: _range, - }) => Self::Subscript { + }) => Self::Subscript(ExprSubscript { value: value.into(), slice: slice.into(), ctx: ctx.into(), - }, - Expr::Starred(ast::ExprStarred { + }), + ast::Expr::Starred(ast::ExprStarred { value, ctx, range: _range, - }) => Self::Starred { + }) => Self::Starred(ExprStarred { value: value.into(), ctx: ctx.into(), - }, - Expr::Name(ast::ExprName { + }), + ast::Expr::Name(ast::ExprName { id, ctx, range: _range, - }) => Self::Name { + }) => Self::Name(ExprName { id: id.as_str(), ctx: ctx.into(), - }, - Expr::List(ast::ExprList { + }), + ast::Expr::List(ast::ExprList { elts, ctx, range: _range, - }) => Self::List { + }) => Self::List(ExprList { elts: elts.iter().map(Into::into).collect(), ctx: ctx.into(), - }, - Expr::Tuple(ast::ExprTuple { + }), + ast::Expr::Tuple(ast::ExprTuple { elts, ctx, range: _range, - }) => Self::Tuple { + }) => Self::Tuple(ExprTuple { elts: elts.iter().map(Into::into).collect(), ctx: ctx.into(), - }, - Expr::Slice(ast::ExprSlice { + }), + ast::Expr::Slice(ast::ExprSlice { lower, upper, step, range: _range, - }) => Self::Slice { + }) => Self::Slice(ExprSlice { lower: lower.as_ref().map(Into::into), upper: upper.as_ref().map(Into::into), step: step.as_ref().map(Into::into), - }, + }), } } } +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtFunctionDef<'a> { + name: &'a str, + args: ComparableArguments<'a>, + body: Vec>, + decorator_list: Vec>, + returns: Option>, + type_comment: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtAsyncFunctionDef<'a> { + name: &'a str, + args: ComparableArguments<'a>, + body: Vec>, + decorator_list: Vec>, + returns: Option>, + type_comment: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtClassDef<'a> { + name: &'a str, + bases: Vec>, + keywords: Vec>, + body: Vec>, + decorator_list: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtReturn<'a> { + value: Option>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtDelete<'a> { + targets: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtAssign<'a> { + targets: Vec>, + value: ComparableExpr<'a>, + type_comment: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtAugAssign<'a> { + target: ComparableExpr<'a>, + op: ComparableOperator, + value: ComparableExpr<'a>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtAnnAssign<'a> { + target: ComparableExpr<'a>, + annotation: ComparableExpr<'a>, + value: Option>, + simple: bool, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtFor<'a> { + target: ComparableExpr<'a>, + iter: ComparableExpr<'a>, + body: Vec>, + orelse: Vec>, + type_comment: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtAsyncFor<'a> { + target: ComparableExpr<'a>, + iter: ComparableExpr<'a>, + body: Vec>, + orelse: Vec>, + type_comment: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtWhile<'a> { + test: ComparableExpr<'a>, + body: Vec>, + orelse: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtIf<'a> { + test: ComparableExpr<'a>, + body: Vec>, + orelse: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtWith<'a> { + items: Vec>, + body: Vec>, + type_comment: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtAsyncWith<'a> { + items: Vec>, + body: Vec>, + type_comment: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtMatch<'a> { + subject: ComparableExpr<'a>, + cases: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtRaise<'a> { + exc: Option>, + cause: Option>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtTry<'a> { + body: Vec>, + handlers: Vec>, + orelse: Vec>, + finalbody: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtTryStar<'a> { + body: Vec>, + handlers: Vec>, + orelse: Vec>, + finalbody: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtAssert<'a> { + test: ComparableExpr<'a>, + msg: Option>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtImport<'a> { + names: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtImportFrom<'a> { + module: Option<&'a str>, + names: Vec>, + level: Option, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtGlobal<'a> { + names: Vec<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtNonlocal<'a> { + names: Vec<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtExpr<'a> { + value: ComparableExpr<'a>, +} + #[derive(Debug, PartialEq, Eq, Hash)] pub enum ComparableStmt<'a> { - FunctionDef { - name: &'a str, - args: ComparableArguments<'a>, - body: Vec>, - decorator_list: Vec>, - returns: Option>, - type_comment: Option<&'a str>, - }, - AsyncFunctionDef { - name: &'a str, - args: ComparableArguments<'a>, - body: Vec>, - decorator_list: Vec>, - returns: Option>, - type_comment: Option<&'a str>, - }, - ClassDef { - name: &'a str, - bases: Vec>, - keywords: Vec>, - body: Vec>, - decorator_list: Vec>, - }, - Return { - value: Option>, - }, - Delete { - targets: Vec>, - }, - Assign { - targets: Vec>, - value: ComparableExpr<'a>, - type_comment: Option<&'a str>, - }, - AugAssign { - target: ComparableExpr<'a>, - op: ComparableOperator, - value: ComparableExpr<'a>, - }, - AnnAssign { - target: ComparableExpr<'a>, - annotation: ComparableExpr<'a>, - value: Option>, - simple: bool, - }, - For { - target: ComparableExpr<'a>, - iter: ComparableExpr<'a>, - body: Vec>, - orelse: Vec>, - type_comment: Option<&'a str>, - }, - AsyncFor { - target: ComparableExpr<'a>, - iter: ComparableExpr<'a>, - body: Vec>, - orelse: Vec>, - type_comment: Option<&'a str>, - }, - While { - test: ComparableExpr<'a>, - body: Vec>, - orelse: Vec>, - }, - If { - test: ComparableExpr<'a>, - body: Vec>, - orelse: Vec>, - }, - With { - items: Vec>, - body: Vec>, - type_comment: Option<&'a str>, - }, - AsyncWith { - items: Vec>, - body: Vec>, - type_comment: Option<&'a str>, - }, - Match { - subject: ComparableExpr<'a>, - cases: Vec>, - }, - Raise { - exc: Option>, - cause: Option>, - }, - Try { - body: Vec>, - handlers: Vec>, - orelse: Vec>, - finalbody: Vec>, - }, - TryStar { - body: Vec>, - handlers: Vec>, - orelse: Vec>, - finalbody: Vec>, - }, - Assert { - test: ComparableExpr<'a>, - msg: Option>, - }, - Import { - names: Vec>, - }, - ImportFrom { - module: Option<&'a str>, - names: Vec>, - level: Option, - }, - Global { - names: Vec<&'a str>, - }, - Nonlocal { - names: Vec<&'a str>, - }, - Expr { - value: ComparableExpr<'a>, - }, + FunctionDef(StmtFunctionDef<'a>), + AsyncFunctionDef(StmtAsyncFunctionDef<'a>), + ClassDef(StmtClassDef<'a>), + Return(StmtReturn<'a>), + Delete(StmtDelete<'a>), + Assign(StmtAssign<'a>), + AugAssign(StmtAugAssign<'a>), + AnnAssign(StmtAnnAssign<'a>), + For(StmtFor<'a>), + AsyncFor(StmtAsyncFor<'a>), + While(StmtWhile<'a>), + If(StmtIf<'a>), + With(StmtWith<'a>), + AsyncWith(StmtAsyncWith<'a>), + Match(StmtMatch<'a>), + Raise(StmtRaise<'a>), + Try(StmtTry<'a>), + TryStar(StmtTryStar<'a>), + Assert(StmtAssert<'a>), + Import(StmtImport<'a>), + ImportFrom(StmtImportFrom<'a>), + Global(StmtGlobal<'a>), + Nonlocal(StmtNonlocal<'a>), + Expr(StmtExpr<'a>), Pass, Break, Continue, } -impl<'a> From<&'a Stmt> for ComparableStmt<'a> { - fn from(stmt: &'a Stmt) -> Self { +impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> { + fn from(stmt: &'a ast::Stmt) -> Self { match stmt { - Stmt::FunctionDef(ast::StmtFunctionDef { + ast::Stmt::FunctionDef(ast::StmtFunctionDef { name, args, body, @@ -923,15 +1106,15 @@ impl<'a> From<&'a Stmt> for ComparableStmt<'a> { returns, type_comment, range: _range, - }) => Self::FunctionDef { + }) => Self::FunctionDef(StmtFunctionDef { name: name.as_str(), args: args.into(), body: body.iter().map(Into::into).collect(), decorator_list: decorator_list.iter().map(Into::into).collect(), returns: returns.as_ref().map(Into::into), type_comment: type_comment.as_ref().map(String::as_str), - }, - Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { + }), + ast::Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { name, args, body, @@ -939,225 +1122,225 @@ impl<'a> From<&'a Stmt> for ComparableStmt<'a> { returns, type_comment, range: _range, - }) => Self::AsyncFunctionDef { + }) => Self::AsyncFunctionDef(StmtAsyncFunctionDef { name: name.as_str(), args: args.into(), body: body.iter().map(Into::into).collect(), decorator_list: decorator_list.iter().map(Into::into).collect(), returns: returns.as_ref().map(Into::into), type_comment: type_comment.as_ref().map(String::as_str), - }, - Stmt::ClassDef(ast::StmtClassDef { + }), + ast::Stmt::ClassDef(ast::StmtClassDef { name, bases, keywords, body, decorator_list, range: _range, - }) => Self::ClassDef { + }) => Self::ClassDef(StmtClassDef { name: name.as_str(), bases: bases.iter().map(Into::into).collect(), keywords: keywords.iter().map(Into::into).collect(), body: body.iter().map(Into::into).collect(), decorator_list: decorator_list.iter().map(Into::into).collect(), - }, - Stmt::Return(ast::StmtReturn { + }), + ast::Stmt::Return(ast::StmtReturn { value, range: _range, - }) => Self::Return { + }) => Self::Return(StmtReturn { value: value.as_ref().map(Into::into), - }, - Stmt::Delete(ast::StmtDelete { + }), + ast::Stmt::Delete(ast::StmtDelete { targets, range: _range, - }) => Self::Delete { + }) => Self::Delete(StmtDelete { targets: targets.iter().map(Into::into).collect(), - }, - Stmt::Assign(ast::StmtAssign { + }), + ast::Stmt::Assign(ast::StmtAssign { targets, value, type_comment, range: _range, - }) => Self::Assign { + }) => Self::Assign(StmtAssign { targets: targets.iter().map(Into::into).collect(), value: value.into(), type_comment: type_comment.as_ref().map(String::as_str), - }, - Stmt::AugAssign(ast::StmtAugAssign { + }), + ast::Stmt::AugAssign(ast::StmtAugAssign { target, op, value, range: _range, - }) => Self::AugAssign { + }) => Self::AugAssign(StmtAugAssign { target: target.into(), op: op.into(), value: value.into(), - }, - Stmt::AnnAssign(ast::StmtAnnAssign { + }), + ast::Stmt::AnnAssign(ast::StmtAnnAssign { target, annotation, value, simple, range: _range, - }) => Self::AnnAssign { + }) => Self::AnnAssign(StmtAnnAssign { target: target.into(), annotation: annotation.into(), value: value.as_ref().map(Into::into), simple: *simple, - }, - Stmt::For(ast::StmtFor { + }), + ast::Stmt::For(ast::StmtFor { target, iter, body, orelse, type_comment, range: _range, - }) => Self::For { + }) => Self::For(StmtFor { target: target.into(), iter: iter.into(), body: body.iter().map(Into::into).collect(), orelse: orelse.iter().map(Into::into).collect(), type_comment: type_comment.as_ref().map(String::as_str), - }, - Stmt::AsyncFor(ast::StmtAsyncFor { + }), + ast::Stmt::AsyncFor(ast::StmtAsyncFor { target, iter, body, orelse, type_comment, range: _range, - }) => Self::AsyncFor { + }) => Self::AsyncFor(StmtAsyncFor { target: target.into(), iter: iter.into(), body: body.iter().map(Into::into).collect(), orelse: orelse.iter().map(Into::into).collect(), type_comment: type_comment.as_ref().map(String::as_str), - }, - Stmt::While(ast::StmtWhile { + }), + ast::Stmt::While(ast::StmtWhile { test, body, orelse, range: _range, - }) => Self::While { + }) => Self::While(StmtWhile { test: test.into(), body: body.iter().map(Into::into).collect(), orelse: orelse.iter().map(Into::into).collect(), - }, - Stmt::If(ast::StmtIf { + }), + ast::Stmt::If(ast::StmtIf { test, body, orelse, range: _range, - }) => Self::If { + }) => Self::If(StmtIf { test: test.into(), body: body.iter().map(Into::into).collect(), orelse: orelse.iter().map(Into::into).collect(), - }, - Stmt::With(ast::StmtWith { + }), + ast::Stmt::With(ast::StmtWith { items, body, type_comment, range: _range, - }) => Self::With { + }) => Self::With(StmtWith { items: items.iter().map(Into::into).collect(), body: body.iter().map(Into::into).collect(), type_comment: type_comment.as_ref().map(String::as_str), - }, - Stmt::AsyncWith(ast::StmtAsyncWith { + }), + ast::Stmt::AsyncWith(ast::StmtAsyncWith { items, body, type_comment, range: _range, - }) => Self::AsyncWith { + }) => Self::AsyncWith(StmtAsyncWith { items: items.iter().map(Into::into).collect(), body: body.iter().map(Into::into).collect(), type_comment: type_comment.as_ref().map(String::as_str), - }, - Stmt::Match(ast::StmtMatch { + }), + ast::Stmt::Match(ast::StmtMatch { subject, cases, range: _range, - }) => Self::Match { + }) => Self::Match(StmtMatch { subject: subject.into(), cases: cases.iter().map(Into::into).collect(), - }, - Stmt::Raise(ast::StmtRaise { + }), + ast::Stmt::Raise(ast::StmtRaise { exc, cause, range: _range, - }) => Self::Raise { + }) => Self::Raise(StmtRaise { exc: exc.as_ref().map(Into::into), cause: cause.as_ref().map(Into::into), - }, - Stmt::Try(ast::StmtTry { + }), + ast::Stmt::Try(ast::StmtTry { body, handlers, orelse, finalbody, range: _range, - }) => Self::Try { + }) => Self::Try(StmtTry { body: body.iter().map(Into::into).collect(), handlers: handlers.iter().map(Into::into).collect(), orelse: orelse.iter().map(Into::into).collect(), finalbody: finalbody.iter().map(Into::into).collect(), - }, - Stmt::TryStar(ast::StmtTryStar { + }), + ast::Stmt::TryStar(ast::StmtTryStar { body, handlers, orelse, finalbody, range: _range, - }) => Self::TryStar { + }) => Self::TryStar(StmtTryStar { body: body.iter().map(Into::into).collect(), handlers: handlers.iter().map(Into::into).collect(), orelse: orelse.iter().map(Into::into).collect(), finalbody: finalbody.iter().map(Into::into).collect(), - }, - Stmt::Assert(ast::StmtAssert { + }), + ast::Stmt::Assert(ast::StmtAssert { test, msg, range: _range, - }) => Self::Assert { + }) => Self::Assert(StmtAssert { test: test.into(), msg: msg.as_ref().map(Into::into), - }, - Stmt::Import(ast::StmtImport { + }), + ast::Stmt::Import(ast::StmtImport { names, range: _range, - }) => Self::Import { + }) => Self::Import(StmtImport { names: names.iter().map(Into::into).collect(), - }, - Stmt::ImportFrom(ast::StmtImportFrom { + }), + ast::Stmt::ImportFrom(ast::StmtImportFrom { module, names, level, range: _range, - }) => Self::ImportFrom { + }) => Self::ImportFrom(StmtImportFrom { module: module.as_deref(), names: names.iter().map(Into::into).collect(), level: *level, - }, - Stmt::Global(ast::StmtGlobal { + }), + ast::Stmt::Global(ast::StmtGlobal { names, range: _range, - }) => Self::Global { - names: names.iter().map(Identifier::as_str).collect(), - }, - Stmt::Nonlocal(ast::StmtNonlocal { + }) => Self::Global(StmtGlobal { + names: names.iter().map(ast::Identifier::as_str).collect(), + }), + ast::Stmt::Nonlocal(ast::StmtNonlocal { names, range: _range, - }) => Self::Nonlocal { - names: names.iter().map(Identifier::as_str).collect(), - }, - Stmt::Expr(ast::StmtExpr { + }) => Self::Nonlocal(StmtNonlocal { + names: names.iter().map(ast::Identifier::as_str).collect(), + }), + ast::Stmt::Expr(ast::StmtExpr { value, range: _range, - }) => Self::Expr { + }) => Self::Expr(StmtExpr { value: value.into(), - }, - Stmt::Pass(_) => Self::Pass, - Stmt::Break(_) => Self::Break, - Stmt::Continue(_) => Self::Continue, + }), + ast::Stmt::Pass(_) => Self::Pass, + ast::Stmt::Break(_) => Self::Break, + ast::Stmt::Continue(_) => Self::Continue, } } } diff --git a/crates/ruff_python_formatter/src/pattern/pattern_match_value.rs b/crates/ruff_python_formatter/src/pattern/pattern_match_value.rs index 9a57fbd15a..0a5251f3aa 100644 --- a/crates/ruff_python_formatter/src/pattern/pattern_match_value.rs +++ b/crates/ruff_python_formatter/src/pattern/pattern_match_value.rs @@ -1,7 +1,9 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; use rustpython_parser::ast::PatternMatchValue; +use ruff_formatter::{write, Buffer, FormatResult}; + +use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; + #[derive(Default)] pub struct FormatPatternMatchValue; From 95ee6dcb3b3f65a98fd7241d0a0d646679f1b8d7 Mon Sep 17 00:00:00 2001 From: konstin Date: Tue, 13 Jun 2023 09:22:17 +0200 Subject: [PATCH 029/447] Add contributor docs to formatter (#5023) I've written done my condensed learnings from working on the formatter so that others can have an easier start working on it. This is a pure docs change --- crates/ruff_python_formatter/Docs.md | 8 -- crates/ruff_python_formatter/README.md | 163 +++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 8 deletions(-) delete mode 100644 crates/ruff_python_formatter/Docs.md create mode 100644 crates/ruff_python_formatter/README.md diff --git a/crates/ruff_python_formatter/Docs.md b/crates/ruff_python_formatter/Docs.md deleted file mode 100644 index 326dd33b81..0000000000 --- a/crates/ruff_python_formatter/Docs.md +++ /dev/null @@ -1,8 +0,0 @@ -# Rust Python Formatter - -For the formatter, we would like to implement `Format` from the rust_formatter crate for all AST -nodes, defined in the rustpython_parser crate. This violates rust's orphan rules. We therefore -generate in `generate.py` a newtype for each AST node with implementations of `FormatNodeRule`, -`FormatRule`, `AsFormat` and `IntoFormat` on it. - -![excalidraw showing the relationships between the different types](orphan_rules_in_the_formatter.svg) diff --git a/crates/ruff_python_formatter/README.md b/crates/ruff_python_formatter/README.md new file mode 100644 index 0000000000..8d693a3aab --- /dev/null +++ b/crates/ruff_python_formatter/README.md @@ -0,0 +1,163 @@ +# Rust Python Formatter + +The goal of our formatter is to be compatible with Black except for rare edge cases (mostly +involving comment placement). + +## Implementing a node + +Formatting each node follows roughly the same structure. We start with a `Format{{Node}}` struct +that implements Default (and `AsFormat`/`IntoFormat` impls in `generated.rs`, see orphan rules below). + +```rust +#[derive(Default)] +pub struct FormatStmtReturn; +``` + +We implement `FormatNodeRule<{{Node}}> for Format{{Node}}`. Inside, we destructure the item to make +sure we're not missing any field. If we want to write multiple items, we use an efficient `write!` +call, for single items `.format().fmt(f)` or `.fmt(f)` is sufficient. + +```rust +impl FormatNodeRule for FormatStmtReturn { + fn fmt_fields(&self, item: &StmtReturn, f: &mut PyFormatter) -> FormatResult<()> { + // Here we destructure item and make sure each field is listed. + // We generally don't need range is it's underscore-ignored + let StmtReturn { range: _, value } = item; + // Implement some formatting logic, in this case no space (and no value) after a return with + // no value + if let Some(value) = value { + write!( + f, + [ + text("return"), + // There are multiple different space and newline types (e.g. + // `soft_line_break_or_space()`, check the builders module), this one will + // always be translate to a normal ascii whitespace character + space(), + // `return a, b` is valid, but if it wraps we'd need parentheses. + // This is different from `(a, b).count(1)` where the parentheses around the + // tuple are mandatory + value.format().with_options(Parenthesize::IfBreaks) + ] + ) + } else { + text("return").fmt(f) + } + } +} +``` + +Check the `builders` module for the primitives that you can use. + +If something such as list or a tuple can break into multiple lines if it is too long for a single +line, wrap it into a `group`. Ignoring comments, we could format a tuple with two items like this: + +```rust +write!( + f, + [group(&format_args![ + text("("), + soft_block_indent(&format_args![ + item1.format() + text(","), + soft_line_break_or_space(), + item2.format(), + if_group_breaks(&text(",")) + ]), + text(")") + ])] +) +``` + +If everything fits on a single line, the group doesn't break and we get something like `("a", "b")`. +If it doesn't, we get something like + +```Python +( + "a", + "b", +) +``` + +For a list of expression, you don't need to format it manually but can use the `JoinBuilder` util, +accessible through `.join_with`. Finish will write to the formatter internally. + +```rust +f.join_with(&format_args!(text(","), soft_line_break_or_space())) + .entries(self.elts.iter().formatted()) + .finish()?; +// Here we need a trailing comma on the last entry of an expanded group since we have more +// than one element +write!(f, [if_group_breaks(&text(","))]) +``` + +If you need avoid second mutable borrows with a builder, you can use `format_with(|f| { ... })` as +a formattable element similar to `text()` or `group()`. + +The generic comment formatting in `FormatNodeRule` handles comments correctly for most nodes, e.g. +preceding and end-of-line comments depending on the node range. Sometimes however, you may have +dangling comments that are not before or after a node but inside of it, e.g. + +```Python +[ + # here we use an empty list +] +``` + +Here, you have to call `dangling_comments` manually and stubbing out `fmt_dangling_comments` in list +formatting. + +```rust +impl FormatNodeRule for FormatExprList { + fn fmt_fields(&self, item: &ExprList, f: &mut PyFormatter) -> FormatResult<()> { + // ... + + write!( + f, + [group(&format_args![ + text("["), + dangling_comments(dangling), + soft_block_indent(&items), + text("]") + ])] + ) + } + + fn fmt_dangling_comments(&self, _node: &ExprList, _f: &mut PyFormatter) -> FormatResult<()> { + // Handled as part of `fmt_fields` + Ok(()) + } +} +``` + +Comments are categorized into `Leading`, `Trailing` and `Dangling`, you can override this in +`place_comment`. + +## Development notes + +Handling parentheses and comments are two major challenges in a Python formatter. + +We have copied the majority of tests over from Black and use [insta](https://insta.rs/docs/cli/) for +snapshot testing with the diff between Ruff and Black, Black output and Ruff output. We put +additional test cases in `resources/test/fixtures/ruff`. + +The full Ruff test suite is slow, `cargo test -p ruff_python_formatter` is a lot faster. + +There is a `ruff_python_formatter` binary that avoid building and linking the main `ruff` crate. + +You can use `scratch.py` as a playground, e.g. +`cargo run --bin ruff_python_formatter -- --emit stdout scratch.py`, which additional `--print-ir` +and `--print-comments` options. + +The origin of Ruff's formatter is the [Rome formatter](https://github.com/rome/tools/tree/main/crates/rome_json_formatter), +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 + +## The orphan rules and trait structure + +For the formatter, we would like to implement `Format` from the rust_formatter crate for all AST +nodes, defined in the rustpython_parser crate. This violates Rust's orphan rules. We therefore +generate in `generate.py` a newtype for each AST node with implementations of `FormatNodeRule`, +`FormatRule`, `AsFormat` and `IntoFormat` on it. + +![excalidraw showing the relationships between the different types](orphan_rules_in_the_formatter.svg) From e1fd3965a23cfe81a3dfb2a3a43d358b8b7dae77 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Tue, 13 Jun 2023 13:14:45 +0200 Subject: [PATCH 030/447] Start with Upper case in error messages (#5045) ## Summary To be consistent with the format used by other errors. ## Test Plan N/A. --- crates/ruff/src/logging.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff/src/logging.rs b/crates/ruff/src/logging.rs index e281a35c9a..df950899eb 100644 --- a/crates/ruff/src/logging.rs +++ b/crates/ruff/src/logging.rs @@ -231,11 +231,11 @@ impl Display for DisplayParseErrorType<'_> { if let Some(expected) = expected.as_ref() { write!( f, - "expected '{expected}', but got {tok}", + "Expected '{expected}', but got {tok}", tok = TruncateAtNewline(&tok) ) } else { - write!(f, "unexpected token {tok}", tok = TruncateAtNewline(&tok)) + write!(f, "Unexpected token {tok}", tok = TruncateAtNewline(&tok)) } } ParseErrorType::Lexical(ref error) => write!(f, "{error}"), From 7b4dde0c6c95b7dfe59ef086c18539566b0df6d3 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 13 Jun 2023 17:15:55 +0300 Subject: [PATCH 031/447] Add JSON Lines (NDJSON) message serialization (#5048) ## Summary This adds `json-lines` (https://jsonlines.org/ or http://ndjson.org/) as an output format. I'm sure you already know, but * JSONL is more greppable (each record is a single line) than the pretty JSON * JSONL is faster to ingest piecewise (and/or in parallel) than JSON ## Test Plan Snapshot test in the new module :) --- crates/ruff/src/message/json.rs | 53 ++++++++++--------- crates/ruff/src/message/json_lines.rs | 39 ++++++++++++++ crates/ruff/src/message/mod.rs | 2 + ...f__message__json_lines__tests__output.snap | 8 +++ crates/ruff/src/settings/types.rs | 1 + crates/ruff_cli/src/printer.rs | 5 +- docs/configuration.md | 2 +- ruff.schema.json | 1 + 8 files changed, 84 insertions(+), 27 deletions(-) create mode 100644 crates/ruff/src/message/json_lines.rs create mode 100644 crates/ruff/src/message/snapshots/ruff__message__json_lines__tests__output.snap diff --git a/crates/ruff/src/message/json.rs b/crates/ruff/src/message/json.rs index c3adda51ba..47008eb64a 100644 --- a/crates/ruff/src/message/json.rs +++ b/crates/ruff/src/message/json.rs @@ -2,7 +2,7 @@ use std::io::Write; use serde::ser::SerializeSeq; use serde::{Serialize, Serializer}; -use serde_json::json; +use serde_json::{json, Value}; use ruff_diagnostics::Edit; use ruff_python_ast::source_code::SourceCode; @@ -38,30 +38,7 @@ impl Serialize for ExpandedMessages<'_> { let mut s = serializer.serialize_seq(Some(self.messages.len()))?; for message in self.messages { - let source_code = message.file.to_source_code(); - - let fix = message.fix.as_ref().map(|fix| { - json!({ - "applicability": fix.applicability(), - "message": message.kind.suggestion.as_deref(), - "edits": &ExpandedEdits { edits: fix.edits(), source_code: &source_code }, - }) - }); - - let start_location = source_code.source_location(message.start()); - let end_location = source_code.source_location(message.end()); - let noqa_location = source_code.source_location(message.noqa_offset); - - let value = json!({ - "code": message.kind.rule().noqa_code().to_string(), - "message": message.kind.body, - "fix": fix, - "location": start_location, - "end_location": end_location, - "filename": message.filename(), - "noqa_row": noqa_location.row - }); - + let value = message_to_json_value(message); s.serialize_element(&value)?; } @@ -69,6 +46,32 @@ impl Serialize for ExpandedMessages<'_> { } } +pub(crate) fn message_to_json_value(message: &Message) -> Value { + let source_code = message.file.to_source_code(); + + let fix = message.fix.as_ref().map(|fix| { + json!({ + "applicability": fix.applicability(), + "message": message.kind.suggestion.as_deref(), + "edits": &ExpandedEdits { edits: fix.edits(), source_code: &source_code }, + }) + }); + + let start_location = source_code.source_location(message.start()); + let end_location = source_code.source_location(message.end()); + let noqa_location = source_code.source_location(message.noqa_offset); + + json!({ + "code": message.kind.rule().noqa_code().to_string(), + "message": message.kind.body, + "fix": fix, + "location": start_location, + "end_location": end_location, + "filename": message.filename(), + "noqa_row": noqa_location.row + }) +} + struct ExpandedEdits<'a> { edits: &'a [Edit], source_code: &'a SourceCode<'a, 'a>, diff --git a/crates/ruff/src/message/json_lines.rs b/crates/ruff/src/message/json_lines.rs new file mode 100644 index 0000000000..931d7b3ade --- /dev/null +++ b/crates/ruff/src/message/json_lines.rs @@ -0,0 +1,39 @@ +use std::io::Write; + +use crate::message::json::message_to_json_value; +use crate::message::{Emitter, EmitterContext, Message}; + +#[derive(Default)] +pub struct JsonLinesEmitter; + +impl Emitter for JsonLinesEmitter { + fn emit( + &mut self, + writer: &mut dyn Write, + messages: &[Message], + _context: &EmitterContext, + ) -> anyhow::Result<()> { + let mut w = writer; + for message in messages { + serde_json::to_writer(&mut w, &message_to_json_value(message))?; + w.write_all(b"\n")?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::message::json_lines::JsonLinesEmitter; + use insta::assert_snapshot; + + use crate::message::tests::{capture_emitter_output, create_messages}; + + #[test] + fn output() { + let mut emitter = JsonLinesEmitter::default(); + let content = capture_emitter_output(&mut emitter, &create_messages()); + + assert_snapshot!(content); + } +} diff --git a/crates/ruff/src/message/mod.rs b/crates/ruff/src/message/mod.rs index be5a03afae..821191bbad 100644 --- a/crates/ruff/src/message/mod.rs +++ b/crates/ruff/src/message/mod.rs @@ -12,6 +12,7 @@ pub use github::GithubEmitter; pub use gitlab::GitlabEmitter; pub use grouped::GroupedEmitter; pub use json::JsonEmitter; +pub use json_lines::JsonLinesEmitter; pub use junit::JunitEmitter; pub use pylint::PylintEmitter; use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix}; @@ -24,6 +25,7 @@ mod github; mod gitlab; mod grouped; mod json; +mod json_lines; mod junit; mod pylint; mod text; diff --git a/crates/ruff/src/message/snapshots/ruff__message__json_lines__tests__output.snap b/crates/ruff/src/message/snapshots/ruff__message__json_lines__tests__output.snap new file mode 100644 index 0000000000..b6edd32a10 --- /dev/null +++ b/crates/ruff/src/message/snapshots/ruff__message__json_lines__tests__output.snap @@ -0,0 +1,8 @@ +--- +source: crates/ruff/src/message/jsonlines.rs +expression: content +--- +{"code":"F401","message":"`os` imported but unused","fix":{"applicability":"Suggested","message":"Remove unused import: `os`","edits":[{"content":"","location":{"row":1,"column":1},"end_location":{"row":2,"column":1}}]},"location":{"row":1,"column":8},"end_location":{"row":1,"column":10},"filename":"fib.py","noqa_row":1} +{"code":"F841","message":"Local variable `x` is assigned to but never used","fix":{"applicability":"Suggested","message":"Remove assignment to unused variable `x`","edits":[{"content":"","location":{"row":6,"column":5},"end_location":{"row":6,"column":10}}]},"location":{"row":6,"column":5},"end_location":{"row":6,"column":6},"filename":"fib.py","noqa_row":6} +{"code":"F821","message":"Undefined name `a`","fix":null,"location":{"row":1,"column":4},"end_location":{"row":1,"column":5},"filename":"undef.py","noqa_row":1} + diff --git a/crates/ruff/src/settings/types.rs b/crates/ruff/src/settings/types.rs index 49689d8948..2346d7506a 100644 --- a/crates/ruff/src/settings/types.rs +++ b/crates/ruff/src/settings/types.rs @@ -214,6 +214,7 @@ impl FromStr for PatternPrefixPair { pub enum SerializationFormat { Text, Json, + JsonLines, Junit, Grouped, Github, diff --git a/crates/ruff_cli/src/printer.rs b/crates/ruff_cli/src/printer.rs index 34e15fbb11..707460675f 100644 --- a/crates/ruff_cli/src/printer.rs +++ b/crates/ruff_cli/src/printer.rs @@ -16,7 +16,7 @@ use ruff::linter::FixTable; use ruff::logging::LogLevel; use ruff::message::{ AzureEmitter, Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter, - JsonEmitter, JunitEmitter, PylintEmitter, TextEmitter, + JsonEmitter, JsonLinesEmitter, JunitEmitter, PylintEmitter, TextEmitter, }; use ruff::notify_user; use ruff::registry::{AsRule, Rule}; @@ -184,6 +184,9 @@ impl Printer { SerializationFormat::Json => { JsonEmitter::default().emit(writer, &diagnostics.messages, &context)?; } + SerializationFormat::JsonLines => { + JsonLinesEmitter::default().emit(writer, &diagnostics.messages, &context)?; + } SerializationFormat::Junit => { JunitEmitter::default().emit(writer, &diagnostics.messages, &context)?; } diff --git a/docs/configuration.md b/docs/configuration.md index f2805e731a..91897bac80 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -211,7 +211,7 @@ Options: --ignore-noqa Ignore any `# noqa` comments --format - Output serialization format for violations [env: RUFF_FORMAT=] [possible values: text, json, junit, grouped, github, gitlab, pylint, azure] + Output serialization format for violations [env: RUFF_FORMAT=] [possible values: text, json, json-lines, junit, grouped, github, gitlab, pylint, azure] --target-version The minimum Python version that should be supported [possible values: py37, py38, py39, py310, py311] --config diff --git a/ruff.schema.json b/ruff.schema.json index 9cd7a4b16a..4af020e35b 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2606,6 +2606,7 @@ "enum": [ "text", "json", + "json-lines", "junit", "grouped", "github", From 65312bad01c71cff29b63916e8fc61a285a66ffc Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 13 Jun 2023 10:21:14 -0400 Subject: [PATCH 032/447] Remove unannotated attributes from RUF008 (#5049) ## Summary In a dataclass: ```py from dataclasses import dataclass @dataclass class X: class_var = {} x: int ``` `class_var` isn't actually a dataclass attribute, since it's unannotated. This PR removes such attributes from RUF008 (`mutable-dataclass-default`), but it does enforce them in RUF012 (`mutable-class-default`), since those should be annotated with `ClassVar` like any other mutable class attribute. Closes #5043. --- .../resources/test/fixtures/ruff/RUF008.py | 4 +- .../resources/test/fixtures/ruff/RUF012.py | 15 ++++- .../function_call_in_dataclass_default.rs | 7 +-- .../rules/ruff/rules/mutable_class_default.rs | 5 +- .../ruff/rules/mutable_dataclass_default.rs | 58 ++++++++++--------- ..._rules__ruff__tests__RUF008_RUF008.py.snap | 34 +++-------- ..._rules__ruff__tests__RUF012_RUF012.py.snap | 36 +++++++----- 7 files changed, 78 insertions(+), 81 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF008.py b/crates/ruff/resources/test/fixtures/ruff/RUF008.py index 3a40f7f094..978b88b0c7 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF008.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF008.py @@ -5,12 +5,11 @@ from typing import ClassVar, Sequence KNOWINGLY_MUTABLE_DEFAULT = [] -@dataclass() +@dataclass class A: mutable_default: list[int] = [] immutable_annotation: typing.Sequence[int] = [] without_annotation = [] - ignored_via_comment: list[int] = [] # noqa: RUF008 correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT perfectly_fine: list[int] = field(default_factory=list) class_variable: typing.ClassVar[list[int]] = [] @@ -21,7 +20,6 @@ class B: mutable_default: list[int] = [] immutable_annotation: Sequence[int] = [] without_annotation = [] - ignored_via_comment: list[int] = [] # noqa: RUF008 correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT perfectly_fine: list[int] = field(default_factory=list) class_variable: ClassVar[list[int]] = [] diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF012.py b/crates/ruff/resources/test/fixtures/ruff/RUF012.py index 4b4c6df0bf..1a51f179cd 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF012.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF012.py @@ -8,7 +8,6 @@ class A: mutable_default: list[int] = [] immutable_annotation: typing.Sequence[int] = [] without_annotation = [] - ignored_via_comment: list[int] = [] # noqa: RUF012 correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT class_variable: typing.ClassVar[list[int]] = [] @@ -17,6 +16,18 @@ class B: mutable_default: list[int] = [] immutable_annotation: Sequence[int] = [] without_annotation = [] - ignored_via_comment: list[int] = [] # noqa: RUF012 correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT class_variable: ClassVar[list[int]] = [] + + +from dataclasses import dataclass, field + + +@dataclass +class C: + mutable_default: list[int] = [] + immutable_annotation: Sequence[int] = [] + without_annotation = [] + correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT + perfectly_fine: list[int] = field(default_factory=list) + class_variable: ClassVar[list[int]] = [] diff --git a/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs b/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs index 373d37a724..54e0f88488 100644 --- a/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs +++ b/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs @@ -95,11 +95,8 @@ pub(crate) fn function_call_in_dataclass_default( }) = statement { if let Expr::Call(ast::ExprCall { func, .. }) = expr.as_ref() { - if is_class_var_annotation(checker.semantic_model(), annotation) { - continue; - } - - if !is_immutable_func(checker.semantic_model(), func, &extend_immutable_calls) + if !is_class_var_annotation(checker.semantic_model(), annotation) + && !is_immutable_func(checker.semantic_model(), func, &extend_immutable_calls) && !is_allowed_dataclass_function(checker.semantic_model(), func) { checker.diagnostics.push(Diagnostic::new( diff --git a/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs b/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs index 9bf066ecad..f126251352 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs @@ -45,10 +45,6 @@ impl Violation for MutableClassDefault { /// RUF012 pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::StmtClassDef) { - if is_dataclass(checker.semantic_model(), class_def) { - return; - } - for statement in &class_def.body { match statement { Stmt::AnnAssign(ast::StmtAnnAssign { @@ -59,6 +55,7 @@ pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::Stmt if is_mutable_expr(value) && !is_class_var_annotation(checker.semantic_model(), annotation) && !is_immutable_annotation(checker.semantic_model(), annotation) + && !is_dataclass(checker.semantic_model(), class_def) { checker .diagnostics diff --git a/crates/ruff/src/rules/ruff/rules/mutable_dataclass_default.rs b/crates/ruff/src/rules/ruff/rules/mutable_dataclass_default.rs index 6c51fc2443..63d4b6d397 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_dataclass_default.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_dataclass_default.rs @@ -8,17 +8,19 @@ use crate::checkers::ast::Checker; use crate::rules::ruff::rules::helpers::{is_class_var_annotation, is_dataclass, is_mutable_expr}; /// ## What it does -/// Checks for mutable default values in dataclasses. +/// Checks for mutable default values in dataclass attributes. /// /// ## Why is this bad? -/// Mutable default values share state across all instances of the dataclass, -/// while not being obvious. This can lead to bugs when the attributes are -/// changed in one instance, as those changes will unexpectedly affect all -/// other instances. +/// Mutable default values share state across all instances of the dataclass. +/// This can lead to bugs when the attributes are changed in one instance, as +/// those changes will unexpectedly affect all other instances. /// /// Instead of sharing mutable defaults, use the `field(default_factory=...)` /// pattern. /// +/// If the default value is intended to be mutable, it should be annotated with +/// `typing.ClassVar`. +/// /// ## Examples /// ```python /// from dataclasses import dataclass @@ -38,6 +40,17 @@ use crate::rules::ruff::rules::helpers::{is_class_var_annotation, is_dataclass, /// class A: /// mutable_default: list[int] = field(default_factory=list) /// ``` +/// +/// Or: +/// ```python +/// from dataclasses import dataclass, field +/// from typing import ClassVar +/// +/// +/// @dataclass +/// class A: +/// mutable_default: ClassVar[list[int]] = [] +/// ``` #[violation] pub struct MutableDataclassDefault; @@ -55,29 +68,20 @@ pub(crate) fn mutable_dataclass_default(checker: &mut Checker, class_def: &ast:: } for statement in &class_def.body { - match statement { - Stmt::AnnAssign(ast::StmtAnnAssign { - annotation, - value: Some(value), - .. - }) => { - if is_mutable_expr(value) - && !is_class_var_annotation(checker.semantic_model(), annotation) - && !is_immutable_annotation(checker.semantic_model(), annotation) - { - checker - .diagnostics - .push(Diagnostic::new(MutableDataclassDefault, value.range())); - } + if let Stmt::AnnAssign(ast::StmtAnnAssign { + annotation, + value: Some(value), + .. + }) = statement + { + if is_mutable_expr(value) + && !is_class_var_annotation(checker.semantic_model(), annotation) + && !is_immutable_annotation(checker.semantic_model(), annotation) + { + checker + .diagnostics + .push(Diagnostic::new(MutableDataclassDefault, value.range())); } - Stmt::Assign(ast::StmtAssign { value, .. }) => { - if is_mutable_expr(value) { - checker - .diagnostics - .push(Diagnostic::new(MutableDataclassDefault, value.range())); - } - } - _ => (), } } } diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF008_RUF008.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF008_RUF008.py.snap index 36c054f145..e1d7054a57 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF008_RUF008.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF008_RUF008.py.snap @@ -3,7 +3,7 @@ source: crates/ruff/src/rules/ruff/mod.rs --- RUF008.py:10:34: RUF008 Do not use mutable default values for dataclass attributes | - 8 | @dataclass() + 8 | @dataclass 9 | class A: 10 | mutable_default: list[int] = [] | ^^ RUF008 @@ -11,34 +11,14 @@ RUF008.py:10:34: RUF008 Do not use mutable default values for dataclass attribut 12 | without_annotation = [] | -RUF008.py:12:26: RUF008 Do not use mutable default values for dataclass attributes +RUF008.py:20:34: RUF008 Do not use mutable default values for dataclass attributes | -10 | mutable_default: list[int] = [] -11 | immutable_annotation: typing.Sequence[int] = [] -12 | without_annotation = [] - | ^^ RUF008 -13 | ignored_via_comment: list[int] = [] # noqa: RUF008 -14 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT - | - -RUF008.py:21:34: RUF008 Do not use mutable default values for dataclass attributes - | -19 | @dataclass -20 | class B: -21 | mutable_default: list[int] = [] +18 | @dataclass +19 | class B: +20 | mutable_default: list[int] = [] | ^^ RUF008 -22 | immutable_annotation: Sequence[int] = [] -23 | without_annotation = [] - | - -RUF008.py:23:26: RUF008 Do not use mutable default values for dataclass attributes - | -21 | mutable_default: list[int] = [] -22 | immutable_annotation: Sequence[int] = [] -23 | without_annotation = [] - | ^^ RUF008 -24 | ignored_via_comment: list[int] = [] # noqa: RUF008 -25 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT +21 | immutable_annotation: Sequence[int] = [] +22 | without_annotation = [] | diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap index 02a09e70f4..55536d8dc2 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap @@ -16,27 +16,37 @@ RUF012.py:10:26: RUF012 Mutable class attributes should be annotated with `typin 9 | immutable_annotation: typing.Sequence[int] = [] 10 | without_annotation = [] | ^^ RUF012 -11 | ignored_via_comment: list[int] = [] # noqa: RUF012 -12 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT +11 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT +12 | class_variable: typing.ClassVar[list[int]] = [] | -RUF012.py:17:34: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` +RUF012.py:16:34: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` | -16 | class B: -17 | mutable_default: list[int] = [] +15 | class B: +16 | mutable_default: list[int] = [] | ^^ RUF012 -18 | immutable_annotation: Sequence[int] = [] -19 | without_annotation = [] +17 | immutable_annotation: Sequence[int] = [] +18 | without_annotation = [] | -RUF012.py:19:26: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` +RUF012.py:18:26: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` | -17 | mutable_default: list[int] = [] -18 | immutable_annotation: Sequence[int] = [] -19 | without_annotation = [] +16 | mutable_default: list[int] = [] +17 | immutable_annotation: Sequence[int] = [] +18 | without_annotation = [] | ^^ RUF012 -20 | ignored_via_comment: list[int] = [] # noqa: RUF012 -21 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT +19 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT +20 | class_variable: ClassVar[list[int]] = [] + | + +RUF012.py:30:26: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` + | +28 | mutable_default: list[int] = [] +29 | immutable_annotation: Sequence[int] = [] +30 | without_annotation = [] + | ^^ RUF012 +31 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT +32 | perfectly_fine: list[int] = field(default_factory=list) | From b0f89fa8143dbd4ae935cde95f8e7ec1ee4f6c44 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Tue, 13 Jun 2023 17:37:13 +0200 Subject: [PATCH 033/447] Support glob patterns in pep8_naming ignore-names (#5024) ## Summary Support glob patterns in pep8_naming ignore-names. Closes #2787 ## Test Plan Added new tests. --- Cargo.lock | 1 + .../fixtures/pep8_naming/ignore_names/N802.py | 14 ++++ .../fixtures/pep8_naming/ignore_names/N803.py | 12 ++++ .../fixtures/pep8_naming/ignore_names/N804.py | 22 ++++++ .../fixtures/pep8_naming/ignore_names/N805.py | 42 +++++++++++ .../fixtures/pep8_naming/ignore_names/N806.py | 6 ++ .../fixtures/pep8_naming/ignore_names/N815.py | 19 +++++ .../fixtures/pep8_naming/ignore_names/N816.py | 8 +++ crates/ruff/src/rules/pep8_naming/mod.rs | 29 +++++++- .../rules/invalid_argument_name.rs | 9 ++- ...id_first_argument_name_for_class_method.rs | 2 +- .../invalid_first_argument_name_for_method.rs | 2 +- .../rules/invalid_function_name.rs | 9 ++- .../mixed_case_variable_in_class_scope.rs | 2 +- .../mixed_case_variable_in_global_scope.rs | 2 +- .../non_lowercase_variable_in_function.rs | 2 +- crates/ruff/src/rules/pep8_naming/settings.rs | 69 ++++++++++++++++--- ...ing__tests__ignore_names_N802_N802.py.snap | 22 ++++++ ...ing__tests__ignore_names_N803_N803.py.snap | 22 ++++++ ...ing__tests__ignore_names_N804_N804.py.snap | 29 ++++++++ ...ing__tests__ignore_names_N805_N805.py.snap | 47 +++++++++++++ ...ing__tests__ignore_names_N806_N806.py.snap | 21 ++++++ ...ing__tests__ignore_names_N815_N815.py.snap | 58 ++++++++++++++++ ...ing__tests__ignore_names_N816_N816.py.snap | 29 ++++++++ crates/ruff/src/settings/mod.rs | 3 +- crates/ruff/src/settings/types.rs | 13 ++++ crates/ruff_cache/Cargo.toml | 1 + crates/ruff_cache/src/cache_key.rs | 7 ++ ruff.schema.json | 2 +- 29 files changed, 482 insertions(+), 22 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N802.py create mode 100644 crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N803.py create mode 100644 crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N804.py create mode 100644 crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N805.py create mode 100644 crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N806.py create mode 100644 crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N815.py create mode 100644 crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N816.py create mode 100644 crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N802_N802.py.snap create mode 100644 crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N803_N803.py.snap create mode 100644 crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N804_N804.py.snap create mode 100644 crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N805_N805.py.snap create mode 100644 crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N806_N806.py.snap create mode 100644 crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N815_N815.py.snap create mode 100644 crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N816_N816.py.snap diff --git a/Cargo.lock b/Cargo.lock index 50fd9272f2..8ffd30015d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1880,6 +1880,7 @@ name = "ruff_cache" version = "0.0.0" dependencies = [ "filetime", + "glob", "globset", "itertools", "regex", diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N802.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N802.py new file mode 100644 index 0000000000..5bd0717e9b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N802.py @@ -0,0 +1,14 @@ +import unittest + +def badAllowed(): + pass + +def stillBad(): + pass + +class Test(unittest.TestCase): + def badAllowed(self): + return super().tearDown() + + def stillBad(self): + return super().tearDown() diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N803.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N803.py new file mode 100644 index 0000000000..2c2d7ba2be --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N803.py @@ -0,0 +1,12 @@ +def func(_, a, badAllowed): + return _, a, badAllowed + +def func(_, a, stillBad): + return _, a, stillBad + +class Class: + def method(self, _, a, badAllowed): + return _, a, badAllowed + + def method(self, _, a, stillBad): + return _, a, stillBad diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N804.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N804.py new file mode 100644 index 0000000000..c3f9598417 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N804.py @@ -0,0 +1,22 @@ +from abc import ABCMeta + + +class Class: + def __init_subclass__(self, default_name, **kwargs): + ... + + @classmethod + def badAllowed(self, x, /, other): + ... + + @classmethod + def stillBad(self, x, /, other): + ... + + +class MetaClass(ABCMeta): + def badAllowed(self): + pass + + def stillBad(self): + pass diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N805.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N805.py new file mode 100644 index 0000000000..ae5206483e --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N805.py @@ -0,0 +1,42 @@ +from abc import ABCMeta + +import pydantic + + +class Class: + def badAllowed(this): + pass + + def stillBad(this): + pass + + if False: + + def badAllowed(this): + pass + + def stillBad(this): + pass + + @pydantic.validator + def badAllowed(cls, my_field: str) -> str: + pass + + @pydantic.validator + def stillBad(cls, my_field: str) -> str: + pass + + @pydantic.validator("my_field") + def badAllowed(cls, my_field: str) -> str: + pass + + @pydantic.validator("my_field") + def stillBad(cls, my_field: str) -> str: + pass + +class PosOnlyClass: + def badAllowed(this, blah, /, self, something: str): + pass + + def stillBad(this, blah, /, self, something: str): + pass diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N806.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N806.py new file mode 100644 index 0000000000..7ece45c61b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N806.py @@ -0,0 +1,6 @@ +def assign(): + badAllowed = 0 + stillBad = 0 + + BAD_ALLOWED = 0 + STILL_BAD = 0 diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N815.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N815.py new file mode 100644 index 0000000000..02c39544ca --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N815.py @@ -0,0 +1,19 @@ +class C: + badAllowed = 0 + stillBad = 0 + + _badAllowed = 0 + _stillBad = 0 + + bad_Allowed = 0 + still_Bad = 0 + +class D(TypedDict): + badAllowed: bool + stillBad: bool + + _badAllowed: list + _stillBad: list + + bad_Allowed: set + still_Bad: set diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N816.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N816.py new file mode 100644 index 0000000000..5ae20d20fe --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N816.py @@ -0,0 +1,8 @@ +badAllowed = 0 +stillBad = 0 + +_badAllowed = 0 +_stillBad = 0 + +bad_Allowed = 0 +still_Bad = 0 diff --git a/crates/ruff/src/rules/pep8_naming/mod.rs b/crates/ruff/src/rules/pep8_naming/mod.rs index 98f58c1e12..e9c965c9ac 100644 --- a/crates/ruff/src/rules/pep8_naming/mod.rs +++ b/crates/ruff/src/rules/pep8_naming/mod.rs @@ -5,13 +5,14 @@ pub mod settings; #[cfg(test)] mod tests { - use std::path::Path; + use std::path::{Path, PathBuf}; use anyhow::Result; use test_case::test_case; use crate::registry::Rule; use crate::rules::pep8_naming; + use crate::settings::types::IdentifierPattern; use crate::test::test_path; use crate::{assert_messages, settings}; @@ -104,4 +105,30 @@ mod tests { assert_messages!(diagnostics); Ok(()) } + + #[test_case(Rule::InvalidFunctionName, "N802.py")] + #[test_case(Rule::InvalidArgumentName, "N803.py")] + #[test_case(Rule::InvalidFirstArgumentNameForClassMethod, "N804.py")] + #[test_case(Rule::InvalidFirstArgumentNameForMethod, "N805.py")] + #[test_case(Rule::NonLowercaseVariableInFunction, "N806.py")] + #[test_case(Rule::MixedCaseVariableInClassScope, "N815.py")] + #[test_case(Rule::MixedCaseVariableInGlobalScope, "N816.py")] + fn ignore_names(rule_code: Rule, path: &str) -> Result<()> { + let snapshot = format!("ignore_names_{}_{path}", rule_code.noqa_code()); + let diagnostics = test_path( + PathBuf::from_iter(["pep8_naming", "ignore_names", path]).as_path(), + &settings::Settings { + pep8_naming: pep8_naming::settings::Settings { + ignore_names: vec![ + IdentifierPattern::new("*Allowed").unwrap(), + IdentifierPattern::new("*ALLOWED").unwrap(), + ], + ..Default::default() + }, + ..settings::Settings::for_rule(rule_code) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } } diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_argument_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_argument_name.rs index c2c8fe8d08..b1dca74a30 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_argument_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_argument_name.rs @@ -3,6 +3,8 @@ use rustpython_parser::ast::{Arg, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use crate::settings::types::IdentifierPattern; + /// ## What it does /// Checks for argument names that do not follow the `snake_case` convention. /// @@ -48,9 +50,12 @@ impl Violation for InvalidArgumentName { pub(crate) fn invalid_argument_name( name: &str, arg: &Arg, - ignore_names: &[String], + ignore_names: &[IdentifierPattern], ) -> Option { - if ignore_names.iter().any(|ignore_name| ignore_name == name) { + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(name)) + { return None; } if name.to_lowercase() != name { diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs index 05f42674d1..e181cfd37c 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs @@ -82,7 +82,7 @@ pub(crate) fn invalid_first_argument_name_for_class_method( .pep8_naming .ignore_names .iter() - .any(|ignore_name| ignore_name == name) + .any(|ignore_name| ignore_name.matches(name)) { return None; } diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs index eeedca93c1..b9c198d1ca 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs @@ -81,7 +81,7 @@ pub(crate) fn invalid_first_argument_name_for_method( .pep8_naming .ignore_names .iter() - .any(|ignore_name| ignore_name == name) + .any(|ignore_name| ignore_name.matches(name)) { return None; } diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs index 3713bc8ef2..4e2b9c8867 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs @@ -7,6 +7,8 @@ use ruff_python_ast::source_code::Locator; use ruff_python_semantic::analyze::visibility; use ruff_python_semantic::model::SemanticModel; +use crate::settings::types::IdentifierPattern; + /// ## What it does /// Checks for functions names that do not follow the `snake_case` naming /// convention. @@ -52,12 +54,15 @@ pub(crate) fn invalid_function_name( stmt: &Stmt, name: &str, decorator_list: &[Decorator], - ignore_names: &[String], + ignore_names: &[IdentifierPattern], model: &SemanticModel, locator: &Locator, ) -> Option { // Ignore any explicitly-ignored function names. - if ignore_names.iter().any(|ignore_name| ignore_name == name) { + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(name)) + { return None; } diff --git a/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs b/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs index 2cb0793789..4fd006a82e 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs @@ -62,7 +62,7 @@ pub(crate) fn mixed_case_variable_in_class_scope( .pep8_naming .ignore_names .iter() - .any(|ignore_name| ignore_name == name) + .any(|ignore_name| ignore_name.matches(name)) { return; } diff --git a/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs b/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs index b0312c50d6..6ea71b349d 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs @@ -71,7 +71,7 @@ pub(crate) fn mixed_case_variable_in_global_scope( .pep8_naming .ignore_names .iter() - .any(|ignore_name| ignore_name == name) + .any(|ignore_name| ignore_name.matches(name)) { return; } diff --git a/crates/ruff/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs b/crates/ruff/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs index ed7648fb34..f395e6fefd 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs @@ -60,7 +60,7 @@ pub(crate) fn non_lowercase_variable_in_function( .pep8_naming .ignore_names .iter() - .any(|ignore_name| ignore_name == name) + .any(|ignore_name| ignore_name.matches(name)) { return; } diff --git a/crates/ruff/src/rules/pep8_naming/settings.rs b/crates/ruff/src/rules/pep8_naming/settings.rs index cfacb63632..12e301ecb8 100644 --- a/crates/ruff/src/rules/pep8_naming/settings.rs +++ b/crates/ruff/src/rules/pep8_naming/settings.rs @@ -1,9 +1,14 @@ //! Settings for the `pep8-naming` plugin. +use std::error::Error; +use std::fmt; + use serde::{Deserialize, Serialize}; use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions}; +use crate::settings::types::IdentifierPattern; + const IGNORE_NAMES: [&str; 12] = [ "setUp", "tearDown", @@ -36,7 +41,7 @@ pub struct Options { ignore-names = ["callMethod"] "# )] - /// A list of names to ignore when considering `pep8-naming` violations. + /// A list of names (or patterns) to ignore when considering `pep8-naming` violations. pub ignore_names: Option>, #[option( default = r#"[]"#, @@ -72,7 +77,7 @@ pub struct Options { #[derive(Debug, CacheKey)] pub struct Settings { - pub ignore_names: Vec, + pub ignore_names: Vec, pub classmethod_decorators: Vec, pub staticmethod_decorators: Vec, } @@ -80,21 +85,59 @@ pub struct Settings { impl Default for Settings { fn default() -> Self { Self { - ignore_names: IGNORE_NAMES.map(String::from).to_vec(), + ignore_names: IGNORE_NAMES + .iter() + .map(|name| IdentifierPattern::new(name).unwrap()) + .collect(), classmethod_decorators: Vec::new(), staticmethod_decorators: Vec::new(), } } } -impl From for Settings { - fn from(options: Options) -> Self { - Self { - ignore_names: options - .ignore_names - .unwrap_or_else(|| IGNORE_NAMES.map(String::from).to_vec()), +impl TryFrom for Settings { + type Error = SettingsError; + + fn try_from(options: Options) -> Result { + Ok(Self { + ignore_names: match options.ignore_names { + Some(names) => names + .into_iter() + .map(|name| { + IdentifierPattern::new(&name).map_err(SettingsError::InvalidIgnoreName) + }) + .collect::, Self::Error>>()?, + None => IGNORE_NAMES + .into_iter() + .map(|name| IdentifierPattern::new(name).unwrap()) + .collect(), + }, classmethod_decorators: options.classmethod_decorators.unwrap_or_default(), staticmethod_decorators: options.staticmethod_decorators.unwrap_or_default(), + }) + } +} + +/// Error returned by the [`TryFrom`] implementation of [`Settings`]. +#[derive(Debug)] +pub enum SettingsError { + InvalidIgnoreName(glob::PatternError), +} + +impl fmt::Display for SettingsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SettingsError::InvalidIgnoreName(err) => { + write!(f, "Invalid pattern in ignore-names: {err}") + } + } + } +} + +impl Error for SettingsError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + SettingsError::InvalidIgnoreName(err) => Some(err), } } } @@ -102,7 +145,13 @@ impl From for Settings { impl From for Options { fn from(settings: Settings) -> Self { Self { - ignore_names: Some(settings.ignore_names), + ignore_names: Some( + settings + .ignore_names + .into_iter() + .map(|pattern| pattern.as_str().to_owned()) + .collect(), + ), classmethod_decorators: Some(settings.classmethod_decorators), staticmethod_decorators: Some(settings.staticmethod_decorators), } diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N802_N802.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N802_N802.py.snap new file mode 100644 index 0000000000..9b33e0be3e --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N802_N802.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N802.py:6:5: N802 Function name `stillBad` should be lowercase + | +4 | pass +5 | +6 | def stillBad(): + | ^^^^^^^^ N802 +7 | pass + | + +N802.py:13:9: N802 Function name `stillBad` should be lowercase + | +11 | return super().tearDown() +12 | +13 | def stillBad(self): + | ^^^^^^^^ N802 +14 | return super().tearDown() + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N803_N803.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N803_N803.py.snap new file mode 100644 index 0000000000..92918cc420 --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N803_N803.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N803.py:4:16: N803 Argument name `stillBad` should be lowercase + | +2 | return _, a, badAllowed +3 | +4 | def func(_, a, stillBad): + | ^^^^^^^^ N803 +5 | return _, a, stillBad + | + +N803.py:11:28: N803 Argument name `stillBad` should be lowercase + | + 9 | return _, a, badAllowed +10 | +11 | def method(self, _, a, stillBad): + | ^^^^^^^^ N803 +12 | return _, a, stillBad + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N804_N804.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N804_N804.py.snap new file mode 100644 index 0000000000..c308109147 --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N804_N804.py.snap @@ -0,0 +1,29 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N804.py:5:27: N804 First argument of a class method should be named `cls` + | +4 | class Class: +5 | def __init_subclass__(self, default_name, **kwargs): + | ^^^^ N804 +6 | ... + | + +N804.py:13:18: N804 First argument of a class method should be named `cls` + | +12 | @classmethod +13 | def stillBad(self, x, /, other): + | ^^^^ N804 +14 | ... + | + +N804.py:21:18: N804 First argument of a class method should be named `cls` + | +19 | pass +20 | +21 | def stillBad(self): + | ^^^^ N804 +22 | pass + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N805_N805.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N805_N805.py.snap new file mode 100644 index 0000000000..46bfb51c01 --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N805_N805.py.snap @@ -0,0 +1,47 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N805.py:10:18: N805 First argument of a method should be named `self` + | + 8 | pass + 9 | +10 | def stillBad(this): + | ^^^^ N805 +11 | pass + | + +N805.py:18:22: N805 First argument of a method should be named `self` + | +16 | pass +17 | +18 | def stillBad(this): + | ^^^^ N805 +19 | pass + | + +N805.py:26:18: N805 First argument of a method should be named `self` + | +25 | @pydantic.validator +26 | def stillBad(cls, my_field: str) -> str: + | ^^^ N805 +27 | pass + | + +N805.py:34:18: N805 First argument of a method should be named `self` + | +33 | @pydantic.validator("my_field") +34 | def stillBad(cls, my_field: str) -> str: + | ^^^ N805 +35 | pass + | + +N805.py:41:18: N805 First argument of a method should be named `self` + | +39 | pass +40 | +41 | def stillBad(this, blah, /, self, something: str): + | ^^^^ N805 +42 | pass + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N806_N806.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N806_N806.py.snap new file mode 100644 index 0000000000..eff8113cce --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N806_N806.py.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N806.py:3:5: N806 Variable `stillBad` in function should be lowercase + | +1 | def assign(): +2 | badAllowed = 0 +3 | stillBad = 0 + | ^^^^^^^^ N806 +4 | +5 | BAD_ALLOWED = 0 + | + +N806.py:6:5: N806 Variable `STILL_BAD` in function should be lowercase + | +5 | BAD_ALLOWED = 0 +6 | STILL_BAD = 0 + | ^^^^^^^^^ N806 + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N815_N815.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N815_N815.py.snap new file mode 100644 index 0000000000..90cdc67709 --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N815_N815.py.snap @@ -0,0 +1,58 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N815.py:3:5: N815 Variable `stillBad` in class scope should not be mixedCase + | +1 | class C: +2 | badAllowed = 0 +3 | stillBad = 0 + | ^^^^^^^^ N815 +4 | +5 | _badAllowed = 0 + | + +N815.py:6:5: N815 Variable `_stillBad` in class scope should not be mixedCase + | +5 | _badAllowed = 0 +6 | _stillBad = 0 + | ^^^^^^^^^ N815 +7 | +8 | bad_Allowed = 0 + | + +N815.py:9:5: N815 Variable `still_Bad` in class scope should not be mixedCase + | + 8 | bad_Allowed = 0 + 9 | still_Bad = 0 + | ^^^^^^^^^ N815 +10 | +11 | class D(TypedDict): + | + +N815.py:13:5: N815 Variable `stillBad` in class scope should not be mixedCase + | +11 | class D(TypedDict): +12 | badAllowed: bool +13 | stillBad: bool + | ^^^^^^^^ N815 +14 | +15 | _badAllowed: list + | + +N815.py:16:5: N815 Variable `_stillBad` in class scope should not be mixedCase + | +15 | _badAllowed: list +16 | _stillBad: list + | ^^^^^^^^^ N815 +17 | +18 | bad_Allowed: set + | + +N815.py:19:5: N815 Variable `still_Bad` in class scope should not be mixedCase + | +18 | bad_Allowed: set +19 | still_Bad: set + | ^^^^^^^^^ N815 + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N816_N816.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N816_N816.py.snap new file mode 100644 index 0000000000..9535fc1ba6 --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N816_N816.py.snap @@ -0,0 +1,29 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N816.py:2:1: N816 Variable `stillBad` in global scope should not be mixedCase + | +1 | badAllowed = 0 +2 | stillBad = 0 + | ^^^^^^^^ N816 +3 | +4 | _badAllowed = 0 + | + +N816.py:5:1: N816 Variable `_stillBad` in global scope should not be mixedCase + | +4 | _badAllowed = 0 +5 | _stillBad = 0 + | ^^^^^^^^^ N816 +6 | +7 | bad_Allowed = 0 + | + +N816.py:8:1: N816 Variable `still_Bad` in global scope should not be mixedCase + | +7 | bad_Allowed = 0 +8 | still_Bad = 0 + | ^^^^^^^^^ N816 + | + + diff --git a/crates/ruff/src/settings/mod.rs b/crates/ruff/src/settings/mod.rs index b98c010dd4..fb5d6f8894 100644 --- a/crates/ruff/src/settings/mod.rs +++ b/crates/ruff/src/settings/mod.rs @@ -265,7 +265,8 @@ impl Settings { .unwrap_or_default(), pep8_naming: config .pep8_naming - .map(pep8_naming::settings::Settings::from) + .map(pep8_naming::settings::Settings::try_from) + .transpose()? .unwrap_or_default(), pycodestyle: config .pycodestyle diff --git a/crates/ruff/src/settings/types.rs b/crates/ruff/src/settings/types.rs index 2346d7506a..63e6376e93 100644 --- a/crates/ruff/src/settings/types.rs +++ b/crates/ruff/src/settings/types.rs @@ -249,3 +249,16 @@ impl Deref for Version { &self.0 } } + +/// Pattern to match an identifier. +/// +/// # Notes +/// +/// [`glob::Pattern`] matches a little differently than we ideally want to. +/// Specifically it uses `**` to match an arbitrary number of subdirectories, +/// luckily this not relevant since identifiers don't contains slashes. +/// +/// For reference pep8-naming uses +/// [`fnmatch`](https://docs.python.org/3.11/library/fnmatch.html) for +/// pattern matching. +pub type IdentifierPattern = glob::Pattern; diff --git a/crates/ruff_cache/Cargo.toml b/crates/ruff_cache/Cargo.toml index 89445ed6c8..b166bbf36a 100644 --- a/crates/ruff_cache/Cargo.toml +++ b/crates/ruff_cache/Cargo.toml @@ -12,6 +12,7 @@ license = { workspace = true } [dependencies] itertools = { workspace = true } +glob = { workspace = true } globset = { workspace = true } regex = { workspace = true } filetime = { workspace = true } diff --git a/crates/ruff_cache/src/cache_key.rs b/crates/ruff_cache/src/cache_key.rs index c05bf48ead..ee9669df09 100644 --- a/crates/ruff_cache/src/cache_key.rs +++ b/crates/ruff_cache/src/cache_key.rs @@ -5,6 +5,7 @@ use std::hash::{Hash, Hasher}; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; +use glob::Pattern; use itertools::Itertools; use regex::Regex; @@ -375,3 +376,9 @@ impl CacheKey for Regex { self.as_str().cache_key(state); } } + +impl CacheKey for Pattern { + fn cache_key(&self, state: &mut CacheKeyHasher) { + self.as_str().cache_key(state); + } +} diff --git a/ruff.schema.json b/ruff.schema.json index 4af020e35b..d7d091bb3f 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1397,7 +1397,7 @@ } }, "ignore-names": { - "description": "A list of names to ignore when considering `pep8-naming` violations.", + "description": "A list of names (or patterns) to ignore when considering `pep8-naming` violations.", "type": [ "array", "null" From 19f972a305eef81d8081c338e6d24125e41e4104 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 13 Jun 2023 11:59:53 -0400 Subject: [PATCH 034/447] Use `Scope#has` in lieu of `Scope#get` (#5051) ## Summary These usages don't actually need the `BindingId`. --- crates/ruff_python_semantic/src/model.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 0edd9451ca..e7bd442985 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -405,7 +405,7 @@ impl<'a> SemanticModel<'a> { if self .scopes() .take(scope_index) - .all(|scope| scope.get(name).is_none()) + .all(|scope| !scope.has(name)) { return Some(ImportedName { name: format!("{name}.{member}"), @@ -428,7 +428,7 @@ impl<'a> SemanticModel<'a> { if self .scopes() .take(scope_index) - .all(|scope| scope.get(name).is_none()) + .all(|scope| !scope.has(name)) { return Some(ImportedName { name: (*name).to_string(), @@ -449,7 +449,7 @@ impl<'a> SemanticModel<'a> { if self .scopes() .take(scope_index) - .all(|scope| scope.get(name).is_none()) + .all(|scope| !scope.has(name)) { return Some(ImportedName { name: format!("{name}.{member}"), From 099a9152d19869d6d70bcec445c3f605ea66cc19 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 13 Jun 2023 12:22:05 -0400 Subject: [PATCH 035/447] Use `.is_unbound()` in flake8-errmsg fix (#5053) ## Summary Trying to bring some more consistent to these APIs as I look to change them to accommodate deletions. --- .../rules/string_in_exception.rs | 213 ++++++++---------- 1 file changed, 99 insertions(+), 114 deletions(-) diff --git a/crates/ruff/src/rules/flake8_errmsg/rules/string_in_exception.rs b/crates/ruff/src/rules/flake8_errmsg/rules/string_in_exception.rs index 9078a9dab7..0641058bf9 100644 --- a/crates/ruff/src/rules/flake8_errmsg/rules/string_in_exception.rs +++ b/crates/ruff/src/rules/flake8_errmsg/rules/string_in_exception.rs @@ -172,6 +172,96 @@ impl Violation for DotFormatInException { } } +/// EM101, EM102, EM103 +pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr) { + if let Expr::Call(ast::ExprCall { args, .. }) = exc { + if let Some(first) = args.first() { + match first { + // Check for string literals. + Expr::Constant(ast::ExprConstant { + value: Constant::Str(string), + .. + }) => { + if checker.enabled(Rule::RawStringInException) { + if string.len() >= checker.settings.flake8_errmsg.max_string_length { + let mut diagnostic = + Diagnostic::new(RawStringInException, first.range()); + if checker.patch(diagnostic.kind.rule()) { + if let Some(indentation) = + whitespace::indentation(checker.locator, stmt) + { + if checker.semantic_model().is_unbound("msg") { + diagnostic.set_fix(generate_fix( + stmt, + first, + indentation, + checker.stylist, + checker.generator(), + )); + } + } + } + checker.diagnostics.push(diagnostic); + } + } + } + // Check for f-strings. + Expr::JoinedStr(_) => { + if checker.enabled(Rule::FStringInException) { + let mut diagnostic = Diagnostic::new(FStringInException, first.range()); + if checker.patch(diagnostic.kind.rule()) { + if let Some(indentation) = + whitespace::indentation(checker.locator, stmt) + { + if checker.semantic_model().is_unbound("msg") { + diagnostic.set_fix(generate_fix( + stmt, + first, + indentation, + checker.stylist, + checker.generator(), + )); + } + } + } + checker.diagnostics.push(diagnostic); + } + } + // Check for .format() calls. + Expr::Call(ast::ExprCall { func, .. }) => { + if checker.enabled(Rule::DotFormatInException) { + if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = + func.as_ref() + { + if attr == "format" && value.is_constant_expr() { + let mut diagnostic = + Diagnostic::new(DotFormatInException, first.range()); + if checker.patch(diagnostic.kind.rule()) { + if let Some(indentation) = + whitespace::indentation(checker.locator, stmt) + { + if checker.semantic_model().is_unbound("msg") { + diagnostic.set_fix(generate_fix( + stmt, + first, + indentation, + checker.stylist, + checker.generator(), + )); + } + } + } + checker.diagnostics.push(diagnostic); + } + } + } + } + _ => {} + } + } + } +} + /// Generate the [`Fix`] for EM001, EM002, and EM003 violations. /// /// This assumes that the violation is fixable and that the patch should @@ -189,24 +279,22 @@ fn generate_fix( stylist: &Stylist, generator: Generator, ) -> Fix { - let node = Expr::Name(ast::ExprName { - id: "msg".into(), - ctx: ExprContext::Store, - range: TextRange::default(), - }); - let node1 = Stmt::Assign(ast::StmtAssign { - targets: vec![node], + let assignment = Stmt::Assign(ast::StmtAssign { + targets: vec![Expr::Name(ast::ExprName { + id: "msg".into(), + ctx: ExprContext::Store, + range: TextRange::default(), + })], value: Box::new(exc_arg.clone()), type_comment: None, range: TextRange::default(), }); - let assignment = generator.stmt(&node1); - #[allow(deprecated)] - Fix::unspecified_edits( + + Fix::suggested_edits( Edit::insertion( format!( "{}{}{}", - assignment, + generator.stmt(&assignment), stylist.line_ending().as_str(), indentation, ), @@ -218,106 +306,3 @@ fn generate_fix( )], ) } - -/// EM101, EM102, EM103 -pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr) { - if let Expr::Call(ast::ExprCall { args, .. }) = exc { - if let Some(first) = args.first() { - match first { - // Check for string literals. - Expr::Constant(ast::ExprConstant { - value: Constant::Str(string), - .. - }) => { - if checker.enabled(Rule::RawStringInException) { - if string.len() >= checker.settings.flake8_errmsg.max_string_length { - let indentation = whitespace::indentation(checker.locator, stmt) - .and_then(|indentation| { - if checker.semantic_model().find_binding("msg").is_none() { - Some(indentation) - } else { - None - } - }); - let mut diagnostic = - Diagnostic::new(RawStringInException, first.range()); - if let Some(indentation) = indentation { - if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(generate_fix( - stmt, - first, - indentation, - checker.stylist, - checker.generator(), - )); - } - } - checker.diagnostics.push(diagnostic); - } - } - } - // Check for f-strings. - Expr::JoinedStr(_) => { - if checker.enabled(Rule::FStringInException) { - let indentation = whitespace::indentation(checker.locator, stmt).and_then( - |indentation| { - if checker.semantic_model().find_binding("msg").is_none() { - Some(indentation) - } else { - None - } - }, - ); - let mut diagnostic = Diagnostic::new(FStringInException, first.range()); - if let Some(indentation) = indentation { - if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(generate_fix( - stmt, - first, - indentation, - checker.stylist, - checker.generator(), - )); - } - } - checker.diagnostics.push(diagnostic); - } - } - // Check for .format() calls. - Expr::Call(ast::ExprCall { func, .. }) => { - if checker.enabled(Rule::DotFormatInException) { - if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = - func.as_ref() - { - if attr == "format" && value.is_constant_expr() { - let indentation = whitespace::indentation(checker.locator, stmt) - .and_then(|indentation| { - if checker.semantic_model().find_binding("msg").is_none() { - Some(indentation) - } else { - None - } - }); - let mut diagnostic = - Diagnostic::new(DotFormatInException, first.range()); - if let Some(indentation) = indentation { - if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(generate_fix( - stmt, - first, - indentation, - checker.stylist, - checker.generator(), - )); - } - } - checker.diagnostics.push(diagnostic); - } - } - } - } - _ => {} - } - } - } -} From a431dd03686e7d9e11a804d841605868f5b89bc1 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 13 Jun 2023 12:22:20 -0400 Subject: [PATCH 036/447] Respect all `__all__` definitions for docstring visibility (#5052) ## Summary We changed the semantics around `__all__` in #4885, but didn't update the docstring visibility code to match those changes. --- crates/ruff/src/checkers/ast/mod.rs | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index f269b58f6a..5bfadea96e 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -5038,21 +5038,26 @@ impl<'a> Checker<'a> { } // Compute visibility of all definitions. - let global_scope = self.semantic_model.global_scope(); - let exports: Option<&[&str]> = global_scope - .get("__all__") - .map(|binding_id| &self.semantic_model.bindings[binding_id]) - .and_then(|binding| match &binding.kind { - BindingKind::Export(Export { names }) => Some(names.as_slice()), - _ => None, - }); - let definitions = std::mem::take(&mut self.semantic_model.definitions); + let exports: Option> = { + let global_scope = self.semantic_model.global_scope(); + global_scope + .bindings_for_name("__all__") + .map(|binding_id| &self.semantic_model.bindings[binding_id]) + .filter_map(|binding| match &binding.kind { + BindingKind::Export(Export { names }) => Some(names.iter().copied()), + _ => None, + }) + .fold(None, |acc, names| { + Some(acc.into_iter().flatten().chain(names).collect()) + }) + }; + let definitions = std::mem::take(&mut self.semantic_model.definitions); let mut overloaded_name: Option = None; for ContextualizedDefinition { definition, visibility, - } in definitions.resolve(exports).iter() + } in definitions.resolve(exports.as_deref()).iter() { let docstring = docstrings::extraction::extract_docstring(definition); From b0984a2868d640aa25d6756c4e3dbacc6054240f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 13 Jun 2023 13:45:51 -0400 Subject: [PATCH 037/447] Treat exception binding as explicit deletion (#5057) ## Summary This PR corrects a misunderstanding I had related to Python's handling of bound exceptions. Previously, I thought this code ran without error: ```py def f(): x = 1 try: 1 / 0 except Exception as x: pass print(x) ``` My understanding was that `except Exception as x` bound `x` within the `except` block, but then restored the `x = 1` binding after exiting the block. In practice, however, this throws a `UnboundLocalError` error, because `x` becomes "unbound" after exiting the exception handler. It's similar to a `del` statement in this way. This PR removes our behavior to "restore" the previous binding. This could lead to faulty analysis in conditional blocks due to our lack of control flow analysis, but those same problems already exist for `del` statements. --- crates/ruff/src/checkers/ast/mod.rs | 6 ---- crates/ruff/src/rules/pyflakes/mod.rs | 14 ++++++++ ...__tests__print_after_shadowing_except.snap | 32 +++++++++++++++++++ 3 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_after_shadowing_except.snap diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 5bfadea96e..acce1cdd4c 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -3944,7 +3944,6 @@ where ); } - let definition = self.semantic_model.scope().get(name); self.handle_node_store( name, &Expr::Name(ast::ExprName { @@ -3979,11 +3978,6 @@ where } } } - - if let Some(binding_id) = definition { - let scope = self.semantic_model.scope_mut(); - scope.add(name, binding_id); - } } None => walk_excepthandler(self, excepthandler), } diff --git a/crates/ruff/src/rules/pyflakes/mod.rs b/crates/ruff/src/rules/pyflakes/mod.rs index 11c6414b7f..ba63fe6ef0 100644 --- a/crates/ruff/src/rules/pyflakes/mod.rs +++ b/crates/ruff/src/rules/pyflakes/mod.rs @@ -311,6 +311,20 @@ mod tests { "#, "del_shadowed_local_import_in_local_scope" )] + #[test_case( + r#" + def f(): + x = 1 + + try: + 1 / 0 + except Exception as x: + pass + + print(x) + "#, + "print_after_shadowing_except" + )] fn contents(contents: &str, snapshot: &str) { let diagnostics = test_snippet(contents, &Settings::for_rules(&Linter::Pyflakes)); assert_messages!(snapshot, diagnostics); diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_after_shadowing_except.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_after_shadowing_except.snap new file mode 100644 index 0000000000..70f280d1fd --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_after_shadowing_except.snap @@ -0,0 +1,32 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +:7:25: F841 [*] Local variable `x` is assigned to but never used + | +5 | try: +6 | 1 / 0 +7 | except Exception as x: + | ^ F841 +8 | pass + | + = help: Remove assignment to unused variable `x` + +ℹ Suggested fix +4 4 | +5 5 | try: +6 6 | 1 / 0 +7 |- except Exception as x: + 7 |+ except Exception: +8 8 | pass +9 9 | +10 10 | print(x) + +:10:11: F821 Undefined name `x` + | + 8 | pass + 9 | +10 | print(x) + | ^ F821 + | + + From f9f08d6b03ed7b954545129cb205e7f5b7799c92 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 13 Jun 2023 13:54:04 -0400 Subject: [PATCH 038/447] Add a few more tests for deletion behaviors (#5058) --- crates/ruff/src/rules/pyflakes/mod.rs | 31 +++++++++++++++++++ ...tests__augmented_assignment_after_del.snap | 21 +++++++++++++ ...shadowed_import_shadow_in_local_scope.snap | 29 +++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__augmented_assignment_after_del.snap create mode 100644 crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__del_shadowed_import_shadow_in_local_scope.snap diff --git a/crates/ruff/src/rules/pyflakes/mod.rs b/crates/ruff/src/rules/pyflakes/mod.rs index ba63fe6ef0..e29f54393c 100644 --- a/crates/ruff/src/rules/pyflakes/mod.rs +++ b/crates/ruff/src/rules/pyflakes/mod.rs @@ -311,6 +311,37 @@ mod tests { "#, "del_shadowed_local_import_in_local_scope" )] + #[test_case( + r#" + import os + + def f(): + os = 1 + print(os) + + del os + + def g(): + # `import os` should still be flagged as shadowing an import. + os = 1 + print(os) + "#, + "del_shadowed_import_shadow_in_local_scope" + )] + #[test_case( + r#" + x = 1 + + def foo(): + x = 2 + del x + # Flake8 treats this as an F823 error, because it removes the binding + # entirely after the `del` statement. However, it should be an F821 + # error, because the name is defined in the scope, but unbound. + x += 1 + "#, + "augmented_assignment_after_del" + )] #[test_case( r#" def f(): diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__augmented_assignment_after_del.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__augmented_assignment_after_del.snap new file mode 100644 index 0000000000..8e311ebc89 --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__augmented_assignment_after_del.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +:10:5: F823 Local variable `x` referenced before assignment + | + 8 | # entirely after the `del` statement. However, it should be an F821 + 9 | # error, because the name is defined in the scope, but unbound. +10 | x += 1 + | ^ F823 + | + +:10:5: F841 Local variable `x` is assigned to but never used + | + 8 | # entirely after the `del` statement. However, it should be an F821 + 9 | # error, because the name is defined in the scope, but unbound. +10 | x += 1 + | ^ F841 + | + = help: Remove assignment to unused variable `x` + + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__del_shadowed_import_shadow_in_local_scope.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__del_shadowed_import_shadow_in_local_scope.snap new file mode 100644 index 0000000000..d8e62a9c2e --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__del_shadowed_import_shadow_in_local_scope.snap @@ -0,0 +1,29 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +:2:8: F401 [*] `os` imported but unused + | +2 | import os + | ^^ F401 +3 | +4 | def f(): + | + = help: Remove unused import: `os` + +ℹ Fix +1 1 | +2 |-import os +3 2 | +4 3 | def f(): +5 4 | os = 1 + +:12:9: F811 Redefinition of unused `os` from line 2 + | +10 | def g(): +11 | # `import os` should still be flagged as shadowing an import. +12 | os = 1 + | ^^ F811 +13 | print(os) + | + + From 364bd82aee3d15733bec381f6359b64e976fcaba Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 13 Jun 2023 14:47:29 -0400 Subject: [PATCH 039/447] Don't treat annotations as resolved in forward references (#5060) ## Summary This behavior dates back to a Pyflakes commit (5fc37cbd), which was used to allow this test to pass: ```py from __future__ import annotations T: object def f(t: T): pass def g(t: 'T'): pass ``` But, I think this is an error. Mypy and Pyright don't accept it -- you can only use variables as type annotations if they're type aliases (i.e., annotated with `TypeAlias`), in which case, there has to be an assignment on the right-hand side (see: [PEP 613](https://peps.python.org/pep-0613/)). --- crates/ruff/src/rules/pyflakes/mod.rs | 24 ++++++++++++++++++++ crates/ruff_python_semantic/src/model.rs | 29 ++++++++++++------------ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/crates/ruff/src/rules/pyflakes/mod.rs b/crates/ruff/src/rules/pyflakes/mod.rs index e29f54393c..80e6fd868d 100644 --- a/crates/ruff/src/rules/pyflakes/mod.rs +++ b/crates/ruff/src/rules/pyflakes/mod.rs @@ -3190,6 +3190,20 @@ mod tests { r#" T: object def g(t: 'T'): pass + "#, + &[Rule::UndefinedName], + ); + flakes( + r#" + T = object + def f(t: T): pass + "#, + &[], + ); + flakes( + r#" + T = object + def g(t: 'T'): pass "#, &[], ); @@ -3381,6 +3395,16 @@ mod tests { T: object def f(t: T): pass def g(t: 'T'): pass + "#, + &[Rule::UndefinedName, Rule::UndefinedName], + ); + + flakes( + r#" + from __future__ import annotations + T = object + def f(t: T): pass + def g(t: 'T'): pass "#, &[], ); diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index e7bd442985..e347dfbe8a 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -177,20 +177,22 @@ impl<'a> SemanticModel<'a> { // should prefer it over local resolutions. if self.in_forward_reference() { if let Some(binding_id) = self.scopes.global().get(symbol) { - // Mark the binding as used. - let context = self.execution_context(); - let reference_id = self.references.push(ScopeId::global(), range, context); - self.bindings[binding_id].references.push(reference_id); - - // Mark any submodule aliases as used. - if let Some(binding_id) = - self.resolve_submodule(symbol, ScopeId::global(), binding_id) - { + if !self.bindings[binding_id].kind.is_annotation() { + // Mark the binding as used. + let context = self.execution_context(); let reference_id = self.references.push(ScopeId::global(), range, context); self.bindings[binding_id].references.push(reference_id); - } - return ResolvedRead::Resolved(binding_id); + // Mark any submodule aliases as used. + if let Some(binding_id) = + self.resolve_submodule(symbol, ScopeId::global(), binding_id) + { + let reference_id = self.references.push(ScopeId::global(), range, context); + self.bindings[binding_id].references.push(reference_id); + } + + return ResolvedRead::Resolved(binding_id); + } } } @@ -226,8 +228,7 @@ impl<'a> SemanticModel<'a> { self.bindings[binding_id].references.push(reference_id); } - // But if it's a type annotation, don't treat it as resolved, unless we're in a - // forward reference. For example, given: + // But if it's a type annotation, don't treat it as resolved. For example, given: // // ```python // name: str @@ -236,7 +237,7 @@ impl<'a> SemanticModel<'a> { // // The `name` in `print(name)` should be treated as unresolved, but the `name` in // `name: str` should be treated as used. - if !self.in_forward_reference() && self.bindings[binding_id].kind.is_annotation() { + if self.bindings[binding_id].kind.is_annotation() { continue; } From 1895011ac26278c35920bc591ba34147d3b72ed0 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 13 Jun 2023 16:45:24 -0400 Subject: [PATCH 040/447] Document some attributes on the semantic model (#5064) --- crates/ruff_python_semantic/src/model.rs | 56 +++++++++++++++++------- crates/ruff_python_semantic/src/scope.rs | 18 ++++++++ 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index e347dfbe8a..4142833083 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -25,34 +25,60 @@ use crate::scope::{Scope, ScopeId, ScopeKind, Scopes}; /// A semantic model for a Python module, to enable querying the module's semantic information. pub struct SemanticModel<'a> { - pub typing_modules: &'a [String], - pub module_path: Option<&'a [String]>, - // Stack of all visited statements, along with the identifier of the current statement. + typing_modules: &'a [String], + module_path: Option<&'a [String]>, + + /// Stack of all visited statements. pub stmts: Nodes<'a>, - pub stmt_id: Option, - // Stack of current expressions. - pub exprs: Vec<&'a Expr>, - // Stack of all scopes, along with the identifier of the current scope. + + /// The identifier of the current statement. + stmt_id: Option, + + /// Stack of current expressions. + exprs: Vec<&'a Expr>, + + /// Stack of all scopes, along with the identifier of the current scope. pub scopes: Scopes<'a>, pub scope_id: ScopeId, pub dead_scopes: Vec, - // Stack of all definitions created in any scope, at any point in execution, along with the - // identifier of the current definition. + + /// Stack of all definitions created in any scope, at any point in execution. pub definitions: Definitions<'a>, + + /// The ID of the current definition. pub definition_id: DefinitionId, - // A stack of all bindings created in any scope, at any point in execution. + + /// A stack of all bindings created in any scope, at any point in execution. pub bindings: Bindings<'a>, - // Stack of all references created in any scope, at any point in execution. + + /// Stack of all references created in any scope, at any point in execution. references: References, - // Arena of global bindings. + + /// Arena of global bindings. globals: GlobalsArena<'a>, - // Map from binding index to indexes of bindings that shadow it in other scopes. + + /// Map from binding ID to binding ID that it shadows (in another scope). + /// + /// For example: + /// ```python + /// import x + /// + /// def f(): + /// x = 1 + /// ``` + /// + /// In this case, the binding created by `x = 1` shadows the binding created by `import x`, + /// despite the fact that they're in different scopes. pub shadowed_bindings: HashMap>, - // Body iteration; used to peek at siblings. + + /// Body iteration; used to peek at siblings. pub body: &'a [Stmt], pub body_index: usize, - // Internal, derivative state. + + /// Flags for the semantic model. pub flags: SemanticModelFlags, + + /// Exceptions that have been handled by the current scope. pub handled_exceptions: Vec, } diff --git a/crates/ruff_python_semantic/src/scope.rs b/crates/ruff_python_semantic/src/scope.rs index e56d4dafa8..f1e9107844 100644 --- a/crates/ruff_python_semantic/src/scope.rs +++ b/crates/ruff_python_semantic/src/scope.rs @@ -13,19 +13,37 @@ use crate::globals::GlobalsId; #[derive(Debug)] pub struct Scope<'a> { + /// The kind of scope. pub kind: ScopeKind<'a>, + + /// The parent scope, if any. pub parent: Option, + /// A list of star imports in this scope. These represent _module_ imports (e.g., `sys` in /// `from sys import *`), rather than individual bindings (e.g., individual members in `sys`). star_imports: Vec>, + /// A map from bound name to binding ID. bindings: FxHashMap<&'a str, BindingId>, + /// A map from binding ID to binding ID that it shadows. + /// + /// For example: + /// ```python + /// def f(): + /// x = 1 + /// x = 2 + /// ``` + /// + /// In this case, the binding created by `x = 2` shadows the binding created by `x = 1`. shadowed_bindings: HashMap>, + /// A list of all names that have been deleted in this scope. deleted_symbols: Vec<&'a str>, + /// Index into the globals arena, if the scope contains any globally-declared symbols. globals_id: Option, + /// Flags for the [`Scope`]. flags: ScopeFlags, } From c2fa568b461998116e74b85361404f1368b6260b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 13 Jun 2023 18:37:06 -0400 Subject: [PATCH 041/447] Use dedicated structs for excepthandler variants (#5065) ## Summary Oversight from #5042. --- crates/ruff_python_ast/src/comparable.rs | 91 ++++++++++++------------ 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/crates/ruff_python_ast/src/comparable.rs b/crates/ruff_python_ast/src/comparable.rs index 94e902814b..79f197c74c 100644 --- a/crates/ruff_python_ast/src/comparable.rs +++ b/crates/ruff_python_ast/src/comparable.rs @@ -27,8 +27,8 @@ pub enum ComparableBoolop { Or, } -impl From<&ast::Boolop> for ComparableBoolop { - fn from(op: &ast::Boolop) -> Self { +impl From for ComparableBoolop { + fn from(op: ast::Boolop) -> Self { match op { ast::Boolop::And => Self::And, ast::Boolop::Or => Self::Or, @@ -53,8 +53,8 @@ pub enum ComparableOperator { FloorDiv, } -impl From<&ast::Operator> for ComparableOperator { - fn from(op: &ast::Operator) -> Self { +impl From for ComparableOperator { + fn from(op: ast::Operator) -> Self { match op { ast::Operator::Add => Self::Add, ast::Operator::Sub => Self::Sub, @@ -81,8 +81,8 @@ pub enum ComparableUnaryop { USub, } -impl From<&ast::Unaryop> for ComparableUnaryop { - fn from(op: &ast::Unaryop) -> Self { +impl From for ComparableUnaryop { + fn from(op: ast::Unaryop) -> Self { match op { ast::Unaryop::Invert => Self::Invert, ast::Unaryop::Not => Self::Not, @@ -106,8 +106,8 @@ pub enum ComparableCmpop { NotIn, } -impl From<&ast::Cmpop> for ComparableCmpop { - fn from(op: &ast::Cmpop) -> Self { +impl From for ComparableCmpop { + fn from(op: ast::Cmpop) -> Self { match op { ast::Cmpop::Eq => Self::Eq, ast::Cmpop::NotEq => Self::NotEq, @@ -125,8 +125,8 @@ impl From<&ast::Cmpop> for ComparableCmpop { #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableAlias<'a> { - pub name: &'a str, - pub asname: Option<&'a str>, + name: &'a str, + asname: Option<&'a str>, } impl<'a> From<&'a ast::Alias> for ComparableAlias<'a> { @@ -140,8 +140,8 @@ impl<'a> From<&'a ast::Alias> for ComparableAlias<'a> { #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableWithitem<'a> { - pub context_expr: ComparableExpr<'a>, - pub optional_vars: Option>, + context_expr: ComparableExpr<'a>, + optional_vars: Option>, } impl<'a> From<&'a ast::Withitem> for ComparableWithitem<'a> { @@ -280,9 +280,9 @@ impl<'a> From<&'a Box> for Box> { #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableMatchCase<'a> { - pub pattern: ComparablePattern<'a>, - pub guard: Option>, - pub body: Vec>, + pattern: ComparablePattern<'a>, + guard: Option>, + body: Vec>, } impl<'a> From<&'a ast::MatchCase> for ComparableMatchCase<'a> { @@ -297,7 +297,7 @@ impl<'a> From<&'a ast::MatchCase> for ComparableMatchCase<'a> { #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableDecorator<'a> { - pub expression: ComparableExpr<'a>, + expression: ComparableExpr<'a>, } impl<'a> From<&'a ast::Decorator> for ComparableDecorator<'a> { @@ -342,13 +342,13 @@ impl<'a> From<&'a ast::Constant> for ComparableConstant<'a> { #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableArguments<'a> { - pub posonlyargs: Vec>, - pub args: Vec>, - pub vararg: Option>, - pub kwonlyargs: Vec>, - pub kw_defaults: Vec>, - pub kwarg: Option>, - pub defaults: Vec>, + posonlyargs: Vec>, + args: Vec>, + vararg: Option>, + kwonlyargs: Vec>, + kw_defaults: Vec>, + kwarg: Option>, + defaults: Vec>, } impl<'a> From<&'a ast::Arguments> for ComparableArguments<'a> { @@ -379,9 +379,9 @@ impl<'a> From<&'a Box> for ComparableArg<'a> { #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableArg<'a> { - pub arg: &'a str, - pub annotation: Option>>, - pub type_comment: Option<&'a str>, + arg: &'a str, + annotation: Option>>, + type_comment: Option<&'a str>, } impl<'a> From<&'a ast::Arg> for ComparableArg<'a> { @@ -396,8 +396,8 @@ impl<'a> From<&'a ast::Arg> for ComparableArg<'a> { #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableKeyword<'a> { - pub arg: Option<&'a str>, - pub value: ComparableExpr<'a>, + arg: Option<&'a str>, + value: ComparableExpr<'a>, } impl<'a> From<&'a ast::Keyword> for ComparableKeyword<'a> { @@ -411,10 +411,10 @@ impl<'a> From<&'a ast::Keyword> for ComparableKeyword<'a> { #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableComprehension<'a> { - pub target: ComparableExpr<'a>, - pub iter: ComparableExpr<'a>, - pub ifs: Vec>, - pub is_async: bool, + target: ComparableExpr<'a>, + iter: ComparableExpr<'a>, + ifs: Vec>, + is_async: bool, } impl<'a> From<&'a ast::Comprehension> for ComparableComprehension<'a> { @@ -428,13 +428,16 @@ impl<'a> From<&'a ast::Comprehension> for ComparableComprehension<'a> { } } +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExcepthandlerExceptHandler<'a> { + type_: Option>>, + name: Option<&'a str>, + body: Vec>, +} + #[derive(Debug, PartialEq, Eq, Hash)] pub enum ComparableExcepthandler<'a> { - ExceptHandler { - type_: Option>, - name: Option<&'a str>, - body: Vec>, - }, + ExceptHandler(ExcepthandlerExceptHandler<'a>), } impl<'a> From<&'a ast::Excepthandler> for ComparableExcepthandler<'a> { @@ -445,11 +448,11 @@ impl<'a> From<&'a ast::Excepthandler> for ComparableExcepthandler<'a> { body, .. }) = excepthandler; - Self::ExceptHandler { + Self::ExceptHandler(ExcepthandlerExceptHandler { type_: type_.as_ref().map(Into::into), name: name.as_deref(), body: body.iter().map(Into::into).collect(), - } + }) } } @@ -670,7 +673,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { values, range: _range, }) => Self::BoolOp(ExprBoolOp { - op: op.into(), + op: (*op).into(), values: values.iter().map(Into::into).collect(), }), ast::Expr::NamedExpr(ast::ExprNamedExpr { @@ -688,7 +691,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { range: _range, }) => Self::BinOp(ExprBinOp { left: left.into(), - op: op.into(), + op: (*op).into(), right: right.into(), }), ast::Expr::UnaryOp(ast::ExprUnaryOp { @@ -696,7 +699,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { operand, range: _range, }) => Self::UnaryOp(ExprUnaryOp { - op: op.into(), + op: (*op).into(), operand: operand.into(), }), ast::Expr::Lambda(ast::ExprLambda { @@ -793,7 +796,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { range: _range, }) => Self::Compare(ExprCompare { left: left.into(), - ops: ops.iter().map(Into::into).collect(), + ops: ops.iter().copied().map(Into::into).collect(), comparators: comparators.iter().map(Into::into).collect(), }), ast::Expr::Call(ast::ExprCall { @@ -1173,7 +1176,7 @@ impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> { range: _range, }) => Self::AugAssign(StmtAugAssign { target: target.into(), - op: op.into(), + op: (*op).into(), value: value.into(), }), ast::Stmt::AnnAssign(ast::StmtAnnAssign { From 3f6584b74fc8870119477ed01c7a4244ae3db964 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 13 Jun 2023 20:01:52 -0400 Subject: [PATCH 042/447] Fix erroneous kwarg reference (#5068) --- crates/ruff_python_ast/src/comparable.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_python_ast/src/comparable.rs b/crates/ruff_python_ast/src/comparable.rs index 79f197c74c..b69f6f31a0 100644 --- a/crates/ruff_python_ast/src/comparable.rs +++ b/crates/ruff_python_ast/src/comparable.rs @@ -359,7 +359,7 @@ impl<'a> From<&'a ast::Arguments> for ComparableArguments<'a> { vararg: arguments.vararg.as_ref().map(Into::into), kwonlyargs: arguments.kwonlyargs.iter().map(Into::into).collect(), kw_defaults: arguments.kw_defaults.iter().map(Into::into).collect(), - kwarg: arguments.vararg.as_ref().map(Into::into), + kwarg: arguments.kwarg.as_ref().map(Into::into), defaults: arguments.defaults.iter().map(Into::into).collect(), } } From 0daeea1f425cc9f767d164c6154503a0427b2fe6 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 13 Jun 2023 21:00:42 -0400 Subject: [PATCH 043/447] Tweak exception-handler handling in AST visitor (#5069) --- crates/ruff/src/checkers/ast/mod.rs | 49 +++++++------------ ...ules__pyflakes__tests__F841_F841_0.py.snap | 2 +- ...ules__pyflakes__tests__F841_F841_3.py.snap | 4 +- ...lakes__tests__f841_dummy_variable_rgx.snap | 2 +- ...__tests__print_after_shadowing_except.snap | 2 +- 5 files changed, 23 insertions(+), 36 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index acce1cdd4c..2231a1207d 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -9,7 +9,7 @@ use rustpython_parser::ast::{ Operator, Pattern, Ranged, Stmt, Suite, Unaryop, }; -use ruff_diagnostics::{Diagnostic, IsolationLevel}; +use ruff_diagnostics::{Diagnostic, Fix, IsolationLevel}; use ruff_python_ast::all::{extract_all_names, AllNamesFlags}; use ruff_python_ast::helpers::{extract_handled_exceptions, to_module_path}; use ruff_python_ast::source_code::{Generator, Indexer, Locator, Quote, Stylist}; @@ -3912,12 +3912,13 @@ where } match name { Some(name) => { + let range = helpers::excepthandler_name_range(excepthandler, self.locator) + .expect("Failed to find `name` range"); + if self.enabled(Rule::AmbiguousVariableName) { - if let Some(diagnostic) = pycodestyle::rules::ambiguous_variable_name( - name, - helpers::excepthandler_name_range(excepthandler, self.locator) - .expect("Failed to find `name` range"), - ) { + if let Some(diagnostic) = + pycodestyle::rules::ambiguous_variable_name(name, range) + { self.diagnostics.push(diagnostic); } } @@ -3930,31 +3931,17 @@ where ); } - let name_range = - helpers::excepthandler_name_range(excepthandler, self.locator).unwrap(); - - if self.semantic_model.scope().has(name) { - self.handle_node_store( - name, - &Expr::Name(ast::ExprName { - id: name.into(), - ctx: ExprContext::Store, - range: name_range, - }), - ); - } - - self.handle_node_store( + // Add the bound exception name to the scope. + self.add_binding( name, - &Expr::Name(ast::ExprName { - id: name.into(), - ctx: ExprContext::Store, - range: name_range, - }), + range, + BindingKind::Assignment, + BindingFlags::empty(), ); walk_excepthandler(self, excepthandler); + // Remove it from the scope immediately after. if let Some(binding_id) = { let scope = self.semantic_model.scope_mut(); scope.delete(name) @@ -3963,15 +3950,15 @@ where if self.enabled(Rule::UnusedVariable) { let mut diagnostic = Diagnostic::new( pyflakes::rules::UnusedVariable { name: name.into() }, - name_range, + range, ); if self.patch(Rule::UnusedVariable) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { - pyflakes::fixes::remove_exception_handler_assignment( + diagnostic.try_set_fix(|| { + let edit = pyflakes::fixes::remove_exception_handler_assignment( excepthandler, self.locator, - ) + )?; + Ok(Fix::automatic(edit)) }); } self.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_0.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_0.py.snap index b5faa05575..d6ecba5159 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_0.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_0.py.snap @@ -11,7 +11,7 @@ F841_0.py:3:22: F841 [*] Local variable `e` is assigned to but never used | = help: Remove assignment to unused variable `e` -ℹ Suggested fix +ℹ Fix 1 1 | try: 2 2 | 1 / 0 3 |-except ValueError as e: diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_3.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_3.py.snap index c2d8095edb..fbf428003b 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_3.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_3.py.snap @@ -225,7 +225,7 @@ F841_3.py:40:26: F841 [*] Local variable `x1` is assigned to but never used | = help: Remove assignment to unused variable `x1` -ℹ Suggested fix +ℹ Fix 37 37 | def f(): 38 38 | try: 39 39 | 1 / 0 @@ -245,7 +245,7 @@ F841_3.py:45:47: F841 [*] Local variable `x2` is assigned to but never used | = help: Remove assignment to unused variable `x2` -ℹ Suggested fix +ℹ Fix 42 42 | 43 43 | try: 44 44 | 1 / 0 diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__f841_dummy_variable_rgx.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__f841_dummy_variable_rgx.snap index e7f674a510..ff53df87e1 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__f841_dummy_variable_rgx.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__f841_dummy_variable_rgx.snap @@ -11,7 +11,7 @@ F841_0.py:3:22: F841 [*] Local variable `e` is assigned to but never used | = help: Remove assignment to unused variable `e` -ℹ Suggested fix +ℹ Fix 1 1 | try: 2 2 | 1 / 0 3 |-except ValueError as e: diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_after_shadowing_except.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_after_shadowing_except.snap index 70f280d1fd..1bc5062f45 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_after_shadowing_except.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_after_shadowing_except.snap @@ -11,7 +11,7 @@ source: crates/ruff/src/rules/pyflakes/mod.rs | = help: Remove assignment to unused variable `x` -ℹ Suggested fix +ℹ Fix 4 4 | 5 5 | try: 6 6 | 1 / 0 From 4d9b0b925d49ab67584a19a5053a9b1372208d7e Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Wed, 14 Jun 2023 02:31:06 +0100 Subject: [PATCH 044/447] Add documentation to `flake8-executable` rules (#5063) ## Summary Completes the documentation for the `flake8-executable` rules. Related to #2646. ## Test Plan `python scripts/check_docs_formatted.py` --- .../rules/shebang_missing.rs | 18 +++++++++++++ .../rules/shebang_newline.rs | 26 +++++++++++++++++++ .../rules/shebang_not_executable.rs | 19 ++++++++++++++ .../flake8_executable/rules/shebang_python.rs | 25 ++++++++++++++++++ .../rules/shebang_whitespace.rs | 24 +++++++++++++++++ scripts/check_docs_formatted.py | 1 + 6 files changed, 113 insertions(+) diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_missing.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_missing.rs index ea33e57d0c..7cf9daed31 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_missing.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_missing.rs @@ -11,6 +11,24 @@ use crate::registry::AsRule; #[cfg(target_family = "unix")] use crate::rules::flake8_executable::helpers::is_executable; +/// ## What it does +/// Checks for executable `.py` files that do not have a shebang. +/// +/// ## Why is this bad? +/// In Python, a shebang (also known as a hashbang) is the first line of a +/// script, which specifies the interpreter that should be used to run the +/// script. +/// +/// If a `.py` file is executable, but does not have a shebang, it may be run +/// with the wrong interpreter, or fail to run at all. +/// +/// If the file is meant to be executable, add a shebang; otherwise, remove the +/// executable bit from the file. +/// +/// _This rule is only available on Unix-like systems._ +/// +/// ## References +/// - [Python documentation: Executable Python Scripts](https://docs.python.org/3/tutorial/appendix.html#executable-python-scripts) #[violation] pub struct ShebangMissingExecutableFile; diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs index 9c605ba940..322c68be19 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs @@ -5,6 +5,32 @@ use ruff_macros::{derive_message_formats, violation}; use crate::rules::flake8_executable::helpers::ShebangDirective; +/// ## What it does +/// Checks for a shebang directive that is not at the beginning of the file. +/// +/// ## Why is this bad? +/// In Python, a shebang (also known as a hashbang) is the first line of a +/// script, which specifies the interpreter that should be used to run the +/// script. +/// +/// The shebang's `#!` prefix must be the first two characters of a file. If +/// the shebang is not at the beginning of the file, it will be ignored, which +/// is likely a mistake. +/// +/// ## Example +/// ```python +/// foo = 1 +/// #!/usr/bin/env python3 +/// ``` +/// +/// Use instead: +/// ```python +/// #!/usr/bin/env python3 +/// foo = 1 +/// ``` +/// +/// ## References +/// - [Python documentation: Executable Python Scripts](https://docs.python.org/3/tutorial/appendix.html#executable-python-scripts) #[violation] pub struct ShebangNotFirstLine; diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs index 6cfcc13887..20c542d0d4 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs @@ -12,6 +12,25 @@ use crate::registry::AsRule; use crate::rules::flake8_executable::helpers::is_executable; use crate::rules::flake8_executable::helpers::ShebangDirective; +/// ## What it does +/// Checks for a shebang directive in a file that is not executable. +/// +/// ## Why is this bad? +/// In Python, a shebang (also known as a hashbang) is the first line of a +/// script, which specifies the interpreter that should be used to run the +/// script. +/// +/// The presence of a shebang suggests that a file is intended to be +/// executable. If a file contains a shebang but is not executable, then the +/// shebang is misleading, or the file is missing the executable bit. +/// +/// If the file is meant to be executable, add a shebang; otherwise, remove the +/// executable bit from the file. +/// +/// _This rule is only available on Unix-like systems._ +/// +/// ## References +/// - [Python documentation: Executable Python Scripts](https://docs.python.org/3/tutorial/appendix.html#executable-python-scripts) #[violation] pub struct ShebangNotExecutable; diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs index 73f3d1f333..b68eb42be9 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs @@ -5,6 +5,31 @@ use ruff_macros::{derive_message_formats, violation}; use crate::rules::flake8_executable::helpers::ShebangDirective; +/// ## What it does +/// Checks for a shebang directive in `.py` files that does not contain `python`. +/// +/// ## Why is this bad? +/// In Python, a shebang (also known as a hashbang) is the first line of a +/// script, which specifies the interpreter that should be used to run the +/// script. +/// +/// For Python scripts, the shebang must contain `python` to indicate that the +/// script should be executed as a Python script. If the shebang does not +/// contain `python`, then the file will be executed with the default +/// interpreter, which is likely a mistake. +/// +/// ## Example +/// ```python +/// #!/usr/bin/env bash +/// ``` +/// +/// Use instead: +/// ```python +/// #!/usr/bin/env python3 +/// ``` +/// +/// ## References +/// - [Python documentation: Executable Python Scripts](https://docs.python.org/3/tutorial/appendix.html#executable-python-scripts) #[violation] pub struct ShebangMissingPython; diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs index 0458d61432..f731033924 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs @@ -5,6 +5,30 @@ use ruff_macros::{derive_message_formats, violation}; use crate::rules::flake8_executable::helpers::ShebangDirective; +/// ## What it does +/// Checks for whitespace before a shebang directive. +/// +/// ## Why is this bad? +/// In Python, a shebang (also known as a hashbang) is the first line of a +/// script, which specifies the interpreter that should be used to run the +/// script. +/// +/// The shebang's `#!` prefix must be the first two characters of a file. The +/// presence of whitespace before the shebang will cause the shebang to be +/// ignored, which is likely a mistake. +/// +/// ## Example +/// ```python +/// #!/usr/bin/env python3 +/// ``` +/// +/// Use instead: +/// ```python +/// #!/usr/bin/env python3 +/// ``` +/// +/// ## References +/// - [Python documentation: Executable Python Scripts](https://docs.python.org/3/tutorial/appendix.html#executable-python-scripts) #[violation] pub struct ShebangLeadingWhitespace; diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index 81752ac742..ef5ec4fc08 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -44,6 +44,7 @@ KNOWN_FORMATTING_VIOLATIONS = [ "no-space-after-inline-comment", "over-indented", "prohibited-trailing-comma", + "shebang-leading-whitespace", "too-few-spaces-before-inline-comment", "trailing-comma-on-bare-tuple", "unexpected-indentation-comment", From fc6580592d526bef13252c6611519d4a8d20fc6c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 14 Jun 2023 00:02:39 -0400 Subject: [PATCH 045/447] Use Expr::is_* methods at more call sites (#5075) --- .../src/rules/pyflakes/rules/starred_expressions.rs | 2 +- crates/ruff/src/rules/pyflakes/rules/strings.rs | 13 +++++-------- crates/ruff/src/rules/pylint/rules/logging.rs | 2 +- .../rules/pyupgrade/rules/use_pep604_isinstance.rs | 5 ++--- .../src/rules/ruff/rules/pairwise_over_zipped.rs | 2 +- crates/ruff_python_ast/src/helpers.rs | 6 +++--- 6 files changed, 13 insertions(+), 17 deletions(-) diff --git a/crates/ruff/src/rules/pyflakes/rules/starred_expressions.rs b/crates/ruff/src/rules/pyflakes/rules/starred_expressions.rs index d4b3a875fa..723d2dcc31 100644 --- a/crates/ruff/src/rules/pyflakes/rules/starred_expressions.rs +++ b/crates/ruff/src/rules/pyflakes/rules/starred_expressions.rs @@ -59,7 +59,7 @@ pub(crate) fn starred_expressions( let mut has_starred: bool = false; let mut starred_index: Option = None; for (index, elt) in elts.iter().enumerate() { - if matches!(elt, Expr::Starred(_)) { + if elt.is_starred_expr() { if has_starred && check_two_starred_expressions { return Some(Diagnostic::new(MultipleStarredExpressions, location)); } diff --git a/crates/ruff/src/rules/pyflakes/rules/strings.rs b/crates/ruff/src/rules/pyflakes/rules/strings.rs index a89a6b602a..a6abde308a 100644 --- a/crates/ruff/src/rules/pyflakes/rules/strings.rs +++ b/crates/ruff/src/rules/pyflakes/rules/strings.rs @@ -514,14 +514,13 @@ impl Violation for StringDotFormatMixingAutomatic { } fn has_star_star_kwargs(keywords: &[Keyword]) -> bool { - keywords.iter().any(|k| { - let Keyword { arg, .. } = &k; - arg.is_none() - }) + keywords + .iter() + .any(|keyword| matches!(keyword, Keyword { arg: None, .. })) } fn has_star_args(args: &[Expr]) -> bool { - args.iter().any(|arg| matches!(&arg, Expr::Starred(_))) + args.iter().any(Expr::is_starred_expr) } /// F502 @@ -805,9 +804,7 @@ pub(crate) fn string_dot_format_extra_positional_arguments( .iter() .enumerate() .filter(|(i, arg)| { - !(matches!(arg, Expr::Starred(_)) - || summary.autos.contains(i) - || summary.indices.contains(i)) + !(arg.is_starred_expr() || summary.autos.contains(i) || summary.indices.contains(i)) }) .map(|(i, _)| i) .collect(); diff --git a/crates/ruff/src/rules/pylint/rules/logging.rs b/crates/ruff/src/rules/pylint/rules/logging.rs index 2cf9930caa..6ddffccb20 100644 --- a/crates/ruff/src/rules/pylint/rules/logging.rs +++ b/crates/ruff/src/rules/pylint/rules/logging.rs @@ -93,7 +93,7 @@ pub(crate) fn logging_call( keywords: &[Keyword], ) { // If there are any starred arguments, abort. - if args.iter().any(|arg| matches!(arg, Expr::Starred(_))) { + if args.iter().any(Expr::is_starred_expr) { return; } diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs index d0f8baaf92..89254e9145 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs @@ -85,14 +85,13 @@ pub(crate) fn use_pep604_isinstance( } // Ex) `(*args,)` - if elts.iter().any(|elt| matches!(elt, Expr::Starred(_))) { + if elts.iter().any(Expr::is_starred_expr) { return; } let mut diagnostic = Diagnostic::new(NonPEP604Isinstance { kind }, expr.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&union(elts)), types.range(), ))); 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 99c2286a75..ee3c5b0d0c 100644 --- a/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs +++ b/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs @@ -115,7 +115,7 @@ pub(crate) fn pairwise_over_zipped(checker: &mut Checker, func: &Expr, args: &[E }; // Require second argument to be a `Subscript`. - if !matches!(&args[1], Expr::Subscript(_)) { + if !args[1].is_subscript_expr() { return; } let Some(second_arg_info) = match_slice_info(&args[1]) else { diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index a205e027e1..8c3e08774a 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -1353,7 +1353,7 @@ impl<'a> SimpleCallArgs<'a> { ) -> Self { let args = args .into_iter() - .take_while(|arg| !matches!(arg, Expr::Starred(_))) + .take_while(|arg| !arg.is_starred_expr()) .collect(); let kwargs = keywords @@ -1404,7 +1404,7 @@ pub fn on_conditional_branch<'a>(parents: &mut impl Iterator) - range: _range, }) = parent { - if matches!(value.as_ref(), Expr::IfExp(_)) { + if value.is_if_exp_expr() { return true; } } @@ -1427,7 +1427,7 @@ pub fn is_unpacking_assignment(parent: &Stmt, child: &Expr) -> bool { match parent { Stmt::With(ast::StmtWith { items, .. }) => items.iter().any(|item| { if let Some(optional_vars) = &item.optional_vars { - if matches!(optional_vars.as_ref(), Expr::Tuple(_)) { + if optional_vars.is_tuple_expr() { if any_over_expr(optional_vars, &|expr| expr == child) { return true; } From bf5fbf89714710ba5ad978a32e849c4c88bd2cbd Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Wed, 14 Jun 2023 10:51:59 +0530 Subject: [PATCH 046/447] Add GitHub CODEOWNERS file (#5054) ## Summary Add GitHub CODEOWNERS file. Initiating this, we can discuss and iterate further. https://help.github.com/articles/about-codeowners/ ## Test Plan Look out for review requests :) --- .github/CODEOWNERS | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..9cd29adcda --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,9 @@ +# GitHub code owners file. For more info: https://help.github.com/articles/about-codeowners/ +# +# - Comment lines begin with `#` character. +# - Each line is a file pattern followed by one or more owners. +# - The '*' pattern is global owners. +# - Order is important. The last matching pattern has the most precedence. + +# Jupyter +/crates/ruff/src/jupyter/ @dhruvmanila From aa41ffcfde55882554820e82387af4f04fc1be76 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 14 Jun 2023 09:27:24 -0400 Subject: [PATCH 047/447] Add `BindingKind` variants to represent deleted bindings (#5071) ## Summary Our current mechanism for handling deletions (e.g., `del x`) is to remove the symbol from the scope's `bindings` table. This "does the right thing", in that if we then reference a deleted symbol, we're able to determine that it's unbound -- but it causes a variety of problems, mostly in that it makes certain bindings and references unreachable after-the-fact. Consider: ```python x = 1 print(x) del x ``` If we analyze this code _after_ running the semantic model over the AST, we'll have no way of knowing that `x` was ever introduced in the scope, much less that it was bound to a value, read, and then deleted -- because we effectively erased `x` from the model entirely when we hit the deletion. In practice, this will make it impossible for us to support local symbol renames. It also means that certain rules that we want to move out of the model-building phase and into the "check dead scopes" phase wouldn't work today, since we'll have lost important information about the source code. This PR introduces two new `BindingKind` variants to model deletions: - `BindingKind::Deletion`, which represents `x = 1; del x`. - `BindingKind::UnboundException`, which represents: ```python try: 1 / 0 except Exception as e: pass ``` In the latter case, `e` gets unbound after the exception handler (assuming it's triggered), so we want to handle it similarly to a deletion. The main challenge here is auditing all of our existing `Binding` and `Scope` usages to understand whether they need to accommodate deletions or otherwise behave differently. If you look one commit back on this branch, you'll see that the code is littered with `NOTE(charlie)` comments that describe the reasoning behind changing (or not) each of those call sites. I've also augmented our test suite in preparation for this change over a few prior PRs. ### Alternatives As an alternative, I considered introducing a flag to `BindingFlags`, like `BindingFlags::UNBOUND`, and setting that at the appropriate time. This turned out to be a much more difficult change, because we tend to match on `BindingKind` all over the place (e.g., we have a bunch of code blocks that only run when a `BindingKind` is `BindingKind::Importation`). As a result, introducing these new `BindingKind` variants requires only a few changes at the client sites. Adding a flag would've required a much wider-reaching change. --- crates/ruff/src/checkers/ast/mod.rs | 116 +++++++++++------- crates/ruff/src/importer/mod.rs | 6 +- .../rules/string_in_exception.rs | 6 +- crates/ruff/src/rules/pyflakes/mod.rs | 9 ++ ...tests__augmented_assignment_after_del.snap | 4 +- ...f__rules__pyflakes__tests__double_del.snap | 12 ++ .../src/rules/pyupgrade/rules/open_alias.rs | 5 +- crates/ruff_python_ast/src/helpers.rs | 2 +- crates/ruff_python_semantic/src/binding.rs | 38 +++++- crates/ruff_python_semantic/src/model.rs | 49 +++++--- crates/ruff_python_semantic/src/scope.rs | 14 --- 11 files changed, 169 insertions(+), 92 deletions(-) create mode 100644 crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__double_del.snap diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 2231a1207d..11e6f4de3a 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -307,18 +307,8 @@ where stmt.range(), ExecutionContext::Runtime, ); - } - - // Ensure that every nonlocal has an existing binding from a parent scope. - if self.enabled(Rule::NonlocalWithoutBinding) { - if self - .semantic_model - .scopes - .ancestors(self.semantic_model.scope_id) - .skip(1) - .take_while(|scope| !scope.kind.is_module()) - .all(|scope| !scope.declares(name.as_str())) - { + } else { + if self.enabled(Rule::NonlocalWithoutBinding) { self.diagnostics.push(Diagnostic::new( pylint::rules::NonlocalWithoutBinding { name: name.to_string(), @@ -3932,7 +3922,7 @@ where } // Add the bound exception name to the scope. - self.add_binding( + let binding_id = self.add_binding( name, range, BindingKind::Assignment, @@ -3942,27 +3932,30 @@ where walk_excepthandler(self, excepthandler); // Remove it from the scope immediately after. - if let Some(binding_id) = { - let scope = self.semantic_model.scope_mut(); - scope.delete(name) - } { - if !self.semantic_model.is_used(binding_id) { - if self.enabled(Rule::UnusedVariable) { - let mut diagnostic = Diagnostic::new( - pyflakes::rules::UnusedVariable { name: name.into() }, - range, - ); - if self.patch(Rule::UnusedVariable) { - diagnostic.try_set_fix(|| { - let edit = pyflakes::fixes::remove_exception_handler_assignment( - excepthandler, - self.locator, - )?; - Ok(Fix::automatic(edit)) - }); - } - self.diagnostics.push(diagnostic); + self.add_binding( + name, + range, + BindingKind::UnboundException, + BindingFlags::empty(), + ); + + // If the exception name wasn't used in the scope, emit a diagnostic. + if !self.semantic_model.is_used(binding_id) { + if self.enabled(Rule::UnusedVariable) { + let mut diagnostic = Diagnostic::new( + pyflakes::rules::UnusedVariable { name: name.into() }, + range, + ); + if self.patch(Rule::UnusedVariable) { + diagnostic.try_set_fix(|| { + pyflakes::fixes::remove_exception_handler_assignment( + excepthandler, + self.locator, + ) + .map(Fix::automatic) + }); } + self.diagnostics.push(diagnostic); } } } @@ -4224,7 +4217,14 @@ impl<'a> Checker<'a> { .ancestors(self.semantic_model.scope_id) .enumerate() .find_map(|(stack_index, scope)| { - scope.get(name).map(|binding_id| (stack_index, binding_id)) + scope.get(name).and_then(|binding_id| { + let binding = &self.semantic_model.bindings[binding_id]; + if binding.is_unbound() { + None + } else { + Some((stack_index, binding_id)) + } + }) }) { let shadowed = &self.semantic_model.bindings[shadowed_id]; @@ -4303,7 +4303,7 @@ impl<'a> Checker<'a> { .map(|binding_id| &self.semantic_model.bindings[binding_id]) { match &shadowed.kind { - BindingKind::Builtin => { + BindingKind::Builtin | BindingKind::Deletion | BindingKind::UnboundException => { // Avoid overriding builtins. } kind @ (BindingKind::Global | BindingKind::Nonlocal) => { @@ -4351,7 +4351,7 @@ impl<'a> Checker<'a> { } fn handle_node_load(&mut self, expr: &Expr) { - let Expr::Name(ast::ExprName { id, .. } )= expr else { + let Expr::Name(ast::ExprName { id, .. }) = expr else { return; }; match self.semantic_model.resolve_read(id, expr.range()) { @@ -4559,12 +4559,29 @@ impl<'a> Checker<'a> { let Expr::Name(ast::ExprName { id, .. } )= expr else { return; }; - if helpers::on_conditional_branch(&mut self.semantic_model.parents()) { - return; - } - let scope = self.semantic_model.scope_mut(); - if scope.delete(id.as_str()).is_none() { + // Treat the deletion of a name as a reference to that name. + if let Some(binding_id) = self.semantic_model.scope().get(id) { + self.semantic_model.add_local_reference( + binding_id, + expr.range(), + ExecutionContext::Runtime, + ); + + // If the name is unbound, then it's an error. + if self.enabled(Rule::UndefinedName) { + let binding = &self.semantic_model.bindings[binding_id]; + if binding.is_unbound() { + self.diagnostics.push(Diagnostic::new( + pyflakes::rules::UndefinedName { + name: id.to_string(), + }, + expr.range(), + )); + } + } + } else { + // If the name isn't bound at all, then it's an error. if self.enabled(Rule::UndefinedName) { self.diagnostics.push(Diagnostic::new( pyflakes::rules::UndefinedName { @@ -4574,6 +4591,19 @@ impl<'a> Checker<'a> { )); } } + + if helpers::on_conditional_branch(&mut self.semantic_model.parents()) { + return; + } + + // Create a binding to model the deletion. + let binding_id = self.semantic_model.push_binding( + expr.range(), + BindingKind::Deletion, + BindingFlags::empty(), + ); + let scope = self.semantic_model.scope_mut(); + scope.add(id, binding_id); } fn check_deferred_future_type_definitions(&mut self) { @@ -4759,8 +4789,8 @@ impl<'a> Checker<'a> { // Mark anything referenced in `__all__` as used. let exports: Vec<(&str, TextRange)> = { - let global_scope = self.semantic_model.global_scope(); - global_scope + self.semantic_model + .global_scope() .bindings_for_name("__all__") .map(|binding_id| &self.semantic_model.bindings[binding_id]) .filter_map(|binding| match &binding.kind { diff --git a/crates/ruff/src/importer/mod.rs b/crates/ruff/src/importer/mod.rs index f57fcfafe5..53b7fbb597 100644 --- a/crates/ruff/src/importer/mod.rs +++ b/crates/ruff/src/importer/mod.rs @@ -239,7 +239,7 @@ impl<'a> Importer<'a> { // Case 1: `from functools import lru_cache` is in scope, and we're trying to reference // `functools.cache`; thus, we add `cache` to the import, and return `"cache"` as the // bound name. - if semantic_model.is_unbound(symbol.member) { + if semantic_model.is_available(symbol.member) { let Ok(import_edit) = self.add_member(stmt, symbol.member) else { return Err(ResolutionError::InvalidEdit); }; @@ -252,7 +252,7 @@ impl<'a> Importer<'a> { ImportStyle::Import => { // Case 2a: No `functools` import is in scope; thus, we add `import functools`, // and return `"functools.cache"` as the bound name. - if semantic_model.is_unbound(symbol.module) { + if semantic_model.is_available(symbol.module) { let import_edit = self.add_import(&AnyImport::Import(Import::module(symbol.module)), at); Ok(( @@ -270,7 +270,7 @@ impl<'a> Importer<'a> { ImportStyle::ImportFrom => { // Case 2b: No `functools` import is in scope; thus, we add // `from functools import cache`, and return `"cache"` as the bound name. - if semantic_model.is_unbound(symbol.member) { + if semantic_model.is_available(symbol.member) { let import_edit = self.add_import( &AnyImport::ImportFrom(ImportFrom::member( symbol.module, diff --git a/crates/ruff/src/rules/flake8_errmsg/rules/string_in_exception.rs b/crates/ruff/src/rules/flake8_errmsg/rules/string_in_exception.rs index 0641058bf9..b1aca25362 100644 --- a/crates/ruff/src/rules/flake8_errmsg/rules/string_in_exception.rs +++ b/crates/ruff/src/rules/flake8_errmsg/rules/string_in_exception.rs @@ -190,7 +190,7 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr if let Some(indentation) = whitespace::indentation(checker.locator, stmt) { - if checker.semantic_model().is_unbound("msg") { + if checker.semantic_model().is_available("msg") { diagnostic.set_fix(generate_fix( stmt, first, @@ -213,7 +213,7 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr if let Some(indentation) = whitespace::indentation(checker.locator, stmt) { - if checker.semantic_model().is_unbound("msg") { + if checker.semantic_model().is_available("msg") { diagnostic.set_fix(generate_fix( stmt, first, @@ -240,7 +240,7 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr if let Some(indentation) = whitespace::indentation(checker.locator, stmt) { - if checker.semantic_model().is_unbound("msg") { + if checker.semantic_model().is_available("msg") { diagnostic.set_fix(generate_fix( stmt, first, diff --git a/crates/ruff/src/rules/pyflakes/mod.rs b/crates/ruff/src/rules/pyflakes/mod.rs index 80e6fd868d..1f8d587325 100644 --- a/crates/ruff/src/rules/pyflakes/mod.rs +++ b/crates/ruff/src/rules/pyflakes/mod.rs @@ -356,6 +356,15 @@ mod tests { "#, "print_after_shadowing_except" )] + #[test_case( + r#" + def f(): + x = 1 + del x + del x + "#, + "double_del" + )] fn contents(contents: &str, snapshot: &str) { let diagnostics = test_snippet(contents, &Settings::for_rules(&Linter::Pyflakes)); assert_messages!(snapshot, diagnostics); diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__augmented_assignment_after_del.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__augmented_assignment_after_del.snap index 8e311ebc89..57a88f04c4 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__augmented_assignment_after_del.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__augmented_assignment_after_del.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/pyflakes/mod.rs --- -:10:5: F823 Local variable `x` referenced before assignment +:10:5: F821 Undefined name `x` | 8 | # entirely after the `del` statement. However, it should be an F821 9 | # error, because the name is defined in the scope, but unbound. 10 | x += 1 - | ^ F823 + | ^ F821 | :10:5: F841 Local variable `x` is assigned to but never used diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__double_del.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__double_del.snap new file mode 100644 index 0000000000..ef01e14323 --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__double_del.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +:5:9: F821 Undefined name `x` + | +3 | x = 1 +4 | del x +5 | del x + | ^ F821 + | + + diff --git a/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs index c8e3809123..a49207bbb7 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs @@ -31,9 +31,8 @@ pub(crate) fn open_alias(checker: &mut Checker, expr: &Expr, func: &Expr) { { let mut diagnostic = Diagnostic::new(OpenAlias, expr.range()); if checker.patch(diagnostic.kind.rule()) { - if checker.semantic_model().is_unbound("open") { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + if checker.semantic_model().is_available("open") { + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( "open".to_string(), func.range(), ))); diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 8c3e08774a..0d86104608 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -1393,7 +1393,7 @@ impl<'a> SimpleCallArgs<'a> { } } -/// Check if a node is parent of a conditional branch. +/// Check if a node is part of a conditional branch. pub fn on_conditional_branch<'a>(parents: &mut impl Iterator) -> bool { parents.any(|parent| { if matches!(parent, Stmt::If(_) | Stmt::While(_) | Stmt::Match(_)) { diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index c3294809d6..42d828a7d2 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -41,11 +41,20 @@ impl<'a> Binding<'a> { } /// Return `true` if this [`Binding`] represents an explicit re-export - /// (e.g., `import FastAPI as FastAPI`). + /// (e.g., `FastAPI` in `from fastapi import FastAPI as FastAPI`). pub const fn is_explicit_export(&self) -> bool { self.flags.contains(BindingFlags::EXPLICIT_EXPORT) } + /// Return `true` if this [`Binding`] represents an unbound variable + /// (e.g., `x` in `x = 1; del x`). + pub const fn is_unbound(&self) -> bool { + matches!( + self.kind, + BindingKind::Annotation | BindingKind::Deletion | BindingKind::UnboundException + ) + } + /// Return `true` if this binding redefines the given binding. pub fn redefines(&self, existing: &'a Binding) -> bool { match &self.kind { @@ -83,10 +92,10 @@ impl<'a> Binding<'a> { _ => {} } } - BindingKind::Annotation => { - return false; - } - BindingKind::FutureImportation => { + BindingKind::Deletion + | BindingKind::Annotation + | BindingKind::FutureImportation + | BindingKind::Builtin => { return false; } _ => {} @@ -95,7 +104,6 @@ impl<'a> Binding<'a> { existing.kind, BindingKind::ClassDefinition | BindingKind::FunctionDefinition - | BindingKind::Builtin | BindingKind::Importation(..) | BindingKind::FromImportation(..) | BindingKind::SubmoduleImportation(..) @@ -367,6 +375,24 @@ pub enum BindingKind<'a> { /// import foo.bar /// ``` SubmoduleImportation(SubmoduleImportation<'a>), + + /// A binding for a deletion, like `x` in: + /// ```python + /// del x + /// ``` + Deletion, + + /// A binding to unbind the local variable, like `x` in: + /// ```python + /// try: + /// ... + /// except Exception as x: + /// ... + /// ``` + /// + /// After the `except` block, `x` is unbound, despite the lack + /// of an explicit `del` statement. + UnboundException, } bitflags! { diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 4142833083..c26b4dfb62 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -191,8 +191,9 @@ impl<'a> SemanticModel<'a> { .map_or(false, |binding| binding.kind.is_builtin()) } - /// Return `true` if `member` is unbound. - pub fn is_unbound(&self, member: &str) -> bool { + /// Return `true` if `member` is an "available" symbol, i.e., a symbol that has not been bound + /// in the current scope, or in any containing scope. + pub fn is_available(&self, member: &str) -> bool { self.find_binding(member) .map_or(true, |binding| binding.kind.is_builtin()) } @@ -203,7 +204,7 @@ impl<'a> SemanticModel<'a> { // should prefer it over local resolutions. if self.in_forward_reference() { if let Some(binding_id) = self.scopes.global().get(symbol) { - if !self.bindings[binding_id].kind.is_annotation() { + if !self.bindings[binding_id].is_unbound() { // Mark the binding as used. let context = self.execution_context(); let reference_id = self.references.push(ScopeId::global(), range, context); @@ -254,17 +255,29 @@ impl<'a> SemanticModel<'a> { self.bindings[binding_id].references.push(reference_id); } - // But if it's a type annotation, don't treat it as resolved. For example, given: - // - // ```python - // name: str - // print(name) - // ``` - // - // The `name` in `print(name)` should be treated as unresolved, but the `name` in - // `name: str` should be treated as used. - if self.bindings[binding_id].kind.is_annotation() { - continue; + match self.bindings[binding_id].kind { + // If it's a type annotation, don't treat it as resolved. For example, given: + // + // ```python + // name: str + // print(name) + // ``` + // + // The `name` in `print(name)` should be treated as unresolved, but the `name` in + // `name: str` should be treated as used. + BindingKind::Annotation => continue, + // If it's a deletion, don't treat it as resolved, since the name is now + // unbound. For example, given: + // + // ```python + // x = 1 + // del x + // print(x) + // ``` + // + // The `x` in `print(x)` should be treated as unresolved. + BindingKind::Deletion | BindingKind::UnboundException => break, + _ => {} } return ResolvedRead::Resolved(binding_id); @@ -618,9 +631,11 @@ impl<'a> SemanticModel<'a> { pub fn set_globals(&mut self, globals: Globals<'a>) { // If any global bindings don't already exist in the global scope, add them. for (name, range) in globals.iter() { - if self.global_scope().get(name).map_or(true, |binding_id| { - self.bindings[binding_id].kind.is_annotation() - }) { + if self + .global_scope() + .get(name) + .map_or(true, |binding_id| self.bindings[binding_id].is_unbound()) + { let id = self.bindings.push(Binding { kind: BindingKind::Assignment, range: *range, diff --git a/crates/ruff_python_semantic/src/scope.rs b/crates/ruff_python_semantic/src/scope.rs index f1e9107844..c53a6b43d2 100644 --- a/crates/ruff_python_semantic/src/scope.rs +++ b/crates/ruff_python_semantic/src/scope.rs @@ -38,9 +38,6 @@ pub struct Scope<'a> { /// In this case, the binding created by `x = 2` shadows the binding created by `x = 1`. shadowed_bindings: HashMap>, - /// A list of all names that have been deleted in this scope. - deleted_symbols: Vec<&'a str>, - /// Index into the globals arena, if the scope contains any globally-declared symbols. globals_id: Option, @@ -56,7 +53,6 @@ impl<'a> Scope<'a> { star_imports: Vec::default(), bindings: FxHashMap::default(), shadowed_bindings: IntMap::default(), - deleted_symbols: Vec::default(), globals_id: None, flags: ScopeFlags::empty(), } @@ -69,7 +65,6 @@ impl<'a> Scope<'a> { star_imports: Vec::default(), bindings: FxHashMap::default(), shadowed_bindings: IntMap::default(), - deleted_symbols: Vec::default(), globals_id: None, flags: ScopeFlags::empty(), } @@ -92,7 +87,6 @@ impl<'a> Scope<'a> { /// Removes the binding with the given name. pub fn delete(&mut self, name: &'a str) -> Option { - self.deleted_symbols.push(name); self.bindings.remove(name) } @@ -101,14 +95,6 @@ impl<'a> Scope<'a> { self.bindings.contains_key(name) } - /// Returns `true` if the scope declares a symbol with the given name. - /// - /// Unlike [`Scope::has`], the name may no longer be bound to a value (e.g., it could be - /// deleted). - pub fn declares(&self, name: &str) -> bool { - self.has(name) || self.deleted_symbols.contains(&name) - } - /// Returns the ids of all bindings defined in this scope. pub fn binding_ids(&self) -> impl Iterator + '_ { self.bindings.values().copied() From 1e497162d1f3dd0c22cd3dba2a3c96efaff93f45 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 14 Jun 2023 09:58:48 -0400 Subject: [PATCH 048/447] Add a dedicated read result for unbound locals (#5083) ## Summary Small follow-up to #4888 to add a dedicated `ResolvedRead` case for unbound locals, mostly for clarity and documentation purposes (no behavior changes). ## Test Plan `cargo test` --- crates/ruff/src/checkers/ast/mod.rs | 6 +- crates/ruff_python_semantic/src/model.rs | 71 +++++++++++++++++++++--- 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 11e6f4de3a..6a496c39de 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -4355,10 +4355,10 @@ impl<'a> Checker<'a> { return; }; match self.semantic_model.resolve_read(id, expr.range()) { - ResolvedRead::Resolved(..) | ResolvedRead::ImplicitGlobal => { + ResolvedRead::Resolved(_) | ResolvedRead::ImplicitGlobal => { // Nothing to do. } - ResolvedRead::StarImport => { + ResolvedRead::WildcardImport => { // F405 if self.enabled(Rule::UndefinedLocalWithImportStarUsage) { let sources: Vec = self @@ -4381,7 +4381,7 @@ impl<'a> Checker<'a> { )); } } - ResolvedRead::NotFound => { + ResolvedRead::NotFound | ResolvedRead::UnboundLocal(_) => { // F821 if self.enabled(Rule::UndefinedName) { // Allow __path__. diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index c26b4dfb62..afb0e389e9 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -59,7 +59,7 @@ pub struct SemanticModel<'a> { /// Map from binding ID to binding ID that it shadows (in another scope). /// - /// For example: + /// For example, given: /// ```python /// import x /// @@ -266,6 +266,7 @@ impl<'a> SemanticModel<'a> { // The `name` in `print(name)` should be treated as unresolved, but the `name` in // `name: str` should be treated as used. BindingKind::Annotation => continue, + // If it's a deletion, don't treat it as resolved, since the name is now // unbound. For example, given: // @@ -276,11 +277,15 @@ impl<'a> SemanticModel<'a> { // ``` // // The `x` in `print(x)` should be treated as unresolved. - BindingKind::Deletion | BindingKind::UnboundException => break, - _ => {} - } + BindingKind::Deletion | BindingKind::UnboundException => { + return ResolvedRead::UnboundLocal(binding_id) + } - return ResolvedRead::Resolved(binding_id); + // Otherwise, treat it as resolved. + _ => { + return ResolvedRead::Resolved(binding_id); + } + } } // Allow usages of `__module__` and `__qualname__` within class scopes, e.g.: @@ -309,7 +314,7 @@ impl<'a> SemanticModel<'a> { } if import_starred { - ResolvedRead::StarImport + ResolvedRead::WildcardImport } else { ResolvedRead::NotFound } @@ -1065,14 +1070,62 @@ pub struct Snapshot { #[derive(Debug)] pub enum ResolvedRead { /// The read reference is resolved to a specific binding. + /// + /// For example, given: + /// ```python + /// x = 1 + /// print(x) + /// ``` + /// + /// The `x` in `print(x)` is resolved to the binding of `x` in `x = 1`. Resolved(BindingId), + /// The read reference is resolved to a context-specific, implicit global (e.g., `__class__` /// within a class scope). + /// + /// For example, given: + /// ```python + /// class C: + /// print(__class__) + /// ``` + /// + /// The `__class__` in `print(__class__)` is resolved to the implicit global `__class__`. ImplicitGlobal, - /// The read reference is unresolved, but at least one of the containing scopes contains a star - /// import. - StarImport, + + /// The read reference is unresolved, but at least one of the containing scopes contains a + /// wildcard import. + /// + /// For example, given: + /// ```python + /// from x import * + /// + /// print(y) + /// ``` + /// + /// The `y` in `print(y)` is unresolved, but the containing scope contains a wildcard import, + /// so `y` _may_ be resolved to a symbol imported by the wildcard import. + WildcardImport, + + /// The read reference is resolved, but to an unbound local variable. + /// + /// For example, given: + /// ```python + /// x = 1 + /// del x + /// print(x) + /// ``` + /// + /// The `x` in `print(x)` is an unbound local. + UnboundLocal(BindingId), + /// The read reference is definitively unresolved. + /// + /// For example, given: + /// ```python + /// print(x) + /// ``` + /// + /// The `x` in `print(x)` is definitively unresolved. NotFound, } From c74ef77e85931523ccb40108027ea85d6afe541e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 14 Jun 2023 10:07:46 -0400 Subject: [PATCH 049/447] Move binding accesses into `SemanticModel` method (#5084) --- crates/ruff/src/checkers/ast/mod.rs | 28 ++++++++----------- .../rules/unused_loop_control_variable.rs | 2 +- .../runtime_import_in_type_checking_block.rs | 2 +- .../rules/typing_only_runtime_import.rs | 2 +- .../rules/unused_arguments.rs | 25 ++++++++--------- .../rules/pyflakes/rules/undefined_local.rs | 2 +- .../rules/pyflakes/rules/unused_annotation.rs | 2 +- .../src/rules/pyflakes/rules/unused_import.rs | 2 +- .../rules/pyflakes/rules/unused_variable.rs | 2 +- .../rules/pylint/rules/global_statement.rs | 2 +- crates/ruff_python_semantic/src/binding.rs | 7 +---- crates/ruff_python_semantic/src/model.rs | 17 +++++++---- crates/ruff_python_semantic/src/reference.rs | 14 ++++++---- 13 files changed, 53 insertions(+), 54 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 6a496c39de..dbe401585a 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -4208,7 +4208,7 @@ impl<'a> Checker<'a> { // Create the `Binding`. let binding_id = self.semantic_model.push_binding(range, kind, flags); - let binding = &self.semantic_model.bindings[binding_id]; + let binding = self.semantic_model.binding(binding_id); // Determine whether the binding shadows any existing bindings. if let Some((stack_index, shadowed_id)) = self @@ -4218,7 +4218,7 @@ impl<'a> Checker<'a> { .enumerate() .find_map(|(stack_index, scope)| { scope.get(name).and_then(|binding_id| { - let binding = &self.semantic_model.bindings[binding_id]; + let binding = self.semantic_model.binding(binding_id); if binding.is_unbound() { None } else { @@ -4227,7 +4227,7 @@ impl<'a> Checker<'a> { }) }) { - let shadowed = &self.semantic_model.bindings[shadowed_id]; + let shadowed = self.semantic_model.binding(shadowed_id); let in_current_scope = stack_index == 0; if !shadowed.kind.is_builtin() && shadowed.source.map_or(true, |left| { @@ -4321,10 +4321,7 @@ impl<'a> Checker<'a> { // If this is an annotation, and we already have an existing value in the same scope, // don't treat it as an assignment (i.e., avoid adding it to the scope). - if self.semantic_model.bindings[binding_id] - .kind - .is_annotation() - { + if self.semantic_model.binding(binding_id).kind.is_annotation() { return binding_id; } } @@ -4424,7 +4421,7 @@ impl<'a> Checker<'a> { .scope() .get(id) .map_or(false, |binding_id| { - self.semantic_model.bindings[binding_id].kind.is_global() + self.semantic_model.binding(binding_id).kind.is_global() }) { pep8_naming::rules::non_lowercase_variable_in_function(self, expr, parent, id); @@ -4570,7 +4567,7 @@ impl<'a> Checker<'a> { // If the name is unbound, then it's an error. if self.enabled(Rule::UndefinedName) { - let binding = &self.semantic_model.bindings[binding_id]; + let binding = self.semantic_model.binding(binding_id); if binding.is_unbound() { self.diagnostics.push(Diagnostic::new( pyflakes::rules::UndefinedName { @@ -4734,10 +4731,7 @@ impl<'a> Checker<'a> { let parent = &self.semantic_model.scopes[scope.parent.unwrap()]; self.diagnostics .extend(flake8_unused_arguments::rules::unused_arguments( - self, - parent, - scope, - &self.semantic_model.bindings, + self, parent, scope, )); } } @@ -4826,7 +4820,7 @@ impl<'a> Checker<'a> { .map(|scope| { scope .binding_ids() - .map(|binding_id| &self.semantic_model.bindings[binding_id]) + .map(|binding_id| self.semantic_model.binding(binding_id)) .filter(|binding| { flake8_type_checking::helpers::is_valid_runtime_import( &self.semantic_model, @@ -4885,7 +4879,7 @@ impl<'a> Checker<'a> { // PLW0602 if self.enabled(Rule::GlobalVariableNotAssigned) { for (name, binding_id) in scope.bindings() { - let binding = &self.semantic_model.bindings[binding_id]; + let binding = self.semantic_model.binding(binding_id); if binding.kind.is_global() { if let Some(source) = binding.source { let stmt = &self.semantic_model.stmts[source]; @@ -4913,7 +4907,7 @@ impl<'a> Checker<'a> { if self.enabled(Rule::RedefinedWhileUnused) { for (name, binding_id) in scope.bindings() { if let Some(shadowed_id) = self.semantic_model.shadowed_binding(binding_id) { - let shadowed = &self.semantic_model.bindings[shadowed_id]; + let shadowed = self.semantic_model.binding(shadowed_id); if shadowed.is_used() { continue; } @@ -4925,7 +4919,7 @@ impl<'a> Checker<'a> { .start(), ); - let binding = &self.semantic_model.bindings[binding_id]; + let binding = self.semantic_model.binding(binding_id); let mut diagnostic = Diagnostic::new( pyflakes::rules::RedefinedWhileUnused { name: (*name).to_string(), diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs b/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs index ad64d880b6..6d813426a1 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs @@ -157,7 +157,7 @@ pub(crate) fn unused_loop_control_variable(checker: &mut Checker, target: &Expr, let scope = checker.semantic_model().scope(); if scope .bindings_for_name(name) - .map(|binding_id| &checker.semantic_model().bindings[binding_id]) + .map(|binding_id| checker.semantic_model().binding(binding_id)) .all(|binding| !binding.is_used()) { diagnostic.set_fix(Fix::suggested(Edit::range_replacement( diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs index c4edc8b8d5..7706c1ae51 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs @@ -75,7 +75,7 @@ pub(crate) fn runtime_import_in_type_checking_block( let mut ignores_by_statement: FxHashMap> = FxHashMap::default(); for binding_id in scope.binding_ids() { - let binding = &checker.semantic_model().bindings[binding_id]; + let binding = checker.semantic_model().binding(binding_id); let Some(qualified_name) = binding.qualified_name() else { continue; diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index c509e252db..87acdf2b1f 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -197,7 +197,7 @@ pub(crate) fn typing_only_runtime_import( FxHashMap::default(); for binding_id in scope.binding_ids() { - let binding = &checker.semantic_model().bindings[binding_id]; + let binding = checker.semantic_model().binding(binding_id); // If we're in un-strict mode, don't flag typing-only imports that are // implicitly loaded by way of a valid runtime import. diff --git a/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs b/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs index f410b822cc..5e571e2d59 100644 --- a/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs +++ b/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs @@ -10,7 +10,7 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_semantic::analyze::function_type; use ruff_python_semantic::analyze::function_type::FunctionType; use ruff_python_semantic::analyze::visibility; -use ruff_python_semantic::binding::Bindings; +use ruff_python_semantic::model::SemanticModel; use ruff_python_semantic::scope::{Scope, ScopeKind}; use crate::checkers::ast::Checker; @@ -221,7 +221,7 @@ fn function( argumentable: Argumentable, args: &Arguments, values: &Scope, - bindings: &Bindings, + model: &SemanticModel, dummy_variable_rgx: &Regex, ignore_variadic_names: bool, ) -> Vec { @@ -240,7 +240,7 @@ fn function( .flatten() .skip(usize::from(ignore_variadic_names)), ); - call(argumentable, args, values, bindings, dummy_variable_rgx) + call(argumentable, args, values, model, dummy_variable_rgx) } /// Check a method for unused arguments. @@ -248,7 +248,7 @@ fn method( argumentable: Argumentable, args: &Arguments, values: &Scope, - bindings: &Bindings, + model: &SemanticModel, dummy_variable_rgx: &Regex, ignore_variadic_names: bool, ) -> Vec { @@ -268,21 +268,21 @@ fn method( .flatten() .skip(usize::from(ignore_variadic_names)), ); - call(argumentable, args, values, bindings, dummy_variable_rgx) + call(argumentable, args, values, model, dummy_variable_rgx) } fn call<'a>( argumentable: Argumentable, args: impl Iterator, values: &Scope, - bindings: &Bindings, + model: &SemanticModel, dummy_variable_rgx: &Regex, ) -> Vec { let mut diagnostics: Vec = vec![]; for arg in args { if let Some(binding) = values .get(arg.arg.as_str()) - .map(|binding_id| &bindings[binding_id]) + .map(|binding_id| model.binding(binding_id)) { if binding.kind.is_argument() && !binding.is_used() @@ -303,7 +303,6 @@ pub(crate) fn unused_arguments( checker: &Checker, parent: &Scope, scope: &Scope, - bindings: &Bindings, ) -> Vec { match &scope.kind { ScopeKind::Function(ast::StmtFunctionDef { @@ -336,7 +335,7 @@ pub(crate) fn unused_arguments( Argumentable::Function, args, scope, - bindings, + checker.semantic_model(), &checker.settings.dummy_variable_rgx, checker .settings @@ -362,7 +361,7 @@ pub(crate) fn unused_arguments( Argumentable::Method, args, scope, - bindings, + checker.semantic_model(), &checker.settings.dummy_variable_rgx, checker .settings @@ -388,7 +387,7 @@ pub(crate) fn unused_arguments( Argumentable::ClassMethod, args, scope, - bindings, + checker.semantic_model(), &checker.settings.dummy_variable_rgx, checker .settings @@ -414,7 +413,7 @@ pub(crate) fn unused_arguments( Argumentable::StaticMethod, args, scope, - bindings, + checker.semantic_model(), &checker.settings.dummy_variable_rgx, checker .settings @@ -433,7 +432,7 @@ pub(crate) fn unused_arguments( Argumentable::Lambda, args, scope, - bindings, + checker.semantic_model(), &checker.settings.dummy_variable_rgx, checker .settings diff --git a/crates/ruff/src/rules/pyflakes/rules/undefined_local.rs b/crates/ruff/src/rules/pyflakes/rules/undefined_local.rs index 312e5a1d77..dae0af9655 100644 --- a/crates/ruff/src/rules/pyflakes/rules/undefined_local.rs +++ b/crates/ruff/src/rules/pyflakes/rules/undefined_local.rs @@ -68,7 +68,7 @@ pub(crate) fn undefined_local(checker: &mut Checker, name: &str) { // If the name was defined in that scope... if let Some(binding) = scope .get(name) - .map(|binding_id| &checker.semantic_model().bindings[binding_id]) + .map(|binding_id| checker.semantic_model().binding(binding_id)) { // And has already been accessed in the current scope... if let Some(range) = binding.references().find_map(|reference_id| { diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_annotation.rs b/crates/ruff/src/rules/pyflakes/rules/unused_annotation.rs index 44e96c53f6..1691dbe01e 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_annotation.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_annotation.rs @@ -39,7 +39,7 @@ pub(crate) fn unused_annotation(checker: &mut Checker, scope: ScopeId) { let bindings: Vec<_> = scope .bindings() .filter_map(|(name, binding_id)| { - let binding = &checker.semantic_model().bindings[binding_id]; + let binding = checker.semantic_model().binding(binding_id); if binding.kind.is_annotation() && !binding.is_used() && !checker.settings.dummy_variable_rgx.is_match(name) diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs index 36b02cb5c0..09d4b98ad0 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs @@ -104,7 +104,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut let mut ignored: FxHashMap<(NodeId, Exceptions), Vec> = FxHashMap::default(); for binding_id in scope.binding_ids() { - let binding = &checker.semantic_model().bindings[binding_id]; + let binding = checker.semantic_model().binding(binding_id); if binding.is_used() || binding.is_explicit_export() { continue; diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs b/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs index fb6932ca3f..6ffa78fae6 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs @@ -292,7 +292,7 @@ pub(crate) fn unused_variable(checker: &mut Checker, scope: ScopeId) { let bindings: Vec<_> = scope .bindings() - .map(|(name, binding_id)| (name, &checker.semantic_model().bindings[binding_id])) + .map(|(name, binding_id)| (name, checker.semantic_model().binding(binding_id))) .filter_map(|(name, binding)| { if (binding.kind.is_assignment() || binding.kind.is_named_expr_assignment()) && !binding.is_used() diff --git a/crates/ruff/src/rules/pylint/rules/global_statement.rs b/crates/ruff/src/rules/pylint/rules/global_statement.rs index c65bf96d57..1bdbe19e26 100644 --- a/crates/ruff/src/rules/pylint/rules/global_statement.rs +++ b/crates/ruff/src/rules/pylint/rules/global_statement.rs @@ -57,7 +57,7 @@ impl Violation for GlobalStatement { pub(crate) fn global_statement(checker: &mut Checker, name: &str) { let scope = checker.semantic_model().scope(); if let Some(binding_id) = scope.get(name) { - let binding = &checker.semantic_model().bindings[binding_id]; + let binding = checker.semantic_model().binding(binding_id); if binding.kind.is_global() { let source = checker.semantic_model().stmts[binding .source diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index 42d828a7d2..d9fd471b41 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -198,15 +198,10 @@ impl nohash_hasher::IsEnabled for BindingId {} pub struct Bindings<'a>(IndexVec>); impl<'a> Bindings<'a> { - /// Pushes a new binding and returns its id + /// Pushes a new [`Binding`] and returns its [`BindingId`]. pub fn push(&mut self, binding: Binding<'a>) -> BindingId { self.0.push(binding) } - - /// Returns the id that will be assigned when pushing the next binding - pub fn next_id(&self) -> BindingId { - self.0.next_index() - } } impl<'a> Deref for Bindings<'a> { diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index afb0e389e9..2e2df908ab 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -106,6 +106,18 @@ impl<'a> SemanticModel<'a> { } } + /// Return the [`Binding`] for the given [`BindingId`]. + #[inline] + pub fn binding(&self, id: BindingId) -> &Binding { + &self.bindings[id] + } + + /// Resolve the [`Reference`] for the given [`ReferenceId`]. + #[inline] + pub fn reference(&self, id: ReferenceId) -> &Reference { + &self.references[id] + } + /// Return `true` if the `Expr` is a reference to `typing.${target}`. pub fn match_typing_expr(&self, expr: &Expr, target: &str) -> bool { self.resolve_call_path(expr).map_or(false, |call_path| { @@ -717,11 +729,6 @@ impl<'a> SemanticModel<'a> { self.bindings[binding_id].references.push(reference_id); } - /// Resolve a [`ReferenceId`]. - pub fn reference(&self, reference_id: ReferenceId) -> &Reference { - self.references.resolve(reference_id) - } - /// Return the [`ExecutionContext`] of the current scope. pub const fn execution_context(&self) -> ExecutionContext { if self.in_type_checking_block() diff --git a/crates/ruff_python_semantic/src/reference.rs b/crates/ruff_python_semantic/src/reference.rs index cac8bb8f39..0626a5c808 100644 --- a/crates/ruff_python_semantic/src/reference.rs +++ b/crates/ruff_python_semantic/src/reference.rs @@ -1,6 +1,7 @@ use ruff_text_size::TextRange; +use std::ops::Deref; -use ruff_index::{newtype_index, IndexVec}; +use ruff_index::{newtype_index, IndexSlice, IndexVec}; use crate::context::ExecutionContext; use crate::scope::ScopeId; @@ -38,7 +39,7 @@ pub struct ReferenceId; pub struct References(IndexVec); impl References { - /// Pushes a new read reference and returns its unique id. + /// Pushes a new [`Reference`] and returns its [`ReferenceId`]. pub fn push( &mut self, scope_id: ScopeId, @@ -51,9 +52,12 @@ impl References { context, }) } +} - /// Returns the [`Reference`] with the given id. - pub fn resolve(&self, id: ReferenceId) -> &Reference { - &self.0[id] +impl Deref for References { + type Target = IndexSlice; + + fn deref(&self) -> &Self::Target { + &self.0 } } From 6f10aeebaa10b621336336c923caf67cb68226e0 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 14 Jun 2023 10:15:14 -0400 Subject: [PATCH 050/447] Remove unused `Scope#delete` method (#5085) ## Summary This is now intentionally unused and is now made impossible (via this PR). --- crates/ruff_python_semantic/src/scope.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/ruff_python_semantic/src/scope.rs b/crates/ruff_python_semantic/src/scope.rs index c53a6b43d2..22f0983915 100644 --- a/crates/ruff_python_semantic/src/scope.rs +++ b/crates/ruff_python_semantic/src/scope.rs @@ -85,11 +85,6 @@ impl<'a> Scope<'a> { } } - /// Removes the binding with the given name. - pub fn delete(&mut self, name: &'a str) -> Option { - self.bindings.remove(name) - } - /// Returns `true` if this scope has a binding with the given name. pub fn has(&self, name: &str) -> bool { self.bindings.contains_key(name) From e7316c1cc60cfc10072564726bdb150cf18db933 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Wed, 14 Jun 2023 16:57:09 +0200 Subject: [PATCH 051/447] Consider ignore-names in all pep8 naming rules (#5079) ## Summary This changes all remaining pep8 naming rules to consider the `ingore-names` argument. Closes #5050 ## Test Plan Added new tests. --- .pre-commit-config.yaml | 1 + _typos.toml | 1 + .../fixtures/pep8_naming/ignore_names/N801.py | 11 +++++ .../fixtures/pep8_naming/ignore_names/N807.py | 13 ++++++ .../fixtures/pep8_naming/ignore_names/N811.py | 5 ++ .../fixtures/pep8_naming/ignore_names/N812.py | 5 ++ .../fixtures/pep8_naming/ignore_names/N813.py | 8 ++++ .../fixtures/pep8_naming/ignore_names/N814.py | 8 ++++ .../fixtures/pep8_naming/ignore_names/N817.py | 5 ++ .../fixtures/pep8_naming/ignore_names/N818.py | 11 +++++ .../ignore_names/N999/badAllowed/__init__.py | 0 crates/ruff/src/checkers/ast/mod.rs | 46 +++++++++++++++---- crates/ruff/src/checkers/filesystem.rs | 4 +- crates/ruff/src/rules/pep8_naming/mod.rs | 15 +++++- .../rules/camelcase_imported_as_acronym.rs | 9 ++++ .../rules/camelcase_imported_as_constant.rs | 9 ++++ .../rules/camelcase_imported_as_lowercase.rs | 9 ++++ .../constant_imported_as_non_constant.rs | 10 ++++ .../pep8_naming/rules/dunder_function_name.rs | 9 ++++ .../rules/error_suffix_on_exception_name.rs | 10 ++++ .../pep8_naming/rules/invalid_class_name.rs | 10 ++++ .../pep8_naming/rules/invalid_module_name.rs | 15 +++++- .../lowercase_imported_as_non_lowercase.rs | 10 ++++ ...ing__tests__ignore_names_N801_N801.py.snap | 22 +++++++++ ...ing__tests__ignore_names_N807_N807.py.snap | 22 +++++++++ ...ing__tests__ignore_names_N811_N811.py.snap | 20 ++++++++ ...ing__tests__ignore_names_N812_N812.py.snap | 20 ++++++++ ...ing__tests__ignore_names_N813_N813.py.snap | 29 ++++++++++++ ...ing__tests__ignore_names_N814_N814.py.snap | 29 ++++++++++++ ...ing__tests__ignore_names_N817_N817.py.snap | 20 ++++++++ ...ing__tests__ignore_names_N818_N818.py.snap | 22 +++++++++ ...es_N999_N999__badAllowed____init__.py.snap | 4 ++ 32 files changed, 400 insertions(+), 12 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N801.py create mode 100644 crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N807.py create mode 100644 crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N811.py create mode 100644 crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N812.py create mode 100644 crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N813.py create mode 100644 crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N814.py create mode 100644 crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N817.py create mode 100644 crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N818.py create mode 100644 crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N999/badAllowed/__init__.py create mode 100644 crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N801_N801.py.snap create mode 100644 crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N807_N807.py.snap create mode 100644 crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N811_N811.py.snap create mode 100644 crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N812_N812.py.snap create mode 100644 crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N813_N813.py.snap create mode 100644 crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N814_N814.py.snap create mode 100644 crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N817_N817.py.snap create mode 100644 crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N818_N818.py.snap create mode 100644 crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N999_N999__badAllowed____init__.py.snap diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 572edd2781..d89b927114 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,6 +3,7 @@ fail_fast: true exclude: | (?x)^( crates/ruff/resources/.*| + crates/ruff/src/rules/.*/snapshots/.*| crates/ruff_python_formatter/resources/.*| crates/ruff_python_formatter/src/snapshots/.* )$ diff --git a/_typos.toml b/_typos.toml index 778ba59eaf..cf274a9fcf 100644 --- a/_typos.toml +++ b/_typos.toml @@ -8,3 +8,4 @@ whos = "whos" spawnve = "spawnve" ned = "ned" poit = "poit" +BA = "BA" # acronym for "Bad Allowed", used in testing. diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N801.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N801.py new file mode 100644 index 0000000000..3266975f8c --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N801.py @@ -0,0 +1,11 @@ +class badAllowed: + pass + +class stillBad: + pass + +class BAD_ALLOWED: + pass + +class STILL_BAD: + pass diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N807.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N807.py new file mode 100644 index 0000000000..e8d3c5ed24 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N807.py @@ -0,0 +1,13 @@ +def __badAllowed__(): + pass + +def __stillBad__(): + pass + + +def nested(): + def __badAllowed__(): + pass + + def __stillBad__(): + pass diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N811.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N811.py new file mode 100644 index 0000000000..fbb20f0399 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N811.py @@ -0,0 +1,5 @@ +import mod.BAD_ALLOWED as badAllowed +import mod.STILL_BAD as stillBad + +from mod import BAD_ALLOWED as badAllowed +from mod import STILL_BAD as stillBad diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N812.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N812.py new file mode 100644 index 0000000000..be6180f86d --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N812.py @@ -0,0 +1,5 @@ +import mod.badallowed as badAllowed +import mod.stillbad as stillBad + +from mod import badallowed as BadAllowed +from mod import stillbad as StillBad diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N813.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N813.py new file mode 100644 index 0000000000..aa44f32b62 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N813.py @@ -0,0 +1,8 @@ +import mod.BadAllowed as badallowed +import mod.stillBad as stillbad + +from mod import BadAllowed as badallowed +from mod import StillBad as stillbad + +from mod import BadAllowed as bad_allowed +from mod import StillBad as still_bad diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N814.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N814.py new file mode 100644 index 0000000000..ac7a530cf0 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N814.py @@ -0,0 +1,8 @@ +import mod.BadAllowed as BADALLOWED +import mod.StillBad as STILLBAD + +from mod import BadAllowed as BADALLOWED +from mod import StillBad as STILLBAD + +from mod import BadAllowed as BAD_ALLOWED +from mod import StillBad as STILL_BAD diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N817.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N817.py new file mode 100644 index 0000000000..27a5ce4299 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N817.py @@ -0,0 +1,5 @@ +import mod.BadAllowed as BA +import mod.StillBad as SB + +from mod import BadAllowed as BA +from mod import StillBad as SB diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N818.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N818.py new file mode 100644 index 0000000000..57f6887a96 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N818.py @@ -0,0 +1,11 @@ +class BadAllowed(Exception): + pass + +class StillBad(Exception): + pass + +class BadAllowed(AnotherError): + pass + +class StillBad(AnotherError): + pass diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N999/badAllowed/__init__.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N999/badAllowed/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index dbe401585a..db4d89f87c 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -457,6 +457,7 @@ where self.semantic_model.scope(), stmt, name, + &self.settings.pep8_naming.ignore_names, self.locator, ) { self.diagnostics.push(diagnostic); @@ -703,9 +704,12 @@ where } } if self.enabled(Rule::InvalidClassName) { - if let Some(diagnostic) = - pep8_naming::rules::invalid_class_name(stmt, name, self.locator) - { + if let Some(diagnostic) = pep8_naming::rules::invalid_class_name( + stmt, + name, + &self.settings.pep8_naming.ignore_names, + self.locator, + ) { self.diagnostics.push(diagnostic); } } @@ -715,6 +719,7 @@ where bases, name, self.locator, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); } @@ -898,7 +903,11 @@ where if self.enabled(Rule::ConstantImportedAsNonConstant) { if let Some(diagnostic) = pep8_naming::rules::constant_imported_as_non_constant( - name, asname, alias, stmt, + name, + asname, + alias, + stmt, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); @@ -908,7 +917,11 @@ where if self.enabled(Rule::LowercaseImportedAsNonLowercase) { if let Some(diagnostic) = pep8_naming::rules::lowercase_imported_as_non_lowercase( - name, asname, alias, stmt, + name, + asname, + alias, + stmt, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); @@ -918,7 +931,11 @@ where if self.enabled(Rule::CamelcaseImportedAsLowercase) { if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_lowercase( - name, asname, alias, stmt, + name, + asname, + alias, + stmt, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); @@ -928,7 +945,11 @@ where if self.enabled(Rule::CamelcaseImportedAsConstant) { if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_constant( - name, asname, alias, stmt, + name, + asname, + alias, + stmt, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); @@ -938,7 +959,11 @@ where if self.enabled(Rule::CamelcaseImportedAsAcronym) { if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_acronym( - name, asname, alias, stmt, + name, + asname, + alias, + stmt, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); @@ -1197,6 +1222,7 @@ where asname, alias, stmt, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); @@ -1210,6 +1236,7 @@ where asname, alias, stmt, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); @@ -1223,6 +1250,7 @@ where asname, alias, stmt, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); @@ -1236,6 +1264,7 @@ where asname, alias, stmt, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); @@ -1249,6 +1278,7 @@ where asname, alias, stmt, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/checkers/filesystem.rs b/crates/ruff/src/checkers/filesystem.rs index 83df0ba87b..ad8a28dab9 100644 --- a/crates/ruff/src/checkers/filesystem.rs +++ b/crates/ruff/src/checkers/filesystem.rs @@ -25,7 +25,9 @@ pub(crate) fn check_file_path( // pep8-naming if settings.rules.enabled(Rule::InvalidModuleName) { - if let Some(diagnostic) = invalid_module_name(path, package) { + if let Some(diagnostic) = + invalid_module_name(path, package, &settings.pep8_naming.ignore_names) + { diagnostics.push(diagnostic); } } diff --git a/crates/ruff/src/rules/pep8_naming/mod.rs b/crates/ruff/src/rules/pep8_naming/mod.rs index e9c965c9ac..f7e55dfe1d 100644 --- a/crates/ruff/src/rules/pep8_naming/mod.rs +++ b/crates/ruff/src/rules/pep8_naming/mod.rs @@ -106,13 +106,22 @@ mod tests { Ok(()) } + #[test_case(Rule::InvalidClassName, "N801.py")] #[test_case(Rule::InvalidFunctionName, "N802.py")] #[test_case(Rule::InvalidArgumentName, "N803.py")] #[test_case(Rule::InvalidFirstArgumentNameForClassMethod, "N804.py")] #[test_case(Rule::InvalidFirstArgumentNameForMethod, "N805.py")] #[test_case(Rule::NonLowercaseVariableInFunction, "N806.py")] + #[test_case(Rule::DunderFunctionName, "N807.py")] + #[test_case(Rule::ConstantImportedAsNonConstant, "N811.py")] + #[test_case(Rule::LowercaseImportedAsNonLowercase, "N812.py")] + #[test_case(Rule::CamelcaseImportedAsLowercase, "N813.py")] + #[test_case(Rule::CamelcaseImportedAsConstant, "N814.py")] #[test_case(Rule::MixedCaseVariableInClassScope, "N815.py")] #[test_case(Rule::MixedCaseVariableInGlobalScope, "N816.py")] + #[test_case(Rule::CamelcaseImportedAsAcronym, "N817.py")] + #[test_case(Rule::ErrorSuffixOnExceptionName, "N818.py")] + #[test_case(Rule::InvalidModuleName, "N999/badAllowed/__init__.py")] fn ignore_names(rule_code: Rule, path: &str) -> Result<()> { let snapshot = format!("ignore_names_{}_{path}", rule_code.noqa_code()); let diagnostics = test_path( @@ -120,8 +129,10 @@ mod tests { &settings::Settings { pep8_naming: pep8_naming::settings::Settings { ignore_names: vec![ - IdentifierPattern::new("*Allowed").unwrap(), - IdentifierPattern::new("*ALLOWED").unwrap(), + IdentifierPattern::new("*allowed*").unwrap(), + IdentifierPattern::new("*Allowed*").unwrap(), + IdentifierPattern::new("*ALLOWED*").unwrap(), + IdentifierPattern::new("BA").unwrap(), // For N817. ], ..Default::default() }, diff --git a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs index a4ceec8d8d..45de159229 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs @@ -5,6 +5,7 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_stdlib::str::{self}; use crate::rules::pep8_naming::helpers; +use crate::settings::types::IdentifierPattern; /// ## What it does /// Checks for `CamelCase` imports that are aliased as acronyms. @@ -52,7 +53,15 @@ pub(crate) fn camelcase_imported_as_acronym( asname: &str, alias: &Alias, stmt: &Stmt, + ignore_names: &[IdentifierPattern], ) -> Option { + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(asname)) + { + return None; + } + if helpers::is_camelcase(name) && !str::is_lower(asname) && str::is_upper(asname) diff --git a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs index 355be20923..bc87539905 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs @@ -5,6 +5,7 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_stdlib::str::{self}; use crate::rules::pep8_naming::helpers; +use crate::settings::types::IdentifierPattern; /// ## What it does /// Checks for `CamelCase` imports that are aliased to constant-style names. @@ -49,7 +50,15 @@ pub(crate) fn camelcase_imported_as_constant( asname: &str, alias: &Alias, stmt: &Stmt, + ignore_names: &[IdentifierPattern], ) -> Option { + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(name)) + { + return None; + } + if helpers::is_camelcase(name) && !str::is_lower(asname) && str::is_upper(asname) diff --git a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs index c427e879a5..b08232ebcc 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs @@ -4,6 +4,7 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::rules::pep8_naming::helpers; +use crate::settings::types::IdentifierPattern; /// ## What it does /// Checks for `CamelCase` imports that are aliased to lowercase names. @@ -48,7 +49,15 @@ pub(crate) fn camelcase_imported_as_lowercase( asname: &str, alias: &Alias, stmt: &Stmt, + ignore_names: &[IdentifierPattern], ) -> Option { + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(asname)) + { + return None; + } + if helpers::is_camelcase(name) && ruff_python_stdlib::str::is_lower(asname) { let mut diagnostic = Diagnostic::new( CamelcaseImportedAsLowercase { diff --git a/crates/ruff/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs b/crates/ruff/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs index 79cf3b32af..ad9e43d13e 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs @@ -4,6 +4,8 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_stdlib::str; +use crate::settings::types::IdentifierPattern; + /// ## What it does /// Checks for constant imports that are aliased to non-constant-style /// names. @@ -48,7 +50,15 @@ pub(crate) fn constant_imported_as_non_constant( asname: &str, alias: &Alias, stmt: &Stmt, + ignore_names: &[IdentifierPattern], ) -> Option { + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(name)) + { + return None; + } + if str::is_upper(name) && !str::is_upper(asname) { let mut diagnostic = Diagnostic::new( ConstantImportedAsNonConstant { diff --git a/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs b/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs index 143576f057..2f94ee0d7b 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs @@ -6,6 +6,8 @@ use ruff_python_ast::helpers::identifier_range; use ruff_python_ast::source_code::Locator; use ruff_python_semantic::scope::{Scope, ScopeKind}; +use crate::settings::types::IdentifierPattern; + /// ## What it does /// Checks for functions with "dunder" names (that is, names with two /// leading and trailing underscores) that are not documented. @@ -45,6 +47,7 @@ pub(crate) fn dunder_function_name( scope: &Scope, stmt: &Stmt, name: &str, + ignore_names: &[IdentifierPattern], locator: &Locator, ) -> Option { if matches!(scope.kind, ScopeKind::Class(_)) { @@ -57,6 +60,12 @@ pub(crate) fn dunder_function_name( if matches!(scope.kind, ScopeKind::Module) && (name == "__getattr__" || name == "__dir__") { return None; } + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(name)) + { + return None; + } Some(Diagnostic::new( DunderFunctionName, diff --git a/crates/ruff/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs b/crates/ruff/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs index c75678682c..50fcb36a1e 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs @@ -5,6 +5,8 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::identifier_range; use ruff_python_ast::source_code::Locator; +use crate::settings::types::IdentifierPattern; + /// ## What it does /// Checks for custom exception definitions that omit the `Error` suffix. /// @@ -47,7 +49,15 @@ pub(crate) fn error_suffix_on_exception_name( bases: &[Expr], name: &str, locator: &Locator, + ignore_names: &[IdentifierPattern], ) -> Option { + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(name)) + { + return None; + } + if !bases.iter().any(|base| { if let Expr::Name(ast::ExprName { id, .. }) = &base { id == "Exception" || id.ends_with("Error") diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_class_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_class_name.rs index d4f8462a55..3c14cfa361 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_class_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_class_name.rs @@ -5,6 +5,8 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::identifier_range; use ruff_python_ast::source_code::Locator; +use crate::settings::types::IdentifierPattern; + /// ## What it does /// Checks for class names that do not follow the `CamelCase` convention. /// @@ -51,8 +53,16 @@ impl Violation for InvalidClassName { pub(crate) fn invalid_class_name( class_def: &Stmt, name: &str, + ignore_names: &[IdentifierPattern], locator: &Locator, ) -> Option { + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(name)) + { + return None; + } + let stripped = name.strip_prefix('_').unwrap_or(name); if !stripped.chars().next().map_or(false, char::is_uppercase) || stripped.contains('_') { return Some(Diagnostic::new( diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_module_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_module_name.rs index aa42ba8a89..c733db5e2f 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_module_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_module_name.rs @@ -7,6 +7,8 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_stdlib::identifiers::{is_migration_name, is_module_name}; +use crate::settings::types::IdentifierPattern; + /// ## What it does /// Checks for module names that do not follow the `snake_case` naming /// convention or are otherwise invalid. @@ -46,7 +48,11 @@ impl Violation for InvalidModuleName { } /// N999 -pub(crate) fn invalid_module_name(path: &Path, package: Option<&Path>) -> Option { +pub(crate) fn invalid_module_name( + path: &Path, + package: Option<&Path>, + ignore_names: &[IdentifierPattern], +) -> Option { if !path .extension() .map_or(false, |ext| ext == "py" || ext == "pyi") @@ -61,6 +67,13 @@ pub(crate) fn invalid_module_name(path: &Path, package: Option<&Path>) -> Option path.file_stem().unwrap().to_string_lossy() }; + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(&module_name)) + { + return None; + } + // As a special case, we allow files in `versions` and `migrations` directories to start // with a digit (e.g., `0001_initial.py`), to support common conventions used by Django // and other frameworks. diff --git a/crates/ruff/src/rules/pep8_naming/rules/lowercase_imported_as_non_lowercase.rs b/crates/ruff/src/rules/pep8_naming/rules/lowercase_imported_as_non_lowercase.rs index 5d347f1f9d..dc803996d1 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/lowercase_imported_as_non_lowercase.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/lowercase_imported_as_non_lowercase.rs @@ -4,6 +4,8 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_stdlib::str; +use crate::settings::types::IdentifierPattern; + /// ## What it does /// Checks for lowercase imports that are aliased to non-lowercase names. /// @@ -47,7 +49,15 @@ pub(crate) fn lowercase_imported_as_non_lowercase( asname: &str, alias: &Alias, stmt: &Stmt, + ignore_names: &[IdentifierPattern], ) -> Option { + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(asname)) + { + return None; + } + if !str::is_upper(name) && str::is_lower(name) && asname.to_lowercase() != asname { let mut diagnostic = Diagnostic::new( LowercaseImportedAsNonLowercase { diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N801_N801.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N801_N801.py.snap new file mode 100644 index 0000000000..cbe43359c0 --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N801_N801.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N801.py:4:7: N801 Class name `stillBad` should use CapWords convention + | +2 | pass +3 | +4 | class stillBad: + | ^^^^^^^^ N801 +5 | pass + | + +N801.py:10:7: N801 Class name `STILL_BAD` should use CapWords convention + | + 8 | pass + 9 | +10 | class STILL_BAD: + | ^^^^^^^^^ N801 +11 | pass + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N807_N807.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N807_N807.py.snap new file mode 100644 index 0000000000..b3ba02329b --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N807_N807.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N807.py:4:5: N807 Function name should not start and end with `__` + | +2 | pass +3 | +4 | def __stillBad__(): + | ^^^^^^^^^^^^ N807 +5 | pass + | + +N807.py:12:9: N807 Function name should not start and end with `__` + | +10 | pass +11 | +12 | def __stillBad__(): + | ^^^^^^^^^^^^ N807 +13 | pass + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N811_N811.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N811_N811.py.snap new file mode 100644 index 0000000000..099823a45b --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N811_N811.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N811.py:2:8: N811 Constant `STILL_BAD` imported as non-constant `stillBad` + | +1 | import mod.BAD_ALLOWED as badAllowed +2 | import mod.STILL_BAD as stillBad + | ^^^^^^^^^^^^^^^^^^^^^^^^^ N811 +3 | +4 | from mod import BAD_ALLOWED as badAllowed + | + +N811.py:5:17: N811 Constant `STILL_BAD` imported as non-constant `stillBad` + | +4 | from mod import BAD_ALLOWED as badAllowed +5 | from mod import STILL_BAD as stillBad + | ^^^^^^^^^^^^^^^^^^^^^ N811 + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N812_N812.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N812_N812.py.snap new file mode 100644 index 0000000000..3f5caafe57 --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N812_N812.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N812.py:2:8: N812 Lowercase `stillbad` imported as non-lowercase `stillBad` + | +1 | import mod.badallowed as badAllowed +2 | import mod.stillbad as stillBad + | ^^^^^^^^^^^^^^^^^^^^^^^^ N812 +3 | +4 | from mod import badallowed as BadAllowed + | + +N812.py:5:17: N812 Lowercase `stillbad` imported as non-lowercase `StillBad` + | +4 | from mod import badallowed as BadAllowed +5 | from mod import stillbad as StillBad + | ^^^^^^^^^^^^^^^^^^^^ N812 + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N813_N813.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N813_N813.py.snap new file mode 100644 index 0000000000..814fb4c88d --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N813_N813.py.snap @@ -0,0 +1,29 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N813.py:2:8: N813 Camelcase `stillBad` imported as lowercase `stillbad` + | +1 | import mod.BadAllowed as badallowed +2 | import mod.stillBad as stillbad + | ^^^^^^^^^^^^^^^^^^^^^^^^ N813 +3 | +4 | from mod import BadAllowed as badallowed + | + +N813.py:5:17: N813 Camelcase `StillBad` imported as lowercase `stillbad` + | +4 | from mod import BadAllowed as badallowed +5 | from mod import StillBad as stillbad + | ^^^^^^^^^^^^^^^^^^^^ N813 +6 | +7 | from mod import BadAllowed as bad_allowed + | + +N813.py:8:17: N813 Camelcase `StillBad` imported as lowercase `still_bad` + | +7 | from mod import BadAllowed as bad_allowed +8 | from mod import StillBad as still_bad + | ^^^^^^^^^^^^^^^^^^^^^ N813 + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N814_N814.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N814_N814.py.snap new file mode 100644 index 0000000000..cd367bfe6a --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N814_N814.py.snap @@ -0,0 +1,29 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N814.py:2:8: N814 Camelcase `StillBad` imported as constant `STILLBAD` + | +1 | import mod.BadAllowed as BADALLOWED +2 | import mod.StillBad as STILLBAD + | ^^^^^^^^^^^^^^^^^^^^^^^^ N814 +3 | +4 | from mod import BadAllowed as BADALLOWED + | + +N814.py:5:17: N814 Camelcase `StillBad` imported as constant `STILLBAD` + | +4 | from mod import BadAllowed as BADALLOWED +5 | from mod import StillBad as STILLBAD + | ^^^^^^^^^^^^^^^^^^^^ N814 +6 | +7 | from mod import BadAllowed as BAD_ALLOWED + | + +N814.py:8:17: N814 Camelcase `StillBad` imported as constant `STILL_BAD` + | +7 | from mod import BadAllowed as BAD_ALLOWED +8 | from mod import StillBad as STILL_BAD + | ^^^^^^^^^^^^^^^^^^^^^ N814 + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N817_N817.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N817_N817.py.snap new file mode 100644 index 0000000000..e67415d87a --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N817_N817.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N817.py:2:8: N817 CamelCase `StillBad` imported as acronym `SB` + | +1 | import mod.BadAllowed as BA +2 | import mod.StillBad as SB + | ^^^^^^^^^^^^^^^^^^ N817 +3 | +4 | from mod import BadAllowed as BA + | + +N817.py:5:17: N817 CamelCase `StillBad` imported as acronym `SB` + | +4 | from mod import BadAllowed as BA +5 | from mod import StillBad as SB + | ^^^^^^^^^^^^^^ N817 + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N818_N818.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N818_N818.py.snap new file mode 100644 index 0000000000..b2df749de6 --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N818_N818.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N818.py:4:7: N818 Exception name `StillBad` should be named with an Error suffix + | +2 | pass +3 | +4 | class StillBad(Exception): + | ^^^^^^^^ N818 +5 | pass + | + +N818.py:10:7: N818 Exception name `StillBad` should be named with an Error suffix + | + 8 | pass + 9 | +10 | class StillBad(AnotherError): + | ^^^^^^^^ N818 +11 | pass + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N999_N999__badAllowed____init__.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N999_N999__badAllowed____init__.py.snap new file mode 100644 index 0000000000..eb9fd7a59b --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N999_N999__badAllowed____init__.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- + From 732b0405d7f00e09080b6650e157805924c447d3 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 14 Jun 2023 11:17:09 -0400 Subject: [PATCH 052/447] Remove `FixMode::None` (#5087) ## Summary We now _always_ generate fixes, so `FixMode::None` and `FixMode::Generate` are redundant. We can also remove the TODO around `--fix-dry-run`, since that's our default behavior. Closes #5081. --- crates/ruff/src/settings/flags.rs | 13 +----- crates/ruff_cli/src/commands/run.rs | 8 ++-- crates/ruff_cli/src/diagnostics.rs | 66 +++++++++++++++-------------- crates/ruff_cli/src/lib.rs | 12 ++---- crates/ruff_cli/src/printer.rs | 6 +-- 5 files changed, 46 insertions(+), 59 deletions(-) diff --git a/crates/ruff/src/settings/flags.rs b/crates/ruff/src/settings/flags.rs index f7e761ade1..a1ce403194 100644 --- a/crates/ruff/src/settings/flags.rs +++ b/crates/ruff/src/settings/flags.rs @@ -1,19 +1,8 @@ -#[derive(Debug, Copy, Clone, Hash)] +#[derive(Debug, Copy, Clone, Hash, is_macro::Is)] pub enum FixMode { Generate, Apply, Diff, - None, -} - -impl From for FixMode { - fn from(value: bool) -> Self { - if value { - Self::Apply - } else { - Self::None - } - } } #[derive(Debug, Copy, Clone, Hash, result_like::BoolLike)] diff --git a/crates/ruff_cli/src/commands/run.rs b/crates/ruff_cli/src/commands/run.rs index 78ef048870..c299be0c09 100644 --- a/crates/ruff_cli/src/commands/run.rs +++ b/crates/ruff_cli/src/commands/run.rs @@ -247,21 +247,21 @@ mod test { &overrides, Cache::Disabled, Noqa::Enabled, - FixMode::None, + FixMode::Generate, )?; let printer = Printer::new( SerializationFormat::Text, LogLevel::Default, - FixMode::None, + FixMode::Generate, Flags::SHOW_VIOLATIONS, ); let mut writer: Vec = Vec::new(); - // Mute the terminal color codes + // Mute the terminal color codes. colored::control::set_override(false); printer.write_once(&diagnostics, &mut writer)?; // TODO(konstin): Set jupyter notebooks as none-fixable for now - // TODO(konstin) 2: Make jupyter notebooks fixable + // TODO(konstin): Make jupyter notebooks fixable let expected = format!( "{valid_ipynb}:cell 1:2:5: F841 [*] Local variable `x` is assigned to but never used {valid_ipynb}:cell 3:1:24: B006 Do not use mutable data structures for argument defaults diff --git a/crates/ruff_cli/src/diagnostics.rs b/crates/ruff_cli/src/diagnostics.rs index 7a5ffa49d2..f8efe174c2 100644 --- a/crates/ruff_cli/src/diagnostics.rs +++ b/crates/ruff_cli/src/diagnostics.rs @@ -110,10 +110,7 @@ pub(crate) fn lint_path( // to cache `fixer::Mode::Apply`, since a file either has no fixes, or we'll // write the fixes to disk, thus invalidating the cache. But it's a bit hard // to reason about. We need to come up with a better solution here.) - let metadata = if cache.into() - && noqa.into() - && matches!(autofix, flags::FixMode::None | flags::FixMode::Generate) - { + let metadata = if cache.into() && noqa.into() && autofix.is_generate() { let metadata = path.metadata()?; if let Some((messages, imports)) = cache::get(path, package, &metadata, settings) { debug!("Cache hit for: {}", path.display()); @@ -170,23 +167,25 @@ pub(crate) fn lint_path( &mut source_kind, ) { if !fixed.is_empty() { - if matches!(autofix, flags::FixMode::Apply) { - match &source_kind { + match autofix { + flags::FixMode::Apply => match &source_kind { SourceKind::Python(_) => { write(path, transformed.as_bytes())?; } SourceKind::Jupyter(notebook) => { notebook.write(path)?; } + }, + flags::FixMode::Diff => { + let mut stdout = io::stdout().lock(); + TextDiff::from_lines(contents.as_str(), &transformed) + .unified_diff() + .header(&fs::relativize_path(path), &fs::relativize_path(path)) + .to_writer(&mut stdout)?; + stdout.write_all(b"\n")?; + stdout.flush()?; } - } else if matches!(autofix, flags::FixMode::Diff) { - let mut stdout = io::stdout().lock(); - TextDiff::from_lines(contents.as_str(), &transformed) - .unified_diff() - .header(&fs::relativize_path(path), &fs::relativize_path(path)) - .to_writer(&mut stdout)?; - stdout.write_all(b"\n")?; - stdout.flush()?; + flags::FixMode::Generate => {} } } (result, fixed) @@ -269,23 +268,28 @@ pub(crate) fn lint_stdin( settings, &mut source_kind, ) { - if matches!(autofix, flags::FixMode::Apply) { - // Write the contents to stdout, regardless of whether any errors were fixed. - io::stdout().write_all(transformed.as_bytes())?; - } else if matches!(autofix, flags::FixMode::Diff) { - // But only write a diff if it's non-empty. - if !fixed.is_empty() { - let text_diff = TextDiff::from_lines(contents, &transformed); - let mut unified_diff = text_diff.unified_diff(); - if let Some(path) = path { - unified_diff.header(&fs::relativize_path(path), &fs::relativize_path(path)); - } - - let mut stdout = io::stdout().lock(); - unified_diff.to_writer(&mut stdout)?; - stdout.write_all(b"\n")?; - stdout.flush()?; + match autofix { + flags::FixMode::Apply => { + // Write the contents to stdout, regardless of whether any errors were fixed. + io::stdout().write_all(transformed.as_bytes())?; } + flags::FixMode::Diff => { + // But only write a diff if it's non-empty. + if !fixed.is_empty() { + let text_diff = TextDiff::from_lines(contents, &transformed); + let mut unified_diff = text_diff.unified_diff(); + if let Some(path) = path { + unified_diff + .header(&fs::relativize_path(path), &fs::relativize_path(path)); + } + + let mut stdout = io::stdout().lock(); + unified_diff.to_writer(&mut stdout)?; + stdout.write_all(b"\n")?; + stdout.flush()?; + } + } + flags::FixMode::Generate => {} } (result, fixed) @@ -301,7 +305,7 @@ pub(crate) fn lint_stdin( let fixed = FxHashMap::default(); // Write the contents to stdout anyway. - if matches!(autofix, flags::FixMode::Apply) { + if autofix.is_apply() { io::stdout().write_all(contents.as_bytes())?; } diff --git a/crates/ruff_cli/src/lib.rs b/crates/ruff_cli/src/lib.rs index 7f7a70f2eb..a9b53365fb 100644 --- a/crates/ruff_cli/src/lib.rs +++ b/crates/ruff_cli/src/lib.rs @@ -192,23 +192,17 @@ fn check(args: CheckArgs, log_level: LogLevel) -> Result { } = pyproject_config.settings.cli; // Autofix rules are as follows: + // - By default, generate all fixes, but don't apply them to the filesystem. // - If `--fix` or `--fix-only` is set, always apply fixes to the filesystem (or // print them to stdout, if we're reading from stdin). - // - Otherwise, if `--format json` is set, generate the fixes (so we print them - // out as part of the JSON payload), but don't write them to disk. // - If `--diff` or `--fix-only` are set, don't print any violations (only // fixes). - // TODO(charlie): Consider adding ESLint's `--fix-dry-run`, which would generate - // but not apply fixes. That would allow us to avoid special-casing JSON - // here. let autofix = if cli.diff { flags::FixMode::Diff } else if fix || fix_only { flags::FixMode::Apply - } else if matches!(format, SerializationFormat::Json) { - flags::FixMode::Generate } else { - flags::FixMode::None + flags::FixMode::Generate }; let cache = !cli.no_cache; let noqa = !cli.ignore_noqa; @@ -238,7 +232,7 @@ fn check(args: CheckArgs, log_level: LogLevel) -> Result { } if cli.add_noqa { - if !matches!(autofix, flags::FixMode::None) { + if !autofix.is_generate() { warn_user_once!("--fix is incompatible with --add-noqa."); } let modifications = diff --git a/crates/ruff_cli/src/printer.rs b/crates/ruff_cli/src/printer.rs index 707460675f..2062b9e823 100644 --- a/crates/ruff_cli/src/printer.rs +++ b/crates/ruff_cli/src/printer.rs @@ -141,9 +141,9 @@ impl Printer { .sum::(); if fixed > 0 { let s = if fixed == 1 { "" } else { "s" }; - if matches!(self.autofix_level, flags::FixMode::Apply) { + if self.autofix_level.is_apply() { writeln!(stdout, "Fixed {fixed} error{s}.")?; - } else if matches!(self.autofix_level, flags::FixMode::Diff) { + } else { writeln!(stdout, "Would fix {fixed} error{s}.")?; } } @@ -391,7 +391,7 @@ const fn show_fix_status(autofix_level: flags::FixMode) -> bool { // this pass! (We're occasionally unable to determine whether a specific // violation is fixable without trying to fix it, so if autofix is not // enabled, we may inadvertently indicate that a rule is fixable.) - !matches!(autofix_level, flags::FixMode::Apply) + !autofix_level.is_apply() } fn print_fix_summary(stdout: &mut T, fixed: &FxHashMap) -> Result<()> { From c1fd2c8a8ec951ef5820e3221d68c87c8b20c4c3 Mon Sep 17 00:00:00 2001 From: MT BENTERKI Date: Wed, 14 Jun 2023 17:17:35 +0200 Subject: [PATCH 053/447] Update tutorial doc typo (#5088) --- docs/tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index 45488161f9..0263712546 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -46,7 +46,7 @@ Ruff identified an unused import, which is a common error in Python code. Ruff c ```shell ❯ ruff check --fix . -Found 1 error (1 fixed, 0 renumbersing). +Found 1 error (1 fixed, 0 remaining). ``` Running `git diff` shows the following: From 916f0889f860d80c4257dace70c5f6a21eaf4e96 Mon Sep 17 00:00:00 2001 From: konstin Date: Wed, 14 Jun 2023 17:55:12 +0200 Subject: [PATCH 054/447] Add pyproject.toml to include option doc (#5080) Fixes an oversight where i didn't update this initially --- crates/ruff/src/settings/options.rs | 6 ++++-- ruff.schema.json | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/ruff/src/settings/options.rs b/crates/ruff/src/settings/options.rs index 7c99dd8a2c..7015b29b60 100644 --- a/crates/ruff/src/settings/options.rs +++ b/crates/ruff/src/settings/options.rs @@ -288,7 +288,7 @@ pub struct Options { /// re-exported with a redundant alias (e.g., `import os as os`). pub ignore_init_module_imports: Option, #[option( - default = r#"["*.py", "*.pyi"]"#, + default = r#"["*.py", "*.pyi", "**/pyproject.toml"]"#, value_type = "list[str]", example = r#" include = ["*.py"] @@ -297,7 +297,9 @@ pub struct Options { /// A list of file patterns to include when linting. /// /// Inclusion are based on globs, and should be single-path patterns, like - /// `*.pyw`, to include any file with the `.pyw` extension. + /// `*.pyw`, to include any file with the `.pyw` extension. `pyproject.toml` is + /// included here not for configuration but because we lint whether e.g. the + /// `[project]` matches the schema. /// /// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax). pub include: Option>, diff --git a/ruff.schema.json b/ruff.schema.json index d7d091bb3f..e0342d8e3e 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -355,7 +355,7 @@ ] }, "include": { - "description": "A list of file patterns to include when linting.\n\nInclusion are based on globs, and should be single-path patterns, like `*.pyw`, to include any file with the `.pyw` extension.\n\nFor more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).", + "description": "A list of file patterns to include when linting.\n\nInclusion are based on globs, and should be single-path patterns, like `*.pyw`, to include any file with the `.pyw` extension. `pyproject.toml` is included here not for configuration but because we lint whether e.g. the `[project]` matches the schema.\n\nFor more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).", "type": [ "array", "null" From c992cfa76e0f00a307a44cf6f73748ffb5215be4 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 14 Jun 2023 13:49:37 -0400 Subject: [PATCH 055/447] Make some of `ruff_python_semantic` `pub(crate)` (#5093) --- crates/ruff_python_semantic/src/definition.rs | 2 +- crates/ruff_python_semantic/src/globals.rs | 4 ++-- crates/ruff_python_semantic/src/node.rs | 24 +++++++++---------- crates/ruff_python_semantic/src/reference.rs | 4 ++-- crates/ruff_python_semantic/src/scope.rs | 10 ++++---- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/crates/ruff_python_semantic/src/definition.rs b/crates/ruff_python_semantic/src/definition.rs index df75490f5e..6d73624a66 100644 --- a/crates/ruff_python_semantic/src/definition.rs +++ b/crates/ruff_python_semantic/src/definition.rs @@ -115,7 +115,7 @@ impl<'a> Definitions<'a> { /// /// Members are assumed to be pushed in traversal order, such that parents are pushed before /// their children. - pub fn push_member(&mut self, member: Member<'a>) -> DefinitionId { + pub(crate) fn push_member(&mut self, member: Member<'a>) -> DefinitionId { self.0.push(Definition::Member(member)) } diff --git a/crates/ruff_python_semantic/src/globals.rs b/crates/ruff_python_semantic/src/globals.rs index dd4b2c460a..876ecfb628 100644 --- a/crates/ruff_python_semantic/src/globals.rs +++ b/crates/ruff_python_semantic/src/globals.rs @@ -49,11 +49,11 @@ impl<'a> Globals<'a> { builder.finish() } - pub fn get(&self, name: &str) -> Option<&TextRange> { + pub(crate) fn get(&self, name: &str) -> Option<&TextRange> { self.0.get(name) } - pub fn iter(&self) -> impl Iterator + '_ { + pub(crate) fn iter(&self) -> impl Iterator + '_ { self.0.iter() } } diff --git a/crates/ruff_python_semantic/src/node.rs b/crates/ruff_python_semantic/src/node.rs index dd71fd5f1d..6cdafd55b4 100644 --- a/crates/ruff_python_semantic/src/node.rs +++ b/crates/ruff_python_semantic/src/node.rs @@ -37,7 +37,7 @@ impl<'a> Nodes<'a> { /// Inserts a new node into the node tree and returns its unique id. /// /// Panics if a node with the same pointer already exists. - pub fn insert(&mut self, stmt: &'a Stmt, parent: Option) -> NodeId { + pub(crate) fn insert(&mut self, stmt: &'a Stmt, parent: Option) -> NodeId { let next_id = self.nodes.next_index(); if let Some(existing_id) = self.node_to_id.insert(RefEquality(stmt), next_id) { panic!("Node already exists with id {existing_id:?}"); @@ -61,23 +61,23 @@ impl<'a> Nodes<'a> { self.nodes[node_id].parent } - /// Return the depth of the node. - #[inline] - pub fn depth(&self, node_id: NodeId) -> u32 { - self.nodes[node_id].depth - } - - /// Returns an iterator over all [`NodeId`] ancestors, starting from the given [`NodeId`]. - pub fn ancestor_ids(&self, node_id: NodeId) -> impl Iterator + '_ { - std::iter::successors(Some(node_id), |&node_id| self.nodes[node_id].parent) - } - /// Return the parent of the given node. pub fn parent(&self, node: &'a Stmt) -> Option<&'a Stmt> { let node_id = self.node_to_id.get(&RefEquality(node))?; let parent_id = self.nodes[*node_id].parent?; Some(self[parent_id]) } + + /// Return the depth of the node. + #[inline] + pub(crate) fn depth(&self, node_id: NodeId) -> u32 { + self.nodes[node_id].depth + } + + /// Returns an iterator over all [`NodeId`] ancestors, starting from the given [`NodeId`]. + pub(crate) fn ancestor_ids(&self, node_id: NodeId) -> impl Iterator + '_ { + std::iter::successors(Some(node_id), |&node_id| self.nodes[node_id].parent) + } } impl<'a> Index for Nodes<'a> { diff --git a/crates/ruff_python_semantic/src/reference.rs b/crates/ruff_python_semantic/src/reference.rs index 0626a5c808..d19b03194a 100644 --- a/crates/ruff_python_semantic/src/reference.rs +++ b/crates/ruff_python_semantic/src/reference.rs @@ -36,11 +36,11 @@ pub struct ReferenceId; /// The references of a program indexed by [`ReferenceId`]. #[derive(Debug, Default)] -pub struct References(IndexVec); +pub(crate) struct References(IndexVec); impl References { /// Pushes a new [`Reference`] and returns its [`ReferenceId`]. - pub fn push( + pub(crate) fn push( &mut self, scope_id: ScopeId, range: TextRange, diff --git a/crates/ruff_python_semantic/src/scope.rs b/crates/ruff_python_semantic/src/scope.rs index 22f0983915..e5100b3859 100644 --- a/crates/ruff_python_semantic/src/scope.rs +++ b/crates/ruff_python_semantic/src/scope.rs @@ -124,12 +124,12 @@ impl<'a> Scope<'a> { } /// Set the globals pointer for this scope. - pub fn set_globals_id(&mut self, globals: GlobalsId) { + pub(crate) fn set_globals_id(&mut self, globals: GlobalsId) { self.globals_id = Some(globals); } /// Returns the globals pointer for this scope. - pub fn globals_id(&self) -> Option { + pub(crate) fn globals_id(&self) -> Option { self.globals_id } @@ -196,17 +196,17 @@ pub struct Scopes<'a>(IndexVec>); impl<'a> Scopes<'a> { /// Returns a reference to the global scope - pub fn global(&self) -> &Scope<'a> { + pub(crate) fn global(&self) -> &Scope<'a> { &self[ScopeId::global()] } /// Returns a mutable reference to the global scope - pub fn global_mut(&mut self) -> &mut Scope<'a> { + pub(crate) fn global_mut(&mut self) -> &mut Scope<'a> { &mut self[ScopeId::global()] } /// Pushes a new scope and returns its unique id - pub fn push_scope(&mut self, kind: ScopeKind<'a>, parent: ScopeId) -> ScopeId { + pub(crate) fn push_scope(&mut self, kind: ScopeKind<'a>, parent: ScopeId) -> ScopeId { let next_id = ScopeId::new(self.0.len()); self.0.push(Scope::local(kind, parent)); next_id From a33bbe63350791238b3ba1b33dec9467531c4ea1 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 14 Jun 2023 13:54:35 -0400 Subject: [PATCH 056/447] Track "delayed" annotations in the semantic model (#5070) ## Summary This PR tackles a corner case that we'll need to support local symbol renaming. It relates to a nuance in how we want handle annotations (i.e., `AnnAssign` statements with no value, like `x: int` in a function body). When we see a statement like: ```python x: int ``` We create a `BindingKind::Annotation` for `x`. This is a special `BindingKind` that the resolver isn't allowed to return. For example, given: ```python x: int print(x) ``` The second line will yield an `undefined-name` error. So why does this `BindingKind` exist at all? In Pyflakes, to support the `unused-annotation` lint: ```python def f(): x: int # unused-annotation ``` If we don't track `BindingKind::Annotation`, we can't lint for unused variables that are only "defined" via annotations. There are a few other wrinkles to `BindingKind::Annotation`. One is that, if a binding already exists in the scope, we actually just discard the `BindingKind`. So in this case: ```python x = 1 x: int ``` When we go to create the `BindingKind::Annotation` for the second statement, we notice that (1) we're creating an annotation but (2) the scope already has binding for the name -- so we just drop the binding on the floor. This has the nice property that annotations aren't considered to "shadow" another binding, which is important in a bunch of places (e.g., if we have `import os; os: int`, we still consider `os` to be an import, as we should). But it also means that these "delayed" annotations are one of the few remaining references that we don't track anywhere in the semantic model. This PR adds explicit support for these via a new `delayed_annotations` attribute on the semantic model. These should be extremely rare, but we do need to track them if we want to support local symbol renaming. ### This isn't the right way to model this This isn't the right way to model this. Here's an alternative: - Remove `BindingKind::Annotation`, and treat annotations as their own, separate concept. - Instead of storing a map from name to `BindingId` on each `Scope`, store a map from name to... `SymbolId`. - Introduce a `Symbol` abstraction, where a symbol can point to a current binding, and a list of annotations, like: ```rust pub struct Symbol { binding: Option, annotations: Vec } ``` If we did this, we could appropriately model the semantics described above. When we go to resolve a binding, we ignore annotations (always). When we try to find unused variables, we look through the list of symbols, and have sufficient information to discriminate between annotations and bound variables. Etc. The main downside of this `Symbol`-based approach is that it's going to take a lot more work to implement, and it'll be less performant (we'll be storing more data per symbol, and our binding lookups will have an added layer of indirection). --- crates/ruff/src/checkers/ast/mod.rs | 20 ++++++------- crates/ruff_python_semantic/src/model.rs | 37 ++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index db4d89f87c..3940ee8e16 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -4328,10 +4328,16 @@ impl<'a> Checker<'a> { } // If there's an existing binding in this scope, copy its references. - if let Some(shadowed) = self.semantic_model.scopes[scope_id] - .get(name) - .map(|binding_id| &self.semantic_model.bindings[binding_id]) - { + if let Some(shadowed_id) = self.semantic_model.scopes[scope_id].get(name) { + // If this is an annotation, and we already have an existing value in the same scope, + // don't treat it as an assignment, but track it as a delayed annotation. + if self.semantic_model.binding(binding_id).kind.is_annotation() { + self.semantic_model + .add_delayed_annotation(shadowed_id, binding_id); + return binding_id; + } + + let shadowed = &self.semantic_model.bindings[shadowed_id]; match &shadowed.kind { BindingKind::Builtin | BindingKind::Deletion | BindingKind::UnboundException => { // Avoid overriding builtins. @@ -4348,12 +4354,6 @@ impl<'a> Checker<'a> { self.semantic_model.bindings[binding_id].references = references; } } - - // If this is an annotation, and we already have an existing value in the same scope, - // don't treat it as an assignment (i.e., avoid adding it to the scope). - if self.semantic_model.binding(binding_id).kind.is_annotation() { - return binding_id; - } } // Add the binding to the scope. diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 2e2df908ab..944e4f4f57 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -71,6 +71,29 @@ pub struct SemanticModel<'a> { /// despite the fact that they're in different scopes. pub shadowed_bindings: HashMap>, + /// Map from binding index to indexes of bindings that annotate it (in the same scope). + /// + /// For example: + /// ```python + /// x = 1 + /// x: int + /// ``` + /// + /// In this case, the binding created by `x = 1` is annotated by the binding created by + /// `x: int`. We don't consider the latter binding to _shadow_ the former, because it doesn't + /// change the value of the binding, and so we don't store in on the scope. But we _do_ want to + /// track the annotation in some form, since it's a reference to `x`. + /// + /// Note that, given: + /// ```python + /// x: int + /// ``` + /// + /// In this case, we _do_ store the binding created by `x: int` directly on the scope, and not + /// as a delayed annotation. Annotations are thus treated as bindings only when they are the + /// first binding in a scope; any annotations that follow are treated as "delayed" annotations. + delayed_annotations: HashMap, BuildNoHashHasher>, + /// Body iteration; used to peek at siblings. pub body: &'a [Stmt], pub body_index: usize, @@ -99,6 +122,7 @@ impl<'a> SemanticModel<'a> { references: References::default(), globals: GlobalsArena::default(), shadowed_bindings: IntMap::default(), + delayed_annotations: IntMap::default(), body: &[], body_index: 0, flags: SemanticModelFlags::new(path), @@ -729,6 +753,19 @@ impl<'a> SemanticModel<'a> { self.bindings[binding_id].references.push(reference_id); } + /// Add a [`BindingId`] to the list of delayed annotations for the given [`BindingId`]. + pub fn add_delayed_annotation(&mut self, binding_id: BindingId, annotation_id: BindingId) { + self.delayed_annotations + .entry(binding_id) + .or_insert_with(Vec::new) + .push(annotation_id); + } + + /// Return the list of delayed annotations for the given [`BindingId`]. + pub fn delayed_annotations(&self, binding_id: BindingId) -> Option<&[BindingId]> { + self.delayed_annotations.get(&binding_id).map(Vec::as_slice) + } + /// Return the [`ExecutionContext`] of the current scope. pub const fn execution_context(&self) -> ExecutionContext { if self.in_type_checking_block() From 86ff1febeaf45a73ba430c7c60c86e8fea9fc4f8 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 14 Jun 2023 14:23:38 -0400 Subject: [PATCH 057/447] Re-export `ruff_python_semantic` members (#5094) ## Summary This PR adds a more unified public API to `ruff_python_semantic`, so that we don't need to do deeply nested imports all over the place. --- crates/ruff/src/checkers/ast/deferred.rs | 2 +- crates/ruff/src/checkers/ast/mod.rs | 69 +++++++++---------- crates/ruff/src/docstrings/extraction.rs | 2 +- crates/ruff/src/docstrings/mod.rs | 2 +- crates/ruff/src/importer/mod.rs | 2 +- crates/ruff/src/rules/flake8_2020/helpers.rs | 3 +- .../src/rules/flake8_annotations/helpers.rs | 3 +- .../flake8_annotations/rules/definition.rs | 10 ++- .../ruff/src/rules/flake8_bandit/helpers.rs | 2 +- .../flake8_bandit/rules/shell_injection.rs | 2 +- .../rules/abstract_base_class.rs | 2 +- .../rules/cached_instance_method.rs | 2 +- .../rules/function_call_argument_default.rs | 2 +- .../rules/mutable_argument_default.rs | 2 +- .../rules/zip_without_explicit_strict.rs | 2 +- .../src/rules/flake8_django/rules/helpers.rs | 2 +- .../rules/locals_in_render_function.rs | 2 +- .../rules/model_without_dunder_str.rs | 2 +- .../rules/unordered_body_content_in_model.rs | 2 +- .../rules/logging_call.rs | 3 +- .../rules/iter_method_return_iterable.rs | 2 +- .../flake8_pyi/rules/non_self_return_type.rs | 3 +- .../rules/flake8_pyi/rules/simple_defaults.rs | 3 +- .../flake8_pytest_style/rules/fixture.rs | 2 +- .../flake8_pytest_style/rules/helpers.rs | 2 +- .../rules/flake8_pytest_style/rules/raises.rs | 2 +- .../src/rules/flake8_return/rules/function.rs | 2 +- .../rules/private_member_access.rs | 2 +- .../src/rules/flake8_simplify/rules/ast_if.rs | 2 +- .../flake8_simplify/rules/ast_unary_op.rs | 2 +- .../rules/open_file_with_context_handler.rs | 2 +- .../src/rules/flake8_type_checking/helpers.rs | 4 +- .../runtime_import_in_type_checking_block.rs | 4 +- .../rules/typing_only_runtime_import.rs | 5 +- .../rules/unused_arguments.rs | 15 ++-- crates/ruff/src/rules/pandas_vet/helpers.rs | 3 +- .../pandas_vet/rules/inplace_argument.rs | 2 +- crates/ruff/src/rules/pep8_naming/helpers.rs | 2 +- .../pep8_naming/rules/dunder_function_name.rs | 2 +- ...id_first_argument_name_for_class_method.rs | 2 +- .../invalid_first_argument_name_for_method.rs | 2 +- .../rules/invalid_function_name.rs | 2 +- .../pycodestyle/rules/lambda_assignment.rs | 2 +- crates/ruff/src/rules/pydocstyle/helpers.rs | 3 +- .../rules/blank_before_after_class.rs | 2 +- .../rules/blank_before_after_function.rs | 2 +- .../src/rules/pydocstyle/rules/capitalized.rs | 2 +- .../src/rules/pydocstyle/rules/if_needed.rs | 2 +- .../rules/multi_line_summary_start.rs | 2 +- .../rules/pydocstyle/rules/no_signature.rs | 2 +- .../pydocstyle/rules/non_imperative_mood.rs | 2 +- .../src/rules/pydocstyle/rules/not_missing.rs | 2 +- .../src/rules/pydocstyle/rules/sections.rs | 2 +- .../pyflakes/rules/return_outside_function.rs | 2 +- .../rules/pyflakes/rules/undefined_export.rs | 2 +- .../rules/pyflakes/rules/unused_annotation.rs | 2 +- .../src/rules/pyflakes/rules/unused_import.rs | 4 +- .../rules/pyflakes/rules/unused_variable.rs | 2 +- .../pyflakes/rules/yield_outside_function.rs | 2 +- crates/ruff/src/rules/pylint/helpers.rs | 12 ++-- .../src/rules/pylint/rules/nested_min_max.rs | 2 +- .../rules/pylint/rules/redefined_loop_name.rs | 2 +- ...convert_named_tuple_functional_to_class.rs | 2 +- .../convert_typed_dict_functional_to_class.rs | 2 +- .../rules/pyupgrade/rules/os_error_alias.rs | 2 +- crates/ruff/src/rules/ruff/rules/helpers.rs | 2 +- .../src/rules/ruff/rules/implicit_optional.rs | 2 +- crates/ruff/src/rules/tryceratops/helpers.rs | 2 +- crates/ruff_python_semantic/src/lib.rs | 25 ++++--- 69 files changed, 129 insertions(+), 146 deletions(-) diff --git a/crates/ruff/src/checkers/ast/deferred.rs b/crates/ruff/src/checkers/ast/deferred.rs index ab74d51234..e72f7dbd46 100644 --- a/crates/ruff/src/checkers/ast/deferred.rs +++ b/crates/ruff/src/checkers/ast/deferred.rs @@ -1,7 +1,7 @@ use ruff_text_size::TextRange; use rustpython_parser::ast::Expr; -use ruff_python_semantic::model::Snapshot; +use ruff_python_semantic::Snapshot; /// A collection of AST nodes that are deferred for later analysis. /// Used to, e.g., store functions, whose bodies shouldn't be analyzed until all diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 3940ee8e16..6fa683363c 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -18,19 +18,13 @@ use ruff_python_ast::types::Node; use ruff_python_ast::typing::{parse_type_annotation, AnnotationKind}; use ruff_python_ast::visitor::{walk_excepthandler, walk_pattern, Visitor}; use ruff_python_ast::{cast, helpers, str, visitor}; -use ruff_python_semantic::analyze; -use ruff_python_semantic::analyze::branch_detection; -use ruff_python_semantic::analyze::typing::{Callable, SubscriptKind}; -use ruff_python_semantic::analyze::visibility::ModuleSource; -use ruff_python_semantic::binding::{ - Binding, BindingFlags, BindingId, BindingKind, Exceptions, Export, FromImportation, - Importation, StarImportation, SubmoduleImportation, +use ruff_python_semantic::analyze::{branch_detection, typing, visibility}; +use ruff_python_semantic::{ + Binding, BindingFlags, BindingId, BindingKind, ContextualizedDefinition, Exceptions, + ExecutionContext, Export, FromImportation, Globals, Importation, Module, ModuleKind, + ResolvedRead, Scope, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImportation, + SubmoduleImportation, }; -use ruff_python_semantic::context::ExecutionContext; -use ruff_python_semantic::definition::{ContextualizedDefinition, Module, ModuleKind}; -use ruff_python_semantic::globals::Globals; -use ruff_python_semantic::model::{ResolvedRead, SemanticModel, SemanticModelFlags}; -use ruff_python_semantic::scope::{Scope, ScopeId, ScopeKind}; use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS}; use ruff_python_stdlib::path::is_python_stub_file; @@ -2079,7 +2073,7 @@ where ) => { self.visit_boolean_test(test); - if analyze::typing::is_type_checking_block(stmt_if, &self.semantic_model) { + if typing::is_type_checking_block(stmt_if, &self.semantic_model) { if self.semantic_model.at_top_level() { self.importer.visit_type_checking_block(stmt); } @@ -2179,7 +2173,7 @@ where Rule::NonPEP604Annotation, ]) { if let Some(operator) = - analyze::typing::to_pep604_operator(value, slice, &self.semantic_model) + typing::to_pep604_operator(value, slice, &self.semantic_model) { if self.enabled(Rule::FutureRewritableTypeAnnotation) { if self.settings.target_version < PythonVersion::Py310 @@ -2211,7 +2205,7 @@ where if self.settings.target_version < PythonVersion::Py39 && !self.semantic_model.future_annotations() && self.semantic_model.in_annotation() - && analyze::typing::is_pep585_generic(value, &self.semantic_model) + && typing::is_pep585_generic(value, &self.semantic_model) { flake8_future_annotations::rules::future_required_type_annotation( self, @@ -2284,7 +2278,7 @@ where Rule::NonPEP585Annotation, ]) { if let Some(replacement) = - analyze::typing::to_pep585_generic(expr, &self.semantic_model) + typing::to_pep585_generic(expr, &self.semantic_model) { if self.enabled(Rule::FutureRewritableTypeAnnotation) { if self.settings.target_version < PythonVersion::Py39 @@ -2360,8 +2354,7 @@ where Rule::FutureRewritableTypeAnnotation, Rule::NonPEP585Annotation, ]) { - if let Some(replacement) = - analyze::typing::to_pep585_generic(expr, &self.semantic_model) + if let Some(replacement) = typing::to_pep585_generic(expr, &self.semantic_model) { if self.enabled(Rule::FutureRewritableTypeAnnotation) { if self.settings.target_version < PythonVersion::Py39 @@ -3576,27 +3569,27 @@ where .semantic_model .match_typing_call_path(&call_path, "cast") { - Some(Callable::Cast) + Some(typing::Callable::Cast) } else if self .semantic_model .match_typing_call_path(&call_path, "NewType") { - Some(Callable::NewType) + Some(typing::Callable::NewType) } else if self .semantic_model .match_typing_call_path(&call_path, "TypeVar") { - Some(Callable::TypeVar) + Some(typing::Callable::TypeVar) } else if self .semantic_model .match_typing_call_path(&call_path, "NamedTuple") { - Some(Callable::NamedTuple) + Some(typing::Callable::NamedTuple) } else if self .semantic_model .match_typing_call_path(&call_path, "TypedDict") { - Some(Callable::TypedDict) + Some(typing::Callable::TypedDict) } else if [ "Arg", "DefaultArg", @@ -3608,15 +3601,15 @@ where .iter() .any(|target| call_path.as_slice() == ["mypy_extensions", target]) { - Some(Callable::MypyExtension) + Some(typing::Callable::MypyExtension) } else if call_path.as_slice() == ["", "bool"] { - Some(Callable::Bool) + Some(typing::Callable::Bool) } else { None } }); match callable { - Some(Callable::Bool) => { + Some(typing::Callable::Bool) => { self.visit_expr(func); let mut args = args.iter(); if let Some(arg) = args.next() { @@ -3626,7 +3619,7 @@ where self.visit_expr(arg); } } - Some(Callable::Cast) => { + Some(typing::Callable::Cast) => { self.visit_expr(func); let mut args = args.iter(); if let Some(arg) = args.next() { @@ -3636,7 +3629,7 @@ where self.visit_expr(arg); } } - Some(Callable::NewType) => { + Some(typing::Callable::NewType) => { self.visit_expr(func); let mut args = args.iter(); if let Some(arg) = args.next() { @@ -3646,7 +3639,7 @@ where self.visit_type_definition(arg); } } - Some(Callable::TypeVar) => { + Some(typing::Callable::TypeVar) => { self.visit_expr(func); let mut args = args.iter(); if let Some(arg) = args.next() { @@ -3670,7 +3663,7 @@ where } } } - Some(Callable::NamedTuple) => { + Some(typing::Callable::NamedTuple) => { self.visit_expr(func); // Ex) NamedTuple("a", [("a", int)]) @@ -3707,7 +3700,7 @@ where self.visit_type_definition(value); } } - Some(Callable::TypedDict) => { + Some(typing::Callable::TypedDict) => { self.visit_expr(func); // Ex) TypedDict("a", {"a": int}) @@ -3739,7 +3732,7 @@ where self.visit_type_definition(value); } } - Some(Callable::MypyExtension) => { + Some(typing::Callable::MypyExtension) => { self.visit_expr(func); let mut args = args.iter(); @@ -3801,7 +3794,7 @@ where self.semantic_model.flags |= SemanticModelFlags::SUBSCRIPT; visitor::walk_expr(self, expr); } else { - match analyze::typing::match_annotated_subscript( + match typing::match_annotated_subscript( value, &self.semantic_model, self.settings.typing_modules.iter().map(String::as_str), @@ -3810,13 +3803,13 @@ where Some(subscript) => { match subscript { // Ex) Optional[int] - SubscriptKind::AnnotatedSubscript => { + typing::SubscriptKind::AnnotatedSubscript => { self.visit_expr(value); self.visit_type_definition(slice); self.visit_expr_context(ctx); } // Ex) Annotated[int, "Hello, world!"] - SubscriptKind::PEP593AnnotatedSubscript => { + typing::SubscriptKind::PEP593AnnotatedSubscript => { // First argument is a type (including forward references); the // rest are arbitrary Python objects. self.visit_expr(value); @@ -4291,7 +4284,7 @@ impl<'a> Checker<'a> { && binding.redefines(shadowed) && (!self.settings.dummy_variable_rgx.is_match(name) || shadows_import) && !(shadowed.kind.is_function_definition() - && analyze::visibility::is_overload( + && visibility::is_overload( &self.semantic_model, cast::decorator_list( self.semantic_model.stmts[shadowed.source.unwrap()], @@ -5298,9 +5291,9 @@ pub(crate) fn check_ast( ModuleKind::Module }, source: if let Some(module_path) = module_path.as_ref() { - ModuleSource::Path(module_path) + visibility::ModuleSource::Path(module_path) } else { - ModuleSource::File(path) + visibility::ModuleSource::File(path) }, python_ast, }; diff --git a/crates/ruff/src/docstrings/extraction.rs b/crates/ruff/src/docstrings/extraction.rs index aa64249425..d4ae103b2f 100644 --- a/crates/ruff/src/docstrings/extraction.rs +++ b/crates/ruff/src/docstrings/extraction.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Stmt}; -use ruff_python_semantic::definition::{Definition, DefinitionId, Definitions, Member, MemberKind}; +use ruff_python_semantic::{Definition, DefinitionId, Definitions, Member, MemberKind}; /// Extract a docstring from a function or class body. pub(crate) fn docstring_from(suite: &[Stmt]) -> Option<&Expr> { diff --git a/crates/ruff/src/docstrings/mod.rs b/crates/ruff/src/docstrings/mod.rs index b9df42d415..3103aa5a62 100644 --- a/crates/ruff/src/docstrings/mod.rs +++ b/crates/ruff/src/docstrings/mod.rs @@ -4,7 +4,7 @@ use std::ops::Deref; use ruff_text_size::{TextRange, TextSize}; use rustpython_parser::ast::{Expr, Ranged}; -use ruff_python_semantic::definition::Definition; +use ruff_python_semantic::Definition; pub(crate) mod extraction; pub(crate) mod google; diff --git a/crates/ruff/src/importer/mod.rs b/crates/ruff/src/importer/mod.rs index 53b7fbb597..b31d97a066 100644 --- a/crates/ruff/src/importer/mod.rs +++ b/crates/ruff/src/importer/mod.rs @@ -10,7 +10,7 @@ use rustpython_parser::ast::{self, Ranged, Stmt, Suite}; use ruff_diagnostics::Edit; use ruff_python_ast::imports::{AnyImport, Import, ImportFrom}; use ruff_python_ast::source_code::{Locator, Stylist}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use ruff_textwrap::indent; use crate::autofix; diff --git a/crates/ruff/src/rules/flake8_2020/helpers.rs b/crates/ruff/src/rules/flake8_2020/helpers.rs index e5bb8fd4b6..81f3e83383 100644 --- a/crates/ruff/src/rules/flake8_2020/helpers.rs +++ b/crates/ruff/src/rules/flake8_2020/helpers.rs @@ -1,6 +1,7 @@ -use ruff_python_semantic::model::SemanticModel; use rustpython_parser::ast::Expr; +use ruff_python_semantic::SemanticModel; + pub(super) fn is_sys(model: &SemanticModel, expr: &Expr, target: &str) -> bool { model .resolve_call_path(expr) diff --git a/crates/ruff/src/rules/flake8_annotations/helpers.rs b/crates/ruff/src/rules/flake8_annotations/helpers.rs index 82732c5819..7a3bd1bb15 100644 --- a/crates/ruff/src/rules/flake8_annotations/helpers.rs +++ b/crates/ruff/src/rules/flake8_annotations/helpers.rs @@ -2,8 +2,7 @@ use rustpython_parser::ast::{self, Arguments, Expr, Stmt}; use ruff_python_ast::cast; use ruff_python_semantic::analyze::visibility; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::{Definition, Member, MemberKind, SemanticModel}; pub(super) fn match_function_def( stmt: &Stmt, diff --git a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs index 492be79613..53498e5058 100644 --- a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs @@ -6,9 +6,7 @@ use ruff_python_ast::helpers::ReturnStatementVisitor; use ruff_python_ast::statement_visitor::StatementVisitor; use ruff_python_ast::{cast, helpers}; use ruff_python_semantic::analyze::visibility; -use ruff_python_semantic::analyze::visibility::Visibility; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::{Definition, Member, MemberKind, SemanticModel}; use ruff_python_stdlib::typing::SIMPLE_MAGIC_RETURN_TYPES; use crate::checkers::ast::Checker; @@ -453,7 +451,7 @@ fn check_dynamically_typed( pub(crate) fn definition( checker: &Checker, definition: &Definition, - visibility: Visibility, + visibility: visibility::Visibility, ) -> Vec { // TODO(charlie): Consider using the AST directly here rather than `Definition`. // We could adhere more closely to `flake8-annotations` by defining public @@ -705,7 +703,7 @@ pub(crate) fn definition( } } else { match visibility { - Visibility::Public => { + visibility::Visibility::Public => { if checker.enabled(Rule::MissingReturnTypeUndocumentedPublicFunction) { diagnostics.push(Diagnostic::new( MissingReturnTypeUndocumentedPublicFunction { @@ -715,7 +713,7 @@ pub(crate) fn definition( )); } } - Visibility::Private => { + visibility::Visibility::Private => { if checker.enabled(Rule::MissingReturnTypePrivateFunction) { diagnostics.push(Diagnostic::new( MissingReturnTypePrivateFunction { diff --git a/crates/ruff/src/rules/flake8_bandit/helpers.rs b/crates/ruff/src/rules/flake8_bandit/helpers.rs index 37b0d6d745..dc6aa6dcdb 100644 --- a/crates/ruff/src/rules/flake8_bandit/helpers.rs +++ b/crates/ruff/src/rules/flake8_bandit/helpers.rs @@ -2,7 +2,7 @@ use once_cell::sync::Lazy; use regex::Regex; use rustpython_parser::ast::{self, Constant, Expr}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; static PASSWORD_CANDIDATE_REGEX: Lazy = Lazy::new(|| { Regex::new(r"(^|_)(?i)(pas+wo?r?d|pass(phrase)?|pwd|token|secrete?)($|_)").unwrap() diff --git a/crates/ruff/src/rules/flake8_bandit/rules/shell_injection.rs b/crates/ruff/src/rules/flake8_bandit/rules/shell_injection.rs index 6eb6c9f5e2..c5d97d7e0b 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/shell_injection.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/shell_injection.rs @@ -5,7 +5,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::Truthiness; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::{ checkers::ast::Checker, registry::Rule, rules::flake8_bandit::helpers::string_literal, 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 59ea9cc665..f8bd621178 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 @@ -4,7 +4,7 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::identifier_range; use ruff_python_semantic::analyze::visibility::{is_abstract, is_overload}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; use crate::registry::Rule; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs b/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs index d16bfabf1f..ff5d889ace 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Decorator, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs b/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs index a36b571c12..bf2cf7b7a5 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs @@ -8,7 +8,7 @@ use ruff_python_ast::call_path::{compose_call_path, from_qualified_name, CallPat use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; use ruff_python_semantic::analyze::typing::is_immutable_func; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; use crate::rules::flake8_bugbear::rules::mutable_argument_default::is_mutable_func; 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 19698f8120..b4fb273aa8 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 @@ -3,7 +3,7 @@ use rustpython_parser::ast::{self, Arguments, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_semantic::analyze::typing::is_immutable_annotation; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; 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 3bbc2efbac..be3570d8fd 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 @@ -3,7 +3,7 @@ use rustpython_parser::ast::{self, Expr, Keyword, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::is_const_none; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/flake8_django/rules/helpers.rs b/crates/ruff/src/rules/flake8_django/rules/helpers.rs index 3dc1353045..b12419a808 100644 --- a/crates/ruff/src/rules/flake8_django/rules/helpers.rs +++ b/crates/ruff/src/rules/flake8_django/rules/helpers.rs @@ -1,6 +1,6 @@ use rustpython_parser::ast::Expr; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; /// Return `true` if a Python class appears to be a Django model, based on its base classes. pub(super) fn is_model(model: &SemanticModel, base: &Expr) -> bool { 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 469ff81eb6..0779e7462b 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 @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Expr, Keyword, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; 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 5e07cbfa53..586928fa00 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 @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; 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 857ebc96ee..b3b198624c 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 @@ -4,7 +4,7 @@ use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs b/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs index dceee291b9..cd92ae23a7 100644 --- a/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs +++ b/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs @@ -4,7 +4,6 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Operator, Ranged}; use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_python_ast::helpers::{find_keyword, SimpleCallArgs}; use ruff_python_semantic::analyze::logging; -use ruff_python_semantic::analyze::logging::exc_info; use ruff_python_stdlib::logging::LoggingLevel; use crate::checkers::ast::Checker; @@ -197,7 +196,7 @@ pub(crate) fn logging_call( if !checker.semantic_model().in_exception_handler() { return; } - let Some(exc_info) = exc_info(keywords, checker.semantic_model()) else { + let Some(exc_info) = logging::exc_info(keywords, checker.semantic_model()) else { return; }; if let LoggingCallType::LevelCall(logging_level) = logging_call_type { 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 ce9b587379..b60b500afb 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 @@ -4,7 +4,7 @@ use rustpython_parser::ast::{Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::prelude::Expr; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; +use ruff_python_semantic::{Definition, Member, MemberKind}; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs b/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs index a5dd877d44..fdaeb56510 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs @@ -4,8 +4,7 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{identifier_range, map_subscript}; use ruff_python_semantic::analyze::visibility::{is_abstract, is_final, is_overload}; -use ruff_python_semantic::model::SemanticModel; -use ruff_python_semantic::scope::ScopeKind; +use ruff_python_semantic::{ScopeKind, SemanticModel}; use crate::checkers::ast::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 609602beab..eff56ed300 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs @@ -3,8 +3,7 @@ use rustpython_parser::ast::{self, Arguments, Constant, Expr, Operator, Ranged, use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::Locator; -use ruff_python_semantic::model::SemanticModel; -use ruff_python_semantic::scope::ScopeKind; +use ruff_python_semantic::{ScopeKind, SemanticModel}; use crate::checkers::ast::Checker; use crate::registry::AsRule; 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 c0c53e8e3a..6542628bf8 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs @@ -14,7 +14,7 @@ use ruff_python_ast::source_code::Locator; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{helpers, visitor}; use ruff_python_semantic::analyze::visibility::is_abstract; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::autofix::edits::remove_argument; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/helpers.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/helpers.rs index f753163036..826dbfd530 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/helpers.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/helpers.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Constant, Decorator, Expr, Keyword}; use ruff_python_ast::call_path::{collect_call_path, CallPath}; use ruff_python_ast::helpers::map_callable; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use ruff_python_whitespace::PythonWhitespace; pub(super) fn get_mark_decorators( diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs index dc79b82839..e2c25bf154 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs @@ -4,7 +4,7 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::format_call_path; use ruff_python_ast::call_path::from_qualified_name; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; use crate::registry::Rule; diff --git a/crates/ruff/src/rules/flake8_return/rules/function.rs b/crates/ruff/src/rules/flake8_return/rules/function.rs index 8f5e3aaf88..25b50a8162 100644 --- a/crates/ruff/src/rules/flake8_return/rules/function.rs +++ b/crates/ruff/src/rules/flake8_return/rules/function.rs @@ -10,7 +10,7 @@ use ruff_python_ast::helpers::elif_else_range; use ruff_python_ast::helpers::is_const_none; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::whitespace::indentation; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::autofix::edits; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs b/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs index 40ebfdf1ab..fb8412aaf8 100644 --- a/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs +++ b/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs @@ -3,7 +3,7 @@ use rustpython_parser::ast::{self, Expr, 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_semantic::scope::ScopeKind; +use ruff_python_semantic::ScopeKind; use crate::checkers::ast::Checker; 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 431838a350..096bb354d0 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs @@ -9,7 +9,7 @@ use ruff_python_ast::comparable::{ComparableConstant, ComparableExpr, Comparable use ruff_python_ast::helpers::{ any_over_expr, contains_effect, first_colon_range, has_comments, has_comments_in, }; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use ruff_python_whitespace::UniversalNewlines; use crate::checkers::ast::Checker; 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 35e4c620a7..9d04312b70 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 @@ -3,7 +3,7 @@ use rustpython_parser::ast::{self, Cmpop, Expr, ExprContext, Ranged, Stmt, Unary use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::scope::ScopeKind; +use ruff_python_semantic::ScopeKind; use crate::checkers::ast::Checker; use crate::registry::AsRule; 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 8d10ddb424..ac19d8f374 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 @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/flake8_type_checking/helpers.rs b/crates/ruff/src/rules/flake8_type_checking/helpers.rs index b8d399aa01..085f89d029 100644 --- a/crates/ruff/src/rules/flake8_type_checking/helpers.rs +++ b/crates/ruff/src/rules/flake8_type_checking/helpers.rs @@ -2,9 +2,7 @@ use rustpython_parser::ast; use ruff_python_ast::call_path::from_qualified_name; use ruff_python_ast::helpers::map_callable; -use ruff_python_semantic::binding::{Binding, BindingKind}; -use ruff_python_semantic::model::SemanticModel; -use ruff_python_semantic::scope::ScopeKind; +use ruff_python_semantic::{Binding, BindingKind, ScopeKind, SemanticModel}; pub(crate) fn is_valid_runtime_import(semantic_model: &SemanticModel, binding: &Binding) -> bool { if matches!( diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs index 7706c1ae51..659e59ac2c 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs @@ -4,9 +4,7 @@ use rustc_hash::FxHashMap; use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::node::NodeId; -use ruff_python_semantic::reference::ReferenceId; -use ruff_python_semantic::scope::Scope; +use ruff_python_semantic::{NodeId, ReferenceId, Scope}; use crate::autofix; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index 87acdf2b1f..97afe9c500 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -4,10 +4,7 @@ use rustc_hash::FxHashMap; use ruff_diagnostics::{AutofixKind, Diagnostic, DiagnosticKind, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::binding::Binding; -use ruff_python_semantic::node::NodeId; -use ruff_python_semantic::reference::ReferenceId; -use ruff_python_semantic::scope::Scope; +use ruff_python_semantic::{Binding, NodeId, ReferenceId, Scope}; use crate::autofix; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs b/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs index 5e571e2d59..eb1c845bd8 100644 --- a/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs +++ b/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs @@ -7,11 +7,8 @@ use rustpython_parser::ast::{Arg, Arguments}; use ruff_diagnostics::DiagnosticKind; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::analyze::function_type; -use ruff_python_semantic::analyze::function_type::FunctionType; -use ruff_python_semantic::analyze::visibility; -use ruff_python_semantic::model::SemanticModel; -use ruff_python_semantic::scope::{Scope, ScopeKind}; +use ruff_python_semantic::analyze::{function_type, visibility}; +use ruff_python_semantic::{Scope, ScopeKind, SemanticModel}; use crate::checkers::ast::Checker; use crate::registry::Rule; @@ -327,7 +324,7 @@ pub(crate) fn unused_arguments( &checker.settings.pep8_naming.classmethod_decorators, &checker.settings.pep8_naming.staticmethod_decorators, ) { - FunctionType::Function => { + function_type::FunctionType::Function => { if checker.enabled(Argumentable::Function.rule_code()) && !visibility::is_overload(checker.semantic_model(), decorator_list) { @@ -346,7 +343,7 @@ pub(crate) fn unused_arguments( vec![] } } - FunctionType::Method => { + function_type::FunctionType::Method => { if checker.enabled(Argumentable::Method.rule_code()) && !helpers::is_empty(body) && (!visibility::is_magic(name) @@ -372,7 +369,7 @@ pub(crate) fn unused_arguments( vec![] } } - FunctionType::ClassMethod => { + function_type::FunctionType::ClassMethod => { if checker.enabled(Argumentable::ClassMethod.rule_code()) && !helpers::is_empty(body) && (!visibility::is_magic(name) @@ -398,7 +395,7 @@ pub(crate) fn unused_arguments( vec![] } } - FunctionType::StaticMethod => { + function_type::FunctionType::StaticMethod => { if checker.enabled(Argumentable::StaticMethod.rule_code()) && !helpers::is_empty(body) && (!visibility::is_magic(name) diff --git a/crates/ruff/src/rules/pandas_vet/helpers.rs b/crates/ruff/src/rules/pandas_vet/helpers.rs index d011be8daf..45240028d9 100644 --- a/crates/ruff/src/rules/pandas_vet/helpers.rs +++ b/crates/ruff/src/rules/pandas_vet/helpers.rs @@ -1,8 +1,7 @@ use rustpython_parser::ast; use rustpython_parser::ast::Expr; -use ruff_python_semantic::binding::{BindingKind, Importation}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::{BindingKind, Importation, SemanticModel}; pub(super) enum Resolution { /// The expression resolves to an irrelevant expression type (e.g., a constant). 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 ebb56710ce..7b87ccc3b9 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged}; use ruff_diagnostics::{AutofixKind, Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::binding::{BindingKind, Importation}; +use ruff_python_semantic::{BindingKind, Importation}; use crate::checkers::ast::Checker; use crate::registry::AsRule; diff --git a/crates/ruff/src/rules/pep8_naming/helpers.rs b/crates/ruff/src/rules/pep8_naming/helpers.rs index 72efccade2..05de2560a3 100644 --- a/crates/ruff/src/rules/pep8_naming/helpers.rs +++ b/crates/ruff/src/rules/pep8_naming/helpers.rs @@ -1,7 +1,7 @@ use itertools::Itertools; use rustpython_parser::ast::{self, Expr, Stmt}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use ruff_python_stdlib::str::{is_lower, is_upper}; pub(super) fn is_camelcase(name: &str) -> bool { diff --git a/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs b/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs index 2f94ee0d7b..6067a04839 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs @@ -4,7 +4,7 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::identifier_range; use ruff_python_ast::source_code::Locator; -use ruff_python_semantic::scope::{Scope, ScopeKind}; +use ruff_python_semantic::{Scope, ScopeKind}; use crate::settings::types::IdentifierPattern; diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs index e181cfd37c..66e9a597cb 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs @@ -3,7 +3,7 @@ use rustpython_parser::ast::{Arguments, Decorator, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_semantic::analyze::function_type; -use ruff_python_semantic::scope::Scope; +use ruff_python_semantic::Scope; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs index b9c198d1ca..bb1fb0a520 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs @@ -3,7 +3,7 @@ use rustpython_parser::ast::{Arguments, Decorator, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_semantic::analyze::function_type; -use ruff_python_semantic::scope::Scope; +use ruff_python_semantic::Scope; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs index 4e2b9c8867..1f8a41887d 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs @@ -5,7 +5,7 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::identifier_range; use ruff_python_ast::source_code::Locator; use ruff_python_semantic::analyze::visibility; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::settings::types::IdentifierPattern; diff --git a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs index bca493a8d8..3982031e3e 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs @@ -5,7 +5,7 @@ use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{has_leading_content, has_trailing_content}; use ruff_python_ast::source_code::Generator; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use ruff_python_whitespace::{leading_indentation, UniversalNewlines}; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/pydocstyle/helpers.rs b/crates/ruff/src/rules/pydocstyle/helpers.rs index 774fd1ff57..00d0eb8b93 100644 --- a/crates/ruff/src/rules/pydocstyle/helpers.rs +++ b/crates/ruff/src/rules/pydocstyle/helpers.rs @@ -4,8 +4,7 @@ use ruff_python_ast::call_path::from_qualified_name; use ruff_python_ast::cast; use ruff_python_ast::helpers::map_callable; use ruff_python_ast::str::is_implicit_concatenation; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::{Definition, Member, MemberKind, SemanticModel}; use ruff_python_whitespace::UniversalNewlines; /// Return the index of the first logical line in a string. 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 17d106d550..5b1a46a174 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 @@ -3,7 +3,7 @@ use rustpython_parser::ast::Ranged; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; +use ruff_python_semantic::{Definition, Member, MemberKind}; use ruff_python_whitespace::{PythonWhitespace, UniversalNewlineIterator, UniversalNewlines}; use crate::checkers::ast::Checker; 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 d8c52c5b66..47aa4e32e1 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 @@ -5,7 +5,7 @@ use rustpython_parser::ast::Ranged; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; +use ruff_python_semantic::{Definition, Member, MemberKind}; use ruff_python_whitespace::{PythonWhitespace, UniversalNewlineIterator, UniversalNewlines}; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs b/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs index 7a77ddcd7f..739cf7a44b 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs @@ -3,7 +3,7 @@ use rustpython_parser::ast::Ranged; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; +use ruff_python_semantic::{Definition, Member, MemberKind}; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; diff --git a/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs b/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs index 1ef2016d5a..c612aad519 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs @@ -3,7 +3,7 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::cast; use ruff_python_ast::helpers::identifier_range; use ruff_python_semantic::analyze::visibility::is_overload; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; +use ruff_python_semantic::{Definition, Member, MemberKind}; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; 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 8453973125..2288843805 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 @@ -4,7 +4,7 @@ use rustpython_parser::ast::Ranged; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::str::{is_triple_quote, leading_quote}; -use ruff_python_semantic::definition::{Definition, Member}; +use ruff_python_semantic::{Definition, Member}; use ruff_python_whitespace::{NewlineWithTrailingNewline, UniversalNewlineIterator}; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs b/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs index bbca269d99..1ae23355ba 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; +use ruff_python_semantic::{Definition, Member, MemberKind}; use ruff_python_whitespace::UniversalNewlines; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs b/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs index 5459f09512..91b67899bb 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs @@ -8,7 +8,7 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::{from_qualified_name, CallPath}; use ruff_python_ast::cast; use ruff_python_semantic::analyze::visibility::{is_property, is_test}; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; +use ruff_python_semantic::{Definition, Member, MemberKind}; use ruff_python_whitespace::UniversalNewlines; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs b/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs index a4ecde3e00..a1cbdb4412 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs @@ -7,7 +7,7 @@ use ruff_python_ast::helpers::identifier_range; use ruff_python_semantic::analyze::visibility::{ is_call, is_init, is_magic, is_new, is_overload, is_override, Visibility, }; -use ruff_python_semantic::definition::{Definition, Member, MemberKind, Module, ModuleKind}; +use ruff_python_semantic::{Definition, Member, MemberKind, Module, ModuleKind}; use crate::checkers::ast::Checker; use crate::registry::Rule; diff --git a/crates/ruff/src/rules/pydocstyle/rules/sections.rs b/crates/ruff/src/rules/pydocstyle/rules/sections.rs index f101a18246..66f77fcade 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/sections.rs @@ -12,7 +12,7 @@ use ruff_python_ast::cast; use ruff_python_ast::docstrings::{clean_space, leading_space}; use ruff_python_ast::helpers::identifier_range; use ruff_python_semantic::analyze::visibility::is_staticmethod; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; +use ruff_python_semantic::{Definition, Member, MemberKind}; use ruff_python_whitespace::NewlineWithTrailingNewline; use ruff_textwrap::dedent; diff --git a/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs b/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs index c41d3a0aaa..b30512b0b9 100644 --- a/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs +++ b/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::scope::ScopeKind; +use ruff_python_semantic::ScopeKind; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs b/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs index e4edea697d..46e9cc5a87 100644 --- a/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs +++ b/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs @@ -2,7 +2,7 @@ use ruff_text_size::TextRange; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::scope::Scope; +use ruff_python_semantic::Scope; /// ## What it does /// Checks for undefined names in `__all__`. diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_annotation.rs b/crates/ruff/src/rules/pyflakes/rules/unused_annotation.rs index 1691dbe01e..55c524edca 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_annotation.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_annotation.rs @@ -1,6 +1,6 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::scope::ScopeId; +use ruff_python_semantic::ScopeId; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs index 09d4b98ad0..5471c84c63 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs @@ -4,9 +4,7 @@ use rustc_hash::FxHashMap; use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::binding::Exceptions; -use ruff_python_semantic::node::NodeId; -use ruff_python_semantic::scope::Scope; +use ruff_python_semantic::{Exceptions, NodeId, Scope}; use crate::autofix; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs b/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs index 6ffa78fae6..193a37255b 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs @@ -7,7 +7,7 @@ use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::contains_effect; use ruff_python_ast::source_code::Locator; -use ruff_python_semantic::scope::ScopeId; +use ruff_python_semantic::ScopeId; use crate::autofix::edits::delete_stmt; use crate::checkers::ast::Checker; 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 baa6cc3b93..d86faeb118 100644 --- a/crates/ruff/src/rules/pyflakes/rules/yield_outside_function.rs +++ b/crates/ruff/src/rules/pyflakes/rules/yield_outside_function.rs @@ -4,7 +4,7 @@ use rustpython_parser::ast::{Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::scope::ScopeKind; +use ruff_python_semantic::ScopeKind; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/pylint/helpers.rs b/crates/ruff/src/rules/pylint/helpers.rs index 306737b28c..4e5dba75da 100644 --- a/crates/ruff/src/rules/pylint/helpers.rs +++ b/crates/ruff/src/rules/pylint/helpers.rs @@ -1,10 +1,10 @@ -use ruff_python_semantic::analyze::function_type; -use ruff_python_semantic::analyze::function_type::FunctionType; -use ruff_python_semantic::model::SemanticModel; -use ruff_python_semantic::scope::ScopeKind; +use std::fmt; + use rustpython_parser::ast; use rustpython_parser::ast::Cmpop; -use std::fmt; + +use ruff_python_semantic::analyze::function_type; +use ruff_python_semantic::{ScopeKind, SemanticModel}; use crate::settings::Settings; @@ -40,7 +40,7 @@ pub(super) fn in_dunder_init(model: &SemanticModel, settings: &Settings) -> bool &settings.pep8_naming.classmethod_decorators, &settings.pep8_naming.staticmethod_decorators, ), - FunctionType::Method + function_type::FunctionType::Method ) { return false; } 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 3c5aee7852..4eab1a008d 100644 --- a/crates/ruff/src/rules/pylint/rules/nested_min_max.rs +++ b/crates/ruff/src/rules/pylint/rules/nested_min_max.rs @@ -4,7 +4,7 @@ use rustpython_parser::ast::{self, Expr, Keyword, Ranged}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::has_comments; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::{checkers::ast::Checker, registry::AsRule}; diff --git a/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs b/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs index 8964206d1c..dce30a8956 100644 --- a/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs +++ b/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs @@ -8,7 +8,7 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor}; use ruff_python_ast::types::Node; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; 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 f9f86618f8..b2fac6b580 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 @@ -7,7 +7,7 @@ use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::is_dunder; use ruff_python_ast::source_code::Generator; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use ruff_python_stdlib::identifiers::is_identifier; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs b/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs index 50ae7304f4..adaf6b033b 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs @@ -7,7 +7,7 @@ use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::is_dunder; use ruff_python_ast::source_code::Generator; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use ruff_python_stdlib::identifiers::is_identifier; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs index 82a3da68ba..890dd665ed 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs @@ -4,7 +4,7 @@ use rustpython_parser::ast::{self, Excepthandler, Expr, ExprContext, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::compose_call_path; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; use crate::registry::AsRule; diff --git a/crates/ruff/src/rules/ruff/rules/helpers.rs b/crates/ruff/src/rules/ruff/rules/helpers.rs index fca12bfd2a..751bf32326 100644 --- a/crates/ruff/src/rules/ruff/rules/helpers.rs +++ b/crates/ruff/src/rules/ruff/rules/helpers.rs @@ -1,7 +1,7 @@ use ruff_python_ast::helpers::map_callable; use rustpython_parser::ast::{self, Expr}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; pub(super) fn is_mutable_expr(expr: &Expr) -> bool { matches!( diff --git a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs index 166a032ba3..0b00b3adbf 100644 --- a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs +++ b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs @@ -5,7 +5,7 @@ use rustpython_parser::ast::{self, Arguments, Constant, Expr, Operator, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use ruff_text_size::TextRange; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/tryceratops/helpers.rs b/crates/ruff/src/rules/tryceratops/helpers.rs index e7067a9eee..2581f86e0c 100644 --- a/crates/ruff/src/rules/tryceratops/helpers.rs +++ b/crates/ruff/src/rules/tryceratops/helpers.rs @@ -3,7 +3,7 @@ use rustpython_parser::ast::{self, Expr}; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; use ruff_python_semantic::analyze::logging; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; /// Collect `logging`-like calls from an AST. pub(super) struct LoggerCandidateVisitor<'a, 'b> { diff --git a/crates/ruff_python_semantic/src/lib.rs b/crates/ruff_python_semantic/src/lib.rs index e876f26181..e47b73ca16 100644 --- a/crates/ruff_python_semantic/src/lib.rs +++ b/crates/ruff_python_semantic/src/lib.rs @@ -1,9 +1,18 @@ pub mod analyze; -pub mod binding; -pub mod context; -pub mod definition; -pub mod globals; -pub mod model; -pub mod node; -pub mod reference; -pub mod scope; +mod binding; +mod context; +mod definition; +mod globals; +mod model; +mod node; +mod reference; +mod scope; + +pub use binding::*; +pub use context::*; +pub use definition::*; +pub use globals::*; +pub use model::*; +pub use node::*; +pub use reference::*; +pub use scope::*; From 65dbfd255667abbc8033955e3566ff7115ecd94b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 14 Jun 2023 14:28:55 -0400 Subject: [PATCH 058/447] Improve names and documentation on scope API (#5095) ## Summary Just minor improvements to improve consistency of method names and availability. --- crates/ruff/src/checkers/ast/mod.rs | 4 +-- .../rules/unused_loop_control_variable.rs | 2 +- crates/ruff_python_semantic/src/scope.rs | 25 ++++++++++++++++--- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 6fa683363c..b11002cfd1 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -4808,7 +4808,7 @@ impl<'a> Checker<'a> { let exports: Vec<(&str, TextRange)> = { self.semantic_model .global_scope() - .bindings_for_name("__all__") + .get_all("__all__") .map(|binding_id| &self.semantic_model.bindings[binding_id]) .filter_map(|binding| match &binding.kind { BindingKind::Export(Export { names }) => { @@ -5069,7 +5069,7 @@ impl<'a> Checker<'a> { let exports: Option> = { let global_scope = self.semantic_model.global_scope(); global_scope - .bindings_for_name("__all__") + .get_all("__all__") .map(|binding_id| &self.semantic_model.bindings[binding_id]) .filter_map(|binding| match &binding.kind { BindingKind::Export(Export { names }) => Some(names.iter().copied()), diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs b/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs index 6d813426a1..380628fc25 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs @@ -156,7 +156,7 @@ pub(crate) fn unused_loop_control_variable(checker: &mut Checker, target: &Expr, // used _after_ the loop. let scope = checker.semantic_model().scope(); if scope - .bindings_for_name(name) + .get_all(name) .map(|binding_id| checker.semantic_model().binding(binding_id)) .all(|binding| !binding.is_used()) { diff --git a/crates/ruff_python_semantic/src/scope.rs b/crates/ruff_python_semantic/src/scope.rs index e5100b3859..69f9cca6d8 100644 --- a/crates/ruff_python_semantic/src/scope.rs +++ b/crates/ruff_python_semantic/src/scope.rs @@ -90,24 +90,41 @@ impl<'a> Scope<'a> { self.bindings.contains_key(name) } - /// Returns the ids of all bindings defined in this scope. + /// Returns the IDs of all bindings defined in this scope. pub fn binding_ids(&self) -> impl Iterator + '_ { self.bindings.values().copied() } - /// Returns a tuple of the name and id of all bindings defined in this scope. + /// Returns a tuple of the name and ID of all bindings defined in this scope. pub fn bindings(&self) -> impl Iterator + '_ { self.bindings.iter().map(|(&name, &id)| (name, id)) } - /// Returns an iterator over all [bindings](BindingId) bound to the given name, including + /// Like [`Scope::get`], but returns all bindings with the given name, including /// those that were shadowed by later bindings. - pub fn bindings_for_name(&self, name: &str) -> impl Iterator + '_ { + pub fn get_all(&self, name: &str) -> impl Iterator + '_ { std::iter::successors(self.bindings.get(name).copied(), |id| { self.shadowed_bindings.get(id).copied() }) } + /// Like [`Scope::binding_ids`], but returns all bindings that were added to the scope, + /// including those that were shadowed by later bindings. + pub fn all_binding_ids(&self) -> impl Iterator + '_ { + self.bindings.values().copied().flat_map(|id| { + std::iter::successors(Some(id), |id| self.shadowed_bindings.get(id).copied()) + }) + } + + /// Like [`Scope::bindings`], but returns all bindings added to the scope, including those that + /// were shadowed by later bindings. + pub fn all_bindings(&self) -> impl Iterator + '_ { + self.bindings.iter().flat_map(|(&name, &id)| { + std::iter::successors(Some(id), |id| self.shadowed_bindings.get(id).copied()) + .map(move |id| (name, id)) + }) + } + /// Adds a reference to a star import (e.g., `from sys import *`) to this scope. pub fn add_star_import(&mut self, import: StarImportation<'a>) { self.star_imports.push(import); From bae183b823b3b00a10c9faef1e03bb0eeff29939 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 14 Jun 2023 15:01:51 -0400 Subject: [PATCH 059/447] Rename `semantic_model` and `model` usages to `semantic` (#5097) ## Summary As discussed in Discord, and similar to oxc, we're going to refer to this as `.semantic()` everywhere. While I was auditing usages of `model: &SemanticModel`, I also changed as many function signatures as I could find to consistently take the model as the _last_ argument, rather than the first. --- crates/ruff/src/checkers/ast/mod.rs | 609 ++++++++---------- crates/ruff/src/importer/mod.rs | 25 +- .../rules/airflow/rules/task_variable_name.rs | 2 +- crates/ruff/src/rules/flake8_2020/helpers.rs | 4 +- .../src/rules/flake8_2020/rules/compare.rs | 6 +- .../flake8_2020/rules/name_or_attribute.rs | 2 +- .../src/rules/flake8_2020/rules/subscript.rs | 2 +- .../src/rules/flake8_annotations/helpers.rs | 8 +- .../flake8_annotations/rules/definition.rs | 32 +- .../flake8_async/rules/blocking_http_call.rs | 4 +- .../flake8_async/rules/blocking_os_call.rs | 4 +- .../rules/open_sleep_or_subprocess_call.rs | 4 +- .../ruff/src/rules/flake8_bandit/helpers.rs | 20 +- .../rules/bad_file_permissions.rs | 2 +- .../rules/hardcoded_sql_expression.rs | 2 +- .../rules/hashlib_insecure_hash_functions.rs | 27 +- .../rules/jinja2_autoescape_false.rs | 2 +- .../rules/logging_config_insecure_listen.rs | 2 +- .../flake8_bandit/rules/paramiko_calls.rs | 2 +- .../rules/request_with_no_cert_validation.rs | 2 +- .../rules/request_without_timeout.rs | 2 +- .../flake8_bandit/rules/shell_injection.rs | 12 +- .../rules/snmp_insecure_version.rs | 2 +- .../rules/snmp_weak_cryptography.rs | 2 +- .../rules/suspicious_function_call.rs | 2 +- .../rules/try_except_continue.rs | 2 +- .../flake8_bandit/rules/try_except_pass.rs | 2 +- .../flake8_bandit/rules/unsafe_yaml_load.rs | 4 +- .../flake8_blind_except/rules/blind_except.rs | 4 +- .../rules/abstract_base_class.rs | 12 +- .../rules/assert_raises_exception.rs | 4 +- .../rules/cached_instance_method.rs | 8 +- .../rules/function_call_argument_default.rs | 11 +- .../rules/mutable_argument_default.rs | 12 +- .../rules/no_explicit_stacklevel.rs | 2 +- .../rules/reuse_of_groupby_generator.rs | 2 +- .../rules/setattr_with_constant.rs | 2 +- .../rules/unused_loop_control_variable.rs | 9 +- .../rules/useless_contextlib_suppress.rs | 2 +- .../rules/useless_expression.rs | 2 +- .../rules/zip_without_explicit_strict.rs | 14 +- .../rules/builtin_attribute_shadowing.rs | 10 +- .../src/rules/flake8_comprehensions/fixes.rs | 2 +- .../rules/unnecessary_call_around_sorted.rs | 2 +- .../rules/unnecessary_collection_call.rs | 2 +- .../rules/unnecessary_comprehension.rs | 2 +- .../unnecessary_comprehension_any_all.rs | 2 +- .../unnecessary_double_cast_or_process.rs | 2 +- .../rules/unnecessary_generator_list.rs | 2 +- .../rules/unnecessary_generator_set.rs | 2 +- .../rules/unnecessary_list_call.rs | 2 +- .../unnecessary_list_comprehension_dict.rs | 2 +- .../unnecessary_list_comprehension_set.rs | 2 +- .../rules/unnecessary_literal_dict.rs | 2 +- .../rules/unnecessary_literal_set.rs | 2 +- .../unnecessary_literal_within_dict_call.rs | 2 +- .../unnecessary_literal_within_list_call.rs | 2 +- .../unnecessary_literal_within_tuple_call.rs | 2 +- .../rules/unnecessary_map.rs | 6 +- .../rules/unnecessary_subscript_reversal.rs | 2 +- .../rules/call_date_fromtimestamp.rs | 2 +- .../flake8_datetimez/rules/call_date_today.rs | 2 +- .../rules/call_datetime_fromtimestamp.rs | 2 +- .../rules/call_datetime_now_without_tzinfo.rs | 2 +- .../call_datetime_strptime_without_zone.rs | 4 +- .../rules/call_datetime_today.rs | 2 +- .../rules/call_datetime_utcfromtimestamp.rs | 2 +- .../rules/call_datetime_utcnow.rs | 2 +- .../rules/call_datetime_without_tzinfo.rs | 2 +- .../rules/flake8_debugger/rules/debugger.rs | 2 +- .../rules/all_with_model_form.rs | 2 +- .../rules/exclude_with_model_form.rs | 2 +- .../src/rules/flake8_django/rules/helpers.rs | 16 +- .../rules/locals_in_render_function.rs | 10 +- .../rules/model_without_dunder_str.rs | 6 +- .../rules/nullable_model_string_field.rs | 2 +- .../rules/unordered_body_content_in_model.rs | 8 +- .../rules/string_in_exception.rs | 6 +- .../future_rewritable_type_annotation.rs | 2 +- .../rules/logging_call.rs | 8 +- .../flake8_pie/rules/non_unique_enums.rs | 4 +- .../rules/reimplemented_list_builtin.rs | 2 +- .../rules/flake8_print/rules/print_call.rs | 13 +- .../flake8_pyi/rules/any_eq_ne_annotation.rs | 9 +- .../rules/bad_version_info_comparison.rs | 2 +- .../rules/collections_named_tuple.rs | 2 +- .../rules/iter_method_return_iterable.rs | 2 +- .../rules/no_return_argument_annotation.rs | 5 +- .../flake8_pyi/rules/non_self_return_type.rs | 40 +- .../flake8_pyi/rules/prefix_type_params.rs | 8 +- .../rules/flake8_pyi/rules/simple_defaults.rs | 100 ++- .../rules/str_or_repr_defined_in_stub.rs | 12 +- .../flake8_pyi/rules/unrecognized_platform.rs | 2 +- .../flake8_pytest_style/rules/assertion.rs | 8 +- .../rules/flake8_pytest_style/rules/fail.rs | 2 +- .../flake8_pytest_style/rules/fixture.rs | 10 +- .../flake8_pytest_style/rules/helpers.rs | 20 +- .../flake8_pytest_style/rules/parametrize.rs | 2 +- .../rules/flake8_pytest_style/rules/raises.rs | 10 +- .../src/rules/flake8_return/rules/function.rs | 8 +- .../rules/private_member_access.rs | 6 +- .../flake8_simplify/rules/ast_bool_op.rs | 23 +- .../rules/flake8_simplify/rules/ast_expr.rs | 2 +- .../src/rules/flake8_simplify/rules/ast_if.rs | 16 +- .../rules/flake8_simplify/rules/ast_ifexp.rs | 2 +- .../flake8_simplify/rules/ast_unary_op.rs | 12 +- .../rules/open_file_with_context_handler.rs | 26 +- .../rules/reimplemented_builtin.rs | 8 +- .../rules/suppressible_exception.rs | 2 +- .../rules/no_slots_in_namedtuple_subclass.rs | 2 +- .../rules/no_slots_in_str_subclass.rs | 2 +- .../rules/no_slots_in_tuple_subclass.rs | 4 +- .../flake8_tidy_imports/rules/banned_api.rs | 2 +- .../src/rules/flake8_type_checking/helpers.rs | 31 +- .../rules/empty_type_checking_block.rs | 4 +- .../runtime_import_in_type_checking_block.rs | 18 +- .../rules/typing_only_runtime_import.rs | 20 +- .../rules/unused_arguments.rs | 46 +- .../rules/replaceable_by_pathlib.rs | 2 +- .../numpy/rules/deprecated_type_alias.rs | 37 +- .../rules/numpy/rules/numpy_legacy_random.rs | 25 +- crates/ruff/src/rules/pandas_vet/helpers.rs | 4 +- .../ruff/src/rules/pandas_vet/rules/attr.rs | 4 +- .../ruff/src/rules/pandas_vet/rules/call.rs | 2 +- .../pandas_vet/rules/inplace_argument.rs | 10 +- .../src/rules/pandas_vet/rules/subscript.rs | 2 +- crates/ruff/src/rules/pep8_naming/helpers.rs | 16 +- ...id_first_argument_name_for_class_method.rs | 4 +- .../invalid_first_argument_name_for_method.rs | 4 +- .../rules/invalid_function_name.rs | 4 +- .../mixed_case_variable_in_class_scope.rs | 4 +- .../mixed_case_variable_in_global_scope.rs | 3 +- .../non_lowercase_variable_in_function.rs | 6 +- .../src/rules/pycodestyle/rules/imports.rs | 3 +- .../pycodestyle/rules/lambda_assignment.rs | 21 +- .../pycodestyle/rules/type_comparison.rs | 12 +- crates/ruff/src/rules/pydocstyle/helpers.rs | 5 +- .../src/rules/pydocstyle/rules/if_needed.rs | 2 +- .../pydocstyle/rules/non_imperative_mood.rs | 2 +- .../src/rules/pydocstyle/rules/not_missing.rs | 6 +- .../src/rules/pydocstyle/rules/sections.rs | 2 +- .../pyflakes/rules/invalid_print_syntax.rs | 2 +- .../pyflakes/rules/return_outside_function.rs | 2 +- .../rules/pyflakes/rules/undefined_local.rs | 13 +- .../rules/pyflakes/rules/unused_annotation.rs | 4 +- .../src/rules/pyflakes/rules/unused_import.rs | 10 +- .../rules/pyflakes/rules/unused_variable.rs | 12 +- .../pyflakes/rules/yield_outside_function.rs | 2 +- .../pygrep_hooks/rules/deprecated_log_warn.rs | 2 +- .../src/rules/pygrep_hooks/rules/no_eval.rs | 2 +- crates/ruff/src/rules/pylint/helpers.rs | 10 +- .../rules/pylint/rules/await_outside_async.rs | 2 +- .../pylint/rules/compare_to_empty_string.rs | 2 +- .../rules/pylint/rules/global_statement.rs | 6 +- .../pylint/rules/invalid_envvar_default.rs | 2 +- .../pylint/rules/invalid_envvar_value.rs | 2 +- .../rules/pylint/rules/invalid_str_return.rs | 2 +- .../rules/load_before_global_declaration.rs | 2 +- crates/ruff/src/rules/pylint/rules/logging.rs | 2 +- .../src/rules/pylint/rules/nested_min_max.rs | 27 +- .../pylint/rules/property_with_parameters.rs | 2 +- .../rules/pylint/rules/redefined_loop_name.rs | 10 +- .../pylint/rules/repeated_isinstance_calls.rs | 2 +- .../src/rules/pylint/rules/return_in_init.rs | 2 +- .../src/rules/pylint/rules/sys_exit_alias.rs | 4 +- .../unexpected_special_method_signature.rs | 4 +- .../rules/yield_from_in_async_function.rs | 2 +- .../src/rules/pylint/rules/yield_in_init.rs | 2 +- ...convert_named_tuple_functional_to_class.rs | 6 +- .../convert_typed_dict_functional_to_class.rs | 6 +- .../pyupgrade/rules/datetime_utc_alias.rs | 2 +- .../rules/lru_cache_with_maxsize_none.rs | 4 +- .../rules/lru_cache_without_parameters.rs | 2 +- .../rules/pyupgrade/rules/native_literals.rs | 4 +- .../src/rules/pyupgrade/rules/open_alias.rs | 4 +- .../rules/pyupgrade/rules/os_error_alias.rs | 21 +- .../pyupgrade/rules/outdated_version_block.rs | 6 +- .../pyupgrade/rules/redundant_open_modes.rs | 2 +- .../pyupgrade/rules/replace_stdout_stderr.rs | 6 +- .../rules/replace_universal_newlines.rs | 2 +- .../rules/super_call_with_parameters.rs | 4 +- .../pyupgrade/rules/type_of_primitive.rs | 2 +- .../pyupgrade/rules/typing_text_str_alias.rs | 2 +- .../rules/unnecessary_builtin_import.rs | 4 +- .../rules/unnecessary_future_import.rs | 4 +- .../pyupgrade/rules/use_pep585_annotation.rs | 6 +- .../pyupgrade/rules/use_pep604_annotation.rs | 4 +- .../pyupgrade/rules/use_pep604_isinstance.rs | 2 +- .../pyupgrade/rules/useless_metaclass_type.rs | 4 +- .../rules/useless_object_inheritance.rs | 2 +- .../rules/collection_literal_concatenation.rs | 2 +- .../explicit_f_string_type_conversion.rs | 2 +- .../function_call_in_dataclass_default.rs | 8 +- crates/ruff/src/rules/ruff/rules/helpers.rs | 12 +- .../src/rules/ruff/rules/implicit_optional.rs | 40 +- .../rules/ruff/rules/mutable_class_default.rs | 6 +- .../ruff/rules/mutable_dataclass_default.rs | 6 +- .../rules/ruff/rules/pairwise_over_zipped.rs | 2 +- crates/ruff/src/rules/tryceratops/helpers.rs | 8 +- .../rules/error_instead_of_exception.rs | 4 +- .../tryceratops/rules/raise_vanilla_class.rs | 2 +- .../tryceratops/rules/try_consider_else.rs | 2 +- .../rules/type_check_without_type_error.rs | 4 +- .../tryceratops/rules/verbose_log_message.rs | 2 +- .../src/analyze/function_type.rs | 10 +- .../src/analyze/logging.rs | 8 +- .../src/analyze/typing.rs | 156 ++--- .../src/analyze/visibility.rs | 36 +- crates/ruff_python_semantic/src/binding.rs | 8 +- 209 files changed, 1058 insertions(+), 1167 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index b11002cfd1..5cb760be85 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -67,7 +67,7 @@ pub(crate) struct Checker<'a> { pub(crate) indexer: &'a Indexer, pub(crate) importer: Importer<'a>, // Stateful fields. - semantic_model: SemanticModel<'a>, + semantic: SemanticModel<'a>, deferred: Deferred<'a>, pub(crate) diagnostics: Vec, // Check-specific state. @@ -100,7 +100,7 @@ impl<'a> Checker<'a> { stylist, indexer, importer, - semantic_model: SemanticModel::new(&settings.typing_modules, path, module), + semantic: SemanticModel::new(&settings.typing_modules, path, module), deferred: Deferred::default(), diagnostics: Vec::default(), flake8_bugbear_seen: Vec::default(), @@ -143,7 +143,7 @@ impl<'a> Checker<'a> { /// /// If the current expression in the context is not an f-string, returns ``None``. pub(crate) fn f_string_quote_style(&self) -> Option { - let model = &self.semantic_model; + let model = &self.semantic; if !model.in_f_string() { return None; } @@ -169,14 +169,14 @@ impl<'a> Checker<'a> { /// thus be applied whenever we delete a statement, but can otherwise be omitted. pub(crate) fn isolation(&self, parent: Option<&Stmt>) -> IsolationLevel { parent - .and_then(|stmt| self.semantic_model.stmts.node_id(stmt)) + .and_then(|stmt| self.semantic.stmts.node_id(stmt)) .map_or(IsolationLevel::default(), |node_id| { IsolationLevel::Group(node_id.into()) }) } - pub(crate) const fn semantic_model(&self) -> &SemanticModel<'a> { - &self.semantic_model + pub(crate) const fn semantic(&self) -> &SemanticModel<'a> { + &self.semantic } pub(crate) const fn package(&self) -> Option<&'a Path> { @@ -205,7 +205,7 @@ where 'b: 'a, { fn visit_stmt(&mut self, stmt: &'b Stmt) { - self.semantic_model.push_stmt(stmt); + self.semantic.push_stmt(stmt); // Track whether we've seen docstrings, non-imports, etc. match stmt { @@ -216,50 +216,50 @@ where .iter() .any(|alias| alias.name.as_str() == "annotations") { - self.semantic_model.flags |= SemanticModelFlags::FUTURE_ANNOTATIONS; + self.semantic.flags |= SemanticModelFlags::FUTURE_ANNOTATIONS; } } else { - self.semantic_model.flags |= SemanticModelFlags::FUTURES_BOUNDARY; + self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY; } } Stmt::Import(_) => { - self.semantic_model.flags |= SemanticModelFlags::FUTURES_BOUNDARY; + self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY; } _ => { - self.semantic_model.flags |= SemanticModelFlags::FUTURES_BOUNDARY; - if !self.semantic_model.seen_import_boundary() + self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY; + if !self.semantic.seen_import_boundary() && !helpers::is_assignment_to_a_dunder(stmt) - && !helpers::in_nested_block(self.semantic_model.parents()) + && !helpers::in_nested_block(self.semantic.parents()) { - self.semantic_model.flags |= SemanticModelFlags::IMPORT_BOUNDARY; + self.semantic.flags |= SemanticModelFlags::IMPORT_BOUNDARY; } } } // Track each top-level import, to guide import insertions. if matches!(stmt, Stmt::Import(_) | Stmt::ImportFrom(_)) { - if self.semantic_model.at_top_level() { + if self.semantic.at_top_level() { self.importer.visit_import(stmt); } } // Store the flags prior to any further descent, so that we can restore them after visiting // the node. - let flags_snapshot = self.semantic_model.flags; + let flags_snapshot = self.semantic.flags; // Pre-visit. match stmt { Stmt::Global(ast::StmtGlobal { names, range: _ }) => { let ranges: Vec = helpers::find_names(stmt, self.locator).collect(); - if !self.semantic_model.scope_id.is_global() { + if !self.semantic.scope_id.is_global() { for (name, range) in names.iter().zip(ranges.iter()) { // Add a binding to the current scope. - let binding_id = self.semantic_model.push_binding( + let binding_id = self.semantic.push_binding( *range, BindingKind::Global, BindingFlags::empty(), ); - let scope = self.semantic_model.scope_mut(); + let scope = self.semantic.scope_mut(); scope.add(name, binding_id); } } @@ -272,15 +272,15 @@ where } Stmt::Nonlocal(ast::StmtNonlocal { names, range: _ }) => { let ranges: Vec = helpers::find_names(stmt, self.locator).collect(); - if !self.semantic_model.scope_id.is_global() { + if !self.semantic.scope_id.is_global() { for (name, range) in names.iter().zip(ranges.iter()) { // Add a binding to the current scope. - let binding_id = self.semantic_model.push_binding( + let binding_id = self.semantic.push_binding( *range, BindingKind::Nonlocal, BindingFlags::empty(), ); - let scope = self.semantic_model.scope_mut(); + let scope = self.semantic.scope_mut(); scope.add(name, binding_id); } @@ -288,15 +288,15 @@ where // and the current scope, and, per standard resolution rules, any class scopes.) for (name, range) in names.iter().zip(ranges.iter()) { let binding_id = self - .semantic_model + .semantic .scopes - .ancestors(self.semantic_model.scope_id) + .ancestors(self.semantic.scope_id) .skip(1) .filter(|scope| !(scope.kind.is_module() || scope.kind.is_class())) .find_map(|scope| scope.get(name.as_str())); if let Some(binding_id) = binding_id { - self.semantic_model.add_local_reference( + self.semantic.add_local_reference( binding_id, stmt.range(), ExecutionContext::Runtime, @@ -324,7 +324,7 @@ where if self.enabled(Rule::BreakOutsideLoop) { if let Some(diagnostic) = pyflakes::rules::break_outside_loop( stmt, - &mut self.semantic_model.parents().skip(1), + &mut self.semantic.parents().skip(1), ) { self.diagnostics.push(diagnostic); } @@ -334,7 +334,7 @@ where if self.enabled(Rule::ContinueOutsideLoop) { if let Some(diagnostic) = pyflakes::rules::continue_outside_loop( stmt, - &mut self.semantic_model.parents().skip(1), + &mut self.semantic.parents().skip(1), ) { self.diagnostics.push(diagnostic); } @@ -360,7 +360,7 @@ where self.diagnostics .extend(flake8_django::rules::non_leading_receiver_decorator( decorator_list, - |expr| self.semantic_model.resolve_call_path(expr), + |expr| self.semantic.resolve_call_path(expr), )); } @@ -382,7 +382,7 @@ where name, decorator_list, &self.settings.pep8_naming.ignore_names, - &self.semantic_model, + &self.semantic, self.locator, ) { self.diagnostics.push(diagnostic); @@ -392,7 +392,7 @@ where if let Some(diagnostic) = pep8_naming::rules::invalid_first_argument_name_for_class_method( self, - self.semantic_model.scope(), + self.semantic.scope(), name, decorator_list, args, @@ -405,7 +405,7 @@ where if let Some(diagnostic) = pep8_naming::rules::invalid_first_argument_name_for_method( self, - self.semantic_model.scope(), + self.semantic.scope(), name, decorator_list, args, @@ -448,7 +448,7 @@ where } if self.enabled(Rule::DunderFunctionName) { if let Some(diagnostic) = pep8_naming::rules::dunder_function_name( - self.semantic_model.scope(), + self.semantic.scope(), stmt, name, &self.settings.pep8_naming.ignore_names, @@ -614,7 +614,7 @@ where if self.enabled(Rule::YieldInForLoop) { pyupgrade::rules::yield_in_for_loop(self, stmt); } - if let ScopeKind::Class(class_def) = self.semantic_model.scope().kind { + if let ScopeKind::Class(class_def) = self.semantic.scope().kind { if self.enabled(Rule::BuiltinAttributeShadowing) { flake8_builtins::rules::builtin_attribute_shadowing( self, @@ -814,7 +814,7 @@ where ); if self.enabled(Rule::LateFutureImport) { - if self.semantic_model.seen_futures_boundary() { + if self.semantic.seen_futures_boundary() { self.diagnostics.push(Diagnostic::new( pyflakes::rules::LateFutureImport, stmt.range(), @@ -1094,7 +1094,7 @@ where } if self.enabled(Rule::LateFutureImport) { - if self.semantic_model.seen_futures_boundary() { + if self.semantic.seen_futures_boundary() { self.diagnostics.push(Diagnostic::new( pyflakes::rules::LateFutureImport, stmt.range(), @@ -1102,12 +1102,12 @@ where } } } else if &alias.name == "*" { - self.semantic_model + self.semantic .scope_mut() .add_star_import(StarImportation { level, module }); if self.enabled(Rule::UndefinedLocalWithNestedImportStarUsage) { - let scope = self.semantic_model.scope(); + let scope = self.semantic.scope(); if !matches!(scope.kind, ScopeKind::Module) { self.diagnostics.push(Diagnostic::new( pyflakes::rules::UndefinedLocalWithNestedImportStarUsage { @@ -1370,14 +1370,14 @@ where test, body, orelse, - self.semantic_model.stmt_parent(), + self.semantic.stmt_parent(), ); } if self.enabled(Rule::IfWithSameArms) { flake8_simplify::rules::if_with_same_arms( self, stmt, - self.semantic_model.stmt_parent(), + self.semantic.stmt_parent(), ); } if self.enabled(Rule::NeedlessBool) { @@ -1390,14 +1390,14 @@ where test, body, orelse, - self.semantic_model.stmt_parent(), + self.semantic.stmt_parent(), ); } if self.enabled(Rule::IfElseBlockInsteadOfIfExp) { flake8_simplify::rules::use_ternary_operator( self, stmt, - self.semantic_model.stmt_parent(), + self.semantic.stmt_parent(), ); } if self.enabled(Rule::IfElseBlockInsteadOfDictGet) { @@ -1407,7 +1407,7 @@ where test, body, orelse, - self.semantic_model.stmt_parent(), + self.semantic.stmt_parent(), ); } if self.enabled(Rule::TypeCheckWithoutTypeError) { @@ -1416,7 +1416,7 @@ where body, test, orelse, - self.semantic_model.stmt_parent(), + self.semantic.stmt_parent(), ); } if self.enabled(Rule::OutdatedVersionBlock) { @@ -1435,7 +1435,7 @@ where msg, range: _, }) => { - if !self.semantic_model.in_type_checking_block() { + if !self.semantic.in_type_checking_block() { if self.enabled(Rule::Assert) { self.diagnostics .push(flake8_bandit::rules::assert_used(stmt)); @@ -1478,7 +1478,7 @@ where self, stmt, body, - self.semantic_model.stmt_parent(), + self.semantic.stmt_parent(), ); } if self.enabled(Rule::RedefinedLoopName) { @@ -1508,7 +1508,7 @@ where .. }) => { if self.enabled(Rule::UnusedLoopControlVariable) { - self.deferred.for_loops.push(self.semantic_model.snapshot()); + self.deferred.for_loops.push(self.semantic.snapshot()); } if self.enabled(Rule::LoopVariableOverridesIterator) { flake8_bugbear::rules::loop_variable_overrides_iterator(self, target, iter); @@ -1533,7 +1533,7 @@ where flake8_simplify::rules::convert_for_loop_to_any_all( self, stmt, - self.semantic_model.sibling_stmt(), + self.semantic.sibling_stmt(), ); } if self.enabled(Rule::InDictKeys) { @@ -1674,7 +1674,7 @@ where ]) { // Ignore assignments in function bodies; those are covered by other rules. if !self - .semantic_model + .semantic .scopes() .any(|scope| scope.kind.is_any_function()) { @@ -1723,7 +1723,7 @@ where if self.enabled(Rule::AssignmentDefaultInStub) { // Ignore assignments in function bodies; those are covered by other rules. if !self - .semantic_model + .semantic .scopes() .any(|scope| scope.kind.is_any_function()) { @@ -1739,10 +1739,7 @@ where ); } } - if self - .semantic_model - .match_typing_expr(annotation, "TypeAlias") - { + if self.semantic.match_typing_expr(annotation, "TypeAlias") { if self.enabled(Rule::SnakeCaseTypeAlias) { flake8_pyi::rules::snake_case_type_alias(self, target); } @@ -1776,7 +1773,7 @@ where } if self.enabled(Rule::AsyncioDanglingTask) { if let Some(diagnostic) = ruff::rules::asyncio_dangling_task(value, |expr| { - self.semantic_model.resolve_call_path(expr) + self.semantic.resolve_call_path(expr) }) { self.diagnostics.push(diagnostic); } @@ -1811,7 +1808,7 @@ where // Function annotations are always evaluated at runtime, unless future annotations // are enabled. - let runtime_annotation = !self.semantic_model.future_annotations(); + let runtime_annotation = !self.semantic.future_annotations(); for arg in &args.posonlyargs { if let Some(expr) = &arg.annotation { @@ -1882,22 +1879,22 @@ where let definition = docstrings::extraction::extract_definition( ExtractionTarget::Function, stmt, - self.semantic_model.definition_id, - &self.semantic_model.definitions, + self.semantic.definition_id, + &self.semantic.definitions, ); - self.semantic_model.push_definition(definition); + self.semantic.push_definition(definition); - self.semantic_model.push_scope(match &stmt { + self.semantic.push_scope(match &stmt { Stmt::FunctionDef(stmt) => ScopeKind::Function(stmt), Stmt::AsyncFunctionDef(stmt) => ScopeKind::AsyncFunction(stmt), _ => unreachable!("Expected Stmt::FunctionDef | Stmt::AsyncFunctionDef"), }); - self.deferred.functions.push(self.semantic_model.snapshot()); + self.deferred.functions.push(self.semantic.snapshot()); // Extract any global bindings from the function body. if let Some(globals) = Globals::from_body(body) { - self.semantic_model.set_globals(globals); + self.semantic.set_globals(globals); } } Stmt::ClassDef( @@ -1922,16 +1919,16 @@ where let definition = docstrings::extraction::extract_definition( ExtractionTarget::Class, stmt, - self.semantic_model.definition_id, - &self.semantic_model.definitions, + self.semantic.definition_id, + &self.semantic.definitions, ); - self.semantic_model.push_definition(definition); + self.semantic.push_definition(definition); - self.semantic_model.push_scope(ScopeKind::Class(class_def)); + self.semantic.push_scope(ScopeKind::Class(class_def)); // Extract any global bindings from the class body. if let Some(globals) = Globals::from_body(body) { - self.semantic_model.set_globals(globals); + self.semantic.set_globals(globals); } self.visit_body(body); @@ -1952,7 +1949,7 @@ where }) => { let mut handled_exceptions = Exceptions::empty(); for type_ in extract_handled_exceptions(handlers) { - if let Some(call_path) = self.semantic_model.resolve_call_path(type_) { + if let Some(call_path) = self.semantic.resolve_call_path(type_) { match call_path.as_slice() { ["", "NameError"] => { handled_exceptions |= Exceptions::NAME_ERROR; @@ -1968,9 +1965,7 @@ where } } - self.semantic_model - .handled_exceptions - .push(handled_exceptions); + self.semantic.handled_exceptions.push(handled_exceptions); if self.enabled(Rule::JumpStatementInFinally) { flake8_bugbear::rules::jump_statement_in_finally(self, finalbody); @@ -1982,9 +1977,9 @@ where } self.visit_body(body); - self.semantic_model.handled_exceptions.pop(); + self.semantic.handled_exceptions.pop(); - self.semantic_model.flags |= SemanticModelFlags::EXCEPTION_HANDLER; + self.semantic.flags |= SemanticModelFlags::EXCEPTION_HANDLER; for excepthandler in handlers { self.visit_excepthandler(excepthandler); } @@ -2001,8 +1996,8 @@ where // If we're in a class or module scope, then the annotation needs to be // available at runtime. // See: https://docs.python.org/3/reference/simple_stmts.html#annotated-assignment-statements - let runtime_annotation = if self.semantic_model.future_annotations() { - if self.semantic_model.scope().kind.is_class() { + let runtime_annotation = if self.semantic.future_annotations() { + if self.semantic.scope().kind.is_class() { let baseclasses = &self .settings .flake8_type_checking @@ -2012,16 +2007,16 @@ where .flake8_type_checking .runtime_evaluated_decorators; flake8_type_checking::helpers::runtime_evaluated( - &self.semantic_model, baseclasses, decorators, + &self.semantic, ) } else { false } } else { matches!( - self.semantic_model.scope().kind, + self.semantic.scope().kind, ScopeKind::Class(_) | ScopeKind::Module ) }; @@ -2032,10 +2027,7 @@ where self.visit_annotation(annotation); } if let Some(expr) = value { - if self - .semantic_model - .match_typing_expr(annotation, "TypeAlias") - { + if self.semantic.match_typing_expr(annotation, "TypeAlias") { self.visit_type_definition(expr); } else { self.visit_expr(expr); @@ -2073,8 +2065,8 @@ where ) => { self.visit_boolean_test(test); - if typing::is_type_checking_block(stmt_if, &self.semantic_model) { - if self.semantic_model.at_top_level() { + if typing::is_type_checking_block(stmt_if, &self.semantic) { + if self.semantic.at_top_level() { self.importer.visit_type_checking_block(stmt); } if self.enabled(Rule::EmptyTypeCheckingBlock) { @@ -2094,12 +2086,12 @@ where // Post-visit. match stmt { Stmt::FunctionDef(_) | Stmt::AsyncFunctionDef(_) => { - self.semantic_model.pop_scope(); - self.semantic_model.pop_definition(); + self.semantic.pop_scope(); + self.semantic.pop_definition(); } Stmt::ClassDef(ast::StmtClassDef { name, .. }) => { - self.semantic_model.pop_scope(); - self.semantic_model.pop_definition(); + self.semantic.pop_scope(); + self.semantic.pop_definition(); self.add_binding( name, stmt.range(), @@ -2110,22 +2102,22 @@ where _ => {} } - self.semantic_model.flags = flags_snapshot; - self.semantic_model.pop_stmt(); + self.semantic.flags = flags_snapshot; + self.semantic.pop_stmt(); } fn visit_annotation(&mut self, expr: &'b Expr) { - let flags_snapshot = self.semantic_model.flags; - self.semantic_model.flags |= SemanticModelFlags::ANNOTATION; + let flags_snapshot = self.semantic.flags; + self.semantic.flags |= SemanticModelFlags::ANNOTATION; self.visit_type_definition(expr); - self.semantic_model.flags = flags_snapshot; + self.semantic.flags = flags_snapshot; } fn visit_expr(&mut self, expr: &'b Expr) { - if !self.semantic_model.in_f_string() - && !self.semantic_model.in_deferred_type_definition() - && self.semantic_model.in_type_definition() - && self.semantic_model.future_annotations() + if !self.semantic.in_f_string() + && !self.semantic.in_deferred_type_definition() + && self.semantic.in_type_definition() + && self.semantic.future_annotations() { if let Expr::Constant(ast::ExprConstant { value: Constant::Str(value), @@ -2135,21 +2127,21 @@ where self.deferred.string_type_definitions.push(( expr.range(), value, - self.semantic_model.snapshot(), + self.semantic.snapshot(), )); } else { self.deferred .future_type_definitions - .push((expr, self.semantic_model.snapshot())); + .push((expr, self.semantic.snapshot())); } return; } - self.semantic_model.push_expr(expr); + self.semantic.push_expr(expr); // Store the flags prior to any further descent, so that we can restore them after visiting // the node. - let flags_snapshot = self.semantic_model.flags; + let flags_snapshot = self.semantic.flags; // If we're in a boolean test (e.g., the `test` of a `Stmt::If`), but now within a // subexpression (e.g., `a` in `f(a)`), then we're no longer in a boolean test. @@ -2161,7 +2153,7 @@ where .. }) ) { - self.semantic_model.flags -= SemanticModelFlags::BOOLEAN_TEST; + self.semantic.flags -= SemanticModelFlags::BOOLEAN_TEST; } // Pre-visit. @@ -2172,14 +2164,13 @@ where Rule::FutureRewritableTypeAnnotation, Rule::NonPEP604Annotation, ]) { - if let Some(operator) = - typing::to_pep604_operator(value, slice, &self.semantic_model) + if let Some(operator) = typing::to_pep604_operator(value, slice, &self.semantic) { if self.enabled(Rule::FutureRewritableTypeAnnotation) { if self.settings.target_version < PythonVersion::Py310 && self.settings.target_version >= PythonVersion::Py37 - && !self.semantic_model.future_annotations() - && self.semantic_model.in_annotation() + && !self.semantic.future_annotations() + && self.semantic.in_annotation() { flake8_future_annotations::rules::future_rewritable_type_annotation( self, value, @@ -2189,8 +2180,8 @@ where if self.enabled(Rule::NonPEP604Annotation) { if self.settings.target_version >= PythonVersion::Py310 || (self.settings.target_version >= PythonVersion::Py37 - && self.semantic_model.future_annotations() - && self.semantic_model.in_annotation()) + && self.semantic.future_annotations() + && self.semantic.in_annotation()) { pyupgrade::rules::use_pep604_annotation( self, expr, slice, operator, @@ -2203,9 +2194,9 @@ where // Ex) list[...] if self.enabled(Rule::FutureRequiredTypeAnnotation) { if self.settings.target_version < PythonVersion::Py39 - && !self.semantic_model.future_annotations() - && self.semantic_model.in_annotation() - && typing::is_pep585_generic(value, &self.semantic_model) + && !self.semantic.future_annotations() + && self.semantic.in_annotation() + && typing::is_pep585_generic(value, &self.semantic) { flake8_future_annotations::rules::future_required_type_annotation( self, @@ -2215,8 +2206,8 @@ where } } - if self.semantic_model.match_typing_expr(value, "Literal") { - self.semantic_model.flags |= SemanticModelFlags::LITERAL; + if self.semantic.match_typing_expr(value, "Literal") { + self.semantic.flags |= SemanticModelFlags::LITERAL; } if self.any_enabled(&[ Rule::SysVersionSlice3, @@ -2278,13 +2269,13 @@ where Rule::NonPEP585Annotation, ]) { if let Some(replacement) = - typing::to_pep585_generic(expr, &self.semantic_model) + typing::to_pep585_generic(expr, &self.semantic) { if self.enabled(Rule::FutureRewritableTypeAnnotation) { if self.settings.target_version < PythonVersion::Py39 && self.settings.target_version >= PythonVersion::Py37 - && !self.semantic_model.future_annotations() - && self.semantic_model.in_annotation() + && !self.semantic.future_annotations() + && self.semantic.in_annotation() { flake8_future_annotations::rules::future_rewritable_type_annotation( self, expr, @@ -2294,8 +2285,8 @@ where if self.enabled(Rule::NonPEP585Annotation) { if self.settings.target_version >= PythonVersion::Py39 || (self.settings.target_version >= PythonVersion::Py37 - && self.semantic_model.future_annotations() - && self.semantic_model.in_annotation()) + && self.semantic.future_annotations() + && self.semantic.in_annotation()) { pyupgrade::rules::use_pep585_annotation( self, @@ -2318,7 +2309,7 @@ where } } - if let ScopeKind::Class(class_def) = self.semantic_model.scope().kind { + if let ScopeKind::Class(class_def) = self.semantic.scope().kind { if self.enabled(Rule::BuiltinAttributeShadowing) { flake8_builtins::rules::builtin_attribute_shadowing( self, @@ -2354,13 +2345,12 @@ where Rule::FutureRewritableTypeAnnotation, Rule::NonPEP585Annotation, ]) { - if let Some(replacement) = typing::to_pep585_generic(expr, &self.semantic_model) - { + if let Some(replacement) = typing::to_pep585_generic(expr, &self.semantic) { if self.enabled(Rule::FutureRewritableTypeAnnotation) { if self.settings.target_version < PythonVersion::Py39 && self.settings.target_version >= PythonVersion::Py37 - && !self.semantic_model.future_annotations() - && self.semantic_model.in_annotation() + && !self.semantic.future_annotations() + && self.semantic.in_annotation() { flake8_future_annotations::rules::future_rewritable_type_annotation( self, expr, @@ -2370,8 +2360,8 @@ where if self.enabled(Rule::NonPEP585Annotation) { if self.settings.target_version >= PythonVersion::Py39 || (self.settings.target_version >= PythonVersion::Py37 - && self.semantic_model.future_annotations() - && self.semantic_model.in_annotation()) + && self.semantic.future_annotations() + && self.semantic.in_annotation()) { pyupgrade::rules::use_pep585_annotation(self, expr, &replacement); } @@ -2416,7 +2406,7 @@ where }) => { if let Expr::Name(ast::ExprName { id, ctx, range: _ }) = func.as_ref() { if id == "locals" && matches!(ctx, ExprContext::Load) { - let scope = self.semantic_model.scope_mut(); + let scope = self.semantic.scope_mut(); scope.set_uses_locals(); } } @@ -2748,7 +2738,7 @@ where flake8_comprehensions::rules::unnecessary_map( self, expr, - self.semantic_model.expr_parent(), + self.semantic.expr_parent(), func, args, ); @@ -3151,8 +3141,8 @@ where // Ex) `str | None` if self.enabled(Rule::FutureRequiredTypeAnnotation) { if self.settings.target_version < PythonVersion::Py310 - && !self.semantic_model.future_annotations() - && self.semantic_model.in_annotation() + && !self.semantic.future_annotations() + && self.semantic.in_annotation() { flake8_future_annotations::rules::future_required_type_annotation( self, @@ -3164,8 +3154,8 @@ where if self.is_stub { if self.enabled(Rule::DuplicateUnionMember) - && self.semantic_model.in_type_definition() - && self.semantic_model.expr_parent().map_or(true, |parent| { + && self.semantic.in_type_definition() + && self.semantic.expr_parent().map_or(true, |parent| { !matches!( parent, Expr::BinOp(ast::ExprBinOp { @@ -3316,14 +3306,14 @@ where kind, range: _, }) => { - if self.semantic_model.in_type_definition() - && !self.semantic_model.in_literal() - && !self.semantic_model.in_f_string() + if self.semantic.in_type_definition() + && !self.semantic.in_literal() + && !self.semantic.in_f_string() { self.deferred.string_type_definitions.push(( expr.range(), value, - self.semantic_model.snapshot(), + self.semantic.snapshot(), )); } if self.enabled(Rule::HardcodedBindAllInterfaces) { @@ -3367,7 +3357,7 @@ where for expr in &args.defaults { self.visit_expr(expr); } - self.semantic_model.push_scope(ScopeKind::Lambda(lambda)); + self.semantic.push_scope(ScopeKind::Lambda(lambda)); } Expr::IfExp(ast::ExprIfExp { test, @@ -3541,9 +3531,7 @@ where self.visit_expr(value); } Expr::Lambda(_) => { - self.deferred - .lambdas - .push((expr, self.semantic_model.snapshot())); + self.deferred.lambdas.push((expr, self.semantic.snapshot())); } Expr::IfExp(ast::ExprIfExp { test, @@ -3561,53 +3549,41 @@ where keywords, range: _, }) => { - let callable = self - .semantic_model - .resolve_call_path(func) - .and_then(|call_path| { - if self - .semantic_model - .match_typing_call_path(&call_path, "cast") - { - Some(typing::Callable::Cast) - } else if self - .semantic_model - .match_typing_call_path(&call_path, "NewType") - { - Some(typing::Callable::NewType) - } else if self - .semantic_model - .match_typing_call_path(&call_path, "TypeVar") - { - Some(typing::Callable::TypeVar) - } else if self - .semantic_model - .match_typing_call_path(&call_path, "NamedTuple") - { - Some(typing::Callable::NamedTuple) - } else if self - .semantic_model - .match_typing_call_path(&call_path, "TypedDict") - { - Some(typing::Callable::TypedDict) - } else if [ - "Arg", - "DefaultArg", - "NamedArg", - "DefaultNamedArg", - "VarArg", - "KwArg", - ] - .iter() - .any(|target| call_path.as_slice() == ["mypy_extensions", target]) - { - Some(typing::Callable::MypyExtension) - } else if call_path.as_slice() == ["", "bool"] { - Some(typing::Callable::Bool) - } else { - None - } - }); + let callable = self.semantic.resolve_call_path(func).and_then(|call_path| { + if self.semantic.match_typing_call_path(&call_path, "cast") { + Some(typing::Callable::Cast) + } else if self.semantic.match_typing_call_path(&call_path, "NewType") { + Some(typing::Callable::NewType) + } else if self.semantic.match_typing_call_path(&call_path, "TypeVar") { + Some(typing::Callable::TypeVar) + } else if self + .semantic + .match_typing_call_path(&call_path, "NamedTuple") + { + Some(typing::Callable::NamedTuple) + } else if self + .semantic + .match_typing_call_path(&call_path, "TypedDict") + { + Some(typing::Callable::TypedDict) + } else if [ + "Arg", + "DefaultArg", + "NamedArg", + "DefaultNamedArg", + "VarArg", + "KwArg", + ] + .iter() + .any(|target| call_path.as_slice() == ["mypy_extensions", target]) + { + Some(typing::Callable::MypyExtension) + } else if call_path.as_slice() == ["", "bool"] { + Some(typing::Callable::Bool) + } else { + None + } + }); match callable { Some(typing::Callable::Bool) => { self.visit_expr(func); @@ -3788,15 +3764,15 @@ where // `obj["foo"]["bar"]`, we need to avoid treating the `obj["foo"]` // portion as an annotation, despite having `ExprContext::Load`. Thus, we track // the `ExprContext` at the top-level. - if self.semantic_model.in_subscript() { + if self.semantic.in_subscript() { visitor::walk_expr(self, expr); } else if matches!(ctx, ExprContext::Store | ExprContext::Del) { - self.semantic_model.flags |= SemanticModelFlags::SUBSCRIPT; + self.semantic.flags |= SemanticModelFlags::SUBSCRIPT; visitor::walk_expr(self, expr); } else { match typing::match_annotated_subscript( value, - &self.semantic_model, + &self.semantic, self.settings.typing_modules.iter().map(String::as_str), &self.settings.pyflakes.extend_generics, ) { @@ -3840,7 +3816,7 @@ where } } Expr::JoinedStr(_) => { - self.semantic_model.flags |= if self.semantic_model.in_f_string() { + self.semantic.flags |= if self.semantic.in_f_string() { SemanticModelFlags::NESTED_F_STRING } else { SemanticModelFlags::F_STRING @@ -3857,13 +3833,13 @@ where | Expr::ListComp(_) | Expr::DictComp(_) | Expr::SetComp(_) => { - self.semantic_model.pop_scope(); + self.semantic.pop_scope(); } _ => {} }; - self.semantic_model.flags = flags_snapshot; - self.semantic_model.pop_expr(); + self.semantic.flags = flags_snapshot; + self.semantic.pop_expr(); } fn visit_excepthandler(&mut self, excepthandler: &'b Excepthandler) { @@ -3963,7 +3939,7 @@ where ); // If the exception name wasn't used in the scope, emit a diagnostic. - if !self.semantic_model.is_used(binding_id) { + if !self.semantic.is_used(binding_id) { if self.enabled(Rule::UnusedVariable) { let mut diagnostic = Diagnostic::new( pyflakes::rules::UnusedVariable { name: name.into() }, @@ -4096,18 +4072,18 @@ where flake8_pie::rules::no_unnecessary_pass(self, body); } - let prev_body = self.semantic_model.body; - let prev_body_index = self.semantic_model.body_index; - self.semantic_model.body = body; - self.semantic_model.body_index = 0; + let prev_body = self.semantic.body; + let prev_body_index = self.semantic.body_index; + self.semantic.body = body; + self.semantic.body_index = 0; for stmt in body { self.visit_stmt(stmt); - self.semantic_model.body_index += 1; + self.semantic.body_index += 1; } - self.semantic_model.body = prev_body; - self.semantic_model.body_index = prev_body_index; + self.semantic.body = prev_body; + self.semantic.body_index = prev_body_index; } } @@ -4158,7 +4134,7 @@ impl<'a> Checker<'a> { // while all subsequent reads and writes are evaluated in the inner scope. In particular, // `x` is local to `foo`, and the `T` in `y=T` skips the class scope when resolving. self.visit_expr(&generator.iter); - self.semantic_model.push_scope(ScopeKind::Generator); + self.semantic.push_scope(ScopeKind::Generator); self.visit_expr(&generator.target); for expr in &generator.ifs { self.visit_boolean_test(expr); @@ -4175,36 +4151,36 @@ impl<'a> Checker<'a> { /// Visit an body of [`Stmt`] nodes within a type-checking block. fn visit_type_checking_block(&mut self, body: &'a [Stmt]) { - let snapshot = self.semantic_model.flags; - self.semantic_model.flags |= SemanticModelFlags::TYPE_CHECKING_BLOCK; + let snapshot = self.semantic.flags; + self.semantic.flags |= SemanticModelFlags::TYPE_CHECKING_BLOCK; self.visit_body(body); - self.semantic_model.flags = snapshot; + self.semantic.flags = snapshot; } /// Visit an [`Expr`], and treat it as a type definition. fn visit_type_definition(&mut self, expr: &'a Expr) { - let snapshot = self.semantic_model.flags; - self.semantic_model.flags |= SemanticModelFlags::TYPE_DEFINITION; + let snapshot = self.semantic.flags; + self.semantic.flags |= SemanticModelFlags::TYPE_DEFINITION; self.visit_expr(expr); - self.semantic_model.flags = snapshot; + self.semantic.flags = snapshot; } /// Visit an [`Expr`], and treat it as _not_ a type definition. fn visit_non_type_definition(&mut self, expr: &'a Expr) { - let snapshot = self.semantic_model.flags; - self.semantic_model.flags -= SemanticModelFlags::TYPE_DEFINITION; + let snapshot = self.semantic.flags; + self.semantic.flags -= SemanticModelFlags::TYPE_DEFINITION; self.visit_expr(expr); - self.semantic_model.flags = snapshot; + self.semantic.flags = snapshot; } /// Visit an [`Expr`], and treat it as a boolean test. This is useful for detecting whether an /// expressions return value is significant, or whether the calling context only relies on /// its truthiness. fn visit_boolean_test(&mut self, expr: &'a Expr) { - let snapshot = self.semantic_model.flags; - self.semantic_model.flags |= SemanticModelFlags::BOOLEAN_TEST; + let snapshot = self.semantic.flags; + self.semantic.flags |= SemanticModelFlags::BOOLEAN_TEST; self.visit_expr(expr); - self.semantic_model.flags = snapshot; + self.semantic.flags = snapshot; } /// Add a [`Binding`] to the current scope, bound to the given name. @@ -4220,28 +4196,28 @@ impl<'a> Checker<'a> { // expressions in generators and comprehensions bind to the scope that contains the // outermost comprehension. let scope_id = if kind.is_named_expr_assignment() { - self.semantic_model + self.semantic .scopes - .ancestor_ids(self.semantic_model.scope_id) - .find_or_last(|scope_id| !self.semantic_model.scopes[*scope_id].kind.is_generator()) - .unwrap_or(self.semantic_model.scope_id) + .ancestor_ids(self.semantic.scope_id) + .find_or_last(|scope_id| !self.semantic.scopes[*scope_id].kind.is_generator()) + .unwrap_or(self.semantic.scope_id) } else { - self.semantic_model.scope_id + self.semantic.scope_id }; // Create the `Binding`. - let binding_id = self.semantic_model.push_binding(range, kind, flags); - let binding = self.semantic_model.binding(binding_id); + let binding_id = self.semantic.push_binding(range, kind, flags); + let binding = self.semantic.binding(binding_id); // Determine whether the binding shadows any existing bindings. if let Some((stack_index, shadowed_id)) = self - .semantic_model + .semantic .scopes - .ancestors(self.semantic_model.scope_id) + .ancestors(self.semantic.scope_id) .enumerate() .find_map(|(stack_index, scope)| { scope.get(name).and_then(|binding_id| { - let binding = self.semantic_model.binding(binding_id); + let binding = self.semantic.binding(binding_id); if binding.is_unbound() { None } else { @@ -4250,12 +4226,12 @@ impl<'a> Checker<'a> { }) }) { - let shadowed = self.semantic_model.binding(shadowed_id); + let shadowed = self.semantic.binding(shadowed_id); let in_current_scope = stack_index == 0; if !shadowed.kind.is_builtin() && shadowed.source.map_or(true, |left| { binding.source.map_or(true, |right| { - !branch_detection::different_forks(left, right, &self.semantic_model.stmts) + !branch_detection::different_forks(left, right, &self.semantic.stmts) }) }) { @@ -4285,18 +4261,14 @@ impl<'a> Checker<'a> { && (!self.settings.dummy_variable_rgx.is_match(name) || shadows_import) && !(shadowed.kind.is_function_definition() && visibility::is_overload( - &self.semantic_model, - cast::decorator_list( - self.semantic_model.stmts[shadowed.source.unwrap()], - ), + cast::decorator_list(self.semantic.stmts[shadowed.source.unwrap()]), + &self.semantic, )) { if self.enabled(Rule::RedefinedWhileUnused) { #[allow(deprecated)] let line = self.locator.compute_line_index( - shadowed - .trimmed_range(&self.semantic_model, self.locator) - .start(), + shadowed.trimmed_range(&self.semantic, self.locator).start(), ); let mut diagnostic = Diagnostic::new( @@ -4304,16 +4276,16 @@ impl<'a> Checker<'a> { name: name.to_string(), line, }, - binding.trimmed_range(&self.semantic_model, self.locator), + binding.trimmed_range(&self.semantic, self.locator), ); - if let Some(range) = binding.parent_range(&self.semantic_model) { + if let Some(range) = binding.parent_range(&self.semantic) { diagnostic.set_parent(range.start()); } self.diagnostics.push(diagnostic); } } } else if shadows_import && binding.redefines(shadowed) { - self.semantic_model + self.semantic .shadowed_bindings .insert(binding_id, shadowed_id); } @@ -4321,16 +4293,16 @@ impl<'a> Checker<'a> { } // If there's an existing binding in this scope, copy its references. - if let Some(shadowed_id) = self.semantic_model.scopes[scope_id].get(name) { + if let Some(shadowed_id) = self.semantic.scopes[scope_id].get(name) { // If this is an annotation, and we already have an existing value in the same scope, // don't treat it as an assignment, but track it as a delayed annotation. - if self.semantic_model.binding(binding_id).kind.is_annotation() { - self.semantic_model + if self.semantic.binding(binding_id).kind.is_annotation() { + self.semantic .add_delayed_annotation(shadowed_id, binding_id); return binding_id; } - let shadowed = &self.semantic_model.bindings[shadowed_id]; + let shadowed = &self.semantic.bindings[shadowed_id]; match &shadowed.kind { BindingKind::Builtin | BindingKind::Deletion | BindingKind::UnboundException => { // Avoid overriding builtins. @@ -4339,18 +4311,18 @@ impl<'a> Checker<'a> { // If the original binding was a global or nonlocal, then the new binding is // too. let references = shadowed.references.clone(); - self.semantic_model.bindings[binding_id].kind = kind.clone(); - self.semantic_model.bindings[binding_id].references = references; + self.semantic.bindings[binding_id].kind = kind.clone(); + self.semantic.bindings[binding_id].references = references; } _ => { let references = shadowed.references.clone(); - self.semantic_model.bindings[binding_id].references = references; + self.semantic.bindings[binding_id].references = references; } } } // Add the binding to the scope. - let scope = &mut self.semantic_model.scopes[scope_id]; + let scope = &mut self.semantic.scopes[scope_id]; scope.add(name, binding_id); binding_id @@ -4364,8 +4336,8 @@ impl<'a> Checker<'a> { .chain(self.settings.builtins.iter().map(String::as_str)) { // Add the builtin to the scope. - let binding_id = self.semantic_model.push_builtin(); - let scope = self.semantic_model.scope_mut(); + let binding_id = self.semantic.push_builtin(); + let scope = self.semantic.scope_mut(); scope.add(builtin, binding_id); } } @@ -4374,7 +4346,7 @@ impl<'a> Checker<'a> { let Expr::Name(ast::ExprName { id, .. }) = expr else { return; }; - match self.semantic_model.resolve_read(id, expr.range()) { + match self.semantic.resolve_read(id, expr.range()) { ResolvedRead::Resolved(_) | ResolvedRead::ImplicitGlobal => { // Nothing to do. } @@ -4382,7 +4354,7 @@ impl<'a> Checker<'a> { // F405 if self.enabled(Rule::UndefinedLocalWithImportStarUsage) { let sources: Vec = self - .semantic_model + .semantic .scopes .iter() .flat_map(Scope::star_imports) @@ -4411,7 +4383,7 @@ impl<'a> Checker<'a> { // Avoid flagging if `NameError` is handled. if self - .semantic_model + .semantic .handled_exceptions .iter() .any(|handler_names| handler_names.contains(Exceptions::NAME_ERROR)) @@ -4431,37 +4403,30 @@ impl<'a> Checker<'a> { } fn handle_node_store(&mut self, id: &'a str, expr: &Expr) { - let parent = self.semantic_model.stmt(); + let parent = self.semantic.stmt(); if self.enabled(Rule::UndefinedLocal) { pyflakes::rules::undefined_local(self, id); } if self.enabled(Rule::NonLowercaseVariableInFunction) { - if self.semantic_model.scope().kind.is_any_function() { + if self.semantic.scope().kind.is_any_function() { // Ignore globals. - if !self - .semantic_model - .scope() - .get(id) - .map_or(false, |binding_id| { - self.semantic_model.binding(binding_id).kind.is_global() - }) - { + if !self.semantic.scope().get(id).map_or(false, |binding_id| { + self.semantic.binding(binding_id).kind.is_global() + }) { pep8_naming::rules::non_lowercase_variable_in_function(self, expr, parent, id); } } } if self.enabled(Rule::MixedCaseVariableInClassScope) { - if let ScopeKind::Class(ast::StmtClassDef { bases, .. }) = - &self.semantic_model.scope().kind - { + if let ScopeKind::Class(ast::StmtClassDef { bases, .. }) = &self.semantic.scope().kind { pep8_naming::rules::mixed_case_variable_in_class_scope( self, expr, parent, id, bases, ); } } if self.enabled(Rule::MixedCaseVariableInGlobalScope) { - if matches!(self.semantic_model.scope().kind, ScopeKind::Module) { + if matches!(self.semantic.scope().kind, ScopeKind::Module) { pep8_naming::rules::mixed_case_variable_in_global_scope(self, expr, parent, id); } } @@ -4499,7 +4464,7 @@ impl<'a> Checker<'a> { return; } - let scope = self.semantic_model.scope(); + let scope = self.semantic.scope(); if scope.kind.is_module() && match parent { @@ -4527,8 +4492,7 @@ impl<'a> Checker<'a> { _ => false, } { - let (names, flags) = - extract_all_names(parent, |name| self.semantic_model.is_builtin(name)); + let (names, flags) = extract_all_names(parent, |name| self.semantic.is_builtin(name)); if self.enabled(Rule::InvalidAllFormat) { if matches!(flags, AllNamesFlags::INVALID_FORMAT) { @@ -4554,7 +4518,7 @@ impl<'a> Checker<'a> { } if self - .semantic_model + .semantic .expr_ancestors() .any(|expr| matches!(expr, Expr::NamedExpr(_))) { @@ -4581,16 +4545,13 @@ impl<'a> Checker<'a> { }; // Treat the deletion of a name as a reference to that name. - if let Some(binding_id) = self.semantic_model.scope().get(id) { - self.semantic_model.add_local_reference( - binding_id, - expr.range(), - ExecutionContext::Runtime, - ); + if let Some(binding_id) = self.semantic.scope().get(id) { + self.semantic + .add_local_reference(binding_id, expr.range(), ExecutionContext::Runtime); // If the name is unbound, then it's an error. if self.enabled(Rule::UndefinedName) { - let binding = self.semantic_model.binding(binding_id); + let binding = self.semantic.binding(binding_id); if binding.is_unbound() { self.diagnostics.push(Diagnostic::new( pyflakes::rules::UndefinedName { @@ -4612,17 +4573,15 @@ impl<'a> Checker<'a> { } } - if helpers::on_conditional_branch(&mut self.semantic_model.parents()) { + if helpers::on_conditional_branch(&mut self.semantic.parents()) { return; } // Create a binding to model the deletion. - let binding_id = self.semantic_model.push_binding( - expr.range(), - BindingKind::Deletion, - BindingFlags::empty(), - ); - let scope = self.semantic_model.scope_mut(); + let binding_id = + self.semantic + .push_binding(expr.range(), BindingKind::Deletion, BindingFlags::empty()); + let scope = self.semantic.scope_mut(); scope.add(id, binding_id); } @@ -4630,9 +4589,9 @@ impl<'a> Checker<'a> { while !self.deferred.future_type_definitions.is_empty() { let type_definitions = std::mem::take(&mut self.deferred.future_type_definitions); for (expr, snapshot) in type_definitions { - self.semantic_model.restore(snapshot); + self.semantic.restore(snapshot); - self.semantic_model.flags |= SemanticModelFlags::TYPE_DEFINITION + self.semantic.flags |= SemanticModelFlags::TYPE_DEFINITION | SemanticModelFlags::FUTURE_TYPE_DEFINITION; self.visit_expr(expr); } @@ -4646,11 +4605,9 @@ impl<'a> Checker<'a> { if let Ok((expr, kind)) = parse_type_annotation(value, range, self.locator) { let expr = allocator.alloc(expr); - self.semantic_model.restore(snapshot); + self.semantic.restore(snapshot); - if self.semantic_model.in_annotation() - && self.semantic_model.future_annotations() - { + if self.semantic.in_annotation() && self.semantic.future_annotations() { if self.enabled(Rule::QuotedAnnotation) { pyupgrade::rules::quoted_annotation(self, value, range); } @@ -4668,7 +4625,7 @@ impl<'a> Checker<'a> { } }; - self.semantic_model.flags |= + self.semantic.flags |= SemanticModelFlags::TYPE_DEFINITION | type_definition_flag; self.visit_expr(expr); } else { @@ -4689,9 +4646,9 @@ impl<'a> Checker<'a> { while !self.deferred.functions.is_empty() { let deferred_functions = std::mem::take(&mut self.deferred.functions); for snapshot in deferred_functions { - self.semantic_model.restore(snapshot); + self.semantic.restore(snapshot); - match &self.semantic_model.stmt() { + match &self.semantic.stmt() { Stmt::FunctionDef(ast::StmtFunctionDef { body, args, .. }) | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { body, args, .. }) => { self.visit_arguments(args); @@ -4711,7 +4668,7 @@ impl<'a> Checker<'a> { while !self.deferred.lambdas.is_empty() { let lambdas = std::mem::take(&mut self.deferred.lambdas); for (expr, snapshot) in lambdas { - self.semantic_model.restore(snapshot); + self.semantic.restore(snapshot); if let Expr::Lambda(ast::ExprLambda { args, @@ -4734,13 +4691,13 @@ impl<'a> Checker<'a> { while !self.deferred.assignments.is_empty() { let assignments = std::mem::take(&mut self.deferred.assignments); for snapshot in assignments { - self.semantic_model.restore(snapshot); + self.semantic.restore(snapshot); if self.enabled(Rule::UnusedVariable) { - pyflakes::rules::unused_variable(self, self.semantic_model.scope_id); + pyflakes::rules::unused_variable(self, self.semantic.scope_id); } if self.enabled(Rule::UnusedAnnotation) { - pyflakes::rules::unused_annotation(self, self.semantic_model.scope_id); + pyflakes::rules::unused_annotation(self, self.semantic.scope_id); } if !self.is_stub { if self.any_enabled(&[ @@ -4750,8 +4707,8 @@ impl<'a> Checker<'a> { Rule::UnusedStaticMethodArgument, Rule::UnusedLambdaArgument, ]) { - let scope = &self.semantic_model.scopes[self.semantic_model.scope_id]; - let parent = &self.semantic_model.scopes[scope.parent.unwrap()]; + let scope = &self.semantic.scopes[self.semantic.scope_id]; + let parent = &self.semantic.scopes[scope.parent.unwrap()]; self.diagnostics .extend(flake8_unused_arguments::rules::unused_arguments( self, parent, scope, @@ -4767,11 +4724,10 @@ impl<'a> Checker<'a> { let for_loops = std::mem::take(&mut self.deferred.for_loops); for snapshot in for_loops { - self.semantic_model.restore(snapshot); + self.semantic.restore(snapshot); if let Stmt::For(ast::StmtFor { target, body, .. }) - | Stmt::AsyncFor(ast::StmtAsyncFor { target, body, .. }) = - &self.semantic_model.stmt() + | Stmt::AsyncFor(ast::StmtAsyncFor { target, body, .. }) = &self.semantic.stmt() { if self.enabled(Rule::UnusedLoopControlVariable) { flake8_bugbear::rules::unused_loop_control_variable(self, target, body); @@ -4806,10 +4762,10 @@ impl<'a> Checker<'a> { // Mark anything referenced in `__all__` as used. let exports: Vec<(&str, TextRange)> = { - self.semantic_model + self.semantic .global_scope() .get_all("__all__") - .map(|binding_id| &self.semantic_model.bindings[binding_id]) + .map(|binding_id| &self.semantic.bindings[binding_id]) .filter_map(|binding| match &binding.kind { BindingKind::Export(Export { names }) => { Some(names.iter().map(|name| (*name, binding.range))) @@ -4821,12 +4777,9 @@ impl<'a> Checker<'a> { }; for (name, range) in &exports { - if let Some(binding_id) = self.semantic_model.global_scope().get(name) { - self.semantic_model.add_global_reference( - binding_id, - *range, - ExecutionContext::Runtime, - ); + if let Some(binding_id) = self.semantic.global_scope().get(name) { + self.semantic + .add_global_reference(binding_id, *range, ExecutionContext::Runtime); } } @@ -4837,17 +4790,17 @@ impl<'a> Checker<'a> { if self.settings.flake8_type_checking.strict { vec![] } else { - self.semantic_model + self.semantic .scopes .iter() .map(|scope| { scope .binding_ids() - .map(|binding_id| self.semantic_model.binding(binding_id)) + .map(|binding_id| self.semantic.binding(binding_id)) .filter(|binding| { flake8_type_checking::helpers::is_valid_runtime_import( - &self.semantic_model, binding, + &self.semantic, ) }) .collect() @@ -4859,8 +4812,8 @@ impl<'a> Checker<'a> { }; let mut diagnostics: Vec = vec![]; - for scope_id in self.semantic_model.dead_scopes.iter().rev() { - let scope = &self.semantic_model.scopes[*scope_id]; + for scope_id in self.semantic.dead_scopes.iter().rev() { + let scope = &self.semantic.scopes[*scope_id]; if scope.kind.is_module() { // F822 @@ -4902,10 +4855,10 @@ impl<'a> Checker<'a> { // PLW0602 if self.enabled(Rule::GlobalVariableNotAssigned) { for (name, binding_id) in scope.bindings() { - let binding = self.semantic_model.binding(binding_id); + let binding = self.semantic.binding(binding_id); if binding.kind.is_global() { if let Some(source) = binding.source { - let stmt = &self.semantic_model.stmts[source]; + let stmt = &self.semantic.stmts[source]; if stmt.is_global_stmt() { diagnostics.push(Diagnostic::new( pylint::rules::GlobalVariableNotAssigned { @@ -4929,28 +4882,26 @@ impl<'a> Checker<'a> { // the bindings are in different scopes. if self.enabled(Rule::RedefinedWhileUnused) { for (name, binding_id) in scope.bindings() { - if let Some(shadowed_id) = self.semantic_model.shadowed_binding(binding_id) { - let shadowed = self.semantic_model.binding(shadowed_id); + if let Some(shadowed_id) = self.semantic.shadowed_binding(binding_id) { + let shadowed = self.semantic.binding(shadowed_id); if shadowed.is_used() { continue; } #[allow(deprecated)] let line = self.locator.compute_line_index( - shadowed - .trimmed_range(&self.semantic_model, self.locator) - .start(), + shadowed.trimmed_range(&self.semantic, self.locator).start(), ); - let binding = self.semantic_model.binding(binding_id); + let binding = self.semantic.binding(binding_id); let mut diagnostic = Diagnostic::new( pyflakes::rules::RedefinedWhileUnused { name: (*name).to_string(), line, }, - binding.trimmed_range(&self.semantic_model, self.locator), + binding.trimmed_range(&self.semantic, self.locator), ); - if let Some(range) = binding.parent_range(&self.semantic_model) { + if let Some(range) = binding.parent_range(&self.semantic) { diagnostic.set_parent(range.start()); } diagnostics.push(diagnostic); @@ -4962,7 +4913,7 @@ impl<'a> Checker<'a> { let runtime_imports: Vec<&Binding> = if self.settings.flake8_type_checking.strict { vec![] } else { - self.semantic_model + self.semantic .scopes .ancestor_ids(*scope_id) .flat_map(|scope_id| runtime_imports[scope_id.as_usize()].iter()) @@ -5067,10 +5018,10 @@ impl<'a> Checker<'a> { // Compute visibility of all definitions. let exports: Option> = { - let global_scope = self.semantic_model.global_scope(); + let global_scope = self.semantic.global_scope(); global_scope .get_all("__all__") - .map(|binding_id| &self.semantic_model.bindings[binding_id]) + .map(|binding_id| &self.semantic.bindings[binding_id]) .filter_map(|binding| match &binding.kind { BindingKind::Export(Export { names }) => Some(names.iter().copied()), _ => None, @@ -5080,7 +5031,7 @@ impl<'a> Checker<'a> { }) }; - let definitions = std::mem::take(&mut self.semantic_model.definitions); + let definitions = std::mem::take(&mut self.semantic.definitions); let mut overloaded_name: Option = None; for ContextualizedDefinition { definition, @@ -5098,9 +5049,9 @@ impl<'a> Checker<'a> { // classes, etc.). if !overloaded_name.map_or(false, |overloaded_name| { flake8_annotations::helpers::is_overload_impl( - &self.semantic_model, definition, &overloaded_name, + &self.semantic, ) }) { self.diagnostics @@ -5111,7 +5062,7 @@ impl<'a> Checker<'a> { )); } overloaded_name = - flake8_annotations::helpers::overloaded_name(&self.semantic_model, definition); + flake8_annotations::helpers::overloaded_name(definition, &self.semantic); } // flake8-pyi @@ -5129,9 +5080,9 @@ impl<'a> Checker<'a> { // pydocstyle if enforce_docstrings { if pydocstyle::helpers::should_ignore_definition( - &self.semantic_model, definition, &self.settings.pydocstyle.ignore_decorators, + &self.semantic, ) { continue; } @@ -5335,8 +5286,8 @@ pub(crate) fn check_ast( checker.check_definitions(); // Reset the scope to module-level, and check all consumed scopes. - checker.semantic_model.scope_id = ScopeId::global(); - checker.semantic_model.dead_scopes.push(ScopeId::global()); + checker.semantic.scope_id = ScopeId::global(); + checker.semantic.dead_scopes.push(ScopeId::global()); checker.check_dead_scopes(); checker.diagnostics diff --git a/crates/ruff/src/importer/mod.rs b/crates/ruff/src/importer/mod.rs index b31d97a066..b7a67141b6 100644 --- a/crates/ruff/src/importer/mod.rs +++ b/crates/ruff/src/importer/mod.rs @@ -116,7 +116,7 @@ impl<'a> Importer<'a> { &self, import: &StmtImports, at: TextSize, - semantic_model: &SemanticModel, + semantic: &SemanticModel, ) -> Result { // Generate the modified import statement. let content = autofix::codemods::retain_imports( @@ -130,7 +130,7 @@ impl<'a> Importer<'a> { let (type_checking_edit, type_checking) = self.get_or_import_symbol( &ImportRequest::import_from("typing", "TYPE_CHECKING"), at, - semantic_model, + semantic, )?; // Add the import to a `TYPE_CHECKING` block. @@ -164,11 +164,11 @@ impl<'a> Importer<'a> { &self, symbol: &ImportRequest, at: TextSize, - semantic_model: &SemanticModel, + semantic: &SemanticModel, ) -> Result<(Edit, String), ResolutionError> { - match self.get_symbol(symbol, at, semantic_model) { + match self.get_symbol(symbol, at, semantic) { Some(result) => result, - None => self.import_symbol(symbol, at, semantic_model), + None => self.import_symbol(symbol, at, semantic), } } @@ -177,11 +177,10 @@ impl<'a> Importer<'a> { &self, symbol: &ImportRequest, at: TextSize, - semantic_model: &SemanticModel, + semantic: &SemanticModel, ) -> Option> { // If the symbol is already available in the current scope, use it. - let imported_name = - semantic_model.resolve_qualified_import_name(symbol.module, symbol.member)?; + let imported_name = semantic.resolve_qualified_import_name(symbol.module, symbol.member)?; // If the symbol source (i.e., the import statement) comes after the current location, // abort. For example, we could be generating an edit within a function, and the import @@ -196,7 +195,7 @@ impl<'a> Importer<'a> { // If the symbol source (i.e., the import statement) is in a typing-only context, but we're // in a runtime context, abort. - if imported_name.context().is_typing() && semantic_model.execution_context().is_runtime() { + if imported_name.context().is_typing() && semantic.execution_context().is_runtime() { return Some(Err(ResolutionError::IncompatibleContext)); } @@ -233,13 +232,13 @@ impl<'a> Importer<'a> { &self, symbol: &ImportRequest, at: TextSize, - semantic_model: &SemanticModel, + semantic: &SemanticModel, ) -> Result<(Edit, String), ResolutionError> { if let Some(stmt) = self.find_import_from(symbol.module, at) { // Case 1: `from functools import lru_cache` is in scope, and we're trying to reference // `functools.cache`; thus, we add `cache` to the import, and return `"cache"` as the // bound name. - if semantic_model.is_available(symbol.member) { + if semantic.is_available(symbol.member) { let Ok(import_edit) = self.add_member(stmt, symbol.member) else { return Err(ResolutionError::InvalidEdit); }; @@ -252,7 +251,7 @@ impl<'a> Importer<'a> { ImportStyle::Import => { // Case 2a: No `functools` import is in scope; thus, we add `import functools`, // and return `"functools.cache"` as the bound name. - if semantic_model.is_available(symbol.module) { + if semantic.is_available(symbol.module) { let import_edit = self.add_import(&AnyImport::Import(Import::module(symbol.module)), at); Ok(( @@ -270,7 +269,7 @@ impl<'a> Importer<'a> { ImportStyle::ImportFrom => { // Case 2b: No `functools` import is in scope; thus, we add // `from functools import cache`, and return `"cache"` as the bound name. - if semantic_model.is_available(symbol.member) { + if semantic.is_available(symbol.member) { let import_edit = self.add_import( &AnyImport::ImportFrom(ImportFrom::member( symbol.module, diff --git a/crates/ruff/src/rules/airflow/rules/task_variable_name.rs b/crates/ruff/src/rules/airflow/rules/task_variable_name.rs index eae2490138..419053c2c3 100644 --- a/crates/ruff/src/rules/airflow/rules/task_variable_name.rs +++ b/crates/ruff/src/rules/airflow/rules/task_variable_name.rs @@ -67,7 +67,7 @@ pub(crate) fn variable_name_task_id( // If the function doesn't come from Airflow, we can't do anything. if !checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| matches!(call_path[0], "airflow")) { diff --git a/crates/ruff/src/rules/flake8_2020/helpers.rs b/crates/ruff/src/rules/flake8_2020/helpers.rs index 81f3e83383..3f86f9bfd9 100644 --- a/crates/ruff/src/rules/flake8_2020/helpers.rs +++ b/crates/ruff/src/rules/flake8_2020/helpers.rs @@ -2,8 +2,8 @@ use rustpython_parser::ast::Expr; use ruff_python_semantic::SemanticModel; -pub(super) fn is_sys(model: &SemanticModel, expr: &Expr, target: &str) -> bool { - model +pub(super) fn is_sys(expr: &Expr, target: &str, semantic: &SemanticModel) -> bool { + semantic .resolve_call_path(expr) .map_or(false, |call_path| call_path.as_slice() == ["sys", target]) } diff --git a/crates/ruff/src/rules/flake8_2020/rules/compare.rs b/crates/ruff/src/rules/flake8_2020/rules/compare.rs index 9877be4fae..185551481c 100644 --- a/crates/ruff/src/rules/flake8_2020/rules/compare.rs +++ b/crates/ruff/src/rules/flake8_2020/rules/compare.rs @@ -69,7 +69,7 @@ impl Violation for SysVersionCmpStr10 { pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &[Expr]) { match left { Expr::Subscript(ast::ExprSubscript { value, slice, .. }) - if is_sys(checker.semantic_model(), value, "version_info") => + if is_sys(value, "version_info", checker.semantic()) => { if let Expr::Constant(ast::ExprConstant { value: Constant::Int(i), @@ -111,7 +111,7 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], compara } Expr::Attribute(ast::ExprAttribute { value, attr, .. }) - if is_sys(checker.semantic_model(), value, "version_info") && attr == "minor" => + if is_sys(value, "version_info", checker.semantic()) && attr == "minor" => { if let ( [Cmpop::Lt | Cmpop::LtE | Cmpop::Gt | Cmpop::GtE], @@ -132,7 +132,7 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], compara _ => {} } - if is_sys(checker.semantic_model(), left, "version") { + if is_sys(left, "version", checker.semantic()) { if let ( [Cmpop::Lt | Cmpop::LtE | Cmpop::Gt | Cmpop::GtE], [Expr::Constant(ast::ExprConstant { diff --git a/crates/ruff/src/rules/flake8_2020/rules/name_or_attribute.rs b/crates/ruff/src/rules/flake8_2020/rules/name_or_attribute.rs index d861abd262..e8afff1335 100644 --- a/crates/ruff/src/rules/flake8_2020/rules/name_or_attribute.rs +++ b/crates/ruff/src/rules/flake8_2020/rules/name_or_attribute.rs @@ -18,7 +18,7 @@ impl Violation for SixPY3 { /// YTT202 pub(crate) fn name_or_attribute(checker: &mut Checker, expr: &Expr) { if checker - .semantic_model() + .semantic() .resolve_call_path(expr) .map_or(false, |call_path| call_path.as_slice() == ["six", "PY3"]) { diff --git a/crates/ruff/src/rules/flake8_2020/rules/subscript.rs b/crates/ruff/src/rules/flake8_2020/rules/subscript.rs index b55a602423..71d6768c86 100644 --- a/crates/ruff/src/rules/flake8_2020/rules/subscript.rs +++ b/crates/ruff/src/rules/flake8_2020/rules/subscript.rs @@ -50,7 +50,7 @@ impl Violation for SysVersionSlice1 { /// YTT101, YTT102, YTT301, YTT303 pub(crate) fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) { - if is_sys(checker.semantic_model(), value, "version") { + if is_sys(value, "version", checker.semantic()) { match slice { Expr::Slice(ast::ExprSlice { lower: None, diff --git a/crates/ruff/src/rules/flake8_annotations/helpers.rs b/crates/ruff/src/rules/flake8_annotations/helpers.rs index 7a3bd1bb15..c3db27fa2f 100644 --- a/crates/ruff/src/rules/flake8_annotations/helpers.rs +++ b/crates/ruff/src/rules/flake8_annotations/helpers.rs @@ -35,14 +35,14 @@ pub(super) fn match_function_def( } /// Return the name of the function, if it's overloaded. -pub(crate) fn overloaded_name(model: &SemanticModel, definition: &Definition) -> Option { +pub(crate) fn overloaded_name(definition: &Definition, semantic: &SemanticModel) -> Option { if let Definition::Member(Member { kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method, stmt, .. }) = definition { - if visibility::is_overload(model, cast::decorator_list(stmt)) { + if visibility::is_overload(cast::decorator_list(stmt), semantic) { let (name, ..) = match_function_def(stmt); Some(name.to_string()) } else { @@ -56,9 +56,9 @@ pub(crate) fn overloaded_name(model: &SemanticModel, definition: &Definition) -> /// Return `true` if the definition is the implementation for an overloaded /// function. pub(crate) fn is_overload_impl( - model: &SemanticModel, definition: &Definition, overloaded_name: &str, + semantic: &SemanticModel, ) -> bool { if let Definition::Member(Member { kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method, @@ -66,7 +66,7 @@ pub(crate) fn is_overload_impl( .. }) = definition { - if visibility::is_overload(model, cast::decorator_list(stmt)) { + if visibility::is_overload(cast::decorator_list(stmt), semantic) { false } else { let (name, ..) = match_function_def(stmt); diff --git a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs index 53498e5058..a5313ec9be 100644 --- a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs @@ -431,15 +431,15 @@ fn is_none_returning(body: &[Stmt]) -> bool { /// ANN401 fn check_dynamically_typed( - model: &SemanticModel, annotation: &Expr, func: F, diagnostics: &mut Vec, is_overridden: bool, + semantic: &SemanticModel, ) where F: FnOnce() -> String, { - if !is_overridden && model.match_typing_expr(annotation, "Any") { + if !is_overridden && semantic.match_typing_expr(annotation, "Any") { diagnostics.push(Diagnostic::new( AnyType { name: func() }, annotation.range(), @@ -480,7 +480,7 @@ pub(crate) fn definition( // unless configured to suppress ANN* for declarations that are fully untyped. let mut diagnostics = Vec::new(); - let is_overridden = visibility::is_override(checker.semantic_model(), decorator_list); + let is_overridden = visibility::is_override(decorator_list, checker.semantic()); // ANN001, ANN401 for arg in args @@ -492,10 +492,7 @@ pub(crate) fn definition( // If this is a non-static method, skip `cls` or `self`. usize::from( is_method - && !visibility::is_staticmethod( - checker.semantic_model(), - cast::decorator_list(stmt), - ), + && !visibility::is_staticmethod(cast::decorator_list(stmt), checker.semantic()), ), ) { @@ -504,11 +501,11 @@ pub(crate) fn definition( has_any_typed_arg = true; if checker.enabled(Rule::AnyType) { check_dynamically_typed( - checker.semantic_model(), annotation, || arg.arg.to_string(), &mut diagnostics, is_overridden, + checker.semantic(), ); } } else { @@ -535,11 +532,11 @@ pub(crate) fn definition( if checker.enabled(Rule::AnyType) { let name = &arg.arg; check_dynamically_typed( - checker.semantic_model(), expr, || format!("*{name}"), &mut diagnostics, is_overridden, + checker.semantic(), ); } } @@ -567,11 +564,11 @@ pub(crate) fn definition( if checker.enabled(Rule::AnyType) { let name = &arg.arg; check_dynamically_typed( - checker.semantic_model(), expr, || format!("**{name}"), &mut diagnostics, is_overridden, + checker.semantic(), ); } } @@ -592,13 +589,10 @@ pub(crate) fn definition( } // ANN101, ANN102 - if is_method - && !visibility::is_staticmethod(checker.semantic_model(), cast::decorator_list(stmt)) - { + if is_method && !visibility::is_staticmethod(cast::decorator_list(stmt), checker.semantic()) { if let Some(arg) = args.posonlyargs.first().or_else(|| args.args.first()) { if arg.annotation.is_none() { - if visibility::is_classmethod(checker.semantic_model(), cast::decorator_list(stmt)) - { + if visibility::is_classmethod(cast::decorator_list(stmt), checker.semantic()) { if checker.enabled(Rule::MissingTypeCls) { diagnostics.push(Diagnostic::new( MissingTypeCls { @@ -628,11 +622,11 @@ pub(crate) fn definition( has_typed_return = true; if checker.enabled(Rule::AnyType) { check_dynamically_typed( - checker.semantic_model(), expr, || name.to_string(), &mut diagnostics, is_overridden, + checker.semantic(), ); } } else if !( @@ -640,9 +634,7 @@ pub(crate) fn definition( // (explicitly or implicitly). checker.settings.flake8_annotations.suppress_none_returning && is_none_returning(body) ) { - if is_method - && visibility::is_classmethod(checker.semantic_model(), cast::decorator_list(stmt)) - { + if is_method && visibility::is_classmethod(cast::decorator_list(stmt), checker.semantic()) { if checker.enabled(Rule::MissingReturnTypeClassMethod) { diagnostics.push(Diagnostic::new( MissingReturnTypeClassMethod { @@ -652,7 +644,7 @@ pub(crate) fn definition( )); } } else if is_method - && visibility::is_staticmethod(checker.semantic_model(), cast::decorator_list(stmt)) + && visibility::is_staticmethod(cast::decorator_list(stmt), checker.semantic()) { if checker.enabled(Rule::MissingReturnTypeStaticMethod) { diagnostics.push(Diagnostic::new( diff --git a/crates/ruff/src/rules/flake8_async/rules/blocking_http_call.rs b/crates/ruff/src/rules/flake8_async/rules/blocking_http_call.rs index e7d72f8e45..dbd00a3aaf 100644 --- a/crates/ruff/src/rules/flake8_async/rules/blocking_http_call.rs +++ b/crates/ruff/src/rules/flake8_async/rules/blocking_http_call.rs @@ -64,9 +64,9 @@ const BLOCKING_HTTP_CALLS: &[&[&str]] = &[ /// ASYNC100 pub(crate) fn blocking_http_call(checker: &mut Checker, expr: &Expr) { - if checker.semantic_model().in_async_context() { + if checker.semantic().in_async_context() { if let Expr::Call(ast::ExprCall { func, .. }) = expr { - let call_path = checker.semantic_model().resolve_call_path(func); + let call_path = checker.semantic().resolve_call_path(func); let is_blocking = call_path.map_or(false, |path| BLOCKING_HTTP_CALLS.contains(&path.as_slice())); diff --git a/crates/ruff/src/rules/flake8_async/rules/blocking_os_call.rs b/crates/ruff/src/rules/flake8_async/rules/blocking_os_call.rs index ab0316f276..861689bb3d 100644 --- a/crates/ruff/src/rules/flake8_async/rules/blocking_os_call.rs +++ b/crates/ruff/src/rules/flake8_async/rules/blocking_os_call.rs @@ -56,10 +56,10 @@ const UNSAFE_OS_METHODS: &[&[&str]] = &[ /// ASYNC102 pub(crate) fn blocking_os_call(checker: &mut Checker, expr: &Expr) { - if checker.semantic_model().in_async_context() { + if checker.semantic().in_async_context() { if let Expr::Call(ast::ExprCall { func, .. }) = expr { let is_unsafe_os_method = checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |path| UNSAFE_OS_METHODS.contains(&path.as_slice())); diff --git a/crates/ruff/src/rules/flake8_async/rules/open_sleep_or_subprocess_call.rs b/crates/ruff/src/rules/flake8_async/rules/open_sleep_or_subprocess_call.rs index 1a252118c7..c0d370da82 100644 --- a/crates/ruff/src/rules/flake8_async/rules/open_sleep_or_subprocess_call.rs +++ b/crates/ruff/src/rules/flake8_async/rules/open_sleep_or_subprocess_call.rs @@ -59,10 +59,10 @@ const OPEN_SLEEP_OR_SUBPROCESS_CALL: &[&[&str]] = &[ /// ASYNC101 pub(crate) fn open_sleep_or_subprocess_call(checker: &mut Checker, expr: &Expr) { - if checker.semantic_model().in_async_context() { + if checker.semantic().in_async_context() { if let Expr::Call(ast::ExprCall { func, .. }) = expr { let is_open_sleep_or_subprocess_call = checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |path| { OPEN_SLEEP_OR_SUBPROCESS_CALL.contains(&path.as_slice()) diff --git a/crates/ruff/src/rules/flake8_bandit/helpers.rs b/crates/ruff/src/rules/flake8_bandit/helpers.rs index dc6aa6dcdb..4a72eb63d0 100644 --- a/crates/ruff/src/rules/flake8_bandit/helpers.rs +++ b/crates/ruff/src/rules/flake8_bandit/helpers.rs @@ -22,20 +22,24 @@ pub(super) fn matches_password_name(string: &str) -> bool { PASSWORD_CANDIDATE_REGEX.is_match(string) } -pub(super) fn is_untyped_exception(type_: Option<&Expr>, model: &SemanticModel) -> bool { +pub(super) fn is_untyped_exception(type_: Option<&Expr>, semantic: &SemanticModel) -> bool { type_.map_or(true, |type_| { if let Expr::Tuple(ast::ExprTuple { elts, .. }) = &type_ { elts.iter().any(|type_| { - model.resolve_call_path(type_).map_or(false, |call_path| { + semantic + .resolve_call_path(type_) + .map_or(false, |call_path| { + call_path.as_slice() == ["", "Exception"] + || call_path.as_slice() == ["", "BaseException"] + }) + }) + } else { + semantic + .resolve_call_path(type_) + .map_or(false, |call_path| { call_path.as_slice() == ["", "Exception"] || call_path.as_slice() == ["", "BaseException"] }) - }) - } else { - model.resolve_call_path(type_).map_or(false, |call_path| { - call_path.as_slice() == ["", "Exception"] - || call_path.as_slice() == ["", "BaseException"] - }) } }) } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs b/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs index 7bcf81dff6..ac5910df40 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs @@ -108,7 +108,7 @@ pub(crate) fn bad_file_permissions( keywords: &[Keyword], ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| call_path.as_slice() == ["os", "chmod"]) { 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 4595dce6c7..41e426b34d 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 @@ -60,7 +60,7 @@ fn unparse_string_format_expression(checker: &mut Checker, expr: &Expr) -> Optio op: Operator::Add | Operator::Mod, .. }) => { - let Some(parent) = checker.semantic_model().expr_parent() else { + let Some(parent) = checker.semantic().expr_parent() else { if any_over_expr(expr, &has_string_literal) { return Some(checker.generator().expr(expr)); } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs b/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs index 6948f6323b..29cfeca780 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs @@ -48,20 +48,19 @@ pub(crate) fn hashlib_insecure_hash_functions( args: &[Expr], keywords: &[Keyword], ) { - if let Some(hashlib_call) = - checker - .semantic_model() - .resolve_call_path(func) - .and_then(|call_path| { - if call_path.as_slice() == ["hashlib", "new"] { - Some(HashlibCall::New) - } else { - WEAK_HASHES - .iter() - .find(|hash| call_path.as_slice() == ["hashlib", hash]) - .map(|hash| HashlibCall::WeakHash(hash)) - } - }) + if let Some(hashlib_call) = checker + .semantic() + .resolve_call_path(func) + .and_then(|call_path| { + if call_path.as_slice() == ["hashlib", "new"] { + Some(HashlibCall::New) + } else { + WEAK_HASHES + .iter() + .find(|hash| call_path.as_slice() == ["hashlib", hash]) + .map(|hash| HashlibCall::WeakHash(hash)) + } + }) { match hashlib_call { HashlibCall::New => { diff --git a/crates/ruff/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs b/crates/ruff/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs index 06a2e77081..2e9fddbac1 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs @@ -37,7 +37,7 @@ pub(crate) fn jinja2_autoescape_false( keywords: &[Keyword], ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["jinja2", "Environment"] diff --git a/crates/ruff/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs b/crates/ruff/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs index 6fc645bb88..009d166786 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs @@ -24,7 +24,7 @@ pub(crate) fn logging_config_insecure_listen( keywords: &[Keyword], ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["logging", "config", "listen"] diff --git a/crates/ruff/src/rules/flake8_bandit/rules/paramiko_calls.rs b/crates/ruff/src/rules/flake8_bandit/rules/paramiko_calls.rs index e340a09ca4..73e2e82b13 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/paramiko_calls.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/paramiko_calls.rs @@ -18,7 +18,7 @@ impl Violation for ParamikoCall { /// S601 pub(crate) fn paramiko_call(checker: &mut Checker, func: &Expr) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["paramiko", "exec_command"] 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 fe45fb6e76..3a11042da2 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 @@ -44,7 +44,7 @@ pub(crate) fn request_with_no_cert_validation( keywords: &[Keyword], ) { if let Some(target) = checker - .semantic_model() + .semantic() .resolve_call_path(func) .and_then(|call_path| { if call_path.len() == 2 { 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 b08edbd4fa..c164dcadc1 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 @@ -34,7 +34,7 @@ pub(crate) fn request_without_timeout( keywords: &[Keyword], ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { HTTP_VERBS diff --git a/crates/ruff/src/rules/flake8_bandit/rules/shell_injection.rs b/crates/ruff/src/rules/flake8_bandit/rules/shell_injection.rs index c5d97d7e0b..ae2e92211a 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/shell_injection.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/shell_injection.rs @@ -102,8 +102,8 @@ pub(crate) fn shell_injection( args: &[Expr], keywords: &[Keyword], ) { - let call_kind = get_call_kind(func, checker.semantic_model()); - let shell_keyword = find_shell_keyword(checker.semantic_model(), keywords); + let call_kind = get_call_kind(func, checker.semantic()); + let shell_keyword = find_shell_keyword(keywords, checker.semantic()); if matches!(call_kind, Some(CallKind::Subprocess)) { if let Some(arg) = args.first() { @@ -227,8 +227,8 @@ enum CallKind { } /// Return the [`CallKind`] of the given function call. -fn get_call_kind(func: &Expr, model: &SemanticModel) -> Option { - model +fn get_call_kind(func: &Expr, semantic: &SemanticModel) -> Option { + semantic .resolve_call_path(func) .and_then(|call_path| match call_path.as_slice() { &[module, submodule] => match module { @@ -269,14 +269,14 @@ struct ShellKeyword<'a> { /// Return the `shell` keyword argument to the given function call, if any. fn find_shell_keyword<'a>( - model: &SemanticModel, keywords: &'a [Keyword], + semantic: &SemanticModel, ) -> Option> { keywords .iter() .find(|keyword| keyword.arg.as_ref().map_or(false, |arg| arg == "shell")) .map(|keyword| ShellKeyword { - truthiness: Truthiness::from_expr(&keyword.value, |id| model.is_builtin(id)), + truthiness: Truthiness::from_expr(&keyword.value, |id| semantic.is_builtin(id)), keyword, }) } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/snmp_insecure_version.rs b/crates/ruff/src/rules/flake8_bandit/rules/snmp_insecure_version.rs index 60fc3b33dd..15541b48c9 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/snmp_insecure_version.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/snmp_insecure_version.rs @@ -25,7 +25,7 @@ pub(crate) fn snmp_insecure_version( keywords: &[Keyword], ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["pysnmp", "hlapi", "CommunityData"] diff --git a/crates/ruff/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs b/crates/ruff/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs index 313c32a187..13b490dc33 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs @@ -27,7 +27,7 @@ pub(crate) fn snmp_weak_cryptography( keywords: &[Keyword], ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["pysnmp", "hlapi", "UsmUserData"] 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 6c6eba80fe..f2d86ea4fa 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 @@ -470,7 +470,7 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, expr: &Expr) { return; }; - let Some(reason) = checker.semantic_model().resolve_call_path(func).and_then(|call_path| { + let Some(reason) = checker.semantic().resolve_call_path(func).and_then(|call_path| { for module in SUSPICIOUS_MEMBERS { for member in module.members { if call_path.as_slice() == *member { diff --git a/crates/ruff/src/rules/flake8_bandit/rules/try_except_continue.rs b/crates/ruff/src/rules/flake8_bandit/rules/try_except_continue.rs index a64c33bc39..e2ce1f3cfa 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/try_except_continue.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/try_except_continue.rs @@ -27,7 +27,7 @@ pub(crate) fn try_except_continue( ) { if body.len() == 1 && body[0].is_continue_stmt() - && (check_typed_exception || is_untyped_exception(type_, checker.semantic_model())) + && (check_typed_exception || is_untyped_exception(type_, checker.semantic())) { checker .diagnostics diff --git a/crates/ruff/src/rules/flake8_bandit/rules/try_except_pass.rs b/crates/ruff/src/rules/flake8_bandit/rules/try_except_pass.rs index c740399349..a89c41b240 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/try_except_pass.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/try_except_pass.rs @@ -27,7 +27,7 @@ pub(crate) fn try_except_pass( ) { if body.len() == 1 && body[0].is_pass_stmt() - && (check_typed_exception || is_untyped_exception(type_, checker.semantic_model())) + && (check_typed_exception || is_untyped_exception(type_, checker.semantic())) { checker .diagnostics diff --git a/crates/ruff/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs b/crates/ruff/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs index e5225eb0d4..f72a68ab4d 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs @@ -38,14 +38,14 @@ pub(crate) fn unsafe_yaml_load( keywords: &[Keyword], ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| call_path.as_slice() == ["yaml", "load"]) { let call_args = SimpleCallArgs::new(args, keywords); if let Some(loader_arg) = call_args.argument("Loader", 1) { if !checker - .semantic_model() + .semantic() .resolve_call_path(loader_arg) .map_or(false, |call_path| { call_path.as_slice() == ["yaml", "SafeLoader"] diff --git a/crates/ruff/src/rules/flake8_blind_except/rules/blind_except.rs b/crates/ruff/src/rules/flake8_blind_except/rules/blind_except.rs index ef4f52524e..843a7b1c31 100644 --- a/crates/ruff/src/rules/flake8_blind_except/rules/blind_except.rs +++ b/crates/ruff/src/rules/flake8_blind_except/rules/blind_except.rs @@ -34,7 +34,7 @@ pub(crate) fn blind_except( return; }; for exception in ["BaseException", "Exception"] { - if id == exception && checker.semantic_model().is_builtin(exception) { + if id == exception && checker.semantic().is_builtin(exception) { // If the exception is re-raised, don't flag an error. if body.iter().any(|stmt| { if let Stmt::Raise(ast::StmtRaise { exc, .. }) = stmt { @@ -58,7 +58,7 @@ pub(crate) fn blind_except( if body.iter().any(|stmt| { if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt { if let Expr::Call(ast::ExprCall { func, keywords, .. }) = value.as_ref() { - if logging::is_logger_candidate(func, checker.semantic_model()) { + if logging::is_logger_candidate(func, checker.semantic()) { if let Some(attribute) = func.as_attribute_expr() { let attr = attribute.attr.as_str(); if attr == "exception" { 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 f8bd621178..6a4b303ec7 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 @@ -36,16 +36,16 @@ impl Violation for EmptyMethodWithoutAbstractDecorator { } } -fn is_abc_class(model: &SemanticModel, bases: &[Expr], keywords: &[Keyword]) -> bool { +fn is_abc_class(bases: &[Expr], keywords: &[Keyword], semantic: &SemanticModel) -> bool { keywords.iter().any(|keyword| { keyword.arg.as_ref().map_or(false, |arg| arg == "metaclass") - && model + && semantic .resolve_call_path(&keyword.value) .map_or(false, |call_path| { call_path.as_slice() == ["abc", "ABCMeta"] }) }) || bases.iter().any(|base| { - model + semantic .resolve_call_path(base) .map_or(false, |call_path| call_path.as_slice() == ["abc", "ABC"]) }) @@ -80,7 +80,7 @@ pub(crate) fn abstract_base_class( if bases.len() + keywords.len() != 1 { return; } - if !is_abc_class(checker.semantic_model(), bases, keywords) { + if !is_abc_class(bases, keywords, checker.semantic()) { return; } @@ -109,7 +109,7 @@ pub(crate) fn abstract_base_class( continue; }; - let has_abstract_decorator = is_abstract(checker.semantic_model(), decorator_list); + let has_abstract_decorator = is_abstract(decorator_list, checker.semantic()); has_abstract_method |= has_abstract_decorator; if !checker.enabled(Rule::EmptyMethodWithoutAbstractDecorator) { @@ -118,7 +118,7 @@ pub(crate) fn abstract_base_class( if !has_abstract_decorator && is_empty_body(body) - && !is_overload(checker.semantic_model(), decorator_list) + && !is_overload(decorator_list, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( EmptyMethodWithoutAbstractDecorator { 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 be7a22f54e..e629112224 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 @@ -66,7 +66,7 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: } if !checker - .semantic_model() + .semantic() .resolve_call_path(args.first().unwrap()) .map_or(false, |call_path| call_path.as_slice() == ["", "Exception"]) { @@ -78,7 +78,7 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: { AssertionKind::AssertRaises } else if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["pytest", "raises"] diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs b/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs index ff5d889ace..ddfba0d174 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs @@ -18,8 +18,8 @@ impl Violation for CachedInstanceMethod { } } -fn is_cache_func(model: &SemanticModel, expr: &Expr) -> bool { - model.resolve_call_path(expr).map_or(false, |call_path| { +fn is_cache_func(expr: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(expr).map_or(false, |call_path| { call_path.as_slice() == ["functools", "lru_cache"] || call_path.as_slice() == ["functools", "cache"] }) @@ -27,7 +27,7 @@ fn is_cache_func(model: &SemanticModel, expr: &Expr) -> bool { /// B019 pub(crate) fn cached_instance_method(checker: &mut Checker, decorator_list: &[Decorator]) { - if !checker.semantic_model().scope().kind.is_class() { + if !checker.semantic().scope().kind.is_class() { return; } for decorator in decorator_list { @@ -41,11 +41,11 @@ pub(crate) fn cached_instance_method(checker: &mut Checker, decorator_list: &[De } for decorator in decorator_list { if is_cache_func( - checker.semantic_model(), match &decorator.expression { Expr::Call(ast::ExprCall { func, .. }) => func, _ => &decorator.expression, }, + checker.semantic(), ) { checker .diagnostics diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs b/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs index bf2cf7b7a5..af95ef699e 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs @@ -73,7 +73,7 @@ impl Violation for FunctionCallInDefaultArgument { } struct ArgumentDefaultVisitor<'a> { - model: &'a SemanticModel<'a>, + semantic: &'a SemanticModel<'a>, extend_immutable_calls: Vec>, diagnostics: Vec<(DiagnosticKind, TextRange)>, } @@ -81,7 +81,7 @@ struct ArgumentDefaultVisitor<'a> { impl<'a> ArgumentDefaultVisitor<'a> { fn new(model: &'a SemanticModel<'a>, extend_immutable_calls: Vec>) -> Self { Self { - model, + semantic: model, extend_immutable_calls, diagnostics: Vec::new(), } @@ -95,8 +95,8 @@ where fn visit_expr(&mut self, expr: &'b Expr) { match expr { Expr::Call(ast::ExprCall { func, .. }) => { - if !is_mutable_func(self.model, func) - && !is_immutable_func(self.model, func, &self.extend_immutable_calls) + if !is_mutable_func(func, self.semantic) + && !is_immutable_func(func, self.semantic, &self.extend_immutable_calls) { self.diagnostics.push(( FunctionCallInDefaultArgument { @@ -125,8 +125,7 @@ pub(crate) fn function_call_argument_default(checker: &mut Checker, arguments: & .map(|target| from_qualified_name(target)) .collect(); let diagnostics = { - let mut visitor = - ArgumentDefaultVisitor::new(checker.semantic_model(), extend_immutable_calls); + let mut visitor = ArgumentDefaultVisitor::new(checker.semantic(), extend_immutable_calls); for expr in arguments .defaults .iter() 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 b4fb273aa8..064d1516b4 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 @@ -26,15 +26,15 @@ const MUTABLE_FUNCS: &[&[&str]] = &[ &["collections", "deque"], ]; -pub(crate) fn is_mutable_func(model: &SemanticModel, func: &Expr) -> bool { - model.resolve_call_path(func).map_or(false, |call_path| { +pub(crate) fn is_mutable_func(func: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(func).map_or(false, |call_path| { MUTABLE_FUNCS .iter() .any(|target| call_path.as_slice() == *target) }) } -fn is_mutable_expr(model: &SemanticModel, expr: &Expr) -> bool { +fn is_mutable_expr(expr: &Expr, semantic: &SemanticModel) -> bool { match expr { Expr::List(_) | Expr::Dict(_) @@ -42,7 +42,7 @@ fn is_mutable_expr(model: &SemanticModel, expr: &Expr) -> bool { | Expr::ListComp(_) | Expr::DictComp(_) | Expr::SetComp(_) => true, - Expr::Call(ast::ExprCall { func, .. }) => is_mutable_func(model, func), + Expr::Call(ast::ExprCall { func, .. }) => is_mutable_func(func, semantic), _ => false, } } @@ -64,9 +64,9 @@ pub(crate) fn mutable_argument_default(checker: &mut Checker, arguments: &Argume .zip(arguments.defaults.iter().rev()), ) { - if is_mutable_expr(checker.semantic_model(), default) + if is_mutable_expr(default, checker.semantic()) && !arg.annotation.as_ref().map_or(false, |expr| { - is_immutable_annotation(checker.semantic_model(), expr) + is_immutable_annotation(expr, checker.semantic()) }) { checker diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs b/crates/ruff/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs index a7d20c2801..0668fc9f39 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs @@ -45,7 +45,7 @@ pub(crate) fn no_explicit_stacklevel( keywords: &[Keyword], ) { if !checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["warnings", "warn"] diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs b/crates/ruff/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs index 618af80195..6e753163ec 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs @@ -342,7 +342,7 @@ pub(crate) fn reuse_of_groupby_generator( }; // Check if the function call is `itertools.groupby` if !checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["itertools", "groupby"] 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 9229339bf3..4866a9ae7c 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 @@ -75,7 +75,7 @@ pub(crate) fn setattr_with_constant( if let Stmt::Expr(ast::StmtExpr { value: child, range: _, - }) = checker.semantic_model().stmt() + }) = checker.semantic().stmt() { if expr == child.as_ref() { let mut diagnostic = Diagnostic::new(SetAttrWithConstant, expr.range()); diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs b/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs index 380628fc25..19432aceed 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs @@ -20,7 +20,6 @@ use rustc_hash::FxHashMap; use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; -use serde::{Deserialize, Serialize}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -30,7 +29,7 @@ use ruff_python_ast::{helpers, visitor}; use crate::checkers::ast::Checker; use crate::registry::AsRule; -#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, result_like::BoolLike)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, result_like::BoolLike)] enum Certainty { Certain, Uncertain, @@ -129,7 +128,7 @@ pub(crate) fn unused_loop_control_variable(checker: &mut Checker, target: &Expr, // Avoid fixing any variables that _may_ be used, but undetectably so. let certainty = Certainty::from(!helpers::uses_magic_variable_access(body, |id| { - checker.semantic_model().is_builtin(id) + checker.semantic().is_builtin(id) })); // Attempt to rename the variable by prepending an underscore, but avoid @@ -154,10 +153,10 @@ pub(crate) fn unused_loop_control_variable(checker: &mut Checker, target: &Expr, if certainty.into() && checker.patch(diagnostic.kind.rule()) { // Avoid fixing if the variable, or any future bindings to the variable, are // used _after_ the loop. - let scope = checker.semantic_model().scope(); + let scope = checker.semantic().scope(); if scope .get_all(name) - .map(|binding_id| checker.semantic_model().binding(binding_id)) + .map(|binding_id| checker.semantic().binding(binding_id)) .all(|binding| !binding.is_used()) { diagnostic.set_fix(Fix::suggested(Edit::range_replacement( diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs b/crates/ruff/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs index 456d47af04..c066a74242 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs @@ -27,7 +27,7 @@ pub(crate) fn useless_contextlib_suppress( ) { if args.is_empty() && checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["contextlib", "suppress"] diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/useless_expression.rs b/crates/ruff/src/rules/flake8_bugbear/rules/useless_expression.rs index 00376dc8b0..9d5f1083d2 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/useless_expression.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/useless_expression.rs @@ -53,7 +53,7 @@ pub(crate) fn useless_expression(checker: &mut Checker, value: &Expr) { } // Ignore statements that have side effects. - if contains_effect(value, |id| checker.semantic_model().is_builtin(id)) { + if contains_effect(value, |id| checker.semantic().is_builtin(id)) { // Flag attributes as useless expressions, even if they're attached to calls or other // expressions. if matches!(value, Expr::Attribute(_)) { 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 be3570d8fd..3a605071b5 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 @@ -27,13 +27,13 @@ pub(crate) fn zip_without_explicit_strict( ) { if let Expr::Name(ast::ExprName { id, .. }) = func { if id == "zip" - && checker.semantic_model().is_builtin("zip") + && checker.semantic().is_builtin("zip") && !kwargs .iter() .any(|keyword| keyword.arg.as_ref().map_or(false, |name| name == "strict")) && !args .iter() - .any(|arg| is_infinite_iterator(arg, checker.semantic_model())) + .any(|arg| is_infinite_iterator(arg, checker.semantic())) { checker .diagnostics @@ -44,14 +44,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, model: &SemanticModel) -> bool { +fn is_infinite_iterator(arg: &Expr, semantic: &SemanticModel) -> bool { let Expr::Call(ast::ExprCall { func, args, keywords, .. }) = &arg else { return false; }; - return model - .resolve_call_path(func) - .map_or(false, |call_path| match call_path.as_slice() { + semantic.resolve_call_path(func).map_or(false, |call_path| { + match call_path.as_slice() { ["itertools", "cycle" | "count"] => true, ["itertools", "repeat"] => { // Ex) `itertools.repeat(1)` @@ -76,5 +75,6 @@ fn is_infinite_iterator(arg: &Expr, model: &SemanticModel) -> bool { false } _ => false, - }); + } + }) } diff --git a/crates/ruff/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs b/crates/ruff/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs index ec7e9fb4e6..8393ba766a 100644 --- a/crates/ruff/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs +++ b/crates/ruff/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs @@ -72,11 +72,11 @@ pub(crate) fn builtin_attribute_shadowing( if shadows_builtin(name, &checker.settings.flake8_builtins.builtins_ignorelist) { // Ignore shadowing within `TypedDict` definitions, since these are only accessible through // subscripting and not through attribute access. - if class_def.bases.iter().any(|base| { - checker - .semantic_model() - .match_typing_expr(base, "TypedDict") - }) { + if class_def + .bases + .iter() + .any(|base| checker.semantic().match_typing_expr(base, "TypedDict")) + { return; } diff --git a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs index ed2ba3ada3..b32379677b 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs @@ -502,7 +502,7 @@ pub(crate) fn fix_unnecessary_collection_call( /// this method will pad the start and end of an expression as needed to /// avoid producing invalid syntax. fn pad_expression(content: String, range: TextRange, checker: &Checker) -> String { - if !checker.semantic_model().in_f_string() { + if !checker.semantic().in_f_string() { return content; } diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_call_around_sorted.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_call_around_sorted.rs index 80be40cf4f..e8770e8ac3 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_call_around_sorted.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_call_around_sorted.rs @@ -75,7 +75,7 @@ pub(crate) fn unnecessary_call_around_sorted( if inner != "sorted" { return; } - if !checker.semantic_model().is_builtin(inner) || !checker.semantic_model().is_builtin(outer) { + if !checker.semantic().is_builtin(inner) || !checker.semantic().is_builtin(outer) { return; } let mut diagnostic = Diagnostic::new( diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_collection_call.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_collection_call.rs index 23ca342702..6a51574d45 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_collection_call.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_collection_call.rs @@ -79,7 +79,7 @@ pub(crate) fn unnecessary_collection_call( } _ => return, }; - if !checker.semantic_model().is_builtin(id) { + if !checker.semantic().is_builtin(id) { return; } let mut diagnostic = Diagnostic::new( diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs index 498e86edc4..56de6768ce 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs @@ -56,7 +56,7 @@ fn add_diagnostic(checker: &mut Checker, expr: &Expr) { Expr::DictComp(_) => "dict", _ => return, }; - if !checker.semantic_model().is_builtin(id) { + if !checker.semantic().is_builtin(id) { return; } let mut diagnostic = Diagnostic::new( 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 c3aac344dc..6bcdb7864e 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 @@ -76,7 +76,7 @@ pub(crate) fn unnecessary_comprehension_any_all( if is_async_generator(elt) { return; } - if !checker.semantic_model().is_builtin(id) { + if !checker.semantic().is_builtin(id) { return; } let mut diagnostic = Diagnostic::new(UnnecessaryComprehensionAnyAll, args[0].range()); 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 96cec22cf0..1ea1ae62fc 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 @@ -90,7 +90,7 @@ pub(crate) fn unnecessary_double_cast_or_process( let Some(inner) = helpers::expr_name(func) else { return; }; - if !checker.semantic_model().is_builtin(inner) || !checker.semantic_model().is_builtin(outer) { + if !checker.semantic().is_builtin(inner) || !checker.semantic().is_builtin(outer) { return; } 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 4cdd85512d..d5f9cea172 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 @@ -52,7 +52,7 @@ pub(crate) fn unnecessary_generator_list( let Some(argument) = helpers::exactly_one_argument_with_matching_function("list", func, args, keywords) else { return; }; - if !checker.semantic_model().is_builtin("list") { + if !checker.semantic().is_builtin("list") { return; } if let Expr::GeneratorExp(_) = argument { 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 345a6c0f91..65af9cb79d 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 @@ -52,7 +52,7 @@ pub(crate) fn unnecessary_generator_set( let Some(argument) = helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) else { return; }; - if !checker.semantic_model().is_builtin("set") { + if !checker.semantic().is_builtin("set") { return; } if let Expr::GeneratorExp(_) = argument { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs index 597253875b..45ef0f4ae0 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs @@ -48,7 +48,7 @@ pub(crate) fn unnecessary_list_call( let Some(argument) = helpers::first_argument_with_matching_function("list", func, args) else { return; }; - if !checker.semantic_model().is_builtin("list") { + if !checker.semantic().is_builtin("list") { return; } if !argument.is_list_comp_expr() { 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 e30434bbe3..e8cc9b954f 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 @@ -50,7 +50,7 @@ pub(crate) fn unnecessary_list_comprehension_dict( let Some(argument) = helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) else { return; }; - if !checker.semantic_model().is_builtin("dict") { + if !checker.semantic().is_builtin("dict") { return; } let Expr::ListComp(ast::ExprListComp { elt, .. }) = argument else { 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 8506e71249..1dc3618654 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 @@ -50,7 +50,7 @@ pub(crate) fn unnecessary_list_comprehension_set( let Some(argument) = helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) else { return; }; - if !checker.semantic_model().is_builtin("set") { + if !checker.semantic().is_builtin("set") { return; } if argument.is_list_comp_expr() { 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 370bedf38f..41e4866233 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 @@ -57,7 +57,7 @@ pub(crate) fn unnecessary_literal_dict( let Some(argument) = helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) else { return; }; - if !checker.semantic_model().is_builtin("dict") { + if !checker.semantic().is_builtin("dict") { return; } let (kind, elts) = match argument { 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 9519109ac9..6f3cf56fb3 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 @@ -58,7 +58,7 @@ pub(crate) fn unnecessary_literal_set( let Some(argument) = helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) else { return; }; - if !checker.semantic_model().is_builtin("set") { + if !checker.semantic().is_builtin("set") { return; } let kind = match argument { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_dict_call.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_dict_call.rs index cc9ba2d5ce..119f53330b 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_dict_call.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_dict_call.rs @@ -76,7 +76,7 @@ pub(crate) fn unnecessary_literal_within_dict_call( let Some(argument) = helpers::first_argument_with_matching_function("dict", func, args) else { return; }; - if !checker.semantic_model().is_builtin("dict") { + if !checker.semantic().is_builtin("dict") { return; } let argument_kind = match argument { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_list_call.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_list_call.rs index d1c240d55f..febd2ce931 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_list_call.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_list_call.rs @@ -79,7 +79,7 @@ pub(crate) fn unnecessary_literal_within_list_call( let Some(argument) = helpers::first_argument_with_matching_function("list", func, args) else { return; }; - if !checker.semantic_model().is_builtin("list") { + if !checker.semantic().is_builtin("list") { return; } let argument_kind = match argument { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs index cd61c1943f..187e1ae3e8 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs @@ -80,7 +80,7 @@ pub(crate) fn unnecessary_literal_within_tuple_call( let Some(argument) = helpers::first_argument_with_matching_function("tuple", func, args) else { return; }; - if !checker.semantic_model().is_builtin("tuple") { + if !checker.semantic().is_builtin("tuple") { return; } let argument_kind = match argument { 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 086a3137a4..8a54343bbf 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs @@ -88,7 +88,7 @@ pub(crate) fn unnecessary_map( }; match id { "map" => { - if !checker.semantic_model().is_builtin(id) { + if !checker.semantic().is_builtin(id) { return; } @@ -119,7 +119,7 @@ pub(crate) fn unnecessary_map( } } "list" | "set" => { - if !checker.semantic_model().is_builtin(id) { + if !checker.semantic().is_builtin(id) { return; } @@ -149,7 +149,7 @@ pub(crate) fn unnecessary_map( } } "dict" => { - if !checker.semantic_model().is_builtin(id) { + if !checker.semantic().is_builtin(id) { return; } 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 8659025978..fc9e678e44 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 @@ -58,7 +58,7 @@ pub(crate) fn unnecessary_subscript_reversal( if !(id == "set" || id == "sorted" || id == "reversed") { return; } - if !checker.semantic_model().is_builtin(id) { + if !checker.semantic().is_builtin(id) { return; } let Expr::Subscript(ast::ExprSubscript { slice, .. }) = first_arg else { diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs index 3029801216..66a6422200 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs @@ -27,7 +27,7 @@ impl Violation for CallDateFromtimestamp { /// Use `datetime.datetime.fromtimestamp(, tz=).date()` instead. pub(crate) fn call_date_fromtimestamp(checker: &mut Checker, func: &Expr, location: TextRange) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["datetime", "date", "fromtimestamp"] diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_date_today.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_date_today.rs index 461fa06761..d159710b99 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_date_today.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_date_today.rs @@ -27,7 +27,7 @@ impl Violation for CallDateToday { /// Use `datetime.datetime.now(tz=).date()` instead. pub(crate) fn call_date_today(checker: &mut Checker, func: &Expr, location: TextRange) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["datetime", "date", "today"] diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs index 4ff08ba885..f75dde8d45 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs @@ -28,7 +28,7 @@ pub(crate) fn call_datetime_fromtimestamp( location: TextRange, ) { if !checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["datetime", "datetime", "fromtimestamp"] diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs index 46202fb6a2..15b930d6d2 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs @@ -26,7 +26,7 @@ pub(crate) fn call_datetime_now_without_tzinfo( location: TextRange, ) { if !checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["datetime", "datetime", "now"] 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 2cc4fb0b99..5432d11563 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 @@ -28,7 +28,7 @@ pub(crate) fn call_datetime_strptime_without_zone( location: TextRange, ) { if !checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["datetime", "datetime", "strptime"] @@ -49,7 +49,7 @@ pub(crate) fn call_datetime_strptime_without_zone( } }; - let (Some(grandparent), Some(parent)) = (checker.semantic_model().expr_grandparent(), checker.semantic_model().expr_parent()) else { + let (Some(grandparent), Some(parent)) = (checker.semantic().expr_grandparent(), checker.semantic().expr_parent()) else { checker.diagnostics.push(Diagnostic::new( CallDatetimeStrptimeWithoutZone, location, diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_today.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_today.rs index 5c42c7c176..e566d38455 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_today.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_today.rs @@ -27,7 +27,7 @@ impl Violation for CallDatetimeToday { /// Use `datetime.datetime.now(tz=)` instead. pub(crate) fn call_datetime_today(checker: &mut Checker, func: &Expr, location: TextRange) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["datetime", "datetime", "today"] diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs index ffa73e5416..db7530cc42 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs @@ -34,7 +34,7 @@ pub(crate) fn call_datetime_utcfromtimestamp( location: TextRange, ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["datetime", "datetime", "utcfromtimestamp"] diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs index 33d89f8673..8444243061 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs @@ -29,7 +29,7 @@ impl Violation for CallDatetimeUtcnow { /// current time in UTC is by calling `datetime.now(timezone.utc)`. pub(crate) fn call_datetime_utcnow(checker: &mut Checker, func: &Expr, location: TextRange) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["datetime", "datetime", "utcnow"] diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs index af7eabf837..e37b47946d 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs @@ -25,7 +25,7 @@ pub(crate) fn call_datetime_without_tzinfo( location: TextRange, ) { if !checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["datetime", "datetime"] diff --git a/crates/ruff/src/rules/flake8_debugger/rules/debugger.rs b/crates/ruff/src/rules/flake8_debugger/rules/debugger.rs index 0dfdc9ce83..e0552c1bcd 100644 --- a/crates/ruff/src/rules/flake8_debugger/rules/debugger.rs +++ b/crates/ruff/src/rules/flake8_debugger/rules/debugger.rs @@ -44,7 +44,7 @@ const DEBUGGERS: &[&[&str]] = &[ /// Checks for the presence of a debugger call. pub(crate) fn debugger_call(checker: &mut Checker, expr: &Expr, func: &Expr) { if let Some(target) = checker - .semantic_model() + .semantic() .resolve_call_path(func) .and_then(|call_path| { DEBUGGERS diff --git a/crates/ruff/src/rules/flake8_django/rules/all_with_model_form.rs b/crates/ruff/src/rules/flake8_django/rules/all_with_model_form.rs index da019f071a..3a3769bda8 100644 --- a/crates/ruff/src/rules/flake8_django/rules/all_with_model_form.rs +++ b/crates/ruff/src/rules/flake8_django/rules/all_with_model_form.rs @@ -54,7 +54,7 @@ pub(crate) fn all_with_model_form( ) -> Option { if !bases .iter() - .any(|base| is_model_form(checker.semantic_model(), base)) + .any(|base| is_model_form(base, checker.semantic())) { return None; } diff --git a/crates/ruff/src/rules/flake8_django/rules/exclude_with_model_form.rs b/crates/ruff/src/rules/flake8_django/rules/exclude_with_model_form.rs index af229a6a32..c920525a19 100644 --- a/crates/ruff/src/rules/flake8_django/rules/exclude_with_model_form.rs +++ b/crates/ruff/src/rules/flake8_django/rules/exclude_with_model_form.rs @@ -52,7 +52,7 @@ pub(crate) fn exclude_with_model_form( ) -> Option { if !bases .iter() - .any(|base| is_model_form(checker.semantic_model(), base)) + .any(|base| is_model_form(base, checker.semantic())) { return None; } diff --git a/crates/ruff/src/rules/flake8_django/rules/helpers.rs b/crates/ruff/src/rules/flake8_django/rules/helpers.rs index b12419a808..5c45066a02 100644 --- a/crates/ruff/src/rules/flake8_django/rules/helpers.rs +++ b/crates/ruff/src/rules/flake8_django/rules/helpers.rs @@ -3,23 +3,23 @@ use rustpython_parser::ast::Expr; use ruff_python_semantic::SemanticModel; /// Return `true` if a Python class appears to be a Django model, based on its base classes. -pub(super) fn is_model(model: &SemanticModel, base: &Expr) -> bool { - model.resolve_call_path(base).map_or(false, |call_path| { +pub(super) fn is_model(base: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(base).map_or(false, |call_path| { call_path.as_slice() == ["django", "db", "models", "Model"] }) } /// Return `true` if a Python class appears to be a Django model form, based on its base classes. -pub(super) fn is_model_form(model: &SemanticModel, base: &Expr) -> bool { - model.resolve_call_path(base).map_or(false, |call_path| { +pub(super) fn is_model_form(base: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(base).map_or(false, |call_path| { call_path.as_slice() == ["django", "forms", "ModelForm"] || call_path.as_slice() == ["django", "forms", "models", "ModelForm"] }) } /// Return `true` if the expression is constructor for a Django model field. -pub(super) fn is_model_field(model: &SemanticModel, expr: &Expr) -> bool { - model.resolve_call_path(expr).map_or(false, |call_path| { +pub(super) fn is_model_field(expr: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(expr).map_or(false, |call_path| { call_path .as_slice() .starts_with(&["django", "db", "models"]) @@ -28,10 +28,10 @@ pub(super) fn is_model_field(model: &SemanticModel, expr: &Expr) -> bool { /// Return the name of the field type, if the expression is constructor for a Django model field. pub(super) fn get_model_field_name<'a>( - model: &'a SemanticModel, expr: &'a Expr, + semantic: &'a SemanticModel, ) -> Option<&'a str> { - model.resolve_call_path(expr).and_then(|call_path| { + semantic.resolve_call_path(expr).and_then(|call_path| { let call_path = call_path.as_slice(); if !call_path.starts_with(&["django", "db", "models"]) { return None; 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 0779e7462b..acd7074222 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 @@ -51,7 +51,7 @@ pub(crate) fn locals_in_render_function( keywords: &[Keyword], ) { if !checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["django", "shortcuts", "render"] @@ -61,7 +61,7 @@ pub(crate) fn locals_in_render_function( } let locals = if args.len() >= 3 { - if !is_locals_call(checker.semantic_model(), &args[2]) { + if !is_locals_call(&args[2], checker.semantic()) { return; } &args[2] @@ -69,7 +69,7 @@ pub(crate) fn locals_in_render_function( .iter() .find(|keyword| keyword.arg.as_ref().map_or(false, |arg| arg == "context")) { - if !is_locals_call(checker.semantic_model(), &keyword.value) { + if !is_locals_call(&keyword.value, checker.semantic()) { return; } &keyword.value @@ -83,11 +83,11 @@ pub(crate) fn locals_in_render_function( )); } -fn is_locals_call(model: &SemanticModel, expr: &Expr) -> bool { +fn is_locals_call(expr: &Expr, semantic: &SemanticModel) -> bool { let Expr::Call(ast::ExprCall { func, .. }) = expr else { return false }; - model + semantic .resolve_call_path(func) .map_or(false, |call_path| 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 586928fa00..c0ca4d99e9 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 @@ -56,7 +56,7 @@ pub(crate) fn model_without_dunder_str( body: &[Stmt], class_location: &Stmt, ) -> Option { - if !checker_applies(checker.semantic_model(), bases, body) { + if !is_non_abstract_model(bases, body, checker.semantic()) { return None; } if !has_dunder_method(body) { @@ -80,12 +80,12 @@ fn has_dunder_method(body: &[Stmt]) -> bool { }) } -fn checker_applies(model: &SemanticModel, bases: &[Expr], body: &[Stmt]) -> bool { +fn is_non_abstract_model(bases: &[Expr], body: &[Stmt], semantic: &SemanticModel) -> bool { for base in bases.iter() { if is_model_abstract(body) { continue; } - if helpers::is_model(model, base) { + if helpers::is_model(base, semantic) { return true; } } 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 d8ccedb65b..35b8d66c14 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 @@ -84,7 +84,7 @@ fn is_nullable_field<'a>(checker: &'a Checker, value: &'a Expr) -> Option<&'a st return None; }; - let Some(valid_field_name) = helpers::get_model_field_name(checker.semantic_model(), func) else { + let Some(valid_field_name) = helpers::get_model_field_name(func, checker.semantic()) else { return None; }; 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 b3b198624c..6404a105f6 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 @@ -100,11 +100,11 @@ impl fmt::Display for ContentType { } } -fn get_element_type(model: &SemanticModel, element: &Stmt) -> Option { +fn get_element_type(element: &Stmt, semantic: &SemanticModel) -> Option { match element { Stmt::Assign(ast::StmtAssign { targets, value, .. }) => { if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() { - if helpers::is_model_field(model, func) { + if helpers::is_model_field(func, semantic) { return Some(ContentType::FieldDeclaration); } } @@ -145,13 +145,13 @@ pub(crate) fn unordered_body_content_in_model( ) { if !bases .iter() - .any(|base| helpers::is_model(checker.semantic_model(), base)) + .any(|base| helpers::is_model(base, checker.semantic())) { return; } let mut elements_type_found = Vec::new(); for element in body.iter() { - let Some(current_element_type) = get_element_type(checker.semantic_model(), element) else { + let Some(current_element_type) = get_element_type(element, checker.semantic()) else { continue; }; let Some(&element_type) = elements_type_found diff --git a/crates/ruff/src/rules/flake8_errmsg/rules/string_in_exception.rs b/crates/ruff/src/rules/flake8_errmsg/rules/string_in_exception.rs index b1aca25362..7cf51caf11 100644 --- a/crates/ruff/src/rules/flake8_errmsg/rules/string_in_exception.rs +++ b/crates/ruff/src/rules/flake8_errmsg/rules/string_in_exception.rs @@ -190,7 +190,7 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr if let Some(indentation) = whitespace::indentation(checker.locator, stmt) { - if checker.semantic_model().is_available("msg") { + if checker.semantic().is_available("msg") { diagnostic.set_fix(generate_fix( stmt, first, @@ -213,7 +213,7 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr if let Some(indentation) = whitespace::indentation(checker.locator, stmt) { - if checker.semantic_model().is_available("msg") { + if checker.semantic().is_available("msg") { diagnostic.set_fix(generate_fix( stmt, first, @@ -240,7 +240,7 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr if let Some(indentation) = whitespace::indentation(checker.locator, stmt) { - if checker.semantic_model().is_available("msg") { + if checker.semantic().is_available("msg") { diagnostic.set_fix(generate_fix( stmt, first, diff --git a/crates/ruff/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs b/crates/ruff/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs index 1594938fec..6a93534241 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs +++ b/crates/ruff/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs @@ -65,7 +65,7 @@ impl Violation for FutureRewritableTypeAnnotation { /// FA100 pub(crate) fn future_rewritable_type_annotation(checker: &mut Checker, expr: &Expr) { let name = checker - .semantic_model() + .semantic() .resolve_call_path(expr) .map(|binding| format_call_path(&binding)); diff --git a/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs b/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs index cd92ae23a7..5dc28bbb3d 100644 --- a/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs +++ b/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs @@ -106,7 +106,7 @@ fn check_log_record_attr_clash(checker: &mut Checker, extra: &Keyword) { } Expr::Call(ast::ExprCall { func, keywords, .. }) => { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| call_path.as_slice() == ["", "dict"]) { @@ -151,7 +151,7 @@ pub(crate) fn logging_call( args: &[Expr], keywords: &[Keyword], ) { - if !logging::is_logger_candidate(func, checker.semantic_model()) { + if !logging::is_logger_candidate(func, checker.semantic()) { return; } @@ -193,10 +193,10 @@ pub(crate) fn logging_call( // G201, G202 if checker.any_enabled(&[Rule::LoggingExcInfo, Rule::LoggingRedundantExcInfo]) { - if !checker.semantic_model().in_exception_handler() { + if !checker.semantic().in_exception_handler() { return; } - let Some(exc_info) = logging::exc_info(keywords, checker.semantic_model()) else { + let Some(exc_info) = logging::exc_info(keywords, checker.semantic()) else { return; }; if let LoggingCallType::LevelCall(logging_level) = logging_call_type { diff --git a/crates/ruff/src/rules/flake8_pie/rules/non_unique_enums.rs b/crates/ruff/src/rules/flake8_pie/rules/non_unique_enums.rs index 47e1a427a5..53697bb340 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/non_unique_enums.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/non_unique_enums.rs @@ -66,7 +66,7 @@ pub(crate) fn non_unique_enums<'a, 'b>( if !bases.iter().any(|expr| { checker - .semantic_model() + .semantic() .resolve_call_path(expr) .map_or(false, |call_path| call_path.as_slice() == ["enum", "Enum"]) }) { @@ -81,7 +81,7 @@ pub(crate) fn non_unique_enums<'a, 'b>( if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| call_path.as_slice() == ["enum", "auto"]) { diff --git a/crates/ruff/src/rules/flake8_pie/rules/reimplemented_list_builtin.rs b/crates/ruff/src/rules/flake8_pie/rules/reimplemented_list_builtin.rs index c7e562a36a..0e53e5febf 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/reimplemented_list_builtin.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/reimplemented_list_builtin.rs @@ -69,7 +69,7 @@ pub(crate) fn reimplemented_list_builtin(checker: &mut Checker, expr: &ExprLambd if elts.is_empty() { let mut diagnostic = Diagnostic::new(ReimplementedListBuiltin, expr.range()); if checker.patch(diagnostic.kind.rule()) { - if checker.semantic_model().is_builtin("list") { + if checker.semantic().is_builtin("list") { diagnostic.set_fix(Fix::automatic(Edit::range_replacement( "list".to_string(), expr.range(), diff --git a/crates/ruff/src/rules/flake8_print/rules/print_call.rs b/crates/ruff/src/rules/flake8_print/rules/print_call.rs index 13993a202c..23b4d95050 100644 --- a/crates/ruff/src/rules/flake8_print/rules/print_call.rs +++ b/crates/ruff/src/rules/flake8_print/rules/print_call.rs @@ -78,7 +78,7 @@ impl Violation for PPrint { /// T201, T203 pub(crate) fn print_call(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) { let diagnostic = { - let call_path = checker.semantic_model().resolve_call_path(func); + let call_path = checker.semantic().resolve_call_path(func); if call_path .as_ref() .map_or(false, |call_path| *call_path.as_slice() == ["", "print"]) @@ -90,14 +90,13 @@ pub(crate) fn print_call(checker: &mut Checker, func: &Expr, keywords: &[Keyword .find(|keyword| keyword.arg.as_ref().map_or(false, |arg| arg == "file")) { if !is_const_none(&keyword.value) { - if checker - .semantic_model() - .resolve_call_path(&keyword.value) - .map_or(true, |call_path| { + if checker.semantic().resolve_call_path(&keyword.value).map_or( + true, + |call_path| { call_path.as_slice() != ["sys", "stdout"] && call_path.as_slice() != ["sys", "stderr"] - }) - { + }, + ) { return; } } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs b/crates/ruff/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs index 03235b31be..0779bb6575 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs @@ -66,14 +66,11 @@ pub(crate) fn any_eq_ne_annotation(checker: &mut Checker, name: &str, args: &Arg return; }; - if !checker.semantic_model().scope().kind.is_class() { + if !checker.semantic().scope().kind.is_class() { return; } - if checker - .semantic_model() - .match_typing_expr(annotation, "Any") - { + if checker.semantic().match_typing_expr(annotation, "Any") { let mut diagnostic = Diagnostic::new( AnyEqNeAnnotation { method_name: name.to_string(), @@ -82,7 +79,7 @@ pub(crate) fn any_eq_ne_annotation(checker: &mut Checker, name: &str, args: &Arg ); if checker.patch(diagnostic.kind.rule()) { // Ex) `def __eq__(self, obj: Any): ...` - if checker.semantic_model().is_builtin("object") { + if checker.semantic().is_builtin("object") { diagnostic.set_fix(Fix::automatic(Edit::range_replacement( "object".to_string(), annotation.range(), 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 8d923d1cfa..a75dd025e3 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 @@ -69,7 +69,7 @@ pub(crate) fn bad_version_info_comparison( }; if !checker - .semantic_model() + .semantic() .resolve_call_path(left) .map_or(false, |call_path| { call_path.as_slice() == ["sys", "version_info"] diff --git a/crates/ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs b/crates/ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs index 7f171b7558..9accde17c3 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs @@ -51,7 +51,7 @@ impl Violation for CollectionsNamedTuple { /// PYI024 pub(crate) fn collections_named_tuple(checker: &mut Checker, expr: &Expr) { if checker - .semantic_model() + .semantic() .resolve_call_path(expr) .map_or(false, |call_path| { matches!(call_path.as_slice(), ["collections", "namedtuple"]) 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 b60b500afb..db3aa58bed 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 @@ -101,7 +101,7 @@ pub(crate) fn iter_method_return_iterable(checker: &mut Checker, definition: &De }; if checker - .semantic_model() + .semantic() .resolve_call_path(annotation) .map_or(false, |call_path| { if async_ { diff --git a/crates/ruff/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs b/crates/ruff/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs index 8fcb45d403..c45275f9db 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs @@ -56,10 +56,7 @@ pub(crate) fn no_return_argument_annotation(checker: &mut Checker, args: &Argume ) .filter_map(|arg| arg.annotation.as_ref()) { - if checker - .semantic_model() - .match_typing_expr(annotation, "NoReturn") - { + if checker.semantic().match_typing_expr(annotation, "NoReturn") { checker.diagnostics.push(Diagnostic::new( NoReturnArgumentAnnotationInStub { module: if checker.settings.target_version >= Py311 { diff --git a/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs b/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs index fdaeb56510..5a3bb60e74 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs @@ -118,7 +118,7 @@ pub(crate) fn non_self_return_type( args: &Arguments, async_: bool, ) { - let ScopeKind::Class(class_def) = checker.semantic_model().scope().kind else { + let ScopeKind::Class(class_def) = checker.semantic().scope().kind else { return; }; @@ -131,8 +131,8 @@ pub(crate) fn non_self_return_type( }; // Skip any abstract or overloaded methods. - if is_abstract(checker.semantic_model(), decorator_list) - || is_overload(checker.semantic_model(), decorator_list) + if is_abstract(decorator_list, checker.semantic()) + || is_overload(decorator_list, checker.semantic()) { return; } @@ -140,7 +140,7 @@ pub(crate) fn non_self_return_type( if async_ { if name == "__aenter__" && is_name(returns, &class_def.name) - && !is_final(checker.semantic_model(), &class_def.decorator_list) + && !is_final(&class_def.decorator_list, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( NonSelfReturnType { @@ -155,7 +155,7 @@ pub(crate) fn non_self_return_type( // In-place methods that are expected to return `Self`. if INPLACE_BINOP_METHODS.contains(&name) { - if !is_self(returns, checker.semantic_model()) { + if !is_self(returns, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( NonSelfReturnType { class_name: class_def.name.to_string(), @@ -169,7 +169,7 @@ pub(crate) fn non_self_return_type( if is_name(returns, &class_def.name) { if matches!(name, "__enter__" | "__new__") - && !is_final(checker.semantic_model(), &class_def.decorator_list) + && !is_final(&class_def.decorator_list, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( NonSelfReturnType { @@ -184,8 +184,8 @@ pub(crate) fn non_self_return_type( match name { "__iter__" => { - if is_iterable(returns, checker.semantic_model()) - && is_iterator(&class_def.bases, checker.semantic_model()) + if is_iterable(returns, checker.semantic()) + && is_iterator(&class_def.bases, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( NonSelfReturnType { @@ -197,8 +197,8 @@ pub(crate) fn non_self_return_type( } } "__aiter__" => { - if is_async_iterable(returns, checker.semantic_model()) - && is_async_iterator(&class_def.bases, checker.semantic_model()) + if is_async_iterable(returns, checker.semantic()) + && is_async_iterator(&class_def.bases, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( NonSelfReturnType { @@ -238,14 +238,14 @@ fn is_name(expr: &Expr, name: &str) -> bool { } /// Return `true` if the given expression resolves to `typing.Self`. -fn is_self(expr: &Expr, model: &SemanticModel) -> bool { - model.match_typing_expr(expr, "Self") +fn is_self(expr: &Expr, semantic: &SemanticModel) -> bool { + semantic.match_typing_expr(expr, "Self") } /// Return `true` if the given class extends `collections.abc.Iterator`. -fn is_iterator(bases: &[Expr], model: &SemanticModel) -> bool { +fn is_iterator(bases: &[Expr], semantic: &SemanticModel) -> bool { bases.iter().any(|expr| { - model + semantic .resolve_call_path(map_subscript(expr)) .map_or(false, |call_path| { matches!( @@ -257,8 +257,8 @@ fn is_iterator(bases: &[Expr], model: &SemanticModel) -> bool { } /// Return `true` if the given expression resolves to `collections.abc.Iterable`. -fn is_iterable(expr: &Expr, model: &SemanticModel) -> bool { - model +fn is_iterable(expr: &Expr, semantic: &SemanticModel) -> bool { + semantic .resolve_call_path(map_subscript(expr)) .map_or(false, |call_path| { matches!( @@ -270,9 +270,9 @@ fn is_iterable(expr: &Expr, model: &SemanticModel) -> bool { } /// Return `true` if the given class extends `collections.abc.AsyncIterator`. -fn is_async_iterator(bases: &[Expr], model: &SemanticModel) -> bool { +fn is_async_iterator(bases: &[Expr], semantic: &SemanticModel) -> bool { bases.iter().any(|expr| { - model + semantic .resolve_call_path(map_subscript(expr)) .map_or(false, |call_path| { matches!( @@ -284,8 +284,8 @@ fn is_async_iterator(bases: &[Expr], model: &SemanticModel) -> bool { } /// Return `true` if the given expression resolves to `collections.abc.AsyncIterable`. -fn is_async_iterable(expr: &Expr, model: &SemanticModel) -> bool { - model +fn is_async_iterable(expr: &Expr, semantic: &SemanticModel) -> bool { + semantic .resolve_call_path(map_subscript(expr)) .map_or(false, |call_path| { matches!( 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 e6c561191e..07f62306b7 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,12 +70,12 @@ 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_model().resolve_call_path(func).and_then(|call_path| { - if checker.semantic_model().match_typing_call_path(&call_path, "ParamSpec") { + 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_model().match_typing_call_path(&call_path, "TypeVar") { + } else if checker.semantic().match_typing_call_path(&call_path, "TypeVar") { Some(VarKind::TypeVar) - } else if checker.semantic_model().match_typing_call_path(&call_path, "TypeVarTuple") { + } else if checker.semantic().match_typing_call_path(&call_path, "TypeVarTuple") { Some(VarKind::TypeVarTuple) } else { None 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 eff56ed300..c7299a7656 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs @@ -123,7 +123,7 @@ fn is_valid_default_value_with_annotation( default: &Expr, allow_container: bool, locator: &Locator, - model: &SemanticModel, + semantic: &SemanticModel, ) -> bool { match default { Expr::Constant(_) => { @@ -136,7 +136,7 @@ fn is_valid_default_value_with_annotation( && elts.len() <= 10 && elts .iter() - .all(|e| is_valid_default_value_with_annotation(e, false, locator, model)); + .all(|e| is_valid_default_value_with_annotation(e, false, locator, semantic)); } Expr::Dict(ast::ExprDict { keys, @@ -147,8 +147,8 @@ fn is_valid_default_value_with_annotation( && keys.len() <= 10 && keys.iter().zip(values).all(|(k, v)| { k.as_ref().map_or(false, |k| { - is_valid_default_value_with_annotation(k, false, locator, model) - }) && is_valid_default_value_with_annotation(v, false, locator, model) + is_valid_default_value_with_annotation(k, false, locator, semantic) + }) && is_valid_default_value_with_annotation(v, false, locator, semantic) }); } Expr::UnaryOp(ast::ExprUnaryOp { @@ -164,12 +164,15 @@ fn is_valid_default_value_with_annotation( }) => return true, // Ex) `-math.inf`, `-math.pi`, etc. Expr::Attribute(_) => { - if model.resolve_call_path(operand).map_or(false, |call_path| { - ALLOWED_MATH_ATTRIBUTES_IN_DEFAULTS.iter().any(|target| { - // reject `-math.nan` - call_path.as_slice() == *target && *target != ["math", "nan"] + if semantic + .resolve_call_path(operand) + .map_or(false, |call_path| { + ALLOWED_MATH_ATTRIBUTES_IN_DEFAULTS.iter().any(|target| { + // reject `-math.nan` + call_path.as_slice() == *target && *target != ["math", "nan"] + }) }) - }) { + { return true; } } @@ -214,12 +217,15 @@ fn is_valid_default_value_with_annotation( } // Ex) `math.inf`, `sys.stdin`, etc. Expr::Attribute(_) => { - if model.resolve_call_path(default).map_or(false, |call_path| { - ALLOWED_MATH_ATTRIBUTES_IN_DEFAULTS - .iter() - .chain(ALLOWED_ATTRIBUTES_IN_DEFAULTS.iter()) - .any(|target| call_path.as_slice() == *target) - }) { + if semantic + .resolve_call_path(default) + .map_or(false, |call_path| { + ALLOWED_MATH_ATTRIBUTES_IN_DEFAULTS + .iter() + .chain(ALLOWED_ATTRIBUTES_IN_DEFAULTS.iter()) + .any(|target| call_path.as_slice() == *target) + }) + { return true; } } @@ -265,11 +271,11 @@ 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(model: &SemanticModel, expr: &Expr) -> bool { +fn is_type_var_like_call(expr: &Expr, semantic: &SemanticModel) -> bool { let Expr::Call(ast::ExprCall { func, .. } )= expr else { return false; }; - model.resolve_call_path(func).map_or(false, |call_path| { + semantic.resolve_call_path(func).map_or(false, |call_path| { matches!( call_path.as_slice(), [ @@ -282,11 +288,11 @@ fn is_type_var_like_call(model: &SemanticModel, expr: &Expr) -> bool { /// Returns `true` if this is a "special" assignment which must have a value (e.g., an assignment to /// `__all__`). -fn is_special_assignment(model: &SemanticModel, target: &Expr) -> bool { +fn is_special_assignment(target: &Expr, semantic: &SemanticModel) -> bool { if let Expr::Name(ast::ExprName { id, .. }) = target { match id.as_str() { - "__all__" => model.scope().kind.is_module(), - "__match_args__" | "__slots__" => model.scope().kind.is_class(), + "__all__" => semantic.scope().kind.is_module(), + "__match_args__" | "__slots__" => semantic.scope().kind.is_class(), _ => false, } } else { @@ -295,9 +301,9 @@ fn is_special_assignment(model: &SemanticModel, target: &Expr) -> bool { } /// Returns `true` if the a class is an enum, based on its base classes. -fn is_enum(model: &SemanticModel, bases: &[Expr]) -> bool { +fn is_enum(bases: &[Expr], semantic: &SemanticModel) -> bool { return bases.iter().any(|expr| { - model.resolve_call_path(expr).map_or(false, |call_path| { + semantic.resolve_call_path(expr).map_or(false, |call_path| { matches!( call_path.as_slice(), [ @@ -323,7 +329,7 @@ pub(crate) fn typed_argument_simple_defaults(checker: &mut Checker, args: &Argum default, true, checker.locator, - checker.semantic_model(), + checker.semantic(), ) { let mut diagnostic = Diagnostic::new(TypedArgumentDefaultInStub, default.range()); @@ -354,7 +360,7 @@ pub(crate) fn typed_argument_simple_defaults(checker: &mut Checker, args: &Argum default, true, checker.locator, - checker.semantic_model(), + checker.semantic(), ) { let mut diagnostic = Diagnostic::new(TypedArgumentDefaultInStub, default.range()); @@ -388,7 +394,7 @@ pub(crate) fn argument_simple_defaults(checker: &mut Checker, args: &Arguments) default, true, checker.locator, - checker.semantic_model(), + checker.semantic(), ) { let mut diagnostic = Diagnostic::new(ArgumentDefaultInStub, default.range()); @@ -419,7 +425,7 @@ pub(crate) fn argument_simple_defaults(checker: &mut Checker, args: &Arguments) default, true, checker.locator, - checker.semantic_model(), + checker.semantic(), ) { let mut diagnostic = Diagnostic::new(ArgumentDefaultInStub, default.range()); @@ -448,21 +454,16 @@ pub(crate) fn assignment_default_in_stub(checker: &mut Checker, targets: &[Expr] if !target.is_name_expr() { return; } - if is_special_assignment(checker.semantic_model(), target) { + if is_special_assignment(target, checker.semantic()) { return; } - if is_type_var_like_call(checker.semantic_model(), value) { + if is_type_var_like_call(value, checker.semantic()) { return; } if is_valid_default_value_without_annotation(value) { return; } - if is_valid_default_value_with_annotation( - value, - true, - checker.locator, - checker.semantic_model(), - ) { + if is_valid_default_value_with_annotation(value, true, checker.locator, checker.semantic()) { return; } @@ -484,23 +485,18 @@ pub(crate) fn annotated_assignment_default_in_stub( annotation: &Expr, ) { if checker - .semantic_model() + .semantic() .match_typing_expr(annotation, "TypeAlias") { return; } - if is_special_assignment(checker.semantic_model(), target) { + if is_special_assignment(target, checker.semantic()) { return; } - if is_type_var_like_call(checker.semantic_model(), value) { + if is_type_var_like_call(value, checker.semantic()) { return; } - if is_valid_default_value_with_annotation( - value, - true, - checker.locator, - checker.semantic_model(), - ) { + if is_valid_default_value_with_annotation(value, true, checker.locator, checker.semantic()) { return; } @@ -527,27 +523,21 @@ pub(crate) fn unannotated_assignment_in_stub( let Expr::Name(ast::ExprName { id, .. }) = target else { return; }; - if is_special_assignment(checker.semantic_model(), target) { + if is_special_assignment(target, checker.semantic()) { return; } - if is_type_var_like_call(checker.semantic_model(), value) { + if is_type_var_like_call(value, checker.semantic()) { return; } if is_valid_default_value_without_annotation(value) { return; } - if !is_valid_default_value_with_annotation( - value, - true, - checker.locator, - checker.semantic_model(), - ) { + if !is_valid_default_value_with_annotation(value, true, checker.locator, checker.semantic()) { return; } - if let ScopeKind::Class(ast::StmtClassDef { bases, .. }) = checker.semantic_model().scope().kind - { - if is_enum(checker.semantic_model(), bases) { + if let ScopeKind::Class(ast::StmtClassDef { bases, .. }) = checker.semantic().scope().kind { + if is_enum(bases, checker.semantic()) { return; } } @@ -569,7 +559,7 @@ pub(crate) fn unassigned_special_variable_in_stub( return; }; - if !is_special_assignment(checker.semantic_model(), target) { + if !is_special_assignment(target, checker.semantic()) { return; } 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 88f204fcc3..0d40ff8f9f 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 @@ -62,7 +62,7 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) { return; } - if !checker.semantic_model().scope().kind.is_class() { + if !checker.semantic().scope().kind.is_class() { return; } @@ -72,12 +72,12 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) { return; } - if is_abstract(checker.semantic_model(), decorator_list) { + if is_abstract(decorator_list, checker.semantic()) { return; } if checker - .semantic_model() + .semantic() .resolve_call_path(returns) .map_or(true, |call_path| { !matches!(call_path.as_slice(), ["" | "builtins", "str"]) @@ -93,8 +93,8 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) { identifier_range(stmt, checker.locator), ); if checker.patch(diagnostic.kind.rule()) { - let stmt = checker.semantic_model().stmt(); - let parent = checker.semantic_model().stmt_parent(); + let stmt = checker.semantic().stmt(); + let parent = checker.semantic().stmt_parent(); let edit = delete_stmt( stmt, parent, @@ -103,7 +103,7 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) { checker.stylist, ); diagnostic.set_fix( - Fix::automatic(edit).isolate(checker.isolation(checker.semantic_model().stmt_parent())), + Fix::automatic(edit).isolate(checker.isolation(checker.semantic().stmt_parent())), ); } checker.diagnostics.push(diagnostic); 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 1efb416a0a..3aa892bb27 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs @@ -103,7 +103,7 @@ pub(crate) fn unrecognized_platform( let diagnostic_unrecognized_platform_check = Diagnostic::new(UnrecognizedPlatformCheck, expr.range()); if !checker - .semantic_model() + .semantic() .resolve_call_path(left) .map_or(false, |call_path| { call_path.as_slice() == ["sys", "platform"] 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 ee2f4f7c10..ef73693881 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs @@ -194,9 +194,9 @@ pub(crate) fn unittest_assertion( if checker.patch(diagnostic.kind.rule()) { // We're converting an expression to a statement, so avoid applying the fix if // the assertion is part of a larger expression. - if checker.semantic_model().stmt().is_expr_stmt() - && checker.semantic_model().expr_parent().is_none() - && !checker.semantic_model().scope().kind.is_lambda() + if checker.semantic().stmt().is_expr_stmt() + && checker.semantic().expr_parent().is_none() + && !checker.semantic().scope().kind.is_lambda() && !has_comments_in(expr.range(), checker.locator) { if let Ok(stmt) = unittest_assert.generate_assert(args, keywords) { @@ -219,7 +219,7 @@ pub(crate) fn unittest_assertion( /// PT015 pub(crate) fn assert_falsy(checker: &mut Checker, stmt: &Stmt, test: &Expr) { - if Truthiness::from_expr(test, |id| checker.semantic_model().is_builtin(id)).is_falsey() { + if Truthiness::from_expr(test, |id| checker.semantic().is_builtin(id)).is_falsey() { checker .diagnostics .push(Diagnostic::new(PytestAssertAlwaysFalse, stmt.range())); diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/fail.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/fail.rs index 5f11d6f2f6..7c840db2a1 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/fail.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/fail.rs @@ -19,7 +19,7 @@ impl Violation for PytestFailWithoutMessage { } pub(crate) fn fail_call(checker: &mut Checker, func: &Expr, args: &[Expr], keywords: &[Keyword]) { - if is_pytest_fail(checker.semantic_model(), func) { + if is_pytest_fail(func, checker.semantic()) { let call_args = SimpleCallArgs::new(args, keywords); // Allow either `pytest.fail(reason="...")` (introduced in pytest 7.0) or 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 6542628bf8..af1250bd87 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs @@ -245,11 +245,11 @@ where } fn get_fixture_decorator<'a>( - model: &SemanticModel, decorators: &'a [Decorator], + semantic: &SemanticModel, ) -> Option<&'a Decorator> { decorators.iter().find(|decorator| { - is_pytest_fixture(model, decorator) || is_pytest_yield_fixture(model, decorator) + is_pytest_fixture(decorator, semantic) || is_pytest_yield_fixture(decorator, semantic) }) } @@ -436,7 +436,7 @@ fn check_test_function_args(checker: &mut Checker, args: &Arguments) { /// PT020 fn check_fixture_decorator_name(checker: &mut Checker, decorator: &Decorator) { - if is_pytest_yield_fixture(checker.semantic_model(), decorator) { + if is_pytest_yield_fixture(decorator, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( PytestDeprecatedYieldFixture, decorator.range(), @@ -504,7 +504,7 @@ pub(crate) fn fixture( decorators: &[Decorator], body: &[Stmt], ) { - let decorator = get_fixture_decorator(checker.semantic_model(), decorators); + let decorator = get_fixture_decorator(decorators, checker.semantic()); if let Some(decorator) = decorator { if checker.enabled(Rule::PytestFixtureIncorrectParenthesesStyle) || checker.enabled(Rule::PytestFixturePositionalArgs) @@ -522,7 +522,7 @@ pub(crate) fn fixture( if (checker.enabled(Rule::PytestMissingFixtureNameUnderscore) || checker.enabled(Rule::PytestIncorrectFixtureNameUnderscore) || checker.enabled(Rule::PytestUselessYieldFixture)) - && !is_abstract(checker.semantic_model(), decorators) + && !is_abstract(decorators, checker.semantic()) { check_fixture_returns(checker, stmt, name, body); } diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/helpers.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/helpers.rs index 826dbfd530..5c2a3c1449 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/helpers.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/helpers.rs @@ -20,41 +20,41 @@ pub(super) fn get_mark_decorators( }) } -pub(super) fn is_pytest_fail(model: &SemanticModel, call: &Expr) -> bool { - model.resolve_call_path(call).map_or(false, |call_path| { +pub(super) fn is_pytest_fail(call: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(call).map_or(false, |call_path| { call_path.as_slice() == ["pytest", "fail"] }) } -pub(super) fn is_pytest_fixture(model: &SemanticModel, decorator: &Decorator) -> bool { - model +pub(super) fn is_pytest_fixture(decorator: &Decorator, semantic: &SemanticModel) -> bool { + semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { call_path.as_slice() == ["pytest", "fixture"] }) } -pub(super) fn is_pytest_yield_fixture(model: &SemanticModel, decorator: &Decorator) -> bool { - model +pub(super) fn is_pytest_yield_fixture(decorator: &Decorator, semantic: &SemanticModel) -> bool { + semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { call_path.as_slice() == ["pytest", "yield_fixture"] }) } -pub(super) fn is_pytest_parametrize(model: &SemanticModel, decorator: &Decorator) -> bool { - model +pub(super) fn is_pytest_parametrize(decorator: &Decorator, semantic: &SemanticModel) -> bool { + semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { call_path.as_slice() == ["pytest", "mark", "parametrize"] }) } -pub(super) fn keyword_is_literal(kw: &Keyword, literal: &str) -> bool { +pub(super) fn keyword_is_literal(keyword: &Keyword, literal: &str) -> bool { if let Expr::Constant(ast::ExprConstant { value: Constant::Str(string), .. - }) = &kw.value + }) = &keyword.value { string == literal } else { diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/parametrize.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/parametrize.rs index a0a3b2e75b..8db8d75c23 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/parametrize.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/parametrize.rs @@ -419,7 +419,7 @@ fn handle_value_rows( pub(crate) fn parametrize(checker: &mut Checker, decorators: &[Decorator]) { for decorator in decorators { - if is_pytest_parametrize(checker.semantic_model(), decorator) { + if is_pytest_parametrize(decorator, checker.semantic()) { if let Expr::Call(ast::ExprCall { args, .. }) = &decorator.expression { if checker.enabled(Rule::PytestParametrizeNamesWrongType) { if let Some(names) = args.get(0) { diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs index e2c25bf154..9e1b62a4b8 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs @@ -47,8 +47,8 @@ impl Violation for PytestRaisesWithoutException { } } -fn is_pytest_raises(func: &Expr, model: &SemanticModel) -> bool { - model.resolve_call_path(func).map_or(false, |call_path| { +fn is_pytest_raises(func: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(func).map_or(false, |call_path| { call_path.as_slice() == ["pytest", "raises"] }) } @@ -64,7 +64,7 @@ const fn is_non_trivial_with_body(body: &[Stmt]) -> bool { } pub(crate) fn raises_call(checker: &mut Checker, func: &Expr, args: &[Expr], keywords: &[Keyword]) { - if is_pytest_raises(func, checker.semantic_model()) { + if is_pytest_raises(func, checker.semantic()) { if checker.enabled(Rule::PytestRaisesWithoutException) { if args.is_empty() && keywords.is_empty() { checker @@ -100,7 +100,7 @@ pub(crate) fn complex_raises( let mut is_too_complex = false; let raises_called = items.iter().any(|item| match &item.context_expr { - Expr::Call(ast::ExprCall { func, .. }) => is_pytest_raises(func, checker.semantic_model()), + Expr::Call(ast::ExprCall { func, .. }) => is_pytest_raises(func, checker.semantic()), _ => false, }); @@ -141,7 +141,7 @@ pub(crate) fn complex_raises( /// PT011 fn exception_needs_match(checker: &mut Checker, exception: &Expr) { if let Some(call_path) = checker - .semantic_model() + .semantic() .resolve_call_path(exception) .and_then(|call_path| { let is_broad_exception = checker diff --git a/crates/ruff/src/rules/flake8_return/rules/function.rs b/crates/ruff/src/rules/flake8_return/rules/function.rs index 25b50a8162..502f76d7ed 100644 --- a/crates/ruff/src/rules/flake8_return/rules/function.rs +++ b/crates/ruff/src/rules/flake8_return/rules/function.rs @@ -398,12 +398,12 @@ const NORETURN_FUNCS: &[&[&str]] = &[ ]; /// Return `true` if the `func` is a known function that never returns. -fn is_noreturn_func(model: &SemanticModel, func: &Expr) -> bool { - model.resolve_call_path(func).map_or(false, |call_path| { +fn is_noreturn_func(func: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(func).map_or(false, |call_path| { NORETURN_FUNCS .iter() .any(|target| call_path.as_slice() == *target) - || model.match_typing_call_path(&call_path, "assert_never") + || semantic.match_typing_call_path(&call_path, "assert_never") }) } @@ -489,7 +489,7 @@ fn implicit_return(checker: &mut Checker, stmt: &Stmt) { if matches!( value.as_ref(), Expr::Call(ast::ExprCall { func, .. }) - if is_noreturn_func(checker.semantic_model(), func) + if is_noreturn_func(func, checker.semantic()) ) => {} _ => { let mut diagnostic = Diagnostic::new(ImplicitReturn, stmt.range()); diff --git a/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs b/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs index fb8412aaf8..7e18208ac3 100644 --- a/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs +++ b/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs @@ -77,7 +77,7 @@ pub(crate) fn private_member_access(checker: &mut Checker, expr: &Expr) { // Ignore accesses on instances within special methods (e.g., `__eq__`). if let ScopeKind::Function(ast::StmtFunctionDef { name, .. }) = - checker.semantic_model().scope().kind + checker.semantic().scope().kind { if matches!( name.as_str(), @@ -151,7 +151,7 @@ pub(crate) fn private_member_access(checker: &mut Checker, expr: &Expr) { // Ignore accesses on class members from _within_ the class. if checker - .semantic_model() + .semantic() .scopes .iter() .rev() @@ -162,7 +162,7 @@ pub(crate) fn private_member_access(checker: &mut Checker, expr: &Expr) { .map_or(false, |name| { if call_path.as_slice() == [name.as_str()] { checker - .semantic_model() + .semantic() .find_binding(name) .map_or(false, |binding| { // TODO(charlie): Could the name ever be bound to a 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 3ed0b7492b..7a21a13ead 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 @@ -323,7 +323,7 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) { if func_name != "isinstance" { continue; } - if !checker.semantic_model().is_builtin("isinstance") { + if !checker.semantic().is_builtin("isinstance") { continue; } @@ -356,7 +356,7 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) { expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - if !contains_effect(target, |id| checker.semantic_model().is_builtin(id)) { + if !contains_effect(target, |id| checker.semantic().is_builtin(id)) { // Grab the types used in each duplicate `isinstance` call (e.g., `int` and `str` // in `isinstance(obj, int) or isinstance(obj, str)`). let types: Vec<&Expr> = indices @@ -478,7 +478,7 @@ pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) { // Avoid rewriting (e.g.) `a == "foo" or a == f()`. if comparators .iter() - .any(|expr| contains_effect(expr, |id| checker.semantic_model().is_builtin(id))) + .any(|expr| contains_effect(expr, |id| checker.semantic().is_builtin(id))) { continue; } @@ -569,7 +569,7 @@ pub(crate) fn expr_and_not_expr(checker: &mut Checker, expr: &Expr) { return; } - if contains_effect(expr, |id| checker.semantic_model().is_builtin(id)) { + if contains_effect(expr, |id| checker.semantic().is_builtin(id)) { return; } @@ -624,7 +624,7 @@ pub(crate) fn expr_or_not_expr(checker: &mut Checker, expr: &Expr) { return; } - if contains_effect(expr, |id| checker.semantic_model().is_builtin(id)) { + if contains_effect(expr, |id| checker.semantic().is_builtin(id)) { return; } @@ -693,15 +693,14 @@ fn is_short_circuit( for (index, (value, next_value)) in values.iter().tuple_windows().enumerate() { // Keep track of the location of the furthest-right, truthy or falsey expression. - let value_truthiness = - Truthiness::from_expr(value, |id| checker.semantic_model().is_builtin(id)); + let value_truthiness = Truthiness::from_expr(value, |id| checker.semantic().is_builtin(id)); let next_value_truthiness = - Truthiness::from_expr(next_value, |id| checker.semantic_model().is_builtin(id)); + Truthiness::from_expr(next_value, |id| checker.semantic().is_builtin(id)); // Keep track of the location of the furthest-right, non-effectful expression. if value_truthiness.is_unknown() - && (!checker.semantic_model().in_boolean_test() - || contains_effect(value, |id| checker.semantic_model().is_builtin(id))) + && (!checker.semantic().in_boolean_test() + || contains_effect(value, |id| checker.semantic().is_builtin(id))) { location = next_value.start(); continue; @@ -721,7 +720,7 @@ fn is_short_circuit( value, TextRange::new(location, expr.end()), short_circuit_truthiness, - checker.semantic_model().in_boolean_test(), + checker.semantic().in_boolean_test(), checker, )); break; @@ -739,7 +738,7 @@ fn is_short_circuit( next_value, TextRange::new(location, expr.end()), short_circuit_truthiness, - checker.semantic_model().in_boolean_test(), + checker.semantic().in_boolean_test(), checker, )); break; 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 f552b6f378..649b90596c 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs @@ -112,7 +112,7 @@ pub(crate) fn use_capital_environment_variables(checker: &mut Checker, expr: &Ex return; }; if !checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["os", "environ", "get"] 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 096bb354d0..14da927c36 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs @@ -449,7 +449,7 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) { if matches!(if_return, Bool::True) && matches!(else_return, Bool::False) && !has_comments(stmt, checker.locator) - && (test.is_compare_expr() || checker.semantic_model().is_builtin("bool")) + && (test.is_compare_expr() || checker.semantic().is_builtin("bool")) { if test.is_compare_expr() { // If the condition is a comparison, we can replace it with the condition. @@ -508,9 +508,9 @@ fn ternary(target_var: &Expr, body_value: &Expr, test: &Expr, orelse_value: &Exp } /// Return `true` if the `Expr` contains a reference to `${module}.${target}`. -fn contains_call_path(model: &SemanticModel, expr: &Expr, target: &[&str]) -> bool { +fn contains_call_path(expr: &Expr, target: &[&str], semantic: &SemanticModel) -> bool { any_over_expr(expr, &|expr| { - model + semantic .resolve_call_path(expr) .map_or(false, |call_path| call_path.as_slice() == target) }) @@ -544,13 +544,13 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: O } // Avoid suggesting ternary for `if sys.version_info >= ...`-style checks. - if contains_call_path(checker.semantic_model(), test, &["sys", "version_info"]) { + if contains_call_path(test, &["sys", "version_info"], checker.semantic()) { return; } // Avoid suggesting ternary for `if sys.platform.startswith("...")`-style // checks. - if contains_call_path(checker.semantic_model(), test, &["sys", "platform"]) { + if contains_call_path(test, &["sys", "platform"], checker.semantic()) { return; } @@ -747,7 +747,7 @@ pub(crate) fn manual_dict_lookup( return; }; if value.as_ref().map_or(false, |value| { - contains_effect(value, |id| checker.semantic_model().is_builtin(id)) + contains_effect(value, |id| checker.semantic().is_builtin(id)) }) { return; } @@ -820,7 +820,7 @@ pub(crate) fn manual_dict_lookup( return; }; if value.as_ref().map_or(false, |value| { - contains_effect(value, |id| checker.semantic_model().is_builtin(id)) + contains_effect(value, |id| checker.semantic().is_builtin(id)) }) { return; }; @@ -903,7 +903,7 @@ pub(crate) fn use_dict_get_with_default( } // Check that the default value is not "complex". - if contains_effect(default_value, |id| checker.semantic_model().is_builtin(id)) { + if contains_effect(default_value, |id| checker.semantic().is_builtin(id)) { 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 d10a58f8ac..7210e6b202 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs @@ -167,7 +167,7 @@ pub(crate) fn explicit_true_false_in_ifexpr( checker.generator().expr(&test.clone()), expr.range(), ))); - } else if checker.semantic_model().is_builtin("bool") { + } else if checker.semantic().is_builtin("bool") { let node = ast::ExprName { id: "bool".into(), ctx: ExprContext::Load, 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 9d04312b70..e303b1974f 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 @@ -150,14 +150,14 @@ pub(crate) fn negation_with_equal_op( if !matches!(&ops[..], [Cmpop::Eq]) { return; } - if is_exception_check(checker.semantic_model().stmt()) { + if is_exception_check(checker.semantic().stmt()) { return; } // Avoid flagging issues in dunder implementations. if let ScopeKind::Function(ast::StmtFunctionDef { name, .. }) | ScopeKind::AsyncFunction(ast::StmtAsyncFunctionDef { name, .. }) = - &checker.semantic_model().scope().kind + &checker.semantic().scope().kind { if DUNDER_METHODS.contains(&name.as_str()) { return; @@ -203,14 +203,14 @@ pub(crate) fn negation_with_not_equal_op( if !matches!(&ops[..], [Cmpop::NotEq]) { return; } - if is_exception_check(checker.semantic_model().stmt()) { + if is_exception_check(checker.semantic().stmt()) { return; } // Avoid flagging issues in dunder implementations. if let ScopeKind::Function(ast::StmtFunctionDef { name, .. }) | ScopeKind::AsyncFunction(ast::StmtAsyncFunctionDef { name, .. }) = - &checker.semantic_model().scope().kind + &checker.semantic().scope().kind { if DUNDER_METHODS.contains(&name.as_str()) { return; @@ -259,13 +259,13 @@ pub(crate) fn double_negation(checker: &mut Checker, expr: &Expr, op: Unaryop, o expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - if checker.semantic_model().in_boolean_test() { + if checker.semantic().in_boolean_test() { #[allow(deprecated)] diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( checker.generator().expr(operand), expr.range(), ))); - } else if checker.semantic_model().is_builtin("bool") { + } else if checker.semantic().is_builtin("bool") { let node = ast::ExprName { id: "bool".into(), ctx: ExprContext::Load, 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 ac19d8f374..7a6614f96c 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 @@ -42,8 +42,8 @@ impl Violation for OpenFileWithContextHandler { /// Return `true` if the current expression is nested in an `await /// exit_stack.enter_async_context` call. -fn match_async_exit_stack(model: &SemanticModel) -> bool { - let Some(expr) = model.expr_grandparent() else { +fn match_async_exit_stack(semantic: &SemanticModel) -> bool { + let Some(expr) = semantic.expr_grandparent() else { return false; }; let Expr::Await(ast::ExprAwait { value, range: _ }) = expr else { @@ -58,11 +58,11 @@ fn match_async_exit_stack(model: &SemanticModel) -> bool { if attr != "enter_async_context" { return false; } - for parent in model.parents() { + for parent in semantic.parents() { if let Stmt::With(ast::StmtWith { items, .. }) = parent { for item in items { if let Expr::Call(ast::ExprCall { func, .. }) = &item.context_expr { - if model.resolve_call_path(func).map_or(false, |call_path| { + if semantic.resolve_call_path(func).map_or(false, |call_path| { call_path.as_slice() == ["contextlib", "AsyncExitStack"] }) { return true; @@ -76,8 +76,8 @@ fn match_async_exit_stack(model: &SemanticModel) -> bool { /// Return `true` if the current expression is nested in an /// `exit_stack.enter_context` call. -fn match_exit_stack(model: &SemanticModel) -> bool { - let Some(expr) = model.expr_parent() else { +fn match_exit_stack(semantic: &SemanticModel) -> bool { + let Some(expr) = semantic.expr_parent() else { return false; }; let Expr::Call(ast::ExprCall { func, .. }) = expr else { @@ -89,11 +89,11 @@ fn match_exit_stack(model: &SemanticModel) -> bool { if attr != "enter_context" { return false; } - for parent in model.parents() { + for parent in semantic.parents() { if let Stmt::With(ast::StmtWith { items, .. }) = parent { for item in items { if let Expr::Call(ast::ExprCall { func, .. }) = &item.context_expr { - if model.resolve_call_path(func).map_or(false, |call_path| { + if semantic.resolve_call_path(func).map_or(false, |call_path| { call_path.as_slice() == ["contextlib", "ExitStack"] }) { return true; @@ -108,23 +108,23 @@ fn match_exit_stack(model: &SemanticModel) -> bool { /// SIM115 pub(crate) fn open_file_with_context_handler(checker: &mut Checker, func: &Expr) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| call_path.as_slice() == ["", "open"]) { - if checker.semantic_model().is_builtin("open") { + if checker.semantic().is_builtin("open") { // Ex) `with open("foo.txt") as f: ...` - if matches!(checker.semantic_model().stmt(), Stmt::With(_)) { + if matches!(checker.semantic().stmt(), Stmt::With(_)) { return; } // Ex) `with contextlib.ExitStack() as exit_stack: ...` - if match_exit_stack(checker.semantic_model()) { + if match_exit_stack(checker.semantic()) { return; } // Ex) `with contextlib.AsyncExitStack() as exit_stack: ...` - if match_async_exit_stack(checker.semantic_model()) { + if match_async_exit_stack(checker.semantic()) { return; } 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 53e3b59718..0641da8aef 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs @@ -262,9 +262,7 @@ pub(crate) fn convert_for_loop_to_any_all( }, TextRange::new(stmt.start(), loop_info.terminal), ); - if checker.patch(diagnostic.kind.rule()) - && checker.semantic_model().is_builtin("any") - { + if checker.patch(diagnostic.kind.rule()) && checker.semantic().is_builtin("any") { #[allow(deprecated)] diagnostic.set_fix(Fix::unspecified(Edit::replacement( contents, @@ -355,9 +353,7 @@ pub(crate) fn convert_for_loop_to_any_all( }, TextRange::new(stmt.start(), loop_info.terminal), ); - if checker.patch(diagnostic.kind.rule()) - && checker.semantic_model().is_builtin("all") - { + if checker.patch(diagnostic.kind.rule()) && checker.semantic().is_builtin("all") { #[allow(deprecated)] diagnostic.set_fix(Fix::unspecified(Edit::replacement( contents, diff --git a/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs b/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs index 869a1298c7..6f738754a7 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs @@ -122,7 +122,7 @@ pub(crate) fn suppressible_exception( let (import_edit, binding) = checker.importer.get_or_import_symbol( &ImportRequest::import("contextlib", "suppress"), stmt.start(), - checker.semantic_model(), + checker.semantic(), )?; let replace_try = Edit::range_replacement( format!("with {binding}({exception})"), diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs index 2928f8b9af..c43f9f35fd 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs @@ -68,7 +68,7 @@ pub(crate) fn no_slots_in_namedtuple_subclass( return false; }; checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { matches!(call_path.as_slice(), ["collections", "namedtuple"]) diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs index 76580bb931..8c0ae80b90 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs @@ -52,7 +52,7 @@ impl Violation for NoSlotsInStrSubclass { pub(crate) fn no_slots_in_str_subclass(checker: &mut Checker, stmt: &Stmt, class: &StmtClassDef) { if class.bases.iter().any(|base| { checker - .semantic_model() + .semantic() .resolve_call_path(base) .map_or(false, |call_path| { matches!(call_path.as_slice(), ["" | "builtins", "str"]) diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs index 3dea2b0057..a4705c2e25 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs @@ -52,12 +52,12 @@ impl Violation for NoSlotsInTupleSubclass { pub(crate) fn no_slots_in_tuple_subclass(checker: &mut Checker, stmt: &Stmt, class: &StmtClassDef) { if class.bases.iter().any(|base| { checker - .semantic_model() + .semantic() .resolve_call_path(map_subscript(base)) .map_or(false, |call_path| { matches!(call_path.as_slice(), ["" | "builtins", "tuple"]) || checker - .semantic_model() + .semantic() .match_typing_call_path(&call_path, "Tuple") }) }) { diff --git a/crates/ruff/src/rules/flake8_tidy_imports/rules/banned_api.rs b/crates/ruff/src/rules/flake8_tidy_imports/rules/banned_api.rs index 668f764354..6730ce8700 100644 --- a/crates/ruff/src/rules/flake8_tidy_imports/rules/banned_api.rs +++ b/crates/ruff/src/rules/flake8_tidy_imports/rules/banned_api.rs @@ -86,7 +86,7 @@ pub(crate) fn banned_attribute_access(checker: &mut Checker, expr: &Expr) { let banned_api = &checker.settings.flake8_tidy_imports.banned_api; if let Some((banned_path, ban)) = checker - .semantic_model() + .semantic() .resolve_call_path(expr) .and_then(|call_path| { banned_api diff --git a/crates/ruff/src/rules/flake8_type_checking/helpers.rs b/crates/ruff/src/rules/flake8_type_checking/helpers.rs index 085f89d029..d9c3ef6075 100644 --- a/crates/ruff/src/rules/flake8_type_checking/helpers.rs +++ b/crates/ruff/src/rules/flake8_type_checking/helpers.rs @@ -4,7 +4,7 @@ use ruff_python_ast::call_path::from_qualified_name; use ruff_python_ast::helpers::map_callable; use ruff_python_semantic::{Binding, BindingKind, ScopeKind, SemanticModel}; -pub(crate) fn is_valid_runtime_import(semantic_model: &SemanticModel, binding: &Binding) -> bool { +pub(crate) fn is_valid_runtime_import(binding: &Binding, semantic: &SemanticModel) -> bool { if matches!( binding.kind, BindingKind::Importation(..) @@ -12,39 +12,36 @@ pub(crate) fn is_valid_runtime_import(semantic_model: &SemanticModel, binding: & | BindingKind::SubmoduleImportation(..) ) { binding.context.is_runtime() - && binding.references().any(|reference_id| { - semantic_model - .reference(reference_id) - .context() - .is_runtime() - }) + && binding + .references() + .any(|reference_id| semantic.reference(reference_id).context().is_runtime()) } else { false } } pub(crate) fn runtime_evaluated( - semantic_model: &SemanticModel, base_classes: &[String], decorators: &[String], + semantic: &SemanticModel, ) -> bool { if !base_classes.is_empty() { - if runtime_evaluated_base_class(semantic_model, base_classes) { + if runtime_evaluated_base_class(base_classes, semantic) { return true; } } if !decorators.is_empty() { - if runtime_evaluated_decorators(semantic_model, decorators) { + if runtime_evaluated_decorators(decorators, semantic) { return true; } } false } -fn runtime_evaluated_base_class(semantic_model: &SemanticModel, base_classes: &[String]) -> bool { - if let ScopeKind::Class(ast::StmtClassDef { bases, .. }) = &semantic_model.scope().kind { +fn runtime_evaluated_base_class(base_classes: &[String], semantic: &SemanticModel) -> bool { + if let ScopeKind::Class(ast::StmtClassDef { bases, .. }) = &semantic.scope().kind { for base in bases.iter() { - if let Some(call_path) = semantic_model.resolve_call_path(base) { + if let Some(call_path) = semantic.resolve_call_path(base) { if base_classes .iter() .any(|base_class| from_qualified_name(base_class) == call_path) @@ -57,12 +54,10 @@ fn runtime_evaluated_base_class(semantic_model: &SemanticModel, base_classes: &[ false } -fn runtime_evaluated_decorators(semantic_model: &SemanticModel, decorators: &[String]) -> bool { - if let ScopeKind::Class(ast::StmtClassDef { decorator_list, .. }) = &semantic_model.scope().kind - { +fn runtime_evaluated_decorators(decorators: &[String], semantic: &SemanticModel) -> bool { + if let ScopeKind::Class(ast::StmtClassDef { decorator_list, .. }) = &semantic.scope().kind { for decorator in decorator_list.iter() { - if let Some(call_path) = - semantic_model.resolve_call_path(map_callable(&decorator.expression)) + if let Some(call_path) = semantic.resolve_call_path(map_callable(&decorator.expression)) { if decorators .iter() diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs b/crates/ruff/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs index 2e90e50284..88ed829041 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs @@ -60,8 +60,8 @@ pub(crate) fn empty_type_checking_block(checker: &mut Checker, stmt: &ast::StmtI let mut diagnostic = Diagnostic::new(EmptyTypeCheckingBlock, stmt.range()); if checker.patch(diagnostic.kind.rule()) { // Delete the entire type-checking block. - let stmt = checker.semantic_model().stmt(); - let parent = checker.semantic_model().stmt_parent(); + let stmt = checker.semantic().stmt(); + let parent = checker.semantic().stmt_parent(); let edit = autofix::edits::delete_stmt( stmt, parent, diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs index 659e59ac2c..b9ffa0c236 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs @@ -73,7 +73,7 @@ pub(crate) fn runtime_import_in_type_checking_block( let mut ignores_by_statement: FxHashMap> = FxHashMap::default(); for binding_id in scope.binding_ids() { - let binding = checker.semantic_model().binding(binding_id); + let binding = checker.semantic().binding(binding_id); let Some(qualified_name) = binding.qualified_name() else { continue; @@ -86,7 +86,7 @@ pub(crate) fn runtime_import_in_type_checking_block( if binding.context.is_typing() && binding.references().any(|reference_id| { checker - .semantic_model() + .semantic() .reference(reference_id) .context() .is_runtime() @@ -99,8 +99,8 @@ pub(crate) fn runtime_import_in_type_checking_block( let import = Import { qualified_name, reference_id, - trimmed_range: binding.trimmed_range(checker.semantic_model(), checker.locator), - parent_range: binding.parent_range(checker.semantic_model()), + trimmed_range: binding.trimmed_range(checker.semantic(), checker.locator), + parent_range: binding.parent_range(checker.semantic()), }; if checker.rule_is_ignored( @@ -188,8 +188,8 @@ struct Import<'a> { /// Generate a [`Fix`] to remove runtime imports from a type-checking block. fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result { - let stmt = checker.semantic_model().stmts[stmt_id]; - let parent = checker.semantic_model().stmts.parent(stmt); + let stmt = checker.semantic().stmts[stmt_id]; + let parent = checker.semantic().stmts.parent(stmt); let qualified_names: Vec<&str> = imports .iter() .map(|Import { qualified_name, .. }| *qualified_name) @@ -199,11 +199,7 @@ fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result let at = imports .iter() .map(|Import { reference_id, .. }| { - checker - .semantic_model() - .reference(*reference_id) - .range() - .start() + checker.semantic().reference(*reference_id).range().start() }) .min() .expect("Expected at least one import"); diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index 97afe9c500..722a6bfbce 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -194,7 +194,7 @@ pub(crate) fn typing_only_runtime_import( FxHashMap::default(); for binding_id in scope.binding_ids() { - let binding = checker.semantic_model().binding(binding_id); + let binding = checker.semantic().binding(binding_id); // If we're in un-strict mode, don't flag typing-only imports that are // implicitly loaded by way of a valid runtime import. @@ -230,7 +230,7 @@ pub(crate) fn typing_only_runtime_import( if binding.context.is_runtime() && binding.references().all(|reference_id| { checker - .semantic_model() + .semantic() .reference(reference_id) .context() .is_typing() @@ -278,8 +278,8 @@ pub(crate) fn typing_only_runtime_import( let import = Import { qualified_name, reference_id, - trimmed_range: binding.trimmed_range(checker.semantic_model(), checker.locator), - parent_range: binding.parent_range(checker.semantic_model()), + trimmed_range: binding.trimmed_range(checker.semantic(), checker.locator), + parent_range: binding.parent_range(checker.semantic()), }; if checker.rule_is_ignored(rule_for(import_type), import.trimmed_range.start()) @@ -413,8 +413,8 @@ fn is_exempt(name: &str, exempt_modules: &[&str]) -> bool { /// Generate a [`Fix`] to remove typing-only imports from a runtime context. fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result { - let stmt = checker.semantic_model().stmts[stmt_id]; - let parent = checker.semantic_model().stmts.parent(stmt); + let stmt = checker.semantic().stmts[stmt_id]; + let parent = checker.semantic().stmts.parent(stmt); let qualified_names: Vec<&str> = imports .iter() .map(|Import { qualified_name, .. }| *qualified_name) @@ -424,11 +424,7 @@ fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result let at = imports .iter() .map(|Import { reference_id, .. }| { - checker - .semantic_model() - .reference(*reference_id) - .range() - .start() + checker.semantic().reference(*reference_id).range().start() }) .min() .expect("Expected at least one import"); @@ -450,7 +446,7 @@ fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result qualified_names, }, at, - checker.semantic_model(), + checker.semantic(), )?; Ok( diff --git a/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs b/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs index eb1c845bd8..76c2d589f8 100644 --- a/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs +++ b/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs @@ -218,7 +218,7 @@ fn function( argumentable: Argumentable, args: &Arguments, values: &Scope, - model: &SemanticModel, + semantic: &SemanticModel, dummy_variable_rgx: &Regex, ignore_variadic_names: bool, ) -> Vec { @@ -237,7 +237,7 @@ fn function( .flatten() .skip(usize::from(ignore_variadic_names)), ); - call(argumentable, args, values, model, dummy_variable_rgx) + call(argumentable, args, values, semantic, dummy_variable_rgx) } /// Check a method for unused arguments. @@ -245,7 +245,7 @@ fn method( argumentable: Argumentable, args: &Arguments, values: &Scope, - model: &SemanticModel, + semantic: &SemanticModel, dummy_variable_rgx: &Regex, ignore_variadic_names: bool, ) -> Vec { @@ -265,21 +265,21 @@ fn method( .flatten() .skip(usize::from(ignore_variadic_names)), ); - call(argumentable, args, values, model, dummy_variable_rgx) + call(argumentable, args, values, semantic, dummy_variable_rgx) } fn call<'a>( argumentable: Argumentable, args: impl Iterator, values: &Scope, - model: &SemanticModel, + semantic: &SemanticModel, dummy_variable_rgx: &Regex, ) -> Vec { let mut diagnostics: Vec = vec![]; for arg in args { if let Some(binding) = values .get(arg.arg.as_str()) - .map(|binding_id| model.binding(binding_id)) + .map(|binding_id| semantic.binding(binding_id)) { if binding.kind.is_argument() && !binding.is_used() @@ -317,22 +317,22 @@ pub(crate) fn unused_arguments( .. }) => { match function_type::classify( - checker.semantic_model(), - parent, name, decorator_list, + parent, + checker.semantic(), &checker.settings.pep8_naming.classmethod_decorators, &checker.settings.pep8_naming.staticmethod_decorators, ) { function_type::FunctionType::Function => { if checker.enabled(Argumentable::Function.rule_code()) - && !visibility::is_overload(checker.semantic_model(), decorator_list) + && !visibility::is_overload(decorator_list, checker.semantic()) { function( Argumentable::Function, args, scope, - checker.semantic_model(), + checker.semantic(), &checker.settings.dummy_variable_rgx, checker .settings @@ -350,15 +350,15 @@ pub(crate) fn unused_arguments( || visibility::is_init(name) || visibility::is_new(name) || visibility::is_call(name)) - && !visibility::is_abstract(checker.semantic_model(), decorator_list) - && !visibility::is_override(checker.semantic_model(), decorator_list) - && !visibility::is_overload(checker.semantic_model(), decorator_list) + && !visibility::is_abstract(decorator_list, checker.semantic()) + && !visibility::is_override(decorator_list, checker.semantic()) + && !visibility::is_overload(decorator_list, checker.semantic()) { method( Argumentable::Method, args, scope, - checker.semantic_model(), + checker.semantic(), &checker.settings.dummy_variable_rgx, checker .settings @@ -376,15 +376,15 @@ pub(crate) fn unused_arguments( || visibility::is_init(name) || visibility::is_new(name) || visibility::is_call(name)) - && !visibility::is_abstract(checker.semantic_model(), decorator_list) - && !visibility::is_override(checker.semantic_model(), decorator_list) - && !visibility::is_overload(checker.semantic_model(), decorator_list) + && !visibility::is_abstract(decorator_list, checker.semantic()) + && !visibility::is_override(decorator_list, checker.semantic()) + && !visibility::is_overload(decorator_list, checker.semantic()) { method( Argumentable::ClassMethod, args, scope, - checker.semantic_model(), + checker.semantic(), &checker.settings.dummy_variable_rgx, checker .settings @@ -402,15 +402,15 @@ pub(crate) fn unused_arguments( || visibility::is_init(name) || visibility::is_new(name) || visibility::is_call(name)) - && !visibility::is_abstract(checker.semantic_model(), decorator_list) - && !visibility::is_override(checker.semantic_model(), decorator_list) - && !visibility::is_overload(checker.semantic_model(), decorator_list) + && !visibility::is_abstract(decorator_list, checker.semantic()) + && !visibility::is_override(decorator_list, checker.semantic()) + && !visibility::is_overload(decorator_list, checker.semantic()) { function( Argumentable::StaticMethod, args, scope, - checker.semantic_model(), + checker.semantic(), &checker.settings.dummy_variable_rgx, checker .settings @@ -429,7 +429,7 @@ pub(crate) fn unused_arguments( Argumentable::Lambda, args, scope, - checker.semantic_model(), + checker.semantic(), &checker.settings.dummy_variable_rgx, checker .settings diff --git a/crates/ruff/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs b/crates/ruff/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs index 5862bd427c..98f25b193d 100644 --- a/crates/ruff/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs +++ b/crates/ruff/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs @@ -15,7 +15,7 @@ use crate::settings::types::PythonVersion; pub(crate) fn replaceable_by_pathlib(checker: &mut Checker, expr: &Expr) { if let Some(diagnostic_kind) = checker - .semantic_model() + .semantic() .resolve_call_path(expr) .and_then(|call_path| match call_path.as_slice() { ["os", "path", "abspath"] => Some(OsPathAbspath.into()), diff --git a/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs b/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs index 834400e5eb..a4bc1a07bd 100644 --- a/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs +++ b/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs @@ -48,25 +48,24 @@ impl AlwaysAutofixableViolation for NumpyDeprecatedTypeAlias { /// NPY001 pub(crate) fn deprecated_type_alias(checker: &mut Checker, expr: &Expr) { - if let Some(type_name) = - checker - .semantic_model() - .resolve_call_path(expr) - .and_then(|call_path| { - if call_path.as_slice() == ["numpy", "bool"] - || call_path.as_slice() == ["numpy", "int"] - || call_path.as_slice() == ["numpy", "float"] - || call_path.as_slice() == ["numpy", "complex"] - || call_path.as_slice() == ["numpy", "object"] - || call_path.as_slice() == ["numpy", "str"] - || call_path.as_slice() == ["numpy", "long"] - || call_path.as_slice() == ["numpy", "unicode"] - { - Some(call_path[1]) - } else { - None - } - }) + if let Some(type_name) = checker + .semantic() + .resolve_call_path(expr) + .and_then(|call_path| { + if call_path.as_slice() == ["numpy", "bool"] + || call_path.as_slice() == ["numpy", "int"] + || call_path.as_slice() == ["numpy", "float"] + || call_path.as_slice() == ["numpy", "complex"] + || call_path.as_slice() == ["numpy", "object"] + || call_path.as_slice() == ["numpy", "str"] + || call_path.as_slice() == ["numpy", "long"] + || call_path.as_slice() == ["numpy", "unicode"] + { + Some(call_path[1]) + } else { + None + } + }) { let mut diagnostic = Diagnostic::new( NumpyDeprecatedTypeAlias { diff --git a/crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs b/crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs index 0e52c39176..736685e813 100644 --- a/crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs +++ b/crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs @@ -58,13 +58,12 @@ impl Violation for NumpyLegacyRandom { /// NPY002 pub(crate) fn numpy_legacy_random(checker: &mut Checker, expr: &Expr) { - if let Some(method_name) = - checker - .semantic_model() - .resolve_call_path(expr) - .and_then(|call_path| { - // seeding state - if call_path.as_slice() == ["numpy", "random", "seed"] + if let Some(method_name) = checker + .semantic() + .resolve_call_path(expr) + .and_then(|call_path| { + // seeding state + if call_path.as_slice() == ["numpy", "random", "seed"] || call_path.as_slice() == ["numpy", "random", "get_state"] || call_path.as_slice() == ["numpy", "random", "set_state"] // simple random data @@ -115,12 +114,12 @@ pub(crate) fn numpy_legacy_random(checker: &mut Checker, expr: &Expr) { || call_path.as_slice() == ["numpy", "random", "wald"] || call_path.as_slice() == ["numpy", "random", "weibull"] || call_path.as_slice() == ["numpy", "random", "zipf"] - { - Some(call_path[2]) - } else { - None - } - }) + { + Some(call_path[2]) + } else { + None + } + }) { checker.diagnostics.push(Diagnostic::new( NumpyLegacyRandom { diff --git a/crates/ruff/src/rules/pandas_vet/helpers.rs b/crates/ruff/src/rules/pandas_vet/helpers.rs index 45240028d9..ec4400a978 100644 --- a/crates/ruff/src/rules/pandas_vet/helpers.rs +++ b/crates/ruff/src/rules/pandas_vet/helpers.rs @@ -15,7 +15,7 @@ pub(super) enum Resolution { } /// Test an [`Expr`] for relevance to Pandas-related operations. -pub(super) fn test_expression(expr: &Expr, model: &SemanticModel) -> Resolution { +pub(super) fn test_expression(expr: &Expr, semantic: &SemanticModel) -> Resolution { match expr { Expr::Constant(_) | Expr::Tuple(_) @@ -27,7 +27,7 @@ pub(super) fn test_expression(expr: &Expr, model: &SemanticModel) -> Resolution | Expr::DictComp(_) | Expr::GeneratorExp(_) => Resolution::IrrelevantExpression, Expr::Name(ast::ExprName { id, .. }) => { - model + semantic .find_binding(id) .map_or(Resolution::IrrelevantBinding, |binding| { match binding.kind { diff --git a/crates/ruff/src/rules/pandas_vet/rules/attr.rs b/crates/ruff/src/rules/pandas_vet/rules/attr.rs index ccdb5616b1..fd0de36a96 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/attr.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/attr.rs @@ -26,7 +26,7 @@ pub(crate) fn attr(checker: &mut Checker, attr: &str, value: &Expr, attr_expr: & }; // Avoid flagging on function calls (e.g., `df.values()`). - if let Some(parent) = checker.semantic_model().expr_parent() { + if let Some(parent) = checker.semantic().expr_parent() { if matches!(parent, Expr::Call(_)) { return; } @@ -35,7 +35,7 @@ pub(crate) fn attr(checker: &mut Checker, attr: &str, value: &Expr, attr_expr: & // Avoid flagging on non-DataFrames (e.g., `{"a": 1}.values`), and on irrelevant bindings // (like imports). if !matches!( - test_expression(value, checker.semantic_model()), + test_expression(value, checker.semantic()), Resolution::RelevantLocal ) { return; diff --git a/crates/ruff/src/rules/pandas_vet/rules/call.rs b/crates/ruff/src/rules/pandas_vet/rules/call.rs index ecea41b6f8..be7dfadf24 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/call.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/call.rs @@ -80,7 +80,7 @@ pub(crate) fn call(checker: &mut Checker, func: &Expr) { // Ignore irrelevant bindings (like imports). if !matches!( - test_expression(value, checker.semantic_model()), + test_expression(value, checker.semantic()), Resolution::RelevantLocal | Resolution::PandasModule ) { return; 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 7b87ccc3b9..39b2cca800 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs @@ -60,12 +60,12 @@ pub(crate) fn inplace_argument( let mut is_checkable = false; let mut is_pandas = false; - if let Some(call_path) = checker.semantic_model().resolve_call_path(func) { + if let Some(call_path) = checker.semantic().resolve_call_path(func) { is_checkable = true; let module = call_path[0]; is_pandas = checker - .semantic_model() + .semantic() .find_binding(module) .map_or(false, |binding| { matches!( @@ -103,9 +103,9 @@ pub(crate) fn inplace_argument( // but we don't currently restore expression stacks when parsing deferred nodes, // and so the parent is lost. if !seen_star - && checker.semantic_model().stmt().is_expr_stmt() - && checker.semantic_model().expr_parent().is_none() - && !checker.semantic_model().scope().kind.is_lambda() + && checker.semantic().stmt().is_expr_stmt() + && checker.semantic().expr_parent().is_none() + && !checker.semantic().scope().kind.is_lambda() { if let Some(fix) = convert_inplace_argument_to_assignment( checker.locator, diff --git a/crates/ruff/src/rules/pandas_vet/rules/subscript.rs b/crates/ruff/src/rules/pandas_vet/rules/subscript.rs index 96430e0151..6bcbb8298b 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/subscript.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/subscript.rs @@ -54,7 +54,7 @@ pub(crate) fn subscript(checker: &mut Checker, value: &Expr, expr: &Expr) { // Avoid flagging on non-DataFrames (e.g., `{"a": 1}.at[0]`), and on irrelevant bindings // (like imports). if !matches!( - test_expression(value, checker.semantic_model()), + test_expression(value, checker.semantic()), Resolution::RelevantLocal ) { return; diff --git a/crates/ruff/src/rules/pep8_naming/helpers.rs b/crates/ruff/src/rules/pep8_naming/helpers.rs index 05de2560a3..93792ad0ab 100644 --- a/crates/ruff/src/rules/pep8_naming/helpers.rs +++ b/crates/ruff/src/rules/pep8_naming/helpers.rs @@ -22,14 +22,14 @@ pub(super) fn is_acronym(name: &str, asname: &str) -> bool { name.chars().filter(|c| c.is_uppercase()).join("") == asname } -pub(super) fn is_named_tuple_assignment(model: &SemanticModel, stmt: &Stmt) -> bool { +pub(super) fn is_named_tuple_assignment(stmt: &Stmt, semantic: &SemanticModel) -> bool { let Stmt::Assign(ast::StmtAssign { value, .. }) = stmt else { return false; }; let Expr::Call(ast::ExprCall {func, ..}) = value.as_ref() else { return false; }; - model.resolve_call_path(func).map_or(false, |call_path| { + semantic.resolve_call_path(func).map_or(false, |call_path| { matches!( call_path.as_slice(), ["collections", "namedtuple"] | ["typing", "NamedTuple"] @@ -37,35 +37,35 @@ pub(super) fn is_named_tuple_assignment(model: &SemanticModel, stmt: &Stmt) -> b }) } -pub(super) fn is_typed_dict_assignment(model: &SemanticModel, stmt: &Stmt) -> bool { +pub(super) fn is_typed_dict_assignment(stmt: &Stmt, semantic: &SemanticModel) -> bool { let Stmt::Assign(ast::StmtAssign { value, .. }) = stmt else { return false; }; let Expr::Call(ast::ExprCall {func, ..}) = value.as_ref() else { return false; }; - model.resolve_call_path(func).map_or(false, |call_path| { + semantic.resolve_call_path(func).map_or(false, |call_path| { call_path.as_slice() == ["typing", "TypedDict"] }) } -pub(super) fn is_type_var_assignment(model: &SemanticModel, stmt: &Stmt) -> bool { +pub(super) fn is_type_var_assignment(stmt: &Stmt, semantic: &SemanticModel) -> bool { let Stmt::Assign(ast::StmtAssign { value, .. }) = stmt else { return false; }; let Expr::Call(ast::ExprCall {func, ..}) = value.as_ref() else { return false; }; - model.resolve_call_path(func).map_or(false, |call_path| { + semantic.resolve_call_path(func).map_or(false, |call_path| { call_path.as_slice() == ["typing", "TypeVar"] || call_path.as_slice() == ["typing", "NewType"] }) } -pub(super) fn is_typed_dict_class(model: &SemanticModel, bases: &[Expr]) -> bool { +pub(super) fn is_typed_dict_class(bases: &[Expr], semantic: &SemanticModel) -> bool { bases .iter() - .any(|base| model.match_typing_expr(base, "TypedDict")) + .any(|base| semantic.match_typing_expr(base, "TypedDict")) } #[cfg(test)] diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs index 66e9a597cb..e3fa06517a 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs @@ -64,10 +64,10 @@ pub(crate) fn invalid_first_argument_name_for_class_method( ) -> Option { if !matches!( function_type::classify( - checker.semantic_model(), - scope, name, decorator_list, + scope, + checker.semantic(), &checker.settings.pep8_naming.classmethod_decorators, &checker.settings.pep8_naming.staticmethod_decorators, ), diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs index bb1fb0a520..95a1194e85 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs @@ -61,10 +61,10 @@ pub(crate) fn invalid_first_argument_name_for_method( ) -> Option { if !matches!( function_type::classify( - checker.semantic_model(), - scope, name, decorator_list, + scope, + checker.semantic(), &checker.settings.pep8_naming.classmethod_decorators, &checker.settings.pep8_naming.staticmethod_decorators, ), diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs index 1f8a41887d..08be500680 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs @@ -55,7 +55,7 @@ pub(crate) fn invalid_function_name( name: &str, decorator_list: &[Decorator], ignore_names: &[IdentifierPattern], - model: &SemanticModel, + semantic: &SemanticModel, locator: &Locator, ) -> Option { // Ignore any explicitly-ignored function names. @@ -73,7 +73,7 @@ pub(crate) fn invalid_function_name( // Ignore any functions that are explicitly `@override`. These are defined elsewhere, // so if they're first-party, we'll flag them at the definition site. - if visibility::is_override(model, decorator_list) { + if visibility::is_override(decorator_list, semantic) { return None; } diff --git a/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs b/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs index 4fd006a82e..c43b384524 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs @@ -67,8 +67,8 @@ pub(crate) fn mixed_case_variable_in_class_scope( return; } if helpers::is_mixed_case(name) - && !helpers::is_named_tuple_assignment(checker.semantic_model(), stmt) - && !helpers::is_typed_dict_class(checker.semantic_model(), bases) + && !helpers::is_named_tuple_assignment(stmt, checker.semantic()) + && !helpers::is_typed_dict_class(bases, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( MixedCaseVariableInClassScope { diff --git a/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs b/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs index 6ea71b349d..158f03eaea 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs @@ -75,8 +75,7 @@ pub(crate) fn mixed_case_variable_in_global_scope( { return; } - if helpers::is_mixed_case(name) - && !helpers::is_named_tuple_assignment(checker.semantic_model(), stmt) + if helpers::is_mixed_case(name) && !helpers::is_named_tuple_assignment(stmt, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( MixedCaseVariableInGlobalScope { diff --git a/crates/ruff/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs b/crates/ruff/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs index f395e6fefd..147531a46b 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs @@ -66,9 +66,9 @@ pub(crate) fn non_lowercase_variable_in_function( } if name.to_lowercase() != name - && !helpers::is_named_tuple_assignment(checker.semantic_model(), stmt) - && !helpers::is_typed_dict_assignment(checker.semantic_model(), stmt) - && !helpers::is_type_var_assignment(checker.semantic_model(), stmt) + && !helpers::is_named_tuple_assignment(stmt, checker.semantic()) + && !helpers::is_typed_dict_assignment(stmt, checker.semantic()) + && !helpers::is_type_var_assignment(stmt, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( NonLowercaseVariableInFunction { diff --git a/crates/ruff/src/rules/pycodestyle/rules/imports.rs b/crates/ruff/src/rules/pycodestyle/rules/imports.rs index b5b724329e..61e163354c 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/imports.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/imports.rs @@ -88,8 +88,7 @@ pub(crate) fn module_import_not_at_top_of_file( stmt: &Stmt, locator: &Locator, ) { - if checker.semantic_model().seen_import_boundary() && locator.is_at_start_of_line(stmt.start()) - { + if checker.semantic().seen_import_boundary() && locator.is_at_start_of_line(stmt.start()) { checker .diagnostics .push(Diagnostic::new(ModuleImportNotAtTopOfFile, stmt.range())); diff --git a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs index 3982031e3e..29767501ed 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs @@ -78,7 +78,7 @@ pub(crate) fn lambda_assignment( // rewritten function definition to be equivalent. // See https://github.com/astral-sh/ruff/issues/3046 if checker.patch(diagnostic.kind.rule()) - && !checker.semantic_model().scope().kind.is_class() + && !checker.semantic().scope().kind.is_class() && !has_leading_content(stmt, checker.locator) && !has_trailing_content(stmt, checker.locator) { @@ -86,11 +86,11 @@ pub(crate) fn lambda_assignment( let indentation = leading_indentation(first_line); let mut indented = String::new(); for (idx, line) in function( - checker.semantic_model(), id, args, body, annotation, + checker.semantic(), checker.generator(), ) .universal_newlines() @@ -120,7 +120,7 @@ pub(crate) fn lambda_assignment( /// The `Callable` import can be from either `collections.abc` or `typing`. /// If an ellipsis is used for the argument types, an empty list is returned. /// The returned values are cloned, so they can be used as-is. -fn extract_types(model: &SemanticModel, annotation: &Expr) -> Option<(Vec, Expr)> { +fn extract_types(annotation: &Expr, semantic: &SemanticModel) -> Option<(Vec, Expr)> { let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = &annotation else { return None; }; @@ -131,10 +131,13 @@ fn extract_types(model: &SemanticModel, annotation: &Expr) -> Option<(Vec, return None; } - if !model.resolve_call_path(value).map_or(false, |call_path| { - call_path.as_slice() == ["collections", "abc", "Callable"] - || model.match_typing_call_path(&call_path, "Callable") - }) { + if !semantic + .resolve_call_path(value) + .map_or(false, |call_path| { + call_path.as_slice() == ["collections", "abc", "Callable"] + || semantic.match_typing_call_path(&call_path, "Callable") + }) + { return None; } @@ -156,11 +159,11 @@ fn extract_types(model: &SemanticModel, annotation: &Expr) -> Option<(Vec, } fn function( - model: &SemanticModel, name: &str, args: &Arguments, body: &Expr, annotation: Option<&Expr>, + semantic: &SemanticModel, generator: Generator, ) -> String { let body = Stmt::Return(ast::StmtReturn { @@ -168,7 +171,7 @@ fn function( range: TextRange::default(), }); if let Some(annotation) = annotation { - if let Some((arg_types, return_type)) = extract_types(model, annotation) { + if let Some((arg_types, return_type)) = extract_types(annotation, semantic) { // A `lambda` expression can only have positional and positional-only // arguments. The order is always positional-only first, then positional. let new_posonlyargs = args diff --git a/crates/ruff/src/rules/pycodestyle/rules/type_comparison.rs b/crates/ruff/src/rules/pycodestyle/rules/type_comparison.rs index 7cec3e0cb4..1af0cc98f8 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/type_comparison.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/type_comparison.rs @@ -52,7 +52,7 @@ pub(crate) fn type_comparison( Expr::Call(ast::ExprCall { func, args, .. }) => { if let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() { // Ex) `type(False)` - if id == "type" && checker.semantic_model().is_builtin("type") { + if id == "type" && checker.semantic().is_builtin("type") { if let Some(arg) = args.first() { // Allow comparison for types which are not obvious. if !matches!( @@ -76,12 +76,12 @@ pub(crate) fn type_comparison( if let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() { // Ex) `types.NoneType` if id == "types" - && checker.semantic_model().resolve_call_path(value).map_or( - false, - |call_path| { + && checker + .semantic() + .resolve_call_path(value) + .map_or(false, |call_path| { call_path.first().map_or(false, |module| *module == "types") - }, - ) + }) { checker .diagnostics diff --git a/crates/ruff/src/rules/pydocstyle/helpers.rs b/crates/ruff/src/rules/pydocstyle/helpers.rs index 00d0eb8b93..f0632f86f8 100644 --- a/crates/ruff/src/rules/pydocstyle/helpers.rs +++ b/crates/ruff/src/rules/pydocstyle/helpers.rs @@ -40,9 +40,9 @@ pub(super) fn ends_with_backslash(line: &str) -> bool { /// Check decorator list to see if function should be ignored. pub(crate) fn should_ignore_definition( - model: &SemanticModel, definition: &Definition, ignore_decorators: &BTreeSet, + semantic: &SemanticModel, ) -> bool { if ignore_decorators.is_empty() { return false; @@ -55,7 +55,8 @@ pub(crate) fn should_ignore_definition( }) = definition { for decorator in cast::decorator_list(stmt) { - if let Some(call_path) = model.resolve_call_path(map_callable(&decorator.expression)) { + if let Some(call_path) = semantic.resolve_call_path(map_callable(&decorator.expression)) + { if ignore_decorators .iter() .any(|decorator| from_qualified_name(decorator) == call_path) diff --git a/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs b/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs index c612aad519..c986fb6ac8 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs @@ -27,7 +27,7 @@ pub(crate) fn if_needed(checker: &mut Checker, docstring: &Docstring) { }) = docstring.definition else { return; }; - if !is_overload(checker.semantic_model(), cast::decorator_list(stmt)) { + if !is_overload(cast::decorator_list(stmt), checker.semantic()) { return; } checker.diagnostics.push(Diagnostic::new( diff --git a/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs b/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs index 91b67899bb..ecda3d504a 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs @@ -41,9 +41,9 @@ pub(crate) fn non_imperative_mood( if is_test(cast::name(stmt)) || is_property( - checker.semantic_model(), cast::decorator_list(stmt), &property_decorators, + checker.semantic(), ) { return; diff --git a/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs b/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs index a1cbdb4412..7f2be87fff 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs @@ -158,7 +158,7 @@ pub(crate) fn not_missing( stmt, .. }) => { - if is_overload(checker.semantic_model(), cast::decorator_list(stmt)) { + if is_overload(cast::decorator_list(stmt), checker.semantic()) { true } else { if checker.enabled(Rule::UndocumentedPublicFunction) { @@ -175,8 +175,8 @@ pub(crate) fn not_missing( stmt, .. }) => { - if is_overload(checker.semantic_model(), cast::decorator_list(stmt)) - || is_override(checker.semantic_model(), cast::decorator_list(stmt)) + if is_overload(cast::decorator_list(stmt), checker.semantic()) + || is_override(cast::decorator_list(stmt), checker.semantic()) { true } else if is_init(cast::name(stmt)) { diff --git a/crates/ruff/src/rules/pydocstyle/rules/sections.rs b/crates/ruff/src/rules/pydocstyle/rules/sections.rs index 66f77fcade..30cbd6687c 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/sections.rs @@ -722,7 +722,7 @@ fn missing_args(checker: &mut Checker, docstring: &Docstring, docstrings_args: & // If this is a non-static method, skip `cls` or `self`. usize::from( docstring.definition.is_method() - && !is_staticmethod(checker.semantic_model(), cast::decorator_list(stmt)), + && !is_staticmethod(cast::decorator_list(stmt), checker.semantic()), ), ) { diff --git a/crates/ruff/src/rules/pyflakes/rules/invalid_print_syntax.rs b/crates/ruff/src/rules/pyflakes/rules/invalid_print_syntax.rs index 651b03d737..bfbb796bf0 100644 --- a/crates/ruff/src/rules/pyflakes/rules/invalid_print_syntax.rs +++ b/crates/ruff/src/rules/pyflakes/rules/invalid_print_syntax.rs @@ -63,7 +63,7 @@ pub(crate) fn invalid_print_syntax(checker: &mut Checker, left: &Expr) { if id != "print" { return; } - if !checker.semantic_model().is_builtin("print") { + if !checker.semantic().is_builtin("print") { return; }; checker diff --git a/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs b/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs index b30512b0b9..54acb483fd 100644 --- a/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs +++ b/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs @@ -33,7 +33,7 @@ impl Violation for ReturnOutsideFunction { pub(crate) fn return_outside_function(checker: &mut Checker, stmt: &Stmt) { if matches!( - checker.semantic_model().scope().kind, + checker.semantic().scope().kind, ScopeKind::Class(_) | ScopeKind::Module ) { checker diff --git a/crates/ruff/src/rules/pyflakes/rules/undefined_local.rs b/crates/ruff/src/rules/pyflakes/rules/undefined_local.rs index dae0af9655..2312963920 100644 --- a/crates/ruff/src/rules/pyflakes/rules/undefined_local.rs +++ b/crates/ruff/src/rules/pyflakes/rules/undefined_local.rs @@ -46,7 +46,7 @@ impl Violation for UndefinedLocal { /// F823 pub(crate) fn undefined_local(checker: &mut Checker, name: &str) { // If the name hasn't already been defined in the current scope... - let current = checker.semantic_model().scope(); + let current = checker.semantic().scope(); if !current.kind.is_any_function() || current.has(name) { return; } @@ -57,7 +57,7 @@ pub(crate) fn undefined_local(checker: &mut Checker, name: &str) { // For every function and module scope above us... let local_access = checker - .semantic_model() + .semantic() .scopes .ancestors(parent) .find_map(|scope| { @@ -68,15 +68,12 @@ pub(crate) fn undefined_local(checker: &mut Checker, name: &str) { // If the name was defined in that scope... if let Some(binding) = scope .get(name) - .map(|binding_id| checker.semantic_model().binding(binding_id)) + .map(|binding_id| checker.semantic().binding(binding_id)) { // And has already been accessed in the current scope... if let Some(range) = binding.references().find_map(|reference_id| { - let reference = checker.semantic_model().reference(reference_id); - if checker - .semantic_model() - .is_current_scope(reference.scope_id()) - { + let reference = checker.semantic().reference(reference_id); + if checker.semantic().is_current_scope(reference.scope_id()) { Some(reference.range()) } else { None diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_annotation.rs b/crates/ruff/src/rules/pyflakes/rules/unused_annotation.rs index 55c524edca..1f2e92f25d 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_annotation.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_annotation.rs @@ -34,12 +34,12 @@ impl Violation for UnusedAnnotation { /// F842 pub(crate) fn unused_annotation(checker: &mut Checker, scope: ScopeId) { - let scope = &checker.semantic_model().scopes[scope]; + let scope = &checker.semantic().scopes[scope]; let bindings: Vec<_> = scope .bindings() .filter_map(|(name, binding_id)| { - let binding = checker.semantic_model().binding(binding_id); + let binding = checker.semantic().binding(binding_id); if binding.kind.is_annotation() && !binding.is_used() && !checker.settings.dummy_variable_rgx.is_match(name) diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs index 5471c84c63..b8d8e4be92 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs @@ -102,7 +102,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut let mut ignored: FxHashMap<(NodeId, Exceptions), Vec> = FxHashMap::default(); for binding_id in scope.binding_ids() { - let binding = checker.semantic_model().binding(binding_id); + let binding = checker.semantic().binding(binding_id); if binding.is_used() || binding.is_explicit_export() { continue; @@ -118,8 +118,8 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut let import = Import { qualified_name, - trimmed_range: binding.trimmed_range(checker.semantic_model(), checker.locator), - parent_range: binding.parent_range(checker.semantic_model()), + trimmed_range: binding.trimmed_range(checker.semantic(), checker.locator), + parent_range: binding.parent_range(checker.semantic()), }; if checker.rule_is_ignored(Rule::UnusedImport, import.trimmed_range.start()) @@ -222,8 +222,8 @@ struct Import<'a> { /// Generate a [`Fix`] to remove unused imports from a statement. fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result { - let stmt = checker.semantic_model().stmts[stmt_id]; - let parent = checker.semantic_model().stmts.parent(stmt); + let stmt = checker.semantic().stmts[stmt_id]; + let parent = checker.semantic().stmts.parent(stmt); let edit = autofix::edits::remove_unused_imports( imports .iter() diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs b/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs index 193a37255b..0909571255 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs @@ -199,7 +199,7 @@ fn remove_unused_variable( if let Some(target) = targets.iter().find(|target| range == target.range()) { if target.is_name_expr() { return if targets.len() > 1 - || contains_effect(value, |id| checker.semantic_model().is_builtin(id)) + || contains_effect(value, |id| checker.semantic().is_builtin(id)) { // If the expression is complex (`x = foo()`), remove the assignment, // but preserve the right-hand side. @@ -231,7 +231,7 @@ fn remove_unused_variable( }) = stmt { if target.is_name_expr() { - return if contains_effect(value, |id| checker.semantic_model().is_builtin(id)) { + return if contains_effect(value, |id| checker.semantic().is_builtin(id)) { // If the expression is complex (`x = foo()`), remove the assignment, // but preserve the right-hand side. let start = stmt.start(); @@ -285,14 +285,14 @@ fn remove_unused_variable( /// F841 pub(crate) fn unused_variable(checker: &mut Checker, scope: ScopeId) { - let scope = &checker.semantic_model().scopes[scope]; + let scope = &checker.semantic().scopes[scope]; if scope.uses_locals() && scope.kind.is_any_function() { return; } let bindings: Vec<_> = scope .bindings() - .map(|(name, binding_id)| (name, checker.semantic_model().binding(binding_id))) + .map(|(name, binding_id)| (name, checker.semantic().binding(binding_id))) .filter_map(|(name, binding)| { if (binding.kind.is_assignment() || binding.kind.is_named_expr_assignment()) && !binding.is_used() @@ -313,8 +313,8 @@ pub(crate) fn unused_variable(checker: &mut Checker, scope: ScopeId) { let mut diagnostic = Diagnostic::new(UnusedVariable { name }, range); if checker.patch(diagnostic.kind.rule()) { if let Some(source) = source { - let stmt = checker.semantic_model().stmts[source]; - let parent = checker.semantic_model().stmts.parent(stmt); + let stmt = checker.semantic().stmts[source]; + let parent = checker.semantic().stmts.parent(stmt); if let Some(fix) = remove_unused_variable(stmt, parent, range, checker) { diagnostic.set_fix(fix); } 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 d86faeb118..54fab4be14 100644 --- a/crates/ruff/src/rules/pyflakes/rules/yield_outside_function.rs +++ b/crates/ruff/src/rules/pyflakes/rules/yield_outside_function.rs @@ -55,7 +55,7 @@ impl Violation for YieldOutsideFunction { pub(crate) fn yield_outside_function(checker: &mut Checker, expr: &Expr) { if matches!( - checker.semantic_model().scope().kind, + checker.semantic().scope().kind, ScopeKind::Class(_) | ScopeKind::Module ) { let keyword = match expr { diff --git a/crates/ruff/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs b/crates/ruff/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs index ba941d1d85..cdf0490fb6 100644 --- a/crates/ruff/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs +++ b/crates/ruff/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs @@ -44,7 +44,7 @@ impl Violation for DeprecatedLogWarn { /// PGH002 pub(crate) fn deprecated_log_warn(checker: &mut Checker, func: &Expr) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["logging", "warn"] diff --git a/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs b/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs index 40b224a22c..70c8b57260 100644 --- a/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs +++ b/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs @@ -46,7 +46,7 @@ pub(crate) fn no_eval(checker: &mut Checker, func: &Expr) { if id != "eval" { return; } - if !checker.semantic_model().is_builtin("eval") { + if !checker.semantic().is_builtin("eval") { return; } checker diff --git a/crates/ruff/src/rules/pylint/helpers.rs b/crates/ruff/src/rules/pylint/helpers.rs index 4e5dba75da..37d48f5104 100644 --- a/crates/ruff/src/rules/pylint/helpers.rs +++ b/crates/ruff/src/rules/pylint/helpers.rs @@ -8,8 +8,8 @@ use ruff_python_semantic::{ScopeKind, SemanticModel}; use crate::settings::Settings; -pub(super) fn in_dunder_init(model: &SemanticModel, settings: &Settings) -> bool { - let scope = model.scope(); +pub(super) fn in_dunder_init(semantic: &SemanticModel, settings: &Settings) -> bool { + let scope = semantic.scope(); let ( ScopeKind::Function(ast::StmtFunctionDef { name, @@ -27,16 +27,16 @@ pub(super) fn in_dunder_init(model: &SemanticModel, settings: &Settings) -> bool if name != "__init__" { return false; } - let Some(parent) = scope.parent.map(|scope_id| &model.scopes[scope_id]) else { + let Some(parent) = scope.parent.map(|scope_id| &semantic.scopes[scope_id]) else { return false; }; if !matches!( function_type::classify( - model, - parent, name, decorator_list, + parent, + semantic, &settings.pep8_naming.classmethod_decorators, &settings.pep8_naming.staticmethod_decorators, ), diff --git a/crates/ruff/src/rules/pylint/rules/await_outside_async.rs b/crates/ruff/src/rules/pylint/rules/await_outside_async.rs index 0a7c2936aa..f0df96fcb0 100644 --- a/crates/ruff/src/rules/pylint/rules/await_outside_async.rs +++ b/crates/ruff/src/rules/pylint/rules/await_outside_async.rs @@ -44,7 +44,7 @@ impl Violation for AwaitOutsideAsync { /// PLE1142 pub(crate) fn await_outside_async(checker: &mut Checker, expr: &Expr) { - if !checker.semantic_model().in_async_context() { + if !checker.semantic().in_async_context() { checker .diagnostics .push(Diagnostic::new(AwaitOutsideAsync, expr.range())); diff --git a/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs b/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs index 8cde351c82..4872e0f807 100644 --- a/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs +++ b/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs @@ -98,7 +98,7 @@ pub(crate) fn compare_to_empty_string( ) { // Omit string comparison rules within subscripts. This is mostly commonly used within // DataFrame and np.ndarray indexing. - for parent in checker.semantic_model().expr_ancestors() { + for parent in checker.semantic().expr_ancestors() { if matches!(parent, Expr::Subscript(_)) { return; } diff --git a/crates/ruff/src/rules/pylint/rules/global_statement.rs b/crates/ruff/src/rules/pylint/rules/global_statement.rs index 1bdbe19e26..6a8613b639 100644 --- a/crates/ruff/src/rules/pylint/rules/global_statement.rs +++ b/crates/ruff/src/rules/pylint/rules/global_statement.rs @@ -55,11 +55,11 @@ impl Violation for GlobalStatement { /// PLW0603 pub(crate) fn global_statement(checker: &mut Checker, name: &str) { - let scope = checker.semantic_model().scope(); + let scope = checker.semantic().scope(); if let Some(binding_id) = scope.get(name) { - let binding = checker.semantic_model().binding(binding_id); + let binding = checker.semantic().binding(binding_id); if binding.kind.is_global() { - let source = checker.semantic_model().stmts[binding + let source = checker.semantic().stmts[binding .source .expect("`global` bindings should always have a `source`")]; let diagnostic = Diagnostic::new( 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 a70ae406d6..d99dc30aba 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_envvar_default.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_envvar_default.rs @@ -84,7 +84,7 @@ pub(crate) fn invalid_envvar_default( keywords: &[Keyword], ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| call_path.as_slice() == ["os", "getenv"]) { diff --git a/crates/ruff/src/rules/pylint/rules/invalid_envvar_value.rs b/crates/ruff/src/rules/pylint/rules/invalid_envvar_value.rs index 2377de8466..6ee35e0bfd 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_envvar_value.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_envvar_value.rs @@ -81,7 +81,7 @@ pub(crate) fn invalid_envvar_value( keywords: &[Keyword], ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| call_path.as_slice() == ["os", "getenv"]) { diff --git a/crates/ruff/src/rules/pylint/rules/invalid_str_return.rs b/crates/ruff/src/rules/pylint/rules/invalid_str_return.rs index 486c8a474e..9f43d0d96c 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_str_return.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_str_return.rs @@ -29,7 +29,7 @@ pub(crate) fn invalid_str_return(checker: &mut Checker, name: &str, body: &[Stmt return; } - if !checker.semantic_model().scope().kind.is_class() { + if !checker.semantic().scope().kind.is_class() { return; } diff --git a/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs b/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs index 5e936bf334..a408e8d4c6 100644 --- a/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs +++ b/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs @@ -54,7 +54,7 @@ impl Violation for LoadBeforeGlobalDeclaration { } /// PLE0118 pub(crate) fn load_before_global_declaration(checker: &mut Checker, name: &str, expr: &Expr) { - if let Some(stmt) = checker.semantic_model().global(name) { + if let Some(stmt) = checker.semantic().global(name) { if expr.start() < stmt.start() { #[allow(deprecated)] let location = checker.locator.compute_source_location(stmt.start()); diff --git a/crates/ruff/src/rules/pylint/rules/logging.rs b/crates/ruff/src/rules/pylint/rules/logging.rs index 6ddffccb20..157844d818 100644 --- a/crates/ruff/src/rules/pylint/rules/logging.rs +++ b/crates/ruff/src/rules/pylint/rules/logging.rs @@ -102,7 +102,7 @@ pub(crate) fn logging_call( return; } - if !logging::is_logger_candidate(func, checker.semantic_model()) { + if !logging::is_logger_candidate(func, checker.semantic()) { 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 4eab1a008d..4551376ef6 100644 --- a/crates/ruff/src/rules/pylint/rules/nested_min_max.rs +++ b/crates/ruff/src/rules/pylint/rules/nested_min_max.rs @@ -59,16 +59,20 @@ impl Violation for NestedMinMax { impl MinMax { /// Converts a function call [`Expr`] into a [`MinMax`] if it is a call to `min` or `max`. - fn try_from_call(func: &Expr, keywords: &[Keyword], model: &SemanticModel) -> Option { + fn try_from_call( + func: &Expr, + keywords: &[Keyword], + semantic: &SemanticModel, + ) -> Option { if !keywords.is_empty() { return None; } let Expr::Name(ast::ExprName { id, .. }) = func else { return None; }; - if id.as_str() == "min" && model.is_builtin("min") { + if id.as_str() == "min" && semantic.is_builtin("min") { Some(MinMax::Min) - } else if id.as_str() == "max" && model.is_builtin("max") { + } else if id.as_str() == "max" && semantic.is_builtin("max") { Some(MinMax::Max) } else { None @@ -87,8 +91,8 @@ impl std::fmt::Display for MinMax { /// Collect a new set of arguments to by either accepting existing args as-is or /// collecting child arguments, if it's a call to the same function. -fn collect_nested_args(model: &SemanticModel, min_max: MinMax, args: &[Expr]) -> Vec { - fn inner(model: &SemanticModel, min_max: MinMax, args: &[Expr], new_args: &mut Vec) { +fn collect_nested_args(min_max: MinMax, args: &[Expr], semantic: &SemanticModel) -> Vec { + fn inner(min_max: MinMax, args: &[Expr], semantic: &SemanticModel, new_args: &mut Vec) { for arg in args { if let Expr::Call(ast::ExprCall { func, @@ -106,8 +110,8 @@ fn collect_nested_args(model: &SemanticModel, min_max: MinMax, args: &[Expr]) -> new_args.push(new_arg); continue; } - if MinMax::try_from_call(func, keywords, model) == Some(min_max) { - inner(model, min_max, args, new_args); + if MinMax::try_from_call(func, keywords, semantic) == Some(min_max) { + inner(min_max, args, semantic, new_args); continue; } } @@ -116,7 +120,7 @@ fn collect_nested_args(model: &SemanticModel, min_max: MinMax, args: &[Expr]) -> } let mut new_args = Vec::with_capacity(args.len()); - inner(model, min_max, args, &mut new_args); + inner(min_max, args, semantic, &mut new_args); new_args } @@ -128,7 +132,7 @@ pub(crate) fn nested_min_max( args: &[Expr], keywords: &[Keyword], ) { - let Some(min_max) = MinMax::try_from_call(func, keywords, checker.semantic_model()) else { + let Some(min_max) = MinMax::try_from_call(func, keywords, checker.semantic()) else { return; }; @@ -142,15 +146,14 @@ pub(crate) fn nested_min_max( let Expr::Call(ast::ExprCall { func, keywords, ..} )= arg else { return false; }; - MinMax::try_from_call(func.as_ref(), keywords.as_ref(), checker.semantic_model()) - == Some(min_max) + MinMax::try_from_call(func.as_ref(), keywords.as_ref(), checker.semantic()) == Some(min_max) }) { let mut diagnostic = Diagnostic::new(NestedMinMax { func: min_max }, expr.range()); if checker.patch(diagnostic.kind.rule()) { if !has_comments(expr, checker.locator) { let flattened_expr = Expr::Call(ast::ExprCall { func: Box::new(func.clone()), - args: collect_nested_args(checker.semantic_model(), min_max, args), + args: collect_nested_args(min_max, args, checker.semantic()), keywords: keywords.to_owned(), range: TextRange::default(), }); diff --git a/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs b/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs index d63dd5025b..d068255511 100644 --- a/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs +++ b/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs @@ -59,7 +59,7 @@ pub(crate) fn property_with_parameters( { return; } - if checker.semantic_model().is_builtin("property") + if checker.semantic().is_builtin("property") && args .args .iter() diff --git a/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs b/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs index dce30a8956..5a3573952d 100644 --- a/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs +++ b/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs @@ -176,7 +176,7 @@ impl<'a, 'b> StatementVisitor<'b> for InnerForWithAssignTargetsVisitor<'a, 'b> { // Check for single-target assignments which are of the // form `x = cast(..., x)`. if targets.first().map_or(false, |target| { - assignment_is_cast_expr(self.context, value, target) + assignment_is_cast_expr(value, target, self.context) }) { return; } @@ -236,7 +236,7 @@ impl<'a, 'b> StatementVisitor<'b> for InnerForWithAssignTargetsVisitor<'a, 'b> { /// /// x = cast(int, x) /// ``` -fn assignment_is_cast_expr(model: &SemanticModel, value: &Expr, target: &Expr) -> bool { +fn assignment_is_cast_expr(value: &Expr, target: &Expr, semantic: &SemanticModel) -> bool { let Expr::Call(ast::ExprCall { func, args, .. }) = value else { return false; }; @@ -252,7 +252,7 @@ fn assignment_is_cast_expr(model: &SemanticModel, value: &Expr, target: &Expr) - if arg_id != target_id { return false; } - model.match_typing_expr(func, "cast") + semantic.match_typing_expr(func, "cast") } fn assignment_targets_from_expr<'a, U>( @@ -345,7 +345,7 @@ pub(crate) fn redefined_loop_name<'a, 'b>(checker: &'a mut Checker<'b>, node: &N }) .collect(); let mut visitor = InnerForWithAssignTargetsVisitor { - context: checker.semantic_model(), + context: checker.semantic(), dummy_variable_rgx: &checker.settings.dummy_variable_rgx, assignment_targets: vec![], }; @@ -365,7 +365,7 @@ pub(crate) fn redefined_loop_name<'a, 'b>(checker: &'a mut Checker<'b>, node: &N }) .collect(); let mut visitor = InnerForWithAssignTargetsVisitor { - context: checker.semantic_model(), + context: checker.semantic(), dummy_variable_rgx: &checker.settings.dummy_variable_rgx, assignment_targets: vec![], }; diff --git a/crates/ruff/src/rules/pylint/rules/repeated_isinstance_calls.rs b/crates/ruff/src/rules/pylint/rules/repeated_isinstance_calls.rs index 031eee89d2..0e578360af 100644 --- a/crates/ruff/src/rules/pylint/rules/repeated_isinstance_calls.rs +++ b/crates/ruff/src/rules/pylint/rules/repeated_isinstance_calls.rs @@ -79,7 +79,7 @@ pub(crate) fn repeated_isinstance_calls( let [obj, types] = &args[..] else { continue; }; - if !checker.semantic_model().is_builtin("isinstance") { + if !checker.semantic().is_builtin("isinstance") { return; } let (num_calls, matches) = obj_to_types diff --git a/crates/ruff/src/rules/pylint/rules/return_in_init.rs b/crates/ruff/src/rules/pylint/rules/return_in_init.rs index 0bd7cd7758..dbffa82463 100644 --- a/crates/ruff/src/rules/pylint/rules/return_in_init.rs +++ b/crates/ruff/src/rules/pylint/rules/return_in_init.rs @@ -62,7 +62,7 @@ pub(crate) fn return_in_init(checker: &mut Checker, stmt: &Stmt) { } } - if in_dunder_init(checker.semantic_model(), checker.settings) { + if in_dunder_init(checker.semantic(), checker.settings) { checker .diagnostics .push(Diagnostic::new(ReturnInInit, stmt.range())); diff --git a/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs b/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs index e9dccdd1cd..dedac5503e 100644 --- a/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs +++ b/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs @@ -65,7 +65,7 @@ pub(crate) fn sys_exit_alias(checker: &mut Checker, func: &Expr) { if id != name { continue; } - if !checker.semantic_model().is_builtin(name) { + if !checker.semantic().is_builtin(name) { continue; } let mut diagnostic = Diagnostic::new( @@ -79,7 +79,7 @@ pub(crate) fn sys_exit_alias(checker: &mut Checker, func: &Expr) { let (import_edit, binding) = checker.importer.get_or_import_symbol( &ImportRequest::import("sys", "exit"), func.start(), - checker.semantic_model(), + checker.semantic(), )?; let reference_edit = Edit::range_replacement(binding, func.range()); #[allow(deprecated)] 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 e22a2a2655..2790999a22 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 @@ -145,7 +145,7 @@ pub(crate) fn unexpected_special_method_signature( args: &Arguments, locator: &Locator, ) { - if !checker.semantic_model().scope().kind.is_class() { + if !checker.semantic().scope().kind.is_class() { return; } @@ -163,7 +163,7 @@ pub(crate) fn unexpected_special_method_signature( let optional_params = args.defaults.len(); let mandatory_params = actual_params - optional_params; - let Some(expected_params) = ExpectedParams::from_method(name, is_staticmethod(checker.semantic_model(), decorator_list)) 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/yield_from_in_async_function.rs b/crates/ruff/src/rules/pylint/rules/yield_from_in_async_function.rs index 4895faa12a..9b329b3a19 100644 --- a/crates/ruff/src/rules/pylint/rules/yield_from_in_async_function.rs +++ b/crates/ruff/src/rules/pylint/rules/yield_from_in_async_function.rs @@ -38,7 +38,7 @@ impl Violation for YieldFromInAsyncFunction { /// PLE1700 pub(crate) fn yield_from_in_async_function(checker: &mut Checker, expr: &ExprYieldFrom) { - let scope = checker.semantic_model().scope(); + let scope = checker.semantic().scope(); if scope.kind.is_async_function() { checker .diagnostics diff --git a/crates/ruff/src/rules/pylint/rules/yield_in_init.rs b/crates/ruff/src/rules/pylint/rules/yield_in_init.rs index 9ee87e70a7..2fabc842cc 100644 --- a/crates/ruff/src/rules/pylint/rules/yield_in_init.rs +++ b/crates/ruff/src/rules/pylint/rules/yield_in_init.rs @@ -39,7 +39,7 @@ impl Violation for YieldInInit { /// PLE0100 pub(crate) fn yield_in_init(checker: &mut Checker, expr: &Expr) { - if in_dunder_init(checker.semantic_model(), checker.settings) { + if in_dunder_init(checker.semantic(), checker.settings) { checker .diagnostics .push(Diagnostic::new(YieldInInit, expr.range())); 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 b2fac6b580..08d348a065 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 @@ -36,9 +36,9 @@ impl Violation for ConvertNamedTupleFunctionalToClass { /// Return the typename, args, keywords, and base class. fn match_named_tuple_assign<'a>( - model: &SemanticModel, targets: &'a [Expr], value: &'a Expr, + semantic: &SemanticModel, ) -> Option<(&'a str, &'a [Expr], &'a [Keyword], &'a Expr)> { let target = targets.get(0)?; let Expr::Name(ast::ExprName { id: typename, .. }) = target else { @@ -52,7 +52,7 @@ fn match_named_tuple_assign<'a>( }) = value else { return None; }; - if !model.resolve_call_path(func).map_or(false, |call_path| { + if !semantic.resolve_call_path(func).map_or(false, |call_path| { call_path.as_slice() == ["typing", "NamedTuple"] }) { return None; @@ -189,7 +189,7 @@ pub(crate) fn convert_named_tuple_functional_to_class( value: &Expr, ) { let Some((typename, args, keywords, base_class)) = - match_named_tuple_assign(checker.semantic_model(), targets, value) else + match_named_tuple_assign(targets, value, checker.semantic()) else { return; }; diff --git a/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs b/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs index adaf6b033b..08ef961118 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs @@ -36,9 +36,9 @@ impl Violation for ConvertTypedDictFunctionalToClass { /// Return the class name, arguments, keywords and base class for a `TypedDict` /// assignment. fn match_typed_dict_assign<'a>( - model: &SemanticModel, targets: &'a [Expr], value: &'a Expr, + semantic: &SemanticModel, ) -> Option<(&'a str, &'a [Expr], &'a [Keyword], &'a Expr)> { let target = targets.get(0)?; let Expr::Name(ast::ExprName { id: class_name, .. }) = target else { @@ -52,7 +52,7 @@ fn match_typed_dict_assign<'a>( }) = value else { return None; }; - if !model.resolve_call_path(func).map_or(false, |call_path| { + if !semantic.resolve_call_path(func).map_or(false, |call_path| { call_path.as_slice() == ["typing", "TypedDict"] }) { return None; @@ -242,7 +242,7 @@ pub(crate) fn convert_typed_dict_functional_to_class( value: &Expr, ) { let Some((class_name, args, keywords, base_class)) = - match_typed_dict_assign(checker.semantic_model(), targets, value) else + match_typed_dict_assign(targets, value, checker.semantic()) else { return; }; diff --git a/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs index 16aa05a145..860e99bd79 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs @@ -28,7 +28,7 @@ impl Violation for DatetimeTimezoneUTC { /// UP017 pub(crate) fn datetime_utc_alias(checker: &mut Checker, expr: &Expr) { if checker - .semantic_model() + .semantic() .resolve_call_path(expr) .map_or(false, |call_path| { call_path.as_slice() == ["datetime", "timezone", "utc"] 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 9ecf931f2c..c19377c187 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 @@ -38,7 +38,7 @@ pub(crate) fn lru_cache_with_maxsize_none(checker: &mut Checker, decorator_list: if args.is_empty() && keywords.len() == 1 && checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["functools", "lru_cache"] @@ -68,7 +68,7 @@ pub(crate) fn lru_cache_with_maxsize_none(checker: &mut Checker, decorator_list: let (import_edit, binding) = checker.importer.get_or_import_symbol( &ImportRequest::import("functools", "cache"), decorator.start(), - checker.semantic_model(), + checker.semantic(), )?; let reference_edit = Edit::range_replacement(binding, decorator.expression.range()); 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 0f482afb58..8073666dcc 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 @@ -37,7 +37,7 @@ pub(crate) fn lru_cache_without_parameters(checker: &mut Checker, decorator_list if args.is_empty() && keywords.is_empty() && checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["functools", "lru_cache"] diff --git a/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs b/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs index a7849d2c4f..aac0f59c0d 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs @@ -62,11 +62,11 @@ pub(crate) fn native_literals( } // There's no way to rewrite, e.g., `f"{f'{str()}'}"` within a nested f-string. - if checker.semantic_model().in_nested_f_string() { + if checker.semantic().in_nested_f_string() { return; } - if (id == "str" || id == "bytes") && checker.semantic_model().is_builtin(id) { + 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 diff --git a/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs index a49207bbb7..8577c0185e 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs @@ -25,13 +25,13 @@ impl Violation for OpenAlias { /// UP020 pub(crate) fn open_alias(checker: &mut Checker, expr: &Expr, func: &Expr) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| call_path.as_slice() == ["io", "open"]) { let mut diagnostic = Diagnostic::new(OpenAlias, expr.range()); if checker.patch(diagnostic.kind.rule()) { - if checker.semantic_model().is_available("open") { + if checker.semantic().is_available("open") { diagnostic.set_fix(Fix::suggested(Edit::range_replacement( "open".to_string(), func.range(), diff --git a/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs index 890dd665ed..81d995875b 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs @@ -39,8 +39,8 @@ const ALIASES: &[(&str, &str)] = &[ ]; /// Return `true` if an [`Expr`] is an alias of `OSError`. -fn is_alias(model: &SemanticModel, expr: &Expr) -> bool { - model.resolve_call_path(expr).map_or(false, |call_path| { +fn is_alias(expr: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(expr).map_or(false, |call_path| { ALIASES .iter() .any(|(module, member)| call_path.as_slice() == [*module, *member]) @@ -48,8 +48,8 @@ fn is_alias(model: &SemanticModel, expr: &Expr) -> bool { } /// Return `true` if an [`Expr`] is `OSError`. -fn is_os_error(model: &SemanticModel, expr: &Expr) -> bool { - model +fn is_os_error(expr: &Expr, semantic: &SemanticModel) -> bool { + semantic .resolve_call_path(expr) .map_or(false, |call_path| call_path.as_slice() == ["", "OSError"]) } @@ -93,10 +93,7 @@ fn tuple_diagnostic(checker: &mut Checker, target: &Expr, aliases: &[&Expr]) { .collect(); // If `OSError` itself isn't already in the tuple, add it. - if elts - .iter() - .all(|elt| !is_os_error(checker.semantic_model(), elt)) - { + if elts.iter().all(|elt| !is_os_error(elt, checker.semantic())) { let node = ast::ExprName { id: "OSError".into(), ctx: ExprContext::Load, @@ -136,7 +133,7 @@ pub(crate) fn os_error_alias_handlers(checker: &mut Checker, handlers: &[Excepth }; match expr.as_ref() { Expr::Name(_) | Expr::Attribute(_) => { - if is_alias(checker.semantic_model(), expr) { + if is_alias(expr, checker.semantic()) { atom_diagnostic(checker, expr); } } @@ -144,7 +141,7 @@ pub(crate) fn os_error_alias_handlers(checker: &mut Checker, handlers: &[Excepth // List of aliases to replace with `OSError`. let mut aliases: Vec<&Expr> = vec![]; for elt in elts { - if is_alias(checker.semantic_model(), elt) { + if is_alias(elt, checker.semantic()) { aliases.push(elt); } } @@ -159,7 +156,7 @@ pub(crate) fn os_error_alias_handlers(checker: &mut Checker, handlers: &[Excepth /// UP024 pub(crate) fn os_error_alias_call(checker: &mut Checker, func: &Expr) { - if is_alias(checker.semantic_model(), func) { + if is_alias(func, checker.semantic()) { atom_diagnostic(checker, func); } } @@ -167,7 +164,7 @@ pub(crate) fn os_error_alias_call(checker: &mut Checker, func: &Expr) { /// UP024 pub(crate) fn os_error_alias_raise(checker: &mut Checker, expr: &Expr) { if matches!(expr, Expr::Name(_) | Expr::Attribute(_)) { - if is_alias(checker.semantic_model(), expr) { + if is_alias(expr, checker.semantic()) { atom_diagnostic(checker, expr); } } 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 96e8d3ac0a..4623ea9cfa 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs @@ -188,8 +188,8 @@ fn fix_py2_block( // Delete the entire statement. If this is an `elif`, know it's the only child // of its parent, so avoid passing in the parent at all. Otherwise, // `delete_stmt` will erroneously include a `pass`. - let stmt = checker.semantic_model().stmt(); - let parent = checker.semantic_model().stmt_parent(); + let stmt = checker.semantic().stmt(); + let parent = checker.semantic().stmt_parent(); let edit = delete_stmt( stmt, if matches!(block.leading_token.tok, StartTok::If) { parent } else { None }, @@ -336,7 +336,7 @@ pub(crate) fn outdated_version_block( }; if !checker - .semantic_model() + .semantic() .resolve_call_path(left) .map_or(false, |call_path| { call_path.as_slice() == ["sys", "version_info"] diff --git a/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs b/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs index 9dabf61d24..b65019e999 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs @@ -173,7 +173,7 @@ fn create_remove_param_fix(locator: &Locator, expr: &Expr, mode_param: &Expr) -> /// UP015 pub(crate) fn redundant_open_modes(checker: &mut Checker, expr: &Expr) { // If `open` has been rebound, skip this check entirely. - if !checker.semantic_model().is_builtin(OPEN_FUNC_NAME) { + if !checker.semantic().is_builtin(OPEN_FUNC_NAME) { return; } let (mode_param, keywords): (Option<&Expr>, Vec) = match_open(expr); 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 b1aee43ed4..64dc64b113 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs @@ -61,7 +61,7 @@ pub(crate) fn replace_stdout_stderr( keywords: &[Keyword], ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["subprocess", "run"] @@ -77,13 +77,13 @@ pub(crate) fn replace_stdout_stderr( // Verify that they're both set to `subprocess.PIPE`. if !checker - .semantic_model() + .semantic() .resolve_call_path(&stdout.value) .map_or(false, |call_path| { call_path.as_slice() == ["subprocess", "PIPE"] }) || !checker - .semantic_model() + .semantic() .resolve_call_path(&stderr.value) .map_or(false, |call_path| { call_path.as_slice() == ["subprocess", "PIPE"] diff --git a/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs b/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs index 23c0e49de0..e9ab4b80ae 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs @@ -25,7 +25,7 @@ impl AlwaysAutofixableViolation for ReplaceUniversalNewlines { /// UP021 pub(crate) fn replace_universal_newlines(checker: &mut Checker, func: &Expr, kwargs: &[Keyword]) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { call_path.as_slice() == ["subprocess", "run"] 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 63b4db3443..a7098a0eab 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 @@ -42,14 +42,14 @@ pub(crate) fn super_call_with_parameters( if !is_super_call_with_arguments(func, args) { return; } - let scope = checker.semantic_model().scope(); + let scope = checker.semantic().scope(); // Check: are we in a Function scope? if !scope.kind.is_any_function() { return; } - let mut parents = checker.semantic_model().parents(); + let mut parents = checker.semantic().parents(); // For a `super` invocation to be unnecessary, the first argument needs to match // the enclosing class, and the second argument needs to match the first 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 7f2c9a5964..c75fe15303 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs @@ -32,7 +32,7 @@ pub(crate) fn type_of_primitive(checker: &mut Checker, expr: &Expr, func: &Expr, return; } if !checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| call_path.as_slice() == ["", "type"]) { diff --git a/crates/ruff/src/rules/pyupgrade/rules/typing_text_str_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/typing_text_str_alias.rs index 5b1a867cca..7c1110689b 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/typing_text_str_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/typing_text_str_alias.rs @@ -23,7 +23,7 @@ impl AlwaysAutofixableViolation for TypingTextStrAlias { /// UP019 pub(crate) fn typing_text_str_alias(checker: &mut Checker, expr: &Expr) { if checker - .semantic_model() + .semantic() .resolve_call_path(expr) .map_or(false, |call_path| { call_path.as_slice() == ["typing", "Text"] diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs index b604684c1c..9353ad8c32 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs @@ -105,8 +105,8 @@ pub(crate) fn unnecessary_builtin_import( ); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { - let stmt = checker.semantic_model().stmt(); - let parent = checker.semantic_model().stmt_parent(); + let stmt = checker.semantic().stmt(); + let parent = checker.semantic().stmt_parent(); let unused_imports: Vec = unused_imports .iter() .map(|alias| format!("{module}.{}", alias.name)) diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs index 34cd3e5ca5..f3f1bbd609 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs @@ -88,8 +88,8 @@ pub(crate) fn unnecessary_future_import(checker: &mut Checker, stmt: &Stmt, name .iter() .map(|alias| format!("__future__.{}", alias.name)) .collect(); - let stmt = checker.semantic_model().stmt(); - let parent = checker.semantic_model().stmt_parent(); + let stmt = checker.semantic().stmt(); + let parent = checker.semantic().stmt_parent(); let edit = autofix::edits::remove_unused_imports( unused_imports.iter().map(String::as_str), stmt, diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep585_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep585_annotation.rs index 20e09a7528..5c8305e185 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep585_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep585_annotation.rs @@ -47,11 +47,11 @@ pub(crate) fn use_pep585_annotation( expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - if !checker.semantic_model().in_complex_string_type_definition() { + if !checker.semantic().in_complex_string_type_definition() { match replacement { ModuleMember::BuiltIn(name) => { // Built-in type, like `list`. - if checker.semantic_model().is_builtin(name) { + if checker.semantic().is_builtin(name) { diagnostic.set_fix(Fix::automatic(Edit::range_replacement( (*name).to_string(), expr.range(), @@ -64,7 +64,7 @@ pub(crate) fn use_pep585_annotation( let (import_edit, binding) = checker.importer.get_or_import_symbol( &ImportRequest::import_from(module, member), expr.start(), - checker.semantic_model(), + checker.semantic(), )?; let reference_edit = Edit::range_replacement(binding, expr.range()); Ok(Fix::suggested_edits(import_edit, [reference_edit])) diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs index 789e89b685..67589ca86e 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs @@ -58,8 +58,8 @@ pub(crate) fn use_pep604_annotation( operator: Pep604Operator, ) { // Avoid fixing forward references, or types not in an annotation. - let fixable = checker.semantic_model().in_type_definition() - && !checker.semantic_model().in_complex_string_type_definition(); + let fixable = checker.semantic().in_type_definition() + && !checker.semantic().in_complex_string_type_definition(); match operator { Pep604Operator::Optional => { let mut diagnostic = Diagnostic::new(NonPEP604Annotation, expr.range()); diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs index 89254e9145..c0f8d6d28b 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs @@ -74,7 +74,7 @@ pub(crate) fn use_pep604_isinstance( let Some(kind) = CallKind::from_name(id) else { return; }; - if !checker.semantic_model().is_builtin(id) { + if !checker.semantic().is_builtin(id) { return; }; if let Some(types) = args.get(1) { 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 783bc520c8..104384c246 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs @@ -46,8 +46,8 @@ pub(crate) fn useless_metaclass_type( let mut diagnostic = Diagnostic::new(UselessMetaclassType, stmt.range()); if checker.patch(diagnostic.kind.rule()) { - let stmt = checker.semantic_model().stmt(); - let parent = checker.semantic_model().stmt_parent(); + let stmt = checker.semantic().stmt(); + let parent = checker.semantic().stmt_parent(); let edit = autofix::edits::delete_stmt( stmt, parent, 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 6fa11e3a04..4c8788324b 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs @@ -38,7 +38,7 @@ pub(crate) fn useless_object_inheritance( if id != "object" { continue; } - if !checker.semantic_model().is_builtin("object") { + if !checker.semantic().is_builtin("object") { continue; } 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 c5b2196d53..ef2da7967b 100644 --- a/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs +++ b/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs @@ -131,7 +131,7 @@ fn concatenate_expressions(expr: &Expr) -> Option<(Expr, Type)> { pub(crate) fn collection_literal_concatenation(checker: &mut Checker, expr: &Expr) { // If the expression is already a child of an addition, we'll have analyzed it already. if matches!( - checker.semantic_model().expr_parent(), + checker.semantic().expr_parent(), Some(Expr::BinOp(ast::ExprBinOp { op: Operator::Add, .. 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 30046ab720..7561fc24ca 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 @@ -152,7 +152,7 @@ pub(crate) fn explicit_f_string_type_conversion( continue; }; - if !checker.semantic_model().is_builtin(id) { + if !checker.semantic().is_builtin(id) { continue; } diff --git a/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs b/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs index 54e0f88488..9aad4078e7 100644 --- a/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs +++ b/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs @@ -75,7 +75,7 @@ pub(crate) fn function_call_in_dataclass_default( checker: &mut Checker, class_def: &ast::StmtClassDef, ) { - if !is_dataclass(checker.semantic_model(), class_def) { + if !is_dataclass(class_def, checker.semantic()) { return; } @@ -95,9 +95,9 @@ pub(crate) fn function_call_in_dataclass_default( }) = statement { if let Expr::Call(ast::ExprCall { func, .. }) = expr.as_ref() { - if !is_class_var_annotation(checker.semantic_model(), annotation) - && !is_immutable_func(checker.semantic_model(), func, &extend_immutable_calls) - && !is_allowed_dataclass_function(checker.semantic_model(), func) + if !is_class_var_annotation(annotation, checker.semantic()) + && !is_immutable_func(func, checker.semantic(), &extend_immutable_calls) + && !is_allowed_dataclass_function(func, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( FunctionCallInDataclassDefaultArgument { diff --git a/crates/ruff/src/rules/ruff/rules/helpers.rs b/crates/ruff/src/rules/ruff/rules/helpers.rs index 751bf32326..75a9b240d7 100644 --- a/crates/ruff/src/rules/ruff/rules/helpers.rs +++ b/crates/ruff/src/rules/ruff/rules/helpers.rs @@ -17,8 +17,8 @@ pub(super) fn is_mutable_expr(expr: &Expr) -> bool { const ALLOWED_DATACLASS_SPECIFIC_FUNCTIONS: &[&[&str]] = &[&["dataclasses", "field"]]; -pub(super) fn is_allowed_dataclass_function(model: &SemanticModel, func: &Expr) -> bool { - model.resolve_call_path(func).map_or(false, |call_path| { +pub(super) fn is_allowed_dataclass_function(func: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(func).map_or(false, |call_path| { ALLOWED_DATACLASS_SPECIFIC_FUNCTIONS .iter() .any(|target| call_path.as_slice() == *target) @@ -26,17 +26,17 @@ pub(super) fn is_allowed_dataclass_function(model: &SemanticModel, func: &Expr) } /// Returns `true` if the given [`Expr`] is a `typing.ClassVar` annotation. -pub(super) fn is_class_var_annotation(model: &SemanticModel, annotation: &Expr) -> bool { +pub(super) fn is_class_var_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool { let Expr::Subscript(ast::ExprSubscript { value, .. }) = &annotation else { return false; }; - model.match_typing_expr(value, "ClassVar") + semantic.match_typing_expr(value, "ClassVar") } /// Returns `true` if the given class is a dataclass. -pub(super) fn is_dataclass(model: &SemanticModel, class_def: &ast::StmtClassDef) -> bool { +pub(super) fn is_dataclass(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool { class_def.decorator_list.iter().any(|decorator| { - model + semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { call_path.as_slice() == ["dataclasses", "dataclass"] diff --git a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs index 0b00b3adbf..f032db7101 100644 --- a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs +++ b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs @@ -145,20 +145,20 @@ enum TypingTarget<'a> { } impl<'a> TypingTarget<'a> { - fn try_from_expr(model: &SemanticModel, expr: &'a Expr) -> Option { + fn try_from_expr(expr: &'a Expr, semantic: &SemanticModel) -> Option { match expr { Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { - if model.match_typing_expr(value, "Optional") { + if semantic.match_typing_expr(value, "Optional") { return Some(TypingTarget::Optional); } let Expr::Tuple(ast::ExprTuple { elts: elements, .. }) = slice.as_ref() else{ return None; }; - if model.match_typing_expr(value, "Literal") { + if semantic.match_typing_expr(value, "Literal") { Some(TypingTarget::Literal(elements.iter().collect())) - } else if model.match_typing_expr(value, "Union") { + } else if semantic.match_typing_expr(value, "Union") { Some(TypingTarget::Union(elements.iter().collect())) - } else if model.match_typing_expr(value, "Annotated") { + } else if semantic.match_typing_expr(value, "Annotated") { elements.first().map(TypingTarget::Annotated) } else { None @@ -171,8 +171,8 @@ impl<'a> TypingTarget<'a> { value: Constant::None, .. }) => Some(TypingTarget::None), - _ => model.resolve_call_path(expr).and_then(|call_path| { - if model.match_typing_call_path(&call_path, "Any") { + _ => semantic.resolve_call_path(expr).and_then(|call_path| { + if semantic.match_typing_call_path(&call_path, "Any") { Some(TypingTarget::Any) } else if matches!(call_path.as_slice(), ["" | "builtins", "object"]) { Some(TypingTarget::Object) @@ -184,40 +184,40 @@ impl<'a> TypingTarget<'a> { } /// Check if the [`TypingTarget`] explicitly allows `None`. - fn contains_none(&self, model: &SemanticModel) -> bool { + fn contains_none(&self, semantic: &SemanticModel) -> bool { match self { TypingTarget::None | TypingTarget::Optional | TypingTarget::Any | TypingTarget::Object => true, TypingTarget::Literal(elements) => elements.iter().any(|element| { - let Some(new_target) = TypingTarget::try_from_expr(model, element) else { + let Some(new_target) = TypingTarget::try_from_expr(element, semantic) 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(model), + TypingTarget::Literal(_) => new_target.contains_none(semantic), _ => false, } }), TypingTarget::Union(elements) => elements.iter().any(|element| { - let Some(new_target) = TypingTarget::try_from_expr(model, element) else { + let Some(new_target) = TypingTarget::try_from_expr(element, semantic) else { return false; }; match new_target { TypingTarget::None => true, - _ => new_target.contains_none(model), + _ => new_target.contains_none(semantic), } }), TypingTarget::Annotated(element) => { - let Some(new_target) = TypingTarget::try_from_expr(model, element) else { + let Some(new_target) = TypingTarget::try_from_expr(element, semantic) else { return false; }; match new_target { TypingTarget::None => true, - _ => new_target.contains_none(model), + _ => new_target.contains_none(semantic), } } } @@ -232,10 +232,10 @@ impl<'a> TypingTarget<'a> { /// /// This function assumes that the annotation is a valid typing annotation expression. fn type_hint_explicitly_allows_none<'a>( - model: &SemanticModel, annotation: &'a Expr, + semantic: &SemanticModel, ) -> Option<&'a Expr> { - let Some(target) = TypingTarget::try_from_expr(model, annotation) else { + let Some(target) = TypingTarget::try_from_expr(annotation, semantic) else { return Some(annotation); }; match target { @@ -245,9 +245,9 @@ fn type_hint_explicitly_allows_none<'a>( // return the inner type if it doesn't allow `None`. If `Annotated` // is found nested inside another type, then the outer type should // be returned. - TypingTarget::Annotated(expr) => type_hint_explicitly_allows_none(model, expr), + TypingTarget::Annotated(expr) => type_hint_explicitly_allows_none(expr, semantic), _ => { - if target.contains_none(model) { + if target.contains_none(semantic) { None } else { Some(annotation) @@ -280,7 +280,7 @@ fn generate_fix(checker: &Checker, conversion_type: ConversionType, expr: &Expr) let (import_edit, binding) = checker.importer.get_or_import_symbol( &ImportRequest::import_from("typing", "Optional"), expr.start(), - checker.semantic_model(), + checker.semantic(), )?; let new_expr = Expr::Subscript(ast::ExprSubscript { range: TextRange::default(), @@ -329,7 +329,7 @@ pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) { let Some(annotation) = &arg.annotation else { continue }; - let Some(expr) = type_hint_explicitly_allows_none(checker.semantic_model(), annotation) else { + let Some(expr) = type_hint_explicitly_allows_none(annotation, checker.semantic()) else { continue; }; let conversion_type = checker.settings.target_version.into(); diff --git a/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs b/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs index f126251352..113c3afc96 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs @@ -53,9 +53,9 @@ pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::Stmt .. }) => { if is_mutable_expr(value) - && !is_class_var_annotation(checker.semantic_model(), annotation) - && !is_immutable_annotation(checker.semantic_model(), annotation) - && !is_dataclass(checker.semantic_model(), class_def) + && !is_class_var_annotation(annotation, checker.semantic()) + && !is_immutable_annotation(annotation, checker.semantic()) + && !is_dataclass(class_def, checker.semantic()) { checker .diagnostics diff --git a/crates/ruff/src/rules/ruff/rules/mutable_dataclass_default.rs b/crates/ruff/src/rules/ruff/rules/mutable_dataclass_default.rs index 63d4b6d397..ac30e0e214 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_dataclass_default.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_dataclass_default.rs @@ -63,7 +63,7 @@ impl Violation for MutableDataclassDefault { /// RUF008 pub(crate) fn mutable_dataclass_default(checker: &mut Checker, class_def: &ast::StmtClassDef) { - if !is_dataclass(checker.semantic_model(), class_def) { + if !is_dataclass(class_def, checker.semantic()) { return; } @@ -75,8 +75,8 @@ pub(crate) fn mutable_dataclass_default(checker: &mut Checker, class_def: &ast:: }) = statement { if is_mutable_expr(value) - && !is_class_var_annotation(checker.semantic_model(), annotation) - && !is_immutable_annotation(checker.semantic_model(), annotation) + && !is_class_var_annotation(annotation, checker.semantic()) + && !is_immutable_annotation(annotation, checker.semantic()) { checker .diagnostics 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 ee3c5b0d0c..3e6f60fd54 100644 --- a/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs +++ b/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs @@ -99,7 +99,7 @@ pub(crate) fn pairwise_over_zipped(checker: &mut Checker, func: &Expr, args: &[E } // Require the function to be the builtin `zip`. - if !(id == "zip" && checker.semantic_model().is_builtin(id)) { + if !(id == "zip" && checker.semantic().is_builtin(id)) { return; } diff --git a/crates/ruff/src/rules/tryceratops/helpers.rs b/crates/ruff/src/rules/tryceratops/helpers.rs index 2581f86e0c..d4a5655893 100644 --- a/crates/ruff/src/rules/tryceratops/helpers.rs +++ b/crates/ruff/src/rules/tryceratops/helpers.rs @@ -7,14 +7,14 @@ use ruff_python_semantic::SemanticModel; /// Collect `logging`-like calls from an AST. pub(super) struct LoggerCandidateVisitor<'a, 'b> { - semantic_model: &'a SemanticModel<'b>, + semantic: &'a SemanticModel<'b>, pub(super) calls: Vec<&'b ast::ExprCall>, } impl<'a, 'b> LoggerCandidateVisitor<'a, 'b> { - pub(super) fn new(semantic_model: &'a SemanticModel<'b>) -> Self { + pub(super) fn new(semantic: &'a SemanticModel<'b>) -> Self { LoggerCandidateVisitor { - semantic_model, + semantic, calls: Vec::new(), } } @@ -23,7 +23,7 @@ impl<'a, 'b> LoggerCandidateVisitor<'a, 'b> { impl<'a, 'b> Visitor<'b> for LoggerCandidateVisitor<'a, 'b> { fn visit_expr(&mut self, expr: &'b Expr) { if let Expr::Call(call) = expr { - if logging::is_logger_candidate(&call.func, self.semantic_model) { + if logging::is_logger_candidate(&call.func, self.semantic) { self.calls.push(call); } } diff --git a/crates/ruff/src/rules/tryceratops/rules/error_instead_of_exception.rs b/crates/ruff/src/rules/tryceratops/rules/error_instead_of_exception.rs index 09af41b601..a335c103f4 100644 --- a/crates/ruff/src/rules/tryceratops/rules/error_instead_of_exception.rs +++ b/crates/ruff/src/rules/tryceratops/rules/error_instead_of_exception.rs @@ -58,14 +58,14 @@ pub(crate) fn error_instead_of_exception(checker: &mut Checker, handlers: &[Exce for handler in handlers { let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { body, .. }) = handler; let calls = { - let mut visitor = LoggerCandidateVisitor::new(checker.semantic_model()); + let mut visitor = LoggerCandidateVisitor::new(checker.semantic()); visitor.visit_body(body); visitor.calls }; for expr in calls { if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = expr.func.as_ref() { if attr == "error" { - if exc_info(&expr.keywords, checker.semantic_model()).is_none() { + if exc_info(&expr.keywords, checker.semantic()).is_none() { checker .diagnostics .push(Diagnostic::new(ErrorInsteadOfException, expr.range())); diff --git a/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_class.rs b/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_class.rs index 46024b2db3..2156550cd0 100644 --- a/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_class.rs +++ b/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_class.rs @@ -63,7 +63,7 @@ impl Violation for RaiseVanillaClass { /// TRY002 pub(crate) fn raise_vanilla_class(checker: &mut Checker, expr: &Expr) { if checker - .semantic_model() + .semantic() .resolve_call_path(if let Expr::Call(ast::ExprCall { func, .. }) = expr { func } else { diff --git a/crates/ruff/src/rules/tryceratops/rules/try_consider_else.rs b/crates/ruff/src/rules/tryceratops/rules/try_consider_else.rs index 91e7624f94..ca866ff95c 100644 --- a/crates/ruff/src/rules/tryceratops/rules/try_consider_else.rs +++ b/crates/ruff/src/rules/tryceratops/rules/try_consider_else.rs @@ -66,7 +66,7 @@ pub(crate) fn try_consider_else( if let Some(stmt) = body.last() { if let Stmt::Return(ast::StmtReturn { value, range: _ }) = stmt { if let Some(value) = value { - if contains_effect(value, |id| checker.semantic_model().is_builtin(id)) { + if contains_effect(value, |id| checker.semantic().is_builtin(id)) { return; } } diff --git a/crates/ruff/src/rules/tryceratops/rules/type_check_without_type_error.rs b/crates/ruff/src/rules/tryceratops/rules/type_check_without_type_error.rs index f87d7b010a..a909a836a6 100644 --- a/crates/ruff/src/rules/tryceratops/rules/type_check_without_type_error.rs +++ b/crates/ruff/src/rules/tryceratops/rules/type_check_without_type_error.rs @@ -77,7 +77,7 @@ fn has_control_flow(stmt: &Stmt) -> bool { /// Returns `true` if an [`Expr`] is a call to check types. fn check_type_check_call(checker: &mut Checker, call: &Expr) -> bool { checker - .semantic_model() + .semantic() .resolve_call_path(call) .map_or(false, |call_path| { call_path.as_slice() == ["", "isinstance"] @@ -101,7 +101,7 @@ fn check_type_check_test(checker: &mut Checker, test: &Expr) -> bool { /// Returns `true` if `exc` is a reference to a builtin exception. fn is_builtin_exception(checker: &mut Checker, exc: &Expr) -> bool { return checker - .semantic_model() + .semantic() .resolve_call_path(exc) .map_or(false, |call_path| { [ diff --git a/crates/ruff/src/rules/tryceratops/rules/verbose_log_message.rs b/crates/ruff/src/rules/tryceratops/rules/verbose_log_message.rs index dd1dd7d34c..7153fe800a 100644 --- a/crates/ruff/src/rules/tryceratops/rules/verbose_log_message.rs +++ b/crates/ruff/src/rules/tryceratops/rules/verbose_log_message.rs @@ -67,7 +67,7 @@ pub(crate) fn verbose_log_message(checker: &mut Checker, handlers: &[Excepthandl // Find all calls to `logging.exception`. let calls = { - let mut visitor = LoggerCandidateVisitor::new(checker.semantic_model()); + let mut visitor = LoggerCandidateVisitor::new(checker.semantic()); visitor.visit_body(body); visitor.calls }; diff --git a/crates/ruff_python_semantic/src/analyze/function_type.rs b/crates/ruff_python_semantic/src/analyze/function_type.rs index 89f68a78ff..1a9851cb3f 100644 --- a/crates/ruff_python_semantic/src/analyze/function_type.rs +++ b/crates/ruff_python_semantic/src/analyze/function_type.rs @@ -19,10 +19,10 @@ pub enum FunctionType { /// Classify a function based on its scope, name, and decorators. pub fn classify( - model: &SemanticModel, - scope: &Scope, name: &str, decorator_list: &[Decorator], + scope: &Scope, + semantic: &SemanticModel, classmethod_decorators: &[String], staticmethod_decorators: &[String], ) -> FunctionType { @@ -32,7 +32,7 @@ pub fn classify( if decorator_list.iter().any(|decorator| { // The method is decorated with a static method decorator (like // `@staticmethod`). - model + semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { call_path.as_slice() == ["", "staticmethod"] @@ -46,7 +46,7 @@ pub fn classify( // Special-case class method, like `__new__`. || scope.bases.iter().any(|expr| { // The class itself extends a known metaclass, so all methods are class methods. - model.resolve_call_path(map_callable(expr)).map_or(false, |call_path| { + semantic.resolve_call_path(map_callable(expr)).map_or(false, |call_path| { METACLASS_BASES .iter() .any(|(module, member)| call_path.as_slice() == [*module, *member]) @@ -54,7 +54,7 @@ pub fn classify( }) || decorator_list.iter().any(|decorator| { // The method is decorated with a class method decorator (like `@classmethod`). - model.resolve_call_path(map_callable(&decorator.expression)).map_or(false, |call_path| { + semantic.resolve_call_path(map_callable(&decorator.expression)).map_or(false, |call_path| { call_path.as_slice() == ["", "classmethod"] || classmethod_decorators .iter() diff --git a/crates/ruff_python_semantic/src/analyze/logging.rs b/crates/ruff_python_semantic/src/analyze/logging.rs index 9dd4079ed6..1976565aa0 100644 --- a/crates/ruff_python_semantic/src/analyze/logging.rs +++ b/crates/ruff_python_semantic/src/analyze/logging.rs @@ -17,9 +17,9 @@ use crate::model::SemanticModel; /// # This is detected to be a logger candidate /// bar.error() /// ``` -pub fn is_logger_candidate(func: &Expr, model: &SemanticModel) -> bool { +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) = model.resolve_call_path(value) { + 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"] { Some(call_path) } else { @@ -41,7 +41,7 @@ pub fn is_logger_candidate(func: &Expr, model: &SemanticModel) -> bool { /// If the keywords to a logging call contain `exc_info=True` or `exc_info=sys.exc_info()`, /// return the `Keyword` for `exc_info`. -pub fn exc_info<'a>(keywords: &'a [Keyword], model: &SemanticModel) -> Option<&'a Keyword> { +pub fn exc_info<'a>(keywords: &'a [Keyword], semantic: &SemanticModel) -> Option<&'a Keyword> { let exc_info = find_keyword(keywords, "exc_info")?; // Ex) `logging.error("...", exc_info=True)` @@ -57,7 +57,7 @@ pub fn exc_info<'a>(keywords: &'a [Keyword], model: &SemanticModel) -> Option<&' // Ex) `logging.error("...", exc_info=sys.exc_info())` if let Expr::Call(ast::ExprCall { func, .. }) = &exc_info.value { - if model.resolve_call_path(func).map_or(false, |call_path| { + if semantic.resolve_call_path(func).map_or(false, |call_path| { call_path.as_slice() == ["sys", "exc_info"] }) { return Some(exc_info); diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 694e97e650..58585e425b 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -29,7 +29,7 @@ pub enum SubscriptKind { pub fn match_annotated_subscript<'a>( expr: &Expr, - semantic_model: &SemanticModel, + semantic: &SemanticModel, typing_modules: impl Iterator, extend_generics: &[String], ) -> Option { @@ -37,39 +37,37 @@ pub fn match_annotated_subscript<'a>( return None; } - semantic_model - .resolve_call_path(expr) - .and_then(|call_path| { - if SUBSCRIPTS.contains(&call_path.as_slice()) - || extend_generics - .iter() - .map(|target| from_qualified_name(target)) - .any(|target| call_path == target) - { - return Some(SubscriptKind::AnnotatedSubscript); - } - if PEP_593_SUBSCRIPTS.contains(&call_path.as_slice()) { - return Some(SubscriptKind::PEP593AnnotatedSubscript); - } + semantic.resolve_call_path(expr).and_then(|call_path| { + if SUBSCRIPTS.contains(&call_path.as_slice()) + || extend_generics + .iter() + .map(|target| from_qualified_name(target)) + .any(|target| call_path == target) + { + return Some(SubscriptKind::AnnotatedSubscript); + } + if PEP_593_SUBSCRIPTS.contains(&call_path.as_slice()) { + return Some(SubscriptKind::PEP593AnnotatedSubscript); + } - for module in typing_modules { - let module_call_path: CallPath = from_unqualified_name(module); - if call_path.starts_with(&module_call_path) { - for subscript in SUBSCRIPTS.iter() { - if call_path.last() == subscript.last() { - return Some(SubscriptKind::AnnotatedSubscript); - } + for module in typing_modules { + let module_call_path: CallPath = from_unqualified_name(module); + if call_path.starts_with(&module_call_path) { + for subscript in SUBSCRIPTS.iter() { + if call_path.last() == subscript.last() { + return Some(SubscriptKind::AnnotatedSubscript); } - for subscript in PEP_593_SUBSCRIPTS.iter() { - if call_path.last() == subscript.last() { - return Some(SubscriptKind::PEP593AnnotatedSubscript); - } + } + for subscript in PEP_593_SUBSCRIPTS.iter() { + if call_path.last() == subscript.last() { + return Some(SubscriptKind::PEP593AnnotatedSubscript); } } } + } - None - }) + None + }) } #[derive(Debug, Clone, Eq, PartialEq)] @@ -91,32 +89,30 @@ impl std::fmt::Display for ModuleMember { /// Returns the PEP 585 standard library generic variant for a `typing` module reference, if such /// a variant exists. -pub fn to_pep585_generic(expr: &Expr, semantic_model: &SemanticModel) -> Option { - semantic_model - .resolve_call_path(expr) - .and_then(|call_path| { - let [module, name] = call_path.as_slice() else { +pub fn to_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> Option { + semantic.resolve_call_path(expr).and_then(|call_path| { + let [module, name] = call_path.as_slice() else { return None; }; - PEP_585_GENERICS.iter().find_map( - |((from_module, from_member), (to_module, to_member))| { - if module == from_module && name == from_member { - if to_module.is_empty() { - Some(ModuleMember::BuiltIn(to_member)) - } else { - Some(ModuleMember::Member(to_module, to_member)) - } + PEP_585_GENERICS + .iter() + .find_map(|((from_module, from_member), (to_module, to_member))| { + if module == from_module && name == from_member { + if to_module.is_empty() { + Some(ModuleMember::BuiltIn(to_member)) } else { - None + Some(ModuleMember::Member(to_module, to_member)) } - }, - ) - }) + } else { + None + } + }) + }) } /// Return whether a given expression uses a PEP 585 standard library generic. -pub fn is_pep585_generic(expr: &Expr, model: &SemanticModel) -> bool { - if let Some(call_path) = model.resolve_call_path(expr) { +pub fn is_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> bool { + if let Some(call_path) = semantic.resolve_call_path(expr) { let [module, name] = call_path.as_slice() else { return false; }; @@ -141,7 +137,7 @@ pub enum Pep604Operator { pub fn to_pep604_operator( value: &Expr, slice: &Expr, - semantic_model: &SemanticModel, + semantic: &SemanticModel, ) -> Option { /// Returns `true` if any argument in the slice is a string. fn any_arg_is_str(slice: &Expr) -> bool { @@ -161,13 +157,13 @@ pub fn to_pep604_operator( return None; } - semantic_model + semantic .resolve_call_path(value) .as_ref() .and_then(|call_path| { - if semantic_model.match_typing_call_path(call_path, "Optional") { + if semantic.match_typing_call_path(call_path, "Optional") { Some(Pep604Operator::Optional) - } else if semantic_model.match_typing_call_path(call_path, "Union") { + } else if semantic.match_typing_call_path(call_path, "Union") { Some(Pep604Operator::Union) } else { None @@ -177,19 +173,17 @@ pub fn to_pep604_operator( /// Return `true` if `Expr` represents a reference to a type annotation that resolves to an /// immutable type. -pub fn is_immutable_annotation(semantic_model: &SemanticModel, expr: &Expr) -> bool { +pub fn is_immutable_annotation(expr: &Expr, semantic: &SemanticModel) -> bool { match expr { Expr::Name(_) | Expr::Attribute(_) => { - semantic_model - .resolve_call_path(expr) - .map_or(false, |call_path| { - IMMUTABLE_TYPES - .iter() - .chain(IMMUTABLE_GENERIC_TYPES) - .any(|target| call_path.as_slice() == *target) - }) + semantic.resolve_call_path(expr).map_or(false, |call_path| { + IMMUTABLE_TYPES + .iter() + .chain(IMMUTABLE_GENERIC_TYPES) + .any(|target| call_path.as_slice() == *target) + }) } - Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => semantic_model + Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => semantic .resolve_call_path(value) .map_or(false, |call_path| { if IMMUTABLE_GENERIC_TYPES @@ -200,16 +194,16 @@ pub fn is_immutable_annotation(semantic_model: &SemanticModel, expr: &Expr) -> b } else if call_path.as_slice() == ["typing", "Union"] { if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() { elts.iter() - .all(|elt| is_immutable_annotation(semantic_model, elt)) + .all(|elt| is_immutable_annotation(elt, semantic)) } else { false } } else if call_path.as_slice() == ["typing", "Optional"] { - is_immutable_annotation(semantic_model, slice) + is_immutable_annotation(slice, semantic) } else if call_path.as_slice() == ["typing", "Annotated"] { if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() { elts.first() - .map_or(false, |elt| is_immutable_annotation(semantic_model, elt)) + .map_or(false, |elt| is_immutable_annotation(elt, semantic)) } else { false } @@ -222,10 +216,7 @@ pub fn is_immutable_annotation(semantic_model: &SemanticModel, expr: &Expr) -> b op: Operator::BitOr, right, range: _range, - }) => { - is_immutable_annotation(semantic_model, left) - && is_immutable_annotation(semantic_model, right) - } + }) => is_immutable_annotation(left, semantic) && is_immutable_annotation(right, semantic), Expr::Constant(ast::ExprConstant { value: Constant::None, .. @@ -257,24 +248,22 @@ const IMMUTABLE_FUNCS: &[&[&str]] = &[ /// Return `true` if `func` is a function that returns an immutable object. pub fn is_immutable_func( - semantic_model: &SemanticModel, func: &Expr, + semantic: &SemanticModel, extend_immutable_calls: &[CallPath], ) -> bool { - semantic_model - .resolve_call_path(func) - .map_or(false, |call_path| { - IMMUTABLE_FUNCS + semantic.resolve_call_path(func).map_or(false, |call_path| { + IMMUTABLE_FUNCS + .iter() + .any(|target| call_path.as_slice() == *target) + || extend_immutable_calls .iter() - .any(|target| call_path.as_slice() == *target) - || extend_immutable_calls - .iter() - .any(|target| call_path == *target) - }) + .any(|target| call_path == *target) + }) } /// Return `true` if [`Expr`] is a guard for a type-checking block. -pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic_model: &SemanticModel) -> bool { +pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> bool { let ast::StmtIf { test, .. } = stmt; // Ex) `if False:` @@ -300,12 +289,9 @@ pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic_model: &SemanticModel } // Ex) `if typing.TYPE_CHECKING:` - if semantic_model - .resolve_call_path(test) - .map_or(false, |call_path| { - call_path.as_slice() == ["typing", "TYPE_CHECKING"] - }) - { + if semantic.resolve_call_path(test).map_or(false, |call_path| { + call_path.as_slice() == ["typing", "TYPE_CHECKING"] + }) { return true; } diff --git a/crates/ruff_python_semantic/src/analyze/visibility.rs b/crates/ruff_python_semantic/src/analyze/visibility.rs index a94ac4ca5c..331e912feb 100644 --- a/crates/ruff_python_semantic/src/analyze/visibility.rs +++ b/crates/ruff_python_semantic/src/analyze/visibility.rs @@ -14,9 +14,9 @@ pub enum Visibility { } /// Returns `true` if a function is a "static method". -pub fn is_staticmethod(model: &SemanticModel, decorator_list: &[Decorator]) -> bool { +pub fn is_staticmethod(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool { decorator_list.iter().any(|decorator| { - model + semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { call_path.as_slice() == ["", "staticmethod"] @@ -25,9 +25,9 @@ pub fn is_staticmethod(model: &SemanticModel, decorator_list: &[Decorator]) -> b } /// Returns `true` if a function is a "class method". -pub fn is_classmethod(model: &SemanticModel, decorator_list: &[Decorator]) -> bool { +pub fn is_classmethod(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool { decorator_list.iter().any(|decorator| { - model + semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { call_path.as_slice() == ["", "classmethod"] @@ -36,23 +36,23 @@ pub fn is_classmethod(model: &SemanticModel, decorator_list: &[Decorator]) -> bo } /// Returns `true` if a function definition is an `@overload`. -pub fn is_overload(model: &SemanticModel, decorator_list: &[Decorator]) -> bool { - decorator_list - .iter() - .any(|decorator| model.match_typing_expr(map_callable(&decorator.expression), "overload")) +pub fn is_overload(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool { + decorator_list.iter().any(|decorator| { + semantic.match_typing_expr(map_callable(&decorator.expression), "overload") + }) } /// Returns `true` if a function definition is an `@override` (PEP 698). -pub fn is_override(model: &SemanticModel, decorator_list: &[Decorator]) -> bool { - decorator_list - .iter() - .any(|decorator| model.match_typing_expr(map_callable(&decorator.expression), "override")) +pub fn is_override(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool { + decorator_list.iter().any(|decorator| { + semantic.match_typing_expr(map_callable(&decorator.expression), "override") + }) } /// Returns `true` if a function definition is an abstract method based on its decorators. -pub fn is_abstract(model: &SemanticModel, decorator_list: &[Decorator]) -> bool { +pub fn is_abstract(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool { decorator_list.iter().any(|decorator| { - model + semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { matches!( @@ -73,12 +73,12 @@ pub fn is_abstract(model: &SemanticModel, decorator_list: &[Decorator]) -> bool /// `extra_properties` can be used to check additional non-standard /// `@property`-like decorators. pub fn is_property( - model: &SemanticModel, decorator_list: &[Decorator], extra_properties: &[CallPath], + semantic: &SemanticModel, ) -> bool { decorator_list.iter().any(|decorator| { - model + semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { call_path.as_slice() == ["", "property"] @@ -91,10 +91,10 @@ pub fn is_property( } /// Returns `true` if a class is an `final`. -pub fn is_final(model: &SemanticModel, decorator_list: &[Decorator]) -> bool { +pub fn is_final(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool { decorator_list .iter() - .any(|decorator| model.match_typing_expr(map_callable(&decorator.expression), "final")) + .any(|decorator| semantic.match_typing_expr(map_callable(&decorator.expression), "final")) } /// Returns `true` if a function is a "magic method". diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index d9fd471b41..b390362ae8 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -142,11 +142,11 @@ impl<'a> Binding<'a> { } /// Returns the appropriate visual range for highlighting this binding. - pub fn trimmed_range(&self, semantic_model: &SemanticModel, locator: &Locator) -> TextRange { + pub fn trimmed_range(&self, semantic: &SemanticModel, locator: &Locator) -> TextRange { match self.kind { BindingKind::ClassDefinition | BindingKind::FunctionDefinition => { self.source.map_or(self.range, |source| { - helpers::identifier_range(semantic_model.stmts[source], locator) + helpers::identifier_range(semantic.stmts[source], locator) }) } _ => self.range, @@ -154,9 +154,9 @@ impl<'a> Binding<'a> { } /// Returns the range of the binding's parent. - pub fn parent_range(&self, semantic_model: &SemanticModel) -> Option { + pub fn parent_range(&self, semantic: &SemanticModel) -> Option { self.source - .map(|node_id| semantic_model.stmts[node_id]) + .map(|node_id| semantic.stmts[node_id]) .and_then(|parent| { if parent.is_import_from_stmt() { Some(parent.range()) From 56476dfd61e441e2f98d41418f51fc5a6bd46267 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 14 Jun 2023 17:06:34 -0400 Subject: [PATCH 060/447] Use `matches!` for `CallPath` comparisons (#5099) ## Summary This PR consistently uses `matches! for static `CallPath` comparisons. In some cases, we can significantly reduce the number of cases or checks. ## Test Plan `cargo test ` --- crates/ruff/src/checkers/ast/mod.rs | 25 ++-- .../flake8_2020/rules/name_or_attribute.rs | 4 +- .../ruff/src/rules/flake8_bandit/helpers.rs | 6 +- .../rules/bad_file_permissions.rs | 4 +- .../rules/hashlib_insecure_hash_functions.rs | 2 +- .../rules/jinja2_autoescape_false.rs | 2 +- .../rules/logging_config_insecure_listen.rs | 2 +- .../flake8_bandit/rules/paramiko_calls.rs | 2 +- .../rules/snmp_insecure_version.rs | 2 +- .../rules/snmp_weak_cryptography.rs | 2 +- .../flake8_bandit/rules/unsafe_yaml_load.rs | 7 +- .../rules/abstract_base_class.rs | 8 +- .../rules/assert_raises_exception.rs | 6 +- .../rules/cached_instance_method.rs | 3 +- .../rules/no_explicit_stacklevel.rs | 2 +- .../rules/reuse_of_groupby_generator.rs | 2 +- .../rules/useless_contextlib_suppress.rs | 2 +- .../rules/call_date_fromtimestamp.rs | 2 +- .../flake8_datetimez/rules/call_date_today.rs | 2 +- .../rules/call_datetime_fromtimestamp.rs | 5 +- .../rules/call_datetime_now_without_tzinfo.rs | 2 +- .../call_datetime_strptime_without_zone.rs | 2 +- .../rules/call_datetime_today.rs | 2 +- .../rules/call_datetime_utcfromtimestamp.rs | 5 +- .../rules/call_datetime_utcnow.rs | 2 +- .../rules/call_datetime_without_tzinfo.rs | 2 +- .../src/rules/flake8_django/rules/helpers.rs | 8 +- .../rules/locals_in_render_function.rs | 8 +- .../rules/non_leading_receiver_decorator.rs | 2 +- .../rules/logging_call.rs | 4 +- .../flake8_pie/rules/non_unique_enums.rs | 8 +- .../rules/flake8_print/rules/print_call.rs | 9 +- .../rules/bad_version_info_comparison.rs | 2 +- .../flake8_pyi/rules/unrecognized_platform.rs | 2 +- .../flake8_pytest_style/rules/fixture.rs | 2 +- .../flake8_pytest_style/rules/helpers.rs | 8 +- .../rules/flake8_pytest_style/rules/raises.rs | 2 +- .../rules/private_member_access.rs | 7 +- .../rules/flake8_simplify/rules/ast_expr.rs | 6 +- .../rules/open_file_with_context_handler.rs | 8 +- .../numpy/rules/deprecated_type_alias.rs | 16 ++- .../rules/numpy/rules/numpy_legacy_random.rs | 110 +++++++++--------- crates/ruff/src/rules/pep8_naming/helpers.rs | 5 +- .../pycodestyle/rules/lambda_assignment.rs | 2 +- .../pygrep_hooks/rules/deprecated_log_warn.rs | 2 +- .../pylint/rules/invalid_envvar_default.rs | 4 +- .../pylint/rules/invalid_envvar_value.rs | 4 +- ...convert_named_tuple_functional_to_class.rs | 4 +- .../convert_typed_dict_functional_to_class.rs | 4 +- .../pyupgrade/rules/datetime_utc_alias.rs | 21 ++-- .../pyupgrade/rules/deprecated_mock_import.rs | 6 +- .../rules/lru_cache_with_maxsize_none.rs | 2 +- .../rules/lru_cache_without_parameters.rs | 2 +- .../src/rules/pyupgrade/rules/open_alias.rs | 4 +- .../rules/pyupgrade/rules/os_error_alias.rs | 6 +- .../pyupgrade/rules/outdated_version_block.rs | 2 +- .../pyupgrade/rules/replace_stdout_stderr.rs | 6 +- .../rules/replace_universal_newlines.rs | 2 +- .../pyupgrade/rules/type_of_primitive.rs | 4 +- .../pyupgrade/rules/typing_text_str_alias.rs | 2 +- crates/ruff/src/rules/ruff/rules/helpers.rs | 2 +- .../tryceratops/rules/raise_vanilla_class.rs | 4 +- .../rules/type_check_without_type_error.rs | 47 ++++---- .../src/analyze/function_type.rs | 4 +- .../src/analyze/logging.rs | 2 +- .../src/analyze/typing.rs | 8 +- .../src/analyze/visibility.rs | 15 +-- 67 files changed, 251 insertions(+), 220 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 5cb760be85..8e0ce79375 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -3566,19 +3566,20 @@ where .match_typing_call_path(&call_path, "TypedDict") { Some(typing::Callable::TypedDict) - } else if [ - "Arg", - "DefaultArg", - "NamedArg", - "DefaultNamedArg", - "VarArg", - "KwArg", - ] - .iter() - .any(|target| call_path.as_slice() == ["mypy_extensions", target]) - { + } else if matches!( + call_path.as_slice(), + [ + "mypy_extensions", + "Arg" + | "DefaultArg" + | "NamedArg" + | "DefaultNamedArg" + | "VarArg" + | "KwArg" + ] + ) { Some(typing::Callable::MypyExtension) - } else if call_path.as_slice() == ["", "bool"] { + } else if matches!(call_path.as_slice(), ["", "bool"]) { Some(typing::Callable::Bool) } else { None diff --git a/crates/ruff/src/rules/flake8_2020/rules/name_or_attribute.rs b/crates/ruff/src/rules/flake8_2020/rules/name_or_attribute.rs index e8afff1335..3ca122af52 100644 --- a/crates/ruff/src/rules/flake8_2020/rules/name_or_attribute.rs +++ b/crates/ruff/src/rules/flake8_2020/rules/name_or_attribute.rs @@ -20,7 +20,9 @@ pub(crate) fn name_or_attribute(checker: &mut Checker, expr: &Expr) { if checker .semantic() .resolve_call_path(expr) - .map_or(false, |call_path| call_path.as_slice() == ["six", "PY3"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["six", "PY3"]) + }) { checker .diagnostics diff --git a/crates/ruff/src/rules/flake8_bandit/helpers.rs b/crates/ruff/src/rules/flake8_bandit/helpers.rs index 4a72eb63d0..b480934b8b 100644 --- a/crates/ruff/src/rules/flake8_bandit/helpers.rs +++ b/crates/ruff/src/rules/flake8_bandit/helpers.rs @@ -29,16 +29,14 @@ pub(super) fn is_untyped_exception(type_: Option<&Expr>, semantic: &SemanticMode semantic .resolve_call_path(type_) .map_or(false, |call_path| { - call_path.as_slice() == ["", "Exception"] - || call_path.as_slice() == ["", "BaseException"] + matches!(call_path.as_slice(), ["", "Exception" | "BaseException"]) }) }) } else { semantic .resolve_call_path(type_) .map_or(false, |call_path| { - call_path.as_slice() == ["", "Exception"] - || call_path.as_slice() == ["", "BaseException"] + matches!(call_path.as_slice(), ["", "Exception" | "BaseException"]) }) } }) diff --git a/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs b/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs index ac5910df40..076c97cea3 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs @@ -110,7 +110,9 @@ pub(crate) fn bad_file_permissions( if checker .semantic() .resolve_call_path(func) - .map_or(false, |call_path| call_path.as_slice() == ["os", "chmod"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["os", "chmod"]) + }) { let call_args = SimpleCallArgs::new(args, keywords); if let Some(mode_arg) = call_args.argument("mode", 1) { diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs b/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs index 29cfeca780..1e624cd5fa 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs @@ -52,7 +52,7 @@ pub(crate) fn hashlib_insecure_hash_functions( .semantic() .resolve_call_path(func) .and_then(|call_path| { - if call_path.as_slice() == ["hashlib", "new"] { + if matches!(call_path.as_slice(), ["hashlib", "new"]) { Some(HashlibCall::New) } else { WEAK_HASHES diff --git a/crates/ruff/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs b/crates/ruff/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs index 2e9fddbac1..bf368e89e7 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs @@ -40,7 +40,7 @@ pub(crate) fn jinja2_autoescape_false( .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["jinja2", "Environment"] + matches!(call_path.as_slice(), ["jinja2", "Environment"]) }) { let call_args = SimpleCallArgs::new(args, keywords); diff --git a/crates/ruff/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs b/crates/ruff/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs index 009d166786..0953a96113 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs @@ -27,7 +27,7 @@ pub(crate) fn logging_config_insecure_listen( .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["logging", "config", "listen"] + matches!(call_path.as_slice(), ["logging", "config", "listen"]) }) { let call_args = SimpleCallArgs::new(args, keywords); diff --git a/crates/ruff/src/rules/flake8_bandit/rules/paramiko_calls.rs b/crates/ruff/src/rules/flake8_bandit/rules/paramiko_calls.rs index 73e2e82b13..e0e50828d1 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/paramiko_calls.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/paramiko_calls.rs @@ -21,7 +21,7 @@ pub(crate) fn paramiko_call(checker: &mut Checker, func: &Expr) { .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["paramiko", "exec_command"] + matches!(call_path.as_slice(), ["paramiko", "exec_command"]) }) { checker diff --git a/crates/ruff/src/rules/flake8_bandit/rules/snmp_insecure_version.rs b/crates/ruff/src/rules/flake8_bandit/rules/snmp_insecure_version.rs index 15541b48c9..1c4b032f97 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/snmp_insecure_version.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/snmp_insecure_version.rs @@ -28,7 +28,7 @@ pub(crate) fn snmp_insecure_version( .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["pysnmp", "hlapi", "CommunityData"] + matches!(call_path.as_slice(), ["pysnmp", "hlapi", "CommunityData"]) }) { let call_args = SimpleCallArgs::new(args, keywords); diff --git a/crates/ruff/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs b/crates/ruff/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs index 13b490dc33..f654c2550e 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs @@ -30,7 +30,7 @@ pub(crate) fn snmp_weak_cryptography( .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["pysnmp", "hlapi", "UsmUserData"] + matches!(call_path.as_slice(), ["pysnmp", "hlapi", "UsmUserData"]) }) { let call_args = SimpleCallArgs::new(args, keywords); diff --git a/crates/ruff/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs b/crates/ruff/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs index f72a68ab4d..9d5274e523 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs @@ -40,7 +40,9 @@ pub(crate) fn unsafe_yaml_load( if checker .semantic() .resolve_call_path(func) - .map_or(false, |call_path| call_path.as_slice() == ["yaml", "load"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["yaml", "load"]) + }) { let call_args = SimpleCallArgs::new(args, keywords); if let Some(loader_arg) = call_args.argument("Loader", 1) { @@ -48,8 +50,7 @@ pub(crate) fn unsafe_yaml_load( .semantic() .resolve_call_path(loader_arg) .map_or(false, |call_path| { - call_path.as_slice() == ["yaml", "SafeLoader"] - || call_path.as_slice() == ["yaml", "CSafeLoader"] + matches!(call_path.as_slice(), ["yaml", "SafeLoader" | "CSafeLoader"]) }) { let loader = match loader_arg { 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 6a4b303ec7..6868c61673 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 @@ -42,12 +42,12 @@ fn is_abc_class(bases: &[Expr], keywords: &[Keyword], semantic: &SemanticModel) && semantic .resolve_call_path(&keyword.value) .map_or(false, |call_path| { - call_path.as_slice() == ["abc", "ABCMeta"] + matches!(call_path.as_slice(), ["abc", "ABCMeta"]) }) }) || bases.iter().any(|base| { - semantic - .resolve_call_path(base) - .map_or(false, |call_path| call_path.as_slice() == ["abc", "ABC"]) + semantic.resolve_call_path(base).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["abc", "ABC"]) + }) }) } 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 e629112224..aae68f9b41 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 @@ -68,7 +68,9 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: if !checker .semantic() .resolve_call_path(args.first().unwrap()) - .map_or(false, |call_path| call_path.as_slice() == ["", "Exception"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["", "Exception"]) + }) { return; } @@ -81,7 +83,7 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["pytest", "raises"] + matches!(call_path.as_slice(), ["pytest", "raises"]) }) && !keywords .iter() diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs b/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs index ddfba0d174..df291f3725 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs @@ -20,8 +20,7 @@ impl Violation for CachedInstanceMethod { fn is_cache_func(expr: &Expr, semantic: &SemanticModel) -> bool { semantic.resolve_call_path(expr).map_or(false, |call_path| { - call_path.as_slice() == ["functools", "lru_cache"] - || call_path.as_slice() == ["functools", "cache"] + matches!(call_path.as_slice(), ["functools", "lru_cache" | "cache"]) }) } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs b/crates/ruff/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs index 0668fc9f39..4d034dce4a 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs @@ -48,7 +48,7 @@ pub(crate) fn no_explicit_stacklevel( .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["warnings", "warn"] + matches!(call_path.as_slice(), ["warnings", "warn"]) }) { return; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs b/crates/ruff/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs index 6e753163ec..976010ee8c 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs @@ -345,7 +345,7 @@ pub(crate) fn reuse_of_groupby_generator( .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["itertools", "groupby"] + matches!(call_path.as_slice(), ["itertools", "groupby"]) }) { return; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs b/crates/ruff/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs index c066a74242..5c023d79ca 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs @@ -30,7 +30,7 @@ pub(crate) fn useless_contextlib_suppress( .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["contextlib", "suppress"] + matches!(call_path.as_slice(), ["contextlib", "suppress"]) }) { checker diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs index 66a6422200..1c41e6c702 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs @@ -30,7 +30,7 @@ pub(crate) fn call_date_fromtimestamp(checker: &mut Checker, func: &Expr, locati .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "date", "fromtimestamp"] + matches!(call_path.as_slice(), ["datetime", "date", "fromtimestamp"]) }) { checker diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_date_today.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_date_today.rs index d159710b99..282dff64fe 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_date_today.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_date_today.rs @@ -30,7 +30,7 @@ pub(crate) fn call_date_today(checker: &mut Checker, func: &Expr, location: Text .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "date", "today"] + matches!(call_path.as_slice(), ["datetime", "date", "today"]) }) { checker diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs index f75dde8d45..0832acc59d 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs @@ -31,7 +31,10 @@ pub(crate) fn call_datetime_fromtimestamp( .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "datetime", "fromtimestamp"] + matches!( + call_path.as_slice(), + ["datetime", "datetime", "fromtimestamp"] + ) }) { return; diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs index 15b930d6d2..54491cc3cb 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs @@ -29,7 +29,7 @@ pub(crate) fn call_datetime_now_without_tzinfo( .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "datetime", "now"] + matches!(call_path.as_slice(), ["datetime", "datetime", "now"]) }) { return; 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 5432d11563..200a75d0bf 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 @@ -31,7 +31,7 @@ pub(crate) fn call_datetime_strptime_without_zone( .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "datetime", "strptime"] + matches!(call_path.as_slice(), ["datetime", "datetime", "strptime"]) }) { return; diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_today.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_today.rs index e566d38455..4ca31f75e5 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_today.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_today.rs @@ -30,7 +30,7 @@ pub(crate) fn call_datetime_today(checker: &mut Checker, func: &Expr, location: .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "datetime", "today"] + matches!(call_path.as_slice(), ["datetime", "datetime", "today"]) }) { checker diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs index db7530cc42..075e5d7352 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs @@ -37,7 +37,10 @@ pub(crate) fn call_datetime_utcfromtimestamp( .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "datetime", "utcfromtimestamp"] + matches!( + call_path.as_slice(), + ["datetime", "datetime", "utcfromtimestamp"] + ) }) { checker diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs index 8444243061..b6abbb4722 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs @@ -32,7 +32,7 @@ pub(crate) fn call_datetime_utcnow(checker: &mut Checker, func: &Expr, location: .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "datetime", "utcnow"] + matches!(call_path.as_slice(), ["datetime", "datetime", "utcnow"]) }) { checker diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs index e37b47946d..86de1e3ea2 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs @@ -28,7 +28,7 @@ pub(crate) fn call_datetime_without_tzinfo( .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "datetime"] + matches!(call_path.as_slice(), ["datetime", "datetime"]) }) { return; diff --git a/crates/ruff/src/rules/flake8_django/rules/helpers.rs b/crates/ruff/src/rules/flake8_django/rules/helpers.rs index 5c45066a02..5c208cc235 100644 --- a/crates/ruff/src/rules/flake8_django/rules/helpers.rs +++ b/crates/ruff/src/rules/flake8_django/rules/helpers.rs @@ -5,15 +5,17 @@ use ruff_python_semantic::SemanticModel; /// Return `true` if a Python class appears to be a Django model, based on its base classes. pub(super) fn is_model(base: &Expr, semantic: &SemanticModel) -> bool { semantic.resolve_call_path(base).map_or(false, |call_path| { - call_path.as_slice() == ["django", "db", "models", "Model"] + matches!(call_path.as_slice(), ["django", "db", "models", "Model"]) }) } /// Return `true` if a Python class appears to be a Django model form, based on its base classes. pub(super) fn is_model_form(base: &Expr, semantic: &SemanticModel) -> bool { semantic.resolve_call_path(base).map_or(false, |call_path| { - call_path.as_slice() == ["django", "forms", "ModelForm"] - || call_path.as_slice() == ["django", "forms", "models", "ModelForm"] + matches!( + call_path.as_slice(), + ["django", "forms", "ModelForm"] | ["django", "forms", "models", "ModelForm"] + ) }) } 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 acd7074222..b5d3340a11 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 @@ -54,7 +54,7 @@ pub(crate) fn locals_in_render_function( .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["django", "shortcuts", "render"] + matches!(call_path.as_slice(), ["django", "shortcuts", "render"]) }) { return; @@ -87,7 +87,7 @@ fn is_locals_call(expr: &Expr, semantic: &SemanticModel) -> bool { let Expr::Call(ast::ExprCall { func, .. }) = expr else { return false }; - semantic - .resolve_call_path(func) - .map_or(false, |call_path| call_path.as_slice() == ["", "locals"]) + 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/non_leading_receiver_decorator.rs b/crates/ruff/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs index 7e68309510..f023431404 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 @@ -61,7 +61,7 @@ where let is_receiver = match &decorator.expression { Expr::Call(ast::ExprCall { func, .. }) => resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["django", "dispatch", "receiver"] + matches!(call_path.as_slice(), ["django", "dispatch", "receiver"]) }), _ => false, }; diff --git a/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs b/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs index 5dc28bbb3d..c3acad982c 100644 --- a/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs +++ b/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs @@ -108,7 +108,9 @@ fn check_log_record_attr_clash(checker: &mut Checker, extra: &Keyword) { if checker .semantic() .resolve_call_path(func) - .map_or(false, |call_path| call_path.as_slice() == ["", "dict"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["", "dict"]) + }) { for keyword in keywords { if let Some(key) = &keyword.arg { diff --git a/crates/ruff/src/rules/flake8_pie/rules/non_unique_enums.rs b/crates/ruff/src/rules/flake8_pie/rules/non_unique_enums.rs index 53697bb340..cfbfa7630a 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/non_unique_enums.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/non_unique_enums.rs @@ -68,7 +68,9 @@ pub(crate) fn non_unique_enums<'a, 'b>( checker .semantic() .resolve_call_path(expr) - .map_or(false, |call_path| call_path.as_slice() == ["enum", "Enum"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["enum", "Enum"]) + }) }) { return; } @@ -83,7 +85,9 @@ pub(crate) fn non_unique_enums<'a, 'b>( if checker .semantic() .resolve_call_path(func) - .map_or(false, |call_path| call_path.as_slice() == ["enum", "auto"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["enum", "auto"]) + }) { continue; } diff --git a/crates/ruff/src/rules/flake8_print/rules/print_call.rs b/crates/ruff/src/rules/flake8_print/rules/print_call.rs index 23b4d95050..a3c9bba035 100644 --- a/crates/ruff/src/rules/flake8_print/rules/print_call.rs +++ b/crates/ruff/src/rules/flake8_print/rules/print_call.rs @@ -79,10 +79,9 @@ impl Violation for PPrint { pub(crate) fn print_call(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) { let diagnostic = { let call_path = checker.semantic().resolve_call_path(func); - if call_path - .as_ref() - .map_or(false, |call_path| *call_path.as_slice() == ["", "print"]) - { + if call_path.as_ref().map_or(false, |call_path| { + matches!(call_path.as_slice(), ["", "print"]) + }) { // If the print call has a `file=` argument (that isn't `None`, `"sys.stdout"`, // or `"sys.stderr"`), don't trigger T201. if let Some(keyword) = keywords @@ -103,7 +102,7 @@ pub(crate) fn print_call(checker: &mut Checker, func: &Expr, keywords: &[Keyword } Diagnostic::new(Print, func.range()) } else if call_path.as_ref().map_or(false, |call_path| { - *call_path.as_slice() == ["pprint", "pprint"] + matches!(call_path.as_slice(), ["pprint", "pprint"]) }) { Diagnostic::new(PPrint, func.range()) } else { 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 a75dd025e3..f660f196e5 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 @@ -72,7 +72,7 @@ pub(crate) fn bad_version_info_comparison( .semantic() .resolve_call_path(left) .map_or(false, |call_path| { - call_path.as_slice() == ["sys", "version_info"] + matches!(call_path.as_slice(), ["sys", "version_info"]) }) { return; 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 3aa892bb27..36088621d0 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs @@ -106,7 +106,7 @@ pub(crate) fn unrecognized_platform( .semantic() .resolve_call_path(left) .map_or(false, |call_path| { - call_path.as_slice() == ["sys", "platform"] + matches!(call_path.as_slice(), ["sys", "platform"]) }) { return; 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 af1250bd87..3825e9929e 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs @@ -233,7 +233,7 @@ where } Expr::Call(ast::ExprCall { func, .. }) => { if collect_call_path(func).map_or(false, |call_path| { - call_path.as_slice() == ["request", "addfinalizer"] + matches!(call_path.as_slice(), ["request", "addfinalizer"]) }) { self.addfinalizer_call = Some(expr); }; diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/helpers.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/helpers.rs index 5c2a3c1449..7c175d3039 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/helpers.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/helpers.rs @@ -22,7 +22,7 @@ pub(super) fn get_mark_decorators( pub(super) fn is_pytest_fail(call: &Expr, semantic: &SemanticModel) -> bool { semantic.resolve_call_path(call).map_or(false, |call_path| { - call_path.as_slice() == ["pytest", "fail"] + matches!(call_path.as_slice(), ["pytest", "fail"]) }) } @@ -30,7 +30,7 @@ pub(super) fn is_pytest_fixture(decorator: &Decorator, semantic: &SemanticModel) semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { - call_path.as_slice() == ["pytest", "fixture"] + matches!(call_path.as_slice(), ["pytest", "fixture"]) }) } @@ -38,7 +38,7 @@ pub(super) fn is_pytest_yield_fixture(decorator: &Decorator, semantic: &Semantic semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { - call_path.as_slice() == ["pytest", "yield_fixture"] + matches!(call_path.as_slice(), ["pytest", "yield_fixture"]) }) } @@ -46,7 +46,7 @@ pub(super) fn is_pytest_parametrize(decorator: &Decorator, semantic: &SemanticMo semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { - call_path.as_slice() == ["pytest", "mark", "parametrize"] + matches!(call_path.as_slice(), ["pytest", "mark", "parametrize"]) }) } diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs index 9e1b62a4b8..fe8bdf7ac6 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs @@ -49,7 +49,7 @@ impl Violation for PytestRaisesWithoutException { fn is_pytest_raises(func: &Expr, semantic: &SemanticModel) -> bool { semantic.resolve_call_path(func).map_or(false, |call_path| { - call_path.as_slice() == ["pytest", "raises"] + matches!(call_path.as_slice(), ["pytest", "raises"]) }) } diff --git a/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs b/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs index 7e18208ac3..6a8b534e3e 100644 --- a/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs +++ b/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs @@ -136,16 +136,13 @@ pub(crate) fn private_member_access(checker: &mut Checker, expr: &Expr) { if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() { // Ignore `super()` calls. if let Some(call_path) = collect_call_path(func) { - if call_path.as_slice() == ["super"] { + if matches!(call_path.as_slice(), ["super"]) { return; } } } else if let Some(call_path) = collect_call_path(value) { // Ignore `self` and `cls` accesses. - if call_path.as_slice() == ["self"] - || call_path.as_slice() == ["cls"] - || call_path.as_slice() == ["mcs"] - { + if matches!(call_path.as_slice(), ["self" | "cls" | "mcs"]) { return; } 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 649b90596c..3ed9b778b9 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs @@ -115,8 +115,10 @@ pub(crate) fn use_capital_environment_variables(checker: &mut Checker, expr: &Ex .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["os", "environ", "get"] - || call_path.as_slice() == ["os", "getenv"] + matches!( + call_path.as_slice(), + ["os", "environ", "get"] | ["os", "getenv"] + ) }) { return; 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 7a6614f96c..bb7014fc8b 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 @@ -63,7 +63,7 @@ fn match_async_exit_stack(semantic: &SemanticModel) -> bool { for item in items { if let Expr::Call(ast::ExprCall { func, .. }) = &item.context_expr { if semantic.resolve_call_path(func).map_or(false, |call_path| { - call_path.as_slice() == ["contextlib", "AsyncExitStack"] + matches!(call_path.as_slice(), ["contextlib", "AsyncExitStack"]) }) { return true; } @@ -94,7 +94,7 @@ fn match_exit_stack(semantic: &SemanticModel) -> bool { for item in items { if let Expr::Call(ast::ExprCall { func, .. }) = &item.context_expr { if semantic.resolve_call_path(func).map_or(false, |call_path| { - call_path.as_slice() == ["contextlib", "ExitStack"] + matches!(call_path.as_slice(), ["contextlib", "ExitStack"]) }) { return true; } @@ -110,7 +110,9 @@ pub(crate) fn open_file_with_context_handler(checker: &mut Checker, func: &Expr) if checker .semantic() .resolve_call_path(func) - .map_or(false, |call_path| call_path.as_slice() == ["", "open"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["", "open"]) + }) { if checker.semantic().is_builtin("open") { // Ex) `with open("foo.txt") as f: ...` diff --git a/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs b/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs index a4bc1a07bd..7889332d44 100644 --- a/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs +++ b/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs @@ -52,15 +52,13 @@ pub(crate) fn deprecated_type_alias(checker: &mut Checker, expr: &Expr) { .semantic() .resolve_call_path(expr) .and_then(|call_path| { - if call_path.as_slice() == ["numpy", "bool"] - || call_path.as_slice() == ["numpy", "int"] - || call_path.as_slice() == ["numpy", "float"] - || call_path.as_slice() == ["numpy", "complex"] - || call_path.as_slice() == ["numpy", "object"] - || call_path.as_slice() == ["numpy", "str"] - || call_path.as_slice() == ["numpy", "long"] - || call_path.as_slice() == ["numpy", "unicode"] - { + if matches!( + call_path.as_slice(), + [ + "numpy", + "bool" | "int" | "float" | "complex" | "object" | "str" | "long" | "unicode" + ] + ) { Some(call_path[1]) } else { None diff --git a/crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs b/crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs index 736685e813..46b66a65e4 100644 --- a/crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs +++ b/crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs @@ -63,58 +63,64 @@ pub(crate) fn numpy_legacy_random(checker: &mut Checker, expr: &Expr) { .resolve_call_path(expr) .and_then(|call_path| { // seeding state - if call_path.as_slice() == ["numpy", "random", "seed"] - || call_path.as_slice() == ["numpy", "random", "get_state"] - || call_path.as_slice() == ["numpy", "random", "set_state"] - // simple random data - || call_path.as_slice() == ["numpy", "random", "rand"] - || call_path.as_slice() == ["numpy", "random", "randn"] - || call_path.as_slice() == ["numpy", "random", "randint"] - || call_path.as_slice() == ["numpy", "random", "random_integers"] - || call_path.as_slice() == ["numpy", "random", "random_sample"] - || call_path.as_slice() == ["numpy", "random", "choice"] - || call_path.as_slice() == ["numpy", "random", "bytes"] - // permutations - || call_path.as_slice() == ["numpy", "random", "shuffle"] - || call_path.as_slice() == ["numpy", "random", "permutation"] - // distributions - || call_path.as_slice() == ["numpy", "random", "beta"] - || call_path.as_slice() == ["numpy", "random", "binomial"] - || call_path.as_slice() == ["numpy", "random", "chisquare"] - || call_path.as_slice() == ["numpy", "random", "dirichlet"] - || call_path.as_slice() == ["numpy", "random", "exponential"] - || call_path.as_slice() == ["numpy", "random", "f"] - || call_path.as_slice() == ["numpy", "random", "gamma"] - || call_path.as_slice() == ["numpy", "random", "geometric"] - || call_path.as_slice() == ["numpy", "random", "get_state"] - || call_path.as_slice() == ["numpy", "random", "gumbel"] - || call_path.as_slice() == ["numpy", "random", "hypergeometric"] - || call_path.as_slice() == ["numpy", "random", "laplace"] - || call_path.as_slice() == ["numpy", "random", "logistic"] - || call_path.as_slice() == ["numpy", "random", "lognormal"] - || call_path.as_slice() == ["numpy", "random", "logseries"] - || call_path.as_slice() == ["numpy", "random", "multinomial"] - || call_path.as_slice() == ["numpy", "random", "multivariate_normal"] - || call_path.as_slice() == ["numpy", "random", "negative_binomial"] - || call_path.as_slice() == ["numpy", "random", "noncentral_chisquare"] - || call_path.as_slice() == ["numpy", "random", "noncentral_f"] - || call_path.as_slice() == ["numpy", "random", "normal"] - || call_path.as_slice() == ["numpy", "random", "pareto"] - || call_path.as_slice() == ["numpy", "random", "poisson"] - || call_path.as_slice() == ["numpy", "random", "power"] - || call_path.as_slice() == ["numpy", "random", "rayleigh"] - || call_path.as_slice() == ["numpy", "random", "standard_cauchy"] - || call_path.as_slice() == ["numpy", "random", "standard_exponential"] - || call_path.as_slice() == ["numpy", "random", "standard_gamma"] - || call_path.as_slice() == ["numpy", "random", "standard_normal"] - || call_path.as_slice() == ["numpy", "random", "standard_t"] - || call_path.as_slice() == ["numpy", "random", "triangular"] - || call_path.as_slice() == ["numpy", "random", "uniform"] - || call_path.as_slice() == ["numpy", "random", "vonmises"] - || call_path.as_slice() == ["numpy", "random", "wald"] - || call_path.as_slice() == ["numpy", "random", "weibull"] - || call_path.as_slice() == ["numpy", "random", "zipf"] - { + if matches!( + call_path.as_slice(), + [ + "numpy", + "random", + // Seeds + "seed" | + "get_state" | + "set_state" | + // Simple random data + "rand" | + "randn" | + "randint" | + "random_integers" | + "random_sample" | + "choice" | + "bytes" | + // Permutations + "shuffle" | + "permutation" | + // Distributions + "beta" | + "binomial" | + "chisquare" | + "dirichlet" | + "exponential" | + "f" | + "gamma" | + "geometric" | + "gumbel" | + "hypergeometric" | + "laplace" | + "logistic" | + "lognormal" | + "logseries" | + "multinomial" | + "multivariate_normal" | + "negative_binomial" | + "noncentral_chisquare" | + "noncentral_f" | + "normal" | + "pareto" | + "poisson" | + "power" | + "rayleigh" | + "standard_cauchy" | + "standard_exponential" | + "standard_gamma" | + "standard_normal" | + "standard_t" | + "triangular" | + "uniform" | + "vonmises" | + "wald" | + "weibull" | + "zipf" + ] + ) { Some(call_path[2]) } else { None diff --git a/crates/ruff/src/rules/pep8_naming/helpers.rs b/crates/ruff/src/rules/pep8_naming/helpers.rs index 93792ad0ab..2d3ed8e6ef 100644 --- a/crates/ruff/src/rules/pep8_naming/helpers.rs +++ b/crates/ruff/src/rules/pep8_naming/helpers.rs @@ -45,7 +45,7 @@ pub(super) fn is_typed_dict_assignment(stmt: &Stmt, semantic: &SemanticModel) -> return false; }; semantic.resolve_call_path(func).map_or(false, |call_path| { - call_path.as_slice() == ["typing", "TypedDict"] + matches!(call_path.as_slice(), ["typing", "TypedDict"]) }) } @@ -57,8 +57,7 @@ pub(super) fn is_type_var_assignment(stmt: &Stmt, semantic: &SemanticModel) -> b return false; }; semantic.resolve_call_path(func).map_or(false, |call_path| { - call_path.as_slice() == ["typing", "TypeVar"] - || call_path.as_slice() == ["typing", "NewType"] + matches!(call_path.as_slice(), ["typing", "TypeVar" | "NewType"]) }) } diff --git a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs index 29767501ed..8c17df32ec 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs @@ -134,7 +134,7 @@ fn extract_types(annotation: &Expr, semantic: &SemanticModel) -> Option<(Vec( }) = value else { return None; }; - if !semantic.resolve_call_path(func).map_or(false, |call_path| { - call_path.as_slice() == ["typing", "NamedTuple"] - }) { + if !semantic.match_typing_expr(func, "NamedTuple") { return None; } Some((typename, args, keywords, func)) diff --git a/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs b/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs index 08ef961118..cd56786e62 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs @@ -52,9 +52,7 @@ fn match_typed_dict_assign<'a>( }) = value else { return None; }; - if !semantic.resolve_call_path(func).map_or(false, |call_path| { - call_path.as_slice() == ["typing", "TypedDict"] - }) { + if !semantic.match_typing_expr(func, "TypedDict") { return None; } Some((class_name, args, keywords, func)) diff --git a/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs index 860e99bd79..9840c8086d 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs @@ -8,9 +8,7 @@ use crate::checkers::ast::Checker; use crate::registry::AsRule; #[violation] -pub struct DatetimeTimezoneUTC { - straight_import: bool, -} +pub struct DatetimeTimezoneUTC; impl Violation for DatetimeTimezoneUTC { const AUTOFIX: AutofixKind = AutofixKind::Sometimes; @@ -31,17 +29,18 @@ pub(crate) fn datetime_utc_alias(checker: &mut Checker, expr: &Expr) { .semantic() .resolve_call_path(expr) .map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "timezone", "utc"] + matches!(call_path.as_slice(), ["datetime", "timezone", "utc"]) }) { - let straight_import = collect_call_path(expr).map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "timezone", "utc"] - }); - let mut diagnostic = Diagnostic::new(DatetimeTimezoneUTC { straight_import }, expr.range()); + let mut diagnostic = Diagnostic::new(DatetimeTimezoneUTC, expr.range()); if checker.patch(diagnostic.kind.rule()) { - if straight_import { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + // If the reference was structured as, e.g., `datetime.timezone.utc`, then we can + // replace it with `datetime.UTC`. If `timezone` was imported via `from datetime import + // timezone`, then the replacement is more complicated. + if collect_call_path(expr).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["datetime", "timezone", "utc"]) + }) { + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( "datetime.UTC".to_string(), expr.range(), ))); diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs index 9680aa8998..0c85c8f47b 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs @@ -228,9 +228,9 @@ fn format_import_from( /// UP026 pub(crate) fn deprecated_mock_attribute(checker: &mut Checker, expr: &Expr) { if let Expr::Attribute(ast::ExprAttribute { value, .. }) = expr { - if collect_call_path(value) - .map_or(false, |call_path| call_path.as_slice() == ["mock", "mock"]) - { + if collect_call_path(value).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["mock", "mock"]) + }) { let mut diagnostic = Diagnostic::new( DeprecatedMockImport { reference_type: MockReference::Attribute, 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 c19377c187..7be2761a0e 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 @@ -41,7 +41,7 @@ pub(crate) fn lru_cache_with_maxsize_none(checker: &mut Checker, decorator_list: .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["functools", "lru_cache"] + matches!(call_path.as_slice(), ["functools", "lru_cache"]) }) { let Keyword { 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 8073666dcc..5088b3398c 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 @@ -40,7 +40,7 @@ pub(crate) fn lru_cache_without_parameters(checker: &mut Checker, decorator_list .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["functools", "lru_cache"] + matches!(call_path.as_slice(), ["functools", "lru_cache"]) }) { let mut diagnostic = Diagnostic::new( diff --git a/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs index 8577c0185e..584602d996 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs @@ -27,7 +27,9 @@ pub(crate) fn open_alias(checker: &mut Checker, expr: &Expr, func: &Expr) { if checker .semantic() .resolve_call_path(func) - .map_or(false, |call_path| call_path.as_slice() == ["io", "open"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["io", "open"]) + }) { let mut diagnostic = Diagnostic::new(OpenAlias, expr.range()); if checker.patch(diagnostic.kind.rule()) { diff --git a/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs index 81d995875b..666030fada 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs @@ -49,9 +49,9 @@ fn is_alias(expr: &Expr, semantic: &SemanticModel) -> bool { /// Return `true` if an [`Expr`] is `OSError`. fn is_os_error(expr: &Expr, semantic: &SemanticModel) -> bool { - semantic - .resolve_call_path(expr) - .map_or(false, |call_path| call_path.as_slice() == ["", "OSError"]) + semantic.resolve_call_path(expr).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["", "OSError"]) + }) } /// Create a [`Diagnostic`] for a single target, like an [`Expr::Name`]. 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 4623ea9cfa..b0bb74a37f 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs @@ -339,7 +339,7 @@ pub(crate) fn outdated_version_block( .semantic() .resolve_call_path(left) .map_or(false, |call_path| { - call_path.as_slice() == ["sys", "version_info"] + matches!(call_path.as_slice(), ["sys", "version_info"]) }) { return; 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 64dc64b113..83f18f75c5 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs @@ -64,7 +64,7 @@ pub(crate) fn replace_stdout_stderr( .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["subprocess", "run"] + matches!(call_path.as_slice(), ["subprocess", "run"]) }) { // Find `stdout` and `stderr` kwargs. @@ -80,13 +80,13 @@ pub(crate) fn replace_stdout_stderr( .semantic() .resolve_call_path(&stdout.value) .map_or(false, |call_path| { - call_path.as_slice() == ["subprocess", "PIPE"] + matches!(call_path.as_slice(), ["subprocess", "PIPE"]) }) || !checker .semantic() .resolve_call_path(&stderr.value) .map_or(false, |call_path| { - call_path.as_slice() == ["subprocess", "PIPE"] + matches!(call_path.as_slice(), ["subprocess", "PIPE"]) }) { return; diff --git a/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs b/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs index e9ab4b80ae..e46f87cc04 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs @@ -28,7 +28,7 @@ pub(crate) fn replace_universal_newlines(checker: &mut Checker, func: &Expr, kwa .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["subprocess", "run"] + matches!(call_path.as_slice(), ["subprocess", "run"]) }) { let Some(kwarg) = find_keyword(kwargs, "universal_newlines") 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 c75fe15303..9ee8cecfb2 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs @@ -34,7 +34,9 @@ pub(crate) fn type_of_primitive(checker: &mut Checker, expr: &Expr, func: &Expr, if !checker .semantic() .resolve_call_path(func) - .map_or(false, |call_path| call_path.as_slice() == ["", "type"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["", "type"]) + }) { return; } diff --git a/crates/ruff/src/rules/pyupgrade/rules/typing_text_str_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/typing_text_str_alias.rs index 7c1110689b..cfdd0bceb5 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/typing_text_str_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/typing_text_str_alias.rs @@ -26,7 +26,7 @@ pub(crate) fn typing_text_str_alias(checker: &mut Checker, expr: &Expr) { .semantic() .resolve_call_path(expr) .map_or(false, |call_path| { - call_path.as_slice() == ["typing", "Text"] + matches!(call_path.as_slice(), ["typing", "Text"]) }) { let mut diagnostic = Diagnostic::new(TypingTextStrAlias, expr.range()); diff --git a/crates/ruff/src/rules/ruff/rules/helpers.rs b/crates/ruff/src/rules/ruff/rules/helpers.rs index 75a9b240d7..65763b7bdc 100644 --- a/crates/ruff/src/rules/ruff/rules/helpers.rs +++ b/crates/ruff/src/rules/ruff/rules/helpers.rs @@ -39,7 +39,7 @@ pub(super) fn is_dataclass(class_def: &ast::StmtClassDef, semantic: &SemanticMod semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { - call_path.as_slice() == ["dataclasses", "dataclass"] + matches!(call_path.as_slice(), ["dataclasses", "dataclass"]) }) }) } diff --git a/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_class.rs b/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_class.rs index 2156550cd0..e12cce062f 100644 --- a/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_class.rs +++ b/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_class.rs @@ -69,7 +69,9 @@ pub(crate) fn raise_vanilla_class(checker: &mut Checker, expr: &Expr) { } else { expr }) - .map_or(false, |call_path| call_path.as_slice() == ["", "Exception"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["", "Exception"]) + }) { checker .diagnostics diff --git a/crates/ruff/src/rules/tryceratops/rules/type_check_without_type_error.rs b/crates/ruff/src/rules/tryceratops/rules/type_check_without_type_error.rs index a909a836a6..26051c90f4 100644 --- a/crates/ruff/src/rules/tryceratops/rules/type_check_without_type_error.rs +++ b/crates/ruff/src/rules/tryceratops/rules/type_check_without_type_error.rs @@ -80,9 +80,10 @@ fn check_type_check_call(checker: &mut Checker, call: &Expr) -> bool { .semantic() .resolve_call_path(call) .map_or(false, |call_path| { - call_path.as_slice() == ["", "isinstance"] - || call_path.as_slice() == ["", "issubclass"] - || call_path.as_slice() == ["", "callable"] + matches!( + call_path.as_slice(), + ["", "isinstance" | "issubclass" | "callable"] + ) }) } @@ -104,25 +105,27 @@ fn is_builtin_exception(checker: &mut Checker, exc: &Expr) -> bool { .semantic() .resolve_call_path(exc) .map_or(false, |call_path| { - [ - "ArithmeticError", - "AssertionError", - "AttributeError", - "BufferError", - "EOFError", - "Exception", - "ImportError", - "LookupError", - "MemoryError", - "NameError", - "ReferenceError", - "RuntimeError", - "SyntaxError", - "SystemError", - "ValueError", - ] - .iter() - .any(|target| call_path.as_slice() == ["", target]) + matches!( + call_path.as_slice(), + [ + "", + "ArithmeticError" + | "AssertionError" + | "AttributeError" + | "BufferError" + | "EOFError" + | "Exception" + | "ImportError" + | "LookupError" + | "MemoryError" + | "NameError" + | "ReferenceError" + | "RuntimeError" + | "SyntaxError" + | "SystemError" + | "ValueError" + ] + ) }); } diff --git a/crates/ruff_python_semantic/src/analyze/function_type.rs b/crates/ruff_python_semantic/src/analyze/function_type.rs index 1a9851cb3f..46d984b276 100644 --- a/crates/ruff_python_semantic/src/analyze/function_type.rs +++ b/crates/ruff_python_semantic/src/analyze/function_type.rs @@ -35,7 +35,7 @@ pub fn classify( semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { - call_path.as_slice() == ["", "staticmethod"] + matches!(call_path.as_slice(), ["", "staticmethod"]) || staticmethod_decorators .iter() .any(|decorator| call_path == from_qualified_name(decorator)) @@ -55,7 +55,7 @@ pub fn classify( || 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| { - call_path.as_slice() == ["", "classmethod"] || + matches!(call_path.as_slice(), ["", "classmethod"]) || classmethod_decorators .iter() .any(|decorator| call_path == from_qualified_name(decorator)) diff --git a/crates/ruff_python_semantic/src/analyze/logging.rs b/crates/ruff_python_semantic/src/analyze/logging.rs index 1976565aa0..a96ebad213 100644 --- a/crates/ruff_python_semantic/src/analyze/logging.rs +++ b/crates/ruff_python_semantic/src/analyze/logging.rs @@ -58,7 +58,7 @@ pub fn exc_info<'a>(keywords: &'a [Keyword], semantic: &SemanticModel) -> Option // Ex) `logging.error("...", exc_info=sys.exc_info())` if let Expr::Call(ast::ExprCall { func, .. }) = &exc_info.value { if semantic.resolve_call_path(func).map_or(false, |call_path| { - call_path.as_slice() == ["sys", "exc_info"] + matches!(call_path.as_slice(), ["sys", "exc_info"]) }) { return Some(exc_info); } diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 58585e425b..30078a4d1e 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -191,16 +191,16 @@ pub fn is_immutable_annotation(expr: &Expr, semantic: &SemanticModel) -> bool { .any(|target| call_path.as_slice() == *target) { true - } else if call_path.as_slice() == ["typing", "Union"] { + } else if matches!(call_path.as_slice(), ["typing", "Union"]) { if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() { elts.iter() .all(|elt| is_immutable_annotation(elt, semantic)) } else { false } - } else if call_path.as_slice() == ["typing", "Optional"] { + } else if matches!(call_path.as_slice(), ["typing", "Optional"]) { is_immutable_annotation(slice, semantic) - } else if call_path.as_slice() == ["typing", "Annotated"] { + } else if matches!(call_path.as_slice(), ["typing", "Annotated"]) { if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() { elts.first() .map_or(false, |elt| is_immutable_annotation(elt, semantic)) @@ -290,7 +290,7 @@ pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> b // Ex) `if typing.TYPE_CHECKING:` if semantic.resolve_call_path(test).map_or(false, |call_path| { - call_path.as_slice() == ["typing", "TYPE_CHECKING"] + matches!(call_path.as_slice(), ["typing", "TYPE_CHECKING"]) }) { return true; } diff --git a/crates/ruff_python_semantic/src/analyze/visibility.rs b/crates/ruff_python_semantic/src/analyze/visibility.rs index 331e912feb..e08916f660 100644 --- a/crates/ruff_python_semantic/src/analyze/visibility.rs +++ b/crates/ruff_python_semantic/src/analyze/visibility.rs @@ -19,7 +19,7 @@ pub fn is_staticmethod(decorator_list: &[Decorator], semantic: &SemanticModel) - semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { - call_path.as_slice() == ["", "staticmethod"] + matches!(call_path.as_slice(), ["", "staticmethod"]) }) }) } @@ -30,7 +30,7 @@ pub fn is_classmethod(decorator_list: &[Decorator], semantic: &SemanticModel) -> semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { - call_path.as_slice() == ["", "classmethod"] + matches!(call_path.as_slice(), ["", "classmethod"]) }) }) } @@ -81,11 +81,12 @@ pub fn is_property( semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { - call_path.as_slice() == ["", "property"] - || call_path.as_slice() == ["functools", "cached_property"] - || extra_properties - .iter() - .any(|extra_property| extra_property.as_slice() == call_path.as_slice()) + matches!( + call_path.as_slice(), + ["", "property"] | ["functools", "cached_property"] + ) || extra_properties + .iter() + .any(|extra_property| extra_property.as_slice() == call_path.as_slice()) }) }) } From 848f184b8c22df8f6de390df1d62a2d7f2b665ce Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 14 Jun 2023 17:13:36 -0400 Subject: [PATCH 061/447] Enable UTC-import for `datetime-utc-alias` fix (#5100) ## Summary Small update to leverage `get_or_import_symbol` to fix `UP017` in more cases (e.g., when we need to import `UTC`, or access it from an alias or something). ## Test Plan Check out the updated snapshot. --- .../pyupgrade/rules/datetime_utc_alias.rs | 22 ++++++------- ...rade__tests__datetime_utc_alias_py311.snap | 33 +++++++++++++++++-- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs index 9840c8086d..5083269d1d 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs @@ -2,9 +2,9 @@ use rustpython_parser::ast::{Expr, Ranged}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::collect_call_path; use crate::checkers::ast::Checker; +use crate::importer::ImportRequest; use crate::registry::AsRule; #[violation] @@ -34,17 +34,15 @@ pub(crate) fn datetime_utc_alias(checker: &mut Checker, expr: &Expr) { { let mut diagnostic = Diagnostic::new(DatetimeTimezoneUTC, expr.range()); if checker.patch(diagnostic.kind.rule()) { - // If the reference was structured as, e.g., `datetime.timezone.utc`, then we can - // replace it with `datetime.UTC`. If `timezone` was imported via `from datetime import - // timezone`, then the replacement is more complicated. - if collect_call_path(expr).map_or(false, |call_path| { - matches!(call_path.as_slice(), ["datetime", "timezone", "utc"]) - }) { - diagnostic.set_fix(Fix::suggested(Edit::range_replacement( - "datetime.UTC".to_string(), - expr.range(), - ))); - } + diagnostic.try_set_fix(|| { + let (import_edit, binding) = checker.importer.get_or_import_symbol( + &ImportRequest::import_from("datetime", "UTC"), + expr.start(), + checker.semantic(), + )?; + let reference_edit = Edit::range_replacement(binding, expr.range()); + Ok(Fix::suggested_edits(import_edit, [reference_edit])) + }); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__datetime_utc_alias_py311.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__datetime_utc_alias_py311.snap index f6a18934b0..7c1bbecb22 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__datetime_utc_alias_py311.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__datetime_utc_alias_py311.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/pyupgrade/mod.rs --- -UP017.py:7:7: UP017 Use `datetime.UTC` alias +UP017.py:7:7: UP017 [*] Use `datetime.UTC` alias | 6 | print(datetime.timezone(-1)) 7 | print(timezone.utc) @@ -10,7 +10,17 @@ UP017.py:7:7: UP017 Use `datetime.UTC` alias | = help: Convert to `datetime.UTC` alias -UP017.py:8:7: UP017 Use `datetime.UTC` alias +ℹ Suggested fix +4 4 | from datetime import timezone as tz +5 5 | +6 6 | print(datetime.timezone(-1)) +7 |-print(timezone.utc) + 7 |+print(datetime.UTC) +8 8 | print(tz.utc) +9 9 | +10 10 | print(datetime.timezone.utc) + +UP017.py:8:7: UP017 [*] Use `datetime.UTC` alias | 6 | print(datetime.timezone(-1)) 7 | print(timezone.utc) @@ -21,6 +31,16 @@ UP017.py:8:7: UP017 Use `datetime.UTC` alias | = help: Convert to `datetime.UTC` alias +ℹ Suggested fix +5 5 | +6 6 | print(datetime.timezone(-1)) +7 7 | print(timezone.utc) +8 |-print(tz.utc) + 8 |+print(datetime.UTC) +9 9 | +10 10 | print(datetime.timezone.utc) +11 11 | print(dt.timezone.utc) + UP017.py:10:7: UP017 [*] Use `datetime.UTC` alias | 8 | print(tz.utc) @@ -39,7 +59,7 @@ UP017.py:10:7: UP017 [*] Use `datetime.UTC` alias 10 |+print(datetime.UTC) 11 11 | print(dt.timezone.utc) -UP017.py:11:7: UP017 Use `datetime.UTC` alias +UP017.py:11:7: UP017 [*] Use `datetime.UTC` alias | 10 | print(datetime.timezone.utc) 11 | print(dt.timezone.utc) @@ -47,4 +67,11 @@ UP017.py:11:7: UP017 Use `datetime.UTC` alias | = help: Convert to `datetime.UTC` alias +ℹ Suggested fix +8 8 | print(tz.utc) +9 9 | +10 10 | print(datetime.timezone.utc) +11 |-print(dt.timezone.utc) + 11 |+print(datetime.UTC) + From 08cd140ea6a1557951e2b9c4a4e8f037f37c7760 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Wed, 14 Jun 2023 23:00:30 +0100 Subject: [PATCH 062/447] Ignore `reimplemented-builtin` if in `async` context (#5101) ## Summary Checks if `checker` is in an `async` context. If yes, return early. Fixes #5098. ## Test Plan `cargo test` --- .../test/fixtures/flake8_simplify/SIM110.py | 14 +++++++++++ .../rules/reimplemented_builtin.rs | 13 +++++++++++ ...ke8_simplify__tests__SIM110_SIM110.py.snap | 23 +++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/crates/ruff/resources/test/fixtures/flake8_simplify/SIM110.py b/crates/ruff/resources/test/fixtures/flake8_simplify/SIM110.py index b02ac7c28c..cc2527e092 100644 --- a/crates/ruff/resources/test/fixtures/flake8_simplify/SIM110.py +++ b/crates/ruff/resources/test/fixtures/flake8_simplify/SIM110.py @@ -171,3 +171,17 @@ def f(): if x.isdigit(): return True return False + +async def f(): + # OK + for x in iterable: + if await check(x): + return True + return False + +async def f(): + # SIM110 + for x in iterable: + if check(x): + return True + 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 0641da8aef..c306b589a8 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs @@ -5,6 +5,7 @@ use rustpython_parser::ast::{ use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::any_over_expr; use ruff_python_ast::source_code::Generator; use crate::checkers::ast::Checker; @@ -224,6 +225,11 @@ fn return_stmt(id: &str, test: &Expr, target: &Expr, iter: &Expr, generator: Gen generator.stmt(&node3.into()) } +/// Return `true` if the `Expr` contains an `await` expression. +fn contains_await(expr: &Expr) -> bool { + any_over_expr(expr, &|expr| matches!(expr, Expr::Await(_))) +} + /// SIM110, SIM111 pub(crate) fn convert_for_loop_to_any_all( checker: &mut Checker, @@ -236,6 +242,13 @@ pub(crate) fn convert_for_loop_to_any_all( if let Some(loop_info) = return_values_for_else(stmt) .or_else(|| sibling.and_then(|sibling| return_values_for_siblings(stmt, sibling))) { + // Check if loop_info.target, loop_info.iter, or loop_info.test contains `await`. + if contains_await(loop_info.target) + || contains_await(loop_info.iter) + || contains_await(loop_info.test) + { + return; + } if loop_info.return_value && !loop_info.next_return_value { if checker.enabled(Rule::ReimplementedBuiltin) { let contents = return_stmt( diff --git a/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM110_SIM110.py.snap b/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM110_SIM110.py.snap index 7c56013102..1a815035bd 100644 --- a/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM110_SIM110.py.snap +++ b/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM110_SIM110.py.snap @@ -294,4 +294,27 @@ SIM110.py:162:5: SIM110 [*] Use `return any(x.isdigit() for x in "012ß9💣2ℝ 167 164 | 168 165 | def f(): +SIM110.py:184:5: SIM110 [*] Use `return any(check(x) for x in iterable)` instead of `for` loop + | +182 | async def f(): +183 | # SIM110 +184 | for x in iterable: + | _____^ +185 | | if check(x): +186 | | return True +187 | | return False + | |________________^ SIM110 + | + = help: Replace with `return any(check(x) for x in iterable)` + +ℹ Suggested fix +181 181 | +182 182 | async def f(): +183 183 | # SIM110 +184 |- for x in iterable: +185 |- if check(x): +186 |- return True +187 |- return False + 184 |+ return any(check(x) for x in iterable) + From 71b3130ff13f849c846acbf5ec4fd20faece3f27 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 14 Jun 2023 18:17:14 -0400 Subject: [PATCH 063/447] Remove manual `await` detection (#5103) We can just use `any_over_expr` instead. --- .../unnecessary_comprehension_any_all.rs | 8 +- .../rules/reimplemented_builtin.rs | 304 +++++++++--------- .../rules/unpacked_list_comprehension.rs | 150 ++------- 3 files changed, 191 insertions(+), 271 deletions(-) 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 6bcdb7864e..ad910495a7 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 @@ -73,7 +73,7 @@ pub(crate) fn unnecessary_comprehension_any_all( let (Expr::ListComp(ast::ExprListComp { elt, .. } )| Expr::SetComp(ast::ExprSetComp { elt, .. })) = &args[0] else { return; }; - if is_async_generator(elt) { + if contains_await(elt) { return; } if !checker.semantic().is_builtin(id) { @@ -89,7 +89,7 @@ pub(crate) fn unnecessary_comprehension_any_all( } } -/// Return `true` if the `Expr` contains an `await` expression. -fn is_async_generator(expr: &Expr) -> bool { - any_over_expr(expr, &|expr| matches!(expr, Expr::Await(_))) +/// Return `true` if the [`Expr`] contains an `await` expression. +fn contains_await(expr: &Expr) -> bool { + any_over_expr(expr, &Expr::is_await_expr) } 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 c306b589a8..fd70c36afb 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs @@ -56,6 +56,156 @@ impl Violation for ReimplementedBuiltin { } } +/// SIM110, SIM111 +pub(crate) fn convert_for_loop_to_any_all( + checker: &mut Checker, + stmt: &Stmt, + sibling: Option<&Stmt>, +) { + // There are two cases to consider: + // - `for` loop with an `else: return True` or `else: return False`. + // - `for` loop followed by `return True` or `return False` + if let Some(loop_info) = return_values_for_else(stmt) + .or_else(|| sibling.and_then(|sibling| return_values_for_siblings(stmt, sibling))) + { + // Check if loop_info.target, loop_info.iter, or loop_info.test contains `await`. + if contains_await(loop_info.target) + || contains_await(loop_info.iter) + || contains_await(loop_info.test) + { + return; + } + if loop_info.return_value && !loop_info.next_return_value { + if checker.enabled(Rule::ReimplementedBuiltin) { + let contents = return_stmt( + "any", + loop_info.test, + loop_info.target, + loop_info.iter, + checker.generator(), + ); + + // Don't flag if the resulting expression would exceed the maximum line length. + let line_start = checker.locator.line_start(stmt.start()); + if LineWidth::new(checker.settings.tab_size) + .add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())]) + .add_str(&contents) + > checker.settings.line_length + { + return; + } + + let mut diagnostic = Diagnostic::new( + ReimplementedBuiltin { + repl: contents.clone(), + }, + TextRange::new(stmt.start(), loop_info.terminal), + ); + if checker.patch(diagnostic.kind.rule()) && checker.semantic().is_builtin("any") { + #[allow(deprecated)] + diagnostic.set_fix(Fix::unspecified(Edit::replacement( + contents, + stmt.start(), + loop_info.terminal, + ))); + } + checker.diagnostics.push(diagnostic); + } + } + + if !loop_info.return_value && loop_info.next_return_value { + if checker.enabled(Rule::ReimplementedBuiltin) { + // Invert the condition. + let test = { + if let Expr::UnaryOp(ast::ExprUnaryOp { + op: Unaryop::Not, + operand, + range: _, + }) = &loop_info.test + { + *operand.clone() + } else if let Expr::Compare(ast::ExprCompare { + left, + ops, + comparators, + range: _, + }) = &loop_info.test + { + if let ([op], [comparator]) = (ops.as_slice(), comparators.as_slice()) { + let op = match op { + Cmpop::Eq => Cmpop::NotEq, + Cmpop::NotEq => Cmpop::Eq, + Cmpop::Lt => Cmpop::GtE, + Cmpop::LtE => Cmpop::Gt, + Cmpop::Gt => Cmpop::LtE, + Cmpop::GtE => Cmpop::Lt, + Cmpop::Is => Cmpop::IsNot, + Cmpop::IsNot => Cmpop::Is, + Cmpop::In => Cmpop::NotIn, + Cmpop::NotIn => Cmpop::In, + }; + let node = ast::ExprCompare { + left: left.clone(), + ops: vec![op], + comparators: vec![comparator.clone()], + range: TextRange::default(), + }; + node.into() + } else { + let node = ast::ExprUnaryOp { + op: Unaryop::Not, + operand: Box::new(loop_info.test.clone()), + range: TextRange::default(), + }; + node.into() + } + } else { + let node = ast::ExprUnaryOp { + op: Unaryop::Not, + operand: Box::new(loop_info.test.clone()), + range: TextRange::default(), + }; + node.into() + } + }; + let contents = return_stmt( + "all", + &test, + loop_info.target, + loop_info.iter, + checker.generator(), + ); + + // Don't flag if the resulting expression would exceed the maximum line length. + let line_start = checker.locator.line_start(stmt.start()); + if LineWidth::new(checker.settings.tab_size) + .add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())]) + .add_str(&contents) + > checker.settings.line_length + { + return; + } + + let mut diagnostic = Diagnostic::new( + ReimplementedBuiltin { + repl: contents.clone(), + }, + TextRange::new(stmt.start(), loop_info.terminal), + ); + if checker.patch(diagnostic.kind.rule()) && checker.semantic().is_builtin("all") { + #[allow(deprecated)] + diagnostic.set_fix(Fix::unspecified(Edit::replacement( + contents, + stmt.start(), + loop_info.terminal, + ))); + } + checker.diagnostics.push(diagnostic); + } + } + } +} + struct Loop<'a> { return_value: bool, next_return_value: bool, @@ -225,157 +375,7 @@ fn return_stmt(id: &str, test: &Expr, target: &Expr, iter: &Expr, generator: Gen generator.stmt(&node3.into()) } -/// Return `true` if the `Expr` contains an `await` expression. +/// Return `true` if the [`Expr`] contains an `await` expression. fn contains_await(expr: &Expr) -> bool { - any_over_expr(expr, &|expr| matches!(expr, Expr::Await(_))) -} - -/// SIM110, SIM111 -pub(crate) fn convert_for_loop_to_any_all( - checker: &mut Checker, - stmt: &Stmt, - sibling: Option<&Stmt>, -) { - // There are two cases to consider: - // - `for` loop with an `else: return True` or `else: return False`. - // - `for` loop followed by `return True` or `return False` - if let Some(loop_info) = return_values_for_else(stmt) - .or_else(|| sibling.and_then(|sibling| return_values_for_siblings(stmt, sibling))) - { - // Check if loop_info.target, loop_info.iter, or loop_info.test contains `await`. - if contains_await(loop_info.target) - || contains_await(loop_info.iter) - || contains_await(loop_info.test) - { - return; - } - if loop_info.return_value && !loop_info.next_return_value { - if checker.enabled(Rule::ReimplementedBuiltin) { - let contents = return_stmt( - "any", - loop_info.test, - loop_info.target, - loop_info.iter, - checker.generator(), - ); - - // Don't flag if the resulting expression would exceed the maximum line length. - let line_start = checker.locator.line_start(stmt.start()); - if LineWidth::new(checker.settings.tab_size) - .add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())]) - .add_str(&contents) - > checker.settings.line_length - { - return; - } - - let mut diagnostic = Diagnostic::new( - ReimplementedBuiltin { - repl: contents.clone(), - }, - TextRange::new(stmt.start(), loop_info.terminal), - ); - if checker.patch(diagnostic.kind.rule()) && checker.semantic().is_builtin("any") { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::replacement( - contents, - stmt.start(), - loop_info.terminal, - ))); - } - checker.diagnostics.push(diagnostic); - } - } - - if !loop_info.return_value && loop_info.next_return_value { - if checker.enabled(Rule::ReimplementedBuiltin) { - // Invert the condition. - let test = { - if let Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::Not, - operand, - range: _, - }) = &loop_info.test - { - *operand.clone() - } else if let Expr::Compare(ast::ExprCompare { - left, - ops, - comparators, - range: _, - }) = &loop_info.test - { - if let ([op], [comparator]) = (ops.as_slice(), comparators.as_slice()) { - let op = match op { - Cmpop::Eq => Cmpop::NotEq, - Cmpop::NotEq => Cmpop::Eq, - Cmpop::Lt => Cmpop::GtE, - Cmpop::LtE => Cmpop::Gt, - Cmpop::Gt => Cmpop::LtE, - Cmpop::GtE => Cmpop::Lt, - Cmpop::Is => Cmpop::IsNot, - Cmpop::IsNot => Cmpop::Is, - Cmpop::In => Cmpop::NotIn, - Cmpop::NotIn => Cmpop::In, - }; - let node = ast::ExprCompare { - left: left.clone(), - ops: vec![op], - comparators: vec![comparator.clone()], - range: TextRange::default(), - }; - node.into() - } else { - let node = ast::ExprUnaryOp { - op: Unaryop::Not, - operand: Box::new(loop_info.test.clone()), - range: TextRange::default(), - }; - node.into() - } - } else { - let node = ast::ExprUnaryOp { - op: Unaryop::Not, - operand: Box::new(loop_info.test.clone()), - range: TextRange::default(), - }; - node.into() - } - }; - let contents = return_stmt( - "all", - &test, - loop_info.target, - loop_info.iter, - checker.generator(), - ); - - // Don't flag if the resulting expression would exceed the maximum line length. - let line_start = checker.locator.line_start(stmt.start()); - if LineWidth::new(checker.settings.tab_size) - .add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())]) - .add_str(&contents) - > checker.settings.line_length - { - return; - } - - let mut diagnostic = Diagnostic::new( - ReimplementedBuiltin { - repl: contents.clone(), - }, - TextRange::new(stmt.start(), loop_info.terminal), - ); - if checker.patch(diagnostic.kind.rule()) && checker.semantic().is_builtin("all") { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::replacement( - contents, - stmt.start(), - loop_info.terminal, - ))); - } - checker.diagnostics.push(diagnostic); - } - } - } + any_over_expr(expr, &Expr::is_await_expr) } 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 3ff0a14ffb..9098faf7fb 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs @@ -2,6 +2,7 @@ use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::any_over_expr; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -20,126 +21,45 @@ impl AlwaysAutofixableViolation for UnpackedListComprehension { } } -/// Returns `true` if `expr` contains an `Expr::Await`. -fn contains_await(expr: &Expr) -> bool { - match expr { - Expr::Await(_) => true, - Expr::BoolOp(ast::ExprBoolOp { values, .. }) => values.iter().any(contains_await), - Expr::NamedExpr(ast::ExprNamedExpr { - target, - value, - range: _, - }) => contains_await(target) || contains_await(value), - Expr::BinOp(ast::ExprBinOp { left, right, .. }) => { - contains_await(left) || contains_await(right) - } - Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => contains_await(operand), - Expr::Lambda(ast::ExprLambda { body, .. }) => contains_await(body), - Expr::IfExp(ast::ExprIfExp { - test, - body, - orelse, - range: _, - }) => contains_await(test) || contains_await(body) || contains_await(orelse), - Expr::Dict(ast::ExprDict { - keys, - values, - range: _, - }) => keys - .iter() - .flatten() - .chain(values.iter()) - .any(contains_await), - Expr::Set(ast::ExprSet { elts, range: _ }) => elts.iter().any(contains_await), - Expr::ListComp(ast::ExprListComp { elt, .. }) => contains_await(elt), - Expr::SetComp(ast::ExprSetComp { elt, .. }) => contains_await(elt), - Expr::DictComp(ast::ExprDictComp { key, value, .. }) => { - contains_await(key) || contains_await(value) - } - Expr::GeneratorExp(ast::ExprGeneratorExp { elt, .. }) => contains_await(elt), - Expr::Yield(ast::ExprYield { value, range: _ }) => { - value.as_ref().map_or(false, |value| contains_await(value)) - } - Expr::YieldFrom(ast::ExprYieldFrom { value, range: _ }) => contains_await(value), - Expr::Compare(ast::ExprCompare { - left, comparators, .. - }) => contains_await(left) || comparators.iter().any(contains_await), - Expr::Call(ast::ExprCall { - func, - args, - keywords, - range: _, - }) => { - contains_await(func) - || args.iter().any(contains_await) - || keywords - .iter() - .any(|keyword| contains_await(&keyword.value)) - } - Expr::FormattedValue(ast::ExprFormattedValue { - value, format_spec, .. - }) => { - contains_await(value) - || format_spec - .as_ref() - .map_or(false, |value| contains_await(value)) - } - Expr::JoinedStr(ast::ExprJoinedStr { values, range: _ }) => { - values.iter().any(contains_await) - } - Expr::Constant(_) => false, - Expr::Attribute(ast::ExprAttribute { value, .. }) => contains_await(value), - Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { - contains_await(value) || contains_await(slice) - } - Expr::Starred(ast::ExprStarred { value, .. }) => contains_await(value), - Expr::Name(_) => false, - Expr::List(ast::ExprList { elts, .. }) => elts.iter().any(contains_await), - Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().any(contains_await), - Expr::Slice(ast::ExprSlice { - lower, - upper, - step, - range: _, - }) => { - lower.as_ref().map_or(false, |value| contains_await(value)) - || upper.as_ref().map_or(false, |value| contains_await(value)) - || step.as_ref().map_or(false, |value| contains_await(value)) - } - } -} - /// UP027 pub(crate) fn unpacked_list_comprehension(checker: &mut Checker, targets: &[Expr], value: &Expr) { let Some(target) = targets.get(0) else { return; }; - if let Expr::Tuple(_) = target { - if let Expr::ListComp(ast::ExprListComp { - elt, - generators, - range: _, - }) = value - { - if generators.iter().any(|generator| generator.is_async) || contains_await(elt) { - return; - } - let mut diagnostic = Diagnostic::new(UnpackedListComprehension, value.range()); - if checker.patch(diagnostic.kind.rule()) { - let existing = checker.locator.slice(value.range()); - - let mut content = String::with_capacity(existing.len()); - content.push('('); - content.push_str(&existing[1..existing.len() - 1]); - content.push(')'); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - content, - value.range(), - ))); - } - checker.diagnostics.push(diagnostic); - } + if !target.is_tuple_expr() { + return; } + + let Expr::ListComp(ast::ExprListComp { + elt, + generators, + range: _, + }) = value else { + return; + }; + + if generators.iter().any(|generator| generator.is_async) || contains_await(elt) { + return; + } + + let mut diagnostic = Diagnostic::new(UnpackedListComprehension, value.range()); + if checker.patch(diagnostic.kind.rule()) { + let existing = checker.locator.slice(value.range()); + + let mut content = String::with_capacity(existing.len()); + content.push('('); + content.push_str(&existing[1..existing.len() - 1]); + content.push(')'); + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( + content, + value.range(), + ))); + } + checker.diagnostics.push(diagnostic); +} + +/// Return `true` if the [`Expr`] contains an `await` expression. +fn contains_await(expr: &Expr) -> bool { + any_over_expr(expr, &Expr::is_await_expr) } From ccbc863960466435be0ea5bf1579d8e4db447e78 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Thu, 15 Jun 2023 00:43:12 +0100 Subject: [PATCH 064/447] Complete `pyupgrade` documentation (#5096) ## Summary Completes the documentation for the `pyupgrade` rules. Related to #2646. ## Test Plan `python scripts/check_docs_formatted.py` --- ...convert_named_tuple_functional_to_class.rs | 30 +++++++++++++ .../convert_typed_dict_functional_to_class.rs | 29 +++++++++++++ .../pyupgrade/rules/datetime_utc_alias.rs | 23 ++++++++++ .../rules/deprecated_c_element_tree.rs | 19 ++++++++ .../pyupgrade/rules/deprecated_import.rs | 17 ++++++++ .../pyupgrade/rules/deprecated_mock_import.rs | 22 ++++++++++ .../rules/deprecated_unittest_alias.rs | 30 +++++++++++++ .../pyupgrade/rules/extraneous_parentheses.rs | 16 +++++++ .../src/rules/pyupgrade/rules/f_strings.rs | 19 ++++++++ .../rules/pyupgrade/rules/format_literals.rs | 24 +++++++++++ .../rules/lru_cache_with_maxsize_none.rs | 30 +++++++++++++ .../rules/lru_cache_without_parameters.rs | 30 +++++++++++++ .../rules/pyupgrade/rules/native_literals.rs | 20 +++++++++ .../src/rules/pyupgrade/rules/open_alias.rs | 25 ++++++++++- .../rules/pyupgrade/rules/os_error_alias.rs | 25 +++++++++++ .../pyupgrade/rules/outdated_version_block.rs | 28 ++++++++++++ .../rules/printf_string_formatting.rs | 22 ++++++++++ .../pyupgrade/rules/quoted_annotation.rs | 30 +++++++++++++ .../pyupgrade/rules/redundant_open_modes.rs | 21 +++++++++ .../pyupgrade/rules/replace_stdout_stderr.rs | 29 ++++++++++++- .../rules/replace_universal_newlines.rs | 27 ++++++++++++ .../rules/super_call_with_parameters.rs | 38 ++++++++++++++++ .../pyupgrade/rules/type_of_primitive.rs | 21 +++++++++ .../pyupgrade/rules/typing_text_str_alias.rs | 43 +++++++++++++++---- .../pyupgrade/rules/unicode_kind_prefix.rs | 38 +++++++++++----- .../rules/unnecessary_builtin_import.rs | 21 +++++++++ .../rules/unnecessary_coding_comment.rs | 20 ++++++++- .../rules/unnecessary_encode_utf8.rs | 19 ++++++++ .../rules/unnecessary_future_import.rs | 24 +++++++++++ .../rules/unpacked_list_comprehension.rs | 21 +++++++++ .../pyupgrade/rules/use_pep585_annotation.rs | 27 ++++++++++++ .../pyupgrade/rules/use_pep604_annotation.rs | 21 +++++++++ .../pyupgrade/rules/use_pep604_isinstance.rs | 24 +++++++++++ .../pyupgrade/rules/useless_metaclass_type.rs | 20 +++++++++ .../rules/useless_object_inheritance.rs | 21 +++++++++ .../pyupgrade/rules/yield_in_for_loop.rs | 21 +++++++++ ...ff__rules__pyupgrade__tests__UP019.py.snap | 8 ++-- ...ff__rules__pyupgrade__tests__UP022.py.snap | 14 +++--- ...ff__rules__pyupgrade__tests__UP025.py.snap | 24 +++++------ scripts/check_docs_formatted.py | 1 + 40 files changed, 896 insertions(+), 46 deletions(-) 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 88da8b51f1..9e83d864cc 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 @@ -13,6 +13,36 @@ use ruff_python_stdlib::identifiers::is_identifier; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for `NamedTuple` declarations that use functional syntax. +/// +/// ## Why is this bad? +/// `NamedTuple` subclasses can be defined either through a functional syntax +/// (`Foo = NamedTuple(...)`) or a class syntax (`class Foo(NamedTuple): ...`). +/// +/// The class syntax is more readable and generally preferred over the +/// functional syntax, which exists primarily for backwards compatibility +/// with `collections.namedtuple`. +/// +/// ## Example +/// ```python +/// from typing import NamedTuple +/// +/// Foo = NamedTuple("Foo", [("a", int), ("b", str)]) +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import NamedTuple +/// +/// +/// class Foo(NamedTuple): +/// a: int +/// b: str +/// ``` +/// +/// ## References +/// - [Python documentation: `typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) #[violation] pub struct ConvertNamedTupleFunctionalToClass { name: String, diff --git a/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs b/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs index cd56786e62..c1d2f5c58d 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs @@ -13,6 +13,35 @@ use ruff_python_stdlib::identifiers::is_identifier; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for `TypedDict` declarations that use functional syntax. +/// +/// ## Why is this bad? +/// `TypedDict` subclasses can be defined either through a functional syntax +/// (`Foo = TypedDict(...)`) or a class syntax (`class Foo(TypedDict): ...`). +/// +/// The class syntax is more readable and generally preferred over the +/// functional syntax. +/// +/// ## Example +/// ```python +/// from typing import TypedDict +/// +/// Foo = TypedDict("Foo", {"a": int, "b": str}) +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import TypedDict +/// +/// +/// class Foo(TypedDict): +/// a: int +/// b: str +/// ``` +/// +/// ## References +/// - [Python documentation: `typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict) #[violation] pub struct ConvertTypedDictFunctionalToClass { name: String, diff --git a/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs index 5083269d1d..b9750fe7ac 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs @@ -7,6 +7,29 @@ use crate::checkers::ast::Checker; use crate::importer::ImportRequest; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of `datetime.timezone.utc`. +/// +/// ## Why is this bad? +/// As of Python 3.11, `datetime.UTC` is an alias for `datetime.timezone.utc`. +/// The alias is more readable and generally preferred over the full path. +/// +/// ## Example +/// ```python +/// import datetime +/// +/// datetime.timezone.utc +/// ``` +/// +/// Use instead: +/// ```python +/// import datetime +/// +/// datetime.UTC +/// ``` +/// +/// ## References +/// - [Python documentation: `datetime.UTC`](https://docs.python.org/3/library/datetime.html#datetime.UTC) #[violation] pub struct DatetimeTimezoneUTC; diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs index dddc5b328f..0b5a075100 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs @@ -6,6 +6,25 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of the `xml.etree.cElementTree` module. +/// +/// ## Why is this bad? +/// In Python 3.3, `xml.etree.cElementTree` was deprecated in favor of +/// `xml.etree.ElementTree`. +/// +/// ## Example +/// ```python +/// from xml.etree import cElementTree +/// ``` +/// +/// Use instead: +/// ```python +/// from xml.etree import ElementTree +/// ``` +/// +/// ## References +/// - [Python documentation: `xml.etree.ElementTree`](https://docs.python.org/3/library/xml.etree.elementtree.html) #[violation] pub struct DeprecatedCElementTree; diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs index b2c5c62394..9f7421be15 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs @@ -35,6 +35,23 @@ enum Deprecation { WithoutRename(WithoutRename), } +/// ## What it does +/// Checks for uses of deprecated imports based on the minimum supported +/// Python version. +/// +/// ## Why is this bad? +/// Deprecated imports may be removed in future versions of Python, and +/// should be replaced with their new equivalents. +/// +/// ## Example +/// ```python +/// from collections import Sequence +/// ``` +/// +/// Use instead: +/// ```python +/// from collections.abc import Sequence +/// ``` #[violation] pub struct DeprecatedImport { deprecation: Deprecation, diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs index 0c85c8f47b..68c3a20ab1 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs @@ -23,6 +23,28 @@ pub(crate) enum MockReference { Attribute, } +/// ## What it does +/// Checks for imports of the `mock` module that should be replaced with +/// `unittest.mock`. +/// +/// ## Why is this bad? +/// Since Python 3.3, `mock` has been a part of the standard library as +/// `unittest.mock`. The `mock` package is deprecated; use `unittest.mock` +/// instead. +/// +/// ## Example +/// ```python +/// import mock +/// ``` +/// +/// Use instead: +/// ```python +/// from unittest import mock +/// ``` +/// +/// ## References +/// - [Python documentation: `unittest.mock`](https://docs.python.org/3/library/unittest.mock.html) +/// - [PyPI: `mock`](https://pypi.org/project/mock/) #[violation] pub struct DeprecatedMockImport { reference_type: MockReference, diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs index e73b6a1efc..19cc009f00 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs @@ -8,6 +8,36 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of deprecated methods from the `unittest` module. +/// +/// ## Why is this bad? +/// The `unittest` module has deprecated aliases for some of its methods. +/// The aliases may be removed in future versions of Python. Instead, +/// use their non-deprecated counterparts. +/// +/// ## Example +/// ```python +/// from unittest import TestCase +/// +/// +/// class SomeTest(TestCase): +/// def test_something(self): +/// self.assertEquals(1, 1) +/// ``` +/// +/// Use instead: +/// ```python +/// from unittest import TestCase +/// +/// +/// class SomeTest(TestCase): +/// def test_something(self): +/// self.assertEqual(1, 1) +/// ``` +/// +/// ## References +/// - [Python documentation: Deprecated aliases](https://docs.python.org/3/library/unittest.html#deprecated-aliases) #[violation] pub struct DeprecatedUnittestAlias { alias: String, diff --git a/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs b/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs index 25884db60b..4eb02acef1 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs @@ -9,6 +9,22 @@ use ruff_python_ast::source_code::Locator; use crate::registry::Rule; use crate::settings::Settings; +/// ## What it does +/// Checks for extraneous parentheses. +/// +/// ## Why is this bad? +/// Extraneous parentheses are redundant, and can be removed to improve +/// readability while retaining identical semantics. +/// +/// ## Example +/// ```python +/// print(("Hello, world")) +/// ``` +/// +/// Use instead: +/// ```python +/// print("Hello, world") +/// ``` #[violation] pub struct ExtraneousParentheses; diff --git a/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs b/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs index 2c23330d2d..896556ed04 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs @@ -17,6 +17,25 @@ use crate::registry::AsRule; use crate::rules::pyflakes::format::FormatSummary; use crate::rules::pyupgrade::helpers::curly_escape; +/// ## What it does +/// Checks for `str#format` calls that can be replaced with f-strings. +/// +/// ## Why is this bad? +/// f-strings are more readable and generally preferred over `str#format` +/// calls. +/// +/// ## Example +/// ```python +/// "{}".format(foo) +/// ``` +/// +/// Use instead: +/// ```python +/// f"{foo}" +/// ``` +/// +/// ## References +/// - [Python documentation: f-strings](https://docs.python.org/3/reference/lexical_analysis.html#f-strings) #[violation] pub struct FString; diff --git a/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs b/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs index dde4279aab..a15974eb94 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs @@ -14,6 +14,30 @@ use crate::cst::matchers::{match_attribute, match_call_mut, match_expression}; use crate::registry::AsRule; use crate::rules::pyflakes::format::FormatSummary; +/// ## What it does +/// Checks for unnecessary positional indices in format strings. +/// +/// ## Why is this bad? +/// In Python 3.1 and later, format strings can use implicit positional +/// references. For example, `"{0}, {1}".format("Hello", "World")` can be +/// rewritten as `"{}, {}".format("Hello", "World")`. +/// +/// If the positional indices appear exactly in-order, they can be omitted +/// in favor of automatic indices to improve readability. +/// +/// ## Example +/// ```python +/// "{0}, {1}".format("Hello", "World") # "Hello, World" +/// ``` +/// +/// Use instead: +/// ```python +/// "{}, {}".format("Hello", "World") # "Hello, World" +/// ``` +/// +/// ## References +/// - [Python documentation: Format String Syntax](https://docs.python.org/3/library/string.html#format-string-syntax) +/// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[violation] pub struct FormatLiterals; 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 7be2761a0e..497e1bdf6d 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 @@ -8,6 +8,36 @@ use crate::checkers::ast::Checker; use crate::importer::ImportRequest; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of `functools.lru_cache` that set `maxsize=None`. +/// +/// ## Why is this bad? +/// Since Python 3.9, `functools.cache` can be used as a drop-in replacement +/// for `functools.lru_cache(maxsize=None)`. When possible, prefer +/// `functools.cache` as it is more readable and idiomatic. +/// +/// ## Example +/// ```python +/// import functools +/// +/// +/// @functools.lru_cache(maxsize=None) +/// def foo(): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// import functools +/// +/// +/// @functools.cache +/// def foo(): +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: `@functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache) #[violation] pub struct LRUCacheWithMaxsizeNone; 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 5088b3398c..07cb2999b5 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 @@ -7,6 +7,36 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for unnecessary parentheses on `functools.lru_cache` decorators. +/// +/// ## Why is this bad? +/// Since Python 3.8, `functools.lru_cache` can be used as a decorator without +/// trailing parentheses, as long as no arguments are passed to it. +/// +/// ## Example +/// ```python +/// import functools +/// +/// +/// @functools.lru_cache() +/// def foo(): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// import functools +/// +/// +/// @functools.lru_cache +/// def foo(): +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: `@functools.lru_cache`](https://docs.python.org/3/library/functools.html#functools.lru_cache) +/// - [Let lru_cache be used as a decorator with no arguments](https://github.com/python/cpython/issues/80953) #[violation] pub struct LRUCacheWithoutParameters; diff --git a/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs b/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs index aac0f59c0d..e62888b9b9 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs @@ -24,6 +24,26 @@ impl fmt::Display for LiteralType { } } +/// ## What it does +/// Checks for unnecessary calls to `str` and `bytes`. +/// +/// ## Why is this bad? +/// The `str` and `bytes` constructors can be replaced with string and bytes +/// literals, which are more readable and idiomatic. +/// +/// ## Example +/// ```python +/// str("foo") +/// ``` +/// +/// Use instead: +/// ```python +/// "foo" +/// ``` +/// +/// ## References +/// - [Python documentation: `str`](https://docs.python.org/3/library/stdtypes.html#str) +/// - [Python documentation: `bytes`](https://docs.python.org/3/library/stdtypes.html#bytes) #[violation] pub struct NativeLiterals { literal_type: LiteralType, diff --git a/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs index 584602d996..259615755a 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs @@ -6,6 +6,29 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of `io.open`. +/// +/// ## Why is this bad? +/// In Python 3, `io.open` is an alias for `open`. Prefer using `open` directly, +/// as it is more idiomatic. +/// +/// ## Example +/// ```python +/// import io +/// +/// with io.open("file.txt") as f: +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// with open("file.txt") as f: +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: `io.open`](https://docs.python.org/3/library/io.html#io.open) #[violation] pub struct OpenAlias; @@ -33,7 +56,7 @@ pub(crate) fn open_alias(checker: &mut Checker, expr: &Expr, func: &Expr) { { let mut diagnostic = Diagnostic::new(OpenAlias, expr.range()); if checker.patch(diagnostic.kind.rule()) { - if checker.semantic().is_available("open") { + if checker.semantic().is_builtin("open") { diagnostic.set_fix(Fix::suggested(Edit::range_replacement( "open".to_string(), func.range(), diff --git a/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs index 666030fada..5068d07348 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs @@ -9,6 +9,31 @@ use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of exceptions that alias `OSError`. +/// +/// ## Why is this bad? +/// `OSError` is the builtin error type used for exceptions that relate to the +/// operating system. +/// +/// In Python 3.3, a variety of other exceptions, like `WindowsError` were +/// aliased to `OSError`. These aliases remain in place for compatibility with +/// older versions of Python, but may be removed in future versions. +/// +/// Prefer using `OSError` directly, as it is more idiomatic and future-proof. +/// +/// ## Example +/// ```python +/// raise IOError +/// ``` +/// +/// Use instead: +/// ```python +/// raise OSError +/// ``` +/// +/// ## References +/// - [Python documentation: `OSError`](https://docs.python.org/3/library/exceptions.html#OSError) #[violation] pub struct OSErrorAlias { pub name: Option, 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 b0bb74a37f..6f40f2786c 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs @@ -16,6 +16,34 @@ use crate::registry::AsRule; use crate::rules::pyupgrade::fixes::adjust_indentation; use crate::settings::types::PythonVersion; +/// ## What it does +/// Checks for conditional blocks gated on `sys.version_info` comparisons +/// that are outdated for the minimum supported Python version. +/// +/// ## Why is this bad? +/// In Python, code can be conditionally executed based on the active +/// Python version by comparing against the `sys.version_info` tuple. +/// +/// If a code block is only executed for Python versions older than the +/// minimum supported version, it should be removed. +/// +/// ## Example +/// ```python +/// import sys +/// +/// if sys.version_info < (3, 0): +/// print("py2") +/// else: +/// print("py3") +/// ``` +/// +/// Use instead: +/// ```python +/// print("py3") +/// ``` +/// +/// ## References +/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] pub struct OutdatedVersionBlock; diff --git a/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs b/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs index 5bcf6c764d..56008adfc4 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs @@ -18,6 +18,28 @@ use crate::checkers::ast::Checker; use crate::registry::AsRule; use crate::rules::pyupgrade::helpers::curly_escape; +/// ## What it does +/// Checks for `printf`-style string formatting. +/// +/// ## Why is this bad? +/// `printf`-style string formatting has a number of quirks, and leads to less +/// readable code than using `str.format` calls or f-strings. In general, prefer +/// the newer `str.format` and f-strings constructs over `printf`-style string +/// formatting. +/// +/// ## Example +/// ```python +/// "%s, %s" % ("Hello", "World") # "Hello, World" +/// ``` +/// +/// Use instead: +/// ```python +/// "{}, {}".format("Hello", "World") # "Hello, World" +/// ``` +/// +/// ## References +/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#old-string-formatting) +/// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[violation] pub struct PrintfStringFormatting; diff --git a/crates/ruff/src/rules/pyupgrade/rules/quoted_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/quoted_annotation.rs index 1a8a29e0a6..c14ad79904 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/quoted_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/quoted_annotation.rs @@ -6,6 +6,36 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::registry::Rule; +/// ## What it does +/// Checks for the presence of unnecessary quotes in type annotations. +/// +/// ## Why is this bad? +/// In Python, type annotations can be quoted to avoid forward references. +/// However, if `from __future__ import annotations` is present, Python +/// will always evaluate type annotations in a deferred manner, making +/// the quotes unnecessary. +/// +/// ## Example +/// ```python +/// from __future__ import annotations +/// +/// +/// def foo(bar: "Bar") -> "Bar": +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// from __future__ import annotations +/// +/// +/// def foo(bar: Bar) -> Bar: +/// ... +/// ``` +/// +/// ## References +/// - [PEP 563](https://peps.python.org/pep-0563/) +/// - [Python documentation: `__future__` - Future statement definitions](https://docs.python.org/3/library/__future__.html#module-__future__) #[violation] pub struct QuotedAnnotation; diff --git a/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs b/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs index b65019e999..3646b9ad86 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs @@ -13,6 +13,27 @@ use ruff_python_ast::source_code::Locator; use crate::checkers::ast::Checker; use crate::registry::Rule; +/// ## What it does +/// Checks for redundant `open` mode parameters. +/// +/// ## Why is this bad? +/// Redundant `open` mode parameters are unnecessary and should be removed to +/// avoid confusion. +/// +/// ## Example +/// ```python +/// with open("foo.txt", "r") as f: +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// with open("foo.txt") as f: +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open) #[violation] pub struct RedundantOpenModes { pub replacement: Option, 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 83f18f75c5..4dab18b024 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs @@ -10,13 +10,40 @@ use crate::autofix::edits::remove_argument; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of `subprocess.run` that send `stdout` and `stderr` to a +/// pipe. +/// +/// ## Why is this bad? +/// As of Python 3.7, `subprocess.run` has a `capture_output` keyword argument +/// that can be set to `True` to capture `stdout` and `stderr` outputs. This is +/// equivalent to setting `stdout` and `stderr` to `subprocess.PIPE`, but is +/// more explicit and readable. +/// +/// ## Example +/// ```python +/// import subprocess +/// +/// subprocess.run(["foo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) +/// ``` +/// +/// Use instead: +/// ```python +/// import subprocess +/// +/// subprocess.run(["foo"], capture_output=True) +/// ``` +/// +/// ## References +/// - [Python 3.7 release notes](https://docs.python.org/3/whatsnew/3.7.html#subprocess) +/// - [Python documentation: `subprocess.run`](https://docs.python.org/3/library/subprocess.html#subprocess.run) #[violation] pub struct ReplaceStdoutStderr; impl AlwaysAutofixableViolation for ReplaceStdoutStderr { #[derive_message_formats] fn message(&self) -> String { - format!("Sending stdout and stderr to pipe is deprecated, use `capture_output`") + format!("Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output`") } fn autofix_title(&self) -> String { diff --git a/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs b/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs index e46f87cc04..2e444211d5 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs @@ -8,6 +8,33 @@ use ruff_python_ast::helpers::find_keyword; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of `subprocess.run` that set the `universal_newlines` +/// keyword argument. +/// +/// ## Why is this bad? +/// As of Python 3.7, the `universal_newlines` keyword argument has been +/// renamed to `text`, and now exists for backwards compatibility. The +/// `universal_newlines` keyword argument may be removed in a future version of +/// Python. Prefer `text`, which is more explicit and readable. +/// +/// ## Example +/// ```python +/// import subprocess +/// +/// subprocess.run(["foo"], universal_newlines=True) +/// ``` +/// +/// Use instead: +/// ```python +/// import subprocess +/// +/// subprocess.run(["foo"], text=True) +/// ``` +/// +/// ## References +/// - [Python 3.7 release notes](https://docs.python.org/3/whatsnew/3.7.html#subprocess) +/// - [Python documentation: `subprocess.run`](https://docs.python.org/3/library/subprocess.html#subprocess.run) #[violation] pub struct ReplaceUniversalNewlines; 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 a7098a0eab..898ef4f364 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 @@ -7,6 +7,44 @@ use crate::checkers::ast::Checker; use crate::registry::AsRule; use crate::rules::pyupgrade::fixes; +/// ## What it does +/// Checks for `super` calls that pass redundant arguments. +/// +/// ## Why is this bad? +/// In Python 3, `super` can be invoked without any arguments when: (1) the +/// first argument is `__class__`, and (2) the second argument is equivalent to +/// the first argument of the enclosing method. +/// +/// When possible, omit the arguments to `super` to make the code more concise +/// and maintainable. +/// +/// ## Example +/// ```python +/// class A: +/// def foo(self): +/// pass +/// +/// +/// class B(A): +/// def bar(self): +/// super(B, self).foo() +/// ``` +/// +/// Use instead: +/// ```python +/// class A: +/// def foo(self): +/// pass +/// +/// +/// class B(A): +/// def bar(self): +/// super().foo() +/// ``` +/// +/// ## References +/// - [Python documentation: `super`](https://docs.python.org/3/library/functions.html#super) +/// - [super/MRO, Python's most misunderstood feature.](https://www.youtube.com/watch?v=X1PQ7zzltz4) #[violation] pub struct SuperCallWithParameters; 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 9ee8cecfb2..e432547cde 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs @@ -8,6 +8,27 @@ use crate::registry::AsRule; use super::super::types::Primitive; +/// ## What it does +/// Checks for uses of `type` that take a primitive as an argument. +/// +/// ## Why is this bad? +/// `type()` returns the type of a given object. A type of a primitive can +/// always be known in advance and accessed directly, which is more concise +/// and explicit than using `type()`. +/// +/// ## Example +/// ```python +/// type(1) +/// ``` +/// +/// Use instead: +/// ```python +/// int +/// ``` +/// +/// ## References +/// - [Python documentation: `type()`](https://docs.python.org/3/library/functions.html#type) +/// - [Python documentation: Built-in types](https://docs.python.org/3/library/stdtypes.html) #[violation] pub struct TypeOfPrimitive { primitive: Primitive, diff --git a/crates/ruff/src/rules/pyupgrade/rules/typing_text_str_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/typing_text_str_alias.rs index cfdd0bceb5..ea8d0955ce 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/typing_text_str_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/typing_text_str_alias.rs @@ -1,22 +1,46 @@ use rustpython_parser::ast::{Expr, Ranged}; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; +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 `typing.Text`. +/// +/// ## Why is this bad? +/// `typing.Text` is an alias for `str`, and only exists for Python 2 +/// compatibility. As of Python 3.11, `typing.Text` is deprecated. Use `str` +/// instead. +/// +/// ## Example +/// ```python +/// from typing import Text +/// +/// foo: Text = "bar" +/// ``` +/// +/// Use instead: +/// ```python +/// foo: str = "bar" +/// ``` +/// +/// ## References +/// - [Python documentation: `typing.Text`](https://docs.python.org/3/library/typing.html#typing.Text) #[violation] pub struct TypingTextStrAlias; -impl AlwaysAutofixableViolation for TypingTextStrAlias { +impl Violation for TypingTextStrAlias { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { format!("`typing.Text` is deprecated, use `str`") } - fn autofix_title(&self) -> String { - "Replace with `str`".to_string() + fn autofix_title(&self) -> Option { + Some("Replace with `str`".to_string()) } } @@ -31,11 +55,12 @@ pub(crate) fn typing_text_str_alias(checker: &mut Checker, expr: &Expr) { { let mut diagnostic = Diagnostic::new(TypingTextStrAlias, expr.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - "str".to_string(), - expr.range(), - ))); + if checker.semantic().is_builtin("str") { + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( + "str".to_string(), + expr.range(), + ))); + } } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/unicode_kind_prefix.rs b/crates/ruff/src/rules/pyupgrade/rules/unicode_kind_prefix.rs index 45dbf962d5..4058be5de3 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unicode_kind_prefix.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unicode_kind_prefix.rs @@ -7,6 +7,25 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of the Unicode kind prefix (`u`) in strings. +/// +/// ## Why is this bad? +/// In Python 3, all strings are Unicode by default. The Unicode kind prefix is +/// unnecessary and should be removed to avoid confusion. +/// +/// ## Example +/// ```python +/// u"foo" +/// ``` +/// +/// Use instead: +/// ```python +/// "foo" +/// ``` +/// +/// ## References +/// - [Python documentation: Unicode HOWTO](https://docs.python.org/3/howto/unicode.html) #[violation] pub struct UnicodeKindPrefix; @@ -23,17 +42,14 @@ impl AlwaysAutofixableViolation for UnicodeKindPrefix { /// UP025 pub(crate) fn unicode_kind_prefix(checker: &mut Checker, expr: &Expr, kind: Option<&str>) { - if let Some(const_kind) = kind { - if const_kind.to_lowercase() == "u" { - let mut diagnostic = Diagnostic::new(UnicodeKindPrefix, expr.range()); - if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(TextRange::at( - expr.start(), - TextSize::from(1), - )))); - } - checker.diagnostics.push(diagnostic); + if matches!(kind, Some("u" | "U")) { + let mut diagnostic = Diagnostic::new(UnicodeKindPrefix, expr.range()); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.set_fix(Fix::automatic(Edit::range_deletion(TextRange::at( + expr.start(), + TextSize::from(1), + )))); } + checker.diagnostics.push(diagnostic); } } diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs index 9353ad8c32..070a1c4aaa 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs @@ -8,6 +8,27 @@ use crate::autofix; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for unnecessary imports of builtins. +/// +/// ## Why is this bad? +/// Builtins are always available. Importing them is unnecessary and should be +/// removed to avoid confusion. +/// +/// ## Example +/// ```python +/// from builtins import str +/// +/// str(1) +/// ``` +/// +/// Use instead: +/// ```python +/// str(1) +/// ``` +/// +/// ## References +/// - [Python documentation: The Python Standard Library](https://docs.python.org/3/library/index.html) #[violation] pub struct UnnecessaryBuiltinImport { pub names: Vec, diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs index 7b474ab5f0..195607e58e 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs @@ -5,7 +5,25 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_whitespace::Line; -// TODO: document referencing [PEP 3120]: https://peps.python.org/pep-3120/ +/// ## What it does +/// Checks for unnecessary UTF-8 encoding declarations. +/// +/// ## Why is this bad? +/// [PEP 3120] makes UTF-8 the default encoding, so a UTF-8 encoding +/// declaration is unnecessary. +/// +/// ## Example +/// ```python +/// # -*- coding: utf-8 -*- +/// print("Hello, world!") +/// ``` +/// +/// Use instead: +/// ```python +/// print("Hello, world!") +/// ``` +/// +/// [PEP 3120]: https://peps.python.org/pep-3120/ #[violation] pub struct UTF8EncodingDeclaration; 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 3e286b2d95..ecd0505c49 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs @@ -16,6 +16,25 @@ pub(crate) enum Reason { DefaultArgument, } +/// ## What it does +/// Checks for unnecessary calls to `encode` as UTF-8. +/// +/// ## Why is this bad? +/// UTF-8 is the default encoding in Python, so there is no need to call +/// `encode` when UTF-8 is the desired encoding. Instead, use a bytes literal. +/// +/// ## Example +/// ```python +/// "foo".encode("utf-8") +/// ``` +/// +/// Use instead: +/// ```python +/// b"foo" +/// ``` +/// +/// ## References +/// - [Python documentation: `str.encode`](https://docs.python.org/3/library/stdtypes.html#str.encode) #[violation] pub struct UnnecessaryEncodeUTF8 { reason: Reason, diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs index f3f1bbd609..676a5ad928 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs @@ -8,6 +8,30 @@ use crate::autofix; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for unnecessary `__future__` imports. +/// +/// ## Why is this bad? +/// The `__future__` module is used to enable features that are not yet +/// available in the current Python version. If a feature is already +/// available in the minimum supported Python version, importing it +/// from `__future__` is unnecessary and should be removed to avoid +/// confusion. +/// +/// ## Example +/// ```python +/// from __future__ import print_function +/// +/// print("Hello, world!") +/// ``` +/// +/// Use instead: +/// ```python +/// print("Hello, world!") +/// ``` +/// +/// ## References +/// - [Python documentation: `__future__` — Future statement definitions](https://docs.python.org/3/library/__future__.html) #[violation] pub struct UnnecessaryFutureImport { pub names: Vec, 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 9098faf7fb..939b5e98bd 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs @@ -7,6 +7,27 @@ use ruff_python_ast::helpers::any_over_expr; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for list comprehensions that are immediately unpacked. +/// +/// ## Why is this bad? +/// There is no reason to use a list comprehension if the result is immediately +/// unpacked. Instead, use a generator expression, which is more efficient as +/// it avoids allocating an intermediary list. +/// +/// ## Example +/// ```python +/// a, b, c = [foo(x) for x in items] +/// ``` +/// +/// Use instead: +/// ```python +/// a, b, c = (foo(x) for x in items) +/// ``` +/// +/// ## References +/// - [Python documentation: Generator expressions](https://docs.python.org/3/reference/expressions.html#generator-expressions) +/// - [Python documentation: List comprehensions](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions) #[violation] pub struct UnpackedListComprehension; diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep585_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep585_annotation.rs index 5c8305e185..42677e097b 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep585_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep585_annotation.rs @@ -9,6 +9,33 @@ use crate::checkers::ast::Checker; use crate::importer::ImportRequest; use crate::registry::AsRule; +/// ## What it does +/// Checks for the use of generics that can be replaced with standard library +/// variants based on [PEP 585]. +/// +/// ## Why is this bad? +/// [PEP 585] enabled collections in the Python standard library (like `list`) +/// to be used as generics directly, instead of importing analogous members +/// from the `typing` module (like `typing.List`). +/// +/// When available, the [PEP 585] syntax should be used instead of importing +/// members from the `typing` module, as it's more concise and readable. +/// Importing those members from `typing` is considered deprecated as of PEP +/// 585. +/// +/// ## Example +/// ```python +/// from typing import List +/// +/// foo: List[int] = [1, 2, 3] +/// ``` +/// +/// Use instead: +/// ```python +/// foo: list[int] = [1, 2, 3] +/// ``` +/// +/// [PEP 585]: https://peps.python.org/pep-0585/ #[violation] pub struct NonPEP585Annotation { from: String, diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs index 67589ca86e..f96ad905d7 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs @@ -8,6 +8,27 @@ use ruff_python_semantic::analyze::typing::Pep604Operator; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Check for type annotations that can be rewritten based on [PEP 604] syntax. +/// +/// ## Why is this bad? +/// [PEP 604] introduced a new syntax for union type annotations based on the +/// `|` operator. This syntax is more concise and readable than the previous +/// `typing.Union` and `typing.Optional` syntaxes. +/// +/// ## Example +/// ```python +/// from typing import Union +/// +/// foo: Union[int, str] = 1 +/// ``` +/// +/// Use instead: +/// ```python +/// foo: int | str = 1 +/// ``` +/// +/// [PEP 604]: https://peps.python.org/pep-0604/ #[violation] pub struct NonPEP604Annotation; diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs index c0f8d6d28b..aa7ce7c1a8 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs @@ -34,6 +34,30 @@ impl CallKind { } } +/// ## What it does +/// Checks for uses of `isinstance` and `issubclass` that take a tuple +/// of types for comparison. +/// +/// ## Why is this bad? +/// Since Python 3.10, `isinstance` and `issubclass` can be passed a +/// `|`-separated union of types, which is more concise and consistent +/// with the union operator introduced in [PEP 604]. +/// +/// ## Example +/// ```python +/// isinstance(x, (int, float)) +/// ``` +/// +/// Use instead: +/// ```python +/// isinstance(x, int | float) +/// ``` +/// +/// ## References +/// - [Python documentation: `isinstance`](https://docs.python.org/3/library/functions.html#isinstance) +/// - [Python documentation: `issubclass`](https://docs.python.org/3/library/functions.html#issubclass) +/// +/// [PEP 604]: https://peps.python.org/pep-0604/ #[violation] pub struct NonPEP604Isinstance { kind: CallKind, 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 104384c246..a739ccad3e 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs @@ -7,6 +7,26 @@ use crate::autofix; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for the use of `__metaclass__ = type` in class definitions. +/// +/// ## Why is this bad? +/// Since Python 3, `__metaclass__ = type` is implied and can thus be omitted. +/// +/// ## Example +/// ```python +/// class Foo: +/// __metaclass__ = type +/// ``` +/// +/// Use instead: +/// ```python +/// class Foo: +/// ... +/// ``` +/// +/// ## References +/// - [PEP 3115](https://www.python.org/dev/peps/pep-3115/) #[violation] pub struct UselessMetaclassType; 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 4c8788324b..d3a86d91fb 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs @@ -8,6 +8,27 @@ use crate::autofix::edits::remove_argument; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for classes that inherit from `object`. +/// +/// ## Why is this bad? +/// Since Python 3, all classes inherit from `object` by default, so `object` can +/// be omitted from the list of base classes. +/// +/// ## Example +/// ```python +/// class Foo(object): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// class Foo: +/// ... +/// ``` +/// +/// ## References +/// - [PEP 3115](https://www.python.org/dev/peps/pep-3115/) #[violation] pub struct UselessObjectInheritance { name: String, diff --git a/crates/ruff/src/rules/pyupgrade/rules/yield_in_for_loop.rs b/crates/ruff/src/rules/pyupgrade/rules/yield_in_for_loop.rs index 999df0fd04..6533a26278 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/yield_in_for_loop.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/yield_in_for_loop.rs @@ -11,6 +11,27 @@ use ruff_python_ast::{statement_visitor, visitor}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for `for` loops that can be replaced with `yield from` expressions. +/// +/// ## Why is this bad? +/// If a `for` loop only contains a `yield` statement, it can be replaced with a +/// `yield from` expression, which is more concise and idiomatic. +/// +/// ## Example +/// ```python +/// for x in foo: +/// yield x +/// ``` +/// +/// Use instead: +/// ```python +/// yield from foo +/// ``` +/// +/// ## References +/// - [Python documentation: The `yield` statement](https://docs.python.org/3/reference/simple_stmts.html#the-yield-statement) +/// - [PEP 380](https://peps.python.org/pep-0380/) #[violation] pub struct YieldInForLoop; diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP019.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP019.py.snap index c4697f0e39..291ef30d33 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP019.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP019.py.snap @@ -9,7 +9,7 @@ UP019.py:7:22: UP019 [*] `typing.Text` is deprecated, use `str` | = help: Replace with `str` -ℹ Suggested fix +ℹ Fix 4 4 | from typing import Text as Goodbye 5 5 | 6 6 | @@ -27,7 +27,7 @@ UP019.py:11:29: UP019 [*] `typing.Text` is deprecated, use `str` | = help: Replace with `str` -ℹ Suggested fix +ℹ Fix 8 8 | print(word) 9 9 | 10 10 | @@ -45,7 +45,7 @@ UP019.py:15:28: UP019 [*] `typing.Text` is deprecated, use `str` | = help: Replace with `str` -ℹ Suggested fix +ℹ Fix 12 12 | print(word) 13 13 | 14 14 | @@ -63,7 +63,7 @@ UP019.py:19:29: UP019 [*] `typing.Text` is deprecated, use `str` | = help: Replace with `str` -ℹ Suggested fix +ℹ Fix 16 16 | print(word) 17 17 | 18 18 | diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP022.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP022.py.snap index 6aade82834..75df54757d 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP022.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP022.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/pyupgrade/mod.rs --- -UP022.py:4:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use `capture_output` +UP022.py:4:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output` | 2 | import subprocess 3 | @@ -22,7 +22,7 @@ UP022.py:4:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use `c 6 6 | output = subprocess.run(["foo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 7 7 | -UP022.py:6:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use `capture_output` +UP022.py:6:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output` | 4 | output = run(["foo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 5 | @@ -43,7 +43,7 @@ UP022.py:6:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use `c 8 8 | output = subprocess.run(stdout=subprocess.PIPE, args=["foo"], stderr=subprocess.PIPE) 9 9 | -UP022.py:8:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use `capture_output` +UP022.py:8:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output` | 6 | output = subprocess.run(["foo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 7 | @@ -64,7 +64,7 @@ UP022.py:8:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use `c 10 10 | output = subprocess.run( 11 11 | ["foo"], stdout=subprocess.PIPE, check=True, stderr=subprocess.PIPE -UP022.py:10:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use `capture_output` +UP022.py:10:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output` | 8 | output = subprocess.run(stdout=subprocess.PIPE, args=["foo"], stderr=subprocess.PIPE) 9 | @@ -88,7 +88,7 @@ UP022.py:10:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use ` 13 13 | 14 14 | output = subprocess.run( -UP022.py:14:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use `capture_output` +UP022.py:14:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output` | 12 | ) 13 | @@ -112,7 +112,7 @@ UP022.py:14:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use ` 17 17 | 18 18 | output = subprocess.run( -UP022.py:18:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use `capture_output` +UP022.py:18:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output` | 16 | ) 17 | @@ -144,7 +144,7 @@ UP022.py:18:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use ` 24 23 | encoding="utf-8", 25 24 | close_fds=True, -UP022.py:29:14: UP022 [*] Sending stdout and stderr to pipe is deprecated, use `capture_output` +UP022.py:29:14: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output` | 28 | if output: 29 | output = subprocess.run( diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP025.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP025.py.snap index 0ef2bd7b18..380d022354 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP025.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP025.py.snap @@ -11,7 +11,7 @@ UP025.py:2:5: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 1 1 | # These should change 2 |-x = u"Hello" 2 |+x = "Hello" @@ -30,7 +30,7 @@ UP025.py:4:1: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 1 1 | # These should change 2 2 | x = u"Hello" 3 3 | @@ -51,7 +51,7 @@ UP025.py:6:7: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 3 3 | 4 4 | u'world' 5 5 | @@ -72,7 +72,7 @@ UP025.py:8:7: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 5 5 | 6 6 | print(u"Hello") 7 7 | @@ -93,7 +93,7 @@ UP025.py:12:5: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 9 9 | 10 10 | import foo 11 11 | @@ -114,7 +114,7 @@ UP025.py:12:15: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 9 9 | 10 10 | import foo 11 11 | @@ -135,7 +135,7 @@ UP025.py:12:27: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 9 9 | 10 10 | import foo 11 11 | @@ -156,7 +156,7 @@ UP025.py:12:39: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 9 9 | 10 10 | import foo 11 11 | @@ -177,7 +177,7 @@ UP025.py:16:5: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 13 13 | 14 14 | # These should stay quoted they way they are 15 15 | @@ -197,7 +197,7 @@ UP025.py:17:5: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 14 14 | # These should stay quoted they way they are 15 15 | 16 16 | x = u'hello' @@ -217,7 +217,7 @@ UP025.py:18:5: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 15 15 | 16 16 | x = u'hello' 17 17 | x = u"""hello""" @@ -238,7 +238,7 @@ UP025.py:19:5: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 16 16 | x = u'hello' 17 17 | x = u"""hello""" 18 18 | x = u'''hello''' diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index ef5ec4fc08..336ce040eb 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -48,6 +48,7 @@ KNOWN_FORMATTING_VIOLATIONS = [ "too-few-spaces-before-inline-comment", "trailing-comma-on-bare-tuple", "unexpected-indentation-comment", + "unicode-kind-prefix", "unnecessary-class-parentheses", "useless-semicolon", "whitespace-after-open-bracket", From 458beccf14af9d42cc6366242b031f1a4ea27549 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 14 Jun 2023 20:04:51 -0400 Subject: [PATCH 065/447] Uniformly put `## Options` at the end of documentation (#5104) --- .../eradicate/rules/commented_out_code.rs | 6 +++--- .../rules/function_call_argument_default.rs | 15 ++------------- .../rules/builtin_argument_shadowing.rs | 7 +++---- .../rules/builtin_attribute_shadowing.rs | 7 +++---- .../rules/builtin_variable_shadowing.rs | 8 ++++---- .../rules/implicit.rs | 6 +++--- .../rules/flake8_quotes/rules/from_tokens.rs | 18 +++++++++--------- .../flake8_self/rules/private_member_access.rs | 6 +++--- .../rules/relative_imports.rs | 6 +++--- .../mccabe/rules/function_is_too_complex.rs | 8 ++++---- ...lid_first_argument_name_for_class_method.rs | 11 +++++------ .../invalid_first_argument_name_for_method.rs | 10 +++++----- .../pep8_naming/rules/invalid_function_name.rs | 6 +++--- .../non_lowercase_variable_in_function.rs | 6 +++--- .../rules/pycodestyle/rules/line_too_long.rs | 6 +++--- .../src/rules/pyflakes/rules/unused_import.rs | 7 +++---- .../rules/pyflakes/rules/unused_variable.rs | 6 +++--- .../function_call_in_dataclass_default.rs | 6 +++--- 18 files changed, 65 insertions(+), 80 deletions(-) diff --git a/crates/ruff/src/rules/eradicate/rules/commented_out_code.rs b/crates/ruff/src/rules/eradicate/rules/commented_out_code.rs index 3d1a5d2290..12302f7786 100644 --- a/crates/ruff/src/rules/eradicate/rules/commented_out_code.rs +++ b/crates/ruff/src/rules/eradicate/rules/commented_out_code.rs @@ -14,13 +14,13 @@ use super::super::detection::comment_contains_code; /// Commented-out code is dead code, and is often included inadvertently. /// It should be removed. /// -/// ## Options -/// - `task-tags` -/// /// ## Example /// ```python /// # print('foo') /// ``` +/// +/// ## Options +/// - `task-tags` #[violation] pub struct CommentedOutCode; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs b/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs index af95ef699e..0443f328b8 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs @@ -21,9 +21,6 @@ use crate::rules::flake8_bugbear::rules::mutable_argument_default::is_mutable_fu /// once, at definition time. The returned value will then be reused by all /// calls to the function, which can lead to unexpected behaviour. /// -/// ## Options -/// - `flake8-bugbear.extend-immutable-calls` -/// /// ## Example /// ```python /// def create_list() -> list[int]: @@ -45,16 +42,8 @@ use crate::rules::flake8_bugbear::rules::mutable_argument_default::is_mutable_fu /// return arg /// ``` /// -/// Alternatively, if shared behavior is desirable, clarify the intent by -/// assigning to a module-level variable: -/// ```python -/// I_KNOW_THIS_IS_SHARED_STATE = create_list() -/// -/// -/// def mutable_default(arg: list[int] = I_KNOW_THIS_IS_SHARED_STATE) -> list[int]: -/// arg.append(4) -/// return arg -/// ``` +/// ## Options +/// - `flake8-bugbear.extend-immutable-calls` #[violation] pub struct FunctionCallInDefaultArgument { pub name: Option, diff --git a/crates/ruff/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs b/crates/ruff/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs index e6aea40d3d..f2e95545e5 100644 --- a/crates/ruff/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs +++ b/crates/ruff/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs @@ -20,10 +20,6 @@ use super::super::helpers::shadows_builtin; /// Builtins can be marked as exceptions to this rule via the /// [`flake8-builtins.builtins-ignorelist`] configuration option. /// -/// ## Options -/// -/// - `flake8-builtins.builtins-ignorelist` -/// /// ## Example /// ```python /// def remove_duplicates(list, list2): @@ -46,6 +42,9 @@ use super::super::helpers::shadows_builtin; /// return list(result) /// ``` /// +/// ## Options +/// - `flake8-builtins.builtins-ignorelist` +/// /// ## References /// - [_Is it bad practice to use a built-in function name as an attribute or method identifier?_](https://stackoverflow.com/questions/9109333/is-it-bad-practice-to-use-a-built-in-function-name-as-an-attribute-or-method-ide) /// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python) diff --git a/crates/ruff/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs b/crates/ruff/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs index 8393ba766a..2adb257426 100644 --- a/crates/ruff/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs +++ b/crates/ruff/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs @@ -20,10 +20,6 @@ use super::super::helpers::{shadows_builtin, AnyShadowing}; /// [`flake8-builtins.builtins-ignorelist`] configuration option, or /// converted to the appropriate dunder method. /// -/// ## Options -/// -/// - `flake8-builtins.builtins-ignorelist` -/// /// ## Example /// ```python /// class Shadow: @@ -46,6 +42,9 @@ use super::super::helpers::{shadows_builtin, AnyShadowing}; /// return 0 /// ``` /// +/// ## Options +/// - `flake8-builtins.builtins-ignorelist` +/// /// ## References /// - [_Is it bad practice to use a built-in function name as an attribute or method identifier?_](https://stackoverflow.com/questions/9109333/is-it-bad-practice-to-use-a-built-in-function-name-as-an-attribute-or-method-ide) /// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python) diff --git a/crates/ruff/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs b/crates/ruff/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs index a965af53ca..d604a29df2 100644 --- a/crates/ruff/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs +++ b/crates/ruff/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs @@ -19,10 +19,6 @@ use super::super::helpers::{shadows_builtin, AnyShadowing}; /// Builtins can be marked as exceptions to this rule via the /// [`flake8-builtins.builtins-ignorelist`] configuration option. /// -/// ## Options -/// -/// - `flake8-builtins.builtins-ignorelist` -/// /// ## Example /// ```python /// def find_max(list_of_lists): @@ -43,6 +39,10 @@ use super::super::helpers::{shadows_builtin, AnyShadowing}; /// return result /// ``` /// +/// ## Options +/// - `flake8-builtins.builtins-ignorelist` +/// +/// ## References /// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python) #[violation] pub struct BuiltinVariableShadowing { diff --git a/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs b/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs index 63e107e3bb..1d7c32a0c7 100644 --- a/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs +++ b/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs @@ -61,9 +61,6 @@ impl Violation for SingleLineImplicitStringConcatenation { /// altogether, set the `flake8-implicit-str-concat.allow-multiline` option /// to `false`. /// -/// ## Options -/// - `flake8-implicit-str-concat.allow-multiline` -/// /// ## Example /// ```python /// z = "The quick brown fox jumps over the lazy "\ @@ -78,6 +75,9 @@ impl Violation for SingleLineImplicitStringConcatenation { /// ) /// ``` /// +/// ## Options +/// - `flake8-implicit-str-concat.allow-multiline` +/// /// ## References /// - [PEP 8](https://peps.python.org/pep-0008/#maximum-line-length) #[violation] 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 7c6f990dd3..87e9e8651c 100644 --- a/crates/ruff/src/rules/flake8_quotes/rules/from_tokens.rs +++ b/crates/ruff/src/rules/flake8_quotes/rules/from_tokens.rs @@ -20,9 +20,6 @@ use super::super::settings::Quote; /// Consistency is good. Use either single or double quotes for inline /// strings, but be consistent. /// -/// ## Options -/// - `flake8-quotes.inline-quotes` -/// /// ## Example /// ```python /// foo = 'bar' @@ -32,6 +29,9 @@ use super::super::settings::Quote; /// ```python /// foo = "bar" /// ``` +/// +/// ## Options +/// - `flake8-quotes.inline-quotes` #[violation] pub struct BadQuotesInlineString { quote: Quote, @@ -65,9 +65,6 @@ impl AlwaysAutofixableViolation for BadQuotesInlineString { /// Consistency is good. Use either single or double quotes for multiline /// strings, but be consistent. /// -/// ## Options -/// - `flake8-quotes.multiline-quotes` -/// /// ## Example /// ```python /// foo = ''' @@ -81,6 +78,9 @@ impl AlwaysAutofixableViolation for BadQuotesInlineString { /// bar /// """ /// ``` +/// +/// ## Options +/// - `flake8-quotes.multiline-quotes` #[violation] pub struct BadQuotesMultilineString { quote: Quote, @@ -113,9 +113,6 @@ impl AlwaysAutofixableViolation for BadQuotesMultilineString { /// Consistency is good. Use either single or double quotes for docstring /// strings, but be consistent. /// -/// ## Options -/// - `flake8-quotes.docstring-quotes` -/// /// ## Example /// ```python /// ''' @@ -129,6 +126,9 @@ impl AlwaysAutofixableViolation for BadQuotesMultilineString { /// bar /// """ /// ``` +/// +/// ## Options +/// - `flake8-quotes.docstring-quotes` #[violation] pub struct BadQuotesDocstring { quote: Quote, diff --git a/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs b/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs index 6a8b534e3e..c20d3f77dd 100644 --- a/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs +++ b/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs @@ -20,9 +20,6 @@ use crate::checkers::ast::Checker; /// versions, that it will have the same type, or that it will have the same /// behavior. Instead, use the class's public interface. /// -/// ## Options -/// - `flake8-self.ignore-names` -/// /// ## Example /// ```python /// class Class: @@ -45,6 +42,9 @@ use crate::checkers::ast::Checker; /// print(var.public_member) /// ``` /// +/// ## Options +/// - `flake8-self.ignore-names` +/// /// ## References /// - [_What is the meaning of single or double underscores before an object name?_](https://stackoverflow.com/questions/1301346/what-is-the-meaning-of-single-and-double-underscore-before-an-object-name) #[violation] diff --git a/crates/ruff/src/rules/flake8_tidy_imports/rules/relative_imports.rs b/crates/ruff/src/rules/flake8_tidy_imports/rules/relative_imports.rs index 9bf9bc41a8..6531cebd7e 100644 --- a/crates/ruff/src/rules/flake8_tidy_imports/rules/relative_imports.rs +++ b/crates/ruff/src/rules/flake8_tidy_imports/rules/relative_imports.rs @@ -31,9 +31,6 @@ use crate::rules::flake8_tidy_imports::settings::Strictness; /// > from .sibling import example /// > ``` /// -/// ## Options -/// - `flake8-tidy-imports.ban-relative-imports` -/// /// ## Example /// ```python /// from .. import foo @@ -44,6 +41,9 @@ use crate::rules::flake8_tidy_imports::settings::Strictness; /// from mypkg import foo /// ``` /// +/// ## Options +/// - `flake8-tidy-imports.ban-relative-imports` +/// /// [PEP 8]: https://peps.python.org/pep-0008/#imports #[violation] pub struct RelativeImports { diff --git a/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs b/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs index 0311470ac7..6393a65f3d 100644 --- a/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs +++ b/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs @@ -8,18 +8,15 @@ use ruff_python_ast::source_code::Locator; /// ## What it does /// Checks for functions with a high `McCabe` complexity. /// +/// ## Why is this bad? /// The `McCabe` complexity of a function is a measure of the complexity of /// the control flow graph of the function. It is calculated by adding /// one to the number of decision points in the function. A decision /// point is a place in the code where the program has a choice of two /// or more paths to follow. /// -/// ## Why is this bad? /// Functions with a high complexity are hard to understand and maintain. /// -/// ## Options -/// - `mccabe.max-complexity` -/// /// ## Example /// ```python /// def foo(a, b, c): @@ -46,6 +43,9 @@ use ruff_python_ast::source_code::Locator; /// return 2 /// return 1 /// ``` +/// +/// ## Options +/// - `mccabe.max-complexity` #[violation] pub struct ComplexStructure { name: String, diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs index e3fa06517a..e7c07e9e82 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs @@ -21,11 +21,6 @@ use crate::checkers::ast::Checker; /// > append a single trailing underscore rather than use an abbreviation or spelling corruption. /// > Thus `class_` is better than `clss`. (Perhaps better is to avoid such clashes by using a synonym.) /// -/// ## Options -/// - `pep8-naming.classmethod-decorators` -/// - `pep8-naming.staticmethod-decorators` -/// - `pep8-naming.ignore-names` -/// /// ## Example /// ```python /// class Example: @@ -42,8 +37,12 @@ use crate::checkers::ast::Checker; /// ... /// ``` /// -/// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments +/// ## Options +/// - `pep8-naming.classmethod-decorators` +/// - `pep8-naming.staticmethod-decorators` +/// - `pep8-naming.ignore-names` /// +/// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments #[violation] pub struct InvalidFirstArgumentNameForClassMethod; diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs index 95a1194e85..96a826be71 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs @@ -21,11 +21,6 @@ use crate::checkers::ast::Checker; /// > append a single trailing underscore rather than use an abbreviation or spelling corruption. /// > Thus `class_` is better than `clss`. (Perhaps better is to avoid such clashes by using a synonym.) /// -/// ## Options -/// - `pep8-naming.classmethod-decorators` -/// - `pep8-naming.staticmethod-decorators` -/// - `pep8-naming.ignore-names` -/// /// ## Example /// ```python /// class Example: @@ -40,6 +35,11 @@ use crate::checkers::ast::Checker; /// ... /// ``` /// +/// ## Options +/// - `pep8-naming.classmethod-decorators` +/// - `pep8-naming.staticmethod-decorators` +/// - `pep8-naming.ignore-names` +/// /// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments #[violation] pub struct InvalidFirstArgumentNameForMethod; diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs index 08be500680..35fdc13a02 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs @@ -20,9 +20,6 @@ use crate::settings::types::IdentifierPattern; /// > improve readability. mixedCase is allowed only in contexts where that’s already the /// > prevailing style (e.g. threading.py), to retain backwards compatibility. /// -/// ## Options -/// - `pep8-naming.ignore-names` -/// /// ## Example /// ```python /// def myFunction(): @@ -35,6 +32,9 @@ use crate::settings::types::IdentifierPattern; /// pass /// ``` /// +/// ## Options +/// - `pep8-naming.ignore-names` +/// /// [PEP 8]: https://peps.python.org/pep-0008/#function-and-variable-names #[violation] pub struct InvalidFunctionName { diff --git a/crates/ruff/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs b/crates/ruff/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs index 147531a46b..a25a82a8ca 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs @@ -17,9 +17,6 @@ use crate::rules::pep8_naming::helpers; /// > is allowed only in contexts where that's already the prevailing style (e.g. threading.py), /// > to retain backwards compatibility. /// -/// ## Options -/// - `pep8-naming.ignore-names` -/// /// ## Example /// ```python /// def my_function(a): @@ -34,6 +31,9 @@ use crate::rules::pep8_naming::helpers; /// return b /// ``` /// +/// ## Options +/// - `pep8-naming.ignore-names` +/// /// [PEP 8]: https://peps.python.org/pep-0008/#function-and-variable-names #[violation] pub struct NonLowercaseVariableInFunction { diff --git a/crates/ruff/src/rules/pycodestyle/rules/line_too_long.rs b/crates/ruff/src/rules/pycodestyle/rules/line_too_long.rs index e59f61f9a0..6cb228239b 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/line_too_long.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/line_too_long.rs @@ -11,9 +11,6 @@ use crate::settings::Settings; /// ## Why is this bad? /// Overlong lines can hurt readability. /// -/// ## Options -/// - `task-tags` -/// /// ## Example /// ```python /// my_function(param1, param2, param3, param4, param5, param6, param7, param8, param9, param10) @@ -26,6 +23,9 @@ use crate::settings::Settings; /// param6, param7, param8, param9, param10 /// ) /// ``` +/// +/// ## Options +/// - `task-tags` #[violation] pub struct LineTooLong(pub usize, pub usize); diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs index b8d8e4be92..50898b388e 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs @@ -26,10 +26,6 @@ enum UnusedImportContext { /// If an import statement is used to check for the availability or existence /// of a module, consider using `importlib.util.find_spec` instead. /// -/// ## Options -/// -/// - `pyflakes.extend-generics` -/// /// ## Example /// ```python /// import numpy as np # unused import @@ -55,6 +51,9 @@ enum UnusedImportContext { /// print("numpy is not installed") /// ``` /// +/// ## Options +/// - `pyflakes.extend-generics` +/// /// ## References /// - [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement) /// - [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec) diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs b/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs index 0909571255..e3edd38b15 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs @@ -24,9 +24,6 @@ use crate::registry::AsRule; /// prefixed with an underscore, or some other value that adheres to the /// [`dummy-variable-rgx`] pattern. /// -/// ## Options -/// - `dummy-variable-rgx` -/// /// ## Example /// ```python /// def foo(): @@ -41,6 +38,9 @@ use crate::registry::AsRule; /// x = 1 /// return x /// ``` +/// +/// ## Options +/// - `dummy-variable-rgx` #[violation] pub struct UnusedVariable { pub name: String, diff --git a/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs b/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs index 9aad4078e7..a41be00b35 100644 --- a/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs +++ b/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs @@ -23,9 +23,6 @@ use crate::rules::ruff::rules::helpers::{ /// If a field needs to be initialized with a mutable object, use the /// `field(default_factory=...)` pattern. /// -/// ## Options -/// - `flake8-bugbear.extend-immutable-calls` -/// /// ## Examples /// ```python /// from dataclasses import dataclass @@ -53,6 +50,9 @@ use crate::rules::ruff::rules::helpers::{ /// class A: /// mutable_default: list[int] = field(default_factory=creating_list) /// ``` +/// +/// ## Options +/// - `flake8-bugbear.extend-immutable-calls` #[violation] pub struct FunctionCallInDataclassDefaultArgument { pub name: Option, From 9ab16fb417e7fd00cc09996152700cf374bc54eb Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 14 Jun 2023 20:12:32 -0400 Subject: [PATCH 066/447] Add `target-version` link to relevant rules (#5105) --- .../rules/future_rewritable_type_annotation.rs | 3 +++ .../src/rules/pylint/rules/bad_str_strip_call.rs | 16 +++++++++++++++- .../rules/pylint/rules/continue_in_finally.rs | 3 +++ .../pylint/rules/repeated_isinstance_calls.rs | 3 +++ .../rules/pyupgrade/rules/datetime_utc_alias.rs | 3 +++ .../rules/lru_cache_with_maxsize_none.rs | 3 +++ .../rules/lru_cache_without_parameters.rs | 3 +++ .../pyupgrade/rules/outdated_version_block.rs | 3 +++ .../pyupgrade/rules/unnecessary_future_import.rs | 3 +++ .../pyupgrade/rules/use_pep585_annotation.rs | 3 +++ .../pyupgrade/rules/use_pep604_annotation.rs | 3 +++ .../pyupgrade/rules/use_pep604_isinstance.rs | 3 +++ .../src/rules/ruff/rules/implicit_optional.rs | 3 +++ 13 files changed, 51 insertions(+), 1 deletion(-) diff --git a/crates/ruff/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs b/crates/ruff/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs index 6a93534241..33e604c821 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs +++ b/crates/ruff/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs @@ -49,6 +49,9 @@ use crate::checkers::ast::Checker; /// def func(obj: dict[str, int | None]) -> None: /// ... /// ``` +/// +/// ## Options +/// - `target-version` #[violation] pub struct FutureRewritableTypeAnnotation { name: String, diff --git a/crates/ruff/src/rules/pylint/rules/bad_str_strip_call.rs b/crates/ruff/src/rules/pylint/rules/bad_str_strip_call.rs index 4e961f434b..c670c7ff14 100644 --- a/crates/ruff/src/rules/pylint/rules/bad_str_strip_call.rs +++ b/crates/ruff/src/rules/pylint/rules/bad_str_strip_call.rs @@ -17,16 +17,30 @@ use crate::settings::types::PythonVersion; /// trailing ends of the string. Including duplicate characters in the call /// is redundant and often indicative of a mistake. /// +/// In Python 3.9 and later, you can use `str#removeprefix` and +/// `str#removesuffix` to remove an exact prefix or suffix from a string, +/// respectively, which should be preferred when possible. +/// /// ## Example /// ```python -/// "bar foo baz".strip("bar baz ") # "foo" +/// # Evaluates to "foo". +/// "bar foo baz".strip("bar baz ") /// ``` /// /// Use instead: /// ```python +/// # Evaluates to "foo". /// "bar foo baz".strip("abrz ") # "foo" /// ``` /// +/// Or: +/// ```python +/// # Evaluates to "foo". +/// "bar foo baz".removeprefix("bar ").removesuffix(" baz") +/// +/// ## Options +/// - `target-version` +/// /// ## References /// - [Python documentation](https://docs.python.org/3/library/stdtypes.html?highlight=strip#str.strip) #[violation] diff --git a/crates/ruff/src/rules/pylint/rules/continue_in_finally.rs b/crates/ruff/src/rules/pylint/rules/continue_in_finally.rs index 09e6c21793..600ee4306e 100644 --- a/crates/ruff/src/rules/pylint/rules/continue_in_finally.rs +++ b/crates/ruff/src/rules/pylint/rules/continue_in_finally.rs @@ -32,6 +32,9 @@ use crate::checkers::ast::Checker; /// else: /// continue /// ``` +/// +/// ## Options +/// - `target-version` #[violation] pub struct ContinueInFinally; diff --git a/crates/ruff/src/rules/pylint/rules/repeated_isinstance_calls.rs b/crates/ruff/src/rules/pylint/rules/repeated_isinstance_calls.rs index 0e578360af..4ddc1cbf55 100644 --- a/crates/ruff/src/rules/pylint/rules/repeated_isinstance_calls.rs +++ b/crates/ruff/src/rules/pylint/rules/repeated_isinstance_calls.rs @@ -36,6 +36,9 @@ use crate::settings::types::PythonVersion; /// return isinstance(x, int | float | complex) /// ``` /// +/// ## Options +/// - `target-version` +/// /// ## References /// - [Python documentation: `isinstance`](https://docs.python.org/3/library/functions.html#isinstance) #[violation] diff --git a/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs index b9750fe7ac..3f0e04cc29 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs @@ -28,6 +28,9 @@ use crate::registry::AsRule; /// datetime.UTC /// ``` /// +/// ## Options +/// - `target-version` +/// /// ## References /// - [Python documentation: `datetime.UTC`](https://docs.python.org/3/library/datetime.html#datetime.UTC) #[violation] 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 497e1bdf6d..277d472f0c 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 @@ -36,6 +36,9 @@ use crate::registry::AsRule; /// ... /// ``` /// +/// ## Options +/// - `target-version` +/// /// ## References /// - [Python documentation: `@functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache) #[violation] 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 07cb2999b5..5e1a3c602f 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 @@ -34,6 +34,9 @@ use crate::registry::AsRule; /// ... /// ``` /// +/// ## Options +/// - `target-version` +/// /// ## References /// - [Python documentation: `@functools.lru_cache`](https://docs.python.org/3/library/functools.html#functools.lru_cache) /// - [Let lru_cache be used as a decorator with no arguments](https://github.com/python/cpython/issues/80953) 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 6f40f2786c..ba35cd4d2a 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs @@ -42,6 +42,9 @@ use crate::settings::types::PythonVersion; /// print("py3") /// ``` /// +/// ## Options +/// - `target-version` +/// /// ## References /// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs index 676a5ad928..97eb9e1269 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs @@ -30,6 +30,9 @@ use crate::registry::AsRule; /// print("Hello, world!") /// ``` /// +/// ## Options +/// - `target-version` +/// /// ## References /// - [Python documentation: `__future__` — Future statement definitions](https://docs.python.org/3/library/__future__.html) #[violation] diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep585_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep585_annotation.rs index 42677e097b..b5ceee9e8a 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep585_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep585_annotation.rs @@ -35,6 +35,9 @@ use crate::registry::AsRule; /// foo: list[int] = [1, 2, 3] /// ``` /// +/// ## Options +/// - `target-version` +/// /// [PEP 585]: https://peps.python.org/pep-0585/ #[violation] pub struct NonPEP585Annotation { diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs index f96ad905d7..ae9350e963 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs @@ -28,6 +28,9 @@ use crate::registry::AsRule; /// foo: int | str = 1 /// ``` /// +/// ## Options +/// - `target-version` +/// /// [PEP 604]: https://peps.python.org/pep-0604/ #[violation] pub struct NonPEP604Annotation; diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs index aa7ce7c1a8..7ffa04286d 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs @@ -53,6 +53,9 @@ impl CallKind { /// isinstance(x, int | float) /// ``` /// +/// ## Options +/// - `target-version` +/// /// ## References /// - [Python documentation: `isinstance`](https://docs.python.org/3/library/functions.html#isinstance) /// - [Python documentation: `issubclass`](https://docs.python.org/3/library/functions.html#issubclass) diff --git a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs index f032db7101..b95a9051bd 100644 --- a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs +++ b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs @@ -55,6 +55,9 @@ use crate::settings::types::PythonVersion; /// pass /// ``` /// +/// ## Options +/// - `target-version` +/// /// [PEP 484]: https://peps.python.org/pep-0484/#union-types #[violation] pub struct ImplicitOptional { From 716cab2f19637d7874a78f4304591e7a97aa5476 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 14 Jun 2023 20:19:05 -0400 Subject: [PATCH 067/447] Run `rustfmt` on nightly to clean up erroneous comments (#5106) ## Summary This PR runs `rustfmt` with a few nightly options as a one-time fix to catch some malformatted comments. I ended up just running with: ```toml condense_wildcard_suffixes = true edition = "2021" max_width = 100 normalize_comments = true normalize_doc_attributes = true reorder_impl_items = true unstable_features = true use_field_init_shorthand = true ``` Since these all seem like reasonable things to fix, so may as well while I'm here. --- crates/ruff/src/docstrings/sections.rs | 2 +- crates/ruff/src/message/diff.rs | 2 +- crates/ruff/src/message/mod.rs | 1 + crates/ruff/src/registry/rule_set.rs | 5 ++--- crates/ruff/src/rule_selector.rs | 2 +- .../rules/mixed_case_variable_in_global_scope.rs | 2 +- .../rules/logical_lines/indentation.rs | 1 - .../rules/pycodestyle/rules/logical_lines/mod.rs | 2 +- .../logical_lines/whitespace_around_keywords.rs | 3 --- .../logical_lines/whitespace_before_comment.rs | 1 - .../pycodestyle/rules/trailing_whitespace.rs | 1 - crates/ruff/src/settings/options_base.rs | 6 +++--- crates/ruff_cache/src/cache_key.rs | 1 + crates/ruff_formatter/src/buffer.rs | 1 - crates/ruff_formatter/src/format_extensions.rs | 2 -- crates/ruff_formatter/src/lib.rs | 1 - crates/ruff_index/src/slice.rs | 4 ++-- crates/ruff_index/src/vec.rs | 6 +++--- crates/ruff_python_ast/src/imports.rs | 2 +- .../src/source_code/comment_ranges.rs | 2 +- .../src/source_code/line_index.rs | 5 ++--- crates/ruff_python_ast/src/visitor/preorder.rs | 16 ++++++++++++++++ .../src/comments/placement.rs | 2 +- .../src/comments/visitor.rs | 1 + .../src/expression/expr_bin_op.rs | 1 + .../ruff_python_formatter/src/expression/mod.rs | 2 ++ crates/ruff_python_formatter/src/module/mod.rs | 2 ++ .../ruff_python_formatter/src/statement/mod.rs | 2 ++ .../src/statement/stmt_assign.rs | 1 - .../ruff_python_formatter/src/statement/suite.rs | 1 + crates/ruff_python_semantic/src/definition.rs | 2 +- crates/ruff_python_semantic/src/scope.rs | 1 + 32 files changed, 49 insertions(+), 34 deletions(-) diff --git a/crates/ruff/src/docstrings/sections.rs b/crates/ruff/src/docstrings/sections.rs index d61697a1c5..48f38e606d 100644 --- a/crates/ruff/src/docstrings/sections.rs +++ b/crates/ruff/src/docstrings/sections.rs @@ -205,8 +205,8 @@ impl<'a> SectionContexts<'a> { } impl<'a> IntoIterator for &'a SectionContexts<'a> { - type Item = SectionContext<'a>; type IntoIter = SectionContextsIter<'a>; + type Item = SectionContext<'a>; fn into_iter(self) -> Self::IntoIter { self.iter() diff --git a/crates/ruff/src/message/diff.rs b/crates/ruff/src/message/diff.rs index 8bda1c0534..c665578c2a 100644 --- a/crates/ruff/src/message/diff.rs +++ b/crates/ruff/src/message/diff.rs @@ -60,7 +60,7 @@ impl Display for Diff<'_> { Applicability::Automatic => "Fix", Applicability::Suggested => "Suggested fix", Applicability::Manual => "Possible fix", - Applicability::Unspecified => "Suggested fix", // For backwards compatibility, unspecified fixes are 'suggested' + Applicability::Unspecified => "Suggested fix", /* For backwards compatibility, unspecified fixes are 'suggested' */ }; writeln!(f, "ℹ {}", message.blue())?; diff --git a/crates/ruff/src/message/mod.rs b/crates/ruff/src/message/mod.rs index 821191bbad..f86e408ecd 100644 --- a/crates/ruff/src/message/mod.rs +++ b/crates/ruff/src/message/mod.rs @@ -94,6 +94,7 @@ struct MessageWithLocation<'a> { impl Deref for MessageWithLocation<'_> { type Target = Message; + fn deref(&self) -> &Self::Target { self.message } diff --git a/crates/ruff/src/registry/rule_set.rs b/crates/ruff/src/registry/rule_set.rs index 7fdbf8b19d..97e7ac7f3b 100644 --- a/crates/ruff/src/registry/rule_set.rs +++ b/crates/ruff/src/registry/rule_set.rs @@ -13,7 +13,6 @@ pub struct RuleSet([u64; RULESET_SIZE]); impl RuleSet { const EMPTY: [u64; RULESET_SIZE] = [0; RULESET_SIZE]; - // 64 fits into a u16 without truncation #[allow(clippy::cast_possible_truncation)] const SLICE_BITS: u16 = u64::BITS as u16; @@ -290,8 +289,8 @@ impl Extend for RuleSet { } impl IntoIterator for RuleSet { - type Item = Rule; type IntoIter = RuleSetIterator; + type Item = Rule; fn into_iter(self) -> Self::IntoIter { self.iter() @@ -299,8 +298,8 @@ impl IntoIterator for RuleSet { } impl IntoIterator for &RuleSet { - type Item = Rule; type IntoIter = RuleSetIterator; + type Item = Rule; fn into_iter(self) -> Self::IntoIter { self.iter() diff --git a/crates/ruff/src/rule_selector.rs b/crates/ruff/src/rule_selector.rs index 6985c1be3c..6247346a51 100644 --- a/crates/ruff/src/rule_selector.rs +++ b/crates/ruff/src/rule_selector.rs @@ -145,8 +145,8 @@ impl From for RuleSelector { } impl IntoIterator for &RuleSelector { - type Item = Rule; type IntoIter = RuleSelectorIter; + type Item = Rule; fn into_iter(self) -> Self::IntoIter { match self { diff --git a/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs b/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs index 158f03eaea..4024ba9998 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs @@ -20,7 +20,7 @@ use crate::rules::pep8_naming::helpers; /// > Modules that are designed for use via from M import * should use the /// __all__ mechanism to prevent exporting globals, or use the older /// convention of prefixing such globals with an underscore (which you might -///want to do to indicate these globals are “module non-public”). +/// want to do to indicate these globals are “module non-public”). /// > /// > ### Function and Variable Names /// > Function names should be lowercase, with words separated by underscores diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/indentation.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/indentation.rs index a059ee9b6c..9f7f3b652b 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/indentation.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/indentation.rs @@ -82,7 +82,6 @@ impl Violation for IndentationWithInvalidMultipleComment { /// ```python /// for item in items: /// pass -/// /// ``` /// /// Use instead: diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs index a00c71e750..9158cac000 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs @@ -95,8 +95,8 @@ impl Debug for LogicalLines<'_> { } impl<'a> IntoIterator for &'a LogicalLines<'a> { - type Item = LogicalLine<'a>; type IntoIter = LogicalLinesIter<'a>; + type Item = LogicalLine<'a>; fn into_iter(self) -> Self::IntoIter { LogicalLinesIter { diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs index d65d1a1693..5c87d7df30 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs @@ -41,7 +41,6 @@ impl Violation for MultipleSpacesAfterKeyword { /// ## Example /// ```python /// True and False -/// /// ``` /// /// Use instead: @@ -67,7 +66,6 @@ impl Violation for MultipleSpacesBeforeKeyword { /// ## Example /// ```python /// True and\tFalse -/// /// ``` /// /// Use instead: @@ -93,7 +91,6 @@ impl Violation for TabAfterKeyword { /// ## Example /// ```python /// True\tand False -/// /// ``` /// /// Use instead: diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs index e9ee52f52d..3a42136dcb 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs @@ -116,7 +116,6 @@ impl Violation for NoSpaceAfterBlockComment { /// ## Example /// ```python /// ### Block comment -/// /// ``` /// /// Use instead: diff --git a/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs b/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs index 836eafe9e9..f4841a385d 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs @@ -50,7 +50,6 @@ impl AlwaysAutofixableViolation for TrailingWhitespace { /// ## Example /// ```python /// class Foo(object):\n \n bang = 12 -/// /// ``` /// /// Use instead: diff --git a/crates/ruff/src/settings/options_base.rs b/crates/ruff/src/settings/options_base.rs index 2450f73bfb..ed325db917 100644 --- a/crates/ruff/src/settings/options_base.rs +++ b/crates/ruff/src/settings/options_base.rs @@ -70,7 +70,7 @@ impl OptionGroup { /// /// ### Find a nested options /// - ///```rust + /// ```rust /// # use ruff::settings::options_base::{OptionGroup, OptionEntry, OptionField}; /// /// const ignore_options: [(&'static str, OptionEntry); 2] = [ @@ -134,8 +134,8 @@ impl OptionGroup { } impl<'a> IntoIterator for &'a OptionGroup { - type Item = &'a (&'a str, OptionEntry); type IntoIter = std::slice::Iter<'a, (&'a str, OptionEntry)>; + type Item = &'a (&'a str, OptionEntry); fn into_iter(self) -> Self::IntoIter { self.0.iter() @@ -143,8 +143,8 @@ impl<'a> IntoIterator for &'a OptionGroup { } impl IntoIterator for OptionGroup { - type Item = &'static (&'static str, OptionEntry); type IntoIter = std::slice::Iter<'static, (&'static str, OptionEntry)>; + type Item = &'static (&'static str, OptionEntry); fn into_iter(self) -> Self::IntoIter { self.0.iter() diff --git a/crates/ruff_cache/src/cache_key.rs b/crates/ruff_cache/src/cache_key.rs index ee9669df09..e015112bda 100644 --- a/crates/ruff_cache/src/cache_key.rs +++ b/crates/ruff_cache/src/cache_key.rs @@ -24,6 +24,7 @@ impl CacheKeyHasher { impl Deref for CacheKeyHasher { type Target = DefaultHasher; + fn deref(&self) -> &Self::Target { &self.inner } diff --git a/crates/ruff_formatter/src/buffer.rs b/crates/ruff_formatter/src/buffer.rs index aa90a73a9a..13cb542085 100644 --- a/crates/ruff_formatter/src/buffer.rs +++ b/crates/ruff_formatter/src/buffer.rs @@ -29,7 +29,6 @@ pub trait Buffer { /// /// assert_eq!(buffer.into_vec(), vec![FormatElement::StaticText { text: "test" }]); /// ``` - /// fn write_element(&mut self, element: FormatElement) -> FormatResult<()>; /// Returns a slice containing all elements written into this buffer. diff --git a/crates/ruff_formatter/src/format_extensions.rs b/crates/ruff_formatter/src/format_extensions.rs index 82b99fda93..317d9c1337 100644 --- a/crates/ruff_formatter/src/format_extensions.rs +++ b/crates/ruff_formatter/src/format_extensions.rs @@ -57,7 +57,6 @@ pub trait MemoizeFormat { /// # Ok(()) /// # } /// ``` - /// fn memoized(self) -> Memoized where Self: Sized + Format, @@ -142,7 +141,6 @@ where /// assert_eq!("Counter:\n\tCount: 0\nCount: 0\n", formatted.print()?.as_code()); /// # Ok(()) /// # } - /// /// ``` pub fn inspect(&mut self, f: &mut Formatter) -> FormatResult<&[FormatElement]> { let result = self diff --git a/crates/ruff_formatter/src/lib.rs b/crates/ruff_formatter/src/lib.rs index e1f9e4f21e..d322b77a7a 100644 --- a/crates/ruff_formatter/src/lib.rs +++ b/crates/ruff_formatter/src/lib.rs @@ -721,7 +721,6 @@ where /// # Ok(()) /// # } /// ``` -/// #[inline(always)] pub fn write( output: &mut dyn Buffer, diff --git a/crates/ruff_index/src/slice.rs b/crates/ruff_index/src/slice.rs index ddb534ea82..77401e7133 100644 --- a/crates/ruff_index/src/slice.rs +++ b/crates/ruff_index/src/slice.rs @@ -127,8 +127,8 @@ impl IndexMut for IndexSlice { } impl<'a, I: Idx, T> IntoIterator for &'a IndexSlice { - type Item = &'a T; type IntoIter = std::slice::Iter<'a, T>; + type Item = &'a T; #[inline] fn into_iter(self) -> std::slice::Iter<'a, T> { @@ -137,8 +137,8 @@ impl<'a, I: Idx, T> IntoIterator for &'a IndexSlice { } impl<'a, I: Idx, T> IntoIterator for &'a mut IndexSlice { - type Item = &'a mut T; type IntoIter = std::slice::IterMut<'a, T>; + type Item = &'a mut T; #[inline] fn into_iter(self) -> std::slice::IterMut<'a, T> { diff --git a/crates/ruff_index/src/vec.rs b/crates/ruff_index/src/vec.rs index 36fa6388ac..516e53487f 100644 --- a/crates/ruff_index/src/vec.rs +++ b/crates/ruff_index/src/vec.rs @@ -121,8 +121,8 @@ impl FromIterator for IndexVec { } impl IntoIterator for IndexVec { - type Item = T; type IntoIter = std::vec::IntoIter; + type Item = T; #[inline] fn into_iter(self) -> std::vec::IntoIter { @@ -131,8 +131,8 @@ impl IntoIterator for IndexVec { } impl<'a, I: Idx, T> IntoIterator for &'a IndexVec { - type Item = &'a T; type IntoIter = std::slice::Iter<'a, T>; + type Item = &'a T; #[inline] fn into_iter(self) -> std::slice::Iter<'a, T> { @@ -141,8 +141,8 @@ impl<'a, I: Idx, T> IntoIterator for &'a IndexVec { } impl<'a, I: Idx, T> IntoIterator for &'a mut IndexVec { - type Item = &'a mut T; type IntoIter = std::slice::IterMut<'a, T>; + type Item = &'a mut T; #[inline] fn into_iter(self) -> std::slice::IterMut<'a, T> { diff --git a/crates/ruff_python_ast/src/imports.rs b/crates/ruff_python_ast/src/imports.rs index 098f75c0d6..6adfb9fce9 100644 --- a/crates/ruff_python_ast/src/imports.rs +++ b/crates/ruff_python_ast/src/imports.rs @@ -160,8 +160,8 @@ impl ImportMap { } impl<'a> IntoIterator for &'a ImportMap { - type Item = (&'a String, &'a Vec); type IntoIter = std::collections::hash_map::Iter<'a, String, Vec>; + type Item = (&'a String, &'a Vec); fn into_iter(self) -> Self::IntoIter { self.module_to_imports.iter() diff --git a/crates/ruff_python_ast/src/source_code/comment_ranges.rs b/crates/ruff_python_ast/src/source_code/comment_ranges.rs index cbbb414b54..189addc418 100644 --- a/crates/ruff_python_ast/src/source_code/comment_ranges.rs +++ b/crates/ruff_python_ast/src/source_code/comment_ranges.rs @@ -25,8 +25,8 @@ impl Debug for CommentRanges { } impl<'a> IntoIterator for &'a CommentRanges { - type Item = &'a TextRange; type IntoIter = std::slice::Iter<'a, TextRange>; + type Item = &'a TextRange; fn into_iter(self) -> Self::IntoIter { self.raw.iter() diff --git a/crates/ruff_python_ast/src/source_code/line_index.rs b/crates/ruff_python_ast/src/source_code/line_index.rs index 1096f1d1bf..157d75868e 100644 --- a/crates/ruff_python_ast/src/source_code/line_index.rs +++ b/crates/ruff_python_ast/src/source_code/line_index.rs @@ -245,12 +245,11 @@ impl IndexKind { pub struct OneIndexed(NonZeroUsize); impl OneIndexed { + /// The largest value that can be represented by this integer type + pub const MAX: Self = unwrap(Self::new(usize::MAX)); // SAFETY: These constants are being initialized with non-zero values /// The smallest value that can be represented by this integer type. pub const MIN: Self = unwrap(Self::new(1)); - /// The largest value that can be represented by this integer type - pub const MAX: Self = unwrap(Self::new(usize::MAX)); - pub const ONE: NonZeroUsize = unwrap(NonZeroUsize::new(1)); /// Creates a non-zero if the given value is not zero. diff --git a/crates/ruff_python_ast/src/visitor/preorder.rs b/crates/ruff_python_ast/src/visitor/preorder.rs index 974469ce0a..248070ec4d 100644 --- a/crates/ruff_python_ast/src/visitor/preorder.rs +++ b/crates/ruff_python_ast/src/visitor/preorder.rs @@ -1078,23 +1078,29 @@ class A: walk_expr(self, expr); self.exit_node(); } + fn visit_expr(&mut self, expr: &Expr) { self.enter_node(expr); walk_expr(self, expr); self.exit_node(); } + fn visit_constant(&mut self, constant: &Constant) { self.emit(&constant); } + fn visit_boolop(&mut self, boolop: &Boolop) { self.emit(&boolop); } + fn visit_operator(&mut self, operator: &Operator) { self.emit(&operator); } + fn visit_unaryop(&mut self, unaryop: &Unaryop) { self.emit(&unaryop); } + fn visit_cmpop(&mut self, cmpop: &Cmpop) { self.emit(&cmpop); } @@ -1104,51 +1110,61 @@ class A: walk_comprehension(self, comprehension); self.exit_node(); } + fn visit_excepthandler(&mut self, excepthandler: &Excepthandler) { self.enter_node(excepthandler); walk_excepthandler(self, excepthandler); self.exit_node(); } + fn visit_format_spec(&mut self, format_spec: &Expr) { self.enter_node(format_spec); walk_expr(self, format_spec); self.exit_node(); } + fn visit_arguments(&mut self, arguments: &Arguments) { self.enter_node(arguments); walk_arguments(self, arguments); self.exit_node(); } + fn visit_arg(&mut self, arg: &Arg) { self.enter_node(arg); walk_arg(self, arg); self.exit_node(); } + fn visit_keyword(&mut self, keyword: &Keyword) { self.enter_node(keyword); walk_keyword(self, keyword); self.exit_node(); } + fn visit_alias(&mut self, alias: &Alias) { self.enter_node(alias); walk_alias(self, alias); self.exit_node(); } + fn visit_withitem(&mut self, withitem: &Withitem) { self.enter_node(withitem); walk_withitem(self, withitem); self.exit_node(); } + fn visit_match_case(&mut self, match_case: &MatchCase) { self.enter_node(match_case); walk_match_case(self, match_case); self.exit_node(); } + fn visit_pattern(&mut self, pattern: &Pattern) { self.enter_node(pattern); walk_pattern(self, pattern); self.exit_node(); } + fn visit_type_ignore(&mut self, type_ignore: &TypeIgnore) { self.enter_node(type_ignore); walk_type_ignore(self, type_ignore); diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index c1c2b8df1f..2dabe23930 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -656,7 +656,7 @@ fn handle_positional_only_arguments_separator_comment<'a>( /// Handles comments between the left side and the operator of a binary expression (trailing comments of the left), /// and trailing end-of-line comments that are on the same line as the operator. /// -///```python +/// ```python /// a = ( /// 5 # trailing left comment /// + # trailing operator comment diff --git a/crates/ruff_python_formatter/src/comments/visitor.rs b/crates/ruff_python_formatter/src/comments/visitor.rs index a6cb1e40cc..4035cd05fe 100644 --- a/crates/ruff_python_formatter/src/comments/visitor.rs +++ b/crates/ruff_python_formatter/src/comments/visitor.rs @@ -257,6 +257,7 @@ impl<'ast> PreorderVisitor<'ast> for CommentsVisitor<'ast> { self.finish_node(withitem); } + fn visit_match_case(&mut self, match_case: &'ast MatchCase) { if self.start_node(match_case).is_traverse() { walk_match_case(self, match_case); diff --git a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs index 0700bc888d..cd9618b28c 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs @@ -142,6 +142,7 @@ impl<'ast> AsFormat> for Operator { impl<'ast> IntoFormat> for Operator { type Format = FormatOwnedWithRule>; + fn into_format(self) -> Self::Format { FormatOwnedWithRule::new(self, FormatOperator) } diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 706c59f29f..b6ca41efbd 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -163,6 +163,7 @@ impl NeedsParentheses for Expr { impl<'ast> AsFormat> for Expr { type Format<'a> = FormatRefWithRule<'a, Expr, FormatExpr, PyFormatContext<'ast>>; + fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new(self, FormatExpr::default()) } @@ -170,6 +171,7 @@ impl<'ast> AsFormat> for Expr { impl<'ast> IntoFormat> for Expr { type Format = FormatOwnedWithRule>; + fn into_format(self) -> Self::Format { FormatOwnedWithRule::new(self, FormatExpr::default()) } diff --git a/crates/ruff_python_formatter/src/module/mod.rs b/crates/ruff_python_formatter/src/module/mod.rs index 6036af3942..34295fab4f 100644 --- a/crates/ruff_python_formatter/src/module/mod.rs +++ b/crates/ruff_python_formatter/src/module/mod.rs @@ -24,6 +24,7 @@ impl FormatRule> for FormatMod { impl<'ast> AsFormat> for Mod { type Format<'a> = FormatRefWithRule<'a, Mod, FormatMod, PyFormatContext<'ast>>; + fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new(self, FormatMod::default()) } @@ -31,6 +32,7 @@ impl<'ast> AsFormat> for Mod { impl<'ast> IntoFormat> for Mod { type Format = FormatOwnedWithRule>; + fn into_format(self) -> Self::Format { FormatOwnedWithRule::new(self, FormatMod::default()) } diff --git a/crates/ruff_python_formatter/src/statement/mod.rs b/crates/ruff_python_formatter/src/statement/mod.rs index 42a18d1d3e..3abc1e6f93 100644 --- a/crates/ruff_python_formatter/src/statement/mod.rs +++ b/crates/ruff_python_formatter/src/statement/mod.rs @@ -70,6 +70,7 @@ impl FormatRule> for FormatStmt { impl<'ast> AsFormat> for Stmt { type Format<'a> = FormatRefWithRule<'a, Stmt, FormatStmt, PyFormatContext<'ast>>; + fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new(self, FormatStmt::default()) } @@ -77,6 +78,7 @@ impl<'ast> AsFormat> for Stmt { impl<'ast> IntoFormat> for Stmt { type Format = FormatOwnedWithRule>; + fn into_format(self) -> Self::Format { FormatOwnedWithRule::new(self, FormatStmt::default()) } diff --git a/crates/ruff_python_formatter/src/statement/stmt_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_assign.rs index 79182dafd9..cbc25342a9 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_assign.rs @@ -7,7 +7,6 @@ use ruff_formatter::{write, Buffer, Format, FormatResult}; use ruff_python_ast::prelude::Expr; use rustpython_parser::ast::StmtAssign; -// // Note: This currently does wrap but not the black way so the types below likely need to be // replaced entirely // diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index 23b98f1798..3c075cf862 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -178,6 +178,7 @@ impl<'ast> AsFormat> for Suite { impl<'ast> IntoFormat> for Suite { type Format = FormatOwnedWithRule>; + fn into_format(self) -> Self::Format { FormatOwnedWithRule::new(self, FormatSuite::default()) } diff --git a/crates/ruff_python_semantic/src/definition.rs b/crates/ruff_python_semantic/src/definition.rs index 6d73624a66..c5cbbefa90 100644 --- a/crates/ruff_python_semantic/src/definition.rs +++ b/crates/ruff_python_semantic/src/definition.rs @@ -202,8 +202,8 @@ impl<'a> Deref for Definitions<'a> { } impl<'a> IntoIterator for Definitions<'a> { - type Item = Definition<'a>; type IntoIter = std::vec::IntoIter; + type Item = Definition<'a>; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() diff --git a/crates/ruff_python_semantic/src/scope.rs b/crates/ruff_python_semantic/src/scope.rs index 69f9cca6d8..885452e7f5 100644 --- a/crates/ruff_python_semantic/src/scope.rs +++ b/crates/ruff_python_semantic/src/scope.rs @@ -250,6 +250,7 @@ impl Default for Scopes<'_> { impl<'a> Deref for Scopes<'a> { type Target = IndexSlice>; + fn deref(&self) -> &Self::Target { &self.0 } From 99486b38f474acc4bc5f4c6fe8c1c590c7829add Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 14 Jun 2023 20:47:00 -0400 Subject: [PATCH 068/447] Disambiguate all Python documentation references (#5107) --- ...an_default_value_in_function_definition.rs | 2 +- ...olean_positional_value_in_function_call.rs | 2 +- .../rules/check_positional_boolean_in_def.rs | 2 +- .../rules/multiple_starts_ends_with.rs | 4 +-- .../flake8_pie/rules/no_unnecessary_pass.rs | 2 +- .../flake8_pie/rules/non_unique_enums.rs | 2 +- .../rules/reimplemented_list_builtin.rs | 2 +- .../rules/unnecessary_dict_kwargs.rs | 4 +-- .../flake8_pie/rules/unnecessary_spread.rs | 2 +- .../flake8_pyi/rules/any_eq_ne_annotation.rs | 2 +- .../rules/flake8_simplify/rules/ast_expr.rs | 2 +- .../rules/open_file_with_context_handler.rs | 2 +- .../rules/return_in_try_except_finally.rs | 2 +- .../src/rules/pyflakes/rules/assert_tuple.rs | 2 +- .../pyflakes/rules/default_except_not_last.rs | 2 +- .../rules/future_feature_not_defined.rs | 2 +- .../ruff/src/rules/pyflakes/rules/if_tuple.rs | 2 +- .../rules/invalid_literal_comparisons.rs | 4 +-- .../pyflakes/rules/invalid_print_syntax.rs | 2 +- .../src/rules/pyflakes/rules/repeated_keys.rs | 4 +-- .../ruff/src/rules/pyflakes/rules/strings.rs | 28 +++++++++---------- .../rules/pyflakes/rules/undefined_name.rs | 2 +- .../pygrep_hooks/rules/deprecated_log_warn.rs | 2 +- .../src/rules/pygrep_hooks/rules/no_eval.rs | 2 +- .../rules/pylint/rules/await_outside_async.rs | 2 +- .../rules/pylint/rules/bad_str_strip_call.rs | 2 +- .../rules/pylint/rules/collapsible_else_if.rs | 2 +- .../pylint/rules/compare_to_empty_string.rs | 2 +- .../pylint/rules/comparison_of_constant.rs | 2 +- .../src/rules/pylint/rules/duplicate_bases.rs | 2 +- .../rules/global_variable_not_assigned.rs | 2 +- .../rules/pylint/rules/invalid_all_format.rs | 2 +- .../rules/pylint/rules/invalid_all_object.rs | 2 +- .../rules/load_before_global_declaration.rs | 2 +- .../rules/pylint/rules/manual_import_from.rs | 2 +- .../pylint/rules/nonlocal_without_binding.rs | 2 +- .../pylint/rules/property_with_parameters.rs | 2 +- .../src/rules/pylint/rules/sys_exit_alias.rs | 2 +- .../unexpected_special_method_signature.rs | 2 +- .../rules/unnecessary_direct_lambda_call.rs | 2 +- .../pylint/rules/useless_else_on_loop.rs | 2 +- .../tryceratops/rules/reraise_no_cause.rs | 2 +- .../tryceratops/rules/try_consider_else.rs | 2 +- .../rules/type_check_without_type_error.rs | 2 +- 44 files changed, 61 insertions(+), 61 deletions(-) diff --git a/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_default_value_in_function_definition.rs b/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_default_value_in_function_definition.rs index 4295b60d54..a920ffb129 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_default_value_in_function_definition.rs +++ b/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_default_value_in_function_definition.rs @@ -46,7 +46,7 @@ use super::super::helpers::FUNC_DEF_NAME_ALLOWLIST; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#calls) +/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls) /// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/) #[violation] pub struct BooleanDefaultValueInFunctionDefinition; diff --git a/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_positional_value_in_function_call.rs b/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_positional_value_in_function_call.rs index 5b39f0f24a..95b84c8b9c 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_positional_value_in_function_call.rs +++ b/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_positional_value_in_function_call.rs @@ -34,7 +34,7 @@ use crate::rules::flake8_boolean_trap::helpers::{add_if_boolean, allow_boolean_t /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#calls) +/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls) /// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/) #[violation] pub struct BooleanPositionalValueInFunctionCall; diff --git a/crates/ruff/src/rules/flake8_boolean_trap/rules/check_positional_boolean_in_def.rs b/crates/ruff/src/rules/flake8_boolean_trap/rules/check_positional_boolean_in_def.rs index 7d8ed6f574..20352a888e 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/rules/check_positional_boolean_in_def.rs +++ b/crates/ruff/src/rules/flake8_boolean_trap/rules/check_positional_boolean_in_def.rs @@ -64,7 +64,7 @@ use crate::rules::flake8_boolean_trap::helpers::FUNC_DEF_NAME_ALLOWLIST; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#calls) +/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls) /// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/) #[violation] pub struct BooleanPositionalArgInFunctionDefinition; 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 f00986155f..3846b11ac8 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 @@ -38,8 +38,8 @@ use crate::registry::AsRule; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#str.startswith) -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#str.endswith) +/// - [Python documentation: `str.startswith`](https://docs.python.org/3/library/stdtypes.html#str.startswith) +/// - [Python documentation: `str.endswith`](https://docs.python.org/3/library/stdtypes.html#str.endswith) #[violation] pub struct MultipleStartsEndsWith { attr: String, diff --git a/crates/ruff/src/rules/flake8_pie/rules/no_unnecessary_pass.rs b/crates/ruff/src/rules/flake8_pie/rules/no_unnecessary_pass.rs index c688376d7f..f9047c5004 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/no_unnecessary_pass.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/no_unnecessary_pass.rs @@ -32,7 +32,7 @@ use crate::registry::AsRule; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/simple_stmts.html#the-pass-statement) +/// - [Python documentation: The `pass` statement](https://docs.python.org/3/reference/simple_stmts.html#the-pass-statement) #[violation] pub struct UnnecessaryPass; diff --git a/crates/ruff/src/rules/flake8_pie/rules/non_unique_enums.rs b/crates/ruff/src/rules/flake8_pie/rules/non_unique_enums.rs index cfbfa7630a..63a5664bad 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/non_unique_enums.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/non_unique_enums.rs @@ -38,7 +38,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/enum.html#enum.Enum) +/// - [Python documentation: `enum.Enum`](https://docs.python.org/3/library/enum.html#enum.Enum) #[violation] pub struct NonUniqueEnums { value: String, diff --git a/crates/ruff/src/rules/flake8_pie/rules/reimplemented_list_builtin.rs b/crates/ruff/src/rules/flake8_pie/rules/reimplemented_list_builtin.rs index 0e53e5febf..e2b68792eb 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/reimplemented_list_builtin.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/reimplemented_list_builtin.rs @@ -34,7 +34,7 @@ use crate::registry::AsRule; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/functions.html#func-list) +/// - [Python documentation: `list`](https://docs.python.org/3/library/functions.html#func-list) #[violation] pub struct ReimplementedListBuiltin; diff --git a/crates/ruff/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs b/crates/ruff/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs index b621d1ff5f..5fedf58108 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs @@ -34,8 +34,8 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#dictionary-displays) -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#calls) +/// - [Python documentation: Dictionary displays](https://docs.python.org/3/reference/expressions.html#dictionary-displays) +/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls) #[violation] pub struct UnnecessaryDictKwargs; diff --git a/crates/ruff/src/rules/flake8_pie/rules/unnecessary_spread.rs b/crates/ruff/src/rules/flake8_pie/rules/unnecessary_spread.rs index 855d4ea26d..278d23d07c 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/unnecessary_spread.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/unnecessary_spread.rs @@ -26,7 +26,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#dictionary-displays) +/// - [Python documentation: Dictionary displays](https://docs.python.org/3/reference/expressions.html#dictionary-displays) #[violation] pub struct UnnecessarySpread; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs b/crates/ruff/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs index 0779bb6575..1f52670794 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs @@ -33,7 +33,7 @@ use crate::registry::AsRule; /// ... /// ``` /// ## References -/// - [Python documentation](https://docs.python.org/3/library/typing.html#the-any-type) +/// - [Python documentation: The `Any` type](https://docs.python.org/3/library/typing.html#the-any-type) /// - [Mypy documentation](https://mypy.readthedocs.io/en/latest/dynamic_typing.html#any-vs-object) #[violation] pub struct AnyEqNeAnnotation { 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 3ed9b778b9..d8de3b0e8a 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs @@ -73,7 +73,7 @@ impl Violation for UncapitalizedEnvironmentVariables { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#dict.get) +/// - [Python documentation: `dict.get`](https://docs.python.org/3/library/stdtypes.html#dict.get) #[violation] pub struct DictGetWithNoneDefault { expected: String, 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 bb7014fc8b..b59d76e040 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 @@ -29,7 +29,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// # References -/// - [Python documentation](https://docs.python.org/3/library/functions.html#open) +/// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open) #[violation] pub struct OpenFileWithContextHandler; diff --git a/crates/ruff/src/rules/flake8_simplify/rules/return_in_try_except_finally.rs b/crates/ruff/src/rules/flake8_simplify/rules/return_in_try_except_finally.rs index f966d4ab8d..ac8ae87bd0 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/return_in_try_except_finally.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/return_in_try_except_finally.rs @@ -38,7 +38,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/tutorial/errors.html#defining-clean-up-actions) +/// - [Python documentation: Defining Clean-up Actions](https://docs.python.org/3/tutorial/errors.html#defining-clean-up-actions) #[violation] pub struct ReturnInTryExceptFinally; diff --git a/crates/ruff/src/rules/pyflakes/rules/assert_tuple.rs b/crates/ruff/src/rules/pyflakes/rules/assert_tuple.rs index 9f76c7970d..1939ba4aa4 100644 --- a/crates/ruff/src/rules/pyflakes/rules/assert_tuple.rs +++ b/crates/ruff/src/rules/pyflakes/rules/assert_tuple.rs @@ -25,7 +25,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement) +/// - [Python documentation: The `assert` statement](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement) #[violation] pub struct AssertTuple; diff --git a/crates/ruff/src/rules/pyflakes/rules/default_except_not_last.rs b/crates/ruff/src/rules/pyflakes/rules/default_except_not_last.rs index 197f34e349..e27c4bd1cd 100644 --- a/crates/ruff/src/rules/pyflakes/rules/default_except_not_last.rs +++ b/crates/ruff/src/rules/pyflakes/rules/default_except_not_last.rs @@ -42,7 +42,7 @@ use ruff_python_ast::source_code::Locator; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/compound_stmts.html#except-clause) +/// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) #[violation] pub struct DefaultExceptNotLast; diff --git a/crates/ruff/src/rules/pyflakes/rules/future_feature_not_defined.rs b/crates/ruff/src/rules/pyflakes/rules/future_feature_not_defined.rs index 9e234ed1a8..11dc78d70b 100644 --- a/crates/ruff/src/rules/pyflakes/rules/future_feature_not_defined.rs +++ b/crates/ruff/src/rules/pyflakes/rules/future_feature_not_defined.rs @@ -15,7 +15,7 @@ use crate::checkers::ast::Checker; /// a `SyntaxError`. /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/__future__.html) +/// - [Python documentation: `__future__`](https://docs.python.org/3/library/__future__.html) #[violation] pub struct FutureFeatureNotDefined { name: String, diff --git a/crates/ruff/src/rules/pyflakes/rules/if_tuple.rs b/crates/ruff/src/rules/pyflakes/rules/if_tuple.rs index 01f9e9a784..98ce068c20 100644 --- a/crates/ruff/src/rules/pyflakes/rules/if_tuple.rs +++ b/crates/ruff/src/rules/pyflakes/rules/if_tuple.rs @@ -25,7 +25,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/compound_stmts.html#the-if-statement) +/// - [Python documentation: The `if` statement](https://docs.python.org/3/reference/compound_stmts.html#the-if-statement) #[violation] pub struct IfTuple; diff --git a/crates/ruff/src/rules/pyflakes/rules/invalid_literal_comparisons.rs b/crates/ruff/src/rules/pyflakes/rules/invalid_literal_comparisons.rs index 10beb70983..1b50e144cf 100644 --- a/crates/ruff/src/rules/pyflakes/rules/invalid_literal_comparisons.rs +++ b/crates/ruff/src/rules/pyflakes/rules/invalid_literal_comparisons.rs @@ -42,8 +42,8 @@ use crate::registry::AsRule; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#is-not) -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#value-comparisons) +/// - [Python documentation: Identity comparisons](https://docs.python.org/3/reference/expressions.html#is-not) +/// - [Python documentation: Value comparisons](https://docs.python.org/3/reference/expressions.html#value-comparisons) /// - [_Why does Python log a SyntaxWarning for ‘is’ with literals?_ by Adam Johnson](https://adamj.eu/tech/2020/01/21/why-does-python-3-8-syntaxwarning-for-is-literal/) #[violation] pub struct IsLiteral { diff --git a/crates/ruff/src/rules/pyflakes/rules/invalid_print_syntax.rs b/crates/ruff/src/rules/pyflakes/rules/invalid_print_syntax.rs index bfbb796bf0..9cc7a3eeaf 100644 --- a/crates/ruff/src/rules/pyflakes/rules/invalid_print_syntax.rs +++ b/crates/ruff/src/rules/pyflakes/rules/invalid_print_syntax.rs @@ -44,7 +44,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/functions.html#print) +/// - [Python documentation: `print`](https://docs.python.org/3/library/functions.html#print) #[violation] pub struct InvalidPrintSyntax; diff --git a/crates/ruff/src/rules/pyflakes/rules/repeated_keys.rs b/crates/ruff/src/rules/pyflakes/rules/repeated_keys.rs index 44f2c70a5d..99393cbddd 100644 --- a/crates/ruff/src/rules/pyflakes/rules/repeated_keys.rs +++ b/crates/ruff/src/rules/pyflakes/rules/repeated_keys.rs @@ -39,7 +39,7 @@ use crate::registry::{AsRule, Rule}; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/tutorial/datastructures.html#dictionaries) +/// - [Python documentation: Dictionaries](https://docs.python.org/3/tutorial/datastructures.html#dictionaries) #[violation] pub struct MultiValueRepeatedKeyLiteral { name: String, @@ -96,7 +96,7 @@ impl Violation for MultiValueRepeatedKeyLiteral { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/tutorial/datastructures.html#dictionaries) +/// - [Python documentation: Dictionaries](https://docs.python.org/3/tutorial/datastructures.html#dictionaries) #[violation] pub struct MultiValueRepeatedKeyVariable { name: String, diff --git a/crates/ruff/src/rules/pyflakes/rules/strings.rs b/crates/ruff/src/rules/pyflakes/rules/strings.rs index a6abde308a..5d5c79a35d 100644 --- a/crates/ruff/src/rules/pyflakes/rules/strings.rs +++ b/crates/ruff/src/rules/pyflakes/rules/strings.rs @@ -35,7 +35,7 @@ use super::super::format::FormatSummary; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) +/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[violation] pub struct PercentFormatInvalidFormat { pub(crate) message: String, @@ -74,7 +74,7 @@ impl Violation for PercentFormatInvalidFormat { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) +/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[violation] pub struct PercentFormatExpectedMapping; @@ -110,7 +110,7 @@ impl Violation for PercentFormatExpectedMapping { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) +/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[violation] pub struct PercentFormatExpectedSequence; @@ -139,7 +139,7 @@ impl Violation for PercentFormatExpectedSequence { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) +/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[violation] pub struct PercentFormatExtraNamedArguments { missing: Vec, @@ -179,7 +179,7 @@ impl AlwaysAutofixableViolation for PercentFormatExtraNamedArguments { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) +/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[violation] pub struct PercentFormatMissingArgument { missing: Vec, @@ -219,7 +219,7 @@ impl Violation for PercentFormatMissingArgument { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) +/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[violation] pub struct PercentFormatMixedPositionalAndNamed; @@ -249,7 +249,7 @@ impl Violation for PercentFormatMixedPositionalAndNamed { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) +/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[violation] pub struct PercentFormatPositionalCountMismatch { wanted: usize, @@ -287,7 +287,7 @@ impl Violation for PercentFormatPositionalCountMismatch { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) +/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[violation] pub struct PercentFormatStarRequiresSequence; @@ -317,7 +317,7 @@ impl Violation for PercentFormatStarRequiresSequence { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) +/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[violation] pub struct PercentFormatUnsupportedFormatCharacter { pub(crate) char: char, @@ -348,7 +348,7 @@ impl Violation for PercentFormatUnsupportedFormatCharacter { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#str.format) +/// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[violation] pub struct StringDotFormatInvalidFormat { pub(crate) message: String, @@ -380,7 +380,7 @@ impl Violation for StringDotFormatInvalidFormat { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#str.format) +/// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[violation] pub struct StringDotFormatExtraNamedArguments { missing: Vec, @@ -421,7 +421,7 @@ impl Violation for StringDotFormatExtraNamedArguments { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#str.format) +/// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[violation] pub struct StringDotFormatExtraPositionalArguments { missing: Vec, @@ -464,7 +464,7 @@ impl Violation for StringDotFormatExtraPositionalArguments { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#str.format) +/// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[violation] pub struct StringDotFormatMissingArguments { missing: Vec, @@ -502,7 +502,7 @@ impl Violation for StringDotFormatMissingArguments { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#str.format) +/// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[violation] pub struct StringDotFormatMixingAutomatic; diff --git a/crates/ruff/src/rules/pyflakes/rules/undefined_name.rs b/crates/ruff/src/rules/pyflakes/rules/undefined_name.rs index a4619cd618..9fae09e6c2 100644 --- a/crates/ruff/src/rules/pyflakes/rules/undefined_name.rs +++ b/crates/ruff/src/rules/pyflakes/rules/undefined_name.rs @@ -20,7 +20,7 @@ use ruff_macros::{derive_message_formats, violation}; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/executionmodel.html#naming-and-binding) +/// - [Python documentation: Naming and binding](https://docs.python.org/3/reference/executionmodel.html#naming-and-binding) #[violation] pub struct UndefinedName { pub(crate) name: String, diff --git a/crates/ruff/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs b/crates/ruff/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs index 8b4001d54a..19765c7876 100644 --- a/crates/ruff/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs +++ b/crates/ruff/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs @@ -30,7 +30,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/logging.html#logging.Logger.warning) +/// - [Python documentation: `logger.Logger.warning`](https://docs.python.org/3/library/logging.html#logging.Logger.warning) #[violation] pub struct DeprecatedLogWarn; diff --git a/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs b/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs index 70c8b57260..921d634e51 100644 --- a/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs +++ b/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs @@ -26,7 +26,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/functions.html#eval) +/// - [Python documentation: `eval`](https://docs.python.org/3/library/functions.html#eval) /// - [_Eval really is dangerous_ by Ned Batchelder](https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html) #[violation] pub struct Eval; diff --git a/crates/ruff/src/rules/pylint/rules/await_outside_async.rs b/crates/ruff/src/rules/pylint/rules/await_outside_async.rs index f0df96fcb0..dd17f4eb7b 100644 --- a/crates/ruff/src/rules/pylint/rules/await_outside_async.rs +++ b/crates/ruff/src/rules/pylint/rules/await_outside_async.rs @@ -30,7 +30,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#await) +/// - [Python documentation: Await expression](https://docs.python.org/3/reference/expressions.html#await) /// - [PEP 492](https://peps.python.org/pep-0492/#await-expression) #[violation] pub struct AwaitOutsideAsync; diff --git a/crates/ruff/src/rules/pylint/rules/bad_str_strip_call.rs b/crates/ruff/src/rules/pylint/rules/bad_str_strip_call.rs index c670c7ff14..e394928bcd 100644 --- a/crates/ruff/src/rules/pylint/rules/bad_str_strip_call.rs +++ b/crates/ruff/src/rules/pylint/rules/bad_str_strip_call.rs @@ -42,7 +42,7 @@ use crate::settings::types::PythonVersion; /// - `target-version` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html?highlight=strip#str.strip) +/// - [Python documentation: `str.strip`](https://docs.python.org/3/library/stdtypes.html?highlight=strip#str.strip) #[violation] pub struct BadStrStripCall { strip: StripKind, diff --git a/crates/ruff/src/rules/pylint/rules/collapsible_else_if.rs b/crates/ruff/src/rules/pylint/rules/collapsible_else_if.rs index d53fd72869..57a1bf8405 100644 --- a/crates/ruff/src/rules/pylint/rules/collapsible_else_if.rs +++ b/crates/ruff/src/rules/pylint/rules/collapsible_else_if.rs @@ -35,7 +35,7 @@ use ruff_python_ast::source_code::Locator; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/tutorial/controlflow.html#if-statements) +/// - [Python documentation: `if` Statements](https://docs.python.org/3/tutorial/controlflow.html#if-statements) #[violation] pub struct CollapsibleElseIf; diff --git a/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs b/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs index 4872e0f807..f79ae2e74f 100644 --- a/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs +++ b/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs @@ -73,7 +73,7 @@ impl std::fmt::Display for EmptyStringCmpop { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#truth-value-testing) +/// - [Python documentation: Truth Value Testing](https://docs.python.org/3/library/stdtypes.html#truth-value-testing) #[violation] pub struct CompareToEmptyString { existing: String, diff --git a/crates/ruff/src/rules/pylint/rules/comparison_of_constant.rs b/crates/ruff/src/rules/pylint/rules/comparison_of_constant.rs index b38f72ad2d..8930264d1e 100644 --- a/crates/ruff/src/rules/pylint/rules/comparison_of_constant.rs +++ b/crates/ruff/src/rules/pylint/rules/comparison_of_constant.rs @@ -26,7 +26,7 @@ use crate::rules::pylint::helpers::CmpopExt; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#comparisons) +/// - [Python documentation: Comparisons](https://docs.python.org/3/reference/expressions.html#comparisons) #[violation] pub struct ComparisonOfConstant { left_constant: String, diff --git a/crates/ruff/src/rules/pylint/rules/duplicate_bases.rs b/crates/ruff/src/rules/pylint/rules/duplicate_bases.rs index 6d6256f472..bd5a6b5c25 100644 --- a/crates/ruff/src/rules/pylint/rules/duplicate_bases.rs +++ b/crates/ruff/src/rules/pylint/rules/duplicate_bases.rs @@ -35,7 +35,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/compound_stmts.html#class-definitions) +/// - [Python documentation: Class definitions](https://docs.python.org/3/reference/compound_stmts.html#class-definitions) #[violation] pub struct DuplicateBases { base: String, diff --git a/crates/ruff/src/rules/pylint/rules/global_variable_not_assigned.rs b/crates/ruff/src/rules/pylint/rules/global_variable_not_assigned.rs index 9ac5cbe295..34857086de 100644 --- a/crates/ruff/src/rules/pylint/rules/global_variable_not_assigned.rs +++ b/crates/ruff/src/rules/pylint/rules/global_variable_not_assigned.rs @@ -34,7 +34,7 @@ use ruff_macros::{derive_message_formats, violation}; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/simple_stmts.html#the-global-statement) +/// - [Python documentation: The `global` statement](https://docs.python.org/3/reference/simple_stmts.html#the-global-statement) #[violation] pub struct GlobalVariableNotAssigned { pub name: String, diff --git a/crates/ruff/src/rules/pylint/rules/invalid_all_format.rs b/crates/ruff/src/rules/pylint/rules/invalid_all_format.rs index 0a818814ee..5b275a48a1 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_all_format.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_all_format.rs @@ -24,7 +24,7 @@ use ruff_macros::{derive_message_formats, violation}; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement) +/// - [Python documentation: The `import` statement](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement) #[violation] pub struct InvalidAllFormat; diff --git a/crates/ruff/src/rules/pylint/rules/invalid_all_object.rs b/crates/ruff/src/rules/pylint/rules/invalid_all_object.rs index 46e05c80e7..c555e64aa1 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_all_object.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_all_object.rs @@ -24,7 +24,7 @@ use ruff_macros::{derive_message_formats, violation}; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement) +/// - [Python documentation: The `import` statement](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement) #[violation] pub struct InvalidAllObject; diff --git a/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs b/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs index a408e8d4c6..fbde805d1c 100644 --- a/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs +++ b/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs @@ -38,7 +38,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/simple_stmts.html#the-global-statement) +/// - [Python documentation: The `global` statement](https://docs.python.org/3/reference/simple_stmts.html#the-global-statement) #[violation] pub struct LoadBeforeGlobalDeclaration { name: String, diff --git a/crates/ruff/src/rules/pylint/rules/manual_import_from.rs b/crates/ruff/src/rules/pylint/rules/manual_import_from.rs index 5201bc906d..7ca43129b9 100644 --- a/crates/ruff/src/rules/pylint/rules/manual_import_from.rs +++ b/crates/ruff/src/rules/pylint/rules/manual_import_from.rs @@ -25,7 +25,7 @@ use crate::registry::AsRule; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/import.html#submodules) +/// - [Python documentation: Submodules](https://docs.python.org/3/reference/import.html#submodules) #[violation] pub struct ManualFromImport { module: String, diff --git a/crates/ruff/src/rules/pylint/rules/nonlocal_without_binding.rs b/crates/ruff/src/rules/pylint/rules/nonlocal_without_binding.rs index 9f66ff859a..d29ef9bf44 100644 --- a/crates/ruff/src/rules/pylint/rules/nonlocal_without_binding.rs +++ b/crates/ruff/src/rules/pylint/rules/nonlocal_without_binding.rs @@ -26,7 +26,7 @@ use ruff_macros::{derive_message_formats, violation}; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/simple_stmts.html#nonlocal) +/// - [Python documentation: The `nonlocal` statement](https://docs.python.org/3/reference/simple_stmts.html#nonlocal) /// - [PEP 3104](https://peps.python.org/pep-3104/) #[violation] pub struct NonlocalWithoutBinding { diff --git a/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs b/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs index d068255511..aaded36e14 100644 --- a/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs +++ b/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs @@ -35,7 +35,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/functions.html#property) +/// - [Python documentation: `property`](https://docs.python.org/3/library/functions.html#property) #[violation] pub struct PropertyWithParameters; diff --git a/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs b/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs index dedac5503e..72005b96a6 100644 --- a/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs +++ b/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs @@ -35,7 +35,7 @@ use crate::registry::AsRule; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/constants.html#constants-added-by-the-site-module) +/// - [Python documentation: Constants added by the `site` module](https://docs.python.org/3/library/constants.html#constants-added-by-the-site-module) #[violation] pub struct SysExitAlias { name: String, 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 2790999a22..3b82d550f9 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 @@ -110,7 +110,7 @@ impl ExpectedParams { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/datamodel.html) +/// - [Python documentation: Data model](https://docs.python.org/3/reference/datamodel.html) #[violation] pub struct UnexpectedSpecialMethodSignature { method_name: String, diff --git a/crates/ruff/src/rules/pylint/rules/unnecessary_direct_lambda_call.rs b/crates/ruff/src/rules/pylint/rules/unnecessary_direct_lambda_call.rs index 32207ad885..528cbee097 100644 --- a/crates/ruff/src/rules/pylint/rules/unnecessary_direct_lambda_call.rs +++ b/crates/ruff/src/rules/pylint/rules/unnecessary_direct_lambda_call.rs @@ -23,7 +23,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#lambda) +/// - [Python documentation: Lambdas](https://docs.python.org/3/reference/expressions.html#lambda) #[violation] pub struct UnnecessaryDirectLambdaCall; diff --git a/crates/ruff/src/rules/pylint/rules/useless_else_on_loop.rs b/crates/ruff/src/rules/pylint/rules/useless_else_on_loop.rs index 79404a5bca..2073339f69 100644 --- a/crates/ruff/src/rules/pylint/rules/useless_else_on_loop.rs +++ b/crates/ruff/src/rules/pylint/rules/useless_else_on_loop.rs @@ -37,7 +37,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/tutorial/controlflow.html#break-and-continue-statements-and-else-clauses-on-loops) +/// - [Python documentation: `break` and `continue` Statements, and `else` Clauses on Loops](https://docs.python.org/3/tutorial/controlflow.html#break-and-continue-statements-and-else-clauses-on-loops) #[violation] pub struct UselessElseOnLoop; diff --git a/crates/ruff/src/rules/tryceratops/rules/reraise_no_cause.rs b/crates/ruff/src/rules/tryceratops/rules/reraise_no_cause.rs index 259dfac223..a8a74e85b6 100644 --- a/crates/ruff/src/rules/tryceratops/rules/reraise_no_cause.rs +++ b/crates/ruff/src/rules/tryceratops/rules/reraise_no_cause.rs @@ -35,7 +35,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/exceptions.html#exception-context) +/// - [Python documentation: Exception context](https://docs.python.org/3/library/exceptions.html#exception-context) #[violation] pub struct ReraiseNoCause; diff --git a/crates/ruff/src/rules/tryceratops/rules/try_consider_else.rs b/crates/ruff/src/rules/tryceratops/rules/try_consider_else.rs index ca866ff95c..10f30c1e25 100644 --- a/crates/ruff/src/rules/tryceratops/rules/try_consider_else.rs +++ b/crates/ruff/src/rules/tryceratops/rules/try_consider_else.rs @@ -44,7 +44,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/tutorial/errors.html) +/// - [Python documentation: Errors and Exceptions](https://docs.python.org/3/tutorial/errors.html) #[violation] pub struct TryConsiderElse; diff --git a/crates/ruff/src/rules/tryceratops/rules/type_check_without_type_error.rs b/crates/ruff/src/rules/tryceratops/rules/type_check_without_type_error.rs index 26051c90f4..193da3a5c9 100644 --- a/crates/ruff/src/rules/tryceratops/rules/type_check_without_type_error.rs +++ b/crates/ruff/src/rules/tryceratops/rules/type_check_without_type_error.rs @@ -32,7 +32,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/exceptions.html#TypeError) +/// - [Python documentation: `TypeError`](https://docs.python.org/3/library/exceptions.html#TypeError) #[violation] pub struct TypeCheckWithoutTypeError; From c654280d84c9cb7c6aec24f3a3f77ebafbf67cbd Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 14 Jun 2023 21:12:23 -0400 Subject: [PATCH 069/447] Use direct links for all PEP 8 references (#5108) --- .../rules/implicit.rs | 5 ++-- .../flake8_simplify/rules/ast_bool_op.rs | 2 +- .../rules/flake8_simplify/rules/ast_with.rs | 2 +- .../rules/pycodestyle/rules/bare_except.rs | 3 +-- .../pycodestyle/rules/compound_statements.rs | 12 ++++----- .../src/rules/pycodestyle/rules/imports.rs | 10 +++---- .../pycodestyle/rules/lambda_assignment.rs | 3 +-- .../pycodestyle/rules/literal_comparisons.rs | 10 +++---- .../logical_lines/extraneous_whitespace.rs | 15 +++++------ .../rules/logical_lines/indentation.rs | 27 +++++++------------ .../logical_lines/space_around_operator.rs | 20 ++++++-------- .../whitespace_before_comment.rs | 9 +++---- .../pycodestyle/rules/trailing_whitespace.rs | 10 +++---- .../ruff/src/rules/pyflakes/rules/imports.rs | 18 +++++-------- .../pylint/rules/magic_value_comparison.rs | 9 +++---- .../pyupgrade/rules/quoted_annotation.rs | 2 +- 16 files changed, 62 insertions(+), 95 deletions(-) diff --git a/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs b/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs index 1d7c32a0c7..69def84c39 100644 --- a/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs +++ b/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs @@ -51,7 +51,7 @@ impl Violation for SingleLineImplicitStringConcatenation { /// Checks for implicitly concatenated strings that span multiple lines. /// /// ## Why is this bad? -/// For string literals that wrap across multiple lines, PEP 8 recommends +/// For string literals that wrap across multiple lines, [PEP 8] recommends /// the use of implicit string concatenation within parentheses instead of /// using a backslash for line continuation, as the former is more readable /// than the latter. @@ -78,8 +78,7 @@ impl Violation for SingleLineImplicitStringConcatenation { /// ## Options /// - `flake8-implicit-str-concat.allow-multiline` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#maximum-line-length) +/// [PEP 8]: https://peps.python.org/pep-0008/#maximum-line-length #[violation] pub struct MultiLineImplicitStringConcatenation; 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 7a21a13ead..4c7fdd4cb0 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 @@ -40,7 +40,7 @@ use crate::registry::AsRule; /// ``` /// /// ## References -/// - [Python: "isinstance"](https://docs.python.org/3/library/functions.html#isinstance) +/// - [Python documentation: `isinstance`](https://docs.python.org/3/library/functions.html#isinstance) #[violation] pub struct DuplicateIsinstanceCall { name: Option, diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs index 7e1cc3a43d..72dbce6b09 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs @@ -40,7 +40,7 @@ use super::fix_with; /// ``` /// /// ## References -/// - [Python: "The with statement"](https://docs.python.org/3/reference/compound_stmts.html#the-with-statement) +/// - [Python documentation: The `with` statement](https://docs.python.org/3/reference/compound_stmts.html#the-with-statement) #[violation] pub struct MultipleWithStatements; diff --git a/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs b/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs index 42adebdc54..0b9eee1598 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs @@ -42,8 +42,7 @@ use ruff_python_ast::source_code::Locator; /// ``` /// /// ## References -/// - [PEP 8](https://www.python.org/dev/peps/pep-0008/#programming-recommendations) -/// - [Python: "Exception hierarchy"](https://docs.python.org/3/library/exceptions.html#exception-hierarchy) +/// - [Python documentation: Exception hierarchy](https://docs.python.org/3/library/exceptions.html#exception-hierarchy) /// - [Google Python Style Guide: "Exceptions"](https://google.github.io/styleguide/pyguide.html#24-exceptions) #[violation] pub struct BareExcept; diff --git a/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs b/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs index 908e1c07f9..6f095d8272 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs @@ -13,7 +13,7 @@ use crate::settings::Settings; /// Checks for compound statements (multiple statements on the same line). /// /// ## Why is this bad? -/// Per PEP 8, "compound statements are generally discouraged". +/// According to [PEP 8], "compound statements are generally discouraged". /// /// ## Example /// ```python @@ -25,9 +25,8 @@ use crate::settings::Settings; /// if foo == "blah": /// do_blah_thing() /// ``` -/// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#other-recommendations) + +/// [PEP 8]: https://peps.python.org/pep-0008/#other-recommendations #[violation] pub struct MultipleStatementsOnOneLineColon; @@ -42,7 +41,7 @@ impl Violation for MultipleStatementsOnOneLineColon { /// Checks for multiline statements on one line. /// /// ## Why is this bad? -/// Per PEP 8, including multi-clause statements on the same line is +/// According to [PEP 8], including multi-clause statements on the same line is /// discouraged. /// /// ## Example @@ -57,8 +56,7 @@ impl Violation for MultipleStatementsOnOneLineColon { /// do_three() /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#other-recommendations) +/// [PEP 8]: https://peps.python.org/pep-0008/#other-recommendations #[violation] pub struct MultipleStatementsOnOneLineSemicolon; diff --git a/crates/ruff/src/rules/pycodestyle/rules/imports.rs b/crates/ruff/src/rules/pycodestyle/rules/imports.rs index 61e163354c..4504ade301 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/imports.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/imports.rs @@ -10,7 +10,7 @@ use crate::checkers::ast::Checker; /// Check for multiple imports on one line. /// /// ## Why is this bad? -/// Per PEP 8, "imports should usually be on separate lines." +/// According to [PEP 8], "imports should usually be on separate lines." /// /// ## Example /// ```python @@ -23,8 +23,7 @@ use crate::checkers::ast::Checker; /// import sys /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#imports) +/// [PEP 8]: https://peps.python.org/pep-0008/#imports #[violation] pub struct MultipleImportsOnOneLine; @@ -39,7 +38,7 @@ impl Violation for MultipleImportsOnOneLine { /// Checks for imports that are not at the top of the file. /// /// ## Why is this bad? -/// Per PEP 8, "imports are always put at the top of the file, just after any +/// According to [PEP 8], "imports are always put at the top of the file, just after any /// module comments and docstrings, and before module globals and constants." /// /// ## Example @@ -61,8 +60,7 @@ impl Violation for MultipleImportsOnOneLine { /// a = 1 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#imports) +/// [PEP 8]: https://peps.python.org/pep-0008/#imports #[violation] pub struct ModuleImportNotAtTopOfFile; diff --git a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs index 8c17df32ec..99ffe1f766 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs @@ -33,8 +33,7 @@ use crate::registry::AsRule; /// return 2 * x /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#programming-recommendations) +/// [PEP 8]: https://peps.python.org/pep-0008/#programming-recommendations #[violation] pub struct LambdaAssignment { name: String, diff --git a/crates/ruff/src/rules/pycodestyle/rules/literal_comparisons.rs b/crates/ruff/src/rules/pycodestyle/rules/literal_comparisons.rs index 8f4eaff3bc..adf7933c32 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/literal_comparisons.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/literal_comparisons.rs @@ -30,7 +30,7 @@ impl EqCmpop { /// Checks for comparisons to `None` which are not using the `is` operator. /// /// ## Why is this bad? -/// Per PEP 8, "Comparisons to singletons like None should always be done with +/// According to [PEP 8], "Comparisons to singletons like None should always be done with /// is or is not, never the equality operators." /// /// ## Example @@ -47,8 +47,7 @@ impl EqCmpop { /// pass /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#programming-recommendations) +/// [PEP 8]: https://peps.python.org/pep-0008/#programming-recommendations #[violation] pub struct NoneComparison(EqCmpop); @@ -75,7 +74,7 @@ impl AlwaysAutofixableViolation for NoneComparison { /// Checks for comparisons to booleans which are not using the `is` operator. /// /// ## Why is this bad? -/// Per PEP 8, "Comparisons to singletons like None should always be done with +/// According to [PEP 8], "Comparisons to singletons like None should always be done with /// is or is not, never the equality operators." /// /// ## Example @@ -94,8 +93,7 @@ impl AlwaysAutofixableViolation for NoneComparison { /// pass /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#programming-recommendations) +/// [PEP 8]: https://peps.python.org/pep-0008/#programming-recommendations #[violation] pub struct TrueFalseComparison(bool, EqCmpop); diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs index d8a3512cf7..ca4b1ed3d0 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs @@ -14,7 +14,7 @@ use super::{LogicalLine, Whitespace}; /// Checks for the use of extraneous whitespace after "(". /// /// ## Why is this bad? -/// PEP 8 recommends the omission of whitespace in the following cases: +/// [PEP 8] recommends the omission of whitespace in the following cases: /// - "Immediately inside parentheses, brackets or braces." /// - "Immediately before a comma, semicolon, or colon." /// @@ -30,8 +30,7 @@ use super::{LogicalLine, Whitespace}; /// spam(ham[1], {eggs: 2}) /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#pet-peeves) +/// [PEP 8]: https://peps.python.org/pep-0008/#pet-peeves #[violation] pub struct WhitespaceAfterOpenBracket { symbol: char, @@ -54,7 +53,7 @@ impl AlwaysAutofixableViolation for WhitespaceAfterOpenBracket { /// Checks for the use of extraneous whitespace before ")". /// /// ## Why is this bad? -/// PEP 8 recommends the omission of whitespace in the following cases: +/// [PEP 8] recommends the omission of whitespace in the following cases: /// - "Immediately inside parentheses, brackets or braces." /// - "Immediately before a comma, semicolon, or colon." /// @@ -70,8 +69,7 @@ impl AlwaysAutofixableViolation for WhitespaceAfterOpenBracket { /// spam(ham[1], {eggs: 2}) /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#pet-peeves) +/// [PEP 8]: https://peps.python.org/pep-0008/#pet-peeves #[violation] pub struct WhitespaceBeforeCloseBracket { symbol: char, @@ -94,7 +92,7 @@ impl AlwaysAutofixableViolation for WhitespaceBeforeCloseBracket { /// Checks for the use of extraneous whitespace before ",", ";" or ":". /// /// ## Why is this bad? -/// PEP 8 recommends the omission of whitespace in the following cases: +/// [PEP 8] recommends the omission of whitespace in the following cases: /// - "Immediately inside parentheses, brackets or braces." /// - "Immediately before a comma, semicolon, or colon." /// @@ -108,8 +106,7 @@ impl AlwaysAutofixableViolation for WhitespaceBeforeCloseBracket { /// if x == 4: print(x, y); x, y = y, x /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#pet-peeves) +/// [PEP 8]: https://peps.python.org/pep-0008/#pet-peeves #[violation] pub struct WhitespaceBeforePunctuation { symbol: char, diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/indentation.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/indentation.rs index 9f7f3b652b..6fa4cb316c 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/indentation.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/indentation.rs @@ -9,7 +9,7 @@ use super::LogicalLine; /// Checks for indentation with a non-multiple of 4 spaces. /// /// ## Why is this bad? -/// Per PEP 8, 4 spaces per indentation level should be preferred. +/// According to [PEP 8], 4 spaces per indentation level should be preferred. /// /// ## Example /// ```python @@ -23,8 +23,7 @@ use super::LogicalLine; /// a = 1 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#indentation) +/// [PEP 8]: https://peps.python.org/pep-0008/#indentation #[violation] pub struct IndentationWithInvalidMultiple { indent_size: usize, @@ -42,7 +41,7 @@ impl Violation for IndentationWithInvalidMultiple { /// Checks for indentation of comments with a non-multiple of 4 spaces. /// /// ## Why is this bad? -/// Per PEP 8, 4 spaces per indentation level should be preferred. +/// According to [PEP 8], 4 spaces per indentation level should be preferred. /// /// ## Example /// ```python @@ -56,8 +55,7 @@ impl Violation for IndentationWithInvalidMultiple { /// # a = 1 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#indentation) +/// [PEP 8]: https://peps.python.org/pep-0008/#indentation #[violation] pub struct IndentationWithInvalidMultipleComment { indent_size: usize, @@ -90,8 +88,7 @@ impl Violation for IndentationWithInvalidMultipleComment { /// pass /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#indentation) +/// [PEP 8]: https://peps.python.org/pep-0008/#indentation #[violation] pub struct NoIndentedBlock; @@ -123,8 +120,7 @@ impl Violation for NoIndentedBlock { /// pass /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#indentation) +/// [PEP 8]: https://peps.python.org/pep-0008/#indentation #[violation] pub struct NoIndentedBlockComment; @@ -153,8 +149,7 @@ impl Violation for NoIndentedBlockComment { /// b = 2 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#indentation) +/// [PEP 8]: https://peps.python.org/pep-0008/#indentation #[violation] pub struct UnexpectedIndentation; @@ -183,8 +178,7 @@ impl Violation for UnexpectedIndentation { /// # b = 2 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#indentation) +/// [PEP 8]: https://peps.python.org/pep-0008/#indentation #[violation] pub struct UnexpectedIndentationComment; @@ -199,7 +193,7 @@ impl Violation for UnexpectedIndentationComment { /// Checks for over-indented code. /// /// ## Why is this bad? -/// Per PEP 8, 4 spaces per indentation level should be preferred. Increased +/// According to [PEP 8], 4 spaces per indentation level should be preferred. Increased /// indentation can lead to inconsistent formatting, which can hurt /// readability. /// @@ -215,8 +209,7 @@ impl Violation for UnexpectedIndentationComment { /// pass /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#indentation) +/// [PEP 8]: https://peps.python.org/pep-0008/#indentation #[violation] pub struct OverIndented { is_comment: bool, diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs index 5c111773de..d7ea88a803 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs @@ -12,7 +12,7 @@ use super::{LogicalLine, Whitespace}; /// Checks for extraneous tabs before an operator. /// /// ## Why is this bad? -/// Per PEP 8, operators should be surrounded by at most a single space on either +/// According to [PEP 8], operators should be surrounded by at most a single space on either /// side. /// /// ## Example @@ -25,8 +25,7 @@ use super::{LogicalLine, Whitespace}; /// a = 12 + 3 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements) +/// [PEP 8]: https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements #[violation] pub struct TabBeforeOperator; @@ -41,7 +40,7 @@ impl Violation for TabBeforeOperator { /// Checks for extraneous whitespace before an operator. /// /// ## Why is this bad? -/// Per PEP 8, operators should be surrounded by at most a single space on either +/// According to [PEP 8], operators should be surrounded by at most a single space on either /// side. /// /// ## Example @@ -54,8 +53,7 @@ impl Violation for TabBeforeOperator { /// a = 12 + 3 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements) +/// [PEP 8]: https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements #[violation] pub struct MultipleSpacesBeforeOperator; @@ -70,7 +68,7 @@ impl Violation for MultipleSpacesBeforeOperator { /// Checks for extraneous tabs after an operator. /// /// ## Why is this bad? -/// Per PEP 8, operators should be surrounded by at most a single space on either +/// According to [PEP 8], operators should be surrounded by at most a single space on either /// side. /// /// ## Example @@ -83,8 +81,7 @@ impl Violation for MultipleSpacesBeforeOperator { /// a = 12 + 3 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements) +/// [PEP 8]: https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements #[violation] pub struct TabAfterOperator; @@ -99,7 +96,7 @@ impl Violation for TabAfterOperator { /// Checks for extraneous whitespace after an operator. /// /// ## Why is this bad? -/// Per PEP 8, operators should be surrounded by at most a single space on either +/// According to [PEP 8], operators should be surrounded by at most a single space on either /// side. /// /// ## Example @@ -112,8 +109,7 @@ impl Violation for TabAfterOperator { /// a = 12 + 3 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements) +/// [PEP 8]: https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements #[violation] pub struct MultipleSpacesAfterOperator; diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs index 3a42136dcb..dd2e4e4271 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs @@ -59,8 +59,7 @@ impl Violation for TooFewSpacesBeforeInlineComment { /// x = x + 1 # Increment x /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#comments) +/// [PEP 8]: https://peps.python.org/pep-0008/#comments #[violation] pub struct NoSpaceAfterInlineComment; @@ -92,8 +91,7 @@ impl Violation for NoSpaceAfterInlineComment { /// # \xa0- Block comment list /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#comments) +/// [PEP 8]: https://peps.python.org/pep-0008/#comments #[violation] pub struct NoSpaceAfterBlockComment; @@ -125,8 +123,7 @@ impl Violation for NoSpaceAfterBlockComment { /// # \xa0- Block comment list /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#comments) +/// [PEP 8]: https://peps.python.org/pep-0008/#comments #[violation] pub struct MultipleLeadingHashesForBlockComment; diff --git a/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs b/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs index f4841a385d..ff7082c9c7 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs @@ -11,7 +11,7 @@ use crate::settings::Settings; /// Checks for superfluous trailing whitespace. /// /// ## Why is this bad? -/// Per PEP 8, "avoid trailing whitespace anywhere. Because it’s usually +/// According to [PEP 8], "avoid trailing whitespace anywhere. Because it’s usually /// invisible, it can be confusing" /// /// ## Example @@ -24,8 +24,7 @@ use crate::settings::Settings; /// spam(1)\n# /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#other-recommendations) +/// [PEP 8]: https://peps.python.org/pep-0008/#other-recommendations #[violation] pub struct TrailingWhitespace; @@ -44,7 +43,7 @@ impl AlwaysAutofixableViolation for TrailingWhitespace { /// Checks for superfluous whitespace in blank lines. /// /// ## Why is this bad? -/// Per PEP 8, "avoid trailing whitespace anywhere. Because it’s usually +/// According to [PEP 8], "avoid trailing whitespace anywhere. Because it’s usually /// invisible, it can be confusing" /// /// ## Example @@ -57,8 +56,7 @@ impl AlwaysAutofixableViolation for TrailingWhitespace { /// class Foo(object):\n\n bang = 12 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#other-recommendations) +/// [PEP 8]: https://peps.python.org/pep-0008/#other-recommendations #[violation] pub struct BlankLineWithWhitespace; diff --git a/crates/ruff/src/rules/pyflakes/rules/imports.rs b/crates/ruff/src/rules/pyflakes/rules/imports.rs index 220925b709..b53b9394f1 100644 --- a/crates/ruff/src/rules/pyflakes/rules/imports.rs +++ b/crates/ruff/src/rules/pyflakes/rules/imports.rs @@ -51,7 +51,7 @@ impl Violation for ImportShadowedByLoopVar { /// ## Why is this bad? /// Wildcard imports (e.g., `from module import *`) make it hard to determine /// which symbols are available in the current namespace, and from which module -/// they were imported. +/// they were imported. They're also discouraged by [PEP 8]. /// /// ## Example /// ```python @@ -71,8 +71,7 @@ impl Violation for ImportShadowedByLoopVar { /// return pi * radius**2 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#imports) +/// [PEP 8]: https://peps.python.org/pep-0008/#imports #[violation] pub struct UndefinedLocalWithImportStar { pub(crate) name: String, @@ -110,7 +109,7 @@ impl Violation for UndefinedLocalWithImportStar { /// ``` /// /// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#module-level-dunder-names) +/// - [Python documentation: Future statements](https://docs.python.org/3/reference/simple_stmts.html#future) #[violation] pub struct LateFutureImport; @@ -155,9 +154,6 @@ impl Violation for LateFutureImport { /// def area(radius): /// return pi * radius**2 /// ``` -/// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#imports) #[violation] pub struct UndefinedLocalWithImportStarUsage { pub(crate) name: String, @@ -183,8 +179,9 @@ impl Violation for UndefinedLocalWithImportStarUsage { /// The use of wildcard imports outside of the module namespace (e.g., within /// functions) can lead to confusion, as the import can shadow local variables. /// -/// Though wildcard imports are discouraged, when necessary, they should be placed -/// in the module namespace (i.e., at the top-level of a module). +/// Though wildcard imports are discouraged by [PEP 8], when necessary, they +/// should be placed in the module namespace (i.e., at the top-level of a +/// module). /// /// ## Example /// ```python @@ -201,8 +198,7 @@ impl Violation for UndefinedLocalWithImportStarUsage { /// ... /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#imports) +/// [PEP 8]: https://peps.python.org/pep-0008/#imports #[violation] pub struct UndefinedLocalWithNestedImportStarUsage { pub(crate) name: String, diff --git a/crates/ruff/src/rules/pylint/rules/magic_value_comparison.rs b/crates/ruff/src/rules/pylint/rules/magic_value_comparison.rs index 60a6a8e4c2..c337641c79 100644 --- a/crates/ruff/src/rules/pylint/rules/magic_value_comparison.rs +++ b/crates/ruff/src/rules/pylint/rules/magic_value_comparison.rs @@ -12,8 +12,9 @@ use crate::rules::pylint::settings::ConstantType; /// comparisons. /// /// ## Why is this bad? -/// The use of "magic" can make code harder to read and maintain, as readers -/// will have to infer the meaning of the value from the context. +/// The use of "magic" values can make code harder to read and maintain, as +/// readers will have to infer the meaning of the value from the context. +/// Such values are discouraged by [PEP 8]. /// /// For convenience, this rule excludes a variety of common values from the /// "magic" value definition, such as `0`, `1`, `""`, and `"__main__"`. @@ -33,9 +34,7 @@ use crate::rules::pylint::settings::ConstantType; /// return price * (1 - DISCOUNT_RATE) /// ``` /// -/// ## References -/// - [Wikipedia](https://en.wikipedia.org/wiki/Magic_number_(programming)#Unnamed_numerical_constants) -/// - [PEP 8](https://peps.python.org/pep-0008/#constants) +/// [PEP 8]: https://peps.python.org/pep-0008/#constants #[violation] pub struct MagicValueComparison { value: String, diff --git a/crates/ruff/src/rules/pyupgrade/rules/quoted_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/quoted_annotation.rs index c14ad79904..12988fecee 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/quoted_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/quoted_annotation.rs @@ -35,7 +35,7 @@ use crate::registry::Rule; /// /// ## References /// - [PEP 563](https://peps.python.org/pep-0563/) -/// - [Python documentation: `__future__` - Future statement definitions](https://docs.python.org/3/library/__future__.html#module-__future__) +/// - [Python documentation: `__future__`](https://docs.python.org/3/library/__future__.html#module-__future__) #[violation] pub struct QuotedAnnotation; From ed8113267c546654de97fcadb7d14f9719757c7b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 14 Jun 2023 22:03:37 -0400 Subject: [PATCH 070/447] Add autofix specification levels for a variety of rules (#5109) --- .../eradicate/rules/commented_out_code.rs | 3 +- ...s__eradicate__tests__ERA001_ERA001.py.snap | 12 +-- .../flake8_bugbear/rules/assert_false.rs | 1 - .../rules/duplicate_exceptions.rs | 3 +- ...__flake8_bugbear__tests__B014_B014.py.snap | 6 +- .../rules/shebang_whitespace.rs | 3 +- ...flake8_executable__tests__EXE004_1.py.snap | 2 +- .../rules/duplicate_union_member.rs | 3 +- .../rules/quoted_annotation_in_stub.rs | 3 +- ..._flake8_pyi__tests__PYI016_PYI016.pyi.snap | 22 ++--- ..._flake8_pyi__tests__PYI020_PYI020.pyi.snap | 18 ++-- .../unnecessary_paren_on_raise_exception.rs | 3 +- ...ry-paren-on-raise-exception_RSE102.py.snap | 12 +-- .../rules/reimplemented_builtin.rs | 6 +- .../flake8_simplify/rules/yoda_conditions.rs | 3 +- ...ke8_simplify__tests__SIM300_SIM300.py.snap | 30 +++--- .../numpy/rules/deprecated_type_alias.rs | 3 +- .../pycodestyle/rules/lambda_assignment.rs | 3 +- .../pycodestyle/rules/literal_comparisons.rs | 3 +- .../src/rules/pycodestyle/rules/not_tests.rs | 6 +- ...les__pycodestyle__tests__E713_E713.py.snap | 10 +- ...les__pycodestyle__tests__E714_E714.py.snap | 4 +- ...pycodestyle__tests__constant_literals.snap | 10 +- .../rules/invalid_literal_comparisons.rs | 3 +- .../pyflakes/rules/raise_not_implemented.rs | 21 +++-- .../src/rules/pyflakes/rules/repeated_keys.rs | 6 +- ..._rules__pyflakes__tests__F632_F632.py.snap | 18 ++-- ..._rules__pyflakes__tests__F901_F901.py.snap | 4 +- .../rules/pylint/rules/manual_import_from.rs | 3 +- ...nt__tests__PLR0402_import_aliasing.py.snap | 4 +- .../rules/deprecated_unittest_alias.rs | 3 +- .../src/rules/pyupgrade/rules/f_strings.rs | 3 +- .../rules/pyupgrade/rules/os_error_alias.rs | 93 ++++++++++--------- .../pyupgrade/rules/outdated_version_block.rs | 27 ++---- .../pyupgrade/rules/quoted_annotation.rs | 3 +- .../pyupgrade/rules/redundant_open_modes.rs | 8 +- .../rules/replace_universal_newlines.rs | 7 +- .../pyupgrade/rules/type_of_primitive.rs | 25 +++-- .../rules/unnecessary_coding_comment.rs | 3 +- .../rules/unnecessary_encode_utf8.rs | 20 ++-- .../pyupgrade/rules/yield_in_for_loop.rs | 3 +- ...ff__rules__pyupgrade__tests__UP003.py.snap | 10 +- ...__rules__pyupgrade__tests__UP009_0.py.snap | 2 +- ...__rules__pyupgrade__tests__UP009_1.py.snap | 2 +- ...ff__rules__pyupgrade__tests__UP012.py.snap | 42 ++++----- ...ff__rules__pyupgrade__tests__UP015.py.snap | 88 +++++++++--------- ...__rules__pyupgrade__tests__UP024_0.py.snap | 26 +++--- ...__rules__pyupgrade__tests__UP024_1.py.snap | 6 +- ...__rules__pyupgrade__tests__UP024_2.py.snap | 40 ++++---- ...__rules__pyupgrade__tests__UP024_4.py.snap | 2 +- ...ff__rules__pyupgrade__tests__UP037.py.snap | 56 +++++------ .../ruff/rules/ambiguous_unicode_character.rs | 3 +- ...ruff__rules__ruff__tests__confusables.snap | 14 +-- 53 files changed, 344 insertions(+), 370 deletions(-) diff --git a/crates/ruff/src/rules/eradicate/rules/commented_out_code.rs b/crates/ruff/src/rules/eradicate/rules/commented_out_code.rs index 12302f7786..1e8aec5126 100644 --- a/crates/ruff/src/rules/eradicate/rules/commented_out_code.rs +++ b/crates/ruff/src/rules/eradicate/rules/commented_out_code.rs @@ -62,8 +62,7 @@ pub(crate) fn commented_out_code( let mut diagnostic = Diagnostic::new(CommentedOutCode, *range); if settings.rules.should_fix(Rule::CommentedOutCode) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_deletion( + diagnostic.set_fix(Fix::manual(Edit::range_deletion( locator.full_lines_range(*range), ))); } diff --git a/crates/ruff/src/rules/eradicate/snapshots/ruff__rules__eradicate__tests__ERA001_ERA001.py.snap b/crates/ruff/src/rules/eradicate/snapshots/ruff__rules__eradicate__tests__ERA001_ERA001.py.snap index 9aff20a52b..17ccde3ca7 100644 --- a/crates/ruff/src/rules/eradicate/snapshots/ruff__rules__eradicate__tests__ERA001_ERA001.py.snap +++ b/crates/ruff/src/rules/eradicate/snapshots/ruff__rules__eradicate__tests__ERA001_ERA001.py.snap @@ -10,7 +10,7 @@ ERA001.py:1:1: ERA001 [*] Found commented-out code | = help: Remove commented-out code -ℹ Suggested fix +ℹ Possible fix 1 |-#import os 2 1 | # from foo import junk 3 2 | #a = 3 @@ -26,7 +26,7 @@ ERA001.py:2:1: ERA001 [*] Found commented-out code | = help: Remove commented-out code -ℹ Suggested fix +ℹ Possible fix 1 1 | #import os 2 |-# from foo import junk 3 2 | #a = 3 @@ -44,7 +44,7 @@ ERA001.py:3:1: ERA001 [*] Found commented-out code | = help: Remove commented-out code -ℹ Suggested fix +ℹ Possible fix 1 1 | #import os 2 2 | # from foo import junk 3 |-#a = 3 @@ -63,7 +63,7 @@ ERA001.py:5:1: ERA001 [*] Found commented-out code | = help: Remove commented-out code -ℹ Suggested fix +ℹ Possible fix 2 2 | # from foo import junk 3 3 | #a = 3 4 4 | a = 4 @@ -82,7 +82,7 @@ ERA001.py:13:5: ERA001 [*] Found commented-out code | = help: Remove commented-out code -ℹ Suggested fix +ℹ Possible fix 10 10 | 11 11 | # This is a real comment. 12 12 | # # This is a (nested) comment. @@ -100,7 +100,7 @@ ERA001.py:21:5: ERA001 [*] Found commented-out code | = help: Remove commented-out code -ℹ Suggested fix +ℹ Possible fix 18 18 | 19 19 | class A(): 20 20 | pass diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs b/crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs index 817483b00e..290fefdfa2 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs @@ -53,7 +53,6 @@ pub(crate) fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: let mut diagnostic = Diagnostic::new(AssertFalse, test.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().stmt(&assertion_error(msg)), stmt.range(), 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 95eed78d56..39b20692a1 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs @@ -89,8 +89,7 @@ fn duplicate_handler_exceptions<'a>( expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::suggested(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( if unique_elts.len() == 1 { checker.generator().expr(unique_elts[0]) } else { diff --git a/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B014_B014.py.snap b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B014_B014.py.snap index 50c0748163..bb8fb6c3c2 100644 --- a/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B014_B014.py.snap +++ b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B014_B014.py.snap @@ -12,7 +12,7 @@ B014.py:17:8: B014 [*] Exception handler with duplicate exception: `OSError` | = help: De-duplicate exceptions -ℹ Suggested fix +ℹ Fix 14 14 | 15 15 | try: 16 16 | pass @@ -33,7 +33,7 @@ B014.py:28:8: B014 [*] Exception handler with duplicate exception: `MyError` | = help: De-duplicate exceptions -ℹ Suggested fix +ℹ Fix 25 25 | 26 26 | try: 27 27 | pass @@ -54,7 +54,7 @@ B014.py:49:8: B014 [*] Exception handler with duplicate exception: `re.error` | = help: De-duplicate exceptions -ℹ Suggested fix +ℹ Fix 46 46 | 47 47 | try: 48 48 | pass diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs index f731033924..93921bfc32 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs @@ -56,8 +56,7 @@ pub(crate) fn shebang_whitespace( TextRange::at(range.start(), *n_spaces), ); if autofix { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(TextRange::at( + diagnostic.set_fix(Fix::automatic(Edit::range_deletion(TextRange::at( range.start(), *n_spaces, )))); diff --git a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE004_1.py.snap b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE004_1.py.snap index 3a7d2f6f30..1cda8e5af0 100644 --- a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE004_1.py.snap +++ b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE004_1.py.snap @@ -8,7 +8,7 @@ EXE004_1.py:1:1: EXE004 [*] Avoid whitespace before shebang | = help: Remove whitespace before shebang -ℹ Suggested fix +ℹ Fix 1 |- #!/usr/bin/python 1 |+#!/usr/bin/python diff --git a/crates/ruff/src/rules/flake8_pyi/rules/duplicate_union_member.rs b/crates/ruff/src/rules/flake8_pyi/rules/duplicate_union_member.rs index cdf2987c19..121333ddc5 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/duplicate_union_member.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/duplicate_union_member.rs @@ -77,8 +77,7 @@ fn traverse_union<'a>( }; // Replace the parent with its non-duplicate child. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( checker .generator() .expr(if expr == left.as_ref() { right } else { left }), diff --git a/crates/ruff/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs index 6041af59d2..49e624735a 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs @@ -24,8 +24,7 @@ impl AlwaysAutofixableViolation for QuotedAnnotationInStub { pub(crate) fn quoted_annotation_in_stub(checker: &mut Checker, annotation: &str, range: TextRange) { let mut diagnostic = Diagnostic::new(QuotedAnnotationInStub, range); if checker.patch(Rule::QuotedAnnotationInStub) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( annotation.to_string(), range, ))); diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap index 031cd8143b..102e686c02 100644 --- a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap @@ -11,7 +11,7 @@ PYI016.pyi:5:15: PYI016 [*] Duplicate union member `str` | = help: Remove duplicate union member `str` -ℹ Suggested fix +ℹ Fix 2 2 | field1: str 3 3 | 4 4 | # Should emit for duplicate field types. @@ -30,7 +30,7 @@ PYI016.pyi:8:23: PYI016 [*] Duplicate union member `int` | = help: Remove duplicate union member `int` -ℹ Suggested fix +ℹ Fix 5 5 | field2: str | str # PYI016: Duplicate union member `str` 6 6 | 7 7 | # Should emit for union types in arguments. @@ -49,7 +49,7 @@ PYI016.pyi:12:22: PYI016 [*] Duplicate union member `str` | = help: Remove duplicate union member `str` -ℹ Suggested fix +ℹ Fix 9 9 | print(arg1) 10 10 | 11 11 | # Should emit for unions in return types. @@ -69,7 +69,7 @@ PYI016.pyi:16:15: PYI016 [*] Duplicate union member `str` | = help: Remove duplicate union member `str` -ℹ Suggested fix +ℹ Fix 13 13 | return "my string" 14 14 | 15 15 | # Should emit in longer unions, even if not directly adjacent. @@ -90,7 +90,7 @@ PYI016.pyi:17:15: PYI016 [*] Duplicate union member `int` | = help: Remove duplicate union member `int` -ℹ Suggested fix +ℹ Fix 14 14 | 15 15 | # Should emit in longer unions, even if not directly adjacent. 16 16 | field3: str | str | int # PYI016: Duplicate union member `str` @@ -110,7 +110,7 @@ PYI016.pyi:18:21: PYI016 [*] Duplicate union member `str` | = help: Remove duplicate union member `str` -ℹ Suggested fix +ℹ Fix 15 15 | # Should emit in longer unions, even if not directly adjacent. 16 16 | field3: str | str | int # PYI016: Duplicate union member `str` 17 17 | field4: int | int | str # PYI016: Duplicate union member `int` @@ -131,7 +131,7 @@ PYI016.pyi:19:28: PYI016 [*] Duplicate union member `int` | = help: Remove duplicate union member `int` -ℹ Suggested fix +ℹ Fix 16 16 | field3: str | str | int # PYI016: Duplicate union member `str` 17 17 | field4: int | int | str # PYI016: Duplicate union member `int` 18 18 | field5: str | int | str # PYI016: Duplicate union member `str` @@ -151,7 +151,7 @@ PYI016.pyi:25:22: PYI016 [*] Duplicate union member `int` | = help: Remove duplicate union member `int` -ℹ Suggested fix +ℹ Fix 22 22 | field7 = str | str 23 23 | 24 24 | # Should emit for strangely-bracketed unions. @@ -170,7 +170,7 @@ PYI016.pyi:28:16: PYI016 [*] Duplicate union member `int` | = help: Remove duplicate union member `int` -ℹ Suggested fix +ℹ Fix 25 25 | field8: int | (str | int) # PYI016: Duplicate union member `int` 26 26 | 27 27 | # Should handle user brackets when fixing. @@ -191,7 +191,7 @@ PYI016.pyi:29:24: PYI016 [*] Duplicate union member `str` | = help: Remove duplicate union member `str` -ℹ Suggested fix +ℹ Fix 26 26 | 27 27 | # Should handle user brackets when fixing. 28 28 | field9: int | (int | str) # PYI016: Duplicate union member `int` @@ -209,7 +209,7 @@ PYI016.pyi:32:21: PYI016 [*] Duplicate union member `int` | = help: Remove duplicate union member `int` -ℹ Suggested fix +ℹ Fix 29 29 | field10: (str | int) | str # PYI016: Duplicate union member `str` 30 30 | 31 31 | # Should emit for nested unions. diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI020_PYI020.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI020_PYI020.pyi.snap index 6231707c54..09bd4a54d8 100644 --- a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI020_PYI020.pyi.snap +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI020_PYI020.pyi.snap @@ -12,7 +12,7 @@ PYI020.pyi:7:10: PYI020 [*] Quoted annotations should not be included in stubs | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 4 4 | 5 5 | import typing_extensions 6 6 | @@ -31,7 +31,7 @@ PYI020.pyi:8:15: PYI020 [*] Quoted annotations should not be included in stubs | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 5 5 | import typing_extensions 6 6 | 7 7 | def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs @@ -52,7 +52,7 @@ PYI020.pyi:9:26: PYI020 [*] Quoted annotations should not be included in stubs | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 6 6 | 7 7 | def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs 8 8 | def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs @@ -72,7 +72,7 @@ PYI020.pyi:13:12: PYI020 [*] Quoted annotations should not be included in stubs | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 10 10 | 11 11 | def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ... 12 12 | @@ -92,7 +92,7 @@ PYI020.pyi:14:25: PYI020 [*] Quoted annotations should not be included in stubs | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 11 11 | def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ... 12 12 | 13 13 | def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs @@ -112,7 +112,7 @@ PYI020.pyi:16:18: PYI020 [*] Quoted annotations should not be included in stubs | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 13 13 | def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs 14 14 | Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs 15 15 | @@ -132,7 +132,7 @@ PYI020.pyi:20:8: PYI020 [*] Quoted annotations should not be included in stubs | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 17 17 | """Documented and guaranteed useful.""" # Y021 Docstrings should not be included in stubs 18 18 | 19 19 | if sys.platform == "linux": @@ -153,7 +153,7 @@ PYI020.pyi:22:8: PYI020 [*] Quoted annotations should not be included in stubs | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 19 19 | if sys.platform == "linux": 20 20 | f: "int" # Y020 Quoted annotations should never be used in stubs 21 21 | elif sys.platform == "win32": @@ -174,7 +174,7 @@ PYI020.pyi:24:8: PYI020 [*] Quoted annotations should not be included in stubs | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 21 21 | elif sys.platform == "win32": 22 22 | f: "str" # Y020 Quoted annotations should never be used in stubs 23 23 | else: diff --git a/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs b/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs index a37c60da20..ddbcec4a35 100644 --- a/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs +++ b/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs @@ -35,8 +35,7 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr: .expect("Expected call to include parentheses"); let mut diagnostic = Diagnostic::new(UnnecessaryParenOnRaiseException, range); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::deletion(func.end(), range.end()))); + diagnostic.set_fix(Fix::automatic(Edit::deletion(func.end(), range.end()))); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/flake8_raise/snapshots/ruff__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap b/crates/ruff/src/rules/flake8_raise/snapshots/ruff__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap index 1982ba25a2..9d84196033 100644 --- a/crates/ruff/src/rules/flake8_raise/snapshots/ruff__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap +++ b/crates/ruff/src/rules/flake8_raise/snapshots/ruff__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap @@ -12,7 +12,7 @@ RSE102.py:5:21: RSE102 [*] Unnecessary parentheses on raised exception | = help: Remove unnecessary parentheses -ℹ Suggested fix +ℹ Fix 2 2 | y = 6 + "7" 3 3 | except TypeError: 4 4 | # RSE102 @@ -32,7 +32,7 @@ RSE102.py:13:16: RSE102 [*] Unnecessary parentheses on raised exception | = help: Remove unnecessary parentheses -ℹ Suggested fix +ℹ Fix 10 10 | raise 11 11 | 12 12 | # RSE102 @@ -52,7 +52,7 @@ RSE102.py:16:17: RSE102 [*] Unnecessary parentheses on raised exception | = help: Remove unnecessary parentheses -ℹ Suggested fix +ℹ Fix 13 13 | raise TypeError() 14 14 | 15 15 | # RSE102 @@ -73,7 +73,7 @@ RSE102.py:20:5: RSE102 [*] Unnecessary parentheses on raised exception | = help: Remove unnecessary parentheses -ℹ Suggested fix +ℹ Fix 16 16 | raise TypeError () 17 17 | 18 18 | # RSE102 @@ -97,7 +97,7 @@ RSE102.py:23:16: RSE102 [*] Unnecessary parentheses on raised exception | = help: Remove unnecessary parentheses -ℹ Suggested fix +ℹ Fix 20 20 | () 21 21 | 22 22 | # RSE102 @@ -122,7 +122,7 @@ RSE102.py:28:16: RSE102 [*] Unnecessary parentheses on raised exception | = help: Remove unnecessary parentheses -ℹ Suggested fix +ℹ Fix 25 25 | ) 26 26 | 27 27 | # RSE102 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 fd70c36afb..20678787c2 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs @@ -102,8 +102,7 @@ pub(crate) fn convert_for_loop_to_any_all( TextRange::new(stmt.start(), loop_info.terminal), ); if checker.patch(diagnostic.kind.rule()) && checker.semantic().is_builtin("any") { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::replacement( + diagnostic.set_fix(Fix::suggested(Edit::replacement( contents, stmt.start(), loop_info.terminal, @@ -193,8 +192,7 @@ pub(crate) fn convert_for_loop_to_any_all( TextRange::new(stmt.start(), loop_info.terminal), ); if checker.patch(diagnostic.kind.rule()) && checker.semantic().is_builtin("all") { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::replacement( + diagnostic.set_fix(Fix::suggested(Edit::replacement( contents, stmt.start(), loop_info.terminal, diff --git a/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs b/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs index cc488b5276..f3fb4c8ce1 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs @@ -182,8 +182,7 @@ pub(crate) fn yoda_conditions( expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( suggestion, expr.range(), ))); diff --git a/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM300_SIM300.py.snap b/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM300_SIM300.py.snap index 1b199b678d..38307e22c2 100644 --- a/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM300_SIM300.py.snap +++ b/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM300_SIM300.py.snap @@ -11,7 +11,7 @@ SIM300.py:2:1: SIM300 [*] Yoda conditions are discouraged, use `compare == "yoda | = help: Replace Yoda condition with `compare == "yoda"` -ℹ Suggested fix +ℹ Fix 1 1 | # Errors 2 |-"yoda" == compare # SIM300 2 |+compare == "yoda" # SIM300 @@ -30,7 +30,7 @@ SIM300.py:3:1: SIM300 [*] Yoda conditions are discouraged, use `compare == "yoda | = help: Replace Yoda condition with `compare == "yoda"` -ℹ Suggested fix +ℹ Fix 1 1 | # Errors 2 2 | "yoda" == compare # SIM300 3 |-"yoda" == compare # SIM300 @@ -50,7 +50,7 @@ SIM300.py:4:1: SIM300 [*] Yoda conditions are discouraged, use `age == 42` inste | = help: Replace Yoda condition with `age == 42` -ℹ Suggested fix +ℹ Fix 1 1 | # Errors 2 2 | "yoda" == compare # SIM300 3 3 | "yoda" == compare # SIM300 @@ -71,7 +71,7 @@ SIM300.py:5:1: SIM300 [*] Yoda conditions are discouraged, use `compare == ("a", | = help: Replace Yoda condition with `compare == ("a", "b")` -ℹ Suggested fix +ℹ Fix 2 2 | "yoda" == compare # SIM300 3 3 | "yoda" == compare # SIM300 4 4 | 42 == age # SIM300 @@ -92,7 +92,7 @@ SIM300.py:6:1: SIM300 [*] Yoda conditions are discouraged, use `compare >= "yoda | = help: Replace Yoda condition with `compare >= "yoda"` -ℹ Suggested fix +ℹ Fix 3 3 | "yoda" == compare # SIM300 4 4 | 42 == age # SIM300 5 5 | ("a", "b") == compare # SIM300 @@ -113,7 +113,7 @@ SIM300.py:7:1: SIM300 [*] Yoda conditions are discouraged, use `compare > "yoda" | = help: Replace Yoda condition with `compare > "yoda"` -ℹ Suggested fix +ℹ Fix 4 4 | 42 == age # SIM300 5 5 | ("a", "b") == compare # SIM300 6 6 | "yoda" <= compare # SIM300 @@ -134,7 +134,7 @@ SIM300.py:8:1: SIM300 [*] Yoda conditions are discouraged, use `age < 42` instea | = help: Replace Yoda condition with `age < 42` -ℹ Suggested fix +ℹ Fix 5 5 | ("a", "b") == compare # SIM300 6 6 | "yoda" <= compare # SIM300 7 7 | "yoda" < compare # SIM300 @@ -155,7 +155,7 @@ SIM300.py:9:1: SIM300 [*] Yoda conditions are discouraged, use `age < -42` inste | = help: Replace Yoda condition with `age < -42` -ℹ Suggested fix +ℹ Fix 6 6 | "yoda" <= compare # SIM300 7 7 | "yoda" < compare # SIM300 8 8 | 42 > age # SIM300 @@ -176,7 +176,7 @@ SIM300.py:10:1: SIM300 [*] Yoda conditions are discouraged, use `age < +42` inst | = help: Replace Yoda condition with `age < +42` -ℹ Suggested fix +ℹ Fix 7 7 | "yoda" < compare # SIM300 8 8 | 42 > age # SIM300 9 9 | -42 > age # SIM300 @@ -197,7 +197,7 @@ SIM300.py:11:1: SIM300 [*] Yoda conditions are discouraged, use `age == YODA` in | = help: Replace Yoda condition with `age == YODA` -ℹ Suggested fix +ℹ Fix 8 8 | 42 > age # SIM300 9 9 | -42 > age # SIM300 10 10 | +42 > age # SIM300 @@ -218,7 +218,7 @@ SIM300.py:12:1: SIM300 [*] Yoda conditions are discouraged, use `age < YODA` ins | = help: Replace Yoda condition with `age < YODA` -ℹ Suggested fix +ℹ Fix 9 9 | -42 > age # SIM300 10 10 | +42 > age # SIM300 11 11 | YODA == age # SIM300 @@ -239,7 +239,7 @@ SIM300.py:13:1: SIM300 [*] Yoda conditions are discouraged, use `age <= YODA` in | = help: Replace Yoda condition with `age <= YODA` -ℹ Suggested fix +ℹ Fix 10 10 | +42 > age # SIM300 11 11 | YODA == age # SIM300 12 12 | YODA > age # SIM300 @@ -260,7 +260,7 @@ SIM300.py:14:1: SIM300 [*] Yoda conditions are discouraged, use `age == JediOrde | = help: Replace Yoda condition with `age == JediOrder.YODA` -ℹ Suggested fix +ℹ Fix 11 11 | YODA == age # SIM300 12 12 | YODA > age # SIM300 13 13 | YODA >= age # SIM300 @@ -280,7 +280,7 @@ SIM300.py:15:1: SIM300 [*] Yoda conditions are discouraged, use `(number - 100) | = help: Replace Yoda condition with `(number - 100) > 0` -ℹ Suggested fix +ℹ Fix 12 12 | YODA > age # SIM300 13 13 | YODA >= age # SIM300 14 14 | JediOrder.YODA == age # SIM300 @@ -301,7 +301,7 @@ SIM300.py:16:1: SIM300 [*] Yoda conditions are discouraged, use `(60 * 60) < Som | = help: Replace Yoda condition with `(60 * 60) < SomeClass().settings.SOME_CONSTANT_VALUE` -ℹ Suggested fix +ℹ Fix 13 13 | YODA >= age # SIM300 14 14 | JediOrder.YODA == age # SIM300 15 15 | 0 < (number - 100) # SIM300 diff --git a/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs b/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs index 7889332d44..c71757dbdd 100644 --- a/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs +++ b/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs @@ -72,8 +72,7 @@ pub(crate) fn deprecated_type_alias(checker: &mut Checker, expr: &Expr) { expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( match type_name { "unicode" => "str", "long" => "int", diff --git a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs index 99ffe1f766..30968ecfab 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs @@ -103,8 +103,7 @@ pub(crate) fn lambda_assignment( indented.push_str(&line); } } - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( indented, stmt.range(), ))); diff --git a/crates/ruff/src/rules/pycodestyle/rules/literal_comparisons.rs b/crates/ruff/src/rules/pycodestyle/rules/literal_comparisons.rs index adf7933c32..b3cfcddc30 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/literal_comparisons.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/literal_comparisons.rs @@ -295,8 +295,7 @@ pub(crate) fn literal_comparisons( .collect::>(); let content = compare(left, &ops, comparators, checker.generator()); for diagnostic in &mut diagnostics { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( content.to_string(), expr.range(), ))); diff --git a/crates/ruff/src/rules/pycodestyle/rules/not_tests.rs b/crates/ruff/src/rules/pycodestyle/rules/not_tests.rs index 3c975789b8..caa76365bf 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/not_tests.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/not_tests.rs @@ -99,8 +99,7 @@ pub(crate) fn not_tests( if check_not_in { let mut diagnostic = Diagnostic::new(NotInTest, operand.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( compare( left, &[Cmpop::NotIn], @@ -117,8 +116,7 @@ pub(crate) fn not_tests( if check_not_is { let mut diagnostic = Diagnostic::new(NotIsTest, operand.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( compare( left, &[Cmpop::IsNot], diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E713_E713.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E713_E713.py.snap index 5a4152e320..a77c7313fc 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E713_E713.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E713_E713.py.snap @@ -11,7 +11,7 @@ E713.py:2:8: E713 [*] Test for membership should be `not in` | = help: Convert to `not in` -ℹ Suggested fix +ℹ Fix 1 1 | #: E713 2 |-if not X in Y: 2 |+if X not in Y: @@ -30,7 +30,7 @@ E713.py:5:8: E713 [*] Test for membership should be `not in` | = help: Convert to `not in` -ℹ Suggested fix +ℹ Fix 2 2 | if not X in Y: 3 3 | pass 4 4 | #: E713 @@ -51,7 +51,7 @@ E713.py:8:8: E713 [*] Test for membership should be `not in` | = help: Convert to `not in` -ℹ Suggested fix +ℹ Fix 5 5 | if not X.B in Y: 6 6 | pass 7 7 | #: E713 @@ -72,7 +72,7 @@ E713.py:11:23: E713 [*] Test for membership should be `not in` | = help: Convert to `not in` -ℹ Suggested fix +ℹ Fix 8 8 | if not X in Y and Z == "zero": 9 9 | pass 10 10 | #: E713 @@ -92,7 +92,7 @@ E713.py:14:9: E713 [*] Test for membership should be `not in` | = help: Convert to `not in` -ℹ Suggested fix +ℹ Fix 11 11 | if X == "zero" or not Y in Z: 12 12 | pass 13 13 | #: E713 diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E714_E714.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E714_E714.py.snap index 444d001733..8286975b5e 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E714_E714.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E714_E714.py.snap @@ -11,7 +11,7 @@ E714.py:2:8: E714 [*] Test for object identity should be `is not` | = help: Convert to `is not` -ℹ Suggested fix +ℹ Fix 1 1 | #: E714 2 |-if not X is Y: 2 |+if X is not Y: @@ -29,7 +29,7 @@ E714.py:5:8: E714 [*] Test for object identity should be `is not` | = help: Convert to `is not` -ℹ Suggested fix +ℹ Fix 2 2 | if not X is Y: 3 3 | pass 4 4 | #: E714 diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__constant_literals.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__constant_literals.snap index c085685d06..b6ae2e3549 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__constant_literals.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__constant_literals.snap @@ -12,7 +12,7 @@ constant_literals.py:4:4: F632 [*] Use `==` to compare constant literals | = help: Replace `is` with `==` -ℹ Suggested fix +ℹ Fix 1 1 | ### 2 2 | # Errors 3 3 | ### @@ -33,7 +33,7 @@ constant_literals.py:6:4: F632 [*] Use `==` to compare constant literals | = help: Replace `is` with `==` -ℹ Suggested fix +ℹ Fix 3 3 | ### 4 4 | if "abc" is "def": # F632 (fix) 5 5 | pass @@ -54,7 +54,7 @@ constant_literals.py:8:4: F632 [*] Use `==` to compare constant literals | = help: Replace `is` with `==` -ℹ Suggested fix +ℹ Fix 5 5 | pass 6 6 | if "abc" is None: # F632 (fix, but leaves behind unfixable E711) 7 7 | pass @@ -75,7 +75,7 @@ constant_literals.py:10:4: F632 [*] Use `==` to compare constant literals | = help: Replace `is` with `==` -ℹ Suggested fix +ℹ Fix 7 7 | pass 8 8 | if None is "abc": # F632 (fix, but leaves behind unfixable E711) 9 9 | pass @@ -96,7 +96,7 @@ constant_literals.py:12:4: F632 [*] Use `==` to compare constant literals | = help: Replace `is` with `==` -ℹ Suggested fix +ℹ Fix 9 9 | pass 10 10 | if "abc" is False: # F632 (fix, but leaves behind unfixable E712) 11 11 | pass diff --git a/crates/ruff/src/rules/pyflakes/rules/invalid_literal_comparisons.rs b/crates/ruff/src/rules/pyflakes/rules/invalid_literal_comparisons.rs index 1b50e144cf..42cfc34b9a 100644 --- a/crates/ruff/src/rules/pyflakes/rules/invalid_literal_comparisons.rs +++ b/crates/ruff/src/rules/pyflakes/rules/invalid_literal_comparisons.rs @@ -96,8 +96,7 @@ pub(crate) fn invalid_literal_comparison( None } } { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( content, located_op.range + expr.start(), ))); diff --git a/crates/ruff/src/rules/pyflakes/rules/raise_not_implemented.rs b/crates/ruff/src/rules/pyflakes/rules/raise_not_implemented.rs index f91e08f0bf..0bbdfffb2b 100644 --- a/crates/ruff/src/rules/pyflakes/rules/raise_not_implemented.rs +++ b/crates/ruff/src/rules/pyflakes/rules/raise_not_implemented.rs @@ -1,6 +1,6 @@ use rustpython_parser::ast::{self, Expr, Ranged}; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; @@ -37,14 +37,16 @@ use crate::registry::AsRule; #[violation] pub struct RaiseNotImplemented; -impl AlwaysAutofixableViolation for RaiseNotImplemented { +impl Violation for RaiseNotImplemented { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { format!("`raise NotImplemented` should be `raise NotImplementedError`") } - fn autofix_title(&self) -> String { - "Use `raise NotImplementedError`".to_string() + fn autofix_title(&self) -> Option { + Some("Use `raise NotImplementedError`".to_string()) } } @@ -74,11 +76,12 @@ pub(crate) fn raise_not_implemented(checker: &mut Checker, expr: &Expr) { }; let mut diagnostic = Diagnostic::new(RaiseNotImplemented, expr.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - "NotImplementedError".to_string(), - expr.range(), - ))); + if checker.semantic().is_builtin("NotImplementedError") { + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( + "NotImplementedError".to_string(), + expr.range(), + ))); + } } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyflakes/rules/repeated_keys.rs b/crates/ruff/src/rules/pyflakes/rules/repeated_keys.rs index 99393cbddd..13c4ceaf9a 100644 --- a/crates/ruff/src/rules/pyflakes/rules/repeated_keys.rs +++ b/crates/ruff/src/rules/pyflakes/rules/repeated_keys.rs @@ -168,8 +168,7 @@ pub(crate) fn repeated_keys(checker: &mut Checker, keys: &[Option], values ); if is_duplicate_value { if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::deletion( + diagnostic.set_fix(Fix::suggested(Edit::deletion( values[i - 1].end(), values[i].end(), ))); @@ -193,8 +192,7 @@ pub(crate) fn repeated_keys(checker: &mut Checker, keys: &[Option], values ); if is_duplicate_value { if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::deletion( + diagnostic.set_fix(Fix::suggested(Edit::deletion( values[i - 1].end(), values[i].end(), ))); diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F632_F632.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F632_F632.py.snap index e84c43f460..fe6785eab5 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F632_F632.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F632_F632.py.snap @@ -9,7 +9,7 @@ F632.py:1:4: F632 [*] Use `==` to compare constant literals | = help: Replace `is` with `==` -ℹ Suggested fix +ℹ Fix 1 |-if x is "abc": 1 |+if x == "abc": 2 2 | pass @@ -26,7 +26,7 @@ F632.py:4:4: F632 [*] Use `!=` to compare constant literals | = help: Replace `is not` with `!=` -ℹ Suggested fix +ℹ Fix 1 1 | if x is "abc": 2 2 | pass 3 3 | @@ -48,7 +48,7 @@ F632.py:7:4: F632 [*] Use `!=` to compare constant literals | = help: Replace `is not` with `!=` -ℹ Suggested fix +ℹ Fix 4 4 | if 123 is not y: 5 5 | pass 6 6 | @@ -69,7 +69,7 @@ F632.py:11:4: F632 [*] Use `==` to compare constant literals | = help: Replace `is` with `==` -ℹ Suggested fix +ℹ Fix 8 8 | not y: 9 9 | pass 10 10 | @@ -89,7 +89,7 @@ F632.py:14:4: F632 [*] Use `==` to compare constant literals | = help: Replace `is` with `==` -ℹ Suggested fix +ℹ Fix 11 11 | if "123" is x < 3: 12 12 | pass 13 13 | @@ -109,7 +109,7 @@ F632.py:17:4: F632 [*] Use `==` to compare constant literals | = help: Replace `is` with `==` -ℹ Suggested fix +ℹ Fix 14 14 | if "123" != x is 3: 15 15 | pass 16 16 | @@ -129,7 +129,7 @@ F632.py:20:14: F632 [*] Use `==` to compare constant literals | = help: Replace `is` with `==` -ℹ Suggested fix +ℹ Fix 17 17 | if ("123" != x) is 3: 18 18 | pass 19 19 | @@ -152,7 +152,7 @@ F632.py:23:2: F632 [*] Use `!=` to compare constant literals | = help: Replace `is not` with `!=` -ℹ Suggested fix +ℹ Fix 20 20 | if "123" != (x is 3): 21 21 | pass 22 22 | @@ -174,7 +174,7 @@ F632.py:26:2: F632 [*] Use `!=` to compare constant literals | = help: Replace `is not` with `!=` -ℹ Suggested fix +ℹ Fix 23 23 | {2 is 24 24 | not ''} 25 25 | diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F901_F901.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F901_F901.py.snap index d9b24f9ee1..a5cac50d13 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F901_F901.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F901_F901.py.snap @@ -9,7 +9,7 @@ F901.py:2:11: F901 [*] `raise NotImplemented` should be `raise NotImplementedErr | = help: Use `raise NotImplementedError` -ℹ Suggested fix +ℹ Fix 1 1 | def f() -> None: 2 |- raise NotImplemented() 2 |+ raise NotImplementedError() @@ -25,7 +25,7 @@ F901.py:6:11: F901 [*] `raise NotImplemented` should be `raise NotImplementedErr | = help: Use `raise NotImplementedError` -ℹ Suggested fix +ℹ Fix 3 3 | 4 4 | 5 5 | def g() -> None: diff --git a/crates/ruff/src/rules/pylint/rules/manual_import_from.rs b/crates/ruff/src/rules/pylint/rules/manual_import_from.rs index 7ca43129b9..bac9cf746b 100644 --- a/crates/ruff/src/rules/pylint/rules/manual_import_from.rs +++ b/crates/ruff/src/rules/pylint/rules/manual_import_from.rs @@ -83,8 +83,7 @@ pub(crate) fn manual_from_import( level: Some(Int::new(0)), range: TextRange::default(), }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( checker.generator().stmt(&node.into()), stmt.range(), ))); diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLR0402_import_aliasing.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLR0402_import_aliasing.py.snap index d3b7da5cc1..2a7894af14 100644 --- a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLR0402_import_aliasing.py.snap +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLR0402_import_aliasing.py.snap @@ -12,7 +12,7 @@ import_aliasing.py:9:8: PLR0402 [*] Use `from os import path` in lieu of alias | = help: Replace with `from os import path` -ℹ Suggested fix +ℹ Fix 6 6 | import collections as collections # [useless-import-alias] 7 7 | from collections import OrderedDict as OrderedDict # [useless-import-alias] 8 8 | from collections import OrderedDict as o_dict @@ -33,7 +33,7 @@ import_aliasing.py:11:8: PLR0402 [*] Use `from foo.bar import foobar` in lieu of | = help: Replace with `from foo.bar import foobar` -ℹ Suggested fix +ℹ Fix 8 8 | from collections import OrderedDict as o_dict 9 9 | import os.path as path # [consider-using-from-import] 10 10 | import os.path as p diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs index 19cc009f00..80bd2aecb6 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs @@ -99,8 +99,7 @@ pub(crate) fn deprecated_unittest_alias(checker: &mut Checker, expr: &Expr) { expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( format!("self.{target}"), expr.range(), ))); diff --git a/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs b/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs index 896556ed04..bacfc9c4c2 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs @@ -348,8 +348,7 @@ pub(crate) fn f_strings(checker: &mut Checker, summary: &FormatSummary, expr: &E let mut diagnostic = Diagnostic::new(FString, expr.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( contents, expr.range(), ))); diff --git a/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs index 5068d07348..0493a63e2a 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs @@ -88,11 +88,12 @@ fn atom_diagnostic(checker: &mut Checker, target: &Expr) { target.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - "OSError".to_string(), - target.range(), - ))); + if checker.semantic().is_builtin("OSError") { + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( + "OSError".to_string(), + target.range(), + ))); + } } checker.diagnostics.push(diagnostic); } @@ -101,49 +102,49 @@ fn atom_diagnostic(checker: &mut Checker, target: &Expr) { fn tuple_diagnostic(checker: &mut Checker, target: &Expr, aliases: &[&Expr]) { let mut diagnostic = Diagnostic::new(OSErrorAlias { name: None }, target.range()); if checker.patch(diagnostic.kind.rule()) { - let Expr::Tuple(ast::ExprTuple { elts, ..}) = target else { - panic!("Expected Expr::Tuple"); - }; - - // Filter out any `OSErrors` aliases. - let mut remaining: Vec = elts - .iter() - .filter_map(|elt| { - if aliases.contains(&elt) { - None - } else { - Some(elt.clone()) - } - }) - .collect(); - - // If `OSError` itself isn't already in the tuple, add it. - if elts.iter().all(|elt| !is_os_error(elt, checker.semantic())) { - let node = ast::ExprName { - id: "OSError".into(), - ctx: ExprContext::Load, - range: TextRange::default(), + if checker.semantic().is_builtin("OSError") { + let Expr::Tuple(ast::ExprTuple { elts, .. }) = target else { + panic!("Expected Expr::Tuple"); }; - remaining.insert(0, node.into()); - } - if remaining.len() == 1 { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - "OSError".to_string(), - target.range(), - ))); - } else { - let node = ast::ExprTuple { - elts: remaining, - ctx: ExprContext::Load, - range: TextRange::default(), - }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - format!("({})", checker.generator().expr(&node.into())), - target.range(), - ))); + // Filter out any `OSErrors` aliases. + let mut remaining: Vec = elts + .iter() + .filter_map(|elt| { + if aliases.contains(&elt) { + None + } else { + Some(elt.clone()) + } + }) + .collect(); + + // If `OSError` itself isn't already in the tuple, add it. + if elts.iter().all(|elt| !is_os_error(elt, checker.semantic())) { + let node = ast::ExprName { + id: "OSError".into(), + ctx: ExprContext::Load, + range: TextRange::default(), + }; + remaining.insert(0, node.into()); + } + + if remaining.len() == 1 { + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( + "OSError".to_string(), + target.range(), + ))); + } else { + let node = ast::ExprTuple { + elts: remaining, + ctx: ExprContext::Load, + range: TextRange::default(), + }; + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( + format!("({})", checker.generator().expr(&node.into())), + target.range(), + ))); + } } } checker.diagnostics.push(diagnostic); 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 ba35cd4d2a..626c8b5ba0 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs @@ -238,8 +238,7 @@ fn fix_py2_block( let end = orelse.last()?; if indentation(checker.locator, start).is_none() { // Inline `else` block (e.g., `else: x = 1`). - #[allow(deprecated)] - Some(Fix::unspecified(Edit::range_replacement( + Some(Fix::suggested(Edit::range_replacement( checker .locator .slice(TextRange::new(start.start(), end.end())) @@ -258,8 +257,7 @@ fn fix_py2_block( .ok() }) .map(|contents| { - #[allow(deprecated)] - Fix::unspecified(Edit::replacement( + Fix::suggested(Edit::replacement( contents, checker.locator.line_start(stmt.start()), stmt.end(), @@ -271,21 +269,13 @@ fn fix_py2_block( // If we have an `if` and an `elif`, turn the `elif` into an `if`. let start_location = leading_token.range.start(); let end_location = trailing_token.range.start() + TextSize::from(2); - #[allow(deprecated)] - Some(Fix::unspecified(Edit::deletion( - start_location, - end_location, - ))) + Some(Fix::suggested(Edit::deletion(start_location, end_location))) } (StartTok::Elif, _) => { // If we have an `elif`, delete up to the `else` or the end of the statement. let start_location = leading_token.range.start(); let end_location = trailing_token.range.start(); - #[allow(deprecated)] - Some(Fix::unspecified(Edit::deletion( - start_location, - end_location, - ))) + Some(Fix::suggested(Edit::deletion(start_location, end_location))) } } } @@ -306,8 +296,7 @@ fn fix_py3_block( let end = body.last()?; if indentation(checker.locator, start).is_none() { // Inline `if` block (e.g., `if ...: x = 1`). - #[allow(deprecated)] - Some(Fix::unspecified(Edit::range_replacement( + Some(Fix::suggested(Edit::range_replacement( checker .locator .slice(TextRange::new(start.start(), end.end())) @@ -326,8 +315,7 @@ fn fix_py3_block( .ok() }) .map(|contents| { - #[allow(deprecated)] - Fix::unspecified(Edit::replacement( + Fix::suggested(Edit::replacement( contents, checker.locator.line_start(stmt.start()), stmt.end(), @@ -340,8 +328,7 @@ fn fix_py3_block( // the rest. let end = body.last()?; let text = checker.locator.slice(TextRange::new(test.end(), end.end())); - #[allow(deprecated)] - Some(Fix::unspecified(Edit::range_replacement( + Some(Fix::suggested(Edit::range_replacement( format!("else{text}"), stmt.range(), ))) diff --git a/crates/ruff/src/rules/pyupgrade/rules/quoted_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/quoted_annotation.rs index 12988fecee..749e3011f5 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/quoted_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/quoted_annotation.rs @@ -54,8 +54,7 @@ impl AlwaysAutofixableViolation for QuotedAnnotation { pub(crate) fn quoted_annotation(checker: &mut Checker, annotation: &str, range: TextRange) { let mut diagnostic = Diagnostic::new(QuotedAnnotation, range); if checker.patch(Rule::QuotedAnnotation) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( annotation.to_string(), range, ))); diff --git a/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs b/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs index 3646b9ad86..60e6f431b4 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs @@ -138,14 +138,14 @@ fn create_check( ); if patch { if let Some(content) = replacement_value { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( content.to_string(), mode_param.range(), ))); } else { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| create_remove_param_fix(locator, expr, mode_param)); + diagnostic.try_set_fix(|| { + create_remove_param_fix(locator, expr, mode_param).map(Fix::automatic) + }); } } diagnostic diff --git a/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs b/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs index 2e444211d5..64b3e59aab 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs @@ -58,12 +58,13 @@ pub(crate) fn replace_universal_newlines(checker: &mut Checker, func: &Expr, kwa matches!(call_path.as_slice(), ["subprocess", "run"]) }) { - let Some(kwarg) = find_keyword(kwargs, "universal_newlines") else { return; }; + let Some(kwarg) = find_keyword(kwargs, "universal_newlines") else { + return; + }; let range = TextRange::at(kwarg.start(), "universal_newlines".text_len()); let mut diagnostic = Diagnostic::new(ReplaceUniversalNewlines, range); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( "text".to_string(), range, ))); 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 e432547cde..81e3f40daf 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs @@ -1,6 +1,6 @@ use rustpython_parser::ast::{self, Expr, Ranged}; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; @@ -34,16 +34,21 @@ pub struct TypeOfPrimitive { primitive: Primitive, } -impl AlwaysAutofixableViolation for TypeOfPrimitive { +impl Violation for TypeOfPrimitive { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { let TypeOfPrimitive { primitive } = self; format!("Use `{}` instead of `type(...)`", primitive.builtin()) } - fn autofix_title(&self) -> String { + fn autofix_title(&self) -> Option { let TypeOfPrimitive { primitive } = self; - format!("Replace `type(...)` with `{}`", primitive.builtin()) + Some(format!( + "Replace `type(...)` with `{}`", + primitive.builtin() + )) } } @@ -69,11 +74,13 @@ pub(crate) fn type_of_primitive(checker: &mut Checker, expr: &Expr, func: &Expr, }; let mut diagnostic = Diagnostic::new(TypeOfPrimitive { primitive }, expr.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - primitive.builtin(), - expr.range(), - ))); + let builtin = primitive.builtin(); + if checker.semantic().is_builtin(&builtin) { + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( + primitive.builtin(), + expr.range(), + ))); + } } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs index 195607e58e..3b2040bdb9 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs @@ -48,8 +48,7 @@ pub(crate) fn unnecessary_coding_comment(line: &Line, autofix: bool) -> Option Fix { } prev = range.end(); } - #[allow(deprecated)] - Fix::unspecified(Edit::range_replacement(replacement, expr.range())) + + Fix::automatic(Edit::range_replacement(replacement, expr.range())) } /// UP012 @@ -187,8 +187,7 @@ pub(crate) fn unnecessary_encode_utf8( expr.range(), ); if checker.patch(Rule::UnnecessaryEncodeUTF8) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { + diagnostic.try_set_fix(|| { remove_argument( checker.locator, func.start(), @@ -197,6 +196,7 @@ pub(crate) fn unnecessary_encode_utf8( kwargs, false, ) + .map(Fix::automatic) }); } checker.diagnostics.push(diagnostic); @@ -209,8 +209,7 @@ pub(crate) fn unnecessary_encode_utf8( expr.range(), ); if checker.patch(Rule::UnnecessaryEncodeUTF8) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { + diagnostic.try_set_fix(|| { remove_argument( checker.locator, func.start(), @@ -219,6 +218,7 @@ pub(crate) fn unnecessary_encode_utf8( kwargs, false, ) + .map(Fix::automatic) }); } checker.diagnostics.push(diagnostic); @@ -238,8 +238,7 @@ pub(crate) fn unnecessary_encode_utf8( expr.range(), ); if checker.patch(Rule::UnnecessaryEncodeUTF8) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { + diagnostic.try_set_fix(|| { remove_argument( checker.locator, func.start(), @@ -248,6 +247,7 @@ pub(crate) fn unnecessary_encode_utf8( kwargs, false, ) + .map(Fix::automatic) }); } checker.diagnostics.push(diagnostic); @@ -260,8 +260,7 @@ pub(crate) fn unnecessary_encode_utf8( expr.range(), ); if checker.patch(Rule::UnnecessaryEncodeUTF8) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { + diagnostic.try_set_fix(|| { remove_argument( checker.locator, func.start(), @@ -270,6 +269,7 @@ pub(crate) fn unnecessary_encode_utf8( kwargs, false, ) + .map(Fix::automatic) }); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pyupgrade/rules/yield_in_for_loop.rs b/crates/ruff/src/rules/pyupgrade/rules/yield_in_for_loop.rs index 6533a26278..e41becee05 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/yield_in_for_loop.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/yield_in_for_loop.rs @@ -190,8 +190,7 @@ pub(crate) fn yield_in_for_loop(checker: &mut Checker, stmt: &Stmt) { if checker.patch(diagnostic.kind.rule()) { let contents = checker.locator.slice(item.iter.range()); let contents = format!("yield from {contents}"); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( contents, item.stmt.range(), ))); diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP003.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP003.py.snap index 5d7d50e18e..c461f5d92c 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP003.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP003.py.snap @@ -10,7 +10,7 @@ UP003.py:1:1: UP003 [*] Use `str` instead of `type(...)` | = help: Replace `type(...)` with `str` -ℹ Suggested fix +ℹ Fix 1 |-type("") 1 |+str 2 2 | type(b"") @@ -27,7 +27,7 @@ UP003.py:2:1: UP003 [*] Use `bytes` instead of `type(...)` | = help: Replace `type(...)` with `bytes` -ℹ Suggested fix +ℹ Fix 1 1 | type("") 2 |-type(b"") 2 |+bytes @@ -46,7 +46,7 @@ UP003.py:3:1: UP003 [*] Use `int` instead of `type(...)` | = help: Replace `type(...)` with `int` -ℹ Suggested fix +ℹ Fix 1 1 | type("") 2 2 | type(b"") 3 |-type(0) @@ -65,7 +65,7 @@ UP003.py:4:1: UP003 [*] Use `float` instead of `type(...)` | = help: Replace `type(...)` with `float` -ℹ Suggested fix +ℹ Fix 1 1 | type("") 2 2 | type(b"") 3 3 | type(0) @@ -86,7 +86,7 @@ UP003.py:5:1: UP003 [*] Use `complex` instead of `type(...)` | = help: Replace `type(...)` with `complex` -ℹ Suggested fix +ℹ Fix 2 2 | type(b"") 3 3 | type(0) 4 4 | type(0.0) diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP009_0.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP009_0.py.snap index 42e1dbd19f..871f3d8580 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP009_0.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP009_0.py.snap @@ -10,7 +10,7 @@ UP009_0.py:1:1: UP009 [*] UTF-8 encoding declaration is unnecessary | = help: Remove unnecessary coding comment -ℹ Suggested fix +ℹ Fix 1 |-# coding=utf8 2 1 | 3 2 | print("Hello world") diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP009_1.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP009_1.py.snap index 9447bf10cc..3019e95616 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP009_1.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP009_1.py.snap @@ -11,7 +11,7 @@ UP009_1.py:2:1: UP009 [*] UTF-8 encoding declaration is unnecessary | = help: Remove unnecessary coding comment -ℹ Suggested fix +ℹ Fix 1 1 | #!/usr/bin/python 2 |-# -*- coding: utf-8 -*- 3 2 | 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 b083c6c0ca..98d0ed5f7a 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 @@ -11,7 +11,7 @@ UP012.py:2:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 1 1 | # ASCII literals should be replaced by a bytes literal 2 |-"foo".encode("utf-8") # b"foo" 2 |+b"foo" # b"foo" @@ -30,7 +30,7 @@ UP012.py:3:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 1 1 | # ASCII literals should be replaced by a bytes literal 2 2 | "foo".encode("utf-8") # b"foo" 3 |-"foo".encode("u8") # b"foo" @@ -50,7 +50,7 @@ UP012.py:4:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 1 1 | # ASCII literals should be replaced by a bytes literal 2 2 | "foo".encode("utf-8") # b"foo" 3 3 | "foo".encode("u8") # b"foo" @@ -71,7 +71,7 @@ UP012.py:5:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 2 2 | "foo".encode("utf-8") # b"foo" 3 3 | "foo".encode("u8") # b"foo" 4 4 | "foo".encode() # b"foo" @@ -92,7 +92,7 @@ UP012.py:6:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 3 3 | "foo".encode("u8") # b"foo" 4 4 | "foo".encode() # b"foo" 5 5 | "foo".encode("UTF8") # b"foo" @@ -113,7 +113,7 @@ UP012.py:7:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 4 4 | "foo".encode() # b"foo" 5 5 | "foo".encode("UTF8") # b"foo" 6 6 | U"foo".encode("utf-8") # b"foo" @@ -140,7 +140,7 @@ UP012.py:8:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 5 5 | "foo".encode("UTF8") # b"foo" 6 6 | U"foo".encode("utf-8") # b"foo" 7 7 | "foo".encode(encoding="utf-8") # b"foo" @@ -170,7 +170,7 @@ UP012.py:16:5: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 13 13 | "utf-8" 14 14 | ) 15 15 | ( @@ -195,7 +195,7 @@ UP012.py:20:5: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 17 17 | "Ipsum".encode() 18 18 | ) 19 19 | ( @@ -217,7 +217,7 @@ UP012.py:24:5: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 21 21 | "Ipsum".encode() # Comment 22 22 | ) 23 23 | ( @@ -237,7 +237,7 @@ UP012.py:32:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Remove unnecessary encoding argument -ℹ Suggested fix +ℹ Fix 29 29 | string.encode("utf-8") 30 30 | 31 31 | bar = "bar" @@ -260,7 +260,7 @@ UP012.py:36:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Remove unnecessary encoding argument -ℹ Suggested fix +ℹ Fix 33 33 | encoding = "latin" 34 34 | "foo".encode(encoding) 35 35 | f"foo{bar}".encode(encoding) @@ -282,7 +282,7 @@ UP012.py:53:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Remove unnecessary encoding argument -ℹ Suggested fix +ℹ Fix 50 50 | "unicode text©".encode(encoding="utf-8", errors="replace") 51 51 | 52 52 | # Unicode literals should only be stripped of default encoding. @@ -303,7 +303,7 @@ UP012.py:55:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Remove unnecessary encoding argument -ℹ Suggested fix +ℹ Fix 52 52 | # Unicode literals should only be stripped of default encoding. 53 53 | "unicode text©".encode("utf-8") # "unicode text©".encode() 54 54 | "unicode text©".encode() @@ -324,7 +324,7 @@ UP012.py:57:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 54 54 | "unicode text©".encode() 55 55 | "unicode text©".encode(encoding="UTF8") # "unicode text©".encode() 56 56 | @@ -344,7 +344,7 @@ UP012.py:58:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 55 55 | "unicode text©".encode(encoding="UTF8") # "unicode text©".encode() 56 56 | 57 57 | r"foo\o".encode("utf-8") # br"foo\o" @@ -365,7 +365,7 @@ UP012.py:59:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 56 56 | 57 57 | r"foo\o".encode("utf-8") # br"foo\o" 58 58 | u"foo".encode("utf-8") # b"foo" @@ -385,7 +385,7 @@ UP012.py:60:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 57 57 | r"foo\o".encode("utf-8") # br"foo\o" 58 58 | u"foo".encode("utf-8") # b"foo" 59 59 | R"foo\o".encode("utf-8") # br"foo\o" @@ -406,7 +406,7 @@ UP012.py:61:7: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 58 58 | u"foo".encode("utf-8") # b"foo" 59 59 | R"foo\o".encode("utf-8") # br"foo\o" 60 60 | U"foo".encode("utf-8") # b"foo" @@ -429,7 +429,7 @@ UP012.py:64:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 62 62 | 63 63 | # `encode` on parenthesized strings. 64 64 | ( @@ -455,7 +455,7 @@ UP012.py:69:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 67 67 | ).encode() 68 68 | 69 69 | (( diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP015.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP015.py.snap index e0e09a15a5..b227068992 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP015.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP015.py.snap @@ -10,7 +10,7 @@ UP015.py:1:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 1 |-open("foo", "U") 1 |+open("foo") 2 2 | open("foo", "Ur") @@ -27,7 +27,7 @@ UP015.py:2:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 1 1 | open("foo", "U") 2 |-open("foo", "Ur") 2 |+open("foo") @@ -46,7 +46,7 @@ UP015.py:3:1: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 1 1 | open("foo", "U") 2 2 | open("foo", "Ur") 3 |-open("foo", "Ub") @@ -66,7 +66,7 @@ UP015.py:4:1: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 1 1 | open("foo", "U") 2 2 | open("foo", "Ur") 3 3 | open("foo", "Ub") @@ -87,7 +87,7 @@ UP015.py:5:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 2 2 | open("foo", "Ur") 3 3 | open("foo", "Ub") 4 4 | open("foo", "rUb") @@ -108,7 +108,7 @@ UP015.py:6:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 3 3 | open("foo", "Ub") 4 4 | open("foo", "rUb") 5 5 | open("foo", "r") @@ -128,7 +128,7 @@ UP015.py:7:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 4 4 | open("foo", "rUb") 5 5 | open("foo", "r") 6 6 | open("foo", "rt") @@ -149,7 +149,7 @@ UP015.py:8:1: UP015 [*] Unnecessary open mode parameters, use ""w"" | = help: Replace with ""w"" -ℹ Suggested fix +ℹ Fix 5 5 | open("foo", "r") 6 6 | open("foo", "rt") 7 7 | open("f", "r", encoding="UTF-8") @@ -170,7 +170,7 @@ UP015.py:10:6: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 7 7 | open("f", "r", encoding="UTF-8") 8 8 | open("f", "wt") 9 9 | @@ -191,7 +191,7 @@ UP015.py:12:6: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 9 9 | 10 10 | with open("foo", "U") as f: 11 11 | pass @@ -212,7 +212,7 @@ UP015.py:14:6: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 11 11 | pass 12 12 | with open("foo", "Ur") as f: 13 13 | pass @@ -233,7 +233,7 @@ UP015.py:16:6: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 13 13 | pass 14 14 | with open("foo", "Ub") as f: 15 15 | pass @@ -254,7 +254,7 @@ UP015.py:18:6: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 15 15 | pass 16 16 | with open("foo", "rUb") as f: 17 17 | pass @@ -275,7 +275,7 @@ UP015.py:20:6: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 17 17 | pass 18 18 | with open("foo", "r") as f: 19 19 | pass @@ -296,7 +296,7 @@ UP015.py:22:6: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 19 19 | pass 20 20 | with open("foo", "rt") as f: 21 21 | pass @@ -316,7 +316,7 @@ UP015.py:24:6: UP015 [*] Unnecessary open mode parameters, use ""w"" | = help: Replace with ""w"" -ℹ Suggested fix +ℹ Fix 21 21 | pass 22 22 | with open("foo", "r", encoding="UTF-8") as f: 23 23 | pass @@ -336,7 +336,7 @@ UP015.py:27:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 24 24 | with open("foo", "wt") as f: 25 25 | pass 26 26 | @@ -356,7 +356,7 @@ UP015.py:28:1: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 25 25 | pass 26 26 | 27 27 | open(f("a", "b", "c"), "U") @@ -377,7 +377,7 @@ UP015.py:30:6: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 27 27 | open(f("a", "b", "c"), "U") 28 28 | open(f("a", "b", "c"), "Ub") 29 29 | @@ -397,7 +397,7 @@ UP015.py:32:6: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 29 29 | 30 30 | with open(f("a", "b", "c"), "U") as f: 31 31 | pass @@ -418,7 +418,7 @@ UP015.py:35:6: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 32 32 | with open(f("a", "b", "c"), "Ub") as f: 33 33 | pass 34 34 | @@ -439,7 +439,7 @@ UP015.py:35:30: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 32 32 | with open(f("a", "b", "c"), "Ub") as f: 33 33 | pass 34 34 | @@ -459,7 +459,7 @@ UP015.py:37:6: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 34 34 | 35 35 | with open("foo", "U") as fa, open("bar", "U") as fb: 36 36 | pass @@ -479,7 +479,7 @@ UP015.py:37:31: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 34 34 | 35 35 | with open("foo", "U") as fa, open("bar", "U") as fb: 36 36 | pass @@ -500,7 +500,7 @@ UP015.py:40:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 37 37 | with open("foo", "Ub") as fa, open("bar", "Ub") as fb: 38 38 | pass 39 39 | @@ -519,7 +519,7 @@ UP015.py:41:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 38 38 | pass 39 39 | 40 40 | open("foo", mode="U") @@ -540,7 +540,7 @@ UP015.py:42:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 39 39 | 40 40 | open("foo", mode="U") 41 41 | open(name="foo", mode="U") @@ -561,7 +561,7 @@ UP015.py:44:6: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 41 41 | open(name="foo", mode="U") 42 42 | open(mode="U", name="foo") 43 43 | @@ -582,7 +582,7 @@ UP015.py:46:6: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 43 43 | 44 44 | with open("foo", mode="U") as f: 45 45 | pass @@ -602,7 +602,7 @@ UP015.py:48:6: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 45 45 | pass 46 46 | with open(name="foo", mode="U") as f: 47 47 | pass @@ -623,7 +623,7 @@ UP015.py:51:1: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 48 48 | with open(mode="U", name="foo") as f: 49 49 | pass 50 50 | @@ -642,7 +642,7 @@ UP015.py:52:1: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 49 49 | pass 50 50 | 51 51 | open("foo", mode="Ub") @@ -663,7 +663,7 @@ UP015.py:53:1: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 50 50 | 51 51 | open("foo", mode="Ub") 52 52 | open(name="foo", mode="Ub") @@ -684,7 +684,7 @@ UP015.py:55:6: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 52 52 | open(name="foo", mode="Ub") 53 53 | open(mode="Ub", name="foo") 54 54 | @@ -705,7 +705,7 @@ UP015.py:57:6: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 54 54 | 55 55 | with open("foo", mode="Ub") as f: 56 56 | pass @@ -725,7 +725,7 @@ UP015.py:59:6: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 56 56 | pass 57 57 | with open(name="foo", mode="Ub") as f: 58 58 | pass @@ -746,7 +746,7 @@ UP015.py:62:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 59 59 | with open(mode="Ub", name="foo") as f: 60 60 | pass 61 61 | @@ -766,7 +766,7 @@ UP015.py:63:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 60 60 | pass 61 61 | 62 62 | open(file="foo", mode='U', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None) @@ -786,7 +786,7 @@ UP015.py:64:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 61 61 | 62 62 | open(file="foo", mode='U', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None) 63 63 | open(file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='U') @@ -807,7 +807,7 @@ UP015.py:65:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 62 62 | open(file="foo", mode='U', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None) 63 63 | open(file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='U') 64 64 | open(file="foo", buffering=- 1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None) @@ -828,7 +828,7 @@ UP015.py:67:1: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 64 64 | open(file="foo", buffering=- 1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None) 65 65 | open(mode='U', file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None) 66 66 | @@ -848,7 +848,7 @@ UP015.py:68:1: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 65 65 | open(mode='U', file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None) 66 66 | 67 67 | open(file="foo", mode='Ub', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None) @@ -868,7 +868,7 @@ UP015.py:69:1: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 66 66 | 67 67 | open(file="foo", mode='Ub', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None) 68 68 | open(file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub') @@ -889,7 +889,7 @@ UP015.py:70:1: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 67 67 | open(file="foo", mode='Ub', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None) 68 68 | open(file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub') 69 69 | open(file="foo", buffering=- 1, encoding=None, errors=None, mode='Ub', newline=None, closefd=True, opener=None) diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_0.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_0.py.snap index cdfd9e18a3..f77d7d36a6 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_0.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_0.py.snap @@ -11,7 +11,7 @@ UP024_0.py:6:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `EnvironmentError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 3 3 | # These should be fixed 4 4 | try: 5 5 | pass @@ -31,7 +31,7 @@ UP024_0.py:11:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `IOError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 8 8 | 9 9 | try: 10 10 | pass @@ -51,7 +51,7 @@ UP024_0.py:16:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `WindowsError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 13 13 | 14 14 | try: 15 15 | pass @@ -71,7 +71,7 @@ UP024_0.py:21:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `mmap.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 18 18 | 19 19 | try: 20 20 | pass @@ -91,7 +91,7 @@ UP024_0.py:26:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `select.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 23 23 | 24 24 | try: 25 25 | pass @@ -111,7 +111,7 @@ UP024_0.py:31:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `socket.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 28 28 | 29 29 | try: 30 30 | pass @@ -131,7 +131,7 @@ UP024_0.py:36:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 33 33 | 34 34 | try: 35 35 | pass @@ -152,7 +152,7 @@ UP024_0.py:43:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace with builtin `OSError` -ℹ Suggested fix +ℹ Fix 40 40 | 41 41 | try: 42 42 | pass @@ -173,7 +173,7 @@ UP024_0.py:47:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace with builtin `OSError` -ℹ Suggested fix +ℹ Fix 44 44 | pass 45 45 | try: 46 46 | pass @@ -193,7 +193,7 @@ UP024_0.py:51:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace with builtin `OSError` -ℹ Suggested fix +ℹ Fix 48 48 | pass 49 49 | try: 50 50 | pass @@ -213,7 +213,7 @@ UP024_0.py:58:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace with builtin `OSError` -ℹ Suggested fix +ℹ Fix 55 55 | 56 56 | try: 57 57 | pass @@ -234,7 +234,7 @@ UP024_0.py:65:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace with builtin `OSError` -ℹ Suggested fix +ℹ Fix 62 62 | from .mmap import error 63 63 | try: 64 64 | pass @@ -254,7 +254,7 @@ UP024_0.py:87:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `mmap.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 84 84 | pass 85 85 | try: 86 86 | pass diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_1.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_1.py.snap index f59ac0307d..ed64b2e20f 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_1.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_1.py.snap @@ -12,7 +12,7 @@ UP024_1.py:5:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace with builtin `OSError` -ℹ Suggested fix +ℹ Fix 2 2 | 3 3 | try: 4 4 | pass @@ -32,7 +32,7 @@ UP024_1.py:7:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace with builtin `OSError` -ℹ Suggested fix +ℹ Fix 4 4 | pass 5 5 | except (OSError, mmap.error, IOError): 6 6 | pass @@ -57,7 +57,7 @@ UP024_1.py:12:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace with builtin `OSError` -ℹ Suggested fix +ℹ Fix 9 9 | 10 10 | try: 11 11 | pass diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_2.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_2.py.snap index 093108e347..36ce30b1e5 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_2.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_2.py.snap @@ -12,7 +12,7 @@ UP024_2.py:10:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `socket.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 7 7 | 8 8 | # Testing the modules 9 9 | import socket, mmap, select @@ -32,7 +32,7 @@ UP024_2.py:11:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `mmap.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 8 8 | # Testing the modules 9 9 | import socket, mmap, select 10 10 | raise socket.error @@ -53,7 +53,7 @@ UP024_2.py:12:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `select.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 9 9 | import socket, mmap, select 10 10 | raise socket.error 11 11 | raise mmap.error @@ -74,7 +74,7 @@ UP024_2.py:14:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `socket.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 11 11 | raise mmap.error 12 12 | raise select.error 13 13 | @@ -93,7 +93,7 @@ UP024_2.py:15:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `mmap.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 12 12 | raise select.error 13 13 | 14 14 | raise socket.error() @@ -114,7 +114,7 @@ UP024_2.py:16:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `select.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 13 13 | 14 14 | raise socket.error() 15 15 | raise mmap.error(1) @@ -135,7 +135,7 @@ UP024_2.py:18:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `socket.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 15 15 | raise mmap.error(1) 16 16 | raise select.error(1, 2) 17 17 | @@ -155,7 +155,7 @@ UP024_2.py:25:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 22 22 | ) 23 23 | 24 24 | from mmap import error @@ -175,7 +175,7 @@ UP024_2.py:28:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 25 25 | raise error 26 26 | 27 27 | from socket import error @@ -195,7 +195,7 @@ UP024_2.py:31:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 28 28 | raise error(1) 29 29 | 30 30 | from select import error @@ -215,7 +215,7 @@ UP024_2.py:34:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `EnvironmentError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 31 31 | raise error(1, 2) 32 32 | 33 33 | # Testing the names @@ -235,7 +235,7 @@ UP024_2.py:35:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `IOError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 32 32 | 33 33 | # Testing the names 34 34 | raise EnvironmentError @@ -256,7 +256,7 @@ UP024_2.py:36:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `WindowsError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 33 33 | # Testing the names 34 34 | raise EnvironmentError 35 35 | raise IOError @@ -277,7 +277,7 @@ UP024_2.py:38:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `EnvironmentError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 35 35 | raise IOError 36 36 | raise WindowsError 37 37 | @@ -296,7 +296,7 @@ UP024_2.py:39:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `IOError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 36 36 | raise WindowsError 37 37 | 38 38 | raise EnvironmentError() @@ -317,7 +317,7 @@ UP024_2.py:40:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `WindowsError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 37 37 | 38 38 | raise EnvironmentError() 39 39 | raise IOError(1) @@ -338,7 +338,7 @@ UP024_2.py:42:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `EnvironmentError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 39 39 | raise IOError(1) 40 40 | raise WindowsError(1, 2) 41 41 | @@ -359,7 +359,7 @@ UP024_2.py:48:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `WindowsError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 45 45 | 3, 46 46 | ) 47 47 | @@ -377,7 +377,7 @@ UP024_2.py:49:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `EnvironmentError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 46 46 | ) 47 47 | 48 48 | raise WindowsError @@ -394,7 +394,7 @@ UP024_2.py:50:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `IOError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 47 47 | 48 48 | raise WindowsError 49 49 | raise EnvironmentError(1) diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_4.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_4.py.snap index 19e9d10e59..d1dec00413 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_4.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_4.py.snap @@ -11,7 +11,7 @@ UP024_4.py:9:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace with builtin `OSError` -ℹ Suggested fix +ℹ Fix 6 6 | conn = Connection(settings.CELERY_BROKER_URL) 7 7 | conn.ensure_connection(max_retries=2) 8 8 | conn._close() diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP037.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP037.py.snap index 00e5a6a66d..8d3af3b967 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP037.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP037.py.snap @@ -9,7 +9,7 @@ UP037.py:18:14: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 15 15 | from mypy_extensions import Arg, DefaultArg, DefaultNamedArg, NamedArg, VarArg 16 16 | 17 17 | @@ -27,7 +27,7 @@ UP037.py:18:28: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 15 15 | from mypy_extensions import Arg, DefaultArg, DefaultNamedArg, NamedArg, VarArg 16 16 | 17 17 | @@ -45,7 +45,7 @@ UP037.py:19:8: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 16 16 | 17 17 | 18 18 | def foo(var: "MyClass") -> "MyClass": @@ -63,7 +63,7 @@ UP037.py:22:21: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 19 19 | x: "MyClass" 20 20 | 21 21 | @@ -81,7 +81,7 @@ UP037.py:26:16: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 23 23 | pass 24 24 | 25 25 | @@ -99,7 +99,7 @@ UP037.py:26:33: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 23 23 | pass 24 24 | 25 25 | @@ -118,7 +118,7 @@ UP037.py:30:10: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 27 27 | pass 28 28 | 29 29 | @@ -137,7 +137,7 @@ UP037.py:32:14: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 29 29 | 30 30 | x: Tuple["MyClass"] 31 31 | @@ -155,7 +155,7 @@ UP037.py:36:8: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 33 33 | 34 34 | 35 35 | class Foo(NamedTuple): @@ -173,7 +173,7 @@ UP037.py:40:27: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 37 37 | 38 38 | 39 39 | class D(TypedDict): @@ -191,7 +191,7 @@ UP037.py:44:31: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 41 41 | 42 42 | 43 43 | class D(TypedDict): @@ -210,7 +210,7 @@ UP037.py:47:14: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 44 44 | E: TypedDict("E", {"foo": "int"}) 45 45 | 46 46 | @@ -231,7 +231,7 @@ UP037.py:49:8: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 46 46 | 47 47 | x: Annotated["str", "metadata"] 48 48 | @@ -252,7 +252,7 @@ UP037.py:51:15: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 48 48 | 49 49 | x: Arg("str", "name") 50 50 | @@ -273,7 +273,7 @@ UP037.py:53:13: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 50 50 | 51 51 | x: DefaultArg("str", "name") 52 52 | @@ -294,7 +294,7 @@ UP037.py:55:20: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 52 52 | 53 53 | x: NamedArg("str", "name") 54 54 | @@ -315,7 +315,7 @@ UP037.py:57:20: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 54 54 | 55 55 | x: DefaultNamedArg("str", "name") 56 56 | @@ -336,7 +336,7 @@ UP037.py:59:11: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 56 56 | 57 57 | x: DefaultNamedArg("str", name="name") 58 58 | @@ -357,7 +357,7 @@ UP037.py:61:19: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 58 58 | 59 59 | x: VarArg("str") 60 60 | @@ -378,7 +378,7 @@ UP037.py:63:29: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 60 60 | 61 61 | x: List[List[List["MyClass"]]] 62 62 | @@ -399,7 +399,7 @@ UP037.py:63:45: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 60 60 | 61 61 | x: List[List[List["MyClass"]]] 62 62 | @@ -420,7 +420,7 @@ UP037.py:65:29: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 62 62 | 63 63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")]) 64 64 | @@ -441,7 +441,7 @@ UP037.py:65:36: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 62 62 | 63 63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")]) 64 64 | @@ -462,7 +462,7 @@ UP037.py:65:45: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 62 62 | 63 63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")]) 64 64 | @@ -483,7 +483,7 @@ UP037.py:65:52: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 62 62 | 63 63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")]) 64 64 | @@ -504,7 +504,7 @@ UP037.py:67:24: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 64 64 | 65 65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")]) 66 66 | @@ -525,7 +525,7 @@ UP037.py:67:38: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 64 64 | 65 65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")]) 66 66 | @@ -546,7 +546,7 @@ UP037.py:67:45: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 64 64 | 65 65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")]) 66 66 | diff --git a/crates/ruff/src/rules/ruff/rules/ambiguous_unicode_character.rs b/crates/ruff/src/rules/ruff/rules/ambiguous_unicode_character.rs index 65d05f9095..3963e44aae 100644 --- a/crates/ruff/src/rules/ruff/rules/ambiguous_unicode_character.rs +++ b/crates/ruff/src/rules/ruff/rules/ambiguous_unicode_character.rs @@ -256,8 +256,7 @@ impl Candidate { ); if settings.rules.enabled(diagnostic.kind.rule()) { if settings.rules.should_fix(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::manual(Edit::range_replacement( self.representant.to_string(), char_range, ))); diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__confusables.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__confusables.snap index ec47da8256..23057ef9eb 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__confusables.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__confusables.snap @@ -9,7 +9,7 @@ confusables.py:1:6: RUF001 [*] String contains ambiguous `𝐁` (MATHEMATICAL BO | = help: Replace `𝐁` (MATHEMATICAL BOLD CAPITAL B) with `B` (LATIN CAPITAL LETTER B) -ℹ Suggested fix +ℹ Possible fix 1 |-x = "𝐁ad string" 1 |+x = "Bad string" 2 2 | y = "−" @@ -26,7 +26,7 @@ confusables.py:6:56: RUF002 [*] Docstring contains ambiguous `)` (FULLWIDTH RI | = help: Replace `)` (FULLWIDTH RIGHT PARENTHESIS) with `)` (RIGHT PARENTHESIS) -ℹ Suggested fix +ℹ Possible fix 3 3 | 4 4 | 5 5 | def f(): @@ -46,7 +46,7 @@ confusables.py:7:62: RUF003 [*] Comment contains ambiguous `᜵` (PHILIPPINE SIN | = help: Replace `᜵` (PHILIPPINE SINGLE PUNCTUATION) with `/` (SOLIDUS) -ℹ Suggested fix +ℹ Possible fix 4 4 | 5 5 | def f(): 6 6 | """Here's a docstring with an unusual parenthesis: )""" @@ -64,7 +64,7 @@ confusables.py:17:6: RUF001 [*] String contains ambiguous `𝐁` (MATHEMATICAL B | = help: Replace `𝐁` (MATHEMATICAL BOLD CAPITAL B) with `B` (LATIN CAPITAL LETTER B) -ℹ Suggested fix +ℹ Possible fix 14 14 | ... 15 15 | 16 16 | @@ -85,7 +85,7 @@ confusables.py:26:10: RUF001 [*] String contains ambiguous `α` (GREEK SMALL LET | = help: Replace `α` (GREEK SMALL LETTER ALPHA) with `a` (LATIN SMALL LETTER A) -ℹ Suggested fix +ℹ Possible fix 23 23 | 24 24 | # The first word should be ignored, while the second should be included, since it 25 25 | # contains ASCII. @@ -104,7 +104,7 @@ confusables.py:31:6: RUF001 [*] String contains ambiguous `Р` (CYRILLIC CAPITAL | = help: Replace `Р` (CYRILLIC CAPITAL LETTER ER) with `P` (LATIN CAPITAL LETTER P) -ℹ Suggested fix +ℹ Possible fix 28 28 | # The two characters should be flagged here. The first character is a "word" 29 29 | # consisting of a single ambiguous character, while the second character is a "word 30 30 | # boundary" (whitespace) that it itself ambiguous. @@ -120,7 +120,7 @@ confusables.py:31:7: RUF001 [*] String contains ambiguous ` ` (EN QUAD). Did y | = help: Replace ` ` (EN QUAD) with ` ` (SPACE) -ℹ Suggested fix +ℹ Possible fix 28 28 | # The two characters should be flagged here. The first character is a "word" 29 29 | # consisting of a single ambiguous character, while the second character is a "word 30 30 | # boundary" (whitespace) that it itself ambiguous. From 097823b56d56e6b54175364e71a6d6c974bd3d46 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 15 Jun 2023 08:04:27 +0530 Subject: [PATCH 071/447] Ability to perform integration test on Jupyter notebooks (#5076) ## Summary Ability to perform integration test on Jupyter notebooks Part of #1218 ## Test Plan `cargo test` --- .../test/fixtures/jupyter/isort.ipynb | 51 ++++++++ .../fixtures/jupyter/isort_expected.ipynb | 53 ++++++++ crates/ruff/src/jupyter/notebook.rs | 19 ++- crates/ruff/src/jupyter/schema.rs | 24 ++-- ...pyter__notebook__test__import_sorting.snap | 52 ++++++++ crates/ruff/src/source_kind.rs | 2 +- crates/ruff/src/test.rs | 119 +++++++++++++++--- 7 files changed, 286 insertions(+), 34 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/jupyter/isort.ipynb create mode 100644 crates/ruff/resources/test/fixtures/jupyter/isort_expected.ipynb create mode 100644 crates/ruff/src/jupyter/snapshots/ruff__jupyter__notebook__test__import_sorting.snap diff --git a/crates/ruff/resources/test/fixtures/jupyter/isort.ipynb b/crates/ruff/resources/test/fixtures/jupyter/isort.ipynb new file mode 100644 index 0000000000..aef5ff2e8b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/jupyter/isort.ipynb @@ -0,0 +1,51 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "0c7535f6-43cb-423f-bfe1-d263b8f55da0", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import random\n", + "import math" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c066fa1a-5682-47af-8c17-5afec3cf4ad0", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Any\n", + "import collections\n", + "# Newline should be added here\n", + "def foo():\n", + " pass" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (ruff)", + "language": "python", + "name": "ruff" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/crates/ruff/resources/test/fixtures/jupyter/isort_expected.ipynb b/crates/ruff/resources/test/fixtures/jupyter/isort_expected.ipynb new file mode 100644 index 0000000000..009c598e71 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/jupyter/isort_expected.ipynb @@ -0,0 +1,53 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "663ba955-baca-4f34-9ebb-840d2573ae3f", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "import random\n", + "from pathlib import Path" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0adfe23-8aea-47e9-bf67-d856cfcb96ea", + "metadata": {}, + "outputs": [], + "source": [ + "import collections\n", + "from typing import Any\n", + "\n", + "\n", + "# Newline should be added here\n", + "def foo():\n", + " pass" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (ruff)", + "language": "python", + "name": "ruff" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/crates/ruff/src/jupyter/notebook.rs b/crates/ruff/src/jupyter/notebook.rs index 70420e89a7..d7381cf56f 100644 --- a/crates/ruff/src/jupyter/notebook.rs +++ b/crates/ruff/src/jupyter/notebook.rs @@ -75,7 +75,7 @@ impl Cell { } } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct Notebook { /// Python source code of the notebook. /// @@ -414,8 +414,9 @@ mod test { use crate::jupyter::is_jupyter_notebook; use crate::jupyter::schema::Cell; use crate::jupyter::Notebook; - - use crate::test::test_resource_path; + use crate::registry::Rule; + use crate::test::{test_notebook_path, test_resource_path}; + use crate::{assert_messages, settings}; /// Read a Jupyter cell from the `resources/test/fixtures/jupyter/cell` directory. fn read_jupyter_cell(path: impl AsRef) -> Result { @@ -523,4 +524,16 @@ print("after empty cells") ] ); } + + #[test] + fn test_import_sorting() -> Result<()> { + let path = "isort.ipynb".to_string(); + let (diagnostics, source_kind) = test_notebook_path( + &path, + Path::new("isort_expected.ipynb"), + &settings::Settings::for_rule(Rule::UnsortedImports), + )?; + assert_messages!(diagnostics, path, source_kind); + Ok(()) + } } diff --git a/crates/ruff/src/jupyter/schema.rs b/crates/ruff/src/jupyter/schema.rs index bc61253e1b..34e168c901 100644 --- a/crates/ruff/src/jupyter/schema.rs +++ b/crates/ruff/src/jupyter/schema.rs @@ -26,7 +26,7 @@ use serde_with::skip_serializing_none; /// Generated by from /// /// Jupyter Notebook v4.5 JSON schema. -#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct RawNotebook { /// Array of cells of the current notebook. pub cells: Vec, @@ -46,7 +46,7 @@ pub struct RawNotebook { /// /// Notebook code cell. #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct Cell { pub attachments: Option>>, @@ -67,7 +67,7 @@ pub struct Cell { /// Cell-level metadata. #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct CellMetadata { /// Raw cell metadata format for nbconvert. pub format: Option, @@ -94,7 +94,7 @@ pub struct CellMetadata { /// Execution time for the code in the cell. This tracks time at which messages are received /// from iopub or shell channels #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct Execution { /// header.date (in ISO 8601 format) of iopub channel's execute_input message. It indicates @@ -124,7 +124,7 @@ pub struct Execution { /// /// Output of an error that occurred during code cell execution. #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct Output { pub data: Option>, @@ -147,7 +147,7 @@ pub struct Output { /// Notebook root-level metadata. #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct JupyterNotebookMetadata { /// The author(s) of the notebook document pub authors: Option>>, @@ -166,7 +166,7 @@ pub struct JupyterNotebookMetadata { } /// Kernel information. -#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Kernelspec { /// Name to display in UI. pub display_name: String, @@ -179,7 +179,7 @@ pub struct Kernelspec { /// Kernel information. #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct LanguageInfo { /// The codemirror mode to use for code in this language. pub codemirror_mode: Option, @@ -202,7 +202,7 @@ pub struct LanguageInfo { /// Contents of the cell, represented as an array of lines. /// /// The stream's text output, represented as an array of strings. -#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(untagged)] pub enum SourceValue { String(String), @@ -210,7 +210,7 @@ pub enum SourceValue { } /// Whether the cell's output is scrolled, unscrolled, or autoscrolled. -#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(untagged)] pub enum ScrolledUnion { Bool(bool), @@ -223,7 +223,7 @@ pub enum ScrolledUnion { /// Contents of the cell, represented as an array of lines. /// /// The stream's text output, represented as an array of strings. -#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(untagged)] pub enum TextUnion { String(String), @@ -231,7 +231,7 @@ pub enum TextUnion { } /// The codemirror mode to use for code in this language. -#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(untagged)] pub enum CodemirrorMode { AnythingMap(HashMap>), diff --git a/crates/ruff/src/jupyter/snapshots/ruff__jupyter__notebook__test__import_sorting.snap b/crates/ruff/src/jupyter/snapshots/ruff__jupyter__notebook__test__import_sorting.snap new file mode 100644 index 0000000000..8470981be3 --- /dev/null +++ b/crates/ruff/src/jupyter/snapshots/ruff__jupyter__notebook__test__import_sorting.snap @@ -0,0 +1,52 @@ +--- +source: crates/ruff/src/jupyter/notebook.rs +--- +isort.ipynb:cell 1:1:1: I001 [*] Import block is un-sorted or un-formatted + | +1 | / from pathlib import Path +2 | | import random +3 | | import math +4 | | from typing import Any + | |_^ I001 +5 | import collections +6 | # Newline should be added here + | + = help: Organize imports + +ℹ Fix + 1 |+import math + 2 |+import random +1 3 | from pathlib import Path +2 |-import random +3 |-import math +4 4 | from typing import Any +5 5 | import collections +6 6 | # Newline should be added here + +isort.ipynb:cell 2:1:1: I001 [*] Import block is un-sorted or un-formatted + | +2 | import random +3 | import math +4 | / from typing import Any +5 | | import collections +6 | | # Newline should be added here + | |_^ I001 +7 | def foo(): +8 | pass + | + = help: Organize imports + +ℹ Fix +1 1 | from pathlib import Path +2 2 | import random +3 3 | import math + 4 |+import collections +4 5 | from typing import Any +5 |-import collections + 6 |+ + 7 |+ +6 8 | # Newline should be added here +7 9 | def foo(): +8 10 | pass + + diff --git a/crates/ruff/src/source_kind.rs b/crates/ruff/src/source_kind.rs index b266e2df57..ab63e89c28 100644 --- a/crates/ruff/src/source_kind.rs +++ b/crates/ruff/src/source_kind.rs @@ -1,6 +1,6 @@ use crate::jupyter::Notebook; -#[derive(Debug, PartialEq, is_macro::Is)] +#[derive(Clone, Debug, PartialEq, is_macro::Is)] pub enum SourceKind { Python(String), Jupyter(Notebook), diff --git a/crates/ruff/src/test.rs b/crates/ruff/src/test.rs index e0913256f5..1e46037db7 100644 --- a/crates/ruff/src/test.rs +++ b/crates/ruff/src/test.rs @@ -15,12 +15,25 @@ use ruff_python_ast::source_code::{Indexer, Locator, SourceFileBuilder, Stylist} use crate::autofix::{fix_file, FixResult}; use crate::directives; +use crate::jupyter::Notebook; use crate::linter::{check_path, LinterResult}; use crate::message::{Emitter, EmitterContext, Message, TextEmitter}; use crate::packaging::detect_package_root; use crate::registry::AsRule; use crate::rules::pycodestyle::rules::syntax_error; use crate::settings::{flags, Settings}; +use crate::source_kind::SourceKind; + +#[cfg(not(fuzzing))] +fn read_jupyter_notebook(path: &Path) -> Result { + Notebook::read(path).map_err(|err| { + anyhow::anyhow!( + "Failed to read notebook file `{}`: {:?}", + path.display(), + err + ) + }) +} #[cfg(not(fuzzing))] pub(crate) fn test_resource_path(path: impl AsRef) -> std::path::PathBuf { @@ -32,14 +45,41 @@ pub(crate) fn test_resource_path(path: impl AsRef) -> std::path::PathBuf { pub(crate) fn test_path(path: impl AsRef, settings: &Settings) -> Result> { let path = test_resource_path("fixtures").join(path); let contents = std::fs::read_to_string(&path)?; - Ok(test_contents(&contents, &path, settings)) + Ok(test_contents( + &mut SourceKind::Python(contents), + &path, + settings, + )) +} + +#[cfg(not(fuzzing))] +pub(crate) fn test_notebook_path( + path: impl AsRef, + expected: impl AsRef, + settings: &Settings, +) -> Result<(Vec, SourceKind)> { + let path = test_resource_path("fixtures/jupyter").join(path); + let mut source_kind = SourceKind::Jupyter(read_jupyter_notebook(&path)?); + let messages = test_contents(&mut source_kind, &path, settings); + let expected_notebook = + read_jupyter_notebook(&test_resource_path("fixtures/jupyter").join(expected))?; + if let SourceKind::Jupyter(notebook) = &source_kind { + assert_eq!(notebook.cell_offsets(), expected_notebook.cell_offsets()); + assert_eq!(notebook.index(), expected_notebook.index()); + assert_eq!(notebook.content(), expected_notebook.content()); + }; + Ok((messages, source_kind)) } /// Run [`check_path`] on a snippet of Python code. pub fn test_snippet(contents: &str, settings: &Settings) -> Vec { let path = Path::new(""); let contents = dedent(contents); - test_contents(&contents, path, settings) + test_contents( + &mut SourceKind::Python(contents.to_string()), + path, + settings, + ) } thread_local! { @@ -56,9 +96,10 @@ pub(crate) fn max_iterations() -> usize { /// A convenient wrapper around [`check_path`], that additionally /// asserts that autofixes converge after a fixed number of iterations. -fn test_contents(contents: &str, path: &Path, settings: &Settings) -> Vec { - let tokens: Vec = ruff_rustpython::tokenize(contents); - let locator = Locator::new(contents); +fn test_contents(source_kind: &mut SourceKind, path: &Path, settings: &Settings) -> Vec { + let contents = source_kind.content().to_string(); + let tokens: Vec = ruff_rustpython::tokenize(&contents); + let locator = Locator::new(&contents); let stylist = Stylist::from_tokens(&tokens, &locator); let indexer = Indexer::from_tokens(&tokens, &locator); let directives = directives::extract_directives( @@ -81,7 +122,7 @@ fn test_contents(contents: &str, path: &Path, settings: &Settings) -> Vec Vec Vec = ruff_rustpython::tokenize(&fixed_contents); let locator = Locator::new(&fixed_contents); let stylist = Stylist::from_tokens(&tokens, &locator); @@ -138,18 +184,18 @@ fn test_contents(contents: &str, path: &Path, settings: &Settings) -> Vec, file_path: &Path, source: &str) -> String { - let source_file = SourceFileBuilder::new( - file_path.file_name().unwrap().to_string_lossy().as_ref(), - source, - ) - .finish(); +fn print_diagnostics( + diagnostics: Vec, + file_path: &Path, + source: &str, + source_kind: &SourceKind, +) -> String { + let filename = file_path.file_name().unwrap().to_string_lossy(); + let source_file = SourceFileBuilder::new(filename.as_ref(), source).finish(); let messages: Vec<_> = diagnostics .into_iter() @@ -219,7 +267,35 @@ fn print_diagnostics(diagnostics: Vec, file_path: &Path, source: &st }) .collect(); - print_messages(&messages) + if source_kind.is_jupyter() { + print_jupyter_messages(&messages, &filename, source_kind) + } else { + print_messages(&messages) + } +} + +pub(crate) fn print_jupyter_messages( + messages: &[Message], + filename: &str, + source_kind: &SourceKind, +) -> String { + let mut output = Vec::new(); + + TextEmitter::default() + .with_show_fix_status(true) + .with_show_fix_diff(true) + .with_show_source(true) + .emit( + &mut output, + messages, + &EmitterContext::new(&FxHashMap::from_iter([( + filename.to_string(), + source_kind.clone(), + )])), + ) + .unwrap(); + + String::from_utf8(output).unwrap() } pub(crate) fn print_messages(messages: &[Message]) -> String { @@ -241,6 +317,13 @@ pub(crate) fn print_messages(messages: &[Message]) -> String { #[macro_export] macro_rules! assert_messages { + ($value:expr, $path:expr, $source_kind:expr) => {{ + insta::with_settings!({ omit_expression => true }, { + insta::assert_snapshot!( + $crate::test::print_jupyter_messages(&$value, &$path, &$source_kind) + ); + }); + }}; ($value:expr, @$snapshot:literal) => {{ insta::with_settings!({ omit_expression => true }, { insta::assert_snapshot!($crate::test::print_messages(&$value), $snapshot); From 66089e1a2e4b8b6ad6de3dc9543cd9a8d2a764e3 Mon Sep 17 00:00:00 2001 From: konstin Date: Thu, 15 Jun 2023 13:24:14 +0200 Subject: [PATCH 072/447] Fix a number of formatter errors from the cpython repository (#5089) ## Summary This fixes a number of problems in the formatter that showed up with various files in the [cpython](https://github.com/python/cpython) repository. These problems surfaced as unstable formatting and invalid code. This is not the entirety of problems discovered through cpython, but a big enough chunk to separate it. Individual fixes are generally individual commits. They were discovered with #5055, which i update as i work through the output ## Test Plan I added regression tests with links to cpython for each entry, except for the two stubs that also got comment stubs since they'll be implemented properly later. --- crates/ruff_python_ast/src/node.rs | 16 +++++++ .../{binary_expression.py => binary.py} | 0 .../test/fixtures/ruff/expression/list.py | 10 ++++ .../{tuple_expression.py => tuple.py} | 0 .../statement/{stmt_assign.py => assign.py} | 0 .../test/fixtures/ruff/statement/function.py | 22 +++++++++ .../ruff/statement/{if_statement.py => if.py} | 0 .../src/comments/placement.rs | 22 ++++++++- .../src/expression/expr_constant.rs | 16 +++++++ .../src/expression/expr_dict.rs | 11 +++++ .../src/expression/expr_list.rs | 39 ++++++++++++++-- .../src/other/arguments.rs | 5 ++ ...atter__tests__black_test__fmtonoff_py.snap | 20 ++++---- ...atter__tests__black_test__function_py.snap | 5 +- ...ts__ruff_test__expression__binary_py.snap} | 0 ...tests__ruff_test__expression__list_py.snap | 35 ++++++++++++++ ...sts__ruff_test__expression__tuple_py.snap} | 0 ...sts__ruff_test__statement__assign_py.snap} | 0 ...ts__ruff_test__statement__function_py.snap | 46 +++++++++++++++++++ ...__tests__ruff_test__statement__if_py.snap} | 0 ...matter__trivia__tests__tokenize_comma.snap | 2 +- ..._trivia__tests__tokenize_continuation.snap | 2 +- ...__trivia__tests__tokenize_parentheses.snap | 2 +- ...er__trivia__tests__tokenize_substring.snap | 2 +- ...atter__trivia__tests__tokenize_trivia.snap | 2 +- 25 files changed, 234 insertions(+), 23 deletions(-) rename crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/{binary_expression.py => binary.py} (100%) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list.py rename crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/{tuple_expression.py => tuple.py} (100%) rename crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/{stmt_assign.py => assign.py} (100%) rename crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/{if_statement.py => if.py} (100%) rename crates/ruff_python_formatter/src/snapshots/{ruff_python_formatter__tests__ruff_test__expression__binary_expression_py.snap => ruff_python_formatter__tests__ruff_test__expression__binary_py.snap} (100%) create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__list_py.snap rename crates/ruff_python_formatter/src/snapshots/{ruff_python_formatter__tests__ruff_test__expression__tuple_expression_py.snap => ruff_python_formatter__tests__ruff_test__expression__tuple_py.snap} (100%) rename crates/ruff_python_formatter/src/snapshots/{ruff_python_formatter__tests__ruff_test__statement__stmt_assign_py.snap => ruff_python_formatter__tests__ruff_test__statement__assign_py.snap} (100%) rename crates/ruff_python_formatter/src/snapshots/{ruff_python_formatter__tests__ruff_test__statement__if_statement_py.snap => ruff_python_formatter__tests__ruff_test__statement__if_py.snap} (100%) diff --git a/crates/ruff_python_ast/src/node.rs b/crates/ruff_python_ast/src/node.rs index 1c8d66ee51..d60be5b284 100644 --- a/crates/ruff_python_ast/src/node.rs +++ b/crates/ruff_python_ast/src/node.rs @@ -4212,6 +4212,22 @@ impl AnyNodeRef<'_> { | AnyNodeRef::Decorator(_) => false, } } + + pub const fn is_node_with_body(self) -> bool { + matches!( + self, + AnyNodeRef::StmtIf(_) + | AnyNodeRef::StmtFor(_) + | AnyNodeRef::StmtAsyncFor(_) + | AnyNodeRef::StmtWhile(_) + | AnyNodeRef::StmtWith(_) + | AnyNodeRef::StmtAsyncWith(_) + | AnyNodeRef::StmtMatch(_) + | AnyNodeRef::StmtFunctionDef(_) + | AnyNodeRef::StmtAsyncFunctionDef(_) + | AnyNodeRef::StmtClassDef(_) + ) + } } impl<'a> From<&'a ModModule> for AnyNodeRef<'a> { diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary_expression.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py similarity index 100% rename from crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary_expression.py rename to crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py 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 new file mode 100644 index 0000000000..2ec1c0e293 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list.py @@ -0,0 +1,10 @@ +# Dangling comment placement in empty lists +# Regression test for https://github.com/python/cpython/blob/03160630319ca26dcbbad65225da4248e54c45ec/Tools/c-analyzer/c_analyzer/datafiles.py#L14-L16 +a1 = [ # a +] +a2 = [ # a + # b +] +a3 = [ + # b +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tuple_expression.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tuple.py similarity index 100% rename from crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tuple_expression.py rename to crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tuple.py diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/stmt_assign.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assign.py similarity index 100% rename from crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/stmt_assign.py rename to crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assign.py diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py index 7e4ddaa48b..8b540fe646 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py @@ -68,3 +68,25 @@ def test(): ... # Comment def with_leading_comment(): ... + +# Comment that could be mistaken for a trailing comment of the function declaration when +# looking from the position of the if +# Regression test for https://github.com/python/cpython/blob/ad56340b665c5d8ac1f318964f71697bba41acb7/Lib/logging/__init__.py#L253-L260 +if True: + def f1(): + pass # a +else: + pass + +# Here it's actually a trailing comment +if True: + def f2(): + pass + # a +else: + pass + +# Make sure the star is printed +# Regression test for https://github.com/python/cpython/blob/7199584ac8632eab57612f595a7162ab8d2ebbc0/Lib/warnings.py#L513 +def f(arg1=1, *, kwonlyarg1, kwonlyarg2=2): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if_statement.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py similarity index 100% rename from crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if_statement.py rename to crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 2dabe23930..3c1166b1f5 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -332,7 +332,20 @@ fn handle_in_between_bodies_end_of_line_comment<'a>( // else: // b // ``` - CommentPlacement::trailing(preceding, comment) + if preceding.is_node_with_body() { + // We can't set this as a trailing comment of the function declaration because it + // will then move behind the function block instead of sticking with the pass + // ```python + // if True: + // def f(): + // pass # a + // else: + // pass + // ``` + CommentPlacement::Default(comment) + } else { + CommentPlacement::trailing(preceding, comment) + } } else if following.is_stmt_if() || following.is_except_handler() { // The `elif` or except handlers have their own body to which we can attach the trailing comment // ```python @@ -837,7 +850,12 @@ fn find_pos_only_slash_offset( return Some(maybe_slash.start()); } - debug_assert_eq!(maybe_slash.kind(), TokenKind::RParen); + debug_assert_eq!( + maybe_slash.kind(), + TokenKind::RParen, + "{:?}", + maybe_slash.kind() + ); } } diff --git a/crates/ruff_python_formatter/src/expression/expr_constant.rs b/crates/ruff_python_formatter/src/expression/expr_constant.rs index 90ee45c1cf..bdb9b7117c 100644 --- a/crates/ruff_python_formatter/src/expression/expr_constant.rs +++ b/crates/ruff_python_formatter/src/expression/expr_constant.rs @@ -39,6 +39,22 @@ impl FormatNodeRule for FormatExprConstant { } } } + + fn fmt_dangling_comments( + &self, + _node: &ExprConstant, + _f: &mut PyFormatter, + ) -> FormatResult<()> { + // TODO(konstin): Reactivate when string formatting works, currently a source of unstable + // formatting, e.g.: + // magic_methods = ( + // "enter exit " + // # we added divmod and rdivmod here instead of numerics + // # because there is no idivmod + // "divmod rdivmod neg pos abs invert " + // ) + Ok(()) + } } impl NeedsParentheses for ExprConstant { diff --git a/crates/ruff_python_formatter/src/expression/expr_dict.rs b/crates/ruff_python_formatter/src/expression/expr_dict.rs index 600841f2da..3ccc6a4dc8 100644 --- a/crates/ruff_python_formatter/src/expression/expr_dict.rs +++ b/crates/ruff_python_formatter/src/expression/expr_dict.rs @@ -18,6 +18,17 @@ impl FormatNodeRule for FormatExprDict { )] ) } + + fn fmt_dangling_comments(&self, _node: &ExprDict, _f: &mut PyFormatter) -> FormatResult<()> { + // TODO(konstin): Reactivate when string formatting works, currently a source of unstable + // formatting, e.g. + // ```python + // coverage_ignore_c_items = { + // # 'cfunction': [...] + // } + // ``` + Ok(()) + } } impl NeedsParentheses for ExprDict { diff --git a/crates/ruff_python_formatter/src/expression/expr_list.rs b/crates/ruff_python_formatter/src/expression/expr_list.rs index 20153d8aa7..a269853579 100644 --- a/crates/ruff_python_formatter/src/expression/expr_list.rs +++ b/crates/ruff_python_formatter/src/expression/expr_list.rs @@ -1,4 +1,4 @@ -use crate::comments::{dangling_comments, Comments}; +use crate::comments::{dangling_comments, CommentTextPosition, Comments}; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; @@ -18,6 +18,39 @@ impl FormatNodeRule for FormatExprList { ctx: _, } = item; + let comments = f.context().comments().clone(); + let dangling = comments.dangling_comments(item.into()); + + // The empty list is special because there can be dangling comments, and they can be in two + // positions: + // ```python + // a3 = [ # end-of-line + // # own line + // ] + // ``` + // In all other cases comments get assigned to a list element + if elts.is_empty() { + let end_of_line_split = dangling + .partition_point(|comment| comment.position() == CommentTextPosition::EndOfLine); + debug_assert!(dangling[end_of_line_split..] + .iter() + .all(|comment| comment.position() == CommentTextPosition::OwnLine)); + return write!( + f, + [group(&format_args![ + text("["), + dangling_comments(&dangling[..end_of_line_split]), + soft_block_indent(&dangling_comments(&dangling[end_of_line_split..])), + text("]") + ])] + ); + } + + debug_assert!( + dangling.is_empty(), + "A non-empty expression list has dangling comments" + ); + let items = format_with(|f| { let mut iter = elts.iter(); @@ -36,14 +69,10 @@ impl FormatNodeRule for FormatExprList { Ok(()) }); - let comments = f.context().comments().clone(); - let dangling = comments.dangling_comments(item.into()); - write!( f, [group(&format_args![ text("["), - dangling_comments(dangling), soft_block_indent(&items), text("]") ])] diff --git a/crates/ruff_python_formatter/src/other/arguments.rs b/crates/ruff_python_formatter/src/other/arguments.rs index 19f8107f00..9e0e88e214 100644 --- a/crates/ruff_python_formatter/src/other/arguments.rs +++ b/crates/ruff_python_formatter/src/other/arguments.rs @@ -58,6 +58,9 @@ impl FormatNodeRule for FormatArguments { last_node = Some(default.map_or_else(|| argument.into(), AnyNodeRef::from)); } + // kw only args need either a `*args` ahead of them capturing all var args or a `*` + // pseudo-argument capturing all fields. We can also have `*args` without any kwargs + // afterwards. if let Some(vararg) = vararg { joiner.entry(&format_args![ leading_node_comments(vararg.as_ref()), @@ -65,6 +68,8 @@ impl FormatNodeRule for FormatArguments { vararg.format() ]); last_node = Some(vararg.as_any_node_ref()); + } else if !kwonlyargs.is_empty() { + joiner.entry(&text("*")); } debug_assert!(defaults.next().is_none()); diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap index 7a3cedbd0b..183ed478e1 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap @@ -222,7 +222,7 @@ d={'a':1, # Comment 1 # Comment 2 -@@ -18,109 +16,111 @@ +@@ -18,109 +16,112 @@ # fmt: off def func_no_args(): @@ -269,6 +269,7 @@ d={'a':1, + number: int, + no_annotation=None, + text: str = "NOT_YET_IMPLEMENTED_STRING", ++ *, + debug: bool = False, + **kwargs, +) -> str: @@ -393,7 +394,7 @@ d={'a':1, # fmt: off # hey, that won't work -@@ -130,13 +130,15 @@ +@@ -130,13 +131,15 @@ def on_and_off_broken(): @@ -414,7 +415,7 @@ d={'a':1, # fmt: on # fmt: off # ...but comments still get reformatted even though they should not be -@@ -145,80 +147,21 @@ +@@ -145,80 +148,21 @@ def long_lines(): if True: @@ -426,13 +427,11 @@ d={'a':1, - implicit_default=True, - ) - ) -+ NOT_IMPLEMENTED_call() - # fmt: off +- # fmt: off - a = ( - unnecessary_bracket() - ) -+ a = NOT_IMPLEMENTED_call() - # fmt: on +- # fmt: on - _type_comment_re = re.compile( - r""" - ^ @@ -453,9 +452,11 @@ d={'a':1, - ) - $ - """, -- # fmt: off ++ NOT_IMPLEMENTED_call() + # fmt: off - re.MULTILINE|re.VERBOSE -- # fmt: on ++ a = NOT_IMPLEMENTED_call() + # fmt: on - ) + _type_comment_re = NOT_IMPLEMENTED_call() @@ -550,6 +551,7 @@ def function_signature_stress_test( number: int, no_annotation=None, text: str = "NOT_YET_IMPLEMENTED_STRING", + *, debug: bool = False, **kwargs, ) -> str: diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap index 1ae6f9a66b..9f5f58ae76 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap @@ -126,7 +126,7 @@ def __await__(): return (yield) def func_no_args(): -@@ -14,135 +13,86 @@ +@@ -14,135 +13,87 @@ b c if True: @@ -161,8 +161,8 @@ def __await__(): return (yield) number: int, no_annotation=None, - text: str = "default", -- *, + text: str = "NOT_YET_IMPLEMENTED_STRING", + *, debug: bool = False, **kwargs, ) -> str: @@ -339,6 +339,7 @@ def function_signature_stress_test( number: int, no_annotation=None, text: str = "NOT_YET_IMPLEMENTED_STRING", + *, debug: bool = False, **kwargs, ) -> str: diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_expression_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap similarity index 100% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_expression_py.snap rename to crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__list_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__list_py.snap new file mode 100644 index 0000000000..6ecef01df1 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__list_py.snap @@ -0,0 +1,35 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +--- +## Input +```py +# Dangling comment placement in empty lists +# Regression test for https://github.com/python/cpython/blob/03160630319ca26dcbbad65225da4248e54c45ec/Tools/c-analyzer/c_analyzer/datafiles.py#L14-L16 +a1 = [ # a +] +a2 = [ # a + # b +] +a3 = [ + # b +] +``` + + + +## Output +```py +# Dangling comment placement in empty lists +# Regression test for https://github.com/python/cpython/blob/03160630319ca26dcbbad65225da4248e54c45ec/Tools/c-analyzer/c_analyzer/datafiles.py#L14-L16 +a1 = [ # a +] +a2 = [ # a + # b +] +a3 = [ + # b +] +``` + + diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__tuple_expression_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__tuple_py.snap similarity index 100% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__tuple_expression_py.snap rename to crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__tuple_py.snap diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__stmt_assign_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__assign_py.snap similarity index 100% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__stmt_assign_py.snap rename to crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__assign_py.snap diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__function_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__function_py.snap index 3d448396f6..ad063ab680 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__function_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__function_py.snap @@ -74,6 +74,28 @@ def test(): ... # Comment def with_leading_comment(): ... + +# Comment that could be mistaken for a trailing comment of the function declaration when +# looking from the position of the if +# Regression test for https://github.com/python/cpython/blob/ad56340b665c5d8ac1f318964f71697bba41acb7/Lib/logging/__init__.py#L253-L260 +if True: + def f1(): + pass # a +else: + pass + +# Here it's actually a trailing comment +if True: + def f2(): + pass + # a +else: + pass + +# Make sure the star is printed +# Regression test for https://github.com/python/cpython/blob/7199584ac8632eab57612f595a7162ab8d2ebbc0/Lib/warnings.py#L513 +def f(arg1=1, *, kwonlyarg1, kwonlyarg2=2): + pass ``` @@ -177,6 +199,30 @@ def test(): # Comment def with_leading_comment(): ... + + +# Comment that could be mistaken for a trailing comment of the function declaration when +# looking from the position of the if +# Regression test for https://github.com/python/cpython/blob/ad56340b665c5d8ac1f318964f71697bba41acb7/Lib/logging/__init__.py#L253-L260 +if True: + def f1(): + pass # a +else: + pass + +# Here it's actually a trailing comment +if True: + def f2(): + pass + # a +else: + pass + + +# Make sure the star is printed +# Regression test for https://github.com/python/cpython/blob/7199584ac8632eab57612f595a7162ab8d2ebbc0/Lib/warnings.py#L513 +def f(arg1=1, *, kwonlyarg1, kwonlyarg2=2): + pass ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_statement_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap similarity index 100% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_statement_py.snap rename to crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_comma.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_comma.snap index aade2db2c9..38d1fed60a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_comma.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_comma.snap @@ -1,6 +1,6 @@ --- source: crates/ruff_python_formatter/src/trivia.rs -expression: tokens +expression: test_case.tokens() --- [ Token { diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_continuation.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_continuation.snap index b537ae611c..83079fe81a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_continuation.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_continuation.snap @@ -1,6 +1,6 @@ --- source: crates/ruff_python_formatter/src/trivia.rs -expression: tokens +expression: test_case.tokens() --- [ Token { diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_parentheses.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_parentheses.snap index f9de9526ae..ccd6969c2d 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_parentheses.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_parentheses.snap @@ -1,6 +1,6 @@ --- source: crates/ruff_python_formatter/src/trivia.rs -expression: tokens +expression: test_case.tokens() --- [ Token { diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_substring.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_substring.snap index 747d504c4b..181b438c3f 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_substring.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_substring.snap @@ -1,6 +1,6 @@ --- source: crates/ruff_python_formatter/src/trivia.rs -expression: tokens +expression: test_case.tokens() --- [ Token { diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_trivia.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_trivia.snap index 685a032be7..f1d708d6cb 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_trivia.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_trivia.snap @@ -1,6 +1,6 @@ --- source: crates/ruff_python_formatter/src/trivia.rs -expression: tokens +expression: test_case.tokens() --- [ Token { From 5ea3e42513ee68e0b305acfa79f28b84e1218dc4 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 15 Jun 2023 14:43:19 -0400 Subject: [PATCH 073/447] Always use identifier ranges to store bindings (#5110) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary At present, when we store a binding, we include a `TextRange` alongside it. The `TextRange` _sometimes_ matches the exact range of the identifier to which the `Binding` is linked, but... not always. For example, given: ```python x = 1 ``` The binding we create _will_ use the range of `x`, because the left-hand side is an `Expr::Name`, which has a valid range on it. However, given: ```python try: pass except ValueError as e: pass ``` When we create a binding for `e`, we don't have a `TextRange`... The AST doesn't give us one. So we end up extracting it via lexing. This PR extends that pattern to the rest of the binding kinds, to ensure that whenever we create a binding, we always use the range of the bound name. This leads to better diagnostics in cases like pattern matching, whereby the diagnostic for "unused variable `x`" here used to include `*x`, instead of just `x`: ```python def f(provided: int) -> int: match provided: case [_, *x]: pass ``` This is _also_ required for symbol renames, since we track writes as bindings -- so we need to know the ranges of the bound symbols. By storing these bindings precisely, we can also remove the `binding.trimmed_range` abstraction -- since bindings already use the "trimmed range". To implement this behavior, I took some of our existing utilities (like the code we had for `except ValueError as e` above), migrated them from a full lexer to a zero-allocation lexer that _only_ identifies "identifiers", and moved the behavior into a trait, so we can now do `stmt.identifier(locator)` to get the range for the identifier. Honestly, we might end up discarding much of this if we decide to put ranges on all identifiers (https://github.com/astral-sh/RustPython-Parser/pull/8). But even if we do, this will _still_ be a good change, because the lexer introduced here is useful beyond names (e.g., we use it find the `except` keyword in an exception handler, to find the `else` after a `for` loop, and so on). So, I'm fine committing this even if we end up changing our minds about the right approach. Closes #5090. ## Benchmarks No significant change, with one statistically significant improvement (-2.1654% on `linter/all-rules/large/dataset.py`): ``` linter/default-rules/numpy/globals.py time: [73.922 µs 73.955 µs 73.986 µs] thrpt: [39.882 MiB/s 39.898 MiB/s 39.916 MiB/s] change: time: [-0.5579% -0.4732% -0.3980%] (p = 0.00 < 0.05) thrpt: [+0.3996% +0.4755% +0.5611%] Change within noise threshold. Found 6 outliers among 100 measurements (6.00%) 4 (4.00%) low severe 1 (1.00%) low mild 1 (1.00%) high mild linter/default-rules/pydantic/types.py time: [1.4909 ms 1.4917 ms 1.4926 ms] thrpt: [17.087 MiB/s 17.096 MiB/s 17.106 MiB/s] change: time: [+0.2140% +0.2741% +0.3392%] (p = 0.00 < 0.05) thrpt: [-0.3380% -0.2734% -0.2136%] Change within noise threshold. Found 4 outliers among 100 measurements (4.00%) 3 (3.00%) high mild 1 (1.00%) high severe linter/default-rules/numpy/ctypeslib.py time: [688.97 µs 691.34 µs 694.15 µs] thrpt: [23.988 MiB/s 24.085 MiB/s 24.168 MiB/s] change: time: [-1.3282% -0.7298% -0.1466%] (p = 0.02 < 0.05) thrpt: [+0.1468% +0.7351% +1.3461%] Change within noise threshold. Found 15 outliers among 100 measurements (15.00%) 1 (1.00%) low mild 2 (2.00%) high mild 12 (12.00%) high severe linter/default-rules/large/dataset.py time: [3.3872 ms 3.4032 ms 3.4191 ms] thrpt: [11.899 MiB/s 11.954 MiB/s 12.011 MiB/s] change: time: [-0.6427% -0.2635% +0.0906%] (p = 0.17 > 0.05) thrpt: [-0.0905% +0.2642% +0.6469%] No change in performance detected. Found 20 outliers among 100 measurements (20.00%) 1 (1.00%) low severe 2 (2.00%) low mild 4 (4.00%) high mild 13 (13.00%) high severe linter/all-rules/numpy/globals.py time: [148.99 µs 149.21 µs 149.42 µs] thrpt: [19.748 MiB/s 19.776 MiB/s 19.805 MiB/s] change: time: [-0.7340% -0.5068% -0.2778%] (p = 0.00 < 0.05) thrpt: [+0.2785% +0.5094% +0.7395%] Change within noise threshold. Found 2 outliers among 100 measurements (2.00%) 1 (1.00%) low mild 1 (1.00%) high severe linter/all-rules/pydantic/types.py time: [3.0362 ms 3.0396 ms 3.0441 ms] thrpt: [8.3779 MiB/s 8.3903 MiB/s 8.3997 MiB/s] change: time: [-0.0957% +0.0618% +0.2125%] (p = 0.45 > 0.05) thrpt: [-0.2121% -0.0618% +0.0958%] No change in performance detected. Found 11 outliers among 100 measurements (11.00%) 1 (1.00%) low severe 3 (3.00%) low mild 5 (5.00%) high mild 2 (2.00%) high severe linter/all-rules/numpy/ctypeslib.py time: [1.6879 ms 1.6894 ms 1.6909 ms] thrpt: [9.8478 MiB/s 9.8562 MiB/s 9.8652 MiB/s] change: time: [-0.2279% -0.0888% +0.0436%] (p = 0.18 > 0.05) thrpt: [-0.0435% +0.0889% +0.2284%] No change in performance detected. Found 5 outliers among 100 measurements (5.00%) 4 (4.00%) low mild 1 (1.00%) high severe linter/all-rules/large/dataset.py time: [7.1520 ms 7.1586 ms 7.1654 ms] thrpt: [5.6777 MiB/s 5.6831 MiB/s 5.6883 MiB/s] change: time: [-2.5626% -2.1654% -1.7780%] (p = 0.00 < 0.05) thrpt: [+1.8102% +2.2133% +2.6300%] Performance has improved. Found 2 outliers among 100 measurements (2.00%) 1 (1.00%) low mild 1 (1.00%) high mild ``` --- .../test/fixtures/pyflakes/F841_3.py | 30 + crates/ruff/src/checkers/ast/mod.rs | 44 +- .../flake8_annotations/rules/definition.rs | 15 +- .../rules/abstract_base_class.rs | 4 +- .../rules/f_string_docstring.rs | 4 +- .../ruff/src/rules/flake8_builtins/helpers.rs | 4 +- .../flake8_pyi/rules/non_self_return_type.rs | 13 +- .../rules/str_or_repr_defined_in_stub.rs | 4 +- .../rules/stub_body_multiple_statements.rs | 4 +- .../flake8_pytest_style/rules/fixture.rs | 7 +- .../rules/no_slots_in_namedtuple_subclass.rs | 4 +- .../rules/no_slots_in_str_subclass.rs | 4 +- .../rules/no_slots_in_tuple_subclass.rs | 5 +- .../runtime_import_in_type_checking_block.rs | 27 +- .../rules/typing_only_runtime_import.rs | 14 +- ...ype_checking__tests__no_typing_import.snap | 4 +- ...__flake8_type_checking__tests__strict.snap | 8 +- ...ests__type_checking_block_after_usage.snap | 4 +- ...g__tests__type_checking_block_comment.snap | 4 +- ...ng__tests__type_checking_block_inline.snap | 4 +- ...__tests__type_checking_block_own_line.snap | 4 +- ...ing-only-third-party-import_TCH002.py.snap | 24 +- ...t_runtime_evaluated_base_classes_2.py.snap | 4 +- ...s__typing_import_after_package_import.snap | 4 +- ...ing__tests__typing_import_after_usage.snap | 4 +- ...__typing_import_before_package_import.snap | 4 +- .../mccabe/rules/function_is_too_complex.rs | 4 +- .../pep8_naming/rules/dunder_function_name.rs | 4 +- .../rules/error_suffix_on_exception_name.rs | 4 +- .../pep8_naming/rules/invalid_class_name.rs | 4 +- .../rules/invalid_function_name.rs | 4 +- .../rules/pycodestyle/rules/bare_except.rs | 4 +- .../src/rules/pydocstyle/rules/if_needed.rs | 4 +- .../src/rules/pydocstyle/rules/not_missing.rs | 16 +- .../src/rules/pydocstyle/rules/sections.rs | 4 +- .../pyflakes/rules/default_except_not_last.rs | 4 +- .../src/rules/pyflakes/rules/unused_import.rs | 14 +- ...ules__pyflakes__tests__F401_F401_0.py.snap | 2 +- ...ules__pyflakes__tests__F401_F401_5.py.snap | 10 +- ...ules__pyflakes__tests__F401_F401_6.py.snap | 8 +- ...ules__pyflakes__tests__F811_F811_1.py.snap | 4 +- ...ules__pyflakes__tests__F811_F811_2.py.snap | 4 +- ...les__pyflakes__tests__F811_F811_23.py.snap | 4 +- ...ules__pyflakes__tests__F841_F841_3.py.snap | 66 ++ .../pylint/rules/property_with_parameters.rs | 4 +- .../rules/pylint/rules/too_many_arguments.rs | 4 +- .../rules/pylint/rules/too_many_branches.rs | 4 +- .../rules/too_many_return_statements.rs | 5 +- .../rules/pylint/rules/too_many_statements.rs | 4 +- .../unexpected_special_method_signature.rs | 4 +- .../pylint/rules/useless_else_on_loop.rs | 4 +- .../rules/unnecessary_class_parentheses.rs | 4 +- .../rules/useless_object_inheritance.rs | 4 +- crates/ruff_python_ast/src/helpers.rs | 488 +++----------- crates/ruff_python_ast/src/identifier.rs | 621 ++++++++++++++++++ crates/ruff_python_ast/src/lib.rs | 1 + crates/ruff_python_formatter/src/trivia.rs | 1 + crates/ruff_python_semantic/src/binding.rs | 14 - 58 files changed, 1001 insertions(+), 576 deletions(-) create mode 100644 crates/ruff_python_ast/src/identifier.rs diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F841_3.py b/crates/ruff/resources/test/fixtures/pyflakes/F841_3.py index 28d5af1f3b..bd2a3f2e02 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F841_3.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F841_3.py @@ -108,3 +108,33 @@ def f(): def f(): toplevel = tt = 1 + + +def f(provided: int) -> int: + match provided: + case [_, *x]: + pass + + +def f(provided: int) -> int: + match provided: + case x: + pass + + +def f(provided: int) -> int: + match provided: + case Foo(bar) as x: + pass + + +def f(provided: int) -> int: + match provided: + case {"foo": 0, **x}: + pass + + +def f(provided: int) -> int: + match provided: + case {**x}: + pass diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 8e0ce79375..6dd4cc885c 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -12,12 +12,13 @@ use rustpython_parser::ast::{ use ruff_diagnostics::{Diagnostic, Fix, IsolationLevel}; use ruff_python_ast::all::{extract_all_names, AllNamesFlags}; use ruff_python_ast::helpers::{extract_handled_exceptions, to_module_path}; +use ruff_python_ast::identifier::{Identifier, TryIdentifier}; use ruff_python_ast::source_code::{Generator, Indexer, Locator, Quote, Stylist}; use ruff_python_ast::str::trailing_quote; use ruff_python_ast::types::Node; use ruff_python_ast::typing::{parse_type_annotation, AnnotationKind}; use ruff_python_ast::visitor::{walk_excepthandler, walk_pattern, Visitor}; -use ruff_python_ast::{cast, helpers, str, visitor}; +use ruff_python_ast::{cast, helpers, identifier, str, visitor}; use ruff_python_semantic::analyze::{branch_detection, typing, visibility}; use ruff_python_semantic::{ Binding, BindingFlags, BindingId, BindingKind, ContextualizedDefinition, Exceptions, @@ -250,7 +251,7 @@ where // Pre-visit. match stmt { Stmt::Global(ast::StmtGlobal { names, range: _ }) => { - let ranges: Vec = helpers::find_names(stmt, self.locator).collect(); + let ranges: Vec = identifier::names(stmt, self.locator).collect(); if !self.semantic.scope_id.is_global() { for (name, range) in names.iter().zip(ranges.iter()) { // Add a binding to the current scope. @@ -271,7 +272,7 @@ where } } Stmt::Nonlocal(ast::StmtNonlocal { names, range: _ }) => { - let ranges: Vec = helpers::find_names(stmt, self.locator).collect(); + let ranges: Vec = identifier::names(stmt, self.locator).collect(); if !self.semantic.scope_id.is_global() { for (name, range) in names.iter().zip(ranges.iter()) { // Add a binding to the current scope. @@ -367,7 +368,7 @@ where if self.enabled(Rule::AmbiguousFunctionName) { if let Some(diagnostic) = pycodestyle::rules::ambiguous_function_name(name, || { - helpers::identifier_range(stmt, self.locator) + stmt.identifier(self.locator) }) { self.diagnostics.push(diagnostic); @@ -692,7 +693,7 @@ where } if self.enabled(Rule::AmbiguousClassName) { if let Some(diagnostic) = pycodestyle::rules::ambiguous_class_name(name, || { - helpers::identifier_range(stmt, self.locator) + stmt.identifier(self.locator) }) { self.diagnostics.push(diagnostic); } @@ -808,7 +809,7 @@ where let name = alias.asname.as_ref().unwrap_or(&alias.name); self.add_binding( name, - alias.range(), + alias.identifier(self.locator), BindingKind::FutureImportation, BindingFlags::empty(), ); @@ -828,7 +829,7 @@ where let qualified_name = &alias.name; self.add_binding( name, - alias.range(), + alias.identifier(self.locator), BindingKind::SubmoduleImportation(SubmoduleImportation { qualified_name, }), @@ -839,7 +840,7 @@ where let qualified_name = &alias.name; self.add_binding( name, - alias.range(), + alias.identifier(self.locator), BindingKind::Importation(Importation { qualified_name }), if alias .asname @@ -1084,7 +1085,7 @@ where self.add_binding( name, - alias.range(), + alias.identifier(self.locator), BindingKind::FutureImportation, BindingFlags::empty(), ); @@ -1145,7 +1146,7 @@ where helpers::format_import_from_member(level, module, &alias.name); self.add_binding( name, - alias.range(), + alias.identifier(self.locator), BindingKind::FromImportation(FromImportation { qualified_name }), if alias .asname @@ -1871,7 +1872,7 @@ where self.add_binding( name, - stmt.range(), + stmt.identifier(self.locator), BindingKind::FunctionDefinition, BindingFlags::empty(), ); @@ -2094,7 +2095,7 @@ where self.semantic.pop_definition(); self.add_binding( name, - stmt.range(), + stmt.identifier(self.locator), BindingKind::ClassDefinition, BindingFlags::empty(), ); @@ -3902,8 +3903,7 @@ where } match name { Some(name) => { - let range = helpers::excepthandler_name_range(excepthandler, self.locator) - .expect("Failed to find `name` range"); + let range = excepthandler.try_identifier(self.locator).unwrap(); if self.enabled(Rule::AmbiguousVariableName) { if let Some(diagnostic) = @@ -4019,7 +4019,7 @@ where // upstream. self.add_binding( &arg.arg, - arg.range(), + arg.identifier(self.locator), BindingKind::Argument, BindingFlags::empty(), ); @@ -4059,7 +4059,7 @@ where { self.add_binding( name, - pattern.range(), + pattern.try_identifier(self.locator).unwrap(), BindingKind::Assignment, BindingFlags::empty(), ); @@ -4268,16 +4268,14 @@ impl<'a> Checker<'a> { { if self.enabled(Rule::RedefinedWhileUnused) { #[allow(deprecated)] - let line = self.locator.compute_line_index( - shadowed.trimmed_range(&self.semantic, self.locator).start(), - ); + let line = self.locator.compute_line_index(shadowed.range.start()); let mut diagnostic = Diagnostic::new( pyflakes::rules::RedefinedWhileUnused { name: name.to_string(), line, }, - binding.trimmed_range(&self.semantic, self.locator), + binding.range, ); if let Some(range) = binding.parent_range(&self.semantic) { diagnostic.set_parent(range.start()); @@ -4890,9 +4888,7 @@ impl<'a> Checker<'a> { } #[allow(deprecated)] - let line = self.locator.compute_line_index( - shadowed.trimmed_range(&self.semantic, self.locator).start(), - ); + let line = self.locator.compute_line_index(shadowed.range.start()); let binding = self.semantic.binding(binding_id); let mut diagnostic = Diagnostic::new( @@ -4900,7 +4896,7 @@ impl<'a> Checker<'a> { name: (*name).to_string(), line, }, - binding.trimmed_range(&self.semantic, self.locator), + binding.range, ); if let Some(range) = binding.parent_range(&self.semantic) { diagnostic.set_parent(range.start()); diff --git a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs index a5313ec9be..e839a092f7 100644 --- a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs @@ -2,9 +2,10 @@ use rustpython_parser::ast::{Expr, Ranged, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::cast; use ruff_python_ast::helpers::ReturnStatementVisitor; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::statement_visitor::StatementVisitor; -use ruff_python_ast::{cast, helpers}; use ruff_python_semantic::analyze::visibility; use ruff_python_semantic::{Definition, Member, MemberKind, SemanticModel}; use ruff_python_stdlib::typing::SIMPLE_MAGIC_RETURN_TYPES; @@ -640,7 +641,7 @@ pub(crate) fn definition( MissingReturnTypeClassMethod { name: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } else if is_method @@ -651,7 +652,7 @@ pub(crate) fn definition( MissingReturnTypeStaticMethod { name: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } else if is_method && visibility::is_init(name) { @@ -663,7 +664,7 @@ pub(crate) fn definition( MissingReturnTypeSpecialMethod { name: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), ); if checker.patch(diagnostic.kind.rule()) { #[allow(deprecated)] @@ -680,7 +681,7 @@ pub(crate) fn definition( MissingReturnTypeSpecialMethod { name: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), ); let return_type = SIMPLE_MAGIC_RETURN_TYPES.get(name); if let Some(return_type) = return_type { @@ -701,7 +702,7 @@ pub(crate) fn definition( MissingReturnTypeUndocumentedPublicFunction { name: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } @@ -711,7 +712,7 @@ pub(crate) fn definition( MissingReturnTypePrivateFunction { name: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } 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 6868c61673..16606d08ff 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 @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::{is_abstract, is_overload}; use ruff_python_semantic::SemanticModel; @@ -134,7 +134,7 @@ pub(crate) fn abstract_base_class( AbstractBaseClassWithoutAbstractMethod { name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs b/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs index 5453b71569..8556160ba5 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; @@ -31,6 +31,6 @@ pub(crate) fn f_string_docstring(checker: &mut Checker, body: &[Stmt]) { }; checker.diagnostics.push(Diagnostic::new( FStringDocstring, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } diff --git a/crates/ruff/src/rules/flake8_builtins/helpers.rs b/crates/ruff/src/rules/flake8_builtins/helpers.rs index d350c11447..5b6c45761e 100644 --- a/crates/ruff/src/rules/flake8_builtins/helpers.rs +++ b/crates/ruff/src/rules/flake8_builtins/helpers.rs @@ -1,7 +1,7 @@ use ruff_text_size::TextRange; use rustpython_parser::ast::{Excepthandler, Expr, Ranged, Stmt}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; use ruff_python_stdlib::builtins::BUILTINS; @@ -20,7 +20,7 @@ impl AnyShadowing<'_> { pub(crate) fn range(self, locator: &Locator) -> TextRange { match self { AnyShadowing::Expression(expr) => expr.range(), - AnyShadowing::Statement(stmt) => identifier_range(stmt, locator), + AnyShadowing::Statement(stmt) => stmt.identifier(locator), AnyShadowing::ExceptHandler(handler) => handler.range(), } } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs b/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs index 5a3bb60e74..78155686c9 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs @@ -2,7 +2,8 @@ use rustpython_parser::ast::{self, Arguments, Decorator, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::{identifier_range, map_subscript}; +use ruff_python_ast::helpers::map_subscript; +use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::{is_abstract, is_final, is_overload}; use ruff_python_semantic::{ScopeKind, SemanticModel}; @@ -147,7 +148,7 @@ pub(crate) fn non_self_return_type( class_name: class_def.name.to_string(), method_name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } return; @@ -161,7 +162,7 @@ pub(crate) fn non_self_return_type( class_name: class_def.name.to_string(), method_name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } return; @@ -176,7 +177,7 @@ pub(crate) fn non_self_return_type( class_name: class_def.name.to_string(), method_name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } return; @@ -192,7 +193,7 @@ pub(crate) fn non_self_return_type( class_name: class_def.name.to_string(), method_name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } @@ -205,7 +206,7 @@ pub(crate) fn non_self_return_type( class_name: class_def.name.to_string(), method_name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } 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 0d40ff8f9f..3ae9eab2f8 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 @@ -3,7 +3,7 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::is_abstract; use crate::autofix::edits::delete_stmt; @@ -90,7 +90,7 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) { StrOrReprDefinedInStub { name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), ); if checker.patch(diagnostic.kind.rule()) { let stmt = checker.semantic().stmt(); diff --git a/crates/ruff/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs b/crates/ruff/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs index 3ab3613237..bfc0e70857 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs @@ -2,8 +2,8 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers; use ruff_python_ast::helpers::is_docstring_stmt; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; @@ -32,6 +32,6 @@ pub(crate) fn stub_body_multiple_statements(checker: &mut Checker, stmt: &Stmt, checker.diagnostics.push(Diagnostic::new( StubBodyMultipleStatements, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } 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 3825e9929e..760aa8f4b6 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs @@ -9,10 +9,11 @@ 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::identifier::Identifier; use ruff_python_ast::prelude::Decorator; use ruff_python_ast::source_code::Locator; +use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; -use ruff_python_ast::{helpers, visitor}; use ruff_python_semantic::analyze::visibility::is_abstract; use ruff_python_semantic::SemanticModel; @@ -378,7 +379,7 @@ fn check_fixture_returns(checker: &mut Checker, stmt: &Stmt, name: &str, body: & PytestIncorrectFixtureNameUnderscore { function: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } else if checker.enabled(Rule::PytestMissingFixtureNameUnderscore) && !visitor.has_return_with_value @@ -389,7 +390,7 @@ fn check_fixture_returns(checker: &mut Checker, stmt: &Stmt, name: &str, body: & PytestMissingFixtureNameUnderscore { function: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs index c43f9f35fd..3a56fc07c6 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs @@ -3,7 +3,7 @@ use rustpython_parser::ast::{Expr, StmtClassDef}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::prelude::Stmt; use crate::checkers::ast::Checker; @@ -77,7 +77,7 @@ pub(crate) fn no_slots_in_namedtuple_subclass( if !has_slots(&class.body) { checker.diagnostics.push(Diagnostic::new( NoSlotsInNamedtupleSubclass, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs index 8c0ae80b90..a1a5182c61 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{Stmt, StmtClassDef}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; use crate::rules::flake8_slots::rules::helpers::has_slots; @@ -61,7 +61,7 @@ pub(crate) fn no_slots_in_str_subclass(checker: &mut Checker, stmt: &Stmt, class if !has_slots(&class.body) { checker.diagnostics.push(Diagnostic::new( NoSlotsInStrSubclass, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs index a4705c2e25..c57cd510b1 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs @@ -2,7 +2,8 @@ use rustpython_parser::ast::{Stmt, StmtClassDef}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::{identifier_range, map_subscript}; +use ruff_python_ast::helpers::map_subscript; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; use crate::rules::flake8_slots::rules::helpers::has_slots; @@ -64,7 +65,7 @@ pub(crate) fn no_slots_in_tuple_subclass(checker: &mut Checker, stmt: &Stmt, cla if !has_slots(&class.body) { checker.diagnostics.push(Diagnostic::new( NoSlotsInTupleSubclass, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs index b9ffa0c236..6863584901 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs @@ -99,17 +99,18 @@ pub(crate) fn runtime_import_in_type_checking_block( let import = Import { qualified_name, reference_id, - trimmed_range: binding.trimmed_range(checker.semantic(), checker.locator), + range: binding.range, parent_range: binding.parent_range(checker.semantic()), }; - if checker.rule_is_ignored( - Rule::RuntimeImportInTypeCheckingBlock, - import.trimmed_range.start(), - ) || import.parent_range.map_or(false, |parent_range| { - checker - .rule_is_ignored(Rule::RuntimeImportInTypeCheckingBlock, parent_range.start()) - }) { + if checker.rule_is_ignored(Rule::RuntimeImportInTypeCheckingBlock, import.range.start()) + || import.parent_range.map_or(false, |parent_range| { + checker.rule_is_ignored( + Rule::RuntimeImportInTypeCheckingBlock, + parent_range.start(), + ) + }) + { ignores_by_statement .entry(stmt_id) .or_default() @@ -131,7 +132,7 @@ pub(crate) fn runtime_import_in_type_checking_block( for Import { qualified_name, - trimmed_range, + range, parent_range, .. } in imports @@ -140,7 +141,7 @@ pub(crate) fn runtime_import_in_type_checking_block( RuntimeImportInTypeCheckingBlock { qualified_name: qualified_name.to_string(), }, - trimmed_range, + range, ); if let Some(range) = parent_range { diagnostic.set_parent(range.start()); @@ -156,7 +157,7 @@ pub(crate) fn runtime_import_in_type_checking_block( // suppression comments aren't marked as unused. for Import { qualified_name, - trimmed_range, + range, parent_range, .. } in ignores_by_statement.into_values().flatten() @@ -165,7 +166,7 @@ pub(crate) fn runtime_import_in_type_checking_block( RuntimeImportInTypeCheckingBlock { qualified_name: qualified_name.to_string(), }, - trimmed_range, + range, ); if let Some(range) = parent_range { diagnostic.set_parent(range.start()); @@ -181,7 +182,7 @@ struct Import<'a> { /// The first reference to the imported symbol. reference_id: ReferenceId, /// The trimmed range of the import (e.g., `List` in `from typing import List`). - trimmed_range: TextRange, + range: TextRange, /// The range of the import's parent statement. parent_range: Option, } diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index 722a6bfbce..34e6eccbdf 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -278,11 +278,11 @@ pub(crate) fn typing_only_runtime_import( let import = Import { qualified_name, reference_id, - trimmed_range: binding.trimmed_range(checker.semantic(), checker.locator), + range: binding.range, parent_range: binding.parent_range(checker.semantic()), }; - if checker.rule_is_ignored(rule_for(import_type), import.trimmed_range.start()) + if checker.rule_is_ignored(rule_for(import_type), import.range.start()) || import.parent_range.map_or(false, |parent_range| { checker.rule_is_ignored(rule_for(import_type), parent_range.start()) }) @@ -311,14 +311,14 @@ pub(crate) fn typing_only_runtime_import( for Import { qualified_name, - trimmed_range, + range, parent_range, .. } in imports { let mut diagnostic = Diagnostic::new( diagnostic_for(import_type, qualified_name.to_string()), - trimmed_range, + range, ); if let Some(range) = parent_range { diagnostic.set_parent(range.start()); @@ -335,14 +335,14 @@ pub(crate) fn typing_only_runtime_import( for ((_, import_type), imports) in ignores_by_statement { for Import { qualified_name, - trimmed_range, + range, parent_range, .. } in imports { let mut diagnostic = Diagnostic::new( diagnostic_for(import_type, qualified_name.to_string()), - trimmed_range, + range, ); if let Some(range) = parent_range { diagnostic.set_parent(range.start()); @@ -359,7 +359,7 @@ struct Import<'a> { /// The first reference to the imported symbol. reference_id: ReferenceId, /// The trimmed range of the import (e.g., `List` in `from typing import List`). - trimmed_range: TextRange, + range: TextRange, /// The range of the import's parent statement. parent_range: Option, } diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__no_typing_import.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__no_typing_import.snap index 49b96c6624..51e4ecc7c4 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__no_typing_import.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__no_typing_import.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:4:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:4:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 2 | from __future__ import annotations 3 | 4 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 5 | 6 | def f(x: pd.DataFrame): | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap index c969cd7bc4..9ef1a60a95 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap @@ -117,12 +117,12 @@ strict.py:62:12: TCH002 [*] Move third-party import `pkg` into a type-checking b 64 67 | 65 68 | def test(value: pkg.A): -strict.py:71:12: TCH002 [*] Move third-party import `pkg.foo` into a type-checking block +strict.py:71:23: TCH002 [*] Move third-party import `pkg.foo` into a type-checking block | 69 | def f(): 70 | # In un-strict mode, this shouldn't raise an error, since `pkg.foo.bar` is used at runtime. 71 | import pkg.foo as F - | ^^^^^^^^^^^^ TCH002 + | ^ TCH002 72 | import pkg.foo.bar as B | = help: Move into type-checking block @@ -201,12 +201,12 @@ strict.py:91:12: TCH002 [*] Move third-party import `pkg` into a type-checking b 93 96 | 94 97 | def test(value: pkg.A): -strict.py:101:12: TCH002 [*] Move third-party import `pkg.foo` into a type-checking block +strict.py:101:23: TCH002 [*] Move third-party import `pkg.foo` into a type-checking block | 99 | # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. 100 | import pkg.bar as B 101 | import pkg.foo as F - | ^^^^^^^^^^^^ TCH002 + | ^ TCH002 102 | 103 | def test(value: F.Foo): | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_after_usage.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_after_usage.snap index d208730b97..8eb591e7db 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_after_usage.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_after_usage.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:6:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:6:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 4 | from typing import TYPE_CHECKING 5 | 6 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 7 | 8 | def f(x: pd.DataFrame): | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_comment.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_comment.snap index e186f50adb..43ed4cbe88 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_comment.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_comment.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:6:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:6:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 4 | from typing import TYPE_CHECKING 5 | 6 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 7 | 8 | if TYPE_CHECKING: | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_inline.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_inline.snap index 5fa5f507ca..983345aae9 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_inline.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_inline.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:6:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:6:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 4 | from typing import TYPE_CHECKING 5 | 6 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 7 | 8 | if TYPE_CHECKING: import os | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_own_line.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_own_line.snap index 3e490001a0..4246c582dc 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_own_line.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_own_line.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:6:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:6:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 4 | from typing import TYPE_CHECKING 5 | 6 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 7 | 8 | if TYPE_CHECKING: | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap index 19eb823d73..7005762cc5 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap @@ -1,11 +1,11 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -TCH002.py:5:12: TCH002 [*] Move third-party import `pandas` into a type-checking block +TCH002.py:5:22: TCH002 [*] Move third-party import `pandas` into a type-checking block | 4 | def f(): 5 | import pandas as pd # TCH002 - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 6 | 7 | x: pd.DataFrame | @@ -53,11 +53,11 @@ TCH002.py:11:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a ty 13 16 | x: DataFrame 14 17 | -TCH002.py:17:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block +TCH002.py:17:37: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block | 16 | def f(): 17 | from pandas import DataFrame as df # TCH002 - | ^^^^^^^^^^^^^^^ TCH002 + | ^^ TCH002 18 | 19 | x: df | @@ -81,11 +81,11 @@ TCH002.py:17:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a ty 19 22 | x: df 20 23 | -TCH002.py:23:12: TCH002 [*] Move third-party import `pandas` into a type-checking block +TCH002.py:23:22: TCH002 [*] Move third-party import `pandas` into a type-checking block | 22 | def f(): 23 | import pandas as pd # TCH002 - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 24 | 25 | x: pd.DataFrame = 1 | @@ -137,11 +137,11 @@ TCH002.py:29:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a ty 31 34 | x: DataFrame = 2 32 35 | -TCH002.py:35:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block +TCH002.py:35:37: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block | 34 | def f(): 35 | from pandas import DataFrame as df # TCH002 - | ^^^^^^^^^^^^^^^ TCH002 + | ^^ TCH002 36 | 37 | x: df = 3 | @@ -165,11 +165,11 @@ TCH002.py:35:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a ty 37 40 | x: df = 3 38 41 | -TCH002.py:41:12: TCH002 [*] Move third-party import `pandas` into a type-checking block +TCH002.py:41:22: TCH002 [*] Move third-party import `pandas` into a type-checking block | 40 | def f(): 41 | import pandas as pd # TCH002 - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 42 | 43 | x: "pd.DataFrame" = 1 | @@ -193,11 +193,11 @@ TCH002.py:41:12: TCH002 [*] Move third-party import `pandas` into a type-checkin 43 46 | x: "pd.DataFrame" = 1 44 47 | -TCH002.py:47:12: TCH002 [*] Move third-party import `pandas` into a type-checking block +TCH002.py:47:22: TCH002 [*] Move third-party import `pandas` into a type-checking block | 46 | def f(): 47 | import pandas as pd # TCH002 - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 48 | 49 | x = dict["pd.DataFrame", "pd.DataFrame"] | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_base_classes_2.py.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_base_classes_2.py.snap index de8fe6ddd7..c90b345fe7 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_base_classes_2.py.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_base_classes_2.py.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -runtime_evaluated_base_classes_2.py:3:8: TCH002 [*] Move third-party import `geopandas` into a type-checking block +runtime_evaluated_base_classes_2.py:3:21: TCH002 [*] Move third-party import `geopandas` into a type-checking block | 1 | from __future__ import annotations 2 | 3 | import geopandas as gpd # TCH002 - | ^^^^^^^^^^^^^^^^ TCH002 + | ^^^ TCH002 4 | import pydantic 5 | import pyproj # TCH002 | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_package_import.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_package_import.snap index 27235f0f48..41b6a8f207 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_package_import.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_package_import.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:4:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:4:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 2 | from __future__ import annotations 3 | 4 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 5 | 6 | from typing import TYPE_CHECKING | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_usage.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_usage.snap index 4c598b4014..271f52f2df 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_usage.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_usage.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:4:8: TCH002 Move third-party import `pandas` into a type-checking block +:4:18: TCH002 Move third-party import `pandas` into a type-checking block | 2 | from __future__ import annotations 3 | 4 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 5 | 6 | def f(x: pd.DataFrame): | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_before_package_import.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_before_package_import.snap index 4530c20fc5..06752d7221 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_before_package_import.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_before_package_import.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:6:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:6:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 4 | from typing import TYPE_CHECKING 5 | 6 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 7 | 8 | def f(x: pd.DataFrame): | diff --git a/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs b/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs index 6393a65f3d..6b27ba7d62 100644 --- a/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs +++ b/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Excepthandler, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; /// ## What it does @@ -152,7 +152,7 @@ pub(crate) fn function_is_too_complex( complexity, max_complexity, }, - identifier_range(stmt, locator), + stmt.identifier(locator), )) } else { None diff --git a/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs b/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs index 6067a04839..4f248b2040 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; use ruff_python_semantic::{Scope, ScopeKind}; @@ -69,6 +69,6 @@ pub(crate) fn dunder_function_name( Some(Diagnostic::new( DunderFunctionName, - identifier_range(stmt, locator), + stmt.identifier(locator), )) } diff --git a/crates/ruff/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs b/crates/ruff/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs index 50fcb36a1e..182ba1e404 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; use crate::settings::types::IdentifierPattern; @@ -75,6 +75,6 @@ pub(crate) fn error_suffix_on_exception_name( ErrorSuffixOnExceptionName { name: name.to_string(), }, - identifier_range(class_def, locator), + class_def.identifier(locator), )) } diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_class_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_class_name.rs index 3c14cfa361..c6d7e72e99 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_class_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_class_name.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; use crate::settings::types::IdentifierPattern; @@ -69,7 +69,7 @@ pub(crate) fn invalid_class_name( InvalidClassName { name: name.to_string(), }, - identifier_range(class_def, locator), + class_def.identifier(locator), )); } None diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs index 35fdc13a02..b23556566e 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{Decorator, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; use ruff_python_semantic::analyze::visibility; use ruff_python_semantic::SemanticModel; @@ -81,6 +81,6 @@ pub(crate) fn invalid_function_name( InvalidFunctionName { name: name.to_string(), }, - identifier_range(stmt, locator), + stmt.identifier(locator), )) } diff --git a/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs b/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs index 0b9eee1598..457bdd6347 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Excepthandler, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::except_range; +use ruff_python_ast::identifier::except; use ruff_python_ast::source_code::Locator; /// ## What it does @@ -66,7 +66,7 @@ pub(crate) fn bare_except( .iter() .any(|stmt| matches!(stmt, Stmt::Raise(ast::StmtRaise { exc: None, .. }))) { - Some(Diagnostic::new(BareExcept, except_range(handler, locator))) + Some(Diagnostic::new(BareExcept, except(handler, locator))) } else { None } diff --git a/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs b/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs index c986fb6ac8..0f531d8d74 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs @@ -1,7 +1,7 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::cast; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::is_overload; use ruff_python_semantic::{Definition, Member, MemberKind}; @@ -32,6 +32,6 @@ pub(crate) fn if_needed(checker: &mut Checker, docstring: &Docstring) { } checker.diagnostics.push(Diagnostic::new( OverloadWithDocstring, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } diff --git a/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs b/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs index 7f2be87fff..1d78d99444 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs @@ -3,7 +3,7 @@ use ruff_text_size::TextRange; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::cast; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::{ is_call, is_init, is_magic, is_new, is_overload, is_override, Visibility, }; @@ -135,7 +135,7 @@ pub(crate) fn not_missing( if checker.enabled(Rule::UndocumentedPublicClass) { checker.diagnostics.push(Diagnostic::new( UndocumentedPublicClass, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } false @@ -148,7 +148,7 @@ pub(crate) fn not_missing( if checker.enabled(Rule::UndocumentedPublicNestedClass) { checker.diagnostics.push(Diagnostic::new( UndocumentedPublicNestedClass, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } false @@ -164,7 +164,7 @@ pub(crate) fn not_missing( if checker.enabled(Rule::UndocumentedPublicFunction) { checker.diagnostics.push(Diagnostic::new( UndocumentedPublicFunction, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } false @@ -183,7 +183,7 @@ pub(crate) fn not_missing( if checker.enabled(Rule::UndocumentedPublicInit) { checker.diagnostics.push(Diagnostic::new( UndocumentedPublicInit, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } true @@ -191,7 +191,7 @@ pub(crate) fn not_missing( if checker.enabled(Rule::UndocumentedPublicMethod) { checker.diagnostics.push(Diagnostic::new( UndocumentedPublicMethod, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } true @@ -199,7 +199,7 @@ pub(crate) fn not_missing( if checker.enabled(Rule::UndocumentedMagicMethod) { checker.diagnostics.push(Diagnostic::new( UndocumentedMagicMethod, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } true @@ -207,7 +207,7 @@ pub(crate) fn not_missing( if checker.enabled(Rule::UndocumentedPublicMethod) { checker.diagnostics.push(Diagnostic::new( UndocumentedPublicMethod, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } true diff --git a/crates/ruff/src/rules/pydocstyle/rules/sections.rs b/crates/ruff/src/rules/pydocstyle/rules/sections.rs index 30cbd6687c..26d3f06109 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/sections.rs @@ -10,7 +10,7 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::cast; use ruff_python_ast::docstrings::{clean_space, leading_space}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::is_staticmethod; use ruff_python_semantic::{Definition, Member, MemberKind}; use ruff_python_whitespace::NewlineWithTrailingNewline; @@ -759,7 +759,7 @@ fn missing_args(checker: &mut Checker, docstring: &Docstring, docstrings_args: & let names = missing_arg_names.into_iter().sorted().collect(); checker.diagnostics.push(Diagnostic::new( UndocumentedParam { names }, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } diff --git a/crates/ruff/src/rules/pyflakes/rules/default_except_not_last.rs b/crates/ruff/src/rules/pyflakes/rules/default_except_not_last.rs index e27c4bd1cd..26a760d2b7 100644 --- a/crates/ruff/src/rules/pyflakes/rules/default_except_not_last.rs +++ b/crates/ruff/src/rules/pyflakes/rules/default_except_not_last.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Excepthandler}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::except_range; +use ruff_python_ast::identifier::except; use ruff_python_ast::source_code::Locator; /// ## What it does @@ -63,7 +63,7 @@ pub(crate) fn default_except_not_last( if type_.is_none() && idx < handlers.len() - 1 { return Some(Diagnostic::new( DefaultExceptNotLast, - except_range(handler, locator), + except(handler, locator), )); } } diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs index 50898b388e..d004b6433c 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs @@ -117,11 +117,11 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut let import = Import { qualified_name, - trimmed_range: binding.trimmed_range(checker.semantic(), checker.locator), + range: binding.range, parent_range: binding.parent_range(checker.semantic()), }; - if checker.rule_is_ignored(Rule::UnusedImport, import.trimmed_range.start()) + if checker.rule_is_ignored(Rule::UnusedImport, import.range.start()) || import.parent_range.map_or(false, |parent_range| { checker.rule_is_ignored(Rule::UnusedImport, parent_range.start()) }) @@ -156,7 +156,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut for Import { qualified_name, - trimmed_range, + range, parent_range, } in imports { @@ -172,7 +172,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut }, multiple, }, - trimmed_range, + range, ); if let Some(range) = parent_range { diagnostic.set_parent(range.start()); @@ -190,7 +190,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut // suppression comments aren't marked as unused. for Import { qualified_name, - trimmed_range, + range, parent_range, } in ignored.into_values().flatten() { @@ -200,7 +200,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut context: None, multiple: false, }, - trimmed_range, + range, ); if let Some(range) = parent_range { diagnostic.set_parent(range.start()); @@ -214,7 +214,7 @@ struct Import<'a> { /// The qualified name of the import (e.g., `typing.List` for `from typing import List`). qualified_name: &'a str, /// The trimmed range of the import (e.g., `List` in `from typing import List`). - trimmed_range: TextRange, + range: TextRange, /// The range of the import's parent statement. parent_range: Option, } diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_0.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_0.py.snap index 67c97bb6ba..d7ce627b24 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_0.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_0.py.snap @@ -44,7 +44,7 @@ F401_0.py:12:8: F401 [*] `logging.handlers` imported but unused 10 | import multiprocessing.process 11 | import logging.config 12 | import logging.handlers - | ^^^^^^^^^^^^^^^^ F401 + | ^^^^^^^ F401 13 | from typing import ( 14 | TYPE_CHECKING, | diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_5.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_5.py.snap index 1c7f2d6cab..2800a738ed 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_5.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_5.py.snap @@ -18,12 +18,12 @@ F401_5.py:2:17: F401 [*] `a.b.c` imported but unused 4 3 | import h.i 5 4 | import j.k as l -F401_5.py:3:17: F401 [*] `d.e.f` imported but unused +F401_5.py:3:22: F401 [*] `d.e.f` imported but unused | 1 | """Test: removal of multi-segment and aliases imports.""" 2 | from a.b import c 3 | from d.e import f as g - | ^^^^^^ F401 + | ^ F401 4 | import h.i 5 | import j.k as l | @@ -41,7 +41,7 @@ F401_5.py:4:8: F401 [*] `h.i` imported but unused 2 | from a.b import c 3 | from d.e import f as g 4 | import h.i - | ^^^ F401 + | ^ F401 5 | import j.k as l | = help: Remove unused import: `h.i` @@ -53,12 +53,12 @@ F401_5.py:4:8: F401 [*] `h.i` imported but unused 4 |-import h.i 5 4 | import j.k as l -F401_5.py:5:8: F401 [*] `j.k` imported but unused +F401_5.py:5:15: F401 [*] `j.k` imported but unused | 3 | from d.e import f as g 4 | import h.i 5 | import j.k as l - | ^^^^^^^^ F401 + | ^ F401 | = help: Remove unused import: `j.k` diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_6.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_6.py.snap index bdfda37456..a6b21228c3 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_6.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_6.py.snap @@ -20,11 +20,11 @@ F401_6.py:7:25: F401 [*] `.background.BackgroundTasks` imported but unused 9 8 | # F401 `datastructures.UploadFile` imported but unused 10 9 | from .datastructures import UploadFile as FileUpload -F401_6.py:10:29: F401 [*] `.datastructures.UploadFile` imported but unused +F401_6.py:10:43: F401 [*] `.datastructures.UploadFile` imported but unused | 9 | # F401 `datastructures.UploadFile` imported but unused 10 | from .datastructures import UploadFile as FileUpload - | ^^^^^^^^^^^^^^^^^^^^^^^^ F401 + | ^^^^^^^^^^ F401 11 | 12 | # OK | @@ -58,11 +58,11 @@ F401_6.py:16:8: F401 [*] `background` imported but unused 18 17 | # F401 `datastructures` imported but unused 19 18 | import datastructures as structures -F401_6.py:19:8: F401 [*] `datastructures` imported but unused +F401_6.py:19:26: F401 [*] `datastructures` imported but unused | 18 | # F401 `datastructures` imported but unused 19 | import datastructures as structures - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F401 + | ^^^^^^^^^^ F401 | = help: Remove unused import: `datastructures` diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_1.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_1.py.snap index 6f08dc26a2..365bef1c04 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_1.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_1.py.snap @@ -1,10 +1,10 @@ --- source: crates/ruff/src/rules/pyflakes/mod.rs --- -F811_1.py:1:18: F811 Redefinition of unused `FU` from line 1 +F811_1.py:1:25: F811 Redefinition of unused `FU` from line 1 | 1 | import fu as FU, bar as FU - | ^^^^^^^^^ F811 + | ^^ F811 | diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_2.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_2.py.snap index 55c6dc663f..bf0275f23e 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_2.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_2.py.snap @@ -1,10 +1,10 @@ --- source: crates/ruff/src/rules/pyflakes/mod.rs --- -F811_2.py:1:27: F811 Redefinition of unused `FU` from line 1 +F811_2.py:1:34: F811 Redefinition of unused `FU` from line 1 | 1 | from moo import fu as FU, bar as FU - | ^^^^^^^^^ F811 + | ^^ F811 | diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_23.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_23.py.snap index 9a73c7bf9b..6f9198fc9d 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_23.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_23.py.snap @@ -1,11 +1,11 @@ --- source: crates/ruff/src/rules/pyflakes/mod.rs --- -F811_23.py:4:8: F811 Redefinition of unused `foo` from line 3 +F811_23.py:4:15: F811 Redefinition of unused `foo` from line 3 | 3 | import foo as foo 4 | import bar as foo - | ^^^^^^^^^^ F811 + | ^^^ F811 | diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_3.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_3.py.snap index fbf428003b..be49b03119 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_3.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_3.py.snap @@ -502,6 +502,9 @@ F841_3.py:110:5: F841 [*] Local variable `toplevel` is assigned to but never use 109 109 | def f(): 110 |- toplevel = tt = 1 110 |+ tt = 1 +111 111 | +112 112 | +113 113 | def f(provided: int) -> int: F841_3.py:110:16: F841 [*] Local variable `tt` is assigned to but never used | @@ -517,5 +520,68 @@ F841_3.py:110:16: F841 [*] Local variable `tt` is assigned to but never used 109 109 | def f(): 110 |- toplevel = tt = 1 110 |+ toplevel = 1 +111 111 | +112 112 | +113 113 | def f(provided: int) -> int: + +F841_3.py:115:19: F841 Local variable `x` is assigned to but never used + | +113 | def f(provided: int) -> int: +114 | match provided: +115 | case [_, *x]: + | ^ F841 +116 | pass + | + = help: Remove assignment to unused variable `x` + +F841_3.py:121:14: F841 Local variable `x` is assigned to but never used + | +119 | def f(provided: int) -> int: +120 | match provided: +121 | case x: + | ^ F841 +122 | pass + | + = help: Remove assignment to unused variable `x` + +F841_3.py:127:18: F841 Local variable `bar` is assigned to but never used + | +125 | def f(provided: int) -> int: +126 | match provided: +127 | case Foo(bar) as x: + | ^^^ F841 +128 | pass + | + = help: Remove assignment to unused variable `bar` + +F841_3.py:127:26: F841 Local variable `x` is assigned to but never used + | +125 | def f(provided: int) -> int: +126 | match provided: +127 | case Foo(bar) as x: + | ^ F841 +128 | pass + | + = help: Remove assignment to unused variable `x` + +F841_3.py:133:27: F841 Local variable `x` is assigned to but never used + | +131 | def f(provided: int) -> int: +132 | match provided: +133 | case {"foo": 0, **x}: + | ^ F841 +134 | pass + | + = help: Remove assignment to unused variable `x` + +F841_3.py:139:17: F841 Local variable `x` is assigned to but never used + | +137 | def f(provided: int) -> int: +138 | match provided: +139 | case {**x}: + | ^ F841 +140 | pass + | + = help: Remove assignment to unused variable `x` diff --git a/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs b/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs index aaded36e14..1029a9d97f 100644 --- a/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs +++ b/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Arguments, Decorator, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; @@ -70,7 +70,7 @@ pub(crate) fn property_with_parameters( { checker.diagnostics.push(Diagnostic::new( PropertyWithParameters, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } diff --git a/crates/ruff/src/rules/pylint/rules/too_many_arguments.rs b/crates/ruff/src/rules/pylint/rules/too_many_arguments.rs index bd06b69c99..2d050ee75c 100644 --- a/crates/ruff/src/rules/pylint/rules/too_many_arguments.rs +++ b/crates/ruff/src/rules/pylint/rules/too_many_arguments.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{Arguments, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; @@ -72,7 +72,7 @@ pub(crate) fn too_many_arguments(checker: &mut Checker, args: &Arguments, stmt: c_args: num_args, max_args: checker.settings.pylint.max_args, }, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } diff --git a/crates/ruff/src/rules/pylint/rules/too_many_branches.rs b/crates/ruff/src/rules/pylint/rules/too_many_branches.rs index 449a261d2b..8a9b0be78f 100644 --- a/crates/ruff/src/rules/pylint/rules/too_many_branches.rs +++ b/crates/ruff/src/rules/pylint/rules/too_many_branches.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Excepthandler, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; /// ## What it does @@ -175,7 +175,7 @@ pub(crate) fn too_many_branches( branches, max_branches, }, - identifier_range(stmt, locator), + stmt.identifier(locator), )) } else { None diff --git a/crates/ruff/src/rules/pylint/rules/too_many_return_statements.rs b/crates/ruff/src/rules/pylint/rules/too_many_return_statements.rs index 2a48517067..ef5e45d2d3 100644 --- a/crates/ruff/src/rules/pylint/rules/too_many_return_statements.rs +++ b/crates/ruff/src/rules/pylint/rules/too_many_return_statements.rs @@ -2,7 +2,8 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::{identifier_range, ReturnStatementVisitor}; +use ruff_python_ast::helpers::ReturnStatementVisitor; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; use ruff_python_ast::statement_visitor::StatementVisitor; @@ -89,7 +90,7 @@ pub(crate) fn too_many_return_statements( returns, max_returns, }, - identifier_range(stmt, locator), + stmt.identifier(locator), )) } else { None diff --git a/crates/ruff/src/rules/pylint/rules/too_many_statements.rs b/crates/ruff/src/rules/pylint/rules/too_many_statements.rs index 87f3d57e9f..755daab2b0 100644 --- a/crates/ruff/src/rules/pylint/rules/too_many_statements.rs +++ b/crates/ruff/src/rules/pylint/rules/too_many_statements.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Excepthandler, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; /// ## What it does @@ -158,7 +158,7 @@ pub(crate) fn too_many_statements( statements, max_statements, }, - identifier_range(stmt, locator), + stmt.identifier(locator), )) } else { None 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 3b82d550f9..8277cb699e 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 @@ -4,7 +4,7 @@ use rustpython_parser::ast::{Arguments, Decorator, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; use ruff_python_semantic::analyze::visibility::is_staticmethod; @@ -189,7 +189,7 @@ pub(crate) fn unexpected_special_method_signature( expected_params, actual_params, }, - identifier_range(stmt, locator), + stmt.identifier(locator), )); } } diff --git a/crates/ruff/src/rules/pylint/rules/useless_else_on_loop.rs b/crates/ruff/src/rules/pylint/rules/useless_else_on_loop.rs index 2073339f69..ab6f737cfb 100644 --- a/crates/ruff/src/rules/pylint/rules/useless_else_on_loop.rs +++ b/crates/ruff/src/rules/pylint/rules/useless_else_on_loop.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Excepthandler, MatchCase, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers; +use ruff_python_ast::identifier; use crate::checkers::ast::Checker; @@ -102,7 +102,7 @@ pub(crate) fn useless_else_on_loop( if !orelse.is_empty() && !loop_exits_early(body) { checker.diagnostics.push(Diagnostic::new( UselessElseOnLoop, - helpers::else_range(stmt, checker.locator).unwrap(), + identifier::else_(stmt, checker.locator).unwrap(), )); } } 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 faa8f49fbd..cd2483fd65 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs @@ -5,7 +5,7 @@ use rustpython_parser::ast::{self, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -53,7 +53,7 @@ pub(crate) fn unnecessary_class_parentheses( return; } - let offset = identifier_range(stmt, checker.locator).start(); + let offset = stmt.identifier(checker.locator).start(); 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 d3a86d91fb..ad61c35c8b 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use crate::autofix::edits::remove_argument; use crate::checkers::ast::Checker; @@ -73,7 +73,7 @@ pub(crate) fn useless_object_inheritance( diagnostic.try_set_fix(|| { let edit = remove_argument( checker.locator, - identifier_range(stmt, checker.locator).start(), + stmt.identifier(checker.locator).start(), expr.range(), &class_def.bases, &class_def.keywords, diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 0d86104608..38b750d00d 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -1,15 +1,13 @@ use std::borrow::Cow; -use std::ops::{Add, Sub}; +use std::ops::Sub; use std::path::Path; -use itertools::Itertools; -use log::error; use num_traits::Zero; use ruff_text_size::{TextRange, TextSize}; use rustc_hash::{FxHashMap, FxHashSet}; +use rustpython_ast::Cmpop; use rustpython_parser::ast::{ - self, Arguments, Cmpop, Constant, Excepthandler, Expr, Keyword, MatchCase, Pattern, Ranged, - Stmt, + self, Arguments, Constant, Excepthandler, Expr, Keyword, MatchCase, Pattern, Ranged, Stmt, }; use rustpython_parser::{lexer, Mode, Tok}; use smallvec::SmallVec; @@ -44,6 +42,7 @@ where range: _range, }) = expr { + // Ex) `list()` if args.is_empty() && keywords.is_empty() { if let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() { if !is_iterable_initializer(id.as_str(), |id| is_builtin(id)) { @@ -1071,185 +1070,6 @@ pub fn match_parens(start: TextSize, locator: &Locator) -> Option { } } -/// Return `true` if the given character is a valid identifier character. -fn is_identifier(c: char) -> bool { - c.is_alphanumeric() || c == '_' -} - -#[derive(Debug)] -enum IdentifierState { - /// We're in a comment, awaiting the identifier at the given index. - InComment { index: usize }, - /// We're looking for the identifier at the given index. - AwaitingIdentifier { index: usize }, - /// We're in the identifier at the given index, starting at the given character. - InIdentifier { index: usize, start: TextSize }, -} - -/// Return the appropriate visual `Range` for any message that spans a `Stmt`. -/// Specifically, this method returns the range of a function or class name, -/// rather than that of the entire function or class body. -pub fn identifier_range(stmt: &Stmt, locator: &Locator) -> TextRange { - match stmt { - Stmt::ClassDef(ast::StmtClassDef { - decorator_list, - range, - .. - }) - | Stmt::FunctionDef(ast::StmtFunctionDef { - decorator_list, - range, - .. - }) - | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { - decorator_list, - range, - .. - }) => { - let header_range = decorator_list.last().map_or(*range, |last_decorator| { - TextRange::new(last_decorator.end(), range.end()) - }); - - // If the statement is an async function, we're looking for the third - // keyword-or-identifier (`foo` in `async def foo()`). Otherwise, it's the - // second keyword-or-identifier (`foo` in `def foo()` or `Foo` in `class Foo`). - let name_index = if stmt.is_async_function_def_stmt() { - 2 - } else { - 1 - }; - - let mut state = IdentifierState::AwaitingIdentifier { index: 0 }; - for (char_index, char) in locator.slice(header_range).char_indices() { - match state { - IdentifierState::InComment { index } => match char { - // Read until the end of the comment. - '\r' | '\n' => { - state = IdentifierState::AwaitingIdentifier { index }; - } - _ => {} - }, - IdentifierState::AwaitingIdentifier { index } => match char { - // Read until we hit an identifier. - '#' => { - state = IdentifierState::InComment { index }; - } - c if is_identifier(c) => { - state = IdentifierState::InIdentifier { - index, - start: TextSize::try_from(char_index).unwrap(), - }; - } - _ => {} - }, - IdentifierState::InIdentifier { index, start } => { - // We've reached the end of the identifier. - if !is_identifier(char) { - if index == name_index { - // We've found the identifier we're looking for. - let end = TextSize::try_from(char_index).unwrap(); - return TextRange::new( - header_range.start().add(start), - header_range.start().add(end), - ); - } - - // We're looking for a different identifier. - state = IdentifierState::AwaitingIdentifier { index: index + 1 }; - } - } - } - } - - error!("Failed to find identifier for {:?}", stmt); - header_range - } - _ => stmt.range(), - } -} - -/// Return the ranges of [`Tok::Name`] tokens within a specified node. -pub fn find_names<'a, T>( - located: &'a T, - locator: &'a Locator, -) -> impl Iterator + 'a -where - T: Ranged, -{ - let contents = locator.slice(located.range()); - - lexer::lex_starts_at(contents, Mode::Module, located.start()) - .flatten() - .filter(|(tok, _)| matches!(tok, Tok::Name { .. })) - .map(|(_, range)| range) -} - -/// Return the `Range` of `name` in `Excepthandler`. -pub fn excepthandler_name_range(handler: &Excepthandler, locator: &Locator) -> Option { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { - name, - type_, - body, - range: _range, - }) = handler; - - match (name, type_) { - (Some(_), Some(type_)) => { - let contents = &locator.contents()[TextRange::new(type_.end(), body[0].start())]; - - lexer::lex_starts_at(contents, Mode::Module, type_.end()) - .flatten() - .tuple_windows() - .find(|(tok, next_tok)| { - matches!(tok.0, Tok::As) && matches!(next_tok.0, Tok::Name { .. }) - }) - .map(|((..), (_, range))| range) - } - _ => None, - } -} - -/// Return the `Range` of `except` in `Excepthandler`. -pub fn except_range(handler: &Excepthandler, locator: &Locator) -> TextRange { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { body, type_, .. }) = handler; - let end = if let Some(type_) = type_ { - type_.end() - } else { - body.first().expect("Expected body to be non-empty").start() - }; - let contents = &locator.contents()[TextRange::new(handler.start(), end)]; - - lexer::lex_starts_at(contents, Mode::Module, handler.start()) - .flatten() - .find(|(kind, _)| matches!(kind, Tok::Except { .. })) - .map(|(_, range)| range) - .expect("Failed to find `except` range") -} - -/// Return the `Range` of `else` in `For`, `AsyncFor`, and `While` statements. -pub fn else_range(stmt: &Stmt, locator: &Locator) -> Option { - match stmt { - Stmt::For(ast::StmtFor { body, orelse, .. }) - | Stmt::AsyncFor(ast::StmtAsyncFor { body, orelse, .. }) - | Stmt::While(ast::StmtWhile { body, orelse, .. }) - if !orelse.is_empty() => - { - let body_end = body.last().expect("Expected body to be non-empty").end(); - let or_else_start = orelse - .first() - .expect("Expected orelse to be non-empty") - .start(); - let contents = &locator.contents()[TextRange::new(body_end, or_else_start)]; - - lexer::lex_starts_at(contents, Mode::Module, body_end) - .flatten() - .find(|(kind, _)| matches!(kind, Tok::Else)) - .map(|(_, range)| range) - } - _ => None, - } -} - /// Return the `Range` of the first `Tok::Colon` token in a `Range`. pub fn first_colon_range(range: TextRange, locator: &Locator) -> Option { let contents = &locator.contents()[range]; @@ -1482,7 +1302,101 @@ pub fn is_unpacking_assignment(parent: &Stmt, child: &Expr) -> bool { } } -#[derive(Clone, PartialEq, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, is_macro::Is)] +pub enum Truthiness { + // An expression evaluates to `False`. + Falsey, + // An expression evaluates to `True`. + Truthy, + // An expression evaluates to an unknown value (e.g., a variable `x` of unknown type). + Unknown, +} + +impl From> for Truthiness { + fn from(value: Option) -> Self { + match value { + Some(true) => Truthiness::Truthy, + Some(false) => Truthiness::Falsey, + None => Truthiness::Unknown, + } + } +} + +impl From for Option { + fn from(truthiness: Truthiness) -> Self { + match truthiness { + Truthiness::Truthy => Some(true), + Truthiness::Falsey => Some(false), + Truthiness::Unknown => None, + } + } +} + +impl Truthiness { + pub fn from_expr(expr: &Expr, is_builtin: F) -> Self + where + F: Fn(&str) -> bool, + { + match expr { + Expr::Constant(ast::ExprConstant { value, .. }) => match value { + Constant::Bool(value) => Some(*value), + Constant::None => Some(false), + Constant::Str(string) => Some(!string.is_empty()), + Constant::Bytes(bytes) => Some(!bytes.is_empty()), + Constant::Int(int) => Some(!int.is_zero()), + Constant::Float(float) => Some(*float != 0.0), + Constant::Complex { real, imag } => Some(*real != 0.0 || *imag != 0.0), + Constant::Ellipsis => Some(true), + Constant::Tuple(elts) => Some(!elts.is_empty()), + }, + 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 { + return false; + }; + !string.is_empty() + }) { + Some(true) + } else { + 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::Call(ast::ExprCall { + func, + args, + keywords, range: _range, + }) => { + if let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() { + if is_iterable_initializer(id.as_str(), |id| is_builtin(id)) { + if args.is_empty() && keywords.is_empty() { + // Ex) `list()` + Some(false) + } else if args.len() == 1 && keywords.is_empty() { + // Ex) `list([1, 2, 3])` + Self::from_expr(&args[0], is_builtin).into() + } else { + None + } + } else { + None + } + } else { + None + } + } + _ => None, + } + .into() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] pub struct LocatedCmpop { pub range: TextRange, pub op: Cmpop, @@ -1581,111 +1495,19 @@ pub fn locate_cmpops(expr: &Expr, locator: &Locator) -> Vec { ops } -#[derive(Copy, Clone, Debug, PartialEq, is_macro::Is)] -pub enum Truthiness { - // An expression evaluates to `False`. - Falsey, - // An expression evaluates to `True`. - Truthy, - // An expression evaluates to an unknown value (e.g., a variable `x` of unknown type). - Unknown, -} - -impl From> for Truthiness { - fn from(value: Option) -> Self { - match value { - Some(true) => Truthiness::Truthy, - Some(false) => Truthiness::Falsey, - None => Truthiness::Unknown, - } - } -} - -impl From for Option { - fn from(truthiness: Truthiness) -> Self { - match truthiness { - Truthiness::Truthy => Some(true), - Truthiness::Falsey => Some(false), - Truthiness::Unknown => None, - } - } -} - -impl Truthiness { - pub fn from_expr(expr: &Expr, is_builtin: F) -> Self - where - F: Fn(&str) -> bool, - { - match expr { - Expr::Constant(ast::ExprConstant { value, .. }) => match value { - Constant::Bool(value) => Some(*value), - Constant::None => Some(false), - Constant::Str(string) => Some(!string.is_empty()), - Constant::Bytes(bytes) => Some(!bytes.is_empty()), - Constant::Int(int) => Some(!int.is_zero()), - Constant::Float(float) => Some(*float != 0.0), - Constant::Complex { real, imag } => Some(*real != 0.0 || *imag != 0.0), - Constant::Ellipsis => Some(true), - Constant::Tuple(elts) => Some(!elts.is_empty()), - }, - 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 { - return false; - }; - !string.is_empty() - }) { - Some(true) - } else { - 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::Call(ast::ExprCall { - func, - args, - keywords, range: _range, - }) => { - if let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() { - if is_iterable_initializer(id.as_str(), |id| is_builtin(id)) { - if args.is_empty() && keywords.is_empty() { - Some(false) - } else if args.len() == 1 && keywords.is_empty() { - Self::from_expr(&args[0], is_builtin).into() - } else { - None - } - } else { - None - } - } else { - None - } - } - _ => None, - } - .into() - } -} - #[cfg(test)] mod tests { use std::borrow::Cow; use anyhow::Result; use ruff_text_size::{TextLen, TextRange, TextSize}; - use rustpython_ast::{Expr, Stmt, Suite}; - use rustpython_parser::ast::Cmpop; + use rustpython_ast::{Cmpop, Expr, Stmt}; + use rustpython_parser::ast::Suite; use rustpython_parser::Parse; use crate::helpers::{ - elif_else_range, else_range, first_colon_range, has_trailing_content, identifier_range, - locate_cmpops, resolve_imported_module_path, LocatedCmpop, + elif_else_range, first_colon_range, has_trailing_content, locate_cmpops, + resolve_imported_module_path, LocatedCmpop, }; use crate::source_code::Locator; @@ -1728,90 +1550,6 @@ y = 2 Ok(()) } - #[test] - fn extract_identifier_range() -> Result<()> { - let contents = "def f(): pass".trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(4), TextSize::from(5)) - ); - - let contents = "async def f(): pass".trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(10), TextSize::from(11)) - ); - - let contents = r#" -def \ - f(): - pass -"# - .trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(8), TextSize::from(9)) - ); - - let contents = "class Class(): pass".trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(6), TextSize::from(11)) - ); - - let contents = "class Class: pass".trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(6), TextSize::from(11)) - ); - - let contents = r#" -@decorator() -class Class(): - pass -"# - .trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(19), TextSize::from(24)) - ); - - let contents = r#" -@decorator() # Comment -class Class(): - pass -"# - .trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(30), TextSize::from(35)) - ); - - let contents = r#"x = y + 1"#.trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(0), TextSize::from(9)) - ); - - Ok(()) - } - #[test] fn resolve_import() { // Return the module directly. @@ -1849,26 +1587,6 @@ class Class(): ); } - #[test] - fn extract_else_range() -> Result<()> { - let contents = r#" -for x in y: - pass -else: - pass -"# - .trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - let range = else_range(&stmt, &locator).unwrap(); - assert_eq!(&contents[range], "else"); - assert_eq!( - range, - TextRange::new(TextSize::from(21), TextSize::from(25)) - ); - Ok(()) - } - #[test] fn extract_first_colon_range() { let contents = "with a: pass"; diff --git a/crates/ruff_python_ast/src/identifier.rs b/crates/ruff_python_ast/src/identifier.rs new file mode 100644 index 0000000000..c1cb6c4a32 --- /dev/null +++ b/crates/ruff_python_ast/src/identifier.rs @@ -0,0 +1,621 @@ +//! Extract [`TextRange`] information from AST nodes. +//! +//! In the `RustPython` AST, each node has a `range` field that contains the +//! start and end byte offsets of the node. However, attributes on those +//! nodes may not have their own ranges. In particular, identifiers are +//! not given their own ranges, unless they're part of a name expression. +//! +//! For example, given: +//! ```python +//! def f(): +//! ... +//! ``` +//! +//! The statement defining `f` has a range, but the identifier `f` does not. +//! +//! This module assists with extracting [`TextRange`] ranges from AST nodes +//! via manual lexical analysis. + +use std::ops::{Add, Sub}; +use std::str::Chars; + +use ruff_text_size::{TextLen, TextRange, TextSize}; +use rustpython_ast::{Alias, Arg, Pattern}; +use rustpython_parser::ast::{self, Excepthandler, Ranged, Stmt}; + +use ruff_python_whitespace::is_python_whitespace; + +use crate::source_code::Locator; + +pub trait Identifier { + /// Return the [`TextRange`] of the identifier in the given AST node. + fn identifier(&self, locator: &Locator) -> TextRange; +} + +pub trait TryIdentifier { + /// Return the [`TextRange`] of the identifier in the given AST node, or `None` if + /// the node does not have an identifier. + fn try_identifier(&self, locator: &Locator) -> Option; +} + +impl Identifier for Stmt { + /// Return the [`TextRange`] of the identifier in the given statement. + /// + /// For example, return the range of `f` in: + /// ```python + /// def f(): + /// ... + /// ``` + fn identifier(&self, locator: &Locator) -> TextRange { + match self { + Stmt::ClassDef(ast::StmtClassDef { + decorator_list, + range, + .. + }) + | Stmt::FunctionDef(ast::StmtFunctionDef { + decorator_list, + range, + .. + }) => { + let range = decorator_list.last().map_or(*range, |last_decorator| { + TextRange::new(last_decorator.end(), range.end()) + }); + + // The first "identifier" is the `def` or `class` keyword. + // The second "identifier" is the function or class name. + IdentifierTokenizer::starts_at(range.start(), locator.contents()) + .nth(1) + .expect("Unable to identify identifier in function or class definition") + } + Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { + decorator_list, + range, + .. + }) => { + let range = decorator_list.last().map_or(*range, |last_decorator| { + TextRange::new(last_decorator.end(), range.end()) + }); + + // The first "identifier" is the `async` keyword. + // The second "identifier" is the `def` or `class` keyword. + // The third "identifier" is the function or class name. + IdentifierTokenizer::starts_at(range.start(), locator.contents()) + .nth(2) + .expect("Unable to identify identifier in function or class definition") + } + _ => self.range(), + } + } +} + +impl Identifier for Arg { + /// Return the [`TextRange`] for the identifier defining an [`Arg`]. + /// + /// For example, return the range of `x` in: + /// ```python + /// def f(x: int = 0): + /// ... + /// ``` + fn identifier(&self, locator: &Locator) -> TextRange { + IdentifierTokenizer::new(locator.contents(), self.range()) + .next() + .expect("Failed to find argument identifier") + } +} + +impl Identifier for Alias { + /// Return the [`TextRange`] for the identifier defining an [`Alias`]. + /// + /// For example, return the range of `x` in: + /// ```python + /// from foo import bar as x + /// ``` + fn identifier(&self, locator: &Locator) -> TextRange { + if matches!(self.name.as_str(), "*") { + self.range() + } else if self.asname.is_none() { + // The first identifier is the module name. + IdentifierTokenizer::new(locator.contents(), self.range()) + .next() + .expect("Failed to find alias identifier") + } else { + // The first identifier is the module name. + // The second identifier is the "as" keyword. + // The third identifier is the alias name. + IdentifierTokenizer::new(locator.contents(), self.range()) + .last() + .expect("Failed to find alias identifier") + } + } +} + +impl TryIdentifier for Pattern { + /// Return the [`TextRange`] of the identifier in the given pattern. + /// + /// For example, return the range of `z` in: + /// ```python + /// match x: + /// # Pattern::MatchAs + /// case z: + /// ... + /// ``` + /// + /// Or: + /// ```python + /// match x: + /// # Pattern::MatchAs + /// case y as z: + /// ... + /// ``` + /// + /// Or : + /// ```python + /// match x: + /// # Pattern::MatchMapping + /// case {"a": 1, **z} + /// ... + /// ``` + /// + /// Or : + /// ```python + /// match x: + /// # Pattern::MatchStar + /// case *z: + /// ... + /// ``` + fn try_identifier(&self, locator: &Locator) -> Option { + match self { + Pattern::MatchAs(ast::PatternMatchAs { + name: Some(_), + pattern, + range, + }) => { + Some(if let Some(pattern) = pattern { + // Identify `z` in: + // ```python + // match x: + // case Foo(bar) as z: + // ... + // ``` + IdentifierTokenizer::starts_at(pattern.end(), locator.contents()) + .nth(1) + .expect("Unable to identify identifier in pattern") + } else { + // Identify `z` in: + // ```python + // match x: + // case z: + // ... + // ``` + *range + }) + } + Pattern::MatchMapping(ast::PatternMatchMapping { + patterns, + rest: Some(_), + .. + }) => { + Some(if let Some(pattern) = patterns.last() { + // Identify `z` in: + // ```python + // match x: + // case {"a": 1, **z} + // ... + // ``` + // + // A mapping pattern can contain at most one double-star pattern, + // and it must be the last pattern in the mapping. + IdentifierTokenizer::starts_at(pattern.end(), locator.contents()) + .next() + .expect("Unable to identify identifier in pattern") + } else { + // Identify `z` in: + // ```python + // match x: + // case {**z} + // ... + // ``` + IdentifierTokenizer::starts_at(self.start(), locator.contents()) + .next() + .expect("Unable to identify identifier in pattern") + }) + } + Pattern::MatchStar(ast::PatternMatchStar { name: Some(_), .. }) => { + // Identify `z` in: + // ```python + // match x: + // case *z: + // ... + // ``` + Some( + IdentifierTokenizer::starts_at(self.start(), locator.contents()) + .next() + .expect("Unable to identify identifier in pattern"), + ) + } + _ => None, + } + } +} + +impl TryIdentifier for Excepthandler { + /// Return the [`TextRange`] of a named exception in an [`Excepthandler`]. + /// + /// For example, return the range of `e` in: + /// ```python + /// try: + /// ... + /// except ValueError as e: + /// ... + /// ``` + fn try_identifier(&self, locator: &Locator) -> Option { + let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { type_, name, .. }) = + self; + + if name.is_none() { + return None; + } + + let Some(type_) = type_ else { + return None; + }; + + // The exception name is the first identifier token after the `as` keyword. + Some( + IdentifierTokenizer::starts_at(type_.end(), locator.contents()) + .nth(1) + .expect("Failed to find exception identifier in exception handler"), + ) + } +} + +/// Return the [`TextRange`] for every name in a [`Stmt`]. +/// +/// Intended to be used for `global` and `nonlocal` statements. +/// +/// For example, return the ranges of `x` and `y` in: +/// ```python +/// global x, y +/// ``` +pub fn names<'a>(stmt: &Stmt, locator: &'a Locator<'a>) -> impl Iterator + 'a { + // Given `global x, y`, the first identifier is `global`, and the remaining identifiers are + // the names. + IdentifierTokenizer::new(locator.contents(), stmt.range()).skip(1) +} + +/// Return the [`TextRange`] of the `except` token in an [`Excepthandler`]. +pub fn except(handler: &Excepthandler, locator: &Locator) -> TextRange { + IdentifierTokenizer::new(locator.contents(), handler.range()) + .next() + .expect("Failed to find `except` token in `Excepthandler`") +} + +/// 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 { + return None; + }; + + if orelse.is_empty() { + return None; + } + + IdentifierTokenizer::starts_at( + body.last().expect("Expected body to be non-empty").end(), + locator.contents(), + ) + .next() +} + +/// Return `true` if the given character starts a valid Python identifier. +/// +/// Python identifiers must start with an alphabetic character or an underscore. +fn is_python_identifier_start(c: char) -> bool { + c.is_alphabetic() || c == '_' +} + +/// Return `true` if the given character is a valid Python identifier continuation character. +/// +/// Python identifiers can contain alphanumeric characters and underscores, but cannot start with a +/// number. +fn is_python_identifier_continue(c: char) -> bool { + c.is_alphanumeric() || c == '_' +} + +/// Simple zero allocation tokenizer for Python identifiers. +/// +/// The tokenizer must operate over a range that can only contain identifiers, keywords, and +/// comments (along with whitespace and continuation characters). It does not support other tokens, +/// like operators, literals, or delimiters. It also does not differentiate between keywords and +/// identifiers, treating every valid token as an "identifier". +/// +/// This is useful for cases like, e.g., identifying the alias name in an aliased import (`bar` in +/// `import foo as bar`), where we're guaranteed to only have identifiers and keywords in the +/// relevant range. +pub(crate) struct IdentifierTokenizer<'a> { + cursor: Cursor<'a>, + offset: TextSize, +} + +impl<'a> IdentifierTokenizer<'a> { + pub(crate) fn new(source: &'a str, range: TextRange) -> Self { + Self { + cursor: Cursor::new(&source[range]), + offset: range.start(), + } + } + + pub(crate) fn starts_at(offset: TextSize, source: &'a str) -> Self { + let range = TextRange::new(offset, source.text_len()); + Self::new(source, range) + } + + fn next_token(&mut self) -> Option { + while let Some(c) = self.cursor.bump() { + match c { + c if is_python_identifier_start(c) => { + let start = self.offset.add(self.cursor.offset()).sub(c.text_len()); + self.cursor.eat_while(is_python_identifier_continue); + let end = self.offset.add(self.cursor.offset()); + return Some(TextRange::new(start, end)); + } + + c if is_python_whitespace(c) => { + self.cursor.eat_while(is_python_whitespace); + } + + '#' => { + self.cursor.eat_while(|c| !matches!(c, '\n' | '\r')); + } + + '\r' => { + self.cursor.eat_char('\n'); + } + + '\n' => { + // Nothing to do. + } + + '\\' => { + // Nothing to do. + } + + _ => { + // Nothing to do. + } + }; + } + + None + } +} + +impl Iterator for IdentifierTokenizer<'_> { + type Item = TextRange; + + fn next(&mut self) -> Option { + self.next_token() + } +} + +const EOF_CHAR: char = '\0'; + +#[derive(Debug, Clone)] +struct Cursor<'a> { + chars: Chars<'a>, + offset: TextSize, +} + +impl<'a> Cursor<'a> { + fn new(source: &'a str) -> Self { + Self { + chars: source.chars(), + offset: TextSize::from(0), + } + } + + const fn offset(&self) -> TextSize { + self.offset + } + + /// Peeks the next character from the input stream without consuming it. + /// Returns [`EOF_CHAR`] if the file is at the end of the file. + fn first(&self) -> char { + self.chars.clone().next().unwrap_or(EOF_CHAR) + } + + /// Returns `true` if the file is at the end of the file. + fn is_eof(&self) -> bool { + self.chars.as_str().is_empty() + } + + /// Consumes the next character. + fn bump(&mut self) -> Option { + if let Some(char) = self.chars.next() { + self.offset += char.text_len(); + Some(char) + } else { + None + } + } + + /// Eats the next character if it matches the given character. + fn eat_char(&mut self, c: char) -> bool { + if self.first() == c { + self.bump(); + true + } else { + false + } + } + + /// Eats symbols while predicate returns true or until the end of file is reached. + fn eat_while(&mut self, mut predicate: impl FnMut(char) -> bool) { + while predicate(self.first()) && !self.is_eof() { + self.bump(); + } + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + use ruff_text_size::{TextRange, TextSize}; + use rustpython_ast::Stmt; + use rustpython_parser::Parse; + + use crate::identifier; + use crate::identifier::Identifier; + use crate::source_code::Locator; + + #[test] + fn extract_arg_range() -> Result<()> { + let contents = "def f(x): pass".trim(); + let stmt = Stmt::parse(contents, "")?; + let function_def = stmt.as_function_def_stmt().unwrap(); + let args = &function_def.args.args; + let arg = &args[0]; + let locator = Locator::new(contents); + assert_eq!( + arg.identifier(&locator), + TextRange::new(TextSize::from(6), TextSize::from(7)) + ); + + let contents = "def f(x: int): pass".trim(); + let stmt = Stmt::parse(contents, "")?; + let function_def = stmt.as_function_def_stmt().unwrap(); + let args = &function_def.args.args; + let arg = &args[0]; + let locator = Locator::new(contents); + assert_eq!( + arg.identifier(&locator), + TextRange::new(TextSize::from(6), TextSize::from(7)) + ); + + let contents = r#" +def f( + x: int, # Comment +): + pass +"# + .trim(); + let stmt = Stmt::parse(contents, "")?; + let function_def = stmt.as_function_def_stmt().unwrap(); + let args = &function_def.args.args; + let arg = &args[0]; + let locator = Locator::new(contents); + assert_eq!( + arg.identifier(&locator), + TextRange::new(TextSize::from(11), TextSize::from(12)) + ); + + Ok(()) + } + + #[test] + fn extract_identifier_range() -> Result<()> { + let contents = "def f(): pass".trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + assert_eq!( + stmt.identifier(&locator), + TextRange::new(TextSize::from(4), TextSize::from(5)) + ); + + let contents = "async def f(): pass".trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + assert_eq!( + stmt.identifier(&locator), + TextRange::new(TextSize::from(10), TextSize::from(11)) + ); + + let contents = r#" +def \ + f(): + pass +"# + .trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + assert_eq!( + stmt.identifier(&locator), + TextRange::new(TextSize::from(8), TextSize::from(9)) + ); + + let contents = "class Class(): pass".trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + assert_eq!( + stmt.identifier(&locator), + TextRange::new(TextSize::from(6), TextSize::from(11)) + ); + + let contents = "class Class: pass".trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + assert_eq!( + stmt.identifier(&locator), + TextRange::new(TextSize::from(6), TextSize::from(11)) + ); + + let contents = r#" +@decorator() +class Class(): + pass +"# + .trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + assert_eq!( + stmt.identifier(&locator), + TextRange::new(TextSize::from(19), TextSize::from(24)) + ); + + let contents = r#" +@decorator() # Comment +class Class(): + pass +"# + .trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + assert_eq!( + stmt.identifier(&locator), + TextRange::new(TextSize::from(30), TextSize::from(35)) + ); + + let contents = r#"x = y + 1"#.trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + assert_eq!( + stmt.identifier(&locator), + TextRange::new(TextSize::from(0), TextSize::from(9)) + ); + + Ok(()) + } + + #[test] + fn extract_else_range() -> Result<()> { + let contents = r#" +for x in y: + pass +else: + pass +"# + .trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + let range = identifier::else_(&stmt, &locator).unwrap(); + assert_eq!(&contents[range], "else"); + assert_eq!( + range, + TextRange::new(TextSize::from(21), TextSize::from(25)) + ); + Ok(()) + } +} diff --git a/crates/ruff_python_ast/src/lib.rs b/crates/ruff_python_ast/src/lib.rs index be82bf8233..275e62aeab 100644 --- a/crates/ruff_python_ast/src/lib.rs +++ b/crates/ruff_python_ast/src/lib.rs @@ -6,6 +6,7 @@ pub mod docstrings; pub mod function; pub mod hashable; pub mod helpers; +pub mod identifier; pub mod imports; pub mod node; pub mod prelude; diff --git a/crates/ruff_python_formatter/src/trivia.rs b/crates/ruff_python_formatter/src/trivia.rs index c1f5bcfdf6..01415e89b1 100644 --- a/crates/ruff_python_formatter/src/trivia.rs +++ b/crates/ruff_python_formatter/src/trivia.rs @@ -477,6 +477,7 @@ impl<'a> Cursor<'a> { self.source_length = self.text_len(); } + /// Returns `true` if the file is at the end of the file. fn is_eof(&self) -> bool { self.chars.as_str().is_empty() } diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index b390362ae8..ea79278389 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -5,8 +5,6 @@ use ruff_text_size::TextRange; use rustpython_parser::ast::Ranged; use ruff_index::{newtype_index, IndexSlice, IndexVec}; -use ruff_python_ast::helpers; -use ruff_python_ast::source_code::Locator; use crate::context::ExecutionContext; use crate::model::SemanticModel; @@ -141,18 +139,6 @@ impl<'a> Binding<'a> { } } - /// Returns the appropriate visual range for highlighting this binding. - pub fn trimmed_range(&self, semantic: &SemanticModel, locator: &Locator) -> TextRange { - match self.kind { - BindingKind::ClassDefinition | BindingKind::FunctionDefinition => { - self.source.map_or(self.range, |source| { - helpers::identifier_range(semantic.stmts[source], locator) - }) - } - _ => self.range, - } - } - /// Returns the range of the binding's parent. pub fn parent_range(&self, semantic: &SemanticModel) -> Option { self.source From c811213302f76c255da89d374bd8ab42f3b223e5 Mon Sep 17 00:00:00 2001 From: konstin Date: Thu, 15 Jun 2023 20:57:00 +0200 Subject: [PATCH 074/447] Allow space in filename for powershell + windows + python module (#5115) Fixes #5077 ## Summary Previously, in a powershell on windows when using `python -m ruff` instead of `ruff` a call such as `python -m ruff "a b.py"` would fail because the space would be split into two arguments. The python docs [recommend](https://docs.python.org/3/library/os.html#os.spawnv) using subprocess instead of os.spawn variants, which does fix the problem. ## Test Plan I manually confirmed that the problem previously occurred and now doesn't anymore. This only happens in a very specific environment (maturin build, windows, powershell), so i could try adding a step on CI for it but i don't think it's worth it. ``` (.venv) PS C:\Users\Konstantin\PycharmProjects\ruff> python -m ruff "a b.py" warning: Detected debug build without --no-cache. error: Failed to lint a: The system cannot find the file specified. (os error 2) error: Failed to lint b.py: The system cannot find the file specified. (os error 2) a:1:1: E902 The system cannot find the file specified. (os error 2) b.py:1:1: E902 The system cannot find the file specified. (os error 2) Found 2 errors. (.venv) PS C:\Users\Konstantin\PycharmProjects\ruff> python -m ruff "a b.py" warning: Detected debug build without --no-cache. a b.py:2:5: F841 [*] Local variable `x` is assigned to but never used Found 1 error. [*] 1 potentially fixable with the --fix option. ``` --- python/ruff/__main__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/ruff/__main__.py b/python/ruff/__main__.py index 9275fcef3d..1631082fbe 100644 --- a/python/ruff/__main__.py +++ b/python/ruff/__main__.py @@ -1,4 +1,5 @@ import os +import subprocess import sys import sysconfig from pathlib import Path @@ -31,4 +32,5 @@ def find_ruff_bin() -> Path: if __name__ == "__main__": ruff = find_ruff_bin() - sys.exit(os.spawnv(os.P_WAIT, ruff, ["ruff", *sys.argv[1:]])) + completed_process = subprocess.run([ruff, *sys.argv[1:]]) + sys.exit(completed_process.returncode) From 107a295af4f51dce1e78dcbfd234b2a3ad99a00f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 15 Jun 2023 15:00:19 -0400 Subject: [PATCH 075/447] Allow `async with` in `redefined-loop-name` (#5125) ## Summary Closes #5124. --- .../fixtures/pylint/redefined_loop_name.py | 15 + crates/ruff/src/checkers/ast/mod.rs | 4 +- .../rules/pylint/rules/redefined_loop_name.rs | 191 ++++++------- ...tests__PLW2901_redefined_loop_name.py.snap | 264 ++++++++++-------- 4 files changed, 251 insertions(+), 223 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pylint/redefined_loop_name.py b/crates/ruff/resources/test/fixtures/pylint/redefined_loop_name.py index b99bfe9323..6b9b499714 100644 --- a/crates/ruff/resources/test/fixtures/pylint/redefined_loop_name.py +++ b/crates/ruff/resources/test/fixtures/pylint/redefined_loop_name.py @@ -31,6 +31,21 @@ with None as i: with None as j: # ok pass +# Async with -> with, variable reused +async with None as i: + with None as i: # error + pass + +# Async with -> with, different variable +async with None as i: + with None as j: # ok + pass + +# Async for -> for, variable reused +async for i in []: + for i in []: # error + pass + # For -> for -> for, doubly nested variable reuse for i in []: for j in []: diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 6dd4cc885c..11c69d01af 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1483,7 +1483,7 @@ where ); } if self.enabled(Rule::RedefinedLoopName) { - pylint::rules::redefined_loop_name(self, &Node::Stmt(stmt)); + pylint::rules::redefined_loop_name(self, stmt); } } Stmt::While(ast::StmtWhile { body, orelse, .. }) => { @@ -1524,7 +1524,7 @@ where pylint::rules::useless_else_on_loop(self, stmt, body, orelse); } if self.enabled(Rule::RedefinedLoopName) { - pylint::rules::redefined_loop_name(self, &Node::Stmt(stmt)); + pylint::rules::redefined_loop_name(self, stmt); } if self.enabled(Rule::IterationOverSet) { pylint::rules::iteration_over_set(self, iter); diff --git a/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs b/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs index 5a3573952d..0bdcb3ce9e 100644 --- a/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs +++ b/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs @@ -7,53 +7,10 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor}; -use ruff_python_ast::types::Node; use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub(crate) enum OuterBindingKind { - For, - With, -} - -impl fmt::Display for OuterBindingKind { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - match self { - OuterBindingKind::For => fmt.write_str("`for` loop"), - OuterBindingKind::With => fmt.write_str("`with` statement"), - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub(crate) enum InnerBindingKind { - For, - With, - Assignment, -} - -impl fmt::Display for InnerBindingKind { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - match self { - InnerBindingKind::For => fmt.write_str("`for` loop"), - InnerBindingKind::With => fmt.write_str("`with` statement"), - InnerBindingKind::Assignment => fmt.write_str("assignment"), - } - } -} - -impl PartialEq for OuterBindingKind { - fn eq(&self, other: &InnerBindingKind) -> bool { - matches!( - (self, other), - (OuterBindingKind::For, InnerBindingKind::For) - | (OuterBindingKind::With, InnerBindingKind::With) - ) - } -} - /// ## What it does /// Checks for variables defined in `for` loops and `with` statements that /// get overwritten within the body, for example by another `for` loop or @@ -128,6 +85,48 @@ impl Violation for RedefinedLoopName { } } +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum OuterBindingKind { + For, + With, +} + +impl fmt::Display for OuterBindingKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + OuterBindingKind::For => fmt.write_str("`for` loop"), + OuterBindingKind::With => fmt.write_str("`with` statement"), + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum InnerBindingKind { + For, + With, + Assignment, +} + +impl fmt::Display for InnerBindingKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + InnerBindingKind::For => fmt.write_str("`for` loop"), + InnerBindingKind::With => fmt.write_str("`with` statement"), + InnerBindingKind::Assignment => fmt.write_str("assignment"), + } + } +} + +impl PartialEq for OuterBindingKind { + fn eq(&self, other: &InnerBindingKind) -> bool { + matches!( + (self, other), + (OuterBindingKind::For, InnerBindingKind::For) + | (OuterBindingKind::With, InnerBindingKind::With) + ) + } +} + struct ExprWithOuterBindingKind<'a> { expr: &'a Expr, binding_kind: OuterBindingKind, @@ -255,10 +254,10 @@ fn assignment_is_cast_expr(value: &Expr, target: &Expr, semantic: &SemanticModel semantic.match_typing_expr(func, "cast") } -fn assignment_targets_from_expr<'a, U>( - expr: &'a Expr, +fn assignment_targets_from_expr<'a>( + expr: &'a Expr, dummy_variable_rgx: &'a Regex, -) -> Box> + 'a> { +) -> Box + 'a> { // The Box is necessary to ensure the match arms have the same return type - we can't use // a cast to "impl Iterator", since at the time of writing that is only allowed for // return types and argument types. @@ -308,77 +307,73 @@ fn assignment_targets_from_expr<'a, U>( } } -fn assignment_targets_from_with_items<'a, U>( - items: &'a [Withitem], +fn assignment_targets_from_with_items<'a>( + items: &'a [Withitem], dummy_variable_rgx: &'a Regex, -) -> impl Iterator> + 'a { +) -> impl Iterator + 'a { items .iter() .filter_map(|item| { item.optional_vars .as_ref() - .map(|expr| assignment_targets_from_expr(&**expr, dummy_variable_rgx)) + .map(|expr| assignment_targets_from_expr(expr, dummy_variable_rgx)) }) .flatten() } -fn assignment_targets_from_assign_targets<'a, U>( - targets: &'a [Expr], +fn assignment_targets_from_assign_targets<'a>( + targets: &'a [Expr], dummy_variable_rgx: &'a Regex, -) -> impl Iterator> + 'a { +) -> impl Iterator + 'a { targets .iter() .flat_map(|target| assignment_targets_from_expr(target, dummy_variable_rgx)) } /// PLW2901 -pub(crate) fn redefined_loop_name<'a, 'b>(checker: &'a mut Checker<'b>, node: &Node<'b>) { - let (outer_assignment_targets, inner_assignment_targets) = match node { - Node::Stmt(stmt) => match stmt { - // With. - Stmt::With(ast::StmtWith { items, body, .. }) => { - let outer_assignment_targets: Vec> = - assignment_targets_from_with_items(items, &checker.settings.dummy_variable_rgx) - .map(|expr| ExprWithOuterBindingKind { - expr, - binding_kind: OuterBindingKind::With, - }) - .collect(); - let mut visitor = InnerForWithAssignTargetsVisitor { - context: checker.semantic(), - dummy_variable_rgx: &checker.settings.dummy_variable_rgx, - assignment_targets: vec![], - }; - for stmt in body { - visitor.visit_stmt(stmt); - } - (outer_assignment_targets, visitor.assignment_targets) +pub(crate) fn redefined_loop_name(checker: &mut Checker, stmt: &Stmt) { + let (outer_assignment_targets, inner_assignment_targets) = match stmt { + Stmt::With(ast::StmtWith { items, body, .. }) + | Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. }) => { + let outer_assignment_targets: Vec = + assignment_targets_from_with_items(items, &checker.settings.dummy_variable_rgx) + .map(|expr| ExprWithOuterBindingKind { + expr, + binding_kind: OuterBindingKind::With, + }) + .collect(); + let mut visitor = InnerForWithAssignTargetsVisitor { + context: checker.semantic(), + dummy_variable_rgx: &checker.settings.dummy_variable_rgx, + assignment_targets: vec![], + }; + for stmt in body { + visitor.visit_stmt(stmt); } - // For and async for. - Stmt::For(ast::StmtFor { target, body, .. }) - | Stmt::AsyncFor(ast::StmtAsyncFor { target, body, .. }) => { - let outer_assignment_targets: Vec> = - assignment_targets_from_expr(target, &checker.settings.dummy_variable_rgx) - .map(|expr| ExprWithOuterBindingKind { - expr, - binding_kind: OuterBindingKind::For, - }) - .collect(); - let mut visitor = InnerForWithAssignTargetsVisitor { - context: checker.semantic(), - dummy_variable_rgx: &checker.settings.dummy_variable_rgx, - assignment_targets: vec![], - }; - for stmt in body { - visitor.visit_stmt(stmt); - } - (outer_assignment_targets, visitor.assignment_targets) + (outer_assignment_targets, visitor.assignment_targets) + } + Stmt::For(ast::StmtFor { target, body, .. }) + | Stmt::AsyncFor(ast::StmtAsyncFor { target, body, .. }) => { + let outer_assignment_targets: Vec = + assignment_targets_from_expr(target, &checker.settings.dummy_variable_rgx) + .map(|expr| ExprWithOuterBindingKind { + expr, + binding_kind: OuterBindingKind::For, + }) + .collect(); + let mut visitor = InnerForWithAssignTargetsVisitor { + context: checker.semantic(), + dummy_variable_rgx: &checker.settings.dummy_variable_rgx, + assignment_targets: vec![], + }; + for stmt in body { + visitor.visit_stmt(stmt); } - _ => panic!( - "redefined_loop_name called on Statement that is not a With, For, or AsyncFor" - ), - }, - Node::Expr(_) => panic!("redefined_loop_name called on Node that is not a Statement"), + (outer_assignment_targets, visitor.assignment_targets) + } + _ => panic!( + "redefined_loop_name called on Statement that is not a With, For, AsyncWith, or AsyncFor" + ) }; let mut diagnostics = Vec::new(); diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW2901_redefined_loop_name.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW2901_redefined_loop_name.py.snap index 10ef840c63..7ca951e257 100644 --- a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW2901_redefined_loop_name.py.snap +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW2901_redefined_loop_name.py.snap @@ -37,197 +37,215 @@ redefined_loop_name.py:21:18: PLW2901 Outer `with` statement variable `i` overwr 22 | pass | -redefined_loop_name.py:37:13: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target +redefined_loop_name.py:36:18: PLW2901 Outer `with` statement variable `i` overwritten by inner `with` statement target | -35 | for i in []: -36 | for j in []: -37 | for i in []: # error - | ^ PLW2901 -38 | pass +34 | # Async with -> with, variable reused +35 | async with None as i: +36 | with None as i: # error + | ^ PLW2901 +37 | pass | -redefined_loop_name.py:43:13: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target +redefined_loop_name.py:46:9: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target | -41 | for i in []: -42 | for j in []: -43 | for i in []: # error - | ^ PLW2901 -44 | for j in []: # error -45 | pass +44 | # Async for -> for, variable reused +45 | async for i in []: +46 | for i in []: # error + | ^ PLW2901 +47 | pass | -redefined_loop_name.py:44:17: PLW2901 Outer `for` loop variable `j` overwritten by inner `for` loop target +redefined_loop_name.py:52:13: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target | -42 | for j in []: -43 | for i in []: # error -44 | for j in []: # error +50 | for i in []: +51 | for j in []: +52 | for i in []: # error + | ^ PLW2901 +53 | pass + | + +redefined_loop_name.py:58:13: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target + | +56 | for i in []: +57 | for j in []: +58 | for i in []: # error + | ^ PLW2901 +59 | for j in []: # error +60 | pass + | + +redefined_loop_name.py:59:17: PLW2901 Outer `for` loop variable `j` overwritten by inner `for` loop target + | +57 | for j in []: +58 | for i in []: # error +59 | for j in []: # error | ^ PLW2901 -45 | pass +60 | pass | -redefined_loop_name.py:52:5: PLW2901 `for` loop variable `i` overwritten by assignment target +redefined_loop_name.py:67:5: PLW2901 `for` loop variable `i` overwritten by assignment target | -50 | i = cast(int, i) -51 | i = typing.cast(int, i) -52 | i = 5 # error +65 | i = cast(int, i) +66 | i = typing.cast(int, i) +67 | i = 5 # error | ^ PLW2901 -53 | -54 | # For -> augmented assignment +68 | +69 | # For -> augmented assignment | -redefined_loop_name.py:56:5: PLW2901 `for` loop variable `i` overwritten by assignment target +redefined_loop_name.py:71:5: PLW2901 `for` loop variable `i` overwritten by assignment target | -54 | # For -> augmented assignment -55 | for i in []: -56 | i += 5 # error +69 | # For -> augmented assignment +70 | for i in []: +71 | i += 5 # error | ^ PLW2901 -57 | -58 | # For -> annotated assignment +72 | +73 | # For -> annotated assignment | -redefined_loop_name.py:60:5: PLW2901 `for` loop variable `i` overwritten by assignment target +redefined_loop_name.py:75:5: PLW2901 `for` loop variable `i` overwritten by assignment target | -58 | # For -> annotated assignment -59 | for i in []: -60 | i: int = 5 # error +73 | # For -> annotated assignment +74 | for i in []: +75 | i: int = 5 # error | ^ PLW2901 -61 | -62 | # For -> annotated assignment without value - | - -redefined_loop_name.py:68:9: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target - | -66 | # Async for -> for, variable reused -67 | async for i in []: -68 | for i in []: # error - | ^ PLW2901 -69 | pass - | - -redefined_loop_name.py:73:15: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target - | -71 | # For -> async for, variable reused -72 | for i in []: -73 | async for i in []: # error - | ^ PLW2901 -74 | pass - | - -redefined_loop_name.py:78:9: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target - | -76 | # For -> for, outer loop unpacks tuple -77 | for i, j in enumerate([]): -78 | for i in []: # error - | ^ PLW2901 -79 | pass +76 | +77 | # For -> annotated assignment without value | redefined_loop_name.py:83:9: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target | -81 | # For -> for, inner loop unpacks tuple -82 | for i in []: -83 | for i, j in enumerate([]): # error +81 | # Async for -> for, variable reused +82 | async for i in []: +83 | for i in []: # error | ^ PLW2901 84 | pass | -redefined_loop_name.py:88:9: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target +redefined_loop_name.py:88:15: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target | -86 | # For -> for, both loops unpack tuple -87 | for (i, (j, k)) in []: -88 | for i, j in enumerate([]): # two errors +86 | # For -> async for, variable reused +87 | for i in []: +88 | async for i in []: # error + | ^ PLW2901 +89 | pass + | + +redefined_loop_name.py:93:9: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target + | +91 | # For -> for, outer loop unpacks tuple +92 | for i, j in enumerate([]): +93 | for i in []: # error | ^ PLW2901 -89 | pass +94 | pass | -redefined_loop_name.py:88:12: PLW2901 Outer `for` loop variable `j` overwritten by inner `for` loop target +redefined_loop_name.py:98:9: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target | -86 | # For -> for, both loops unpack tuple -87 | for (i, (j, k)) in []: -88 | for i, j in enumerate([]): # two errors - | ^ PLW2901 -89 | pass +96 | # For -> for, inner loop unpacks tuple +97 | for i in []: +98 | for i, j in enumerate([]): # error + | ^ PLW2901 +99 | pass | -redefined_loop_name.py:105:9: PLW2901 Outer `for` loop variable `j` overwritten by inner `for` loop target +redefined_loop_name.py:103:9: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target | -103 | # For -> for, outer loop unpacks with asterisk -104 | for i, *j in []: -105 | for j in []: # error +101 | # For -> for, both loops unpack tuple +102 | for (i, (j, k)) in []: +103 | for i, j in enumerate([]): # two errors | ^ PLW2901 -106 | pass +104 | pass | -redefined_loop_name.py:122:13: PLW2901 `for` loop variable `i` overwritten by assignment target +redefined_loop_name.py:103:12: PLW2901 Outer `for` loop variable `j` overwritten by inner `for` loop target | -120 | def f(): -121 | for i in []: # no error -122 | i = 2 # error +101 | # For -> for, both loops unpack tuple +102 | for (i, (j, k)) in []: +103 | for i, j in enumerate([]): # two errors + | ^ PLW2901 +104 | pass + | + +redefined_loop_name.py:120:9: PLW2901 Outer `for` loop variable `j` overwritten by inner `for` loop target + | +118 | # For -> for, outer loop unpacks with asterisk +119 | for i, *j in []: +120 | for j in []: # error + | ^ PLW2901 +121 | pass + | + +redefined_loop_name.py:137:13: PLW2901 `for` loop variable `i` overwritten by assignment target + | +135 | def f(): +136 | for i in []: # no error +137 | i = 2 # error | ^ PLW2901 -123 | -124 | # For -> class definition -> for -> for +138 | +139 | # For -> class definition -> for -> for | -redefined_loop_name.py:128:17: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target +redefined_loop_name.py:143:17: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target | -126 | class A: -127 | for i in []: # no error -128 | for i in []: # error +141 | class A: +142 | for i in []: # no error +143 | for i in []: # error | ^ PLW2901 -129 | pass +144 | pass | -redefined_loop_name.py:143:5: PLW2901 `for` loop variable `a[0]` overwritten by assignment target +redefined_loop_name.py:158:5: PLW2901 `for` loop variable `a[0]` overwritten by assignment target | -141 | # For target with subscript -> assignment -142 | for a[0] in []: -143 | a[0] = 2 # error +156 | # For target with subscript -> assignment +157 | for a[0] in []: +158 | a[0] = 2 # error | ^^^^ PLW2901 -144 | a[1] = 2 # no error +159 | a[1] = 2 # no error | -redefined_loop_name.py:148:5: PLW2901 `for` loop variable `a['i']` overwritten by assignment target +redefined_loop_name.py:163:5: PLW2901 `for` loop variable `a['i']` overwritten by assignment target | -146 | # For target with subscript -> assignment -147 | for a['i'] in []: -148 | a['i'] = 2 # error +161 | # For target with subscript -> assignment +162 | for a['i'] in []: +163 | a['i'] = 2 # error | ^^^^^^ PLW2901 -149 | a['j'] = 2 # no error +164 | a['j'] = 2 # no error | -redefined_loop_name.py:153:5: PLW2901 `for` loop variable `a.i` overwritten by assignment target +redefined_loop_name.py:168:5: PLW2901 `for` loop variable `a.i` overwritten by assignment target | -151 | # For target with attribute -> assignment -152 | for a.i in []: -153 | a.i = 2 # error +166 | # For target with attribute -> assignment +167 | for a.i in []: +168 | a.i = 2 # error | ^^^ PLW2901 -154 | a.j = 2 # no error +169 | a.j = 2 # no error | -redefined_loop_name.py:158:5: PLW2901 `for` loop variable `a.i.j` overwritten by assignment target +redefined_loop_name.py:173:5: PLW2901 `for` loop variable `a.i.j` overwritten by assignment target | -156 | # For target with double nested attribute -> assignment -157 | for a.i.j in []: -158 | a.i.j = 2 # error +171 | # For target with double nested attribute -> assignment +172 | for a.i.j in []: +173 | a.i.j = 2 # error | ^^^^^ PLW2901 -159 | a.j.i = 2 # no error +174 | a.j.i = 2 # no error | -redefined_loop_name.py:163:5: PLW2901 `for` loop variable `a.i` overwritten by assignment target +redefined_loop_name.py:178:5: PLW2901 `for` loop variable `a.i` overwritten by assignment target | -161 | # For target with attribute -> assignment with different spacing -162 | for a.i in []: -163 | a. i = 2 # error +176 | # For target with attribute -> assignment with different spacing +177 | for a.i in []: +178 | a. i = 2 # error | ^^^^ PLW2901 -164 | for a. i in []: -165 | a.i = 2 # error +179 | for a. i in []: +180 | a.i = 2 # error | -redefined_loop_name.py:165:5: PLW2901 `for` loop variable `a.i` overwritten by assignment target +redefined_loop_name.py:180:5: PLW2901 `for` loop variable `a.i` overwritten by assignment target | -163 | a. i = 2 # error -164 | for a. i in []: -165 | a.i = 2 # error +178 | a. i = 2 # error +179 | for a. i in []: +180 | a.i = 2 # error | ^^^ PLW2901 | From 6143065fc28d4572b7923725578faa8c4b6bda4f Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Thu, 15 Jun 2023 15:49:54 -0500 Subject: [PATCH 076/447] Add Applicability to flake8_comma fixes (#5127) ## Summary Fixes some of #4184 --- .../flake8_commas/rules/trailing_commas.rs | 6 +- ...rules__flake8_commas__tests__COM81.py.snap | 86 +++++++++---------- 2 files changed, 45 insertions(+), 47 deletions(-) diff --git a/crates/ruff/src/rules/flake8_commas/rules/trailing_commas.rs b/crates/ruff/src/rules/flake8_commas/rules/trailing_commas.rs index eaee333c3c..979bfd80c2 100644 --- a/crates/ruff/src/rules/flake8_commas/rules/trailing_commas.rs +++ b/crates/ruff/src/rules/flake8_commas/rules/trailing_commas.rs @@ -326,8 +326,7 @@ pub(crate) fn trailing_commas( let comma = prev.spanned.unwrap(); let mut diagnostic = Diagnostic::new(ProhibitedTrailingComma, comma.1); if settings.rules.should_fix(Rule::ProhibitedTrailingComma) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(diagnostic.range()))); + diagnostic.set_fix(Fix::automatic(Edit::range_deletion(diagnostic.range()))); } diagnostics.push(diagnostic); } @@ -367,8 +366,7 @@ pub(crate) fn trailing_commas( // removing any brackets in the same linter pass - doing both at the same time could // lead to a syntax error. let contents = locator.slice(missing_comma.1); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( format!("{contents},"), missing_comma.1, ))); diff --git a/crates/ruff/src/rules/flake8_commas/snapshots/ruff__rules__flake8_commas__tests__COM81.py.snap b/crates/ruff/src/rules/flake8_commas/snapshots/ruff__rules__flake8_commas__tests__COM81.py.snap index eb778af586..e4314c5b64 100644 --- a/crates/ruff/src/rules/flake8_commas/snapshots/ruff__rules__flake8_commas__tests__COM81.py.snap +++ b/crates/ruff/src/rules/flake8_commas/snapshots/ruff__rules__flake8_commas__tests__COM81.py.snap @@ -12,7 +12,7 @@ COM81.py:4:18: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 1 1 | # ==> bad_function_call.py <== 2 2 | bad_function_call( 3 3 | param1='test', @@ -32,7 +32,7 @@ COM81.py:10:6: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 7 7 | bad_list = [ 8 8 | 1, 9 9 | 2, @@ -53,7 +53,7 @@ COM81.py:16:6: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 13 13 | bad_list_with_comment = [ 14 14 | 1, 15 15 | 2, @@ -72,7 +72,7 @@ COM81.py:23:6: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 20 20 | bad_list_with_extra_empty = [ 21 21 | 1, 22 22 | 2, @@ -159,7 +159,7 @@ COM81.py:70:8: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 67 67 | pass 68 68 | 69 69 | {'foo': foo}['foo']( @@ -178,7 +178,7 @@ COM81.py:78:8: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 75 75 | ) 76 76 | 77 77 | (foo)( @@ -197,7 +197,7 @@ COM81.py:86:8: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 83 83 | ) 84 84 | 85 85 | [foo][0]( @@ -217,7 +217,7 @@ COM81.py:152:6: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 149 149 | 150 150 | # ==> keyword_before_parenth_form/base_bad.py <== 151 151 | from x import ( @@ -237,7 +237,7 @@ COM81.py:158:11: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 155 155 | assert( 156 156 | SyntaxWarning, 157 157 | ThrownHere, @@ -258,7 +258,7 @@ COM81.py:293:15: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 290 290 | 291 291 | # ==> multiline_bad_dict.py <== 292 292 | multiline_bad_dict = { @@ -279,7 +279,7 @@ COM81.py:304:14: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 301 301 | 302 302 | def func_bad( 303 303 | a = 3, @@ -300,7 +300,7 @@ COM81.py:310:14: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 307 307 | 308 308 | # ==> multiline_bad_function_one_param.py <== 309 309 | def func( @@ -319,7 +319,7 @@ COM81.py:316:10: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 313 313 | 314 314 | 315 315 | func( @@ -339,7 +339,7 @@ COM81.py:322:15: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 319 319 | # ==> multiline_bad_or_dict.py <== 320 320 | multiline_bad_or_dict = { 321 321 | "good": True or False, @@ -359,7 +359,7 @@ COM81.py:368:15: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 365 365 | 366 366 | multiline_index_access[ 367 367 | "probably fine", @@ -379,7 +379,7 @@ COM81.py:375:15: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 372 372 | "fine", 373 373 | "fine", 374 374 | : @@ -399,7 +399,7 @@ COM81.py:404:15: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 401 401 | "fine", 402 402 | "fine" 403 403 | : @@ -419,7 +419,7 @@ COM81.py:432:15: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 429 429 | "fine" 430 430 | : 431 431 | "fine", @@ -439,7 +439,7 @@ COM81.py:485:21: COM819 [*] Trailing comma prohibited | = help: Remove trailing comma -ℹ Suggested fix +ℹ Fix 482 482 | ) 483 483 | 484 484 | # ==> prohibited.py <== @@ -460,7 +460,7 @@ COM81.py:487:13: COM819 [*] Trailing comma prohibited | = help: Remove trailing comma -ℹ Suggested fix +ℹ Fix 484 484 | # ==> prohibited.py <== 485 485 | foo = ['a', 'b', 'c',] 486 486 | @@ -480,7 +480,7 @@ COM81.py:489:18: COM819 [*] Trailing comma prohibited | = help: Remove trailing comma -ℹ Suggested fix +ℹ Fix 486 486 | 487 487 | bar = { a: b,} 488 488 | @@ -501,7 +501,7 @@ COM81.py:494:6: COM819 [*] Trailing comma prohibited | = help: Remove trailing comma -ℹ Suggested fix +ℹ Fix 491 491 | 492 492 | (0,) 493 493 | @@ -522,7 +522,7 @@ COM81.py:496:21: COM819 [*] Trailing comma prohibited | = help: Remove trailing comma -ℹ Suggested fix +ℹ Fix 493 493 | 494 494 | (0, 1,) 495 495 | @@ -543,7 +543,7 @@ COM81.py:498:13: COM819 [*] Trailing comma prohibited | = help: Remove trailing comma -ℹ Suggested fix +ℹ Fix 495 495 | 496 496 | foo = ['a', 'b', 'c', ] 497 497 | @@ -563,7 +563,7 @@ COM81.py:500:18: COM819 [*] Trailing comma prohibited | = help: Remove trailing comma -ℹ Suggested fix +ℹ Fix 497 497 | 498 498 | bar = { a: b, } 499 499 | @@ -584,7 +584,7 @@ COM81.py:505:6: COM819 [*] Trailing comma prohibited | = help: Remove trailing comma -ℹ Suggested fix +ℹ Fix 502 502 | 503 503 | (0, ) 504 504 | @@ -605,7 +605,7 @@ COM81.py:511:10: COM819 [*] Trailing comma prohibited | = help: Remove trailing comma -ℹ Suggested fix +ℹ Fix 508 508 | 509 509 | image[:,] 510 510 | @@ -626,7 +626,7 @@ COM81.py:513:9: COM819 [*] Trailing comma prohibited | = help: Remove trailing comma -ℹ Suggested fix +ℹ Fix 510 510 | 511 511 | image[:,:,] 512 512 | @@ -647,7 +647,7 @@ COM81.py:519:13: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 516 516 | def function( 517 517 | foo, 518 518 | bar, @@ -668,7 +668,7 @@ COM81.py:526:10: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 523 523 | def function( 524 524 | foo, 525 525 | bar, @@ -689,7 +689,7 @@ COM81.py:534:16: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 531 531 | foo, 532 532 | bar, 533 533 | *args, @@ -709,7 +709,7 @@ COM81.py:541:13: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 538 538 | result = function( 539 539 | foo, 540 540 | bar, @@ -729,7 +729,7 @@ COM81.py:547:24: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 544 544 | result = function( 545 545 | foo, 546 546 | bar, @@ -750,7 +750,7 @@ COM81.py:554:15: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 551 551 | ham, 552 552 | spam, 553 553 | *args, @@ -769,7 +769,7 @@ COM81.py:561:13: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 558 558 | # In python 3.5 if it's not a function def, commas are mandatory. 559 559 | 560 560 | foo( @@ -788,7 +788,7 @@ COM81.py:565:13: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 562 562 | ) 563 563 | 564 564 | { @@ -807,7 +807,7 @@ COM81.py:573:10: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 570 570 | ) 571 571 | 572 572 | { @@ -826,7 +826,7 @@ COM81.py:577:10: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 574 574 | } 575 575 | 576 576 | [ @@ -847,7 +847,7 @@ COM81.py:583:10: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 580 580 | def foo( 581 581 | ham, 582 582 | spam, @@ -868,7 +868,7 @@ COM81.py:590:13: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 587 587 | def foo( 588 588 | ham, 589 589 | spam, @@ -889,7 +889,7 @@ COM81.py:598:15: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 595 595 | ham, 596 596 | spam, 597 597 | *args, @@ -909,7 +909,7 @@ COM81.py:627:20: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 624 624 | result = function( 625 625 | foo, 626 626 | bar, @@ -929,7 +929,7 @@ COM81.py:632:42: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 629 629 | 630 630 | # Make sure the COM812 and UP034 rules don't autofix simultaneously and cause a syntax error. 631 631 | the_first_one = next( From 89b328c6be9daeb59242664c26c6912526f6ef76 Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Thu, 15 Jun 2023 15:50:19 -0500 Subject: [PATCH 077/447] Add Applicability to flake8_logging_format fixes (#5129) ## Summary Fixes some of #4184 --- .../ruff/src/rules/flake8_logging_format/rules/logging_call.rs | 3 +-- .../ruff__rules__flake8_logging_format__tests__G010.py.snap | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs b/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs index c3acad982c..574ae4c488 100644 --- a/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs +++ b/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs @@ -177,8 +177,7 @@ pub(crate) fn logging_call( { let mut diagnostic = Diagnostic::new(LoggingWarn, level_call_range); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( "warning".to_string(), level_call_range, ))); diff --git a/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G010.py.snap b/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G010.py.snap index 66a96150f2..b0a57cc6d1 100644 --- a/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G010.py.snap +++ b/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G010.py.snap @@ -11,7 +11,7 @@ G010.py:4:9: G010 [*] Logging statement uses `warn` instead of `warning` | = help: Convert to `warn` -ℹ Suggested fix +ℹ Fix 1 1 | import logging 2 2 | from distutils import log 3 3 | From 1e383483f76356ceb6ead2291069a394a2e9fea9 Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Thu, 15 Jun 2023 15:50:54 -0500 Subject: [PATCH 078/447] Add Applicability to flake8_quotes fixes (#5130) ## Summary Fixes some of #4184 --- .../src/rules/flake8_quotes/rules/from_tokens.rs | 12 ++++-------- ...ocstring_doubles_over_docstring_doubles.py.snap | 10 +++++----- ...ng_doubles_over_docstring_doubles_class.py.snap | 4 ++-- ...doubles_over_docstring_doubles_function.py.snap | 10 +++++----- ...over_docstring_doubles_module_multiline.py.snap | 4 ++-- ...ver_docstring_doubles_module_singleline.py.snap | 4 ++-- ...ocstring_doubles_over_docstring_singles.py.snap | 6 +++--- ...ng_doubles_over_docstring_singles_class.py.snap | 6 +++--- ...doubles_over_docstring_singles_function.py.snap | 6 +++--- ...over_docstring_singles_module_multiline.py.snap | 2 +- ...ver_docstring_singles_module_singleline.py.snap | 2 +- ...ocstring_singles_over_docstring_doubles.py.snap | 6 +++--- ...ng_singles_over_docstring_doubles_class.py.snap | 6 +++--- ...singles_over_docstring_doubles_function.py.snap | 6 +++--- ...over_docstring_doubles_module_multiline.py.snap | 2 +- ...ver_docstring_doubles_module_singleline.py.snap | 2 +- ...ocstring_singles_over_docstring_singles.py.snap | 12 ++++++------ ...ng_singles_over_docstring_singles_class.py.snap | 4 ++-- ...singles_over_docstring_singles_function.py.snap | 10 +++++----- ...over_docstring_singles_module_multiline.py.snap | 4 ++-- ...ver_docstring_singles_module_singleline.py.snap | 4 ++-- ...es__tests__require_doubles_over_singles.py.snap | 6 +++--- ...s__require_doubles_over_singles_escaped.py.snap | 4 ++-- ...__require_doubles_over_singles_implicit.py.snap | 14 +++++++------- ...e_doubles_over_singles_multiline_string.py.snap | 2 +- ...es__tests__require_singles_over_doubles.py.snap | 6 +++--- ...s__require_singles_over_doubles_escaped.py.snap | 6 +++--- ...__require_singles_over_doubles_implicit.py.snap | 14 +++++++------- ...e_singles_over_doubles_multiline_string.py.snap | 2 +- 29 files changed, 86 insertions(+), 90 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 87e9e8651c..6f0000e92c 100644 --- a/crates/ruff/src/rules/flake8_quotes/rules/from_tokens.rs +++ b/crates/ruff/src/rules/flake8_quotes/rules/from_tokens.rs @@ -284,8 +284,7 @@ fn docstring(locator: &Locator, range: TextRange, settings: &Settings) -> Option fixed_contents.push_str("e); fixed_contents.push_str(string_contents); fixed_contents.push_str("e); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( fixed_contents, range, ))); @@ -358,8 +357,7 @@ fn strings(locator: &Locator, sequence: &[TextRange], settings: &Settings) -> Ve fixed_contents.push_str(quote); fixed_contents.push_str(string_contents); fixed_contents.push_str(quote); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( fixed_contents, *range, ))); @@ -425,8 +423,7 @@ fn strings(locator: &Locator, sequence: &[TextRange], settings: &Settings) -> Ve fixed_contents.push(quote); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( fixed_contents, *range, ))); @@ -452,8 +449,7 @@ fn strings(locator: &Locator, sequence: &[TextRange], settings: &Settings) -> Ve fixed_contents.push(quote); fixed_contents.push_str(string_contents); fixed_contents.push(quote); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( fixed_contents, *range, ))); diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles.py.snap index 56a74f2ee0..00664f2737 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles.py.snap @@ -14,7 +14,7 @@ docstring_doubles.py:5:1: Q001 [*] Double quote multiline found but single quote | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 2 2 | Double quotes multiline module docstring 3 3 | """ 4 4 | @@ -41,7 +41,7 @@ docstring_doubles.py:16:5: Q001 [*] Double quote multiline found but single quot | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 13 13 | Double quotes multiline class docstring 14 14 | """ 15 15 | @@ -66,7 +66,7 @@ docstring_doubles.py:21:21: Q001 [*] Double quote multiline found but single quo | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 18 18 | """ 19 19 | 20 20 | # The colon in the list indexing below is an edge case for the docstring scanner @@ -92,7 +92,7 @@ docstring_doubles.py:30:9: Q001 [*] Double quote multiline found but single quot | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 27 27 | 28 28 | some_expression = 'hello world' 29 29 | @@ -117,7 +117,7 @@ docstring_doubles.py:35:13: Q001 [*] Double quote multiline found but single quo | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 32 32 | """ 33 33 | 34 34 | if l: diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_class.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_class.py.snap index 39bbce6bb6..1cac8d3b14 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_class.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_class.py.snap @@ -12,7 +12,7 @@ docstring_doubles_class.py:3:5: Q001 [*] Double quote multiline found but single | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | class SingleLineDocstrings(): 2 2 | """ Double quotes single line class docstring """ 3 |- """ Not a docstring """ @@ -32,7 +32,7 @@ docstring_doubles_class.py:5:23: Q001 [*] Double quote multiline found but singl | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 2 2 | """ Double quotes single line class docstring """ 3 3 | """ Not a docstring """ 4 4 | diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_function.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_function.py.snap index 8116a79371..561aba67da 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_function.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_function.py.snap @@ -11,7 +11,7 @@ docstring_doubles_function.py:3:5: Q001 [*] Double quote multiline found but sin | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | def foo(): 2 2 | """function without params, single line docstring""" 3 |- """ not a docstring""" @@ -30,7 +30,7 @@ docstring_doubles_function.py:11:5: Q001 [*] Double quote multiline found but si | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 8 8 | """ 9 9 | function without params, multiline docstring 10 10 | """ @@ -51,7 +51,7 @@ docstring_doubles_function.py:15:39: Q001 [*] Double quote multiline found but s | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 12 12 | return 13 13 | 14 14 | @@ -74,7 +74,7 @@ docstring_doubles_function.py:17:5: Q001 [*] Double quote multiline found but si | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 14 14 | 15 15 | def fun_with_params_no_docstring(a, b=""" 16 16 | not a @@ -93,7 +93,7 @@ docstring_doubles_function.py:22:5: Q001 [*] Double quote multiline found but si | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 19 19 | 20 20 | 21 21 | def fun_with_params_no_docstring2(a, b=c[foo():], c=\ diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_multiline.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_multiline.py.snap index 83e43cece8..dc6f5236d4 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_multiline.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_multiline.py.snap @@ -14,7 +14,7 @@ docstring_doubles_module_multiline.py:4:1: Q001 [*] Double quote multiline found | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | """ 2 2 | Double quotes multiline module docstring 3 3 | """ @@ -38,7 +38,7 @@ docstring_doubles_module_multiline.py:9:1: Q001 [*] Double quote multiline found | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 6 6 | """ 7 7 | def foo(): 8 8 | pass diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_singleline.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_singleline.py.snap index 2b1e4c9f7b..86ca4127b2 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_singleline.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_singleline.py.snap @@ -11,7 +11,7 @@ docstring_doubles_module_singleline.py:2:1: Q001 [*] Double quote multiline foun | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | """ Double quotes singleline module docstring """ 2 |-""" this is not a docstring """ 2 |+''' this is not a docstring ''' @@ -28,7 +28,7 @@ docstring_doubles_module_singleline.py:6:1: Q001 [*] Double quote multiline foun | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 3 3 | 4 4 | def foo(): 5 5 | pass diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles.py.snap index 5b500bfc98..2315b0b9f0 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles.py.snap @@ -12,7 +12,7 @@ docstring_singles.py:1:1: Q002 [*] Single quote docstring found but double quote | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 1 |-''' 1 |+""" 2 2 | Single quotes multiline module docstring @@ -36,7 +36,7 @@ docstring_singles.py:14:5: Q002 [*] Single quote docstring found but double quot | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 11 11 | class Cls(MakeKlass(''' 12 12 | class params \t not a docstring 13 13 | ''')): @@ -63,7 +63,7 @@ docstring_singles.py:26:9: Q002 [*] Single quote docstring found but double quot | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 23 23 | def f(self, bar=''' 24 24 | definitely not a docstring''', 25 25 | val=l[Cls():3]): diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_class.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_class.py.snap index 1ff2255082..2c822e35c8 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_class.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_class.py.snap @@ -10,7 +10,7 @@ docstring_singles_class.py:2:5: Q002 [*] Single quote docstring found but double | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | class SingleLineDocstrings(): 2 |- ''' Double quotes single line class docstring ''' 2 |+ """ Double quotes single line class docstring """ @@ -27,7 +27,7 @@ docstring_singles_class.py:6:9: Q002 [*] Single quote docstring found but double | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 3 3 | ''' Not a docstring ''' 4 4 | 5 5 | def foo(self, bar='''not a docstring'''): @@ -46,7 +46,7 @@ docstring_singles_class.py:9:29: Q002 [*] Single quote docstring found but doubl | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 6 6 | ''' Double quotes single line method docstring''' 7 7 | pass 8 8 | diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_function.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_function.py.snap index b0065abd99..6d878507ce 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_function.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_function.py.snap @@ -11,7 +11,7 @@ docstring_singles_function.py:2:5: Q002 [*] Single quote docstring found but dou | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | def foo(): 2 |- '''function without params, single line docstring''' 2 |+ """function without params, single line docstring""" @@ -32,7 +32,7 @@ docstring_singles_function.py:8:5: Q002 [*] Single quote docstring found but dou | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 5 5 | 6 6 | 7 7 | def foo2(): @@ -53,7 +53,7 @@ docstring_singles_function.py:27:5: Q002 [*] Single quote docstring found but do | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 24 24 | 25 25 | 26 26 | def function_with_single_docstring(a): diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_module_multiline.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_module_multiline.py.snap index a4858db55d..3ec2a92e01 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_module_multiline.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_module_multiline.py.snap @@ -12,7 +12,7 @@ docstring_singles_module_multiline.py:1:1: Q002 [*] Single quote docstring found | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 1 |-''' 1 |+""" 2 2 | Double quotes multiline module docstring diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_module_singleline.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_module_singleline.py.snap index b6f815e5e1..75b12d38c6 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_module_singleline.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_module_singleline.py.snap @@ -9,7 +9,7 @@ docstring_singles_module_singleline.py:1:1: Q002 [*] Single quote docstring foun | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 1 |-''' Double quotes singleline module docstring ''' 1 |+""" Double quotes singleline module docstring """ 2 2 | ''' this is not a docstring ''' diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles.py.snap index 60352254df..3e4aff52d5 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles.py.snap @@ -12,7 +12,7 @@ docstring_doubles.py:1:1: Q002 [*] Double quote docstring found but single quote | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 1 |-""" 1 |+''' 2 2 | Double quotes multiline module docstring @@ -35,7 +35,7 @@ docstring_doubles.py:12:5: Q002 [*] Double quote docstring found but single quot | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 9 9 | l = [] 10 10 | 11 11 | class Cls: @@ -62,7 +62,7 @@ docstring_doubles.py:24:9: Q002 [*] Double quote docstring found but single quot | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 21 21 | def f(self, bar=""" 22 22 | definitely not a docstring""", 23 23 | val=l[Cls():3]): diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_class.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_class.py.snap index 743b6d6525..4f1be427c9 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_class.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_class.py.snap @@ -10,7 +10,7 @@ docstring_doubles_class.py:2:5: Q002 [*] Double quote docstring found but single | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | class SingleLineDocstrings(): 2 |- """ Double quotes single line class docstring """ 2 |+ ''' Double quotes single line class docstring ''' @@ -27,7 +27,7 @@ docstring_doubles_class.py:6:9: Q002 [*] Double quote docstring found but single | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 3 3 | """ Not a docstring """ 4 4 | 5 5 | def foo(self, bar="""not a docstring"""): @@ -46,7 +46,7 @@ docstring_doubles_class.py:9:29: Q002 [*] Double quote docstring found but singl | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 6 6 | """ Double quotes single line method docstring""" 7 7 | pass 8 8 | diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_function.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_function.py.snap index 87d3c0c358..013f064e86 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_function.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_function.py.snap @@ -11,7 +11,7 @@ docstring_doubles_function.py:2:5: Q002 [*] Double quote docstring found but sin | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | def foo(): 2 |- """function without params, single line docstring""" 2 |+ '''function without params, single line docstring''' @@ -32,7 +32,7 @@ docstring_doubles_function.py:8:5: Q002 [*] Double quote docstring found but sin | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 5 5 | 6 6 | 7 7 | def foo2(): @@ -53,7 +53,7 @@ docstring_doubles_function.py:27:5: Q002 [*] Double quote docstring found but si | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 24 24 | 25 25 | 26 26 | def function_with_single_docstring(a): diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_module_multiline.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_module_multiline.py.snap index 4a4a7d27dc..e2c5e024af 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_module_multiline.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_module_multiline.py.snap @@ -12,7 +12,7 @@ docstring_doubles_module_multiline.py:1:1: Q002 [*] Double quote docstring found | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 1 |-""" 1 |+''' 2 2 | Double quotes multiline module docstring diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_module_singleline.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_module_singleline.py.snap index ba51829241..c6332e463f 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_module_singleline.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_module_singleline.py.snap @@ -9,7 +9,7 @@ docstring_doubles_module_singleline.py:1:1: Q002 [*] Double quote docstring foun | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 1 |-""" Double quotes singleline module docstring """ 1 |+''' Double quotes singleline module docstring ''' 2 2 | """ this is not a docstring """ diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles.py.snap index 8724ea381a..90eb8de9cc 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles.py.snap @@ -14,7 +14,7 @@ docstring_singles.py:5:1: Q001 [*] Single quote multiline found but double quote | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 2 2 | Single quotes multiline module docstring 3 3 | ''' 4 4 | @@ -41,7 +41,7 @@ docstring_singles.py:11:21: Q001 [*] Single quote multiline found but double quo | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 8 8 | 9 9 | l = [] 10 10 | @@ -68,7 +68,7 @@ docstring_singles.py:18:5: Q001 [*] Single quote multiline found but double quot | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 15 15 | Single quotes multiline class docstring 16 16 | ''' 17 17 | @@ -93,7 +93,7 @@ docstring_singles.py:23:21: Q001 [*] Single quote multiline found but double quo | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 20 20 | ''' 21 21 | 22 22 | # The colon in the list indexing below is an edge case for the docstring scanner @@ -119,7 +119,7 @@ docstring_singles.py:32:9: Q001 [*] Single quote multiline found but double quot | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 29 29 | 30 30 | some_expression = 'hello world' 31 31 | @@ -144,7 +144,7 @@ docstring_singles.py:37:13: Q001 [*] Single quote multiline found but double quo | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 34 34 | ''' 35 35 | 36 36 | if l: diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_class.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_class.py.snap index e8fc6da0e5..323f502a22 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_class.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_class.py.snap @@ -12,7 +12,7 @@ docstring_singles_class.py:3:5: Q001 [*] Single quote multiline found but double | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | class SingleLineDocstrings(): 2 2 | ''' Double quotes single line class docstring ''' 3 |- ''' Not a docstring ''' @@ -32,7 +32,7 @@ docstring_singles_class.py:5:23: Q001 [*] Single quote multiline found but doubl | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 2 2 | ''' Double quotes single line class docstring ''' 3 3 | ''' Not a docstring ''' 4 4 | diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_function.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_function.py.snap index 1c5f902f93..fe7371af8f 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_function.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_function.py.snap @@ -11,7 +11,7 @@ docstring_singles_function.py:3:5: Q001 [*] Single quote multiline found but dou | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | def foo(): 2 2 | '''function without params, single line docstring''' 3 |- ''' not a docstring''' @@ -30,7 +30,7 @@ docstring_singles_function.py:11:5: Q001 [*] Single quote multiline found but do | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 8 8 | ''' 9 9 | function without params, multiline docstring 10 10 | ''' @@ -51,7 +51,7 @@ docstring_singles_function.py:15:39: Q001 [*] Single quote multiline found but d | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 12 12 | return 13 13 | 14 14 | @@ -74,7 +74,7 @@ docstring_singles_function.py:17:5: Q001 [*] Single quote multiline found but do | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 14 14 | 15 15 | def fun_with_params_no_docstring(a, b=''' 16 16 | not a @@ -93,7 +93,7 @@ docstring_singles_function.py:22:5: Q001 [*] Single quote multiline found but do | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 19 19 | 20 20 | 21 21 | def fun_with_params_no_docstring2(a, b=c[foo():], c=\ diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_multiline.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_multiline.py.snap index 6286084ba4..ca3d7d259d 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_multiline.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_multiline.py.snap @@ -14,7 +14,7 @@ docstring_singles_module_multiline.py:4:1: Q001 [*] Single quote multiline found | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | ''' 2 2 | Double quotes multiline module docstring 3 3 | ''' @@ -38,7 +38,7 @@ docstring_singles_module_multiline.py:9:1: Q001 [*] Single quote multiline found | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 6 6 | ''' 7 7 | def foo(): 8 8 | pass diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_singleline.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_singleline.py.snap index 4b822211be..3d9926a1e6 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_singleline.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_singleline.py.snap @@ -11,7 +11,7 @@ docstring_singles_module_singleline.py:2:1: Q001 [*] Single quote multiline foun | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | ''' Double quotes singleline module docstring ''' 2 |-''' this is not a docstring ''' 2 |+""" this is not a docstring """ @@ -28,7 +28,7 @@ docstring_singles_module_singleline.py:6:1: Q001 [*] Single quote multiline foun | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 3 3 | 4 4 | def foo(): 5 5 | pass diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles.py.snap index 7513bc9db0..83354230b3 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles.py.snap @@ -10,7 +10,7 @@ singles.py:1:25: Q000 [*] Single quotes found but double quotes preferred | = help: Replace single quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 |-this_should_be_linted = 'single quote string' 1 |+this_should_be_linted = "single quote string" 2 2 | this_should_be_linted = u'double quote string' @@ -27,7 +27,7 @@ singles.py:2:25: Q000 [*] Single quotes found but double quotes preferred | = help: Replace single quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | this_should_be_linted = 'single quote string' 2 |-this_should_be_linted = u'double quote string' 2 |+this_should_be_linted = u"double quote string" @@ -44,7 +44,7 @@ singles.py:3:25: Q000 [*] Single quotes found but double quotes preferred | = help: Replace single quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | this_should_be_linted = 'single quote string' 2 2 | this_should_be_linted = u'double quote string' 3 |-this_should_be_linted = f'double quote string' diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_escaped.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_escaped.py.snap index c78cedfe0a..6dd7d7e8f6 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_escaped.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_escaped.py.snap @@ -10,7 +10,7 @@ singles_escaped.py:1:26: Q003 [*] Change outer quotes to avoid escaping inner qu | = help: Change outer quotes to avoid escaping inner quotes -ℹ Suggested fix +ℹ Fix 1 |-this_should_raise_Q003 = "This is a \"string\"" 1 |+this_should_raise_Q003 = 'This is a "string"' 2 2 | this_is_fine = "'This' is a \"string\"" @@ -27,7 +27,7 @@ singles_escaped.py:9:5: Q003 [*] Change outer quotes to avoid escaping inner quo | = help: Change outer quotes to avoid escaping inner quotes -ℹ Suggested fix +ℹ Fix 6 6 | this_is_fine = R"This is a \"string\"" 7 7 | this_should_raise = ( 8 8 | "This is a" diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_implicit.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_implicit.py.snap index 23f7be19e5..24ebd5c6f3 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_implicit.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_implicit.py.snap @@ -11,7 +11,7 @@ singles_implicit.py:2:5: Q000 [*] Single quotes found but double quotes preferre | = help: Replace single quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | x = ( 2 |- 'This' 2 |+ "This" @@ -30,7 +30,7 @@ singles_implicit.py:3:5: Q000 [*] Single quotes found but double quotes preferre | = help: Replace single quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | x = ( 2 2 | 'This' 3 |- 'is' @@ -49,7 +49,7 @@ singles_implicit.py:4:5: Q000 [*] Single quotes found but double quotes preferre | = help: Replace single quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | x = ( 2 2 | 'This' 3 3 | 'is' @@ -69,7 +69,7 @@ singles_implicit.py:8:5: Q000 [*] Single quotes found but double quotes preferre | = help: Replace single quotes with double quotes -ℹ Suggested fix +ℹ Fix 5 5 | ) 6 6 | 7 7 | x = ( @@ -90,7 +90,7 @@ singles_implicit.py:9:5: Q000 [*] Single quotes found but double quotes preferre | = help: Replace single quotes with double quotes -ℹ Suggested fix +ℹ Fix 6 6 | 7 7 | x = ( 8 8 | 'This' \ @@ -110,7 +110,7 @@ singles_implicit.py:10:5: Q000 [*] Single quotes found but double quotes preferr | = help: Replace single quotes with double quotes -ℹ Suggested fix +ℹ Fix 7 7 | x = ( 8 8 | 'This' \ 9 9 | 'is' \ @@ -129,7 +129,7 @@ singles_implicit.py:27:1: Q000 [*] Single quotes found but double quotes preferr | = help: Replace single quotes with double quotes -ℹ Suggested fix +ℹ Fix 24 24 | 25 25 | if True: 26 26 | 'This can use "single" quotes' diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_multiline_string.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_multiline_string.py.snap index 8477c544b3..20cd5ce322 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_multiline_string.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_multiline_string.py.snap @@ -13,7 +13,7 @@ singles_multiline_string.py:1:5: Q001 [*] Single quote multiline found but doubl | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 |-s = ''' This 'should' 1 |+s = """ This 'should' 2 2 | be diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles.py.snap index b7206b147e..0550913c80 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles.py.snap @@ -10,7 +10,7 @@ doubles.py:1:25: Q000 [*] Double quotes found but single quotes preferred | = help: Replace double quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 |-this_should_be_linted = "double quote string" 1 |+this_should_be_linted = 'double quote string' 2 2 | this_should_be_linted = u"double quote string" @@ -27,7 +27,7 @@ doubles.py:2:25: Q000 [*] Double quotes found but single quotes preferred | = help: Replace double quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | this_should_be_linted = "double quote string" 2 |-this_should_be_linted = u"double quote string" 2 |+this_should_be_linted = u'double quote string' @@ -44,7 +44,7 @@ doubles.py:3:25: Q000 [*] Double quotes found but single quotes preferred | = help: Replace double quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | this_should_be_linted = "double quote string" 2 2 | this_should_be_linted = u"double quote string" 3 |-this_should_be_linted = f"double quote string" diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_escaped.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_escaped.py.snap index 743d3219d3..6a31d7a11f 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_escaped.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_escaped.py.snap @@ -10,7 +10,7 @@ doubles_escaped.py:1:26: Q003 [*] Change outer quotes to avoid escaping inner qu | = help: Change outer quotes to avoid escaping inner quotes -ℹ Suggested fix +ℹ Fix 1 |-this_should_raise_Q003 = 'This is a \'string\'' 1 |+this_should_raise_Q003 = "This is a 'string'" 2 2 | this_should_raise_Q003 = 'This is \\ a \\\'string\'' @@ -27,7 +27,7 @@ doubles_escaped.py:2:26: Q003 [*] Change outer quotes to avoid escaping inner qu | = help: Change outer quotes to avoid escaping inner quotes -ℹ Suggested fix +ℹ Fix 1 1 | this_should_raise_Q003 = 'This is a \'string\'' 2 |-this_should_raise_Q003 = 'This is \\ a \\\'string\'' 2 |+this_should_raise_Q003 = "This is \\ a \\'string'" @@ -45,7 +45,7 @@ doubles_escaped.py:10:5: Q003 [*] Change outer quotes to avoid escaping inner qu | = help: Change outer quotes to avoid escaping inner quotes -ℹ Suggested fix +ℹ Fix 7 7 | this_is_fine = R'This is a \'string\'' 8 8 | this_should_raise = ( 9 9 | 'This is a' diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_implicit.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_implicit.py.snap index 6b1bb3c26a..dcd5cd4f75 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_implicit.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_implicit.py.snap @@ -11,7 +11,7 @@ doubles_implicit.py:2:5: Q000 [*] Double quotes found but single quotes preferre | = help: Replace double quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | x = ( 2 |- "This" 2 |+ 'This' @@ -30,7 +30,7 @@ doubles_implicit.py:3:5: Q000 [*] Double quotes found but single quotes preferre | = help: Replace double quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | x = ( 2 2 | "This" 3 |- "is" @@ -49,7 +49,7 @@ doubles_implicit.py:4:5: Q000 [*] Double quotes found but single quotes preferre | = help: Replace double quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | x = ( 2 2 | "This" 3 3 | "is" @@ -69,7 +69,7 @@ doubles_implicit.py:8:5: Q000 [*] Double quotes found but single quotes preferre | = help: Replace double quotes with single quotes -ℹ Suggested fix +ℹ Fix 5 5 | ) 6 6 | 7 7 | x = ( @@ -90,7 +90,7 @@ doubles_implicit.py:9:5: Q000 [*] Double quotes found but single quotes preferre | = help: Replace double quotes with single quotes -ℹ Suggested fix +ℹ Fix 6 6 | 7 7 | x = ( 8 8 | "This" \ @@ -110,7 +110,7 @@ doubles_implicit.py:10:5: Q000 [*] Double quotes found but single quotes preferr | = help: Replace double quotes with single quotes -ℹ Suggested fix +ℹ Fix 7 7 | x = ( 8 8 | "This" \ 9 9 | "is" \ @@ -129,7 +129,7 @@ doubles_implicit.py:27:1: Q000 [*] Double quotes found but single quotes preferr | = help: Replace double quotes with single quotes -ℹ Suggested fix +ℹ Fix 24 24 | 25 25 | if True: 26 26 | "This can use 'double' quotes" diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_multiline_string.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_multiline_string.py.snap index 67502e94f2..07ee108c25 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_multiline_string.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_multiline_string.py.snap @@ -13,7 +13,7 @@ doubles_multiline_string.py:1:5: Q001 [*] Double quote multiline found but singl | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 |-s = """ This "should" 1 |+s = ''' This "should" 2 2 | be From 1f856aa57603a2837f8f9f861349fcbb861ff445 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 15 Jun 2023 16:53:02 -0400 Subject: [PATCH 079/447] Don't treat straight imports of __future__ as `__future__` imports (#5128) ## Summary If you `import __future__`, it's not subject to the same rules as `from __future__ import feature` -- i.e., this is fine: ```python x = 1 import __future__ ``` It doesn't really make sense to treat these as `__future__` imports (though I can't imagine anyone ever does this anyway). --- .../test/fixtures/pyflakes/F401_18.py | 11 ++++++++++ crates/ruff/src/checkers/ast/mod.rs | 19 +--------------- crates/ruff/src/rules/pyflakes/mod.rs | 1 + ...les__pyflakes__tests__F401_F401_18.py.snap | 22 +++++++++++++++++++ ..._rules__pyflakes__tests__F404_F404.py.snap | 8 ------- 5 files changed, 35 insertions(+), 26 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/pyflakes/F401_18.py create mode 100644 crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_18.py.snap diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F401_18.py b/crates/ruff/resources/test/fixtures/pyflakes/F401_18.py new file mode 100644 index 0000000000..d23a05ef48 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pyflakes/F401_18.py @@ -0,0 +1,11 @@ +"""Test that straight `__future__` imports are considered unused.""" + + +def f(): + import __future__ + + +def f(): + import __future__ + + print(__future__.absolute_import) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 11c69d01af..3048a372a9 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -805,24 +805,7 @@ where } for alias in names { - if &alias.name == "__future__" { - let name = alias.asname.as_ref().unwrap_or(&alias.name); - self.add_binding( - name, - alias.identifier(self.locator), - BindingKind::FutureImportation, - BindingFlags::empty(), - ); - - if self.enabled(Rule::LateFutureImport) { - if self.semantic.seen_futures_boundary() { - self.diagnostics.push(Diagnostic::new( - pyflakes::rules::LateFutureImport, - stmt.range(), - )); - } - } - } else if alias.name.contains('.') && alias.asname.is_none() { + if alias.name.contains('.') && alias.asname.is_none() { // Given `import foo.bar`, `name` would be "foo", and `qualified_name` would be // "foo.bar". let name = alias.name.split('.').next().unwrap(); diff --git a/crates/ruff/src/rules/pyflakes/mod.rs b/crates/ruff/src/rules/pyflakes/mod.rs index 1f8d587325..87876e96d6 100644 --- a/crates/ruff/src/rules/pyflakes/mod.rs +++ b/crates/ruff/src/rules/pyflakes/mod.rs @@ -43,6 +43,7 @@ mod tests { #[test_case(Rule::UnusedImport, Path::new("F401_15.py"))] #[test_case(Rule::UnusedImport, Path::new("F401_16.py"))] #[test_case(Rule::UnusedImport, Path::new("F401_17.py"))] + #[test_case(Rule::UnusedImport, Path::new("F401_18.py"))] #[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"))] #[test_case(Rule::UndefinedLocalWithImportStar, Path::new("F403.py"))] #[test_case(Rule::LateFutureImport, Path::new("F404.py"))] diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_18.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_18.py.snap new file mode 100644 index 0000000000..d49e67f268 --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_18.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +F401_18.py:5:12: F401 [*] `__future__` imported but unused + | +4 | def f(): +5 | import __future__ + | ^^^^^^^^^^ F401 + | + = help: Remove unused import: `future` + +ℹ Fix +2 2 | +3 3 | +4 4 | def f(): +5 |- import __future__ + 5 |+ pass +6 6 | +7 7 | +8 8 | def f(): + + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F404_F404.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F404_F404.py.snap index 6c63ee705b..feba0f8d20 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F404_F404.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F404_F404.py.snap @@ -11,12 +11,4 @@ F404.py:6:1: F404 `from __future__` imports must occur at the beginning of the f 8 | import __future__ | -F404.py:8:1: F404 `from __future__` imports must occur at the beginning of the file - | -6 | from __future__ import print_function -7 | -8 | import __future__ - | ^^^^^^^^^^^^^^^^^ F404 - | - From 26d19655db60faf4941fd093d1d4a4a00a99ede5 Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Thu, 15 Jun 2023 17:09:00 -0500 Subject: [PATCH 080/447] Add Applicability to flake8_tidy_imports (#5131) ## Summary Fixes some of https://github.com/astral-sh/ruff/issues/4184 --- .../src/rules/flake8_tidy_imports/rules/relative_imports.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/ruff/src/rules/flake8_tidy_imports/rules/relative_imports.rs b/crates/ruff/src/rules/flake8_tidy_imports/rules/relative_imports.rs index 6531cebd7e..6b8227be28 100644 --- a/crates/ruff/src/rules/flake8_tidy_imports/rules/relative_imports.rs +++ b/crates/ruff/src/rules/flake8_tidy_imports/rules/relative_imports.rs @@ -100,8 +100,7 @@ fn fix_banned_relative_import( range: TextRange::default(), }; let content = generator.stmt(&node.into()); - #[allow(deprecated)] - Some(Fix::unspecified(Edit::range_replacement( + Some(Fix::suggested(Edit::range_replacement( content, stmt.range(), ))) From 70c01257ca743c59fca2924a6957009c8dc811f6 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 15 Jun 2023 22:42:21 -0400 Subject: [PATCH 081/447] Minor formatting changes to `Checker` (#5135) --- crates/ruff/src/checkers/ast/mod.rs | 13 ------------- .../pylint/rules/load_before_global_declaration.rs | 1 + 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 3048a372a9..5161846b17 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -364,7 +364,6 @@ where |expr| self.semantic.resolve_call_path(expr), )); } - if self.enabled(Rule::AmbiguousFunctionName) { if let Some(diagnostic) = pycodestyle::rules::ambiguous_function_name(name, || { @@ -891,7 +890,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::LowercaseImportedAsNonLowercase) { if let Some(diagnostic) = pep8_naming::rules::lowercase_imported_as_non_lowercase( @@ -905,7 +903,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::CamelcaseImportedAsLowercase) { if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_lowercase( @@ -919,7 +916,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::CamelcaseImportedAsConstant) { if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_constant( @@ -933,7 +929,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::CamelcaseImportedAsAcronym) { if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_acronym( @@ -1076,7 +1071,6 @@ where if self.enabled(Rule::FutureFeatureNotDefined) { pyflakes::rules::future_feature_not_defined(self, alias); } - if self.enabled(Rule::LateFutureImport) { if self.semantic.seen_futures_boundary() { self.diagnostics.push(Diagnostic::new( @@ -1101,7 +1095,6 @@ where )); } } - if self.enabled(Rule::UndefinedLocalWithImportStar) { self.diagnostics.push(Diagnostic::new( pyflakes::rules::UndefinedLocalWithImportStar { @@ -1206,7 +1199,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::LowercaseImportedAsNonLowercase) { if let Some(diagnostic) = pep8_naming::rules::lowercase_imported_as_non_lowercase( @@ -1220,7 +1212,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::CamelcaseImportedAsLowercase) { if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_lowercase( @@ -1234,7 +1225,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::CamelcaseImportedAsConstant) { if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_constant( @@ -1248,7 +1238,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::CamelcaseImportedAsAcronym) { if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_acronym( @@ -2475,7 +2464,6 @@ where } } } - if self.enabled(Rule::TypeOfPrimitive) { pyupgrade::rules::type_of_primitive(self, expr, func, args); } @@ -3895,7 +3883,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::BuiltinVariableShadowing) { flake8_builtins::rules::builtin_variable_shadowing( self, diff --git a/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs b/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs index fbde805d1c..687692ffbd 100644 --- a/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs +++ b/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs @@ -52,6 +52,7 @@ impl Violation for LoadBeforeGlobalDeclaration { format!("Name `{name}` is used prior to global declaration on line {line}") } } + /// PLE0118 pub(crate) fn load_before_global_declaration(checker: &mut Checker, name: &str, expr: &Expr) { if let Some(stmt) = checker.semantic().global(name) { From 13813dc1b118fd5e3b6c4a62ee557c588955364b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 15 Jun 2023 23:49:40 -0400 Subject: [PATCH 082/447] Skip `DJ008` enforcement in stub files (#5139) Closes #5138. --- crates/ruff/src/checkers/ast/mod.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 5161846b17..c5260823c9 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -671,16 +671,18 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::DjangoModelWithoutDunderStr) { - if let Some(diagnostic) = - flake8_django::rules::model_without_dunder_str(self, bases, body, stmt) - { - self.diagnostics.push(diagnostic); - } - } if self.enabled(Rule::DjangoUnorderedBodyContentInModel) { flake8_django::rules::unordered_body_content_in_model(self, bases, body); } + 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); + } + } + } if self.enabled(Rule::GlobalStatement) { pylint::rules::global_statement(self, name); } From fab2a4adf79d7b41027d4ff41b10d6d58bc77345 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 16 Jun 2023 00:18:32 -0400 Subject: [PATCH 083/447] Use `matches!` for insecure hash rule (#5141) --- .../rules/hashlib_insecure_hash_functions.rs | 59 +++++++++---------- crates/ruff_python_ast/src/helpers.rs | 12 ++++ 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs b/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs index 1e624cd5fa..91ee429343 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs @@ -1,8 +1,8 @@ -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_false, SimpleCallArgs}; use crate::checkers::ast::Checker; @@ -21,26 +21,6 @@ impl Violation for HashlibInsecureHashFunction { } } -const WEAK_HASHES: [&str; 4] = ["md4", "md5", "sha", "sha1"]; - -fn is_used_for_security(call_args: &SimpleCallArgs) -> bool { - match call_args.keyword_argument("usedforsecurity") { - Some(expr) => !matches!( - expr, - Expr::Constant(ast::ExprConstant { - value: Constant::Bool(false), - .. - }) - ), - _ => true, - } -} - -enum HashlibCall { - New, - WeakHash(&'static str), -} - /// S324 pub(crate) fn hashlib_insecure_hash_functions( checker: &mut Checker, @@ -51,15 +31,13 @@ pub(crate) fn hashlib_insecure_hash_functions( if let Some(hashlib_call) = checker .semantic() .resolve_call_path(func) - .and_then(|call_path| { - if matches!(call_path.as_slice(), ["hashlib", "new"]) { - Some(HashlibCall::New) - } else { - WEAK_HASHES - .iter() - .find(|hash| call_path.as_slice() == ["hashlib", hash]) - .map(|hash| HashlibCall::WeakHash(hash)) - } + .and_then(|call_path| match call_path.as_slice() { + ["hashlib", "new"] => Some(HashlibCall::New), + ["hashlib", "md4"] => Some(HashlibCall::WeakHash("md4")), + ["hashlib", "md5"] => Some(HashlibCall::WeakHash("md5")), + ["hashlib", "sha"] => Some(HashlibCall::WeakHash("sha")), + ["hashlib", "sha1"] => Some(HashlibCall::WeakHash("sha1")), + _ => None, }) { match hashlib_call { @@ -72,7 +50,12 @@ pub(crate) fn hashlib_insecure_hash_functions( if let Some(name_arg) = call_args.argument("name", 0) { if let Some(hash_func_name) = string_literal(name_arg) { - if WEAK_HASHES.contains(&hash_func_name.to_lowercase().as_str()) { + // `hashlib.new` accepts both lowercase and uppercase names for hash + // functions. + if matches!( + hash_func_name, + "md4" | "md5" | "sha" | "sha1" | "MD4" | "MD5" | "SHA" | "SHA1" + ) { checker.diagnostics.push(Diagnostic::new( HashlibInsecureHashFunction { string: hash_func_name.to_string(), @@ -100,3 +83,15 @@ pub(crate) fn hashlib_insecure_hash_functions( } } } + +fn is_used_for_security(call_args: &SimpleCallArgs) -> bool { + match call_args.keyword_argument("usedforsecurity") { + Some(expr) => !is_const_false(expr), + _ => true, + } +} + +enum HashlibCall { + New, + WeakHash(&'static str), +} diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 38b750d00d..f8ab144d28 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -634,6 +634,18 @@ pub const fn is_const_true(expr: &Expr) -> bool { ) } +/// Return `true` if an [`Expr`] is `False`. +pub const fn is_const_false(expr: &Expr) -> bool { + matches!( + expr, + Expr::Constant(ast::ExprConstant { + value: Constant::Bool(false), + kind: None, + .. + }), + ) +} + /// Return `true` if a keyword argument is present with a non-`None` value. pub fn has_non_none_keyword(keywords: &[Keyword], keyword: &str) -> bool { find_keyword(keywords, keyword).map_or(false, |keyword| { From 5526699535dea230f873687494f75bfd288da168 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 16 Jun 2023 00:28:35 -0400 Subject: [PATCH 084/447] Use const-singleton helpers in more rules (#5142) --- .../rules/request_with_no_cert_validation.rs | 10 ++----- .../flake8_bugbear/rules/assert_false.rs | 10 +++---- .../rules/model_without_dunder_str.rs | 7 +++-- .../rules/nullable_model_string_field.rs | 8 ++--- .../src/rules/flake8_return/rules/function.rs | 30 ++++--------------- .../rules/flake8_simplify/rules/ast_expr.rs | 11 ++----- .../pycodestyle/rules/literal_comparisons.rs | 23 ++------------ .../src/rules/pylint/rules/return_in_init.rs | 11 ++----- .../rules/lru_cache_with_maxsize_none.rs | 14 ++------- .../src/rules/ruff/rules/implicit_optional.rs | 9 ++---- .../src/analyze/typing.rs | 11 ++----- 11 files changed, 37 insertions(+), 107 deletions(-) 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 3a11042da2..b03020e7eb 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 @@ -1,8 +1,8 @@ -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_false, SimpleCallArgs}; use crate::checkers::ast::Checker; @@ -60,11 +60,7 @@ pub(crate) fn request_with_no_cert_validation( { let call_args = SimpleCallArgs::new(args, keywords); if let Some(verify_arg) = call_args.keyword_argument("verify") { - if let Expr::Constant(ast::ExprConstant { - value: Constant::Bool(false), - .. - }) = &verify_arg - { + if is_const_false(verify_arg) { checker.diagnostics.push(Diagnostic::new( RequestWithNoCertValidation { string: target.to_string(), diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs b/crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs index 290fefdfa2..54b9912265 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs @@ -1,8 +1,9 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Ranged, Stmt}; +use rustpython_parser::ast::{self, Expr, ExprContext, Ranged, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_false; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -44,12 +45,9 @@ fn assertion_error(msg: Option<&Expr>) -> Stmt { /// B011 pub(crate) fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: Option<&Expr>) { - let Expr::Constant(ast::ExprConstant { - value: Constant::Bool(false), - .. - } )= &test else { + if !is_const_false(test) { return; - }; + } let mut diagnostic = Diagnostic::new(AssertFalse, test.range()); if checker.patch(diagnostic.kind.rule()) { 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 c0ca4d99e9..a02992757e 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 @@ -1,7 +1,8 @@ -use rustpython_parser::ast::{self, Constant, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_true; use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; @@ -112,9 +113,9 @@ fn is_model_abstract(body: &[Stmt]) -> bool { if id != "abstract" { continue; } - let Expr::Constant(ast::ExprConstant{value: Constant::Bool(true), ..}) = value.as_ref() else { + if !is_const_true(value) { continue; - }; + } return true; } } 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 35b8d66c14..a6ed90c648 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 @@ -1,8 +1,8 @@ -use rustpython_parser::ast::Constant::Bool; use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_true; use crate::checkers::ast::Checker; @@ -96,12 +96,12 @@ fn is_nullable_field<'a>(checker: &'a Checker, value: &'a Expr) -> Option<&'a st let mut blank_key = false; let mut unique_key = false; for keyword in keywords.iter() { - let Expr::Constant(ast::ExprConstant {value: Bool(true), ..}) = &keyword.value else { - continue - }; let Some(argument) = &keyword.arg else { continue }; + if !is_const_true(&keyword.value) { + continue; + } match argument.as_str() { "blank" => blank_key = true, "null" => null_key = true, diff --git a/crates/ruff/src/rules/flake8_return/rules/function.rs b/crates/ruff/src/rules/flake8_return/rules/function.rs index 502f76d7ed..cae9574f5b 100644 --- a/crates/ruff/src/rules/flake8_return/rules/function.rs +++ b/crates/ruff/src/rules/flake8_return/rules/function.rs @@ -1,13 +1,13 @@ use std::ops::Add; use ruff_text_size::{TextRange, TextSize}; -use rustpython_parser::ast::{self, Constant, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::elif_else_range; use ruff_python_ast::helpers::is_const_none; +use ruff_python_ast::helpers::{elif_else_range, is_const_false, is_const_true}; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::whitespace::indentation; use ruff_python_semantic::SemanticModel; @@ -339,13 +339,7 @@ fn unnecessary_return_none(checker: &mut Checker, stack: &Stack) { let Some(expr) = stmt.value.as_deref() else { continue; }; - if !matches!( - expr, - Expr::Constant(ast::ExprConstant { - value: Constant::None, - .. - }) - ) { + if !is_const_none(expr) { continue; } let mut diagnostic = Diagnostic::new(UnnecessaryReturnNone, stmt.range); @@ -433,22 +427,8 @@ fn implicit_return(checker: &mut Checker, stmt: &Stmt) { checker.diagnostics.push(diagnostic); } } - Stmt::Assert(ast::StmtAssert { test, .. }) - if matches!( - test.as_ref(), - Expr::Constant(ast::ExprConstant { - value: Constant::Bool(false), - .. - }) - ) => {} - Stmt::While(ast::StmtWhile { test, .. }) - if matches!( - test.as_ref(), - Expr::Constant(ast::ExprConstant { - value: Constant::Bool(true), - .. - }) - ) => {} + Stmt::Assert(ast::StmtAssert { test, .. }) if is_const_false(test) => {} + Stmt::While(ast::StmtWhile { test, .. }) if is_const_true(test) => {} Stmt::For(ast::StmtFor { orelse, .. }) | Stmt::AsyncFor(ast::StmtAsyncFor { orelse, .. }) | Stmt::While(ast::StmtWhile { orelse, .. }) => { 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 d8de3b0e8a..45424d41a9 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs @@ -3,6 +3,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_none; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -208,15 +209,9 @@ pub(crate) fn dict_get_with_none_default(checker: &mut Checker, expr: &Expr) { let Some(default) = args.get(1) else { return; }; - if !matches!( - default, - Expr::Constant(ast::ExprConstant { - value: Constant::None, - .. - }) - ) { + if !is_const_none(default) { return; - }; + } let expected = format!( "{}({})", diff --git a/crates/ruff/src/rules/pycodestyle/rules/literal_comparisons.rs b/crates/ruff/src/rules/pycodestyle/rules/literal_comparisons.rs index b3cfcddc30..53c67aaca9 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/literal_comparisons.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/literal_comparisons.rs @@ -5,6 +5,7 @@ use rustpython_parser::ast::{self, Cmpop, Constant, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers; +use ruff_python_ast::helpers::is_const_none; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -153,16 +154,7 @@ pub(crate) fn literal_comparisons( if !helpers::is_constant_non_singleton(next) { if let Some(op) = EqCmpop::try_from(*op) { - if check_none_comparisons - && matches!( - comparator, - Expr::Constant(ast::ExprConstant { - value: Constant::None, - kind: None, - range: _ - }) - ) - { + if check_none_comparisons && is_const_none(comparator) { match op { EqCmpop::Eq => { let diagnostic = Diagnostic::new(NoneComparison(op), comparator.range()); @@ -223,16 +215,7 @@ pub(crate) fn literal_comparisons( } if let Some(op) = EqCmpop::try_from(*op) { - if check_none_comparisons - && matches!( - next, - Expr::Constant(ast::ExprConstant { - value: Constant::None, - kind: None, - range: _ - }) - ) - { + if check_none_comparisons && is_const_none(next) { match op { EqCmpop::Eq => { let diagnostic = Diagnostic::new(NoneComparison(op), next.range()); diff --git a/crates/ruff/src/rules/pylint/rules/return_in_init.rs b/crates/ruff/src/rules/pylint/rules/return_in_init.rs index dbffa82463..0a6a81ebdc 100644 --- a/crates/ruff/src/rules/pylint/rules/return_in_init.rs +++ b/crates/ruff/src/rules/pylint/rules/return_in_init.rs @@ -1,7 +1,8 @@ -use rustpython_parser::ast::{self, Constant, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_none; use crate::checkers::ast::Checker; use crate::rules::pylint::helpers::in_dunder_init; @@ -46,13 +47,7 @@ impl Violation for ReturnInInit { pub(crate) fn return_in_init(checker: &mut Checker, stmt: &Stmt) { if let Stmt::Return(ast::StmtReturn { value, range: _ }) = stmt { if let Some(expr) = value { - if matches!( - expr.as_ref(), - Expr::Constant(ast::ExprConstant { - value: Constant::None, - .. - }) - ) { + if is_const_none(expr) { // Explicit `return None`. 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 277d472f0c..d6717f3cce 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 @@ -1,8 +1,9 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Constant, Decorator, Expr, Keyword, Ranged}; +use rustpython_parser::ast::{self, Decorator, Expr, Keyword, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_none; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; @@ -82,16 +83,7 @@ pub(crate) fn lru_cache_with_maxsize_none(checker: &mut Checker, decorator_list: value, range: _, } = &keywords[0]; - if arg.as_ref().map_or(false, |arg| arg == "maxsize") - && matches!( - value, - Expr::Constant(ast::ExprConstant { - value: Constant::None, - kind: None, - range: _, - }) - ) - { + if arg.as_ref().map_or(false, |arg| arg == "maxsize") && is_const_none(value) { let mut diagnostic = Diagnostic::new( LRUCacheWithMaxsizeNone, TextRange::new(func.end(), decorator.end()), diff --git a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs index b95a9051bd..573122e469 100644 --- a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs +++ b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs @@ -5,6 +5,7 @@ use rustpython_parser::ast::{self, Arguments, Constant, Expr, Operator, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_none; use ruff_python_semantic::SemanticModel; use ruff_text_size::TextRange; @@ -320,13 +321,7 @@ pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) { .zip(arguments.defaults.iter().rev()), ); for (arg, default) in arguments_with_defaults { - if !matches!( - default, - Expr::Constant(ast::ExprConstant { - value: Constant::None, - .. - }), - ) { + if !is_const_none(default) { continue; } let Some(annotation) = &arg.annotation else { diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 30078a4d1e..8dea42177e 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -1,9 +1,10 @@ //! Analysis rules for the `typing` module. +use num_traits::identities::Zero; use rustpython_parser::ast::{self, Constant, Expr, Operator}; -use num_traits::identities::Zero; use ruff_python_ast::call_path::{from_qualified_name, from_unqualified_name, CallPath}; +use ruff_python_ast::helpers::is_const_false; use ruff_python_stdlib::typing::{ IMMUTABLE_GENERIC_TYPES, IMMUTABLE_TYPES, PEP_585_GENERICS, PEP_593_SUBSCRIPTS, SUBSCRIPTS, }; @@ -267,13 +268,7 @@ pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> b let ast::StmtIf { test, .. } = stmt; // Ex) `if False:` - if matches!( - test.as_ref(), - Expr::Constant(ast::ExprConstant { - value: Constant::Bool(false), - .. - }) - ) { + if is_const_false(test) { return true; } From 3af9dfeb0a906f52df4f8ca47e407c0ee6f1c780 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 16 Jun 2023 08:57:20 -0400 Subject: [PATCH 085/447] Rewrite `suspicious_function_call` as a match statement (#5140) ## Summary @konstin mentioned that in profiling, this function accounted for a non-trivial amount of time (0.33% of total execution, the most of any rule). This PR attempts to rewrite it as a match statement for better performance over a looping comparison. ## Test Plan `cargo test` --- .../rules/suspicious_function_call.rs | 334 +++--------------- 1 file changed, 53 insertions(+), 281 deletions(-) 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 f2d86ea4fa..cfbb0df50c 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,298 +219,70 @@ impl Violation for SuspiciousFTPLibUsage { } } -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub(crate) enum Reason { - Pickle, - Marshal, - InsecureHash, - InsecureCipher, - InsecureCipherMode, - Mktemp, - Eval, - MarkSafe, - URLOpen, - NonCryptographicRandom, - XMLCElementTree, - XMLElementTree, - XMLExpatReader, - XMLExpatBuilder, - XMLSax, - XMLMiniDOM, - XMLPullDOM, - XMLETree, - UnverifiedContext, - Telnet, - FTPLib, -} - -struct SuspiciousMembers<'a> { - members: &'a [&'a [&'a str]], - reason: Reason, -} - -impl<'a> SuspiciousMembers<'a> { - pub(crate) const fn new(members: &'a [&'a [&'a str]], reason: Reason) -> Self { - Self { members, reason } - } -} - -struct SuspiciousModule<'a> { - name: &'a str, - reason: Reason, -} - -impl<'a> SuspiciousModule<'a> { - pub(crate) const fn new(name: &'a str, reason: Reason) -> Self { - Self { name, reason } - } -} - -const SUSPICIOUS_MEMBERS: &[SuspiciousMembers] = &[ - SuspiciousMembers::new( - &[ - &["pickle", "loads"], - &["pickle", "load"], - &["pickle", "Unpickler"], - &["dill", "loads"], - &["dill", "load"], - &["dill", "Unpickler"], - &["shelve", "open"], - &["shelve", "DbfilenameShelf"], - &["jsonpickle", "decode"], - &["jsonpickle", "unpickler", "decode"], - &["pandas", "read_pickle"], - ], - Reason::Pickle, - ), - SuspiciousMembers::new( - &[&["marshal", "loads"], &["marshal", "load"]], - Reason::Marshal, - ), - SuspiciousMembers::new( - &[ - &["Crypto", "Hash", "MD5", "new"], - &["Crypto", "Hash", "MD4", "new"], - &["Crypto", "Hash", "MD3", "new"], - &["Crypto", "Hash", "MD2", "new"], - &["Crypto", "Hash", "SHA", "new"], - &["Cryptodome", "Hash", "MD5", "new"], - &["Cryptodome", "Hash", "MD4", "new"], - &["Cryptodome", "Hash", "MD3", "new"], - &["Cryptodome", "Hash", "MD2", "new"], - &["Cryptodome", "Hash", "SHA", "new"], - &["cryptography", "hazmat", "primitives", "hashes", "MD5"], - &["cryptography", "hazmat", "primitives", "hashes", "SHA1"], - ], - Reason::InsecureHash, - ), - SuspiciousMembers::new( - &[ - &["Crypto", "Cipher", "ARC2", "new"], - &["Crypto", "Cipher", "ARC2", "new"], - &["Crypto", "Cipher", "Blowfish", "new"], - &["Crypto", "Cipher", "DES", "new"], - &["Crypto", "Cipher", "XOR", "new"], - &["Cryptodome", "Cipher", "ARC2", "new"], - &["Cryptodome", "Cipher", "ARC2", "new"], - &["Cryptodome", "Cipher", "Blowfish", "new"], - &["Cryptodome", "Cipher", "DES", "new"], - &["Cryptodome", "Cipher", "XOR", "new"], - &[ - "cryptography", - "hazmat", - "primitives", - "ciphers", - "algorithms", - "ARC4", - ], - &[ - "cryptography", - "hazmat", - "primitives", - "ciphers", - "algorithms", - "Blowfish", - ], - &[ - "cryptography", - "hazmat", - "primitives", - "ciphers", - "algorithms", - "IDEA", - ], - ], - Reason::InsecureCipher, - ), - SuspiciousMembers::new( - &[&[ - "cryptography", - "hazmat", - "primitives", - "ciphers", - "modes", - "ECB", - ]], - Reason::InsecureCipherMode, - ), - SuspiciousMembers::new(&[&["tempfile", "mktemp"]], Reason::Mktemp), - SuspiciousMembers::new(&[&["eval"]], Reason::Eval), - SuspiciousMembers::new( - &[&["django", "utils", "safestring", "mark_safe"]], - Reason::MarkSafe, - ), - SuspiciousMembers::new( - &[ - &["urllib", "urlopen"], - &["urllib", "request", "urlopen"], - &["urllib", "urlretrieve"], - &["urllib", "request", "urlretrieve"], - &["urllib", "URLopener"], - &["urllib", "request", "URLopener"], - &["urllib", "FancyURLopener"], - &["urllib", "request", "FancyURLopener"], - &["urllib2", "urlopen"], - &["urllib2", "Request"], - &["six", "moves", "urllib", "request", "urlopen"], - &["six", "moves", "urllib", "request", "urlretrieve"], - &["six", "moves", "urllib", "request", "URLopener"], - &["six", "moves", "urllib", "request", "FancyURLopener"], - ], - Reason::URLOpen, - ), - SuspiciousMembers::new( - &[ - &["random", "random"], - &["random", "randrange"], - &["random", "randint"], - &["random", "choice"], - &["random", "choices"], - &["random", "uniform"], - &["random", "triangular"], - ], - Reason::NonCryptographicRandom, - ), - SuspiciousMembers::new( - &[&["ssl", "_create_unverified_context"]], - Reason::UnverifiedContext, - ), - SuspiciousMembers::new( - &[ - &["xml", "etree", "cElementTree", "parse"], - &["xml", "etree", "cElementTree", "iterparse"], - &["xml", "etree", "cElementTree", "fromstring"], - &["xml", "etree", "cElementTree", "XMLParser"], - ], - Reason::XMLCElementTree, - ), - SuspiciousMembers::new( - &[ - &["xml", "etree", "ElementTree", "parse"], - &["xml", "etree", "ElementTree", "iterparse"], - &["xml", "etree", "ElementTree", "fromstring"], - &["xml", "etree", "ElementTree", "XMLParser"], - ], - Reason::XMLElementTree, - ), - SuspiciousMembers::new( - &[&["xml", "sax", "expatreader", "create_parser"]], - Reason::XMLExpatReader, - ), - SuspiciousMembers::new( - &[ - &["xml", "dom", "expatbuilder", "parse"], - &["xml", "dom", "expatbuilder", "parseString"], - ], - Reason::XMLExpatBuilder, - ), - SuspiciousMembers::new( - &[ - &["xml", "sax", "parse"], - &["xml", "sax", "parseString"], - &["xml", "sax", "make_parser"], - ], - Reason::XMLSax, - ), - SuspiciousMembers::new( - &[ - &["xml", "dom", "minidom", "parse"], - &["xml", "dom", "minidom", "parseString"], - ], - Reason::XMLMiniDOM, - ), - SuspiciousMembers::new( - &[ - &["xml", "dom", "pulldom", "parse"], - &["xml", "dom", "pulldom", "parseString"], - ], - Reason::XMLPullDOM, - ), - SuspiciousMembers::new( - &[ - &["lxml", "etree", "parse"], - &["lxml", "etree", "fromstring"], - &["lxml", "etree", "RestrictedElement"], - &["lxml", "etree", "GlobalParserTLS"], - &["lxml", "etree", "getDefaultParser"], - &["lxml", "etree", "check_docinfo"], - ], - Reason::XMLETree, - ), -]; - -const SUSPICIOUS_MODULES: &[SuspiciousModule] = &[ - SuspiciousModule::new("telnetlib", Reason::Telnet), - SuspiciousModule::new("ftplib", Reason::FTPLib), -]; - /// S001 pub(crate) fn suspicious_function_call(checker: &mut Checker, expr: &Expr) { let Expr::Call(ast::ExprCall { func, .. }) = expr else { return; }; - let Some(reason) = checker.semantic().resolve_call_path(func).and_then(|call_path| { - for module in SUSPICIOUS_MEMBERS { - for member in module.members { - if call_path.as_slice() == *member { - return Some(module.reason); - } - } + let Some(diagnostic_kind) = checker.semantic().resolve_call_path(func).and_then(|call_path| { + match call_path.as_slice() { + // Pickle + ["pickle" | "dill", "load" | "loads" | "Unpickler"] | + ["shelve", "open" | "DbfilenameShelf"] | + ["jsonpickle", "decode"] | + ["jsonpickle", "unpickler", "decode"] | + ["pandas", "read_pickle"] => Some(SuspiciousPickleUsage.into()), + // Marshal + ["marshal", "load" | "loads"] => Some(SuspiciousMarshalUsage.into()), + // InsecureHash + ["Crypto" | "Cryptodome", "Hash", "SHA" | "MD2" | "MD3" | "MD4" | "MD5", "new"] | + ["cryptography", "hazmat", "primitives", "hashes", "SHA1" | "MD5"] => Some(SuspiciousInsecureHashUsage.into()), + // InsecureCipher + ["Crypto" | "Cryptodome", "Cipher", "ARC2" | "Blowfish" | "DES" | "XOR", "new"] | + ["cryptography", "hazmat", "primitives", "ciphers", "algorithms", "ARC4" | "Blowfish" | "IDEA" ] => Some(SuspiciousInsecureCipherUsage.into()), + // InsecureCipherMode + ["cryptography", "hazmat", "primitives", "ciphers", "modes", "ECB"] => Some(SuspiciousInsecureCipherModeUsage.into()), + // Mktemp + ["tempfile", "mktemp"] => Some(SuspiciousMktempUsage.into()), + // Eval + ["eval"] => Some(SuspiciousEvalUsage.into()), + // MarkSafe + ["django", "utils", "safestring", "mark_safe"] => Some(SuspiciousMarkSafeUsage.into()), + // URLOpen + ["urllib", "urlopen" | "urlretrieve" | "URLopener" | "FancyURLopener" | "Request"] | + ["urllib", "request", "urlopen" | "urlretrieve" | "URLopener" | "FancyURLopener"] | + ["six", "moves", "urllib", "request", "urlopen" | "urlretrieve" | "URLopener" | "FancyURLopener"] => Some(SuspiciousURLOpenUsage.into()), + // NonCryptographicRandom + ["random", "random" | "randrange" | "randint" | "choice" | "choices" | "uniform" | "triangular"] => Some(SuspiciousNonCryptographicRandomUsage.into()), + // UnverifiedContext + ["ssl", "_create_unverified_context"] => Some(SuspiciousUnverifiedContextUsage.into()), + // XMLCElementTree + ["xml", "etree", "cElementTree", "parse" | "iterparse" | "fromstring" | "XMLParser"] => Some(SuspiciousXMLCElementTreeUsage.into()), + // XMLElementTree + ["xml", "etree", "ElementTree", "parse" | "iterparse" | "fromstring" | "XMLParser"] => Some(SuspiciousXMLElementTreeUsage.into()), + // XMLExpatReader + ["xml", "sax", "expatreader", "create_parser"] => Some(SuspiciousXMLExpatReaderUsage.into()), + // XMLExpatBuilder + ["xml", "dom", "expatbuilder", "parse" | "parseString"] => Some(SuspiciousXMLExpatBuilderUsage.into()), + // XMLSax + ["xml", "sax", "parse" | "parseString" | "make_parser"] => Some(SuspiciousXMLSaxUsage.into()), + // XMLMiniDOM + ["xml", "dom", "minidom", "parse" | "parseString"] => Some(SuspiciousXMLMiniDOMUsage.into()), + // XMLPullDOM + ["xml", "dom", "pulldom", "parse" | "parseString"] => Some(SuspiciousXMLPullDOMUsage.into()), + // XMLETree + ["lxml", "etree", "parse" | "fromstring" | "RestrictedElement" | "GlobalParserTLS" | "getDefaultParser" | "check_docinfo"] => Some(SuspiciousXMLETreeUsage.into()), + // Telnet + ["telnetlib", ..] => Some(SuspiciousTelnetUsage.into()), + // FTPLib + ["ftplib", ..] => Some(SuspiciousFTPLibUsage.into()), + _ => None } - for module in SUSPICIOUS_MODULES { - if call_path.first() == Some(&module.name) { - return Some(module.reason); - } - } - None }) else { return; }; - let diagnostic_kind = match reason { - Reason::Pickle => SuspiciousPickleUsage.into(), - Reason::Marshal => SuspiciousMarshalUsage.into(), - Reason::InsecureHash => SuspiciousInsecureHashUsage.into(), - Reason::InsecureCipher => SuspiciousInsecureCipherUsage.into(), - Reason::InsecureCipherMode => SuspiciousInsecureCipherModeUsage.into(), - Reason::Mktemp => SuspiciousMktempUsage.into(), - Reason::Eval => SuspiciousEvalUsage.into(), - Reason::MarkSafe => SuspiciousMarkSafeUsage.into(), - Reason::URLOpen => SuspiciousURLOpenUsage.into(), - Reason::NonCryptographicRandom => SuspiciousNonCryptographicRandomUsage.into(), - Reason::XMLCElementTree => SuspiciousXMLCElementTreeUsage.into(), - Reason::XMLElementTree => SuspiciousXMLElementTreeUsage.into(), - Reason::XMLExpatReader => SuspiciousXMLExpatReaderUsage.into(), - Reason::XMLExpatBuilder => SuspiciousXMLExpatBuilderUsage.into(), - Reason::XMLSax => SuspiciousXMLSaxUsage.into(), - Reason::XMLMiniDOM => SuspiciousXMLMiniDOMUsage.into(), - Reason::XMLPullDOM => SuspiciousXMLPullDOMUsage.into(), - Reason::XMLETree => SuspiciousXMLETreeUsage.into(), - Reason::UnverifiedContext => SuspiciousUnverifiedContextUsage.into(), - Reason::Telnet => SuspiciousTelnetUsage.into(), - Reason::FTPLib => SuspiciousFTPLibUsage.into(), - }; let diagnostic = Diagnostic::new::(diagnostic_kind, expr.range()); if checker.enabled(diagnostic.kind.rule()) { checker.diagnostics.push(diagnostic); From 307f7a735c40d5661b33e2873dbb53331d7f46a0 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 16 Jun 2023 08:57:43 -0400 Subject: [PATCH 086/447] Avoid allocations in lowercase comparisons (#5137) ## Summary I noticed that we have a few hot comparisons that involve called `s.to_lowercase()`. We can avoid an allocation by comparing characters directly. --- .../rules/raise_without_from_inside_except.rs | 4 +- .../flake8_simplify/rules/yoda_conditions.rs | 4 +- crates/ruff/src/rules/isort/sorting.rs | 2 +- crates/ruff/src/rules/pep8_naming/helpers.rs | 6 +- .../rules/camelcase_imported_as_acronym.rs | 4 +- .../rules/camelcase_imported_as_constant.rs | 4 +- .../rules/camelcase_imported_as_lowercase.rs | 2 +- .../constant_imported_as_non_constant.rs | 2 +- .../rules/invalid_argument_name.rs | 3 +- .../rules/invalid_function_name.rs | 3 +- .../lowercase_imported_as_non_lowercase.rs | 3 +- .../non_lowercase_variable_in_function.rs | 3 +- .../rules/invalid_escape_sequence.rs | 2 +- crates/ruff_python_stdlib/src/str.rs | 123 ++++++++++++------ 14 files changed, 108 insertions(+), 57 deletions(-) diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs b/crates/ruff/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs index ccb0d84c55..01a0e2ad4b 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs @@ -4,7 +4,7 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::RaiseStatementVisitor; use ruff_python_ast::statement_visitor::StatementVisitor; -use ruff_python_stdlib::str::is_lower; +use ruff_python_stdlib::str::is_cased_lowercase; use crate::checkers::ast::Checker; @@ -33,7 +33,7 @@ pub(crate) fn raise_without_from_inside_except(checker: &mut Checker, body: &[St if cause.is_none() { if let Some(exc) = exc { match exc { - Expr::Name(ast::ExprName { id, .. }) if is_lower(id) => {} + Expr::Name(ast::ExprName { id, .. }) if is_cased_lowercase(id) => {} _ => { checker .diagnostics diff --git a/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs b/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs index f3fb4c8ce1..b95681958f 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs @@ -71,10 +71,10 @@ impl Violation for YodaConditions { /// Return `true` if an [`Expr`] is a constant or a constant-like name. fn is_constant_like(expr: &Expr) -> bool { match expr { - Expr::Attribute(ast::ExprAttribute { attr, .. }) => str::is_upper(attr), + Expr::Attribute(ast::ExprAttribute { attr, .. }) => str::is_cased_uppercase(attr), Expr::Constant(_) => true, Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().all(is_constant_like), - Expr::Name(ast::ExprName { id, .. }) => str::is_upper(id), + Expr::Name(ast::ExprName { id, .. }) => str::is_cased_uppercase(id), Expr::UnaryOp(ast::ExprUnaryOp { op: Unaryop::UAdd | Unaryop::USub | Unaryop::Invert, operand, diff --git a/crates/ruff/src/rules/isort/sorting.rs b/crates/ruff/src/rules/isort/sorting.rs index 9ce7d63bb3..ec8e15af4e 100644 --- a/crates/ruff/src/rules/isort/sorting.rs +++ b/crates/ruff/src/rules/isort/sorting.rs @@ -32,7 +32,7 @@ fn prefix( } else if variables.contains(name) { // Ex) `variable` Prefix::Variables - } else if name.len() > 1 && str::is_upper(name) { + } else if name.len() > 1 && str::is_cased_uppercase(name) { // Ex) `CONSTANT` Prefix::Constants } else if name.chars().next().map_or(false, char::is_uppercase) { diff --git a/crates/ruff/src/rules/pep8_naming/helpers.rs b/crates/ruff/src/rules/pep8_naming/helpers.rs index 2d3ed8e6ef..ac6d369b38 100644 --- a/crates/ruff/src/rules/pep8_naming/helpers.rs +++ b/crates/ruff/src/rules/pep8_naming/helpers.rs @@ -2,14 +2,14 @@ use itertools::Itertools; use rustpython_parser::ast::{self, Expr, Stmt}; use ruff_python_semantic::SemanticModel; -use ruff_python_stdlib::str::{is_lower, is_upper}; +use ruff_python_stdlib::str::{is_cased_lowercase, is_cased_uppercase}; pub(super) fn is_camelcase(name: &str) -> bool { - !is_lower(name) && !is_upper(name) && !name.contains('_') + !is_cased_lowercase(name) && !is_cased_uppercase(name) && !name.contains('_') } pub(super) fn is_mixed_case(name: &str) -> bool { - !is_lower(name) + !is_cased_lowercase(name) && name .strip_prefix('_') .unwrap_or(name) diff --git a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs index 45de159229..b81b30abb2 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs @@ -63,8 +63,8 @@ pub(crate) fn camelcase_imported_as_acronym( } if helpers::is_camelcase(name) - && !str::is_lower(asname) - && str::is_upper(asname) + && !str::is_cased_lowercase(asname) + && str::is_cased_uppercase(asname) && helpers::is_acronym(name, asname) { let mut diagnostic = Diagnostic::new( diff --git a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs index bc87539905..b43f54b977 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs @@ -60,8 +60,8 @@ pub(crate) fn camelcase_imported_as_constant( } if helpers::is_camelcase(name) - && !str::is_lower(asname) - && str::is_upper(asname) + && !str::is_cased_lowercase(asname) + && str::is_cased_uppercase(asname) && !helpers::is_acronym(name, asname) { let mut diagnostic = Diagnostic::new( diff --git a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs index b08232ebcc..b5f051de6a 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs @@ -58,7 +58,7 @@ pub(crate) fn camelcase_imported_as_lowercase( return None; } - if helpers::is_camelcase(name) && ruff_python_stdlib::str::is_lower(asname) { + if helpers::is_camelcase(name) && ruff_python_stdlib::str::is_cased_lowercase(asname) { let mut diagnostic = Diagnostic::new( CamelcaseImportedAsLowercase { name: name.to_string(), diff --git a/crates/ruff/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs b/crates/ruff/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs index ad9e43d13e..3ce0ab006f 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs @@ -59,7 +59,7 @@ pub(crate) fn constant_imported_as_non_constant( return None; } - if str::is_upper(name) && !str::is_upper(asname) { + if str::is_cased_uppercase(name) && !str::is_cased_uppercase(asname) { let mut diagnostic = Diagnostic::new( ConstantImportedAsNonConstant { name: name.to_string(), diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_argument_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_argument_name.rs index b1dca74a30..be9e86723f 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_argument_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_argument_name.rs @@ -2,6 +2,7 @@ use rustpython_parser::ast::{Arg, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_stdlib::str; use crate::settings::types::IdentifierPattern; @@ -58,7 +59,7 @@ pub(crate) fn invalid_argument_name( { return None; } - if name.to_lowercase() != name { + if !str::is_lowercase(name) { return Some(Diagnostic::new( InvalidArgumentName { name: name.to_string(), diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs index b23556566e..f14ba2c709 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs @@ -6,6 +6,7 @@ use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; use ruff_python_semantic::analyze::visibility; use ruff_python_semantic::SemanticModel; +use ruff_python_stdlib::str; use crate::settings::types::IdentifierPattern; @@ -67,7 +68,7 @@ pub(crate) fn invalid_function_name( } // Ignore any function names that are already lowercase. - if name.to_lowercase() == name { + if str::is_lowercase(name) { return None; } diff --git a/crates/ruff/src/rules/pep8_naming/rules/lowercase_imported_as_non_lowercase.rs b/crates/ruff/src/rules/pep8_naming/rules/lowercase_imported_as_non_lowercase.rs index dc803996d1..63dd420c00 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/lowercase_imported_as_non_lowercase.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/lowercase_imported_as_non_lowercase.rs @@ -58,7 +58,8 @@ pub(crate) fn lowercase_imported_as_non_lowercase( return None; } - if !str::is_upper(name) && str::is_lower(name) && asname.to_lowercase() != asname { + if !str::is_cased_uppercase(name) && str::is_cased_lowercase(name) && !str::is_lowercase(asname) + { let mut diagnostic = Diagnostic::new( LowercaseImportedAsNonLowercase { name: name.to_string(), diff --git a/crates/ruff/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs b/crates/ruff/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs index a25a82a8ca..4fe3edd36f 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs @@ -2,6 +2,7 @@ use rustpython_parser::ast::{Expr, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_stdlib::str; use crate::checkers::ast::Checker; use crate::rules::pep8_naming::helpers; @@ -65,7 +66,7 @@ pub(crate) fn non_lowercase_variable_in_function( return; } - if name.to_lowercase() != name + if !str::is_lowercase(name) && !helpers::is_named_tuple_assignment(stmt, checker.semantic()) && !helpers::is_typed_dict_assignment(stmt, checker.semantic()) && !helpers::is_type_var_assignment(stmt, checker.semantic()) diff --git a/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs b/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs index ceef5cd796..4bd8d52ed9 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs @@ -22,7 +22,7 @@ use ruff_python_ast::source_code::Locator; /// regex = r"\.png$" /// ``` #[violation] -pub struct InvalidEscapeSequence(pub char); +pub struct InvalidEscapeSequence(char); impl AlwaysAutofixableViolation for InvalidEscapeSequence { #[derive_message_formats] diff --git a/crates/ruff_python_stdlib/src/str.rs b/crates/ruff_python_stdlib/src/str.rs index 1c1e6ffae7..2b7b90b64b 100644 --- a/crates/ruff_python_stdlib/src/str.rs +++ b/crates/ruff_python_stdlib/src/str.rs @@ -1,14 +1,67 @@ -/// See: -pub const TRIPLE_QUOTE_PREFIXES: &[&str] = &[ - "u\"\"\"", "u'''", "r\"\"\"", "r'''", "U\"\"\"", "U'''", "R\"\"\"", "R'''", "\"\"\"", "'''", -]; -pub const SINGLE_QUOTE_PREFIXES: &[&str] = &[ - "u\"", "u'", "r\"", "r'", "U\"", "U'", "R\"", "R'", "\"", "'", -]; -pub const TRIPLE_QUOTE_SUFFIXES: &[&str] = &["\"\"\"", "'''"]; -pub const SINGLE_QUOTE_SUFFIXES: &[&str] = &["\"", "'"]; +/// Return `true` if a string is lowercase. +/// +/// A string is lowercase if all alphabetic characters in the string are lowercase. +/// +/// ## Examples +/// +/// ```rust +/// use ruff_python_stdlib::str::is_lowercase; +/// +/// assert!(is_lowercase("abc")); +/// assert!(is_lowercase("a_b_c")); +/// assert!(is_lowercase("a2c")); +/// assert!(!is_lowercase("aBc")); +/// assert!(!is_lowercase("ABC")); +/// assert!(is_lowercase("")); +/// assert!(is_lowercase("_")); +/// ``` +pub fn is_lowercase(s: &str) -> bool { + s.chars().all(|c| !c.is_alphabetic() || c.is_lowercase()) +} -pub fn is_lower(s: &str) -> bool { +/// Return `true` if a string is uppercase. +/// +/// A string is uppercase if all alphabetic characters in the string are uppercase. +/// +/// ## Examples +/// +/// ```rust +/// use ruff_python_stdlib::str::is_uppercase; +/// +/// assert!(is_uppercase("ABC")); +/// assert!(is_uppercase("A_B_C")); +/// assert!(is_uppercase("A2C")); +/// assert!(!is_uppercase("aBc")); +/// assert!(!is_uppercase("abc")); +/// assert!(is_uppercase("")); +/// assert!(is_uppercase("_")); +/// ``` +pub fn is_uppercase(s: &str) -> bool { + s.chars().all(|c| !c.is_alphabetic() || c.is_uppercase()) +} + +/// Return `true` if a string is _cased_ as lowercase. +/// +/// A string is cased as lowercase if it contains at least one lowercase character and no uppercase +/// characters. +/// +/// This differs from `str::is_lowercase` in that it returns `false` for empty strings and strings +/// that contain only underscores or other non-alphabetic characters. +/// +/// ## Examples +/// +/// ```rust +/// use ruff_python_stdlib::str::is_cased_lowercase; +/// +/// assert!(is_cased_lowercase("abc")); +/// assert!(is_cased_lowercase("a_b_c")); +/// assert!(is_cased_lowercase("a2c")); +/// assert!(!is_cased_lowercase("aBc")); +/// assert!(!is_cased_lowercase("ABC")); +/// assert!(!is_cased_lowercase("")); +/// assert!(!is_cased_lowercase("_")); +/// ``` +pub fn is_cased_lowercase(s: &str) -> bool { let mut cased = false; for c in s.chars() { if c.is_uppercase() { @@ -20,7 +73,28 @@ pub fn is_lower(s: &str) -> bool { cased } -pub fn is_upper(s: &str) -> bool { +/// Return `true` if a string is _cased_ as uppercase. +/// +/// A string is cased as uppercase if it contains at least one uppercase character and no lowercase +/// characters. +/// +/// This differs from `str::is_uppercase` in that it returns `false` for empty strings and strings +/// that contain only underscores or other non-alphabetic characters. +/// +/// ## Examples +/// +/// ```rust +/// use ruff_python_stdlib::str::is_cased_uppercase; +/// +/// assert!(is_cased_uppercase("ABC")); +/// assert!(is_cased_uppercase("A_B_C")); +/// assert!(is_cased_uppercase("A2C")); +/// assert!(!is_cased_uppercase("aBc")); +/// assert!(!is_cased_uppercase("abc")); +/// assert!(!is_cased_uppercase("")); +/// assert!(!is_cased_uppercase("_")); +/// ``` +pub fn is_cased_uppercase(s: &str) -> bool { let mut cased = false; for c in s.chars() { if c.is_lowercase() { @@ -31,30 +105,3 @@ pub fn is_upper(s: &str) -> bool { } cased } - -#[cfg(test)] -mod tests { - use crate::str::{is_lower, is_upper}; - - #[test] - fn test_is_lower() { - assert!(is_lower("abc")); - assert!(is_lower("a_b_c")); - assert!(is_lower("a2c")); - assert!(!is_lower("aBc")); - assert!(!is_lower("ABC")); - assert!(!is_lower("")); - assert!(!is_lower("_")); - } - - #[test] - fn test_is_upper() { - assert!(is_upper("ABC")); - assert!(is_upper("A_B_C")); - assert!(is_upper("A2C")); - assert!(!is_upper("aBc")); - assert!(!is_upper("abc")); - assert!(!is_upper("")); - assert!(!is_upper("_")); - } -} From b9754bd5c5d1966b3aa16a054431434219a13f0b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 16 Jun 2023 10:12:33 -0400 Subject: [PATCH 087/447] Add autofix for `Set`-to-`AbstractSet` rewrite using reference tracking (#5074) ## Summary This PR enables autofix behavior for the `flake8-pyi` rule that asks you to alias `Set` to `AbstractSet` when importing `collections.abc.Set`. It's not the most important rule, but it's a good isolated test-case for local symbol renaming. The renaming algorithm is outlined in-detail in the `renamer.rs` module. But to demonstrate the behavior, here's the diff when running this fix over a complex file that exercises a few edge cases: ```diff --- a/foo.pyi +++ b/foo.pyi @@ -1,16 +1,16 @@ if True: - from collections.abc import Set + from collections.abc import Set as AbstractSet else: - Set = 1 + AbstractSet = 1 -x: Set = set() +x: AbstractSet = set() -x: Set +x: AbstractSet -del Set +del AbstractSet def f(): - print(Set) + print(AbstractSet) def Set(): pass ``` Making this work required resolving a bunch of edge cases in the semantic model that were causing us to "lose track" of references. For example, the above wasn't possible with our previous approach to handling deletions (#5071). Similarly, the `x: Set` "delayed annotation" tracking was enabled via #5070. And many of these edits would've failed if we hadn't changed `BindingKind` to always match the identifier range (#5090). So it's really the culmination of a bunch of changes over the course of the week. The main outstanding TODO is that this doesn't support `global` or `nonlocal` usages. I'm going to take a look at that tonight, but I'm comfortable merging this as-is. Closes #1106. Closes #5091. --- .../test/fixtures/flake8_pyi/PYI025.py | 28 +-- .../test/fixtures/flake8_pyi/PYI025.pyi | 39 ++-- crates/ruff/src/checkers/ast/mod.rs | 98 ++++++---- crates/ruff/src/importer/mod.rs | 5 +- crates/ruff/src/lib.rs | 1 + crates/ruff/src/renamer.rs | 176 ++++++++++++++++++ .../unaliased_collections_abc_set_import.rs | 50 +++-- ..._flake8_pyi__tests__PYI025_PYI025.pyi.snap | 87 +++++++-- crates/ruff_python_semantic/src/binding.rs | 30 ++- 9 files changed, 414 insertions(+), 100 deletions(-) create mode 100644 crates/ruff/src/renamer.rs diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI025.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI025.py index 5c8b8fa26b..e500a3f4a9 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI025.py +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI025.py @@ -1,19 +1,19 @@ -from collections.abc import Set as AbstractSet # Ok +def f(): + from collections.abc import Set as AbstractSet # Ok -from collections.abc import Set # Ok +def f(): + from collections.abc import Container, Sized, Set as AbstractSet, ValuesView # Ok -from collections.abc import ( - Container, - Sized, - Set, # Ok - ValuesView -) +def f(): + from collections.abc import Set # PYI025 -from collections.abc import ( - Container, - Sized, - Set as AbstractSet, # Ok - ValuesView -) + +def f(): + from collections.abc import Container, Sized, Set, ValuesView # PYI025 + + GLOBAL: Set[int] = set() + + class Class: + member: Set[int] diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI025.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI025.pyi index c12ccdffb5..bda9c0d083 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI025.pyi +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI025.pyi @@ -1,19 +1,30 @@ -from collections.abc import Set as AbstractSet # Ok +def f(): + from collections.abc import Set as AbstractSet # Ok +def f(): + from collections.abc import Container, Sized, Set as AbstractSet, ValuesView # Ok -from collections.abc import Set # PYI025 +def f(): + from collections.abc import Set # PYI025 +def f(): + from collections.abc import Container, Sized, Set, ValuesView # PYI025 -from collections.abc import ( - Container, - Sized, - Set, # PYI025 - ValuesView -) +def f(): + if True: + from collections.abc import Set + else: + Set = 1 -from collections.abc import ( - Container, - Sized, - Set as AbstractSet, - ValuesView # Ok -) + x: Set = set() + + x: Set + + del Set + + def f(): + print(Set) + + def Set(): + pass + print(Set) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index c5260823c9..d9b1b9d3d4 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -817,24 +817,28 @@ where BindingKind::SubmoduleImportation(SubmoduleImportation { qualified_name, }), - BindingFlags::empty(), + BindingFlags::EXTERNAL, ); } else { + let mut flags = BindingFlags::EXTERNAL; + if alias.asname.is_some() { + flags |= BindingFlags::ALIAS; + } + if alias + .asname + .as_ref() + .map_or(false, |asname| asname == &alias.name) + { + flags |= BindingFlags::EXPLICIT_EXPORT; + } + let name = alias.asname.as_ref().unwrap_or(&alias.name); let qualified_name = &alias.name; self.add_binding( name, alias.identifier(self.locator), BindingKind::Importation(Importation { qualified_name }), - if alias - .asname - .as_ref() - .map_or(false, |asname| asname == &alias.name) - { - BindingFlags::EXPLICIT_EXPORT - } else { - BindingFlags::empty() - }, + flags, ); if let Some(asname) = &alias.asname { @@ -1052,9 +1056,6 @@ where } if self.is_stub { - if self.enabled(Rule::UnaliasedCollectionsAbcSetImport) { - flake8_pyi::rules::unaliased_collections_abc_set_import(self, import_from); - } if self.enabled(Rule::FutureAnnotationsInStub) { flake8_pyi::rules::from_future_import(self, import_from); } @@ -1116,6 +1117,18 @@ where } } + let mut flags = BindingFlags::EXTERNAL; + if alias.asname.is_some() { + flags |= BindingFlags::ALIAS; + } + if alias + .asname + .as_ref() + .map_or(false, |asname| asname == &alias.name) + { + flags |= BindingFlags::EXPLICIT_EXPORT; + } + // Given `from foo import bar`, `name` would be "bar" and `qualified_name` would // be "foo.bar". Given `from foo import bar as baz`, `name` would be "baz" // and `qualified_name` would be "foo.bar". @@ -1126,15 +1139,7 @@ where name, alias.identifier(self.locator), BindingKind::FromImportation(FromImportation { qualified_name }), - if alias - .asname - .as_ref() - .map_or(false, |asname| asname == &alias.name) - { - BindingFlags::EXPLICIT_EXPORT - } else { - BindingFlags::empty() - }, + flags, ); } if self.enabled(Rule::RelativeImports) { @@ -4711,23 +4716,18 @@ impl<'a> Checker<'a> { } fn check_dead_scopes(&mut self) { - let enforce_typing_imports = !self.is_stub - && self.any_enabled(&[ - Rule::GlobalVariableNotAssigned, - Rule::RuntimeImportInTypeCheckingBlock, - Rule::TypingOnlyFirstPartyImport, - Rule::TypingOnlyThirdPartyImport, - Rule::TypingOnlyStandardLibraryImport, - ]); - - if !(enforce_typing_imports - || self.any_enabled(&[ - Rule::UnusedImport, - Rule::UndefinedLocalWithImportStarUsage, - Rule::RedefinedWhileUnused, - Rule::UndefinedExport, - ])) - { + if !self.any_enabled(&[ + Rule::UnusedImport, + Rule::GlobalVariableNotAssigned, + Rule::UndefinedLocalWithImportStarUsage, + Rule::RedefinedWhileUnused, + Rule::RuntimeImportInTypeCheckingBlock, + Rule::TypingOnlyFirstPartyImport, + Rule::TypingOnlyThirdPartyImport, + Rule::TypingOnlyStandardLibraryImport, + Rule::UndefinedExport, + Rule::UnaliasedCollectionsAbcSetImport, + ]) { return; } @@ -4757,6 +4757,16 @@ impl<'a> Checker<'a> { // Identify any valid runtime imports. If a module is imported at runtime, and // used at runtime, then by default, we avoid flagging any other // imports from that model as typing-only. + let enforce_typing_imports = if self.is_stub { + false + } else { + self.any_enabled(&[ + Rule::RuntimeImportInTypeCheckingBlock, + Rule::TypingOnlyFirstPartyImport, + Rule::TypingOnlyThirdPartyImport, + Rule::TypingOnlyStandardLibraryImport, + ]) + }; let runtime_imports: Vec> = if enforce_typing_imports { if self.settings.flake8_type_checking.strict { vec![] @@ -4907,6 +4917,16 @@ impl<'a> Checker<'a> { if self.enabled(Rule::UnusedImport) { pyflakes::rules::unused_import(self, scope, &mut diagnostics); } + + if self.is_stub { + if self.enabled(Rule::UnaliasedCollectionsAbcSetImport) { + flake8_pyi::rules::unaliased_collections_abc_set_import( + self, + scope, + &mut diagnostics, + ); + } + } } self.diagnostics.extend(diagnostics); } diff --git a/crates/ruff/src/importer/mod.rs b/crates/ruff/src/importer/mod.rs index b7a67141b6..6acab6e471 100644 --- a/crates/ruff/src/importer/mod.rs +++ b/crates/ruff/src/importer/mod.rs @@ -1,4 +1,7 @@ -//! Add and modify import statements to make module members available during fix execution. +//! Code modification struct to add and modify import statements. +//! +//! Enables rules to make module members available (that may be not yet be imported) during fix +//! execution. use std::error::Error; diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs index 4cba40f704..f1d884303e 100644 --- a/crates/ruff/src/lib.rs +++ b/crates/ruff/src/lib.rs @@ -29,6 +29,7 @@ mod noqa; pub mod packaging; pub mod pyproject_toml; pub mod registry; +mod renamer; pub mod resolver; mod rule_redirects; mod rule_selector; diff --git a/crates/ruff/src/renamer.rs b/crates/ruff/src/renamer.rs new file mode 100644 index 0000000000..05ab127f2e --- /dev/null +++ b/crates/ruff/src/renamer.rs @@ -0,0 +1,176 @@ +//! Code modification struct to support symbol renaming within a scope. + +use anyhow::{anyhow, Result}; + +use ruff_diagnostics::Edit; +use ruff_python_semantic::{Binding, BindingKind, Scope, SemanticModel}; + +pub(crate) struct Renamer; + +impl Renamer { + /// Rename a symbol (from `name` to `target`) within a [`Scope`]. + /// + /// ## How it works + /// + /// The renaming algorithm is as follows: + /// + /// 1. Start with the first [`Binding`] in the scope, for the given name. For example, in the + /// following snippet, we'd start by examining the `x = 1` binding: + /// + /// ```python + /// if True: + /// x = 1 + /// print(x) + /// else: + /// x = 2 + /// print(x) + /// + /// print(x) + /// ``` + /// + /// 2. Rename the [`Binding`]. In most cases, this is a simple replacement. For example, + /// renaming `x` to `y` above would require replacing `x = 1` with `y = 1`. After the + /// first replacement in the snippet above, we'd have: + /// + /// ```python + /// if True: + /// y = 1 + /// print(x) + /// else: + /// x = 2 + /// print(x) + /// + /// print(x) + /// ``` + /// + /// Note that, when renaming imports, we need to instead rename (or add) an alias. For + /// example, to rename `pandas` to `pd`, we may need to rewrite `import pandas` to + /// `import pandas as pd`, rather than `import pd`. + /// + /// 3. Rename every reference to the [`Binding`]. For example, renaming the references to the + /// `x = 1` binding above would give us: + /// + /// ```python + /// if True: + /// y = 1 + /// print(y) + /// else: + /// x = 2 + /// print(x) + /// + /// print(x) + /// ``` + /// + /// 4. Rename every delayed annotation. (See [`SemanticModel::delayed_annotations`].) + /// + /// 5. Repeat the above process for every [`Binding`] in the scope with the given name. + /// After renaming the `x = 2` binding, we'd have: + /// + /// ```python + /// if True: + /// y = 1 + /// print(y) + /// else: + /// y = 2 + /// print(y) + /// + /// print(y) + /// ``` + /// + /// ## Limitations + /// + /// `global` and `nonlocal` declarations are not yet supported. + /// + /// `global` and `nonlocal` declarations add some additional complexity. If we're renaming a + /// name that's declared as `global` or `nonlocal` in a child scope, we need to rename the name + /// in that scope too, repeating the above process. + /// + /// If we're renaming a name that's declared as `global` or `nonlocal` in the current scope, + /// then we need to identify the scope in which the name is declared, and perform the rename + /// in that scope instead (which will in turn trigger the above process on the current scope). + pub(crate) fn rename( + name: &str, + target: &str, + scope: &Scope, + semantic: &SemanticModel, + ) -> Result<(Edit, Vec)> { + let mut edits = vec![]; + + // Iterate over every binding to the name in the scope. + for binding_id in scope.get_all(name) { + let binding = semantic.binding(binding_id); + + // Rename the binding. + if let Some(edit) = Renamer::rename_binding(binding, name, target) { + edits.push(edit); + + // Rename any delayed annotations. + if let Some(annotations) = semantic.delayed_annotations(binding_id) { + edits.extend(annotations.iter().filter_map(|annotation_id| { + let annotation = semantic.binding(*annotation_id); + Renamer::rename_binding(annotation, name, target) + })); + } + + // Rename the references to the binding. + edits.extend(binding.references().map(|reference_id| { + let reference = semantic.reference(reference_id); + Edit::range_replacement(target.to_string(), reference.range()) + })); + } + } + + // Deduplicate any edits. In some cases, a reference can be both a read _and_ a write. For + // example, `x += 1` is both a read of and a write to `x`. + edits.sort(); + edits.dedup(); + + let edit = edits + .pop() + .ok_or(anyhow!("Unable to rename any references to `{name}`"))?; + Ok((edit, edits)) + } + + /// Rename a [`Binding`] reference. + fn rename_binding(binding: &Binding, name: &str, target: &str) -> Option { + match &binding.kind { + BindingKind::Importation(_) | BindingKind::FromImportation(_) => { + if binding.is_alias() { + // Ex) Rename `import pandas as alias` to `import pandas as pd`. + Some(Edit::range_replacement(target.to_string(), binding.range)) + } else { + // Ex) Rename `import pandas` to `import pandas as pd`. + Some(Edit::range_replacement( + format!("{name} as {target}"), + binding.range, + )) + } + } + BindingKind::SubmoduleImportation(import) => { + // Ex) Rename `import pandas.core` to `import pandas as pd`. + let module_name = import.qualified_name.split('.').next().unwrap(); + Some(Edit::range_replacement( + format!("{module_name} as {target}"), + binding.range, + )) + } + // Avoid renaming builtins and other "special" bindings. + BindingKind::FutureImportation | BindingKind::Builtin | BindingKind::Export(_) => None, + // By default, replace the binding's name with the target name. + BindingKind::Annotation + | BindingKind::Argument + | BindingKind::NamedExprAssignment + | BindingKind::UnpackedAssignment + | BindingKind::Assignment + | BindingKind::LoopVar + | BindingKind::Global + | BindingKind::Nonlocal + | BindingKind::ClassDefinition + | BindingKind::FunctionDefinition + | BindingKind::Deletion + | BindingKind::UnboundException => { + Some(Edit::range_replacement(target.to_string(), binding.range)) + } + } + } +} diff --git a/crates/ruff/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs b/crates/ruff/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs index db17d9c272..07a3039083 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs @@ -1,9 +1,10 @@ -use rustpython_parser::ast::StmtImportFrom; - -use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_semantic::{BindingKind, FromImportation, Scope}; use crate::checkers::ast::Checker; +use crate::registry::AsRule; +use crate::renamer::Renamer; /// ## What it does /// Checks for `from collections.abc import Set` imports that do not alias @@ -30,6 +31,8 @@ use crate::checkers::ast::Checker; pub struct UnaliasedCollectionsAbcSetImport; impl Violation for UnaliasedCollectionsAbcSetImport { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { format!( @@ -43,20 +46,33 @@ impl Violation for UnaliasedCollectionsAbcSetImport { } /// PYI025 -pub(crate) fn unaliased_collections_abc_set_import(checker: &mut Checker, stmt: &StmtImportFrom) { - let Some(module_id) = &stmt.module else { - return; - }; - if module_id.as_str() != "collections.abc" { - return; - } - - for name in &stmt.names { - if name.name.as_str() == "Set" && name.asname.is_none() { - checker.diagnostics.push(Diagnostic::new( - UnaliasedCollectionsAbcSetImport, - name.range, - )); +pub(crate) fn unaliased_collections_abc_set_import( + checker: &Checker, + scope: &Scope, + diagnostics: &mut Vec, +) { + for (name, binding_id) in scope.all_bindings() { + let binding = checker.semantic().binding(binding_id); + let BindingKind::FromImportation(FromImportation { qualified_name }) = &binding.kind else { + continue; + }; + if qualified_name.as_str() != "collections.abc.Set" { + continue; } + if name == "AbstractSet" { + continue; + } + + let mut diagnostic = Diagnostic::new(UnaliasedCollectionsAbcSetImport, binding.range); + if checker.patch(diagnostic.kind.rule()) { + if checker.semantic().is_available("AbstractSet") { + diagnostic.try_set_fix(|| { + let (edit, rest) = + Renamer::rename(name, "AbstractSet", scope, checker.semantic())?; + Ok(Fix::suggested_edits(edit, rest)) + }); + } + } + diagnostics.push(diagnostic); } } diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI025_PYI025.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI025_PYI025.pyi.snap index 5c0dc3649d..b3659c1544 100644 --- a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI025_PYI025.pyi.snap +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI025_PYI025.pyi.snap @@ -1,22 +1,81 @@ --- source: crates/ruff/src/rules/flake8_pyi/mod.rs --- -PYI025.pyi:4:29: PYI025 Use `from collections.abc import Set as AbstractSet` to avoid confusion with the `set` builtin - | -4 | from collections.abc import Set # PYI025 - | ^^^ PYI025 - | - = help: Alias `Set` to `AbstractSet` - -PYI025.pyi:10:5: PYI025 Use `from collections.abc import Set as AbstractSet` to avoid confusion with the `set` builtin +PYI025.pyi:8:33: PYI025 [*] Use `from collections.abc import Set as AbstractSet` to avoid confusion with the `set` builtin | - 8 | Container, - 9 | Sized, -10 | Set, # PYI025 - | ^^^ PYI025 -11 | ValuesView -12 | ) + 7 | def f(): + 8 | from collections.abc import Set # PYI025 + | ^^^ PYI025 + 9 | +10 | def f(): | = help: Alias `Set` to `AbstractSet` +ℹ Suggested fix +5 5 | from collections.abc import Container, Sized, Set as AbstractSet, ValuesView # Ok +6 6 | +7 7 | def f(): +8 |- from collections.abc import Set # PYI025 + 8 |+ from collections.abc import Set as AbstractSet # PYI025 +9 9 | +10 10 | def f(): +11 11 | from collections.abc import Container, Sized, Set, ValuesView # PYI025 + +PYI025.pyi:11:51: PYI025 [*] Use `from collections.abc import Set as AbstractSet` to avoid confusion with the `set` builtin + | +10 | def f(): +11 | from collections.abc import Container, Sized, Set, ValuesView # PYI025 + | ^^^ PYI025 +12 | +13 | def f(): + | + = help: Alias `Set` to `AbstractSet` + +ℹ Suggested fix +8 8 | from collections.abc import Set # PYI025 +9 9 | +10 10 | def f(): +11 |- from collections.abc import Container, Sized, Set, ValuesView # PYI025 + 11 |+ from collections.abc import Container, Sized, Set as AbstractSet, ValuesView # PYI025 +12 12 | +13 13 | def f(): +14 14 | if True: + +PYI025.pyi:15:37: PYI025 [*] Use `from collections.abc import Set as AbstractSet` to avoid confusion with the `set` builtin + | +13 | def f(): +14 | if True: +15 | from collections.abc import Set + | ^^^ PYI025 +16 | else: +17 | Set = 1 + | + = help: Alias `Set` to `AbstractSet` + +ℹ Suggested fix +12 12 | +13 13 | def f(): +14 14 | if True: +15 |- from collections.abc import Set + 15 |+ from collections.abc import Set as AbstractSet +16 16 | else: +17 |- Set = 1 + 17 |+ AbstractSet = 1 +18 18 | +19 |- x: Set = set() + 19 |+ x: AbstractSet = set() +20 20 | +21 |- x: Set + 21 |+ x: AbstractSet +22 22 | +23 |- del Set + 23 |+ del AbstractSet +24 24 | +25 25 | def f(): +26 |- print(Set) + 26 |+ print(AbstractSet) +27 27 | +28 28 | def Set(): +29 29 | pass + diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index ea79278389..819782a3af 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -44,6 +44,18 @@ impl<'a> Binding<'a> { self.flags.contains(BindingFlags::EXPLICIT_EXPORT) } + /// Return `true` if this [`Binding`] represents an external symbol + /// (e.g., `FastAPI` in `from fastapi import FastAPI`). + pub const fn is_external(&self) -> bool { + self.flags.contains(BindingFlags::EXTERNAL) + } + + /// Return `true` if this [`Binding`] represents an aliased symbol + /// (e.g., `app` in `from fastapi import FastAPI as app`). + pub const fn is_alias(&self) -> bool { + self.flags.contains(BindingFlags::ALIAS) + } + /// Return `true` if this [`Binding`] represents an unbound variable /// (e.g., `x` in `x = 1; del x`). pub const fn is_unbound(&self) -> bool { @@ -161,9 +173,25 @@ bitflags! { /// /// For example, the binding could be `FastAPI` in: /// ```python - /// import FastAPI as FastAPI + /// from fastapi import FastAPI as FastAPI /// ``` const EXPLICIT_EXPORT = 1 << 0; + + /// The binding represents an external symbol, like an import or a builtin. + /// + /// For example, the binding could be `FastAPI` in: + /// ```python + /// from fastapi import FastAPI + /// ``` + const EXTERNAL = 1 << 1; + + /// The binding is an aliased symbol. + /// + /// For example, the binding could be `app` in: + /// ```python + /// from fastapi import FastAPI as app + /// ``` + const ALIAS = 1 << 2; } } From fd1dfc3bfa291d7a0689fa1fa4a75b03083623c3 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 16 Jun 2023 10:35:10 -0400 Subject: [PATCH 088/447] Add support for global and nonlocal symbol renames (#5134) ## Summary In #5074, we introduced an abstraction to support local symbol renames ("local" here refers to "within a module"). However, that abstraction didn't support `global` and `nonlocal` symbols. This PR extends it to those cases. Broadly, there are considerations. First, if we're renaming a symbol in a scope in which it is declared `global` or `nonlocal`. For example, given: ```python x = 1 def foo(): global x ``` Then when renaming `x` in `foo`, we need to detect that it's `global` and instead perform the rename starting from the module scope. Second, when renaming a symbol, we need to determine the scopes in which it is declared `global` or `nonlocal`. This is effectively the inverse of the above: when renaming `x` in the module scope, we need to detect that we should _also_ rename `x` in `foo`. To support these cases, the renaming algorithm was adjusted as follows: - When we start a rename in a scope, determine whether the symbol is declared `global` or `nonlocal` by looking for a `global` or `nonlocal` binding. If it is, start the rename in the defining scope. (This requires storing the defining scope on the `nonlocal` binding, which is new.) - We then perform the rename in the defining scope. - We then check whether the symbol was declared as `global` or `nonlocal` in any scopes, and perform the rename in those scopes too. (Thankfully, this doesn't need to be done recursively.) Closes #5092. ## Test Plan Added some additional snapshot tests. --- .../test/fixtures/flake8_pyi/PYI025.pyi | 20 +++ crates/ruff/src/checkers/ast/mod.rs | 49 +++---- crates/ruff/src/renamer.rs | 131 ++++++++++++++---- ..._flake8_pyi__tests__PYI025_PYI025.pyi.snap | 128 +++++++++++++---- crates/ruff/src/rules/pandas_vet/helpers.rs | 2 +- crates/ruff_python_semantic/src/binding.rs | 3 +- crates/ruff_python_semantic/src/model.rs | 53 ++++++- 7 files changed, 305 insertions(+), 81 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI025.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI025.pyi index bda9c0d083..26a2a69ce9 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI025.pyi +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI025.pyi @@ -11,6 +11,7 @@ def f(): from collections.abc import Container, Sized, Set, ValuesView # PYI025 def f(): + """Test: local symbol renaming.""" if True: from collections.abc import Set else: @@ -28,3 +29,22 @@ def f(): def Set(): pass print(Set) + +from collections.abc import Set + +def f(): + """Test: global symbol renaming.""" + global Set + + Set = 1 + print(Set) + +def f(): + """Test: nonlocal symbol renaming.""" + from collections.abc import Set + + def g(): + nonlocal Set + + Set = 1 + print(Set) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index d9b1b9d3d4..4fe244633d 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -254,6 +254,12 @@ where let ranges: Vec = identifier::names(stmt, self.locator).collect(); if !self.semantic.scope_id.is_global() { for (name, range) in names.iter().zip(ranges.iter()) { + if let Some(binding_id) = self.semantic.global_scope().get(name) { + // Mark the binding in the global scope as "rebound" in the current scope. + self.semantic + .add_rebinding_scope(binding_id, self.semantic.scope_id); + } + // Add a binding to the current scope. let binding_id = self.semantic.push_binding( *range, @@ -264,6 +270,7 @@ where scope.add(name, binding_id); } } + if self.enabled(Rule::AmbiguousVariableName) { self.diagnostics .extend(names.iter().zip(ranges.iter()).filter_map(|(name, range)| { @@ -275,33 +282,27 @@ where let ranges: Vec = identifier::names(stmt, self.locator).collect(); if !self.semantic.scope_id.is_global() { for (name, range) in names.iter().zip(ranges.iter()) { - // Add a binding to the current scope. - let binding_id = self.semantic.push_binding( - *range, - BindingKind::Nonlocal, - BindingFlags::empty(), - ); - let scope = self.semantic.scope_mut(); - scope.add(name, binding_id); - } - - // Mark the binding in the defining scopes as used too. (Skip the global scope - // and the current scope, and, per standard resolution rules, any class scopes.) - for (name, range) in names.iter().zip(ranges.iter()) { - let binding_id = self - .semantic - .scopes - .ancestors(self.semantic.scope_id) - .skip(1) - .filter(|scope| !(scope.kind.is_module() || scope.kind.is_class())) - .find_map(|scope| scope.get(name.as_str())); - - if let Some(binding_id) = binding_id { + if let Some((scope_id, binding_id)) = self.semantic.nonlocal(name) { + // Mark the binding as "used". self.semantic.add_local_reference( binding_id, - stmt.range(), + *range, ExecutionContext::Runtime, ); + + // Mark the binding in the enclosing scope as "rebound" in the current + // scope. + self.semantic + .add_rebinding_scope(binding_id, self.semantic.scope_id); + + // Add a binding to the current scope. + let binding_id = self.semantic.push_binding( + *range, + BindingKind::Nonlocal(scope_id), + BindingFlags::empty(), + ); + let scope = self.semantic.scope_mut(); + scope.add(name, binding_id); } else { if self.enabled(Rule::NonlocalWithoutBinding) { self.diagnostics.push(Diagnostic::new( @@ -4283,7 +4284,7 @@ impl<'a> Checker<'a> { BindingKind::Builtin | BindingKind::Deletion | BindingKind::UnboundException => { // Avoid overriding builtins. } - kind @ (BindingKind::Global | BindingKind::Nonlocal) => { + kind @ (BindingKind::Global | BindingKind::Nonlocal(_)) => { // If the original binding was a global or nonlocal, then the new binding is // too. let references = shadowed.references.clone(); diff --git a/crates/ruff/src/renamer.rs b/crates/ruff/src/renamer.rs index 05ab127f2e..6c75d9176d 100644 --- a/crates/ruff/src/renamer.rs +++ b/crates/ruff/src/renamer.rs @@ -1,21 +1,41 @@ //! Code modification struct to support symbol renaming within a scope. use anyhow::{anyhow, Result}; +use itertools::Itertools; use ruff_diagnostics::Edit; -use ruff_python_semantic::{Binding, BindingKind, Scope, SemanticModel}; +use ruff_python_semantic::{Binding, BindingKind, Scope, ScopeId, SemanticModel}; pub(crate) struct Renamer; impl Renamer { - /// Rename a symbol (from `name` to `target`) within a [`Scope`]. + /// Rename a symbol (from `name` to `target`). /// /// ## How it works /// /// The renaming algorithm is as follows: /// - /// 1. Start with the first [`Binding`] in the scope, for the given name. For example, in the - /// following snippet, we'd start by examining the `x = 1` binding: + /// 1. Determine the scope in which the rename should occur. This is typically the scope passed + /// in by the caller. However, if a symbol is `nonlocal` or `global`, then the rename needs + /// to occur in the scope in which the symbol is declared. For example, attempting to rename + /// `x` in `foo` below should trigger a rename in the module scope: + /// + /// ```python + /// x = 1 + /// + /// def foo(): + /// global x + /// x = 2 + /// ``` + /// + /// 1. Determine whether the symbol is rebound in another scope. This is effectively the inverse + /// of the previous step: when attempting to rename `x` in the module scope, we need to + /// detect that `x` is rebound in the `foo` scope. Determine every scope in which the symbol + /// is rebound, and add it to the set of scopes in which the rename should occur. + /// + /// 1. Start with the first scope in the stack. Take the first [`Binding`] in the scope, for the + /// given name. For example, in the following snippet, we'd start by examining the `x = 1` + /// binding: /// /// ```python /// if True: @@ -28,7 +48,7 @@ impl Renamer { /// print(x) /// ``` /// - /// 2. Rename the [`Binding`]. In most cases, this is a simple replacement. For example, + /// 1. Rename the [`Binding`]. In most cases, this is a simple replacement. For example, /// renaming `x` to `y` above would require replacing `x = 1` with `y = 1`. After the /// first replacement in the snippet above, we'd have: /// @@ -47,7 +67,7 @@ impl Renamer { /// example, to rename `pandas` to `pd`, we may need to rewrite `import pandas` to /// `import pandas as pd`, rather than `import pd`. /// - /// 3. Rename every reference to the [`Binding`]. For example, renaming the references to the + /// 1. Rename every reference to the [`Binding`]. For example, renaming the references to the /// `x = 1` binding above would give us: /// /// ```python @@ -61,9 +81,9 @@ impl Renamer { /// print(x) /// ``` /// - /// 4. Rename every delayed annotation. (See [`SemanticModel::delayed_annotations`].) + /// 1. Rename every delayed annotation. (See [`SemanticModel::delayed_annotations`].) /// - /// 5. Repeat the above process for every [`Binding`] in the scope with the given name. + /// 1. Repeat the above process for every [`Binding`] in the scope with the given name. /// After renaming the `x = 2` binding, we'd have: /// /// ```python @@ -77,17 +97,7 @@ impl Renamer { /// print(y) /// ``` /// - /// ## Limitations - /// - /// `global` and `nonlocal` declarations are not yet supported. - /// - /// `global` and `nonlocal` declarations add some additional complexity. If we're renaming a - /// name that's declared as `global` or `nonlocal` in a child scope, we need to rename the name - /// in that scope too, repeating the above process. - /// - /// If we're renaming a name that's declared as `global` or `nonlocal` in the current scope, - /// then we need to identify the scope in which the name is declared, and perform the rename - /// in that scope instead (which will in turn trigger the above process on the current scope). + /// 1. Repeat the above process for every scope in the stack. pub(crate) fn rename( name: &str, target: &str, @@ -96,6 +106,82 @@ impl Renamer { ) -> Result<(Edit, Vec)> { let mut edits = vec![]; + // Determine whether the symbol is `nonlocal` or `global`. (A symbol can't be both; Python + // raises a `SyntaxError`.) If the symbol is `nonlocal` or `global`, we need to rename it in + // the scope in which it's declared, rather than the current scope. For example, given: + // + // ```python + // x = 1 + // + // def foo(): + // global x + // ``` + // + // When renaming `x` in `foo`, we detect that `x` is a global, and back out to the module + // scope. + let scope_id = scope.get_all(name).find_map(|binding_id| { + let binding = semantic.binding(binding_id); + match binding.kind { + BindingKind::Global => Some(ScopeId::global()), + BindingKind::Nonlocal(symbol_id) => Some(symbol_id), + _ => None, + } + }); + + let scope = scope_id.map_or(scope, |scope_id| &semantic.scopes[scope_id]); + edits.extend(Renamer::rename_in_scope(name, target, scope, semantic)); + + // Find any scopes in which the symbol is referenced as `nonlocal` or `global`. For example, + // given: + // + // ```python + // x = 1 + // + // def foo(): + // global x + // + // def bar(): + // global x + // ``` + // + // When renaming `x` in `foo`, we detect that `x` is a global, and back out to the module + // scope. But we need to rename `x` in `bar` too. + // + // Note that it's impossible for a symbol to be referenced as both `nonlocal` and `global` + // in the same program. If a symbol is referenced as `global`, then it must be defined in + // the module scope. If a symbol is referenced as `nonlocal`, then it _can't_ be defined in + // the module scope (because `nonlocal` can only be used in a nested scope). + for scope_id in scope + .get_all(name) + .filter_map(|binding_id| semantic.rebinding_scopes(binding_id)) + .flatten() + .dedup() + .copied() + { + let scope = &semantic.scopes[scope_id]; + edits.extend(Renamer::rename_in_scope(name, target, scope, semantic)); + } + + // Deduplicate any edits. + edits.sort(); + edits.dedup(); + + let edit = edits + .pop() + .ok_or(anyhow!("Unable to rename any references to `{name}`"))?; + + Ok((edit, edits)) + } + + /// Rename a symbol in a single [`Scope`]. + fn rename_in_scope( + name: &str, + target: &str, + scope: &Scope, + semantic: &SemanticModel, + ) -> Vec { + let mut edits = vec![]; + // Iterate over every binding to the name in the scope. for binding_id in scope.get_all(name) { let binding = semantic.binding(binding_id); @@ -125,10 +211,7 @@ impl Renamer { edits.sort(); edits.dedup(); - let edit = edits - .pop() - .ok_or(anyhow!("Unable to rename any references to `{name}`"))?; - Ok((edit, edits)) + edits } /// Rename a [`Binding`] reference. @@ -164,7 +247,7 @@ impl Renamer { | BindingKind::Assignment | BindingKind::LoopVar | BindingKind::Global - | BindingKind::Nonlocal + | BindingKind::Nonlocal(_) | BindingKind::ClassDefinition | BindingKind::FunctionDefinition | BindingKind::Deletion diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI025_PYI025.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI025_PYI025.pyi.snap index b3659c1544..6da7686105 100644 --- a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI025_PYI025.pyi.snap +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI025_PYI025.pyi.snap @@ -39,43 +39,111 @@ PYI025.pyi:11:51: PYI025 [*] Use `from collections.abc import Set as AbstractSet 11 |+ from collections.abc import Container, Sized, Set as AbstractSet, ValuesView # PYI025 12 12 | 13 13 | def f(): -14 14 | if True: +14 14 | """Test: local symbol renaming.""" -PYI025.pyi:15:37: PYI025 [*] Use `from collections.abc import Set as AbstractSet` to avoid confusion with the `set` builtin +PYI025.pyi:16:37: PYI025 [*] Use `from collections.abc import Set as AbstractSet` to avoid confusion with the `set` builtin | -13 | def f(): -14 | if True: -15 | from collections.abc import Set +14 | """Test: local symbol renaming.""" +15 | if True: +16 | from collections.abc import Set | ^^^ PYI025 -16 | else: -17 | Set = 1 +17 | else: +18 | Set = 1 | = help: Alias `Set` to `AbstractSet` ℹ Suggested fix -12 12 | 13 13 | def f(): -14 14 | if True: -15 |- from collections.abc import Set - 15 |+ from collections.abc import Set as AbstractSet -16 16 | else: -17 |- Set = 1 - 17 |+ AbstractSet = 1 -18 18 | -19 |- x: Set = set() - 19 |+ x: AbstractSet = set() -20 20 | -21 |- x: Set - 21 |+ x: AbstractSet -22 22 | -23 |- del Set - 23 |+ del AbstractSet -24 24 | -25 25 | def f(): -26 |- print(Set) - 26 |+ print(AbstractSet) -27 27 | -28 28 | def Set(): -29 29 | pass +14 14 | """Test: local symbol renaming.""" +15 15 | if True: +16 |- from collections.abc import Set + 16 |+ from collections.abc import Set as AbstractSet +17 17 | else: +18 |- Set = 1 + 18 |+ AbstractSet = 1 +19 19 | +20 20 | x: Set = set() +21 21 | +22 22 | x: Set +23 23 | +24 |- del Set + 24 |+ del AbstractSet +25 25 | +26 26 | def f(): +27 |- print(Set) + 27 |+ print(AbstractSet) +28 28 | +29 29 | def Set(): +30 30 | pass + +PYI025.pyi:33:29: PYI025 [*] Use `from collections.abc import Set as AbstractSet` to avoid confusion with the `set` builtin + | +31 | print(Set) +32 | +33 | from collections.abc import Set + | ^^^ PYI025 +34 | +35 | def f(): + | + = help: Alias `Set` to `AbstractSet` + +ℹ Suggested fix +17 17 | else: +18 18 | Set = 1 +19 19 | +20 |- x: Set = set() + 20 |+ x: AbstractSet = set() +21 21 | +22 |- x: Set + 22 |+ x: AbstractSet +23 23 | +24 24 | del Set +25 25 | +-------------------------------------------------------------------------------- +30 30 | pass +31 31 | print(Set) +32 32 | +33 |-from collections.abc import Set + 33 |+from collections.abc import Set as AbstractSet +34 34 | +35 35 | def f(): +36 36 | """Test: global symbol renaming.""" +37 |- global Set + 37 |+ global AbstractSet +38 38 | +39 |- Set = 1 +40 |- print(Set) + 39 |+ AbstractSet = 1 + 40 |+ print(AbstractSet) +41 41 | +42 42 | def f(): +43 43 | """Test: nonlocal symbol renaming.""" + +PYI025.pyi:44:33: PYI025 [*] Use `from collections.abc import Set as AbstractSet` to avoid confusion with the `set` builtin + | +42 | def f(): +43 | """Test: nonlocal symbol renaming.""" +44 | from collections.abc import Set + | ^^^ PYI025 +45 | +46 | def g(): + | + = help: Alias `Set` to `AbstractSet` + +ℹ Suggested fix +41 41 | +42 42 | def f(): +43 43 | """Test: nonlocal symbol renaming.""" +44 |- from collections.abc import Set + 44 |+ from collections.abc import Set as AbstractSet +45 45 | +46 46 | def g(): +47 |- nonlocal Set + 47 |+ nonlocal AbstractSet +48 48 | +49 |- Set = 1 +50 |- print(Set) + 49 |+ AbstractSet = 1 + 50 |+ print(AbstractSet) diff --git a/crates/ruff/src/rules/pandas_vet/helpers.rs b/crates/ruff/src/rules/pandas_vet/helpers.rs index ec4400a978..1b92dd9867 100644 --- a/crates/ruff/src/rules/pandas_vet/helpers.rs +++ b/crates/ruff/src/rules/pandas_vet/helpers.rs @@ -38,7 +38,7 @@ pub(super) fn test_expression(expr: &Expr, semantic: &SemanticModel) -> Resoluti | BindingKind::UnpackedAssignment | BindingKind::LoopVar | BindingKind::Global - | BindingKind::Nonlocal => Resolution::RelevantLocal, + | BindingKind::Nonlocal(_) => Resolution::RelevantLocal, BindingKind::Importation(Importation { qualified_name: module, }) if module == "pandas" => Resolution::PandasModule, diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index 819782a3af..7284de17f6 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -10,6 +10,7 @@ use crate::context::ExecutionContext; use crate::model::SemanticModel; use crate::node::NodeId; use crate::reference::ReferenceId; +use crate::ScopeId; #[derive(Debug, Clone)] pub struct Binding<'a> { @@ -336,7 +337,7 @@ pub enum BindingKind<'a> { /// def foo(): /// nonlocal x /// ``` - Nonlocal, + Nonlocal(ScopeId), /// A binding for a builtin, like `print` or `bool`. Builtin, diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 944e4f4f57..bae2be65ca 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -73,7 +73,7 @@ pub struct SemanticModel<'a> { /// Map from binding index to indexes of bindings that annotate it (in the same scope). /// - /// For example: + /// For example, given: /// ```python /// x = 1 /// x: int @@ -94,6 +94,21 @@ pub struct SemanticModel<'a> { /// first binding in a scope; any annotations that follow are treated as "delayed" annotations. delayed_annotations: HashMap, BuildNoHashHasher>, + /// Map from binding ID to the IDs of all scopes in which it is declared a `global` or + /// `nonlocal`. + /// + /// For example, given: + /// ```python + /// x = 1 + /// + /// def f(): + /// global x + /// ``` + /// + /// In this case, the binding created by `x = 1` is rebound within the scope created by `f` + /// by way of the `global x` statement. + rebinding_scopes: HashMap, BuildNoHashHasher>, + /// Body iteration; used to peek at siblings. pub body: &'a [Stmt], pub body_index: usize, @@ -123,6 +138,7 @@ impl<'a> SemanticModel<'a> { globals: GlobalsArena::default(), shadowed_bindings: IntMap::default(), delayed_annotations: IntMap::default(), + rebinding_scopes: IntMap::default(), body: &[], body_index: 0, flags: SemanticModelFlags::new(path), @@ -699,6 +715,26 @@ impl<'a> SemanticModel<'a> { self.globals[global_id].get(name).copied() } + /// Given a `name` that has been declared `nonlocal`, return the [`ScopeId`] and [`BindingId`] + /// to which it refers. + /// + /// Unlike `global` declarations, for which the scope is unambiguous, Python requires that + /// `nonlocal` declarations refer to the closest enclosing scope that contains a binding for + /// the given name. + pub fn nonlocal(&self, name: &str) -> Option<(ScopeId, BindingId)> { + self.scopes + .ancestor_ids(self.scope_id) + .skip(1) + .find_map(|scope_id| { + let scope = &self.scopes[scope_id]; + if scope.kind.is_module() || scope.kind.is_class() { + None + } else { + scope.get(name).map(|binding_id| (scope_id, binding_id)) + } + }) + } + /// Return `true` if the given [`ScopeId`] matches that of the current scope. pub fn is_current_scope(&self, scope_id: ScopeId) -> bool { self.scope_id == scope_id @@ -766,6 +802,21 @@ impl<'a> SemanticModel<'a> { self.delayed_annotations.get(&binding_id).map(Vec::as_slice) } + /// Mark the given [`BindingId`] as rebound in the given [`ScopeId`] (i.e., declared as + /// `global` or `nonlocal`). + pub fn add_rebinding_scope(&mut self, binding_id: BindingId, scope_id: ScopeId) { + self.rebinding_scopes + .entry(binding_id) + .or_insert_with(Vec::new) + .push(scope_id); + } + + /// Return the list of [`ScopeId`]s in which the given [`BindingId`] is rebound (i.e., declared + /// as `global` or `nonlocal`). + pub fn rebinding_scopes(&self, binding_id: BindingId) -> Option<&[ScopeId]> { + self.rebinding_scopes.get(&binding_id).map(Vec::as_slice) + } + /// Return the [`ExecutionContext`] of the current scope. pub const fn execution_context(&self) -> ExecutionContext { if self.in_type_checking_block() From b3240dbfa2c2b42dc88a1bc3bb14557ca41f9382 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 16 Jun 2023 11:06:59 -0400 Subject: [PATCH 089/447] Avoid propagating `BindingKind::Global` and `BindingKind::Nonlocal` (#5136) ## Summary This PR fixes a small quirk in the semantic model. Typically, when we see an import, like `import foo`, we create a `BindingKind::Importation` for it. However, if `foo` has been declared as a `global`, then we propagate the kind forward. So given: ```python global foo import foo ``` We'd create two bindings for `foo`, both with type `global`. This was originally borrowed from Pyflakes, and it exists to help avoid false-positives like: ```python def f(): global foo # Don't mark `foo` as "assigned but unused"! It's a global! foo = 1 ``` This PR removes that behavior, and instead tracks "Does this binding refer to a global?" as a flag. This is much cleaner, since it means we don't "lose" the identity of various bindings. As a very strange example of why this matters, consider: ```python def foo(): global Member from module import Member x: Member = 1 ``` `Member` is only used in a typing context, so we should flag it and say "move it to a `TYPE_CHECKING` block". However, when we go to analyze `from module import Member`, it has `BindingKind::Global`. So we don't even know that it's an import! --- .../fixtures/flake8_type_checking/TCH002.py | 8 +++ .../test/fixtures/pyflakes/F841_3.py | 9 ++++ .../test/fixtures/pylint/global_statement.py | 7 +++ crates/ruff/src/checkers/ast/mod.rs | 54 +++++++++---------- ...ing-only-third-party-import_TCH002.py.snap | 28 ++++++++++ .../src/rules/pyflakes/rules/unused_import.rs | 6 ++- .../rules/pyflakes/rules/unused_variable.rs | 2 + .../rules/pylint/rules/global_statement.rs | 25 +++++---- ...t__tests__PLW0603_global_statement.py.snap | 19 +++++++ crates/ruff_python_semantic/src/binding.rs | 35 ++++++++++++ 10 files changed, 152 insertions(+), 41 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_type_checking/TCH002.py b/crates/ruff/resources/test/fixtures/flake8_type_checking/TCH002.py index 82d6d2f10b..9248c10775 100644 --- a/crates/ruff/resources/test/fixtures/flake8_type_checking/TCH002.py +++ b/crates/ruff/resources/test/fixtures/flake8_type_checking/TCH002.py @@ -164,3 +164,11 @@ def f(): ) x: DataFrame = 2 + + +def f(): + global Member + + from module import Member + + x: Member = 1 diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F841_3.py b/crates/ruff/resources/test/fixtures/pyflakes/F841_3.py index bd2a3f2e02..526c999441 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F841_3.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F841_3.py @@ -138,3 +138,12 @@ def f(provided: int) -> int: match provided: case {**x}: pass + + +global CONSTANT + + +def f() -> None: + global CONSTANT + CONSTANT = 1 + CONSTANT = 2 diff --git a/crates/ruff/resources/test/fixtures/pylint/global_statement.py b/crates/ruff/resources/test/fixtures/pylint/global_statement.py index 69bcc36775..1d28c61955 100644 --- a/crates/ruff/resources/test/fixtures/pylint/global_statement.py +++ b/crates/ruff/resources/test/fixtures/pylint/global_statement.py @@ -73,3 +73,10 @@ def override_class(): pass CLASS() + + +def multiple_assignment(): + """Should warn on every assignment.""" + global CONSTANT # [global-statement] + CONSTANT = 1 + CONSTANT = 2 diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 4fe244633d..a784e6d743 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -264,7 +264,7 @@ where let binding_id = self.semantic.push_binding( *range, BindingKind::Global, - BindingFlags::empty(), + BindingFlags::GLOBAL, ); let scope = self.semantic.scope_mut(); scope.add(name, binding_id); @@ -299,7 +299,7 @@ where let binding_id = self.semantic.push_binding( *range, BindingKind::Nonlocal(scope_id), - BindingFlags::empty(), + BindingFlags::NONLOCAL, ); let scope = self.semantic.scope_mut(); scope.add(name, binding_id); @@ -4279,22 +4279,27 @@ impl<'a> Checker<'a> { return binding_id; } + // Avoid shadowing builtins. let shadowed = &self.semantic.bindings[shadowed_id]; - match &shadowed.kind { - BindingKind::Builtin | BindingKind::Deletion | BindingKind::UnboundException => { - // Avoid overriding builtins. + if !matches!( + shadowed.kind, + BindingKind::Builtin | BindingKind::Deletion | BindingKind::UnboundException, + ) { + let references = shadowed.references.clone(); + let is_global = shadowed.is_global(); + let is_nonlocal = shadowed.is_nonlocal(); + + // If the shadowed binding was global, then this one is too. + if is_global { + self.semantic.bindings[binding_id].flags |= BindingFlags::GLOBAL; } - kind @ (BindingKind::Global | BindingKind::Nonlocal(_)) => { - // If the original binding was a global or nonlocal, then the new binding is - // too. - let references = shadowed.references.clone(); - self.semantic.bindings[binding_id].kind = kind.clone(); - self.semantic.bindings[binding_id].references = references; - } - _ => { - let references = shadowed.references.clone(); - self.semantic.bindings[binding_id].references = references; + + // If the shadowed binding was non-local, then this one is too. + if is_nonlocal { + self.semantic.bindings[binding_id].flags |= BindingFlags::NONLOCAL; } + + self.semantic.bindings[binding_id].references = references; } } @@ -4389,7 +4394,7 @@ impl<'a> Checker<'a> { if self.semantic.scope().kind.is_any_function() { // Ignore globals. if !self.semantic.scope().get(id).map_or(false, |binding_id| { - self.semantic.binding(binding_id).kind.is_global() + self.semantic.binding(binding_id).is_global() }) { pep8_naming::rules::non_lowercase_variable_in_function(self, expr, parent, id); } @@ -4839,17 +4844,12 @@ impl<'a> Checker<'a> { for (name, binding_id) in scope.bindings() { let binding = self.semantic.binding(binding_id); if binding.kind.is_global() { - if let Some(source) = binding.source { - let stmt = &self.semantic.stmts[source]; - if stmt.is_global_stmt() { - diagnostics.push(Diagnostic::new( - pylint::rules::GlobalVariableNotAssigned { - name: (*name).to_string(), - }, - binding.range, - )); - } - } + diagnostics.push(Diagnostic::new( + pylint::rules::GlobalVariableNotAssigned { + name: (*name).to_string(), + }, + binding.range, + )); } } } diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap index 7005762cc5..ab4ed38714 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap @@ -221,4 +221,32 @@ TCH002.py:47:22: TCH002 [*] Move third-party import `pandas` into a type-checkin 49 52 | x = dict["pd.DataFrame", "pd.DataFrame"] 50 53 | +TCH002.py:172:24: TCH002 [*] Move third-party import `module.Member` into a type-checking block + | +170 | global Member +171 | +172 | from module import Member + | ^^^^^^ TCH002 +173 | +174 | x: Member = 1 + | + = help: Move into type-checking block + +ℹ Suggested fix +1 1 | """Tests to determine accurate detection of typing-only imports.""" + 2 |+from typing import TYPE_CHECKING + 3 |+ + 4 |+if TYPE_CHECKING: + 5 |+ from module import Member +2 6 | +3 7 | +4 8 | def f(): +-------------------------------------------------------------------------------- +169 173 | def f(): +170 174 | global Member +171 175 | +172 |- from module import Member +173 176 | +174 177 | x: Member = 1 + diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs index d004b6433c..ca76ccfda2 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs @@ -103,7 +103,11 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut for binding_id in scope.binding_ids() { let binding = checker.semantic().binding(binding_id); - if binding.is_used() || binding.is_explicit_export() { + if binding.is_used() + || binding.is_explicit_export() + || binding.is_nonlocal() + || binding.is_global() + { continue; } diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs b/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs index e3edd38b15..de24c4d795 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs @@ -295,6 +295,8 @@ pub(crate) fn unused_variable(checker: &mut Checker, scope: ScopeId) { .map(|(name, binding_id)| (name, checker.semantic().binding(binding_id))) .filter_map(|(name, binding)| { if (binding.kind.is_assignment() || binding.kind.is_named_expr_assignment()) + && !binding.is_nonlocal() + && !binding.is_global() && !binding.is_used() && !checker.settings.dummy_variable_rgx.is_match(name) && name != "__tracebackhide__" diff --git a/crates/ruff/src/rules/pylint/rules/global_statement.rs b/crates/ruff/src/rules/pylint/rules/global_statement.rs index 6a8613b639..3eef3e4123 100644 --- a/crates/ruff/src/rules/pylint/rules/global_statement.rs +++ b/crates/ruff/src/rules/pylint/rules/global_statement.rs @@ -58,19 +58,18 @@ pub(crate) fn global_statement(checker: &mut Checker, name: &str) { let scope = checker.semantic().scope(); if let Some(binding_id) = scope.get(name) { let binding = checker.semantic().binding(binding_id); - if binding.kind.is_global() { - let source = checker.semantic().stmts[binding - .source - .expect("`global` bindings should always have a `source`")]; - let diagnostic = Diagnostic::new( - GlobalStatement { - name: name.to_string(), - }, - // Match Pylint's behavior by reporting on the `global` statement`, rather - // than the variable usage. - source.range(), - ); - checker.diagnostics.push(diagnostic); + if binding.is_global() { + if let Some(source) = binding.source { + let source = checker.semantic().stmts[source]; + checker.diagnostics.push(Diagnostic::new( + GlobalStatement { + name: name.to_string(), + }, + // Match Pylint's behavior by reporting on the `global` statement`, rather + // than the variable usage. + source.range(), + )); + } } } } diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0603_global_statement.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0603_global_statement.py.snap index 5228b0e568..3d858431c5 100644 --- a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0603_global_statement.py.snap +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0603_global_statement.py.snap @@ -79,4 +79,23 @@ global_statement.py:70:5: PLW0603 Using the global statement to update `CLASS` i 72 | class CLASS: | +global_statement.py:80:5: PLW0603 Using the global statement to update `CONSTANT` is discouraged + | +78 | def multiple_assignment(): +79 | """Should warn on every assignment.""" +80 | global CONSTANT # [global-statement] + | ^^^^^^^^^^^^^^^ PLW0603 +81 | CONSTANT = 1 +82 | CONSTANT = 2 + | + +global_statement.py:81:5: PLW0603 Using the global statement to update `CONSTANT` is discouraged + | +79 | """Should warn on every assignment.""" +80 | global CONSTANT # [global-statement] +81 | CONSTANT = 1 + | ^^^^^^^^^^^^ PLW0603 +82 | CONSTANT = 2 + | + diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index 7284de17f6..1eb256e089 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -57,6 +57,19 @@ impl<'a> Binding<'a> { self.flags.contains(BindingFlags::ALIAS) } + /// Return `true` if this [`Binding`] represents a `nonlocal`. A [`Binding`] is a `nonlocal` + /// if it's declared by a `nonlocal` statement, or shadows a [`Binding`] declared by a + /// `nonlocal` statement. + pub const fn is_nonlocal(&self) -> bool { + self.flags.contains(BindingFlags::NONLOCAL) + } + + /// Return `true` if this [`Binding`] represents a `global`. A [`Binding`] is a `global` if it's + /// declared by a `global` statement, or shadows a [`Binding`] declared by a `global` statement. + pub const fn is_global(&self) -> bool { + self.flags.contains(BindingFlags::GLOBAL) + } + /// Return `true` if this [`Binding`] represents an unbound variable /// (e.g., `x` in `x = 1; del x`). pub const fn is_unbound(&self) -> bool { @@ -193,6 +206,28 @@ bitflags! { /// from fastapi import FastAPI as app /// ``` const ALIAS = 1 << 2; + + /// The binding is `nonlocal` to the declaring scope. This could be a binding created by + /// a `nonlocal` statement, or a binding that shadows such a binding. + /// + /// For example, both of the bindings in the following function are `nonlocal`: + /// ```python + /// def f(): + /// nonlocal x + /// x = 1 + /// ``` + const NONLOCAL = 1 << 3; + + /// The binding is `global`. This could be a binding created by a `global` statement, or a + /// binding that shadows such a binding. + /// + /// For example, both of the bindings in the following function are `global`: + /// ```python + /// def f(): + /// global x + /// x = 1 + /// ``` + const GLOBAL = 1 << 4; } } From d0ad1ed0af15bf71664ae88e114c39aeee58767a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 16 Jun 2023 13:34:42 -0400 Subject: [PATCH 090/447] Replace static `CallPath` vectors with `matches!` macros (#5148) ## Summary After #5140, I audited the codebase for similar patterns (defining a list of `CallPath` entities in a static vector, then looping over them to pattern-match). This PR migrates all other such cases to use `match` and `matches!` where possible. There are a few benefits to this: 1. It more clearly denotes the intended semantics (branches are exclusive). 2. The compiler can help deduplicate the patterns and detect unreachable branches. 3. Performance: in the benchmark below, the all-rules performance is increased by nearly 10%... ## Benchmarks I decided to benchmark against a large file in the Airflow repository with a lot of type annotations ([`views.py`](https://raw.githubusercontent.com/apache/airflow/f03f73100e8a7d6019249889de567cb00e71e457/airflow/www/views.py)): ``` linter/default-rules/airflow/views.py time: [10.871 ms 10.882 ms 10.894 ms] thrpt: [19.739 MiB/s 19.761 MiB/s 19.781 MiB/s] change: time: [-2.7182% -2.5687% -2.4204%] (p = 0.00 < 0.05) thrpt: [+2.4805% +2.6364% +2.7942%] Performance has improved. linter/all-rules/airflow/views.py time: [24.021 ms 24.038 ms 24.062 ms] thrpt: [8.9373 MiB/s 8.9461 MiB/s 8.9527 MiB/s] change: time: [-8.9537% -8.8516% -8.7527%] (p = 0.00 < 0.05) thrpt: [+9.5923% +9.7112% +9.8342%] Performance has improved. Found 12 outliers among 100 measurements (12.00%) 5 (5.00%) high mild 7 (7.00%) high severe ``` The impact is dramatic -- nearly a 10% improvement for `all-rules`. --- .../flake8_annotations/rules/definition.rs | 17 +- .../flake8_async/rules/blocking_http_call.rs | 51 +- .../flake8_async/rules/blocking_os_call.rs | 45 +- .../rules/open_sleep_or_subprocess_call.rs | 48 +- .../rules/function_call_argument_default.rs | 3 +- .../rules/mutable_argument_default.rs | 35 +- .../rules/flake8_debugger/rules/debugger.rs | 89 +-- .../rules/flake8_pyi/rules/simple_defaults.rs | 66 +- .../src/rules/flake8_return/rules/function.rs | 33 +- .../function_call_in_dataclass_default.rs | 4 +- crates/ruff/src/rules/ruff/rules/helpers.rs | 23 +- .../rules/ruff/rules/mutable_class_default.rs | 8 +- .../ruff/rules/mutable_dataclass_default.rs | 6 +- .../src/analyze/typing.rs | 116 ++- crates/ruff_python_semantic/src/model.rs | 4 +- crates/ruff_python_stdlib/src/typing.rs | 677 +++++++++++------- 16 files changed, 641 insertions(+), 584 deletions(-) diff --git a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs index e839a092f7..22d63d89a3 100644 --- a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs @@ -1,6 +1,6 @@ use rustpython_parser::ast::{Expr, Ranged, Stmt}; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Violation}; +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::cast; use ruff_python_ast::helpers::ReturnStatementVisitor; @@ -8,7 +8,7 @@ use ruff_python_ast::identifier::Identifier; use ruff_python_ast::statement_visitor::StatementVisitor; use ruff_python_semantic::analyze::visibility; use ruff_python_semantic::{Definition, Member, MemberKind, SemanticModel}; -use ruff_python_stdlib::typing::SIMPLE_MAGIC_RETURN_TYPES; +use ruff_python_stdlib::typing::simple_magic_return_type; use crate::checkers::ast::Checker; use crate::registry::{AsRule, Rule}; @@ -667,9 +667,9 @@ pub(crate) fn definition( stmt.identifier(checker.locator), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { + diagnostic.try_set_fix(|| { fixes::add_return_annotation(checker.locator, stmt, "None") + .map(Fix::suggested) }); } diagnostics.push(diagnostic); @@ -683,12 +683,11 @@ pub(crate) fn definition( }, stmt.identifier(checker.locator), ); - let return_type = SIMPLE_MAGIC_RETURN_TYPES.get(name); - if let Some(return_type) = return_type { - if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { + if checker.patch(diagnostic.kind.rule()) { + if let Some(return_type) = simple_magic_return_type(name) { + diagnostic.try_set_fix(|| { fixes::add_return_annotation(checker.locator, stmt, return_type) + .map(Fix::suggested) }); } } diff --git a/crates/ruff/src/rules/flake8_async/rules/blocking_http_call.rs b/crates/ruff/src/rules/flake8_async/rules/blocking_http_call.rs index dbd00a3aaf..2ab4e94f4d 100644 --- a/crates/ruff/src/rules/flake8_async/rules/blocking_http_call.rs +++ b/crates/ruff/src/rules/flake8_async/rules/blocking_http_call.rs @@ -3,6 +3,7 @@ use rustpython_parser::ast::{Expr, 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; @@ -40,37 +41,35 @@ impl Violation for BlockingHttpCallInAsyncFunction { } } -const BLOCKING_HTTP_CALLS: &[&[&str]] = &[ - &["urllib", "request", "urlopen"], - &["httpx", "get"], - &["httpx", "post"], - &["httpx", "delete"], - &["httpx", "patch"], - &["httpx", "put"], - &["httpx", "head"], - &["httpx", "connect"], - &["httpx", "options"], - &["httpx", "trace"], - &["requests", "get"], - &["requests", "post"], - &["requests", "delete"], - &["requests", "patch"], - &["requests", "put"], - &["requests", "head"], - &["requests", "connect"], - &["requests", "options"], - &["requests", "trace"], -]; +fn is_blocking_http_call(call_path: &CallPath) -> bool { + matches!( + call_path.as_slice(), + ["urllib", "request", "urlopen"] + | [ + "httpx" | "requests", + "get" + | "post" + | "delete" + | "patch" + | "put" + | "head" + | "connect" + | "options" + | "trace" + ] + ) +} /// ASYNC100 pub(crate) fn blocking_http_call(checker: &mut Checker, expr: &Expr) { if checker.semantic().in_async_context() { if let Expr::Call(ast::ExprCall { func, .. }) = expr { - let call_path = checker.semantic().resolve_call_path(func); - let is_blocking = - call_path.map_or(false, |path| BLOCKING_HTTP_CALLS.contains(&path.as_slice())); - - if is_blocking { + if checker + .semantic() + .resolve_call_path(func) + .as_ref() + .map_or(false, is_blocking_http_call) + { checker.diagnostics.push(Diagnostic::new( BlockingHttpCallInAsyncFunction, func.range(), diff --git a/crates/ruff/src/rules/flake8_async/rules/blocking_os_call.rs b/crates/ruff/src/rules/flake8_async/rules/blocking_os_call.rs index 861689bb3d..c08dece6f4 100644 --- a/crates/ruff/src/rules/flake8_async/rules/blocking_os_call.rs +++ b/crates/ruff/src/rules/flake8_async/rules/blocking_os_call.rs @@ -3,6 +3,7 @@ use rustpython_parser::ast::{Expr, 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; @@ -39,31 +40,16 @@ impl Violation for BlockingOsCallInAsyncFunction { } } -const UNSAFE_OS_METHODS: &[&[&str]] = &[ - &["os", "popen"], - &["os", "posix_spawn"], - &["os", "posix_spawnp"], - &["os", "spawnl"], - &["os", "spawnle"], - &["os", "spawnlp"], - &["os", "spawnlpe"], - &["os", "spawnv"], - &["os", "spawnve"], - &["os", "spawnvp"], - &["os", "spawnvpe"], - &["os", "system"], -]; - /// ASYNC102 pub(crate) fn blocking_os_call(checker: &mut Checker, expr: &Expr) { if checker.semantic().in_async_context() { if let Expr::Call(ast::ExprCall { func, .. }) = expr { - let is_unsafe_os_method = checker + if checker .semantic() .resolve_call_path(func) - .map_or(false, |path| UNSAFE_OS_METHODS.contains(&path.as_slice())); - - if is_unsafe_os_method { + .as_ref() + .map_or(false, is_unsafe_os_method) + { checker .diagnostics .push(Diagnostic::new(BlockingOsCallInAsyncFunction, func.range())); @@ -71,3 +57,24 @@ pub(crate) fn blocking_os_call(checker: &mut Checker, expr: &Expr) { } } } + +fn is_unsafe_os_method(call_path: &CallPath) -> bool { + matches!( + call_path.as_slice(), + [ + "os", + "popen" + | "posix_spawn" + | "posix_spawnp" + | "spawnl" + | "spawnle" + | "spawnlp" + | "spawnlpe" + | "spawnv" + | "spawnve" + | "spawnvp" + | "spawnvpe" + | "system" + ] + ) +} diff --git a/crates/ruff/src/rules/flake8_async/rules/open_sleep_or_subprocess_call.rs b/crates/ruff/src/rules/flake8_async/rules/open_sleep_or_subprocess_call.rs index c0d370da82..0d1f813ec6 100644 --- a/crates/ruff/src/rules/flake8_async/rules/open_sleep_or_subprocess_call.rs +++ b/crates/ruff/src/rules/flake8_async/rules/open_sleep_or_subprocess_call.rs @@ -3,6 +3,7 @@ use rustpython_parser::ast::{Expr, 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; @@ -39,36 +40,16 @@ impl Violation for OpenSleepOrSubprocessInAsyncFunction { } } -const OPEN_SLEEP_OR_SUBPROCESS_CALL: &[&[&str]] = &[ - &["", "open"], - &["time", "sleep"], - &["subprocess", "run"], - &["subprocess", "Popen"], - // Deprecated subprocess calls: - &["subprocess", "call"], - &["subprocess", "check_call"], - &["subprocess", "check_output"], - &["subprocess", "getoutput"], - &["subprocess", "getstatusoutput"], - &["os", "wait"], - &["os", "wait3"], - &["os", "wait4"], - &["os", "waitid"], - &["os", "waitpid"], -]; - /// ASYNC101 pub(crate) fn open_sleep_or_subprocess_call(checker: &mut Checker, expr: &Expr) { if checker.semantic().in_async_context() { if let Expr::Call(ast::ExprCall { func, .. }) = expr { - let is_open_sleep_or_subprocess_call = checker + if checker .semantic() .resolve_call_path(func) - .map_or(false, |path| { - OPEN_SLEEP_OR_SUBPROCESS_CALL.contains(&path.as_slice()) - }); - - if is_open_sleep_or_subprocess_call { + .as_ref() + .map_or(false, is_open_sleep_or_subprocess_call) + { checker.diagnostics.push(Diagnostic::new( OpenSleepOrSubprocessInAsyncFunction, func.range(), @@ -77,3 +58,22 @@ pub(crate) fn open_sleep_or_subprocess_call(checker: &mut Checker, expr: &Expr) } } } + +fn is_open_sleep_or_subprocess_call(call_path: &CallPath) -> bool { + matches!( + call_path.as_slice(), + ["", "open"] + | ["time", "sleep"] + | [ + "subprocess", + "run" + | "Popen" + | "call" + | "check_call" + | "check_output" + | "getoutput" + | "getstatusoutput" + ] + | ["os", "wait" | "wait3" | "wait4" | "waitid" | "waitpid"] + ) +} diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs b/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs index 0443f328b8..6d0fcc4e21 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs @@ -7,11 +7,10 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::{compose_call_path, from_qualified_name, CallPath}; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; -use ruff_python_semantic::analyze::typing::is_immutable_func; +use ruff_python_semantic::analyze::typing::{is_immutable_func, is_mutable_func}; use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; -use crate::rules::flake8_bugbear::rules::mutable_argument_default::is_mutable_func; /// ## What it does /// Checks for function calls in default function arguments. 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 064d1516b4..986aca83d4 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 @@ -1,9 +1,8 @@ -use rustpython_parser::ast::{self, Arguments, Expr, Ranged}; +use rustpython_parser::ast::{Arguments, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::analyze::typing::is_immutable_annotation; -use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr}; use crate::checkers::ast::Checker; @@ -16,36 +15,6 @@ impl Violation for MutableArgumentDefault { format!("Do not use mutable data structures for argument defaults") } } -const MUTABLE_FUNCS: &[&[&str]] = &[ - &["", "dict"], - &["", "list"], - &["", "set"], - &["collections", "Counter"], - &["collections", "OrderedDict"], - &["collections", "defaultdict"], - &["collections", "deque"], -]; - -pub(crate) fn is_mutable_func(func: &Expr, semantic: &SemanticModel) -> bool { - semantic.resolve_call_path(func).map_or(false, |call_path| { - MUTABLE_FUNCS - .iter() - .any(|target| call_path.as_slice() == *target) - }) -} - -fn is_mutable_expr(expr: &Expr, semantic: &SemanticModel) -> bool { - match expr { - Expr::List(_) - | Expr::Dict(_) - | Expr::Set(_) - | Expr::ListComp(_) - | Expr::DictComp(_) - | Expr::SetComp(_) => true, - Expr::Call(ast::ExprCall { func, .. }) => is_mutable_func(func, semantic), - _ => false, - } -} /// B006 pub(crate) fn mutable_argument_default(checker: &mut Checker, arguments: &Arguments) { diff --git a/crates/ruff/src/rules/flake8_debugger/rules/debugger.rs b/crates/ruff/src/rules/flake8_debugger/rules/debugger.rs index e0552c1bcd..94c151c95b 100644 --- a/crates/ruff/src/rules/flake8_debugger/rules/debugger.rs +++ b/crates/ruff/src/rules/flake8_debugger/rules/debugger.rs @@ -23,59 +23,32 @@ impl Violation for Debugger { } } -const DEBUGGERS: &[&[&str]] = &[ - &["pdb", "set_trace"], - &["pudb", "set_trace"], - &["ipdb", "set_trace"], - &["ipdb", "sset_trace"], - &["IPython", "terminal", "embed", "InteractiveShellEmbed"], - &[ - "IPython", - "frontend", - "terminal", - "embed", - "InteractiveShellEmbed", - ], - &["celery", "contrib", "rdb", "set_trace"], - &["builtins", "breakpoint"], - &["", "breakpoint"], -]; - /// Checks for the presence of a debugger call. pub(crate) fn debugger_call(checker: &mut Checker, expr: &Expr, func: &Expr) { - if let Some(target) = checker + if let Some(using_type) = checker .semantic() .resolve_call_path(func) .and_then(|call_path| { - DEBUGGERS - .iter() - .find(|target| call_path.as_slice() == **target) + if is_debugger_call(&call_path) { + Some(DebuggerUsingType::Call(format_call_path(&call_path))) + } else { + None + } }) { - checker.diagnostics.push(Diagnostic::new( - Debugger { - using_type: DebuggerUsingType::Call(format_call_path(target)), - }, - expr.range(), - )); + checker + .diagnostics + .push(Diagnostic::new(Debugger { using_type }, expr.range())); } } /// Checks for the presence of a debugger import. pub(crate) fn debugger_import(stmt: &Stmt, module: Option<&str>, name: &str) -> Option { - // Special-case: allow `import builtins`, which is far more general than (e.g.) - // `import celery.contrib.rdb`). - if module.is_none() && name == "builtins" { - return None; - } - if let Some(module) = module { let mut call_path: CallPath = from_unqualified_name(module); call_path.push(name); - if DEBUGGERS - .iter() - .any(|target| call_path.as_slice() == *target) - { + + if is_debugger_call(&call_path) { return Some(Diagnostic::new( Debugger { using_type: DebuggerUsingType::Import(format_call_path(&call_path)), @@ -84,11 +57,9 @@ pub(crate) fn debugger_import(stmt: &Stmt, module: Option<&str>, name: &str) -> )); } } else { - let parts: CallPath = from_unqualified_name(name); - if DEBUGGERS - .iter() - .any(|call_path| &call_path[..call_path.len() - 1] == parts.as_slice()) - { + let call_path: CallPath = from_unqualified_name(name); + + if is_debugger_import(&call_path) { return Some(Diagnostic::new( Debugger { using_type: DebuggerUsingType::Import(name.to_string()), @@ -99,3 +70,35 @@ pub(crate) fn debugger_import(stmt: &Stmt, module: Option<&str>, name: &str) -> } None } + +fn is_debugger_call(call_path: &CallPath) -> bool { + matches!( + call_path.as_slice(), + ["pdb" | "pudb" | "ipdb", "set_trace"] + | ["ipdb", "sset_trace"] + | ["IPython", "terminal", "embed", "InteractiveShellEmbed"] + | [ + "IPython", + "frontend", + "terminal", + "embed", + "InteractiveShellEmbed" + ] + | ["celery", "contrib", "rdb", "set_trace"] + | ["builtins" | "", "breakpoint"] + ) +} + +fn is_debugger_import(call_path: &CallPath) -> bool { + // Constructed by taking every pattern in `is_debugger_call`, removing the last element in + // each pattern, and de-duplicating the values. + // As a special-case, we omit `builtins` to allow `import builtins`, which is far more general + // than (e.g.) `import celery.contrib.rdb`. + matches!( + call_path.as_slice(), + ["pdb" | "pudb" | "ipdb"] + | ["IPython", "terminal", "embed"] + | ["IPython", "frontend", "terminal", "embed",] + | ["celery", "contrib", "rdb"] + ) +} 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 c7299a7656..14df881ff0 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs @@ -2,6 +2,7 @@ use rustpython_parser::ast::{self, Arguments, Constant, Expr, Operator, Ranged, use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::call_path::CallPath; use ruff_python_ast::source_code::Locator; use ruff_python_semantic::{ScopeKind, SemanticModel}; @@ -94,30 +95,33 @@ impl Violation for UnassignedSpecialVariableInStub { } } -const ALLOWED_MATH_ATTRIBUTES_IN_DEFAULTS: &[&[&str]] = &[ - &["math", "inf"], - &["math", "nan"], - &["math", "e"], - &["math", "pi"], - &["math", "tau"], -]; +fn is_allowed_negated_math_attribute(call_path: &CallPath) -> bool { + matches!(call_path.as_slice(), ["math", "inf" | "e" | "pi" | "tau"]) +} -const ALLOWED_ATTRIBUTES_IN_DEFAULTS: &[&[&str]] = &[ - &["sys", "stdin"], - &["sys", "stdout"], - &["sys", "stderr"], - &["sys", "version"], - &["sys", "version_info"], - &["sys", "platform"], - &["sys", "executable"], - &["sys", "prefix"], - &["sys", "exec_prefix"], - &["sys", "base_prefix"], - &["sys", "byteorder"], - &["sys", "maxsize"], - &["sys", "hexversion"], - &["sys", "winver"], -]; +fn is_allowed_math_attribute(call_path: &CallPath) -> bool { + matches!( + call_path.as_slice(), + ["math", "inf" | "nan" | "e" | "pi" | "tau"] + | [ + "sys", + "stdin" + | "stdout" + | "stderr" + | "version" + | "version_info" + | "platform" + | "executable" + | "prefix" + | "exec_prefix" + | "base_prefix" + | "byteorder" + | "maxsize" + | "hexversion" + | "winver" + ] + ) +} fn is_valid_default_value_with_annotation( default: &Expr, @@ -166,12 +170,8 @@ fn is_valid_default_value_with_annotation( Expr::Attribute(_) => { if semantic .resolve_call_path(operand) - .map_or(false, |call_path| { - ALLOWED_MATH_ATTRIBUTES_IN_DEFAULTS.iter().any(|target| { - // reject `-math.nan` - call_path.as_slice() == *target && *target != ["math", "nan"] - }) - }) + .as_ref() + .map_or(false, is_allowed_negated_math_attribute) { return true; } @@ -219,12 +219,8 @@ fn is_valid_default_value_with_annotation( Expr::Attribute(_) => { if semantic .resolve_call_path(default) - .map_or(false, |call_path| { - ALLOWED_MATH_ATTRIBUTES_IN_DEFAULTS - .iter() - .chain(ALLOWED_ATTRIBUTES_IN_DEFAULTS.iter()) - .any(|target| call_path.as_slice() == *target) - }) + .as_ref() + .map_or(false, is_allowed_math_attribute) { return true; } diff --git a/crates/ruff/src/rules/flake8_return/rules/function.rs b/crates/ruff/src/rules/flake8_return/rules/function.rs index cae9574f5b..1fb986ca2b 100644 --- a/crates/ruff/src/rules/flake8_return/rules/function.rs +++ b/crates/ruff/src/rules/flake8_return/rules/function.rs @@ -370,34 +370,17 @@ fn implicit_return_value(checker: &mut Checker, stack: &Stack) { } } -const NORETURN_FUNCS: &[&[&str]] = &[ - // builtins - &["", "exit"], - &["", "quit"], - // stdlib - &["builtins", "exit"], - &["builtins", "quit"], - &["os", "_exit"], - &["os", "abort"], - &["posix", "_exit"], - &["posix", "abort"], - &["sys", "exit"], - &["_thread", "exit"], - &["_winapi", "ExitProcess"], - // third-party modules - &["pytest", "exit"], - &["pytest", "fail"], - &["pytest", "skip"], - &["pytest", "xfail"], -]; - /// Return `true` if the `func` is a known function that never returns. fn is_noreturn_func(func: &Expr, semantic: &SemanticModel) -> bool { semantic.resolve_call_path(func).map_or(false, |call_path| { - NORETURN_FUNCS - .iter() - .any(|target| call_path.as_slice() == *target) - || semantic.match_typing_call_path(&call_path, "assert_never") + matches!( + call_path.as_slice(), + ["" | "builtins" | "sys" | "_thread" | "pytest", "exit"] + | ["" | "builtins", "quit"] + | ["os" | "posix", "_exit" | "abort"] + | ["_winapi", "ExitProcess"] + | ["pytest", "fail" | "skip" | "xfail"] + ) || semantic.match_typing_call_path(&call_path, "assert_never") }) } diff --git a/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs b/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs index a41be00b35..f2bea235a7 100644 --- a/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs +++ b/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs @@ -8,7 +8,7 @@ use ruff_python_semantic::analyze::typing::is_immutable_func; use crate::checkers::ast::Checker; use crate::rules::ruff::rules::helpers::{ - is_allowed_dataclass_function, is_class_var_annotation, is_dataclass, + is_class_var_annotation, is_dataclass, is_dataclass_field, }; /// ## What it does @@ -97,7 +97,7 @@ pub(crate) fn function_call_in_dataclass_default( if let Expr::Call(ast::ExprCall { func, .. }) = expr.as_ref() { if !is_class_var_annotation(annotation, checker.semantic()) && !is_immutable_func(func, checker.semantic(), &extend_immutable_calls) - && !is_allowed_dataclass_function(func, checker.semantic()) + && !is_dataclass_field(func, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( FunctionCallInDataclassDefaultArgument { diff --git a/crates/ruff/src/rules/ruff/rules/helpers.rs b/crates/ruff/src/rules/ruff/rules/helpers.rs index 65763b7bdc..b5d09012e5 100644 --- a/crates/ruff/src/rules/ruff/rules/helpers.rs +++ b/crates/ruff/src/rules/ruff/rules/helpers.rs @@ -1,27 +1,12 @@ -use ruff_python_ast::helpers::map_callable; use rustpython_parser::ast::{self, Expr}; +use ruff_python_ast::helpers::map_callable; use ruff_python_semantic::SemanticModel; -pub(super) fn is_mutable_expr(expr: &Expr) -> bool { - matches!( - expr, - Expr::List(_) - | Expr::Dict(_) - | Expr::Set(_) - | Expr::ListComp(_) - | Expr::DictComp(_) - | Expr::SetComp(_) - ) -} - -const ALLOWED_DATACLASS_SPECIFIC_FUNCTIONS: &[&[&str]] = &[&["dataclasses", "field"]]; - -pub(super) fn is_allowed_dataclass_function(func: &Expr, semantic: &SemanticModel) -> bool { +/// Returns `true` if the given [`Expr`] is a `dataclasses.field` call. +pub(super) fn is_dataclass_field(func: &Expr, semantic: &SemanticModel) -> bool { semantic.resolve_call_path(func).map_or(false, |call_path| { - ALLOWED_DATACLASS_SPECIFIC_FUNCTIONS - .iter() - .any(|target| call_path.as_slice() == *target) + matches!(call_path.as_slice(), ["dataclasses", "field"]) }) } diff --git a/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs b/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs index 113c3afc96..d2c263ec21 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs @@ -2,10 +2,10 @@ use rustpython_parser::ast::{self, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::analyze::typing::is_immutable_annotation; +use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr}; use crate::checkers::ast::Checker; -use crate::rules::ruff::rules::helpers::{is_class_var_annotation, is_dataclass, is_mutable_expr}; +use crate::rules::ruff::rules::helpers::{is_class_var_annotation, is_dataclass}; /// ## What it does /// Checks for mutable default values in class attributes. @@ -52,7 +52,7 @@ pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::Stmt value: Some(value), .. }) => { - if is_mutable_expr(value) + if is_mutable_expr(value, checker.semantic()) && !is_class_var_annotation(annotation, checker.semantic()) && !is_immutable_annotation(annotation, checker.semantic()) && !is_dataclass(class_def, checker.semantic()) @@ -63,7 +63,7 @@ pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::Stmt } } Stmt::Assign(ast::StmtAssign { value, .. }) => { - if is_mutable_expr(value) { + if is_mutable_expr(value, checker.semantic()) { checker .diagnostics .push(Diagnostic::new(MutableClassDefault, value.range())); diff --git a/crates/ruff/src/rules/ruff/rules/mutable_dataclass_default.rs b/crates/ruff/src/rules/ruff/rules/mutable_dataclass_default.rs index ac30e0e214..2b47c32a46 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_dataclass_default.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_dataclass_default.rs @@ -2,10 +2,10 @@ use rustpython_parser::ast::{self, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::analyze::typing::is_immutable_annotation; +use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr}; use crate::checkers::ast::Checker; -use crate::rules::ruff::rules::helpers::{is_class_var_annotation, is_dataclass, is_mutable_expr}; +use crate::rules::ruff::rules::helpers::{is_class_var_annotation, is_dataclass}; /// ## What it does /// Checks for mutable default values in dataclass attributes. @@ -74,7 +74,7 @@ pub(crate) fn mutable_dataclass_default(checker: &mut Checker, class_def: &ast:: .. }) = statement { - if is_mutable_expr(value) + if is_mutable_expr(value, checker.semantic()) && !is_class_var_annotation(annotation, checker.semantic()) && !is_immutable_annotation(annotation, checker.semantic()) { diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 8dea42177e..4da230cd70 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -6,7 +6,10 @@ use rustpython_parser::ast::{self, Constant, Expr, Operator}; use ruff_python_ast::call_path::{from_qualified_name, from_unqualified_name, CallPath}; use ruff_python_ast::helpers::is_const_false; use ruff_python_stdlib::typing::{ - IMMUTABLE_GENERIC_TYPES, IMMUTABLE_TYPES, PEP_585_GENERICS, PEP_593_SUBSCRIPTS, SUBSCRIPTS, + as_pep_585_generic, has_pep_585_generic, is_immutable_generic_type, + is_immutable_non_generic_type, is_immutable_return_type, is_mutable_return_type, + is_pep_593_generic_member, is_pep_593_generic_type, is_standard_library_generic, + is_standard_library_generic_member, }; use crate::model::SemanticModel; @@ -34,12 +37,8 @@ pub fn match_annotated_subscript<'a>( typing_modules: impl Iterator, extend_generics: &[String], ) -> Option { - if !matches!(expr, Expr::Name(_) | Expr::Attribute(_)) { - return None; - } - semantic.resolve_call_path(expr).and_then(|call_path| { - if SUBSCRIPTS.contains(&call_path.as_slice()) + if is_standard_library_generic(call_path.as_slice()) || extend_generics .iter() .map(|target| from_qualified_name(target)) @@ -47,20 +46,19 @@ pub fn match_annotated_subscript<'a>( { return Some(SubscriptKind::AnnotatedSubscript); } - if PEP_593_SUBSCRIPTS.contains(&call_path.as_slice()) { + + if is_pep_593_generic_type(call_path.as_slice()) { return Some(SubscriptKind::PEP593AnnotatedSubscript); } for module in typing_modules { let module_call_path: CallPath = from_unqualified_name(module); if call_path.starts_with(&module_call_path) { - for subscript in SUBSCRIPTS.iter() { - if call_path.last() == subscript.last() { + if let Some(member) = call_path.last() { + if is_standard_library_generic_member(member) { return Some(SubscriptKind::AnnotatedSubscript); } - } - for subscript in PEP_593_SUBSCRIPTS.iter() { - if call_path.last() == subscript.last() { + if is_pep_593_generic_member(member) { return Some(SubscriptKind::PEP593AnnotatedSubscript); } } @@ -92,38 +90,27 @@ impl std::fmt::Display for ModuleMember { /// a variant exists. pub fn to_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> Option { semantic.resolve_call_path(expr).and_then(|call_path| { - let [module, name] = call_path.as_slice() else { + let [module, member] = call_path.as_slice() else { return None; }; - PEP_585_GENERICS - .iter() - .find_map(|((from_module, from_member), (to_module, to_member))| { - if module == from_module && name == from_member { - if to_module.is_empty() { - Some(ModuleMember::BuiltIn(to_member)) - } else { - Some(ModuleMember::Member(to_module, to_member)) - } - } else { - None - } - }) + as_pep_585_generic(module, member).map(|(module, member)| { + if module.is_empty() { + ModuleMember::BuiltIn(member) + } else { + ModuleMember::Member(module, member) + } + }) }) } /// Return whether a given expression uses a PEP 585 standard library generic. pub fn is_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> bool { - if let Some(call_path) = semantic.resolve_call_path(expr) { + semantic.resolve_call_path(expr).map_or(false, |call_path| { let [module, name] = call_path.as_slice() else { return false; }; - for (_, (to_module, to_member)) in PEP_585_GENERICS { - if module == to_module && name == to_member { - return true; - } - } - } - false + has_pep_585_generic(module, name) + }) } #[derive(Debug, Copy, Clone)] @@ -178,19 +165,14 @@ pub fn is_immutable_annotation(expr: &Expr, semantic: &SemanticModel) -> bool { match expr { Expr::Name(_) | Expr::Attribute(_) => { semantic.resolve_call_path(expr).map_or(false, |call_path| { - IMMUTABLE_TYPES - .iter() - .chain(IMMUTABLE_GENERIC_TYPES) - .any(|target| call_path.as_slice() == *target) + is_immutable_non_generic_type(call_path.as_slice()) + || is_immutable_generic_type(call_path.as_slice()) }) } Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => semantic .resolve_call_path(value) .map_or(false, |call_path| { - if IMMUTABLE_GENERIC_TYPES - .iter() - .any(|target| call_path.as_slice() == *target) - { + if is_immutable_generic_type(call_path.as_slice()) { true } else if matches!(call_path.as_slice(), ["typing", "Union"]) { if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() { @@ -226,43 +208,43 @@ pub fn is_immutable_annotation(expr: &Expr, semantic: &SemanticModel) -> bool { } } -const IMMUTABLE_FUNCS: &[&[&str]] = &[ - &["", "bool"], - &["", "complex"], - &["", "float"], - &["", "frozenset"], - &["", "int"], - &["", "str"], - &["", "tuple"], - &["datetime", "date"], - &["datetime", "datetime"], - &["datetime", "timedelta"], - &["decimal", "Decimal"], - &["fractions", "Fraction"], - &["operator", "attrgetter"], - &["operator", "itemgetter"], - &["operator", "methodcaller"], - &["pathlib", "Path"], - &["types", "MappingProxyType"], - &["re", "compile"], -]; - -/// Return `true` if `func` is a function that returns an immutable object. +/// Return `true` if `func` is a function that returns an immutable value. pub fn is_immutable_func( func: &Expr, semantic: &SemanticModel, extend_immutable_calls: &[CallPath], ) -> bool { semantic.resolve_call_path(func).map_or(false, |call_path| { - IMMUTABLE_FUNCS - .iter() - .any(|target| call_path.as_slice() == *target) + is_immutable_return_type(call_path.as_slice()) || extend_immutable_calls .iter() .any(|target| call_path == *target) }) } +/// Return `true` if `func` is a function that returns a mutable value. +pub fn is_mutable_func(func: &Expr, semantic: &SemanticModel) -> bool { + semantic + .resolve_call_path(func) + .as_ref() + .map(CallPath::as_slice) + .map_or(false, is_mutable_return_type) +} + +/// Return `true` if `expr` is an expression that resolves to a mutable value. +pub fn is_mutable_expr(expr: &Expr, semantic: &SemanticModel) -> bool { + match expr { + Expr::List(_) + | Expr::Dict(_) + | Expr::Set(_) + | Expr::ListComp(_) + | Expr::DictComp(_) + | Expr::SetComp(_) => true, + Expr::Call(ast::ExprCall { func, .. }) => is_mutable_func(func, semantic), + _ => false, + } +} + /// Return `true` if [`Expr`] is a guard for a type-checking block. pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> bool { let ast::StmtIf { test, .. } = stmt; diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index bae2be65ca..56778df02e 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -10,7 +10,7 @@ use smallvec::smallvec; use ruff_python_ast::call_path::{collect_call_path, from_unqualified_name, CallPath}; use ruff_python_ast::helpers::from_relative_import; use ruff_python_stdlib::path::is_python_stub_file; -use ruff_python_stdlib::typing::TYPING_EXTENSIONS; +use ruff_python_stdlib::typing::is_typing_extension; use crate::binding::{ Binding, BindingFlags, BindingId, BindingKind, Bindings, Exceptions, FromImportation, @@ -175,7 +175,7 @@ impl<'a> SemanticModel<'a> { return true; } - if TYPING_EXTENSIONS.contains(target) { + if is_typing_extension(target) { if call_path.as_slice() == ["typing_extensions", target] { return true; } diff --git a/crates/ruff_python_stdlib/src/typing.rs b/crates/ruff_python_stdlib/src/typing.rs index 48895413f6..796f7c3a07 100644 --- a/crates/ruff_python_stdlib/src/typing.rs +++ b/crates/ruff_python_stdlib/src/typing.rs @@ -1,279 +1,414 @@ -use once_cell::sync::Lazy; -use rustc_hash::{FxHashMap, FxHashSet}; +/// Returns `true` if a name is a member of Python's `typing_extensions` module. +/// +/// See: +pub fn is_typing_extension(member: &str) -> bool { + matches!( + member, + "Annotated" + | "Any" + | "AsyncContextManager" + | "AsyncGenerator" + | "AsyncIterable" + | "AsyncIterator" + | "Awaitable" + | "ChainMap" + | "ClassVar" + | "Concatenate" + | "ContextManager" + | "Coroutine" + | "Counter" + | "DefaultDict" + | "Deque" + | "Final" + | "Literal" + | "LiteralString" + | "NamedTuple" + | "Never" + | "NewType" + | "NotRequired" + | "OrderedDict" + | "ParamSpec" + | "ParamSpecArgs" + | "ParamSpecKwargs" + | "Protocol" + | "Required" + | "Self" + | "TYPE_CHECKING" + | "Text" + | "Type" + | "TypeAlias" + | "TypeGuard" + | "TypeVar" + | "TypeVarTuple" + | "TypedDict" + | "Unpack" + | "assert_never" + | "assert_type" + | "clear_overloads" + | "final" + | "get_type_hints" + | "get_args" + | "get_origin" + | "get_overloads" + | "is_typeddict" + | "overload" + | "override" + | "reveal_type" + | "runtime_checkable" + ) +} -// See: https://pypi.org/project/typing-extensions/ -pub static TYPING_EXTENSIONS: Lazy> = Lazy::new(|| { - FxHashSet::from_iter([ - "Annotated", - "Any", - "AsyncContextManager", - "AsyncGenerator", - "AsyncIterable", - "AsyncIterator", - "Awaitable", - "ChainMap", - "ClassVar", - "Concatenate", - "ContextManager", - "Coroutine", - "Counter", - "DefaultDict", - "Deque", - "Final", - "Literal", - "LiteralString", - "NamedTuple", - "Never", - "NewType", - "NotRequired", - "OrderedDict", - "ParamSpec", - "ParamSpecArgs", - "ParamSpecKwargs", - "Protocol", - "Required", - "Self", - "TYPE_CHECKING", - "Text", - "Type", - "TypeAlias", - "TypeGuard", - "TypeVar", - "TypeVarTuple", - "TypedDict", - "Unpack", - "assert_never", - "assert_type", - "clear_overloads", - "final", - "get_type_hints", - "get_args", - "get_origin", - "get_overloads", - "is_typeddict", - "overload", - "override", - "reveal_type", - "runtime_checkable", - ]) -}); +/// Returns `true` if a call path is a generic from the Python standard library (e.g. `list`, which +/// can be used as `list[int]`). +/// +/// See: +pub fn is_standard_library_generic(call_path: &[&str]) -> bool { + matches!( + call_path, + ["", "dict" | "frozenset" | "list" | "set" | "tuple" | "type"] + | [ + "collections" | "typing" | "typing_extensions", + "ChainMap" | "Counter" + ] + | ["collections" | "typing", "OrderedDict"] + | ["collections", "defaultdict" | "deque"] + | [ + "collections", + "abc", + "AsyncGenerator" + | "AsyncIterable" + | "AsyncIterator" + | "Awaitable" + | "ByteString" + | "Callable" + | "Collection" + | "Container" + | "Coroutine" + | "Generator" + | "ItemsView" + | "Iterable" + | "Iterator" + | "KeysView" + | "Mapping" + | "MappingView" + | "MutableMapping" + | "MutableSequence" + | "MutableSet" + | "Reversible" + | "Sequence" + | "Set" + | "ValuesView" + ] + | [ + "contextlib", + "AbstractAsyncContextManager" | "AbstractContextManager" + ] + | ["re" | "typing", "Match" | "Pattern"] + | [ + "typing", + "AbstractSet" + | "AsyncContextManager" + | "AsyncGenerator" + | "AsyncIterator" + | "Awaitable" + | "BinaryIO" + | "ByteString" + | "Callable" + | "ClassVar" + | "Collection" + | "Concatenate" + | "Container" + | "ContextManager" + | "Coroutine" + | "DefaultDict" + | "Deque" + | "Dict" + | "Final" + | "FrozenSet" + | "Generator" + | "Generic" + | "IO" + | "ItemsView" + | "Iterable" + | "Iterator" + | "KeysView" + | "List" + | "Mapping" + | "MutableMapping" + | "MutableSequence" + | "MutableSet" + | "Optional" + | "Reversible" + | "Sequence" + | "Set" + | "TextIO" + | "Tuple" + | "Type" + | "TypeGuard" + | "Union" + | "Unpack" + | "ValuesView" + ] + | ["typing", "io", "BinaryIO" | "IO" | "TextIO"] + | ["typing", "re", "Match" | "Pattern"] + | [ + "typing_extensions", + "AsyncContextManager" + | "AsyncGenerator" + | "AsyncIterable" + | "AsyncIterator" + | "Awaitable" + | "ClassVar" + | "Concatenate" + | "ContextManager" + | "Coroutine" + | "DefaultDict" + | "Deque" + | "Type" + ] + | [ + "weakref", + "WeakKeyDictionary" | "WeakSet" | "WeakValueDictionary" + ] + ) +} -// See: https://docs.python.org/3/library/typing.html -pub const SUBSCRIPTS: &[&[&str]] = &[ - // builtins - &["", "dict"], - &["", "frozenset"], - &["", "list"], - &["", "set"], - &["", "tuple"], - &["", "type"], - // `collections` - &["collections", "ChainMap"], - &["collections", "Counter"], - &["collections", "OrderedDict"], - &["collections", "defaultdict"], - &["collections", "deque"], - // `collections.abc` - &["collections", "abc", "AsyncGenerator"], - &["collections", "abc", "AsyncIterable"], - &["collections", "abc", "AsyncIterator"], - &["collections", "abc", "Awaitable"], - &["collections", "abc", "ByteString"], - &["collections", "abc", "Callable"], - &["collections", "abc", "Collection"], - &["collections", "abc", "Container"], - &["collections", "abc", "Coroutine"], - &["collections", "abc", "Generator"], - &["collections", "abc", "ItemsView"], - &["collections", "abc", "Iterable"], - &["collections", "abc", "Iterator"], - &["collections", "abc", "KeysView"], - &["collections", "abc", "Mapping"], - &["collections", "abc", "MappingView"], - &["collections", "abc", "MutableMapping"], - &["collections", "abc", "MutableSequence"], - &["collections", "abc", "MutableSet"], - &["collections", "abc", "Reversible"], - &["collections", "abc", "Sequence"], - &["collections", "abc", "Set"], - &["collections", "abc", "ValuesView"], - // `contextlib` - &["contextlib", "AbstractAsyncContextManager"], - &["contextlib", "AbstractContextManager"], - // `re` - &["re", "Match"], - &["re", "Pattern"], - // `typing` - &["typing", "AbstractSet"], - &["typing", "AsyncContextManager"], - &["typing", "AsyncGenerator"], - &["typing", "AsyncIterator"], - &["typing", "Awaitable"], - &["typing", "BinaryIO"], - &["typing", "ByteString"], - &["typing", "Callable"], - &["typing", "ChainMap"], - &["typing", "ClassVar"], - &["typing", "Collection"], - &["typing", "Concatenate"], - &["typing", "Container"], - &["typing", "ContextManager"], - &["typing", "Coroutine"], - &["typing", "Counter"], - &["typing", "DefaultDict"], - &["typing", "Deque"], - &["typing", "Dict"], - &["typing", "Final"], - &["typing", "FrozenSet"], - &["typing", "Generator"], - &["typing", "Generic"], - &["typing", "IO"], - &["typing", "ItemsView"], - &["typing", "Iterable"], - &["typing", "Iterator"], - &["typing", "KeysView"], - &["typing", "List"], - &["typing", "Mapping"], - &["typing", "Match"], - &["typing", "MutableMapping"], - &["typing", "MutableSequence"], - &["typing", "MutableSet"], - &["typing", "Optional"], - &["typing", "OrderedDict"], - &["typing", "Pattern"], - &["typing", "Reversible"], - &["typing", "Sequence"], - &["typing", "Set"], - &["typing", "TextIO"], - &["typing", "Tuple"], - &["typing", "Type"], - &["typing", "TypeGuard"], - &["typing", "Union"], - &["typing", "Unpack"], - &["typing", "ValuesView"], - // `typing.io` - &["typing", "io", "BinaryIO"], - &["typing", "io", "IO"], - &["typing", "io", "TextIO"], - // `typing.re` - &["typing", "re", "Match"], - &["typing", "re", "Pattern"], - // `typing_extensions` - &["typing_extensions", "AsyncContextManager"], - &["typing_extensions", "AsyncGenerator"], - &["typing_extensions", "AsyncIterable"], - &["typing_extensions", "AsyncIterator"], - &["typing_extensions", "Awaitable"], - &["typing_extensions", "ChainMap"], - &["typing_extensions", "ClassVar"], - &["typing_extensions", "Concatenate"], - &["typing_extensions", "ContextManager"], - &["typing_extensions", "Coroutine"], - &["typing_extensions", "Counter"], - &["typing_extensions", "DefaultDict"], - &["typing_extensions", "Deque"], - &["typing_extensions", "Type"], - // `weakref` - &["weakref", "WeakKeyDictionary"], - &["weakref", "WeakSet"], - &["weakref", "WeakValueDictionary"], -]; +/// Returns `true` if a call path is a [PEP 593] generic (e.g. `Annotated`). +/// +/// See: +/// +/// [PEP 593]: https://peps.python.org/pep-0593/ +pub fn is_pep_593_generic_type(call_path: &[&str]) -> bool { + matches!(call_path, ["typing" | "typing_extensions", "Annotated"]) +} -// See: https://docs.python.org/3/library/typing.html -pub const PEP_593_SUBSCRIPTS: &[&[&str]] = &[ - // `typing` - &["typing", "Annotated"], - // `typing_extensions` - &["typing_extensions", "Annotated"], -]; +/// Returns `true` if a name matches that of a generic from the Python standard library (e.g. +/// `list` or `Set`). +/// +/// See: +pub fn is_standard_library_generic_member(member: &str) -> bool { + // Constructed by taking every pattern from `is_standard_library_generic`, removing all but + // the last element in each pattern, and de-duplicating the values. + matches!( + member, + "dict" + | "AbstractAsyncContextManager" + | "AbstractContextManager" + | "AbstractSet" + | "AsyncContextManager" + | "AsyncGenerator" + | "AsyncIterable" + | "AsyncIterator" + | "Awaitable" + | "BinaryIO" + | "ByteString" + | "Callable" + | "ChainMap" + | "ClassVar" + | "Collection" + | "Concatenate" + | "Container" + | "ContextManager" + | "Coroutine" + | "Counter" + | "DefaultDict" + | "Deque" + | "Dict" + | "Final" + | "FrozenSet" + | "Generator" + | "Generic" + | "IO" + | "ItemsView" + | "Iterable" + | "Iterator" + | "KeysView" + | "List" + | "Mapping" + | "MappingView" + | "Match" + | "MutableMapping" + | "MutableSequence" + | "MutableSet" + | "Optional" + | "OrderedDict" + | "Pattern" + | "Reversible" + | "Sequence" + | "Set" + | "TextIO" + | "Tuple" + | "Type" + | "TypeGuard" + | "Union" + | "Unpack" + | "ValuesView" + | "WeakKeyDictionary" + | "WeakSet" + | "WeakValueDictionary" + | "defaultdict" + | "deque" + | "frozenset" + | "list" + | "set" + | "tuple" + | "type" + ) +} + +/// Returns `true` if a name matches that of a generic from [PEP 593] (e.g. `Annotated`). +/// +/// See: +/// +/// [PEP 593]: https://peps.python.org/pep-0593/ +pub fn is_pep_593_generic_member(member: &str) -> bool { + // Constructed by taking every pattern from `is_pep_593_generic`, removing all but + // the last element in each pattern, and de-duplicating the values. + matches!(member, "Annotated") +} + +/// Returns `true` if a call path represents that of an immutable, non-generic type from the Python +/// standard library (e.g. `int` or `str`). +pub fn is_immutable_non_generic_type(call_path: &[&str]) -> bool { + matches!( + call_path, + ["collections", "abc", "Sized"] + | ["typing", "LiteralString" | "Sized"] + | [ + "", + "bool" + | "bytes" + | "complex" + | "float" + | "frozenset" + | "int" + | "object" + | "range" + | "str" + ] + ) +} + +/// Returns `true` if a call path represents that of an immutable, generic type from the Python +/// standard library (e.g. `tuple`). +pub fn is_immutable_generic_type(call_path: &[&str]) -> bool { + matches!( + call_path, + ["", "tuple"] + | [ + "collections", + "abc", + "ByteString" + | "Collection" + | "Container" + | "Iterable" + | "Mapping" + | "Reversible" + | "Sequence" + | "Set" + ] + | [ + "typing", + "AbstractSet" + | "ByteString" + | "Callable" + | "Collection" + | "Container" + | "FrozenSet" + | "Iterable" + | "Literal" + | "Mapping" + | "Never" + | "NoReturn" + | "Reversible" + | "Sequence" + | "Tuple" + ] + ) +} + +/// Returns `true` if a call path represents a function from the Python standard library that +/// returns a mutable value (e.g., `dict`). +pub fn is_mutable_return_type(call_path: &[&str]) -> bool { + matches!( + call_path, + ["", "dict" | "list" | "set"] + | [ + "collections", + "Counter" | "OrderedDict" | "defaultdict" | "deque" + ] + ) +} + +/// Returns `true` if a call path represents a function from the Python standard library that +/// returns a immutable value (e.g., `bool`). +pub fn is_immutable_return_type(call_path: &[&str]) -> bool { + matches!( + call_path, + ["datetime", "date" | "datetime" | "timedelta"] + | ["decimal", "Decimal"] + | ["fractions", "Fraction"] + | ["operator", "attrgetter" | "itemgetter" | "methodcaller"] + | ["pathlib", "Path"] + | ["types", "MappingProxyType"] + | ["re", "compile"] + | [ + "", + "bool" | "complex" | "float" | "frozenset" | "int" | "str" | "tuple" + ] + ) +} type ModuleMember = (&'static str, &'static str); -type SymbolReplacement = (ModuleMember, ModuleMember); +/// Given a typing member, returns the module and member name for a generic from the Python standard +/// library (e.g., `list` for `typing.List`), if such a generic was introduced by [PEP 585]. +/// +/// [PEP 585]: https://peps.python.org/pep-0585/ +pub fn as_pep_585_generic(module: &str, member: &str) -> Option { + match (module, member) { + ("typing", "Dict") => Some(("", "dict")), + ("typing", "FrozenSet") => Some(("", "frozenset")), + ("typing", "List") => Some(("", "list")), + ("typing", "Set") => Some(("", "set")), + ("typing", "Tuple") => Some(("", "tuple")), + ("typing", "Type") => Some(("", "type")), + ("typing_extensions", "Type") => Some(("", "type")), + ("typing", "Deque") => Some(("collections", "deque")), + ("typing_extensions", "Deque") => Some(("collections", "deque")), + ("typing", "DefaultDict") => Some(("collections", "defaultdict")), + ("typing_extensions", "DefaultDict") => Some(("collections", "defaultdict")), + _ => None, + } +} -// See: https://peps.python.org/pep-0585/ -pub const PEP_585_GENERICS: &[SymbolReplacement] = &[ - (("typing", "Dict"), ("", "dict")), - (("typing", "FrozenSet"), ("", "frozenset")), - (("typing", "List"), ("", "list")), - (("typing", "Set"), ("", "set")), - (("typing", "Tuple"), ("", "tuple")), - (("typing", "Type"), ("", "type")), - (("typing_extensions", "Type"), ("", "type")), - (("typing", "Deque"), ("collections", "deque")), - (("typing_extensions", "Deque"), ("collections", "deque")), - (("typing", "DefaultDict"), ("collections", "defaultdict")), - ( - ("typing_extensions", "DefaultDict"), - ("collections", "defaultdict"), - ), -]; +/// Given a typing member, returns `true` if a generic equivalent exists in the Python standard +/// library (e.g., `list` for `typing.List`), as introduced by [PEP 585]. +/// +/// [PEP 585]: https://peps.python.org/pep-0585/ +pub fn has_pep_585_generic(module: &str, member: &str) -> bool { + // Constructed by taking every pattern from `as_pep_585_generic`, removing all but + // the last element in each pattern, and de-duplicating the values. + matches!( + (module, member), + ("", "dict" | "frozenset" | "list" | "set" | "tuple" | "type") + | ("collections", "deque" | "defaultdict") + ) +} -// See: https://github.com/JelleZijlstra/autotyping/blob/0adba5ba0eee33c1de4ad9d0c79acfd737321dd9/autotyping/autotyping.py#L69-L91 -pub static SIMPLE_MAGIC_RETURN_TYPES: Lazy> = - Lazy::new(|| { - FxHashMap::from_iter([ - ("__str__", "str"), - ("__repr__", "str"), - ("__len__", "int"), - ("__length_hint__", "int"), - ("__init__", "None"), - ("__del__", "None"), - ("__bool__", "bool"), - ("__bytes__", "bytes"), - ("__format__", "str"), - ("__contains__", "bool"), - ("__complex__", "complex"), - ("__int__", "int"), - ("__float__", "float"), - ("__index__", "int"), - ("__setattr__", "None"), - ("__delattr__", "None"), - ("__setitem__", "None"), - ("__delitem__", "None"), - ("__set__", "None"), - ("__instancecheck__", "bool"), - ("__subclasscheck__", "bool"), - ]) - }); - -pub const IMMUTABLE_TYPES: &[&[&str]] = &[ - &["", "bool"], - &["", "bytes"], - &["", "complex"], - &["", "float"], - &["", "frozenset"], - &["", "int"], - &["", "object"], - &["", "range"], - &["", "str"], - &["collections", "abc", "Sized"], - &["typing", "LiteralString"], - &["typing", "Sized"], -]; - -pub const IMMUTABLE_GENERIC_TYPES: &[&[&str]] = &[ - &["", "tuple"], - &["collections", "abc", "ByteString"], - &["collections", "abc", "Collection"], - &["collections", "abc", "Container"], - &["collections", "abc", "Iterable"], - &["collections", "abc", "Mapping"], - &["collections", "abc", "Reversible"], - &["collections", "abc", "Sequence"], - &["collections", "abc", "Set"], - &["typing", "AbstractSet"], - &["typing", "ByteString"], - &["typing", "Callable"], - &["typing", "Collection"], - &["typing", "Container"], - &["typing", "FrozenSet"], - &["typing", "Iterable"], - &["typing", "Literal"], - &["typing", "Mapping"], - &["typing", "Never"], - &["typing", "NoReturn"], - &["typing", "Reversible"], - &["typing", "Sequence"], - &["typing", "Tuple"], -]; +/// Returns the expected return type for a magic method. +/// +/// See: +pub fn simple_magic_return_type(method: &str) -> Option<&'static str> { + match method { + "__str__" | "__repr__" | "__format__" => Some("str"), + "__bytes__" => Some("bytes"), + "__len__" | "__length_hint__" | "__int__" | "__index__" => Some("int"), + "__float__" => Some("float"), + "__complex__" => Some("complex"), + "__bool__" | "__contains__" | "__instancecheck__" | "__subclasscheck__" => Some("bool"), + "__init__" | "__del__" | "__setattr__" | "__delattr__" | "__setitem__" | "__delitem__" + | "__set__" => Some("None"), + _ => None, + } +} From be107dad64510e3e34fdd322ba333da06b61be6c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 16 Jun 2023 23:24:32 -0400 Subject: [PATCH 091/447] Add a PNG variant of the Astral badge (#5155) --- assets/png/Astral.png | Bin 0 -> 3866 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/png/Astral.png diff --git a/assets/png/Astral.png b/assets/png/Astral.png new file mode 100644 index 0000000000000000000000000000000000000000..c55bead1cee00599702cd5a186b4c60816e59129 GIT binary patch literal 3866 zcmV+#59RQQP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR927N7$F1ONa40RR91FaQ7m0KM{G9{>Ojgh@m}RCodHT?c#=RT}?xQ#L)D zLP7}%M2tw0qF@1}Sg@gBLA(?BsaQbpJn$^1qK9X_Lr(=!K@SU_X9EBr0fB%T0yd(0gUz;`$ol9$OYS=G8=zFeD1IZ;B?Hwu5XS`q0s(=5 zHw3IUnv-YQvBZ#L90WZNS5D*BcBEYN_qfwPhFbD($0s;Xa z5fF;SN5BNTKtLd%j(|`s>KF+Qfq+23M+Ago@ewe=E)Wn1s3Ra0i#kSvLm(g!@DTx_ zSbPLbunPnP0_q3|#iEXp;1CE11bjpw+(&-F-USdCHX(tgKafEw-6I0Nnx;l8ZTYx@ zUcUbjSzC$1BMfxoBfV+F#6*gS41C@?R@_9dE-a#*UsbisdfNGkH20Z4?fXg_JHM%- zr*1i*bl9zZ%G~ZWd3L&6yT1)ppwE7&%hW8beXS}Ey~H; zQAbPfETHn@W*T#OGR|-Tm(qC^{}oH^DEt zIh~5~8fe|iN9n>Fy3s{9rs>wVIW27gc?^Gp^E9-W}|bXC5Q7erD^lZ~FZ{#S~O(uPS)f^V3p3ZcLd z_>`j1v1krX1bSwhbX6+SRb~&4;hWO8)UL-j9)|8&k$QC1>3xKS8z^f)G#|Hb8%BBx zk$s9CcRl;mqc35x=z(Gg^3pLhSx$A*hT?<9U80nTLv$U{s zp3MHyn)q`!RCTCg(JXKmV$?;kK&ecbV}Fq9s#|HxClyp*ZTE-eSWTLh&Vpl(2kxyW z4|`QCGak%f4^kBGqOPiyzIm^lni@PSqPQfcSlVQek15~Yv#FZ?Ideaimo&?p`S!Vg zOr?w*yUFTyu&rVD#%fK9B|a%kRhH`(^r9!3Z@lFbhhoUzQ%CccWmC6ak#y?0iFC)< zz0}&`e7&<4Wzi{P5(p3Gn&*!wi5m3nuRc^rpS@N_OScSazsEr@v=C4;0=g&``Diim zVT6bD!xu{B&4i%gdDo^owRzj=H#ElS2M0fUDvRebDm@w+m z*F=Lx$J4+w2xnPSOpKfk`lN-wa8{R3~ENSJLey)}(iEp{l6u~#K?rGgVyEGQ()5qAaI-dTuj zY-SlHvx=pNOR24B36U!jVgdnO6$>7`^ehuya#K33eX*2mvdMPBHK`Pv5KhMqH)%3| z&%Pp=-=XbaR#MR~4K#_lP~)yn(WF>DT3$+fzN>M%!-JV(S-2{PvQCbsaZ{5O-Kss` zRnsdE6gr(7$PTQW{Y+WO3Cb7N7}Af3=F6?HZJ$>%%j_7PeMJgQV+!b-_sbb(%@iGP zq#G96(}^(MKYyc4-et&cuXs>9)^X;}$!LTum_a1Cu8IXQh*6W1sLy~H8hN3a#Stpn zFLlz*_7vpu`wr2a%-r`zh;fac(xzBg3;}TsD3;+95()8(s$;&-Ub!2pc`d{+*2fta zCn+ixZ*u8%teMq*fNIz*ELSR0|I6+@L_^23YfTQP>Gx;Qzh)nxsq^htP9saXF218c zV(pOc<@X&VxSn!C3c26VSb0-G+n8uhI?WzG^VSjSSUy@-N|j~4-*a#M`g5!#zmeWv zblC0GIjp9}s6$ZvlNCp4-}lZFBHZ3_Q&Zf=4E6?H6$|1a?>}2Y^IphiiXg3Bu^_mM zW2kjNa91r@2e&1qhdsTB;N;Q5Cc*<{iZ*hEvyNFL{?H+&prBZ!^q;3blF}pS#NqA_ z5UeqO6dD4K(Q%H@eKNZ)>4&mvafv4p_&Ce|dXVD?>k4%cz!UVJCk|7|0pDFRj{_+* zB9>Ko)cHX^?L4!tisfV$l*dO4oi!Auwg2`BeKd}z^Gv6$%Dx#et@?Zpd$`V5Sw z{5|z7R$$Krt!58WT5t7w1j!95KF~mif3-hEF#qx69?u>$e-%qhligB?Gb>;A7-lKq z<4Q~ZoX>-euI+03kA2kNBg>LISbwi8yEDkFjk_v^hM$v2v!BZ1eIWj`=6TP`$m1Y> zHZEE}fK|~tKMZD;bvs_v`=n^YEt;6*86r_dx{!qb>_0&+~m=a@| zQp-3oiY~jYI~6n28YyEXdVXF3J-2ZnFK9z{Xz}d_IKE2!dbJ}fC`HI+F|fB66)UkgwL^yVOp{Za>kfNMJ+HDwI~3u2 z&lI;O5?py*_i;^$u8kqOud8B_Fu+WF^ypGx%KkHLAi}}LlfKTZ1fhemc@N;9+pdIPBPOy zEBdK+4=eZKX?d!AdNLrh2N^q?#W>efpR%%L>$$#8T;c`j(Qs3 zt!?a;4maenwa#vYWOBmi^hdE|QeG1bnm-k_bVi zSm0l-f3u7x&ge#KmO6^Oyj>7V_@RhEvrpgM?yE}YK=Hix^H9zMt3^q%Ok%5TF%yEq z*bLH(Wu%a4LP{`yukw-u@IwLJ^jL3fx)IWWzJh$pn0QLdbi^!@S=I@ua}eStuJtw( zu1TQ*th8SuUw#DAIeL5}*7VxKLLMKRFim@5Bknhpg&LtW0VM#TNLxRxVD(hxY}rN+ zUKgeJHN1HDK^{j!ty8Fyi%p>KWgu`1Wgu0-s`WnhO1s(m(^mU3b65Tbwzg#3XB9N+ zvSgliM!kn?$ve(DvrVbEp1+$FzoDex+xn!wgJK9DPxQn8s(e-~*S8h5x%St}vM$T+ zJ;+n(@?)qDM&#TA9Q&eJu--{adKAkmLRGOKAP;O1>qEh%-h|5Wc(z34 zcduA>x6M4RCFWRU_u^d#op48ykyO8dcS%Tugw6ikI>JrcA$zHhbX@vg%`#4Sw*@Gf zy{HHQPEd`drM+L?(Dp~`B1FufYKssdyB0z)R=P%4ek?+SPrvokO7hg-_Vtx4nCl)~ z_QdPjcPQ=8cqoIWu^615?>`Bp>+kBa>SD2=3>_a&1S>;Ma3wsIaU-hz`ol$XR*&2J zw$&0ou7t`=PyM>`DecSGmZ<^^vCI9VUcwN$@@h;OE8eH*aU;joh5@%ES-h)?_?afvw9QaJteYt*W|Nu0mP_| zHw9bm$nnNQKE5ulki0{&9oz^YaIK|^7%zeb@#<9#RmXS_4+!;2SQm!%VUZf@Sold^ z{vbPrmuXAyy9F!ONpTA5C3pE3!nzjPPdgiuk!3K3%AQbFr;g=SjVy#hAgSxo| ziwyNkTKbljIbboX7Jgr`LQR@KWXMS#di+(tKaz`c1Oftq;6%V?dtA6e!Fg+hR04sJ zMnEW*kiLCFc!5B0A|Mn?aNZXol|Ue*5fF+cq;H=PULX*h2nfXzocBdYB@hT{1cYJ< z>DwoS7YGC=0z$C_=Y0`U2?RnK0V8{ro~UC9>02m-7YGCw0#-6ujbtG8m4Cq{w~$64 zAP@*@1gu5)Zz%5swTwbIfq+0DWDsBuP$MPXGV_07*qoM6N<$f>_f+9RL6T literal 0 HcmV?d00001 From 4b9b6829dccabdd4faf6efa6a118b4868347a701 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Sat, 17 Jun 2023 09:31:29 +0100 Subject: [PATCH 092/447] format StmtBreak (#5158) ## Summary format `StmtBreak` trying to learn how to help out with the formatter. starting simple ## Test Plan new snapshot test --- .../test/fixtures/ruff/statement/break.py | 5 ++++ ...tests__ruff_test__statement__break_py.snap | 25 +++++++++++++++++++ .../src/statement/stmt_break.rs | 9 ++++--- 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/break.py create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__break_py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/break.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/break.py new file mode 100644 index 0000000000..c171873a02 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/break.py @@ -0,0 +1,5 @@ +# leading comment +while True: # block comment + # inside comment + break # break comment + # post comment diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__break_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__break_py.snap new file mode 100644 index 0000000000..3087672f75 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__break_py.snap @@ -0,0 +1,25 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +--- +## Input +```py +# leading comment +while True: # block comment + # inside comment + break # break comment + # post comment +``` + + + +## Output +```py +# leading comment +while True: # block comment + # inside comment + break # break comment + # post comment +``` + + diff --git a/crates/ruff_python_formatter/src/statement/stmt_break.rs b/crates/ruff_python_formatter/src/statement/stmt_break.rs index f3e9af3ca2..53cb75f6cc 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_break.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_break.rs @@ -1,12 +1,13 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::{FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::text; +use ruff_formatter::{Format, FormatResult}; use rustpython_parser::ast::StmtBreak; #[derive(Default)] pub struct FormatStmtBreak; impl FormatNodeRule for FormatStmtBreak { - fn fmt_fields(&self, item: &StmtBreak, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + fn fmt_fields(&self, _item: &StmtBreak, f: &mut PyFormatter) -> FormatResult<()> { + text("break").fmt(f) } } From e1e1d2d3415ddbfa0a505404fe86a6d978bd7b3e Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Sat, 17 Jun 2023 11:05:43 -0500 Subject: [PATCH 093/447] Add Applicability to flynt (#5160) ## Summary Fixes some of #4184. --- crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs b/crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs index 2faab59911..ea5b962073 100644 --- a/crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs +++ b/crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs @@ -122,8 +122,7 @@ pub(crate) fn static_join_to_fstring(checker: &mut Checker, expr: &Expr, joiner: expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( contents, expr.range(), ))); From 98920909c6d22387814887f271b525a0b9b40b94 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Sat, 17 Jun 2023 17:56:27 +0100 Subject: [PATCH 094/447] Complete documentation for `flake8-blind-except` and `flake8-raise` rules (#5143) ## Summary Completes the documentation for the `flake8-blind-except` and `flake8-raise` rules. Related to #2646. ## Test Plan `python scripts/check_docs_formatted.py` --- .../flake8_blind_except/rules/blind_except.rs | 29 +++++++++++++++++++ .../unnecessary_paren_on_raise_exception.rs | 22 ++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/crates/ruff/src/rules/flake8_blind_except/rules/blind_except.rs b/crates/ruff/src/rules/flake8_blind_except/rules/blind_except.rs index 843a7b1c31..428809b394 100644 --- a/crates/ruff/src/rules/flake8_blind_except/rules/blind_except.rs +++ b/crates/ruff/src/rules/flake8_blind_except/rules/blind_except.rs @@ -7,6 +7,35 @@ use ruff_python_semantic::analyze::logging; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for blind `except` clauses. +/// +/// ## Why is this bad? +/// Blind exception handling can hide bugs and make debugging difficult. It can +/// also lead to unexpected behavior, such as catching `KeyboardInterrupt` or +/// `SystemExit` exceptions that prevent the user from exiting the program. +/// +/// Instead of catching all exceptions, catch only the exceptions you expect. +/// +/// ## Example +/// ```python +/// try: +/// foo() +/// except BaseException: +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// try: +/// foo() +/// except FileNotFoundError: +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: The `try` statement](https://docs.python.org/3/reference/compound_stmts.html#the-try-statement) +/// - [Python documentation: Exception hierarchy](https://docs.python.org/3/library/exceptions.html#exception-hierarchy) #[violation] pub struct BlindExcept { name: String, diff --git a/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs b/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs index ddbcec4a35..b73cd55e01 100644 --- a/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs +++ b/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs @@ -7,6 +7,28 @@ use ruff_python_ast::helpers::match_parens; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for unnecessary parentheses on raised exceptions. +/// +/// ## Why is this bad? +/// If no arguments are passed to an exception, parentheses are not required. +/// This is because the `raise` statement accepts either an exception instance +/// or an exception class (which is then implicitly instantiated). +/// +/// Removing unnecessary parentheses makes code more readable and idiomatic. +/// +/// ## Example +/// ```python +/// raise TypeError() +/// ``` +/// +/// Use instead: +/// ```python +/// raise TypeError +/// ``` +/// +/// ## References +/// - [Python documentation: The `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement) #[violation] pub struct UnnecessaryParenOnRaiseException; From f18e10183f439e076daef36b93e564bb62f29a35 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 17 Jun 2023 13:04:50 -0400 Subject: [PATCH 095/447] Add some minor tweaks to latest docs (#5164) --- .../rules/flake8_blind_except/rules/blind_except.rs | 11 ++++++----- .../rules/unnecessary_paren_on_raise_exception.rs | 6 +++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/ruff/src/rules/flake8_blind_except/rules/blind_except.rs b/crates/ruff/src/rules/flake8_blind_except/rules/blind_except.rs index 428809b394..b5f0eae9e9 100644 --- a/crates/ruff/src/rules/flake8_blind_except/rules/blind_except.rs +++ b/crates/ruff/src/rules/flake8_blind_except/rules/blind_except.rs @@ -8,14 +8,15 @@ use ruff_python_semantic::analyze::logging; use crate::checkers::ast::Checker; /// ## What it does -/// Checks for blind `except` clauses. +/// Checks for `except` clauses that catch all exceptions. /// /// ## Why is this bad? -/// Blind exception handling can hide bugs and make debugging difficult. It can -/// also lead to unexpected behavior, such as catching `KeyboardInterrupt` or -/// `SystemExit` exceptions that prevent the user from exiting the program. +/// Overly broad `except` clauses can lead to unexpected behavior, such as +/// catching `KeyboardInterrupt` or `SystemExit` exceptions that prevent the +/// user from exiting the program. /// -/// Instead of catching all exceptions, catch only the exceptions you expect. +/// Instead of catching all exceptions, catch only those that are expected to +/// be raised in the `try` block. /// /// ## Example /// ```python diff --git a/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs b/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs index b73cd55e01..ee3b0d7e69 100644 --- a/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs +++ b/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs @@ -11,11 +11,11 @@ use crate::registry::AsRule; /// Checks for unnecessary parentheses on raised exceptions. /// /// ## Why is this bad? -/// If no arguments are passed to an exception, parentheses are not required. -/// This is because the `raise` statement accepts either an exception instance +/// If an exception is raised without any arguments, parentheses are not +/// required, as the `raise` statement accepts either an exception instance /// or an exception class (which is then implicitly instantiated). /// -/// Removing unnecessary parentheses makes code more readable and idiomatic. +/// Removing the parentheses makes the code more concise. /// /// ## Example /// ```python From 95448ba66925ce152816d53be79be38544370c36 Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Sat, 17 Jun 2023 14:08:11 -0500 Subject: [PATCH 096/447] Add Applicability to isort (#5161) ## Summary Fixes some of #4184. --- crates/ruff/src/rules/isort/rules/add_required_imports.rs | 3 +-- ..._isort__tests__combined_required_imports_docstring.py.snap | 4 ++-- ...ruff__rules__isort__tests__required_import_comment.py.snap | 2 +- ...ff__rules__isort__tests__required_import_docstring.py.snap | 2 +- ...tests__required_import_docstring_with_continuation.py.snap | 2 +- ...t__tests__required_import_docstring_with_semicolon.py.snap | 2 +- ...les__isort__tests__required_import_existing_import.py.snap | 2 +- ..._isort__tests__required_import_multiline_docstring.py.snap | 2 +- .../ruff__rules__isort__tests__required_import_off.py.snap | 2 +- ...f__rules__isort__tests__required_imports_docstring.py.snap | 4 ++-- ...__isort__tests__straight_required_import_docstring.py.snap | 2 +- ..._isort__tests__straight_required_import_docstring.pyi.snap | 2 +- 12 files changed, 14 insertions(+), 15 deletions(-) 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 1d05832f1f..8542b4ea9a 100644 --- a/crates/ruff/src/rules/isort/rules/add_required_imports.rs +++ b/crates/ruff/src/rules/isort/rules/add_required_imports.rs @@ -118,8 +118,7 @@ fn add_required_import( TextRange::default(), ); if settings.rules.should_fix(Rule::MissingRequiredImport) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified( + diagnostic.set_fix(Fix::automatic( Importer::new(python_ast, locator, stylist) .add_import(required_import, TextSize::default()), )); diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__combined_required_imports_docstring.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__combined_required_imports_docstring.py.snap index 06f61e148e..c5a0bd69f0 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__combined_required_imports_docstring.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__combined_required_imports_docstring.py.snap @@ -10,7 +10,7 @@ docstring.py:1:1: I002 [*] Missing required import: `from __future__ import anno | = help: Insert required import: `from future import annotations` -ℹ Suggested fix +ℹ Fix 1 1 | """Hello, world!""" 2 |+from __future__ import annotations 2 3 | @@ -25,7 +25,7 @@ docstring.py:1:1: I002 [*] Missing required import: `from __future__ import gene | = help: Insert required import: `from future import generator_stop` -ℹ Suggested fix +ℹ Fix 1 1 | """Hello, world!""" 2 |+from __future__ import generator_stop 2 3 | diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_comment.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_comment.py.snap index 9606d7a44e..ad5b85c1d1 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_comment.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_comment.py.snap @@ -10,7 +10,7 @@ comment.py:1:1: I002 [*] Missing required import: `from __future__ import annota | = help: Insert required import: `from future import annotations` -ℹ Suggested fix +ℹ Fix 1 1 | #!/usr/bin/env python3 2 |+from __future__ import annotations 2 3 | diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring.py.snap index 4bad7ae0ec..ae57622574 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring.py.snap @@ -10,7 +10,7 @@ docstring.py:1:1: I002 [*] Missing required import: `from __future__ import anno | = help: Insert required import: `from future import annotations` -ℹ Suggested fix +ℹ Fix 1 1 | """Hello, world!""" 2 |+from __future__ import annotations 2 3 | diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring_with_continuation.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring_with_continuation.py.snap index 2e4dfc38c9..47eff34dad 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring_with_continuation.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring_with_continuation.py.snap @@ -9,7 +9,7 @@ docstring_with_continuation.py:1:1: I002 [*] Missing required import: `from __fu | = help: Insert required import: `from future import annotations` -ℹ Suggested fix +ℹ Fix 1 |-"""Hello, world!"""; x = \ 1 |+"""Hello, world!"""; from __future__ import annotations; x = \ 2 2 | 1; y = 2 diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring_with_semicolon.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring_with_semicolon.py.snap index 0a084316d8..4e43866591 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring_with_semicolon.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring_with_semicolon.py.snap @@ -8,7 +8,7 @@ docstring_with_semicolon.py:1:1: I002 [*] Missing required import: `from __futur | = help: Insert required import: `from future import annotations` -ℹ Suggested fix +ℹ Fix 1 |-"""Hello, world!"""; x = 1 1 |+"""Hello, world!"""; from __future__ import annotations; x = 1 diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_existing_import.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_existing_import.py.snap index d094c96a6c..39faabae25 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_existing_import.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_existing_import.py.snap @@ -9,7 +9,7 @@ existing_import.py:1:1: I002 [*] Missing required import: `from __future__ impor | = help: Insert required import: `from future import annotations` -ℹ Suggested fix +ℹ Fix 1 |+from __future__ import annotations 1 2 | from __future__ import generator_stop 2 3 | import os diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_multiline_docstring.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_multiline_docstring.py.snap index f0832f6f76..c5c8bf78df 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_multiline_docstring.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_multiline_docstring.py.snap @@ -10,7 +10,7 @@ multiline_docstring.py:1:1: I002 [*] Missing required import: `from __future__ i | = help: Insert required import: `from future import annotations` -ℹ Suggested fix +ℹ Fix 1 1 | """a 2 2 | b""" 3 3 | # b diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_off.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_off.py.snap index 0d506cdcab..ada290ffd3 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_off.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_off.py.snap @@ -10,7 +10,7 @@ off.py:1:1: I002 [*] Missing required import: `from __future__ import annotation | = help: Insert required import: `from future import annotations` -ℹ Suggested fix +ℹ Fix 1 1 | # isort: off 2 |+from __future__ import annotations 2 3 | diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_imports_docstring.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_imports_docstring.py.snap index 06f61e148e..c5a0bd69f0 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_imports_docstring.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_imports_docstring.py.snap @@ -10,7 +10,7 @@ docstring.py:1:1: I002 [*] Missing required import: `from __future__ import anno | = help: Insert required import: `from future import annotations` -ℹ Suggested fix +ℹ Fix 1 1 | """Hello, world!""" 2 |+from __future__ import annotations 2 3 | @@ -25,7 +25,7 @@ docstring.py:1:1: I002 [*] Missing required import: `from __future__ import gene | = help: Insert required import: `from future import generator_stop` -ℹ Suggested fix +ℹ Fix 1 1 | """Hello, world!""" 2 |+from __future__ import generator_stop 2 3 | diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__straight_required_import_docstring.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__straight_required_import_docstring.py.snap index 7a579953f1..a03f5f7702 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__straight_required_import_docstring.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__straight_required_import_docstring.py.snap @@ -10,7 +10,7 @@ docstring.py:1:1: I002 [*] Missing required import: `import os` | = help: Insert required import: `import os` -ℹ Suggested fix +ℹ Fix 1 1 | """Hello, world!""" 2 |+import os 2 3 | diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__straight_required_import_docstring.pyi.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__straight_required_import_docstring.pyi.snap index 6225322977..41b00e0ee3 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__straight_required_import_docstring.pyi.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__straight_required_import_docstring.pyi.snap @@ -10,7 +10,7 @@ docstring.pyi:1:1: I002 [*] Missing required import: `import os` | = help: Insert required import: `import os` -ℹ Suggested fix +ℹ Fix 1 1 | """Hello, world!""" 2 |+import os 2 3 | From 653a0ebf2d3bac67c170c247676aabd8d087d11f Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Sat, 17 Jun 2023 14:33:11 -0500 Subject: [PATCH 097/447] Add Applicability to pyupgrade (#5162) ## Summary Fixes some of #4184. --- .../rules/deprecated_c_element_tree.rs | 3 +-- .../pyupgrade/rules/deprecated_import.rs | 3 +-- .../pyupgrade/rules/deprecated_mock_import.rs | 6 ++--- .../pyupgrade/rules/extraneous_parentheses.rs | 3 +-- .../rules/lru_cache_with_maxsize_none.rs | 3 +-- .../rules/lru_cache_without_parameters.rs | 3 +-- .../rules/pyupgrade/rules/native_literals.rs | 6 ++--- .../rules/printf_string_formatting.rs | 3 +-- .../pyupgrade/rules/replace_stdout_stderr.rs | 3 +-- .../pyupgrade/rules/use_pep604_annotation.rs | 9 +++---- ...ff__rules__pyupgrade__tests__UP007.py.snap | 24 +++++++++---------- ...ff__rules__pyupgrade__tests__UP011.py.snap | 8 +++---- ...ff__rules__pyupgrade__tests__UP018.py.snap | 14 +++++------ ...__rules__pyupgrade__tests__UP033_0.py.snap | 6 ++--- ...__rules__pyupgrade__tests__UP033_1.py.snap | 6 ++--- ...ff__rules__pyupgrade__tests__UP034.py.snap | 20 ++++++++-------- ...tests__future_annotations_pep_604_p37.snap | 2 +- ...sts__future_annotations_pep_604_py310.snap | 4 ++-- 18 files changed, 56 insertions(+), 70 deletions(-) diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs index 0b5a075100..fed0bda549 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs @@ -46,8 +46,7 @@ where let mut diagnostic = Diagnostic::new(DeprecatedCElementTree, node.range()); if checker.patch(diagnostic.kind.rule()) { let contents = checker.locator.slice(node.range()); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( contents.replacen("cElementTree", "ElementTree", 1), node.range(), ))); diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs index 9f7421be15..335354234c 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs @@ -563,8 +563,7 @@ pub(crate) fn deprecated_import( ); if checker.patch(Rule::DeprecatedImport) { if let Some(content) = fix { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( content, stmt.range(), ))); diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs index 68c3a20ab1..30ecd488e5 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs @@ -260,8 +260,7 @@ pub(crate) fn deprecated_mock_attribute(checker: &mut Checker, expr: &Expr) { value.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( "mock".to_string(), value.range(), ))); @@ -307,8 +306,7 @@ pub(crate) fn deprecated_mock_import(checker: &mut Checker, stmt: &Stmt) { name.range(), ); if let Some(content) = content.as_ref() { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( content.clone(), stmt.range(), ))); diff --git a/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs b/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs index 4eb02acef1..6bdcee9b63 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs @@ -157,8 +157,7 @@ pub(crate) fn extraneous_parentheses( if settings.rules.should_fix(Rule::ExtraneousParentheses) { let contents = locator.slice(TextRange::new(start_range.start(), end_range.end())); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::replacement( + diagnostic.set_fix(Fix::automatic(Edit::replacement( contents[1..contents.len() - 1].to_string(), start_range.start(), end_range.end(), 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 d6717f3cce..52a38c173d 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 @@ -97,8 +97,7 @@ pub(crate) fn lru_cache_with_maxsize_none(checker: &mut Checker, decorator_list: )?; let reference_edit = Edit::range_replacement(binding, decorator.expression.range()); - #[allow(deprecated)] - Ok(Fix::unspecified_edits(import_edit, [reference_edit])) + Ok(Fix::automatic_edits(import_edit, [reference_edit])) }); } checker.diagnostics.push(diagnostic); 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 5e1a3c602f..2b0058fdcf 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 @@ -81,8 +81,7 @@ pub(crate) fn lru_cache_without_parameters(checker: &mut Checker, decorator_list TextRange::new(func.end(), decorator.end()), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( checker.generator().expr(func), decorator.expression.range(), ))); diff --git a/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs b/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs index e62888b9b9..365cba7d26 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs @@ -100,8 +100,7 @@ pub(crate) fn native_literals( Constant::Str(String::new()) }; let content = checker.generator().constant(&constant); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( content, expr.range(), ))); @@ -153,8 +152,7 @@ pub(crate) fn native_literals( expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( arg_code.to_string(), expr.range(), ))); diff --git a/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs b/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs index 56008adfc4..b0ea539d23 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs @@ -468,8 +468,7 @@ pub(crate) fn printf_string_formatting( let mut diagnostic = Diagnostic::new(PrintfStringFormatting, expr.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( contents, expr.range(), ))); 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 4dab18b024..01592e3c29 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs @@ -65,8 +65,7 @@ fn generate_fix( } else { (stderr, stdout) }; - #[allow(deprecated)] - Ok(Fix::unspecified_edits( + Ok(Fix::suggested_edits( Edit::range_replacement("capture_output=True".to_string(), first.range()), [remove_argument( locator, diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs index ae9350e963..4be7de1fe3 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs @@ -88,8 +88,7 @@ pub(crate) fn use_pep604_annotation( Pep604Operator::Optional => { let mut diagnostic = Diagnostic::new(NonPEP604Annotation, expr.range()); if fixable && checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::manual(Edit::range_replacement( checker.generator().expr(&optional(slice)), expr.range(), ))); @@ -104,16 +103,14 @@ pub(crate) fn use_pep604_annotation( // Invalid type annotation. } Expr::Tuple(ast::ExprTuple { elts, .. }) => { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::manual(Edit::range_replacement( checker.generator().expr(&union(elts)), expr.range(), ))); } _ => { // Single argument. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::manual(Edit::range_replacement( checker.generator().expr(slice), expr.range(), ))); diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP007.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP007.py.snap index 4f87a31ecf..cd61499b3c 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP007.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP007.py.snap @@ -9,7 +9,7 @@ UP007.py:6:10: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Suggested fix +ℹ Possible fix 3 3 | from typing import Union 4 4 | 5 5 | @@ -27,7 +27,7 @@ UP007.py:10:10: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Suggested fix +ℹ Possible fix 7 7 | ... 8 8 | 9 9 | @@ -45,7 +45,7 @@ UP007.py:14:10: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Suggested fix +ℹ Possible fix 11 11 | ... 12 12 | 13 13 | @@ -63,7 +63,7 @@ UP007.py:14:26: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Suggested fix +ℹ Possible fix 11 11 | ... 12 12 | 13 13 | @@ -81,7 +81,7 @@ UP007.py:18:10: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Suggested fix +ℹ Possible fix 15 15 | ... 16 16 | 17 17 | @@ -99,7 +99,7 @@ UP007.py:22:10: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Suggested fix +ℹ Possible fix 19 19 | ... 20 20 | 21 21 | @@ -117,7 +117,7 @@ UP007.py:26:10: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Suggested fix +ℹ Possible fix 23 23 | ... 24 24 | 25 25 | @@ -135,7 +135,7 @@ UP007.py:30:11: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Suggested fix +ℹ Possible fix 27 27 | ... 28 28 | 29 29 | @@ -153,7 +153,7 @@ UP007.py:30:27: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Suggested fix +ℹ Possible fix 27 27 | ... 28 28 | 29 29 | @@ -171,7 +171,7 @@ UP007.py:34:11: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Suggested fix +ℹ Possible fix 31 31 | ... 32 32 | 33 33 | @@ -190,7 +190,7 @@ UP007.py:47:8: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Suggested fix +ℹ Possible fix 44 44 | 45 45 | 46 46 | def f() -> None: @@ -232,7 +232,7 @@ UP007.py:52:8: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Suggested fix +ℹ Possible fix 49 49 | 50 50 | x = Union[str, int] 51 51 | x = Union["str", "int"] diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP011.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP011.py.snap index 58b5775f91..a52adcc916 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP011.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP011.py.snap @@ -10,7 +10,7 @@ UP011.py:5:21: UP011 [*] Unnecessary parentheses to `functools.lru_cache` | = help: Remove unnecessary parentheses -ℹ Suggested fix +ℹ Fix 2 2 | from functools import lru_cache 3 3 | 4 4 | @@ -29,7 +29,7 @@ UP011.py:10:11: UP011 [*] Unnecessary parentheses to `functools.lru_cache` | = help: Remove unnecessary parentheses -ℹ Suggested fix +ℹ Fix 7 7 | pass 8 8 | 9 9 | @@ -49,7 +49,7 @@ UP011.py:16:21: UP011 [*] Unnecessary parentheses to `functools.lru_cache` | = help: Remove unnecessary parentheses -ℹ Suggested fix +ℹ Fix 13 13 | 14 14 | 15 15 | @other_decorator @@ -68,7 +68,7 @@ UP011.py:21:21: UP011 [*] Unnecessary parentheses to `functools.lru_cache` | = help: Remove unnecessary parentheses -ℹ Suggested fix +ℹ Fix 18 18 | pass 19 19 | 20 20 | diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP018.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP018.py.snap index 537f50448f..d51fd2112e 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP018.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP018.py.snap @@ -11,7 +11,7 @@ UP018.py:21:1: UP018 [*] Unnecessary call to `str` | = help: Replace with empty string -ℹ Suggested fix +ℹ Fix 18 18 | f"{f'{str()}'}" 19 19 | 20 20 | # These become string or byte literals @@ -32,7 +32,7 @@ UP018.py:22:1: UP018 [*] Unnecessary call to `str` | = help: Replace with empty string -ℹ Suggested fix +ℹ Fix 19 19 | 20 20 | # These become string or byte literals 21 21 | str() @@ -54,7 +54,7 @@ UP018.py:23:1: UP018 [*] Unnecessary call to `str` | = help: Replace with empty string -ℹ Suggested fix +ℹ Fix 20 20 | # These become string or byte literals 21 21 | str() 22 22 | str("foo") @@ -77,7 +77,7 @@ UP018.py:25:1: UP018 [*] Unnecessary call to `bytes` | = help: Replace with empty bytes -ℹ Suggested fix +ℹ Fix 22 22 | str("foo") 23 23 | str(""" 24 24 | foo""") @@ -98,7 +98,7 @@ UP018.py:26:1: UP018 [*] Unnecessary call to `bytes` | = help: Replace with empty bytes -ℹ Suggested fix +ℹ Fix 23 23 | str(""" 24 24 | foo""") 25 25 | bytes() @@ -119,7 +119,7 @@ UP018.py:27:1: UP018 [*] Unnecessary call to `bytes` | = help: Replace with empty bytes -ℹ Suggested fix +ℹ Fix 24 24 | foo""") 25 25 | bytes() 26 26 | bytes(b"foo") @@ -138,7 +138,7 @@ UP018.py:29:4: UP018 [*] Unnecessary call to `str` | = help: Replace with empty string -ℹ Suggested fix +ℹ Fix 26 26 | bytes(b"foo") 27 27 | bytes(b""" 28 28 | foo""") diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP033_0.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP033_0.py.snap index 0a752a2cdf..dc40338e7e 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP033_0.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP033_0.py.snap @@ -10,7 +10,7 @@ UP033_0.py:4:21: UP033 [*] Use `@functools.cache` instead of `@functools.lru_cac | = help: Rewrite with `@functools.cache -ℹ Suggested fix +ℹ Fix 1 1 | import functools 2 2 | 3 3 | @@ -30,7 +30,7 @@ UP033_0.py:10:21: UP033 [*] Use `@functools.cache` instead of `@functools.lru_ca | = help: Rewrite with `@functools.cache -ℹ Suggested fix +ℹ Fix 7 7 | 8 8 | 9 9 | @other_decorator @@ -49,7 +49,7 @@ UP033_0.py:15:21: UP033 [*] Use `@functools.cache` instead of `@functools.lru_ca | = help: Rewrite with `@functools.cache -ℹ Suggested fix +ℹ Fix 12 12 | pass 13 13 | 14 14 | diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP033_1.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP033_1.py.snap index 91f6560faf..5fe701079b 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP033_1.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP033_1.py.snap @@ -10,7 +10,7 @@ UP033_1.py:4:11: UP033 [*] Use `@functools.cache` instead of `@functools.lru_cac | = help: Rewrite with `@functools.cache -ℹ Suggested fix +ℹ Fix 1 |-from functools import lru_cache 1 |+from functools import lru_cache, cache 2 2 | @@ -31,7 +31,7 @@ UP033_1.py:10:11: UP033 [*] Use `@functools.cache` instead of `@functools.lru_ca | = help: Rewrite with `@functools.cache -ℹ Suggested fix +ℹ Fix 1 |-from functools import lru_cache 1 |+from functools import lru_cache, cache 2 2 | @@ -56,7 +56,7 @@ UP033_1.py:15:11: UP033 [*] Use `@functools.cache` instead of `@functools.lru_ca | = help: Rewrite with `@functools.cache -ℹ Suggested fix +ℹ Fix 1 |-from functools import lru_cache 1 |+from functools import lru_cache, cache 2 2 | diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP034.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP034.py.snap index 17e5427eb5..4373bdabd9 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP034.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP034.py.snap @@ -11,7 +11,7 @@ UP034.py:2:7: UP034 [*] Avoid extraneous parentheses | = help: Remove extraneous parentheses -ℹ Suggested fix +ℹ Fix 1 1 | # UP034 2 |-print(("foo")) 2 |+print("foo") @@ -29,7 +29,7 @@ UP034.py:5:7: UP034 [*] Avoid extraneous parentheses | = help: Remove extraneous parentheses -ℹ Suggested fix +ℹ Fix 2 2 | print(("foo")) 3 3 | 4 4 | # UP034 @@ -49,7 +49,7 @@ UP034.py:8:7: UP034 [*] Avoid extraneous parentheses | = help: Remove extraneous parentheses -ℹ Suggested fix +ℹ Fix 5 5 | print(("hell((goodybe))o")) 6 6 | 7 7 | # UP034 @@ -69,7 +69,7 @@ UP034.py:11:7: UP034 [*] Avoid extraneous parentheses | = help: Remove extraneous parentheses -ℹ Suggested fix +ℹ Fix 8 8 | print((("foo"))) 9 9 | 10 10 | # UP034 @@ -89,7 +89,7 @@ UP034.py:14:7: UP034 [*] Avoid extraneous parentheses | = help: Remove extraneous parentheses -ℹ Suggested fix +ℹ Fix 11 11 | print((((1)))) 12 12 | 13 13 | # UP034 @@ -109,7 +109,7 @@ UP034.py:18:5: UP034 [*] Avoid extraneous parentheses | = help: Remove extraneous parentheses -ℹ Suggested fix +ℹ Fix 15 15 | 16 16 | # UP034 17 17 | print( @@ -132,7 +132,7 @@ UP034.py:23:5: UP034 [*] Avoid extraneous parentheses | = help: Remove extraneous parentheses -ℹ Suggested fix +ℹ Fix 20 20 | 21 21 | # UP034 22 22 | print( @@ -156,7 +156,7 @@ UP034.py:30:13: UP034 [*] Avoid extraneous parentheses | = help: Remove extraneous parentheses -ℹ Suggested fix +ℹ Fix 27 27 | 28 28 | # UP034 29 29 | def f(): @@ -176,7 +176,7 @@ UP034.py:35:9: UP034 [*] Avoid extraneous parentheses | = help: Remove extraneous parentheses -ℹ Suggested fix +ℹ Fix 32 32 | # UP034 33 33 | if True: 34 34 | print( @@ -196,7 +196,7 @@ UP034.py:39:7: UP034 [*] Avoid extraneous parentheses | = help: Remove extraneous parentheses -ℹ Suggested fix +ℹ Fix 36 36 | ) 37 37 | 38 38 | # UP034 diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_pep_604_p37.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_pep_604_p37.snap index a5a07f6b84..b20911af4e 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_pep_604_p37.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_pep_604_p37.snap @@ -10,7 +10,7 @@ future_annotations.py:40:4: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Suggested fix +ℹ Possible fix 37 37 | return y 38 38 | 39 39 | diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_pep_604_py310.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_pep_604_py310.snap index db003ea51f..b025a5bc9b 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_pep_604_py310.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_pep_604_py310.snap @@ -10,7 +10,7 @@ future_annotations.py:40:4: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Suggested fix +ℹ Possible fix 37 37 | return y 38 38 | 39 39 | @@ -28,7 +28,7 @@ future_annotations.py:42:21: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Suggested fix +ℹ Possible fix 39 39 | 40 40 | x: Optional[int] = None 41 41 | From 763d38cafbc7d198f5fdd5b6d9046de2c28afbed Mon Sep 17 00:00:00 2001 From: konstin Date: Sun, 18 Jun 2023 12:39:06 +0200 Subject: [PATCH 098/447] Refactor top llvm-lines entry (#5147) ## Summary This refactors the top entry in terms of llvm lines, `RuleCodePrefix::iter()`. It's only used for generating the schema and the clap completion so no effect on performance. I've confirmed with ``` CARGO_TARGET_DIR=target-llvm-lines RUSTFLAGS="-Csymbol-mangling-version=v0" cargo llvm-lines -p ruff --lib | head -n 20 ``` that this indeed remove the method from the list of heaviest symbols in terms of llvm-lines Before: ``` Lines Copies Function name ----- ------ ------------- 1768469 40538 (TOTAL) 10391 (0.6%, 0.6%) 1 (0.0%, 0.0%) ::iter 8250 (0.5%, 1.1%) 1 (0.0%, 0.0%) ::noqa_code 7427 (0.4%, 1.5%) 1 (0.0%, 0.0%) ::visit_stmt 6536 (0.4%, 1.8%) 1 (0.0%, 0.0%) <::deserialize::__Visitor as serde[1a28808d63625aed]::de::Visitor>::visit_map::> 6536 (0.4%, 2.2%) 1 (0.0%, 0.0%) <::deserialize::__Visitor as serde[1a28808d63625aed]::de::Visitor>::visit_map:: 6533 (0.4%, 2.6%) 1 (0.0%, 0.0%) <::deserialize::__Visitor as serde[1a28808d63625aed]::de::Visitor>::visit_map:: 5727 (0.3%, 2.9%) 1 (0.0%, 0.0%) ::visit_expr 4453 (0.3%, 3.2%) 1 (0.0%, 0.0%) ruff[fa0f2e8ef07114da]::flake8_to_ruff::converter::convert 3790 (0.2%, 3.4%) 1 (0.0%, 0.0%) <&ruff[fa0f2e8ef07114da]::registry::Linter as core[da82827a87f140f9]::iter::traits::collect::IntoIterator>::into_iter 3416 (0.2%, 3.6%) 1 (0.0%, 0.0%) ::code_for_rule 3187 (0.2%, 3.7%) 1 (0.0%, 0.0%) ::fmt 3185 (0.2%, 3.9%) 1 (0.0%, 0.0%) <&str as core[da82827a87f140f9]::convert::From<&ruff[fa0f2e8ef07114da]::codes::Rule>>::from 3185 (0.2%, 4.1%) 1 (0.0%, 0.0%) <&str as core[da82827a87f140f9]::convert::From>::from 3185 (0.2%, 4.3%) 1 (0.0%, 0.0%) >::as_ref 3183 (0.2%, 4.5%) 1 (0.0%, 0.0%) ::get 2718 (0.2%, 4.6%) 1 (0.0%, 0.0%) <::deserialize::__Visitor as serde[1a28808d63625aed]::de::Visitor>::visit_seq:: 2706 (0.2%, 4.8%) 1 (0.0%, 0.0%) <&ruff[fa0f2e8ef07114da]::codes::Pylint as core[da82827a87f140f9]::iter::traits::collect::IntoIterator>::into_iter ``` After: ``` Lines Copies Function name ----- ------ ------------- 1763380 40806 (TOTAL) 8250 (0.5%, 0.5%) 1 (0.0%, 0.0%) ::noqa_code 7427 (0.4%, 0.9%) 1 (0.0%, 0.0%) ::visit_stmt 6536 (0.4%, 1.3%) 1 (0.0%, 0.0%) <::deserialize::__Visitor as serde[1a28808d63625aed]::de::Visitor>::visit_map::> 6536 (0.4%, 1.6%) 1 (0.0%, 0.0%) <::deserialize::__Visitor as serde[1a28808d63625aed]::de::Visitor>::visit_map:: 6533 (0.4%, 2.0%) 1 (0.0%, 0.0%) <::deserialize::__Visitor as serde[1a28808d63625aed]::de::Visitor>::visit_map:: 5727 (0.3%, 2.3%) 1 (0.0%, 0.0%) ::visit_expr 4453 (0.3%, 2.6%) 1 (0.0%, 0.0%) ruff[fa0f2e8ef07114da]::flake8_to_ruff::converter::convert 3790 (0.2%, 2.8%) 1 (0.0%, 0.0%) <&ruff[fa0f2e8ef07114da]::registry::Linter as core[da82827a87f140f9]::iter::traits::collect::IntoIterator>::into_iter 3416 (0.2%, 3.0%) 1 (0.0%, 0.0%) ::code_for_rule 3187 (0.2%, 3.2%) 1 (0.0%, 0.0%) ::fmt 3185 (0.2%, 3.3%) 1 (0.0%, 0.0%) <&str as core[da82827a87f140f9]::convert::From<&ruff[fa0f2e8ef07114da]::codes::Rule>>::from 3185 (0.2%, 3.5%) 1 (0.0%, 0.0%) <&str as core[da82827a87f140f9]::convert::From>::from 3185 (0.2%, 3.7%) 1 (0.0%, 0.0%) >::as_ref 3183 (0.2%, 3.9%) 1 (0.0%, 0.0%) ::get 2718 (0.2%, 4.0%) 1 (0.0%, 0.0%) <::deserialize::__Visitor as serde[1a28808d63625aed]::de::Visitor>::visit_seq:: 2706 (0.2%, 4.2%) 1 (0.0%, 0.0%) <&ruff[fa0f2e8ef07114da]::codes::Pylint as core[da82827a87f140f9]::iter::traits::collect::IntoIterator>::into_iter 2573 (0.1%, 4.3%) 1 (0.0%, 0.0%) <::deserialize::__Visitor as serde[1a28808d63625aed]::de::Visitor>::visit_map::> ``` I didn't measure the effect on binary size this time. ## Testing `cargo test` which uses this to generate the schema didn't change --- crates/ruff_macros/src/map_codes.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/ruff_macros/src/map_codes.rs b/crates/ruff_macros/src/map_codes.rs index 327b9a9d3d..950c66f7c8 100644 --- a/crates/ruff_macros/src/map_codes.rs +++ b/crates/ruff_macros/src/map_codes.rs @@ -178,8 +178,7 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result { let rule_to_code = generate_rule_to_code(&linter_to_rules); output.extend(rule_to_code); - let iter = generate_iter_impl(&linter_to_rules, &all_codes); - output.extend(iter); + output.extend(generate_iter_impl(&linter_to_rules, &linter_idents)); Ok(output) } @@ -326,7 +325,7 @@ See also https://github.com/astral-sh/ruff/issues/2186. /// Implement `impl IntoIterator for &Linter` and `RuleCodePrefix::iter()` fn generate_iter_impl( linter_to_rules: &BTreeMap>, - all_codes: &[TokenStream], + linter_idents: &[&Ident], ) -> TokenStream { let mut linter_into_iter_match_arms = quote!(); for (linter, map) in linter_to_rules { @@ -352,8 +351,11 @@ fn generate_iter_impl( } impl RuleCodePrefix { - pub fn iter() -> ::std::vec::IntoIter { - vec![ #(#all_codes,)* ].into_iter() + pub fn iter() -> impl Iterator { + use strum::IntoEnumIterator; + + std::iter::empty() + #(.chain(#linter_idents::iter().map(|x| Self::#linter_idents(x))))* } } } From 5c416e4d9b67635be7b18b6e03e5151d6c446ef8 Mon Sep 17 00:00:00 2001 From: konstin Date: Sun, 18 Jun 2023 13:00:42 +0200 Subject: [PATCH 099/447] Pre commit without cargo and other pre-PR improvements (#5146) This tackles three problems: * pre-commit was slow because it ran cargo commands * Improve the clarity on what you need to run to get your PR pass on CI (and make those fast) * You had to compile and run `cargo dev generate-all` separately, which was slow The first change is to remove all cargo commands except running ruff itself from pre-commit. With `cargo run --bin ruff` already compiled it takes about 7s on my machine. It would make sense to also use the ruff pre-commit action here even if we're then lagging a release behind for checking ruff on ruff. The contributing guide is now clear about what you need to run: ```shell cargo clippy --workspace --all-targets --all-features -- -D warnings # Linting... RUFF_UPDATE_SCHEMA=1 cargo test # Testing and updating ruff.schema.json pre-commit run --all-files # rust and python formatting, markdown and python linting, etc. ``` Example timings from my machine: `cargo clippy --workspace --all-targets --all-features -- -D warnings`: 23s `RUFF_UPDATE_SCHEMA=1 cargo test`: 2min (recompiling), 1min (no code changes, this is mainly doc tests) `pre-commit run --all-files`: 7s The exact numbers don't matter so much as the approximate experience (6s is easier to just wait than 1min, esp if you need to fix and rerun). The biggest remaining block seems to be doc tests, i'm surprised i didn't find any solution to speeding them up (nextest simply doesn't run them at all). Also note that the formatter has it's own tests which are much faster since they avoid linking ruff (`cargo test ruff_python_formatter`). The third change is to enable `cargo test` to update the schema. Similar to `INSTA_UPDATE=always`, i've added `RUFF_UPDATE_SCHEMA=1` (name open to bikeshedding), so `RUFF_UPDATE_SCHEMA=1 cargo test` updates the schema, while `cargo test` still fails as expected if the repo isn't up-to-date. --------- Co-authored-by: Dhruv Manilawala --- .pre-commit-config.yaml | 20 +++++--------------- CONTRIBUTING.md | 19 +++++++++---------- crates/ruff_dev/src/generate_json_schema.rs | 19 +++++++++++++++---- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d89b927114..16ffe82286 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,29 +38,19 @@ repos: name: cargo fmt entry: cargo fmt -- language: system - types: [rust] - - id: clippy - name: clippy - entry: cargo clippy --workspace --all-targets --all-features -- -D warnings - language: system - pass_filenames: false + types: [ rust ] + pass_filenames: false # This makes it a lot faster - id: ruff name: ruff - entry: cargo run -p ruff_cli -- check --no-cache --force-exclude --fix --exit-non-zero-on-fix + entry: cargo run --bin ruff -- check --no-cache --force-exclude --fix --exit-non-zero-on-fix language: system - types_or: [python, pyi] + types_or: [ python, pyi ] require_serial: true exclude: | (?x)^( crates/ruff/resources/.*| crates/ruff_python_formatter/resources/.* )$ - - id: dev-generate-all - name: dev-generate-all - entry: cargo dev generate-all - language: system - pass_filenames: false - exclude: target # Black - repo: https://github.com/psf/black @@ -69,4 +59,4 @@ repos: - id: black ci: - skip: [cargo-fmt, clippy, dev-generate-all] + skip: [ cargo-fmt, dev-generate-all ] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8ad067e335..3feb49bd1f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,6 +45,12 @@ You'll also need [Insta](https://insta.rs/docs/) to update snapshot tests: cargo install cargo-insta ``` +and pre-commit to run some validation checks: + +```shell +pipx install pre-commit # or `pip install pre-commit` if you have a virtualenv +``` + ### Development After cloning the repository, run Ruff locally with: @@ -57,9 +63,9 @@ Prior to opening a pull request, ensure that your code has been auto-formatted, and that it passes both the lint and test validation checks: ```shell -cargo fmt # Auto-formatting... -cargo test # Testing... -cargo clippy --workspace --all-targets --all-features -- -D warnings # Linting... +cargo clippy --workspace --all-targets --all-features -- -D warnings # Rust linting +RUFF_UPDATE_SCHEMA=1 cargo test # Rust testing and updating ruff.schema.json +pre-commit run --all-files --show-diff-on-failure # Rust and Python formatting, Markdown and Python linting, etc. ``` These checks will run on GitHub Actions when you open your Pull Request, but running them locally @@ -72,13 +78,6 @@ after running `cargo test` like so: cargo insta review ``` -If you have `pre-commit` [installed](https://pre-commit.com/#installation) then you can use it to -assist with formatting and linting. The following command will run the `pre-commit` hooks: - -```shell -pre-commit run --all-files -``` - Your Pull Request will be reviewed by a maintainer, which may involve a few rounds of iteration prior to merging. diff --git a/crates/ruff_dev/src/generate_json_schema.rs b/crates/ruff_dev/src/generate_json_schema.rs index 147638dba6..046b917294 100644 --- a/crates/ruff_dev/src/generate_json_schema.rs +++ b/crates/ruff_dev/src/generate_json_schema.rs @@ -32,15 +32,20 @@ pub(crate) fn main(args: &Args) -> Result<()> { Mode::Check => { let current = fs::read_to_string(schema_path)?; if current == schema_string { - println!("up-to-date: {filename}"); + println!("Up-to-date: {filename}"); } else { let comparison = StrComparison::new(¤t, &schema_string); bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}"); } } Mode::Write => { - let file = schema_path; - fs::write(file, schema_string.as_bytes())?; + let current = fs::read_to_string(&schema_path)?; + if current == schema_string { + println!("Up-to-date: {filename}"); + } else { + println!("Updating: {filename}"); + fs::write(schema_path, schema_string.as_bytes())?; + } } } @@ -50,6 +55,7 @@ pub(crate) fn main(args: &Args) -> Result<()> { #[cfg(test)] mod test { use anyhow::Result; + use std::env; use crate::generate_all::Mode; @@ -57,6 +63,11 @@ mod test { #[test] fn test_generate_json_schema() -> Result<()> { - main(&Args { mode: Mode::Check }) + let mode = if env::var("RUFF_UPDATE_SCHEMA").as_deref() == Ok("1") { + Mode::Write + } else { + Mode::Check + }; + main(&Args { mode }) } } From 195b36c429d12bad19fe5931bb24aa31a3d6c439 Mon Sep 17 00:00:00 2001 From: Chris Pryer <14341145+cnpryer@users.noreply.github.com> Date: Sun, 18 Jun 2023 07:25:59 -0400 Subject: [PATCH 100/447] Format `continue` statement (#5165) ## Summary Format `continue` statement. ## Test Plan `continue` is used already in some tests, but if a new test is needed I could add it. --------- Co-authored-by: konstin --- ...hon_formatter__tests__black_test__comments2_py.snap | 10 ++++------ .../src/statement/stmt_continue.rs | 9 +++++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap index 90b9263196..7869c241a8 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap @@ -303,7 +303,7 @@ instruction()#comment with bad spacing pass # no newline before or after short = [ -@@ -91,75 +88,29 @@ +@@ -91,48 +88,14 @@ ] # no newline after @@ -356,10 +356,8 @@ instruction()#comment with bad spacing + lcomp3 = [i for i in []] while True: if False: -- continue -+ NOT_YET_IMPLEMENTED_StmtContinue - - # and round and round we go + continue +@@ -141,25 +104,13 @@ # and round and round we go # let's return @@ -502,7 +500,7 @@ def inline_comments_in_brackets_ruin_everything(): lcomp3 = [i for i in []] while True: if False: - NOT_YET_IMPLEMENTED_StmtContinue + continue # and round and round we go # and round and round we go diff --git a/crates/ruff_python_formatter/src/statement/stmt_continue.rs b/crates/ruff_python_formatter/src/statement/stmt_continue.rs index b216ba7c46..d6403fd1bf 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_continue.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_continue.rs @@ -1,12 +1,13 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::{FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::text; +use ruff_formatter::{Format, FormatResult}; use rustpython_parser::ast::StmtContinue; #[derive(Default)] pub struct FormatStmtContinue; impl FormatNodeRule for FormatStmtContinue { - fn fmt_fields(&self, item: &StmtContinue, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + fn fmt_fields(&self, _item: &StmtContinue, f: &mut PyFormatter) -> FormatResult<()> { + text("continue").fmt(f) } } From a0b750f74bf9c2fdd4f6db293fa18554e47aea89 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 18 Jun 2023 11:23:40 -0400 Subject: [PATCH 101/447] Move unconventional import rule to post-binding phase (#5151) ## Summary This PR moves the "unconventional import alias" rule (which enforces, e.g., that `pandas` is imported as `pd`) to the "dead scopes" phase, after the main linter pass. This (1) avoids an allocation since we no longer need to create the qualified name in the linter pass; and (2) will allow us to autofix it, since we'll have access to all references. ## Test Plan `cargo test` -- all changes are to ranges (which are improvements IMO). --- crates/ruff/src/checkers/ast/mod.rs | 39 ++---- .../flake8_import_conventions/rules/mod.rs | 4 +- ...lias.rs => unconventional_import_alias.rs} | 42 ++++--- ...ke8_import_conventions__tests__custom.snap | 112 +++++++++--------- ...8_import_conventions__tests__defaults.snap | 40 +++---- ...port_conventions__tests__from_imports.snap | 32 ++--- ..._conventions__tests__override_default.snap | 40 +++---- ...rt_conventions__tests__remove_default.snap | 32 ++--- 8 files changed, 167 insertions(+), 174 deletions(-) rename crates/ruff/src/rules/flake8_import_conventions/rules/{conventional_import_alias.rs => unconventional_import_alias.rs} (58%) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index a784e6d743..f6f3f1ab45 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -950,18 +950,6 @@ where } } } - if self.enabled(Rule::UnconventionalImportAlias) { - if let Some(diagnostic) = - flake8_import_conventions::rules::conventional_import_alias( - stmt, - &alias.name, - alias.asname.as_deref(), - &self.settings.flake8_import_conventions.aliases, - ) - { - self.diagnostics.push(diagnostic); - } - } if self.enabled(Rule::BannedImportAlias) { if let Some(asname) = &alias.asname { if let Some(diagnostic) = @@ -1162,20 +1150,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::UnconventionalImportAlias) { - let qualified_name = - helpers::format_import_from_member(level, module, &alias.name); - if let Some(diagnostic) = - flake8_import_conventions::rules::conventional_import_alias( - stmt, - &qualified_name, - alias.asname.as_deref(), - &self.settings.flake8_import_conventions.aliases, - ) - { - self.diagnostics.push(diagnostic); - } - } if self.enabled(Rule::BannedImportAlias) { if let Some(asname) = &alias.asname { let qualified_name = @@ -1192,7 +1166,6 @@ where } } } - if let Some(asname) = &alias.asname { if self.enabled(Rule::ConstantImportedAsNonConstant) { if let Some(diagnostic) = @@ -1259,8 +1232,6 @@ where self.diagnostics.push(diagnostic); } } - - // pylint if !self.is_stub { if self.enabled(Rule::UselessImportAlias) { pylint::rules::useless_import_alias(self, alias); @@ -4733,6 +4704,7 @@ impl<'a> Checker<'a> { Rule::TypingOnlyStandardLibraryImport, Rule::UndefinedExport, Rule::UnaliasedCollectionsAbcSetImport, + Rule::UnconventionalImportAlias, ]) { return; } @@ -4918,7 +4890,14 @@ impl<'a> Checker<'a> { if self.enabled(Rule::UnusedImport) { pyflakes::rules::unused_import(self, scope, &mut diagnostics); } - + if self.enabled(Rule::UnconventionalImportAlias) { + flake8_import_conventions::rules::unconventional_import_alias( + self, + scope, + &mut diagnostics, + &self.settings.flake8_import_conventions.aliases, + ); + } if self.is_stub { if self.enabled(Rule::UnaliasedCollectionsAbcSetImport) { flake8_pyi::rules::unaliased_collections_abc_set_import( diff --git a/crates/ruff/src/rules/flake8_import_conventions/rules/mod.rs b/crates/ruff/src/rules/flake8_import_conventions/rules/mod.rs index 9d6802ed73..96b426deb6 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_import_conventions/rules/mod.rs @@ -1,7 +1,7 @@ pub(crate) use banned_import_alias::*; pub(crate) use banned_import_from::*; -pub(crate) use conventional_import_alias::*; +pub(crate) use unconventional_import_alias::*; mod banned_import_alias; mod banned_import_from; -mod conventional_import_alias; +mod unconventional_import_alias; diff --git a/crates/ruff/src/rules/flake8_import_conventions/rules/conventional_import_alias.rs b/crates/ruff/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs similarity index 58% rename from crates/ruff/src/rules/flake8_import_conventions/rules/conventional_import_alias.rs rename to crates/ruff/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs index 58ee94eca0..c64b4ee4d0 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/rules/conventional_import_alias.rs +++ b/crates/ruff/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs @@ -1,8 +1,10 @@ use rustc_hash::FxHashMap; -use rustpython_parser::ast::{Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_semantic::Scope; + +use crate::checkers::ast::Checker; /// ## What it does /// Checks for imports that are typically imported using a common convention, @@ -40,22 +42,34 @@ impl Violation for UnconventionalImportAlias { } /// ICN001 -pub(crate) fn conventional_import_alias( - stmt: &Stmt, - name: &str, - asname: Option<&str>, +pub(crate) fn unconventional_import_alias( + checker: &Checker, + scope: &Scope, + diagnostics: &mut Vec, conventions: &FxHashMap, ) -> Option { - if let Some(expected_alias) = conventions.get(name) { - if asname != Some(expected_alias) { - return Some(Diagnostic::new( - UnconventionalImportAlias { - name: name.to_string(), - asname: expected_alias.to_string(), - }, - stmt.range(), - )); + for (name, binding_id) in scope.all_bindings() { + let binding = checker.semantic().binding(binding_id); + + let Some(qualified_name) = binding.qualified_name() else { + continue; + }; + + let Some(expected_alias) = conventions.get(qualified_name) else { + continue; + }; + + if binding.is_alias() && name == expected_alias { + continue; } + + diagnostics.push(Diagnostic::new( + UnconventionalImportAlias { + name: qualified_name.to_string(), + asname: expected_alias.to_string(), + }, + binding.range, + )); } None } diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__custom.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__custom.snap index c201a802f9..8431359f62 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__custom.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__custom.snap @@ -1,278 +1,278 @@ --- source: crates/ruff/src/rules/flake8_import_conventions/mod.rs --- -custom.py:3:1: ICN001 `altair` should be imported as `alt` +custom.py:3:8: ICN001 `altair` should be imported as `alt` | 1 | import math # not checked 2 | 3 | import altair # unconventional - | ^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 4 | import dask.array # unconventional 5 | import dask.dataframe # unconventional | -custom.py:4:1: ICN001 `dask.array` should be imported as `da` +custom.py:4:8: ICN001 `dask.array` should be imported as `da` | 3 | import altair # unconventional 4 | import dask.array # unconventional - | ^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 5 | import dask.dataframe # unconventional 6 | import matplotlib.pyplot # unconventional | -custom.py:5:1: ICN001 `dask.dataframe` should be imported as `dd` +custom.py:5:8: ICN001 `dask.dataframe` should be imported as `dd` | 3 | import altair # unconventional 4 | import dask.array # unconventional 5 | import dask.dataframe # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 6 | import matplotlib.pyplot # unconventional 7 | import numpy # unconventional | -custom.py:6:1: ICN001 `matplotlib.pyplot` should be imported as `plt` +custom.py:6:8: ICN001 `matplotlib.pyplot` should be imported as `plt` | 4 | import dask.array # unconventional 5 | import dask.dataframe # unconventional 6 | import matplotlib.pyplot # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^^ ICN001 7 | import numpy # unconventional 8 | import pandas # unconventional | -custom.py:7:1: ICN001 `numpy` should be imported as `np` +custom.py:7:8: ICN001 `numpy` should be imported as `np` | 5 | import dask.dataframe # unconventional 6 | import matplotlib.pyplot # unconventional 7 | import numpy # unconventional - | ^^^^^^^^^^^^ ICN001 + | ^^^^^ ICN001 8 | import pandas # unconventional 9 | import seaborn # unconventional | -custom.py:8:1: ICN001 `pandas` should be imported as `pd` +custom.py:8:8: ICN001 `pandas` should be imported as `pd` | 6 | import matplotlib.pyplot # unconventional 7 | import numpy # unconventional 8 | import pandas # unconventional - | ^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 9 | import seaborn # unconventional 10 | import tensorflow # unconventional | -custom.py:9:1: ICN001 `seaborn` should be imported as `sns` +custom.py:9:8: ICN001 `seaborn` should be imported as `sns` | 7 | import numpy # unconventional 8 | import pandas # unconventional 9 | import seaborn # unconventional - | ^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^ ICN001 10 | import tensorflow # unconventional 11 | import holoviews # unconventional | -custom.py:10:1: ICN001 `tensorflow` should be imported as `tf` +custom.py:10:8: ICN001 `tensorflow` should be imported as `tf` | 8 | import pandas # unconventional 9 | import seaborn # unconventional 10 | import tensorflow # unconventional - | ^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^^ ICN001 11 | import holoviews # unconventional 12 | import panel # unconventional | -custom.py:11:1: ICN001 `holoviews` should be imported as `hv` +custom.py:11:8: ICN001 `holoviews` should be imported as `hv` | 9 | import seaborn # unconventional 10 | import tensorflow # unconventional 11 | import holoviews # unconventional - | ^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^ ICN001 12 | import panel # unconventional 13 | import plotly.express # unconventional | -custom.py:12:1: ICN001 `panel` should be imported as `pn` +custom.py:12:8: ICN001 `panel` should be imported as `pn` | 10 | import tensorflow # unconventional 11 | import holoviews # unconventional 12 | import panel # unconventional - | ^^^^^^^^^^^^ ICN001 + | ^^^^^ ICN001 13 | import plotly.express # unconventional 14 | import matplotlib # unconventional | -custom.py:13:1: ICN001 `plotly.express` should be imported as `px` +custom.py:13:8: ICN001 `plotly.express` should be imported as `px` | 11 | import holoviews # unconventional 12 | import panel # unconventional 13 | import plotly.express # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 14 | import matplotlib # unconventional 15 | import polars # unconventional | -custom.py:14:1: ICN001 `matplotlib` should be imported as `mpl` +custom.py:14:8: ICN001 `matplotlib` should be imported as `mpl` | 12 | import panel # unconventional 13 | import plotly.express # unconventional 14 | import matplotlib # unconventional - | ^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^^ ICN001 15 | import polars # unconventional 16 | import pyarrow # unconventional | -custom.py:15:1: ICN001 `polars` should be imported as `pl` +custom.py:15:8: ICN001 `polars` should be imported as `pl` | 13 | import plotly.express # unconventional 14 | import matplotlib # unconventional 15 | import polars # unconventional - | ^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 16 | import pyarrow # unconventional | -custom.py:16:1: ICN001 `pyarrow` should be imported as `pa` +custom.py:16:8: ICN001 `pyarrow` should be imported as `pa` | 14 | import matplotlib # unconventional 15 | import polars # unconventional 16 | import pyarrow # unconventional - | ^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^ ICN001 17 | 18 | import altair as altr # unconventional | -custom.py:18:1: ICN001 `altair` should be imported as `alt` +custom.py:18:18: ICN001 `altair` should be imported as `alt` | 16 | import pyarrow # unconventional 17 | 18 | import altair as altr # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 19 | import matplotlib.pyplot as plot # unconventional 20 | import dask.array as darray # unconventional | -custom.py:19:1: ICN001 `matplotlib.pyplot` should be imported as `plt` +custom.py:19:29: ICN001 `matplotlib.pyplot` should be imported as `plt` | 18 | import altair as altr # unconventional 19 | import matplotlib.pyplot as plot # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 20 | import dask.array as darray # unconventional 21 | import dask.dataframe as ddf # unconventional | -custom.py:20:1: ICN001 `dask.array` should be imported as `da` +custom.py:20:22: ICN001 `dask.array` should be imported as `da` | 18 | import altair as altr # unconventional 19 | import matplotlib.pyplot as plot # unconventional 20 | import dask.array as darray # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 21 | import dask.dataframe as ddf # unconventional 22 | import numpy as nmp # unconventional | -custom.py:21:1: ICN001 `dask.dataframe` should be imported as `dd` +custom.py:21:26: ICN001 `dask.dataframe` should be imported as `dd` | 19 | import matplotlib.pyplot as plot # unconventional 20 | import dask.array as darray # unconventional 21 | import dask.dataframe as ddf # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^ ICN001 22 | import numpy as nmp # unconventional 23 | import pandas as pdas # unconventional | -custom.py:22:1: ICN001 `numpy` should be imported as `np` +custom.py:22:17: ICN001 `numpy` should be imported as `np` | 20 | import dask.array as darray # unconventional 21 | import dask.dataframe as ddf # unconventional 22 | import numpy as nmp # unconventional - | ^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^ ICN001 23 | import pandas as pdas # unconventional 24 | import seaborn as sbrn # unconventional | -custom.py:23:1: ICN001 `pandas` should be imported as `pd` +custom.py:23:18: ICN001 `pandas` should be imported as `pd` | 21 | import dask.dataframe as ddf # unconventional 22 | import numpy as nmp # unconventional 23 | import pandas as pdas # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 24 | import seaborn as sbrn # unconventional 25 | import tensorflow as tfz # unconventional | -custom.py:24:1: ICN001 `seaborn` should be imported as `sns` +custom.py:24:19: ICN001 `seaborn` should be imported as `sns` | 22 | import numpy as nmp # unconventional 23 | import pandas as pdas # unconventional 24 | import seaborn as sbrn # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 25 | import tensorflow as tfz # unconventional 26 | import holoviews as hsv # unconventional | -custom.py:25:1: ICN001 `tensorflow` should be imported as `tf` +custom.py:25:22: ICN001 `tensorflow` should be imported as `tf` | 23 | import pandas as pdas # unconventional 24 | import seaborn as sbrn # unconventional 25 | import tensorflow as tfz # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^ ICN001 26 | import holoviews as hsv # unconventional 27 | import panel as pns # unconventional | -custom.py:26:1: ICN001 `holoviews` should be imported as `hv` +custom.py:26:21: ICN001 `holoviews` should be imported as `hv` | 24 | import seaborn as sbrn # unconventional 25 | import tensorflow as tfz # unconventional 26 | import holoviews as hsv # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^ ICN001 27 | import panel as pns # unconventional 28 | import plotly.express as pltx # unconventional | -custom.py:27:1: ICN001 `panel` should be imported as `pn` +custom.py:27:17: ICN001 `panel` should be imported as `pn` | 25 | import tensorflow as tfz # unconventional 26 | import holoviews as hsv # unconventional 27 | import panel as pns # unconventional - | ^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^ ICN001 28 | import plotly.express as pltx # unconventional 29 | import matplotlib as ml # unconventional | -custom.py:28:1: ICN001 `plotly.express` should be imported as `px` +custom.py:28:26: ICN001 `plotly.express` should be imported as `px` | 26 | import holoviews as hsv # unconventional 27 | import panel as pns # unconventional 28 | import plotly.express as pltx # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 29 | import matplotlib as ml # unconventional 30 | import polars as ps # unconventional | -custom.py:29:1: ICN001 `matplotlib` should be imported as `mpl` +custom.py:29:22: ICN001 `matplotlib` should be imported as `mpl` | 27 | import panel as pns # unconventional 28 | import plotly.express as pltx # unconventional 29 | import matplotlib as ml # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^ ICN001 30 | import polars as ps # unconventional 31 | import pyarrow as arr # unconventional | -custom.py:30:1: ICN001 `polars` should be imported as `pl` +custom.py:30:18: ICN001 `polars` should be imported as `pl` | 28 | import plotly.express as pltx # unconventional 29 | import matplotlib as ml # unconventional 30 | import polars as ps # unconventional - | ^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^ ICN001 31 | import pyarrow as arr # unconventional | -custom.py:31:1: ICN001 `pyarrow` should be imported as `pa` +custom.py:31:19: ICN001 `pyarrow` should be imported as `pa` | 29 | import matplotlib as ml # unconventional 30 | import polars as ps # unconventional 31 | import pyarrow as arr # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^ ICN001 32 | 33 | import altair as alt # conventional | diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__defaults.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__defaults.snap index 16167a773c..64cb07efa9 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__defaults.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__defaults.snap @@ -1,98 +1,98 @@ --- source: crates/ruff/src/rules/flake8_import_conventions/mod.rs --- -defaults.py:3:1: ICN001 `altair` should be imported as `alt` +defaults.py:3:8: ICN001 `altair` should be imported as `alt` | 1 | import math # not checked 2 | 3 | import altair # unconventional - | ^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 4 | import matplotlib.pyplot # unconventional 5 | import numpy # unconventional | -defaults.py:4:1: ICN001 `matplotlib.pyplot` should be imported as `plt` +defaults.py:4:8: ICN001 `matplotlib.pyplot` should be imported as `plt` | 3 | import altair # unconventional 4 | import matplotlib.pyplot # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^^ ICN001 5 | import numpy # unconventional 6 | import pandas # unconventional | -defaults.py:5:1: ICN001 `numpy` should be imported as `np` +defaults.py:5:8: ICN001 `numpy` should be imported as `np` | 3 | import altair # unconventional 4 | import matplotlib.pyplot # unconventional 5 | import numpy # unconventional - | ^^^^^^^^^^^^ ICN001 + | ^^^^^ ICN001 6 | import pandas # unconventional 7 | import seaborn # unconventional | -defaults.py:6:1: ICN001 `pandas` should be imported as `pd` +defaults.py:6:8: ICN001 `pandas` should be imported as `pd` | 4 | import matplotlib.pyplot # unconventional 5 | import numpy # unconventional 6 | import pandas # unconventional - | ^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 7 | import seaborn # unconventional | -defaults.py:7:1: ICN001 `seaborn` should be imported as `sns` +defaults.py:7:8: ICN001 `seaborn` should be imported as `sns` | 5 | import numpy # unconventional 6 | import pandas # unconventional 7 | import seaborn # unconventional - | ^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^ ICN001 8 | 9 | import altair as altr # unconventional | -defaults.py:9:1: ICN001 `altair` should be imported as `alt` +defaults.py:9:18: ICN001 `altair` should be imported as `alt` | 7 | import seaborn # unconventional 8 | 9 | import altair as altr # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 10 | import matplotlib.pyplot as plot # unconventional 11 | import numpy as nmp # unconventional | -defaults.py:10:1: ICN001 `matplotlib.pyplot` should be imported as `plt` +defaults.py:10:29: ICN001 `matplotlib.pyplot` should be imported as `plt` | 9 | import altair as altr # unconventional 10 | import matplotlib.pyplot as plot # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 11 | import numpy as nmp # unconventional 12 | import pandas as pdas # unconventional | -defaults.py:11:1: ICN001 `numpy` should be imported as `np` +defaults.py:11:17: ICN001 `numpy` should be imported as `np` | 9 | import altair as altr # unconventional 10 | import matplotlib.pyplot as plot # unconventional 11 | import numpy as nmp # unconventional - | ^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^ ICN001 12 | import pandas as pdas # unconventional 13 | import seaborn as sbrn # unconventional | -defaults.py:12:1: ICN001 `pandas` should be imported as `pd` +defaults.py:12:18: ICN001 `pandas` should be imported as `pd` | 10 | import matplotlib.pyplot as plot # unconventional 11 | import numpy as nmp # unconventional 12 | import pandas as pdas # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 13 | import seaborn as sbrn # unconventional | -defaults.py:13:1: ICN001 `seaborn` should be imported as `sns` +defaults.py:13:19: ICN001 `seaborn` should be imported as `sns` | 11 | import numpy as nmp # unconventional 12 | import pandas as pdas # unconventional 13 | import seaborn as sbrn # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 14 | 15 | import altair as alt # conventional | diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__from_imports.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__from_imports.snap index 31d49549f1..06d88962d8 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__from_imports.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__from_imports.snap @@ -1,81 +1,81 @@ --- source: crates/ruff/src/rules/flake8_import_conventions/mod.rs --- -from_imports.py:3:1: ICN001 `xml.dom.minidom` should be imported as `md` +from_imports.py:3:8: ICN001 `xml.dom.minidom` should be imported as `md` | 1 | # Test absolute imports 2 | # Violation cases 3 | import xml.dom.minidom - | ^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^ ICN001 4 | import xml.dom.minidom as wrong 5 | from xml.dom import minidom as wrong | -from_imports.py:4:1: ICN001 `xml.dom.minidom` should be imported as `md` +from_imports.py:4:27: ICN001 `xml.dom.minidom` should be imported as `md` | 2 | # Violation cases 3 | import xml.dom.minidom 4 | import xml.dom.minidom as wrong - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^ ICN001 5 | from xml.dom import minidom as wrong 6 | from xml.dom import minidom | -from_imports.py:5:1: ICN001 `xml.dom.minidom` should be imported as `md` +from_imports.py:5:32: ICN001 `xml.dom.minidom` should be imported as `md` | 3 | import xml.dom.minidom 4 | import xml.dom.minidom as wrong 5 | from xml.dom import minidom as wrong - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^ ICN001 6 | from xml.dom import minidom 7 | from xml.dom.minidom import parseString as wrong # Ensure ICN001 throws on function import. | -from_imports.py:6:1: ICN001 `xml.dom.minidom` should be imported as `md` +from_imports.py:6:21: ICN001 `xml.dom.minidom` should be imported as `md` | 4 | import xml.dom.minidom as wrong 5 | from xml.dom import minidom as wrong 6 | from xml.dom import minidom - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^ ICN001 7 | from xml.dom.minidom import parseString as wrong # Ensure ICN001 throws on function import. 8 | from xml.dom.minidom import parseString | -from_imports.py:7:1: ICN001 `xml.dom.minidom.parseString` should be imported as `pstr` +from_imports.py:7:44: ICN001 `xml.dom.minidom.parseString` should be imported as `pstr` | 5 | from xml.dom import minidom as wrong 6 | from xml.dom import minidom 7 | from xml.dom.minidom import parseString as wrong # Ensure ICN001 throws on function import. - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^ ICN001 8 | from xml.dom.minidom import parseString 9 | from xml.dom.minidom import parse, parseString | -from_imports.py:8:1: ICN001 `xml.dom.minidom.parseString` should be imported as `pstr` +from_imports.py:8:29: ICN001 `xml.dom.minidom.parseString` should be imported as `pstr` | 6 | from xml.dom import minidom 7 | from xml.dom.minidom import parseString as wrong # Ensure ICN001 throws on function import. 8 | from xml.dom.minidom import parseString - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^^^ ICN001 9 | from xml.dom.minidom import parse, parseString 10 | from xml.dom.minidom import parse as ps, parseString as wrong | -from_imports.py:9:1: ICN001 `xml.dom.minidom.parseString` should be imported as `pstr` +from_imports.py:9:36: ICN001 `xml.dom.minidom.parseString` should be imported as `pstr` | 7 | from xml.dom.minidom import parseString as wrong # Ensure ICN001 throws on function import. 8 | from xml.dom.minidom import parseString 9 | from xml.dom.minidom import parse, parseString - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^^^ ICN001 10 | from xml.dom.minidom import parse as ps, parseString as wrong | -from_imports.py:10:1: ICN001 `xml.dom.minidom.parseString` should be imported as `pstr` +from_imports.py:10:57: ICN001 `xml.dom.minidom.parseString` should be imported as `pstr` | 8 | from xml.dom.minidom import parseString 9 | from xml.dom.minidom import parse, parseString 10 | from xml.dom.minidom import parse as ps, parseString as wrong - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^ ICN001 11 | 12 | # No ICN001 violations | diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__override_default.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__override_default.snap index 905d5e6c24..2efcdae6d4 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__override_default.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__override_default.snap @@ -1,98 +1,98 @@ --- source: crates/ruff/src/rules/flake8_import_conventions/mod.rs --- -override_default.py:3:1: ICN001 `altair` should be imported as `alt` +override_default.py:3:8: ICN001 `altair` should be imported as `alt` | 1 | import math # not checked 2 | 3 | import altair # unconventional - | ^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 4 | import matplotlib.pyplot # unconventional 5 | import numpy # unconventional | -override_default.py:4:1: ICN001 `matplotlib.pyplot` should be imported as `plt` +override_default.py:4:8: ICN001 `matplotlib.pyplot` should be imported as `plt` | 3 | import altair # unconventional 4 | import matplotlib.pyplot # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^^ ICN001 5 | import numpy # unconventional 6 | import pandas # unconventional | -override_default.py:5:1: ICN001 `numpy` should be imported as `nmp` +override_default.py:5:8: ICN001 `numpy` should be imported as `nmp` | 3 | import altair # unconventional 4 | import matplotlib.pyplot # unconventional 5 | import numpy # unconventional - | ^^^^^^^^^^^^ ICN001 + | ^^^^^ ICN001 6 | import pandas # unconventional 7 | import seaborn # unconventional | -override_default.py:6:1: ICN001 `pandas` should be imported as `pd` +override_default.py:6:8: ICN001 `pandas` should be imported as `pd` | 4 | import matplotlib.pyplot # unconventional 5 | import numpy # unconventional 6 | import pandas # unconventional - | ^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 7 | import seaborn # unconventional | -override_default.py:7:1: ICN001 `seaborn` should be imported as `sns` +override_default.py:7:8: ICN001 `seaborn` should be imported as `sns` | 5 | import numpy # unconventional 6 | import pandas # unconventional 7 | import seaborn # unconventional - | ^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^ ICN001 8 | 9 | import altair as altr # unconventional | -override_default.py:9:1: ICN001 `altair` should be imported as `alt` +override_default.py:9:18: ICN001 `altair` should be imported as `alt` | 7 | import seaborn # unconventional 8 | 9 | import altair as altr # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 10 | import matplotlib.pyplot as plot # unconventional 11 | import numpy as np # unconventional | -override_default.py:10:1: ICN001 `matplotlib.pyplot` should be imported as `plt` +override_default.py:10:29: ICN001 `matplotlib.pyplot` should be imported as `plt` | 9 | import altair as altr # unconventional 10 | import matplotlib.pyplot as plot # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 11 | import numpy as np # unconventional 12 | import pandas as pdas # unconventional | -override_default.py:11:1: ICN001 `numpy` should be imported as `nmp` +override_default.py:11:17: ICN001 `numpy` should be imported as `nmp` | 9 | import altair as altr # unconventional 10 | import matplotlib.pyplot as plot # unconventional 11 | import numpy as np # unconventional - | ^^^^^^^^^^^^^^^^^^ ICN001 + | ^^ ICN001 12 | import pandas as pdas # unconventional 13 | import seaborn as sbrn # unconventional | -override_default.py:12:1: ICN001 `pandas` should be imported as `pd` +override_default.py:12:18: ICN001 `pandas` should be imported as `pd` | 10 | import matplotlib.pyplot as plot # unconventional 11 | import numpy as np # unconventional 12 | import pandas as pdas # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 13 | import seaborn as sbrn # unconventional | -override_default.py:13:1: ICN001 `seaborn` should be imported as `sns` +override_default.py:13:19: ICN001 `seaborn` should be imported as `sns` | 11 | import numpy as np # unconventional 12 | import pandas as pdas # unconventional 13 | import seaborn as sbrn # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 14 | 15 | import altair as alt # conventional | diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__remove_default.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__remove_default.snap index 55b3c4fda9..ad8723313c 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__remove_default.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__remove_default.snap @@ -1,78 +1,78 @@ --- source: crates/ruff/src/rules/flake8_import_conventions/mod.rs --- -remove_default.py:3:1: ICN001 `altair` should be imported as `alt` +remove_default.py:3:8: ICN001 `altair` should be imported as `alt` | 1 | import math # not checked 2 | 3 | import altair # unconventional - | ^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 4 | import matplotlib.pyplot # unconventional 5 | import numpy # not checked | -remove_default.py:4:1: ICN001 `matplotlib.pyplot` should be imported as `plt` +remove_default.py:4:8: ICN001 `matplotlib.pyplot` should be imported as `plt` | 3 | import altair # unconventional 4 | import matplotlib.pyplot # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^^ ICN001 5 | import numpy # not checked 6 | import pandas # unconventional | -remove_default.py:6:1: ICN001 `pandas` should be imported as `pd` +remove_default.py:6:8: ICN001 `pandas` should be imported as `pd` | 4 | import matplotlib.pyplot # unconventional 5 | import numpy # not checked 6 | import pandas # unconventional - | ^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 7 | import seaborn # unconventional | -remove_default.py:7:1: ICN001 `seaborn` should be imported as `sns` +remove_default.py:7:8: ICN001 `seaborn` should be imported as `sns` | 5 | import numpy # not checked 6 | import pandas # unconventional 7 | import seaborn # unconventional - | ^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^ ICN001 8 | 9 | import altair as altr # unconventional | -remove_default.py:9:1: ICN001 `altair` should be imported as `alt` +remove_default.py:9:18: ICN001 `altair` should be imported as `alt` | 7 | import seaborn # unconventional 8 | 9 | import altair as altr # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 10 | import matplotlib.pyplot as plot # unconventional 11 | import numpy as nmp # not checked | -remove_default.py:10:1: ICN001 `matplotlib.pyplot` should be imported as `plt` +remove_default.py:10:29: ICN001 `matplotlib.pyplot` should be imported as `plt` | 9 | import altair as altr # unconventional 10 | import matplotlib.pyplot as plot # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 11 | import numpy as nmp # not checked 12 | import pandas as pdas # unconventional | -remove_default.py:12:1: ICN001 `pandas` should be imported as `pd` +remove_default.py:12:18: ICN001 `pandas` should be imported as `pd` | 10 | import matplotlib.pyplot as plot # unconventional 11 | import numpy as nmp # not checked 12 | import pandas as pdas # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 13 | import seaborn as sbrn # unconventional | -remove_default.py:13:1: ICN001 `seaborn` should be imported as `sns` +remove_default.py:13:19: ICN001 `seaborn` should be imported as `sns` | 11 | import numpy as nmp # not checked 12 | import pandas as pdas # unconventional 13 | import seaborn as sbrn # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 14 | 15 | import altair as alt # conventional | From 524a2045ba208f66ffad09f1eed87e9d83d9d81d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 18 Jun 2023 11:56:42 -0400 Subject: [PATCH 102/447] Enable autofix for unconventional imports rule (#5152) ## Summary We can now automatically rewrite `import pandas` to `import pandas as pd`, with minimal changes needed. --- .../rules/unconventional_import_alias.rs | 25 +++++++++++++++-- ...ke8_import_conventions__tests__custom.snap | 28 +++++++++++++++++++ ...8_import_conventions__tests__defaults.snap | 10 +++++++ ...port_conventions__tests__from_imports.snap | 8 ++++++ ..._conventions__tests__override_default.snap | 10 +++++++ ...rt_conventions__tests__remove_default.snap | 8 ++++++ 6 files changed, 86 insertions(+), 3 deletions(-) diff --git a/crates/ruff/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs b/crates/ruff/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs index c64b4ee4d0..dc5876dac3 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs +++ b/crates/ruff/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs @@ -1,10 +1,12 @@ use rustc_hash::FxHashMap; -use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_semantic::Scope; use crate::checkers::ast::Checker; +use crate::registry::AsRule; +use crate::renamer::Renamer; /// ## What it does /// Checks for imports that are typically imported using a common convention, @@ -34,11 +36,18 @@ pub struct UnconventionalImportAlias { } impl Violation for UnconventionalImportAlias { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { let UnconventionalImportAlias { name, asname } = self; format!("`{name}` should be imported as `{asname}`") } + + fn autofix_title(&self) -> Option { + let UnconventionalImportAlias { name, asname } = self; + Some(format!("Alias `{name}` to `{asname}`")) + } } /// ICN001 @@ -63,13 +72,23 @@ pub(crate) fn unconventional_import_alias( continue; } - diagnostics.push(Diagnostic::new( + let mut diagnostic = Diagnostic::new( UnconventionalImportAlias { name: qualified_name.to_string(), asname: expected_alias.to_string(), }, binding.range, - )); + ); + if checker.patch(diagnostic.kind.rule()) { + if checker.semantic().is_available(expected_alias) { + diagnostic.try_set_fix(|| { + let (edit, rest) = + Renamer::rename(name, expected_alias, scope, checker.semantic())?; + Ok(Fix::suggested_edits(edit, rest)) + }); + } + } + diagnostics.push(diagnostic); } None } diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__custom.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__custom.snap index 8431359f62..8552fb09ea 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__custom.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__custom.snap @@ -10,6 +10,7 @@ custom.py:3:8: ICN001 `altair` should be imported as `alt` 4 | import dask.array # unconventional 5 | import dask.dataframe # unconventional | + = help: Alias `altair` to `alt` custom.py:4:8: ICN001 `dask.array` should be imported as `da` | @@ -19,6 +20,7 @@ custom.py:4:8: ICN001 `dask.array` should be imported as `da` 5 | import dask.dataframe # unconventional 6 | import matplotlib.pyplot # unconventional | + = help: Alias `dask.array` to `da` custom.py:5:8: ICN001 `dask.dataframe` should be imported as `dd` | @@ -29,6 +31,7 @@ custom.py:5:8: ICN001 `dask.dataframe` should be imported as `dd` 6 | import matplotlib.pyplot # unconventional 7 | import numpy # unconventional | + = help: Alias `dask.dataframe` to `dd` custom.py:6:8: ICN001 `matplotlib.pyplot` should be imported as `plt` | @@ -39,6 +42,7 @@ custom.py:6:8: ICN001 `matplotlib.pyplot` should be imported as `plt` 7 | import numpy # unconventional 8 | import pandas # unconventional | + = help: Alias `matplotlib.pyplot` to `plt` custom.py:7:8: ICN001 `numpy` should be imported as `np` | @@ -49,6 +53,7 @@ custom.py:7:8: ICN001 `numpy` should be imported as `np` 8 | import pandas # unconventional 9 | import seaborn # unconventional | + = help: Alias `numpy` to `np` custom.py:8:8: ICN001 `pandas` should be imported as `pd` | @@ -59,6 +64,7 @@ custom.py:8:8: ICN001 `pandas` should be imported as `pd` 9 | import seaborn # unconventional 10 | import tensorflow # unconventional | + = help: Alias `pandas` to `pd` custom.py:9:8: ICN001 `seaborn` should be imported as `sns` | @@ -69,6 +75,7 @@ custom.py:9:8: ICN001 `seaborn` should be imported as `sns` 10 | import tensorflow # unconventional 11 | import holoviews # unconventional | + = help: Alias `seaborn` to `sns` custom.py:10:8: ICN001 `tensorflow` should be imported as `tf` | @@ -79,6 +86,7 @@ custom.py:10:8: ICN001 `tensorflow` should be imported as `tf` 11 | import holoviews # unconventional 12 | import panel # unconventional | + = help: Alias `tensorflow` to `tf` custom.py:11:8: ICN001 `holoviews` should be imported as `hv` | @@ -89,6 +97,7 @@ custom.py:11:8: ICN001 `holoviews` should be imported as `hv` 12 | import panel # unconventional 13 | import plotly.express # unconventional | + = help: Alias `holoviews` to `hv` custom.py:12:8: ICN001 `panel` should be imported as `pn` | @@ -99,6 +108,7 @@ custom.py:12:8: ICN001 `panel` should be imported as `pn` 13 | import plotly.express # unconventional 14 | import matplotlib # unconventional | + = help: Alias `panel` to `pn` custom.py:13:8: ICN001 `plotly.express` should be imported as `px` | @@ -109,6 +119,7 @@ custom.py:13:8: ICN001 `plotly.express` should be imported as `px` 14 | import matplotlib # unconventional 15 | import polars # unconventional | + = help: Alias `plotly.express` to `px` custom.py:14:8: ICN001 `matplotlib` should be imported as `mpl` | @@ -119,6 +130,7 @@ custom.py:14:8: ICN001 `matplotlib` should be imported as `mpl` 15 | import polars # unconventional 16 | import pyarrow # unconventional | + = help: Alias `matplotlib` to `mpl` custom.py:15:8: ICN001 `polars` should be imported as `pl` | @@ -128,6 +140,7 @@ custom.py:15:8: ICN001 `polars` should be imported as `pl` | ^^^^^^ ICN001 16 | import pyarrow # unconventional | + = help: Alias `polars` to `pl` custom.py:16:8: ICN001 `pyarrow` should be imported as `pa` | @@ -138,6 +151,7 @@ custom.py:16:8: ICN001 `pyarrow` should be imported as `pa` 17 | 18 | import altair as altr # unconventional | + = help: Alias `pyarrow` to `pa` custom.py:18:18: ICN001 `altair` should be imported as `alt` | @@ -148,6 +162,7 @@ custom.py:18:18: ICN001 `altair` should be imported as `alt` 19 | import matplotlib.pyplot as plot # unconventional 20 | import dask.array as darray # unconventional | + = help: Alias `altair` to `alt` custom.py:19:29: ICN001 `matplotlib.pyplot` should be imported as `plt` | @@ -157,6 +172,7 @@ custom.py:19:29: ICN001 `matplotlib.pyplot` should be imported as `plt` 20 | import dask.array as darray # unconventional 21 | import dask.dataframe as ddf # unconventional | + = help: Alias `matplotlib.pyplot` to `plt` custom.py:20:22: ICN001 `dask.array` should be imported as `da` | @@ -167,6 +183,7 @@ custom.py:20:22: ICN001 `dask.array` should be imported as `da` 21 | import dask.dataframe as ddf # unconventional 22 | import numpy as nmp # unconventional | + = help: Alias `dask.array` to `da` custom.py:21:26: ICN001 `dask.dataframe` should be imported as `dd` | @@ -177,6 +194,7 @@ custom.py:21:26: ICN001 `dask.dataframe` should be imported as `dd` 22 | import numpy as nmp # unconventional 23 | import pandas as pdas # unconventional | + = help: Alias `dask.dataframe` to `dd` custom.py:22:17: ICN001 `numpy` should be imported as `np` | @@ -187,6 +205,7 @@ custom.py:22:17: ICN001 `numpy` should be imported as `np` 23 | import pandas as pdas # unconventional 24 | import seaborn as sbrn # unconventional | + = help: Alias `numpy` to `np` custom.py:23:18: ICN001 `pandas` should be imported as `pd` | @@ -197,6 +216,7 @@ custom.py:23:18: ICN001 `pandas` should be imported as `pd` 24 | import seaborn as sbrn # unconventional 25 | import tensorflow as tfz # unconventional | + = help: Alias `pandas` to `pd` custom.py:24:19: ICN001 `seaborn` should be imported as `sns` | @@ -207,6 +227,7 @@ custom.py:24:19: ICN001 `seaborn` should be imported as `sns` 25 | import tensorflow as tfz # unconventional 26 | import holoviews as hsv # unconventional | + = help: Alias `seaborn` to `sns` custom.py:25:22: ICN001 `tensorflow` should be imported as `tf` | @@ -217,6 +238,7 @@ custom.py:25:22: ICN001 `tensorflow` should be imported as `tf` 26 | import holoviews as hsv # unconventional 27 | import panel as pns # unconventional | + = help: Alias `tensorflow` to `tf` custom.py:26:21: ICN001 `holoviews` should be imported as `hv` | @@ -227,6 +249,7 @@ custom.py:26:21: ICN001 `holoviews` should be imported as `hv` 27 | import panel as pns # unconventional 28 | import plotly.express as pltx # unconventional | + = help: Alias `holoviews` to `hv` custom.py:27:17: ICN001 `panel` should be imported as `pn` | @@ -237,6 +260,7 @@ custom.py:27:17: ICN001 `panel` should be imported as `pn` 28 | import plotly.express as pltx # unconventional 29 | import matplotlib as ml # unconventional | + = help: Alias `panel` to `pn` custom.py:28:26: ICN001 `plotly.express` should be imported as `px` | @@ -247,6 +271,7 @@ custom.py:28:26: ICN001 `plotly.express` should be imported as `px` 29 | import matplotlib as ml # unconventional 30 | import polars as ps # unconventional | + = help: Alias `plotly.express` to `px` custom.py:29:22: ICN001 `matplotlib` should be imported as `mpl` | @@ -257,6 +282,7 @@ custom.py:29:22: ICN001 `matplotlib` should be imported as `mpl` 30 | import polars as ps # unconventional 31 | import pyarrow as arr # unconventional | + = help: Alias `matplotlib` to `mpl` custom.py:30:18: ICN001 `polars` should be imported as `pl` | @@ -266,6 +292,7 @@ custom.py:30:18: ICN001 `polars` should be imported as `pl` | ^^ ICN001 31 | import pyarrow as arr # unconventional | + = help: Alias `polars` to `pl` custom.py:31:19: ICN001 `pyarrow` should be imported as `pa` | @@ -276,5 +303,6 @@ custom.py:31:19: ICN001 `pyarrow` should be imported as `pa` 32 | 33 | import altair as alt # conventional | + = help: Alias `pyarrow` to `pa` diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__defaults.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__defaults.snap index 64cb07efa9..89782e181f 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__defaults.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__defaults.snap @@ -10,6 +10,7 @@ defaults.py:3:8: ICN001 `altair` should be imported as `alt` 4 | import matplotlib.pyplot # unconventional 5 | import numpy # unconventional | + = help: Alias `altair` to `alt` defaults.py:4:8: ICN001 `matplotlib.pyplot` should be imported as `plt` | @@ -19,6 +20,7 @@ defaults.py:4:8: ICN001 `matplotlib.pyplot` should be imported as `plt` 5 | import numpy # unconventional 6 | import pandas # unconventional | + = help: Alias `matplotlib.pyplot` to `plt` defaults.py:5:8: ICN001 `numpy` should be imported as `np` | @@ -29,6 +31,7 @@ defaults.py:5:8: ICN001 `numpy` should be imported as `np` 6 | import pandas # unconventional 7 | import seaborn # unconventional | + = help: Alias `numpy` to `np` defaults.py:6:8: ICN001 `pandas` should be imported as `pd` | @@ -38,6 +41,7 @@ defaults.py:6:8: ICN001 `pandas` should be imported as `pd` | ^^^^^^ ICN001 7 | import seaborn # unconventional | + = help: Alias `pandas` to `pd` defaults.py:7:8: ICN001 `seaborn` should be imported as `sns` | @@ -48,6 +52,7 @@ defaults.py:7:8: ICN001 `seaborn` should be imported as `sns` 8 | 9 | import altair as altr # unconventional | + = help: Alias `seaborn` to `sns` defaults.py:9:18: ICN001 `altair` should be imported as `alt` | @@ -58,6 +63,7 @@ defaults.py:9:18: ICN001 `altair` should be imported as `alt` 10 | import matplotlib.pyplot as plot # unconventional 11 | import numpy as nmp # unconventional | + = help: Alias `altair` to `alt` defaults.py:10:29: ICN001 `matplotlib.pyplot` should be imported as `plt` | @@ -67,6 +73,7 @@ defaults.py:10:29: ICN001 `matplotlib.pyplot` should be imported as `plt` 11 | import numpy as nmp # unconventional 12 | import pandas as pdas # unconventional | + = help: Alias `matplotlib.pyplot` to `plt` defaults.py:11:17: ICN001 `numpy` should be imported as `np` | @@ -77,6 +84,7 @@ defaults.py:11:17: ICN001 `numpy` should be imported as `np` 12 | import pandas as pdas # unconventional 13 | import seaborn as sbrn # unconventional | + = help: Alias `numpy` to `np` defaults.py:12:18: ICN001 `pandas` should be imported as `pd` | @@ -86,6 +94,7 @@ defaults.py:12:18: ICN001 `pandas` should be imported as `pd` | ^^^^ ICN001 13 | import seaborn as sbrn # unconventional | + = help: Alias `pandas` to `pd` defaults.py:13:19: ICN001 `seaborn` should be imported as `sns` | @@ -96,5 +105,6 @@ defaults.py:13:19: ICN001 `seaborn` should be imported as `sns` 14 | 15 | import altair as alt # conventional | + = help: Alias `seaborn` to `sns` diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__from_imports.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__from_imports.snap index 06d88962d8..f69e23b91c 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__from_imports.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__from_imports.snap @@ -10,6 +10,7 @@ from_imports.py:3:8: ICN001 `xml.dom.minidom` should be imported as `md` 4 | import xml.dom.minidom as wrong 5 | from xml.dom import minidom as wrong | + = help: Alias `xml.dom.minidom` to `md` from_imports.py:4:27: ICN001 `xml.dom.minidom` should be imported as `md` | @@ -20,6 +21,7 @@ from_imports.py:4:27: ICN001 `xml.dom.minidom` should be imported as `md` 5 | from xml.dom import minidom as wrong 6 | from xml.dom import minidom | + = help: Alias `xml.dom.minidom` to `md` from_imports.py:5:32: ICN001 `xml.dom.minidom` should be imported as `md` | @@ -30,6 +32,7 @@ from_imports.py:5:32: ICN001 `xml.dom.minidom` should be imported as `md` 6 | from xml.dom import minidom 7 | from xml.dom.minidom import parseString as wrong # Ensure ICN001 throws on function import. | + = help: Alias `xml.dom.minidom` to `md` from_imports.py:6:21: ICN001 `xml.dom.minidom` should be imported as `md` | @@ -40,6 +43,7 @@ from_imports.py:6:21: ICN001 `xml.dom.minidom` should be imported as `md` 7 | from xml.dom.minidom import parseString as wrong # Ensure ICN001 throws on function import. 8 | from xml.dom.minidom import parseString | + = help: Alias `xml.dom.minidom` to `md` from_imports.py:7:44: ICN001 `xml.dom.minidom.parseString` should be imported as `pstr` | @@ -50,6 +54,7 @@ from_imports.py:7:44: ICN001 `xml.dom.minidom.parseString` should be imported as 8 | from xml.dom.minidom import parseString 9 | from xml.dom.minidom import parse, parseString | + = help: Alias `xml.dom.minidom.parseString` to `pstr` from_imports.py:8:29: ICN001 `xml.dom.minidom.parseString` should be imported as `pstr` | @@ -60,6 +65,7 @@ from_imports.py:8:29: ICN001 `xml.dom.minidom.parseString` should be imported as 9 | from xml.dom.minidom import parse, parseString 10 | from xml.dom.minidom import parse as ps, parseString as wrong | + = help: Alias `xml.dom.minidom.parseString` to `pstr` from_imports.py:9:36: ICN001 `xml.dom.minidom.parseString` should be imported as `pstr` | @@ -69,6 +75,7 @@ from_imports.py:9:36: ICN001 `xml.dom.minidom.parseString` should be imported as | ^^^^^^^^^^^ ICN001 10 | from xml.dom.minidom import parse as ps, parseString as wrong | + = help: Alias `xml.dom.minidom.parseString` to `pstr` from_imports.py:10:57: ICN001 `xml.dom.minidom.parseString` should be imported as `pstr` | @@ -79,5 +86,6 @@ from_imports.py:10:57: ICN001 `xml.dom.minidom.parseString` should be imported a 11 | 12 | # No ICN001 violations | + = help: Alias `xml.dom.minidom.parseString` to `pstr` diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__override_default.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__override_default.snap index 2efcdae6d4..267ed836dd 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__override_default.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__override_default.snap @@ -10,6 +10,7 @@ override_default.py:3:8: ICN001 `altair` should be imported as `alt` 4 | import matplotlib.pyplot # unconventional 5 | import numpy # unconventional | + = help: Alias `altair` to `alt` override_default.py:4:8: ICN001 `matplotlib.pyplot` should be imported as `plt` | @@ -19,6 +20,7 @@ override_default.py:4:8: ICN001 `matplotlib.pyplot` should be imported as `plt` 5 | import numpy # unconventional 6 | import pandas # unconventional | + = help: Alias `matplotlib.pyplot` to `plt` override_default.py:5:8: ICN001 `numpy` should be imported as `nmp` | @@ -29,6 +31,7 @@ override_default.py:5:8: ICN001 `numpy` should be imported as `nmp` 6 | import pandas # unconventional 7 | import seaborn # unconventional | + = help: Alias `numpy` to `nmp` override_default.py:6:8: ICN001 `pandas` should be imported as `pd` | @@ -38,6 +41,7 @@ override_default.py:6:8: ICN001 `pandas` should be imported as `pd` | ^^^^^^ ICN001 7 | import seaborn # unconventional | + = help: Alias `pandas` to `pd` override_default.py:7:8: ICN001 `seaborn` should be imported as `sns` | @@ -48,6 +52,7 @@ override_default.py:7:8: ICN001 `seaborn` should be imported as `sns` 8 | 9 | import altair as altr # unconventional | + = help: Alias `seaborn` to `sns` override_default.py:9:18: ICN001 `altair` should be imported as `alt` | @@ -58,6 +63,7 @@ override_default.py:9:18: ICN001 `altair` should be imported as `alt` 10 | import matplotlib.pyplot as plot # unconventional 11 | import numpy as np # unconventional | + = help: Alias `altair` to `alt` override_default.py:10:29: ICN001 `matplotlib.pyplot` should be imported as `plt` | @@ -67,6 +73,7 @@ override_default.py:10:29: ICN001 `matplotlib.pyplot` should be imported as `plt 11 | import numpy as np # unconventional 12 | import pandas as pdas # unconventional | + = help: Alias `matplotlib.pyplot` to `plt` override_default.py:11:17: ICN001 `numpy` should be imported as `nmp` | @@ -77,6 +84,7 @@ override_default.py:11:17: ICN001 `numpy` should be imported as `nmp` 12 | import pandas as pdas # unconventional 13 | import seaborn as sbrn # unconventional | + = help: Alias `numpy` to `nmp` override_default.py:12:18: ICN001 `pandas` should be imported as `pd` | @@ -86,6 +94,7 @@ override_default.py:12:18: ICN001 `pandas` should be imported as `pd` | ^^^^ ICN001 13 | import seaborn as sbrn # unconventional | + = help: Alias `pandas` to `pd` override_default.py:13:19: ICN001 `seaborn` should be imported as `sns` | @@ -96,5 +105,6 @@ override_default.py:13:19: ICN001 `seaborn` should be imported as `sns` 14 | 15 | import altair as alt # conventional | + = help: Alias `seaborn` to `sns` diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__remove_default.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__remove_default.snap index ad8723313c..60ce87e652 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__remove_default.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__remove_default.snap @@ -10,6 +10,7 @@ remove_default.py:3:8: ICN001 `altair` should be imported as `alt` 4 | import matplotlib.pyplot # unconventional 5 | import numpy # not checked | + = help: Alias `altair` to `alt` remove_default.py:4:8: ICN001 `matplotlib.pyplot` should be imported as `plt` | @@ -19,6 +20,7 @@ remove_default.py:4:8: ICN001 `matplotlib.pyplot` should be imported as `plt` 5 | import numpy # not checked 6 | import pandas # unconventional | + = help: Alias `matplotlib.pyplot` to `plt` remove_default.py:6:8: ICN001 `pandas` should be imported as `pd` | @@ -28,6 +30,7 @@ remove_default.py:6:8: ICN001 `pandas` should be imported as `pd` | ^^^^^^ ICN001 7 | import seaborn # unconventional | + = help: Alias `pandas` to `pd` remove_default.py:7:8: ICN001 `seaborn` should be imported as `sns` | @@ -38,6 +41,7 @@ remove_default.py:7:8: ICN001 `seaborn` should be imported as `sns` 8 | 9 | import altair as altr # unconventional | + = help: Alias `seaborn` to `sns` remove_default.py:9:18: ICN001 `altair` should be imported as `alt` | @@ -48,6 +52,7 @@ remove_default.py:9:18: ICN001 `altair` should be imported as `alt` 10 | import matplotlib.pyplot as plot # unconventional 11 | import numpy as nmp # not checked | + = help: Alias `altair` to `alt` remove_default.py:10:29: ICN001 `matplotlib.pyplot` should be imported as `plt` | @@ -57,6 +62,7 @@ remove_default.py:10:29: ICN001 `matplotlib.pyplot` should be imported as `plt` 11 | import numpy as nmp # not checked 12 | import pandas as pdas # unconventional | + = help: Alias `matplotlib.pyplot` to `plt` remove_default.py:12:18: ICN001 `pandas` should be imported as `pd` | @@ -66,6 +72,7 @@ remove_default.py:12:18: ICN001 `pandas` should be imported as `pd` | ^^^^ ICN001 13 | import seaborn as sbrn # unconventional | + = help: Alias `pandas` to `pd` remove_default.py:13:19: ICN001 `seaborn` should be imported as `sns` | @@ -76,5 +83,6 @@ remove_default.py:13:19: ICN001 `seaborn` should be imported as `sns` 14 | 15 | import altair as alt # conventional | + = help: Alias `seaborn` to `sns` From a6cf31cc89a0dc4d27fa3a79d612f7f1a0c3459e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 18 Jun 2023 11:57:38 -0400 Subject: [PATCH 103/447] Move `dead_scopes` to `deferred.scopes` (#5171) ## Summary This is more consistent with the rest of the `deferred` patterns. --- crates/ruff/src/checkers/ast/deferred.rs | 3 ++- crates/ruff/src/checkers/ast/mod.rs | 11 +++++++---- crates/ruff_python_semantic/src/model.rs | 3 --- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/ruff/src/checkers/ast/deferred.rs b/crates/ruff/src/checkers/ast/deferred.rs index e72f7dbd46..fdf375f30d 100644 --- a/crates/ruff/src/checkers/ast/deferred.rs +++ b/crates/ruff/src/checkers/ast/deferred.rs @@ -1,13 +1,14 @@ use ruff_text_size::TextRange; use rustpython_parser::ast::Expr; -use ruff_python_semantic::Snapshot; +use ruff_python_semantic::{ScopeId, Snapshot}; /// A collection of AST nodes that are deferred for later analysis. /// Used to, e.g., store functions, whose bodies shouldn't be analyzed until all /// module-level definitions have been analyzed. #[derive(Debug, Default)] pub(crate) struct Deferred<'a> { + pub(crate) scopes: Vec, pub(crate) string_type_definitions: Vec<(TextRange, &'a str, Snapshot)>, pub(crate) future_type_definitions: Vec<(&'a Expr, Snapshot)>, pub(crate) functions: Vec, diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index f6f3f1ab45..67237eb737 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -2038,10 +2038,12 @@ where // Post-visit. match stmt { Stmt::FunctionDef(_) | Stmt::AsyncFunctionDef(_) => { + self.deferred.scopes.push(self.semantic.scope_id); self.semantic.pop_scope(); self.semantic.pop_definition(); } Stmt::ClassDef(ast::StmtClassDef { name, .. }) => { + self.deferred.scopes.push(self.semantic.scope_id); self.semantic.pop_scope(); self.semantic.pop_definition(); self.add_binding( @@ -3785,6 +3787,7 @@ where | Expr::ListComp(_) | Expr::DictComp(_) | Expr::SetComp(_) => { + self.deferred.scopes.push(self.semantic.scope_id); self.semantic.pop_scope(); } _ => {} @@ -4692,7 +4695,7 @@ impl<'a> Checker<'a> { } } - fn check_dead_scopes(&mut self) { + fn check_deferred_scopes(&mut self) { if !self.any_enabled(&[ Rule::UnusedImport, Rule::GlobalVariableNotAssigned, @@ -4771,7 +4774,7 @@ impl<'a> Checker<'a> { }; let mut diagnostics: Vec = vec![]; - for scope_id in self.semantic.dead_scopes.iter().rev() { + for scope_id in self.deferred.scopes.iter().rev() { let scope = &self.semantic.scopes[*scope_id]; if scope.kind.is_module() { @@ -5256,8 +5259,8 @@ pub(crate) fn check_ast( // Reset the scope to module-level, and check all consumed scopes. checker.semantic.scope_id = ScopeId::global(); - checker.semantic.dead_scopes.push(ScopeId::global()); - checker.check_dead_scopes(); + checker.deferred.scopes.push(ScopeId::global()); + checker.check_deferred_scopes(); checker.diagnostics } diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 56778df02e..c69e52f999 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -40,7 +40,6 @@ pub struct SemanticModel<'a> { /// Stack of all scopes, along with the identifier of the current scope. pub scopes: Scopes<'a>, pub scope_id: ScopeId, - pub dead_scopes: Vec, /// Stack of all definitions created in any scope, at any point in execution. pub definitions: Definitions<'a>, @@ -130,7 +129,6 @@ impl<'a> SemanticModel<'a> { exprs: Vec::default(), scopes: Scopes::default(), scope_id: ScopeId::global(), - dead_scopes: Vec::default(), definitions: Definitions::for_module(module), definition_id: DefinitionId::module(), bindings: Bindings::default(), @@ -596,7 +594,6 @@ impl<'a> SemanticModel<'a> { /// Pop the current [`Scope`] off the stack. pub fn pop_scope(&mut self) { - self.dead_scopes.push(self.scope_id); self.scope_id = self.scopes[self.scope_id] .parent .expect("Attempted to pop without scope"); From 2b82caa163989a236110f77ea8ab9f860b7c49a2 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 19 Jun 2023 00:09:02 -0400 Subject: [PATCH 104/447] Detect continuations at start-of-file (#5173) ## Summary Given: ```python \ import os ``` Deleting `import os` leaves a syntax error: a file can't end in a continuation. We have code to handle this case, but it failed to pick up continuations at the _very start_ of a file. Closes #5156. --- crates/ruff_python_ast/src/source_code/indexer.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/ruff_python_ast/src/source_code/indexer.rs b/crates/ruff_python_ast/src/source_code/indexer.rs index 6cfdf693ca..3d10678a50 100644 --- a/crates/ruff_python_ast/src/source_code/indexer.rs +++ b/crates/ruff_python_ast/src/source_code/indexer.rs @@ -49,10 +49,7 @@ impl Indexer { } // Newlines after a newline never form a continuation. - if !matches!( - prev_token, - Some(Tok::Newline | Tok::NonLogicalNewline) | None - ) { + if !matches!(prev_token, Some(Tok::Newline | Tok::NonLogicalNewline)) { continuation_lines.push(line_start); } From be11cae619d5a24adb4da34e64d3c5f270f9727b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 19 Jun 2023 00:19:41 -0400 Subject: [PATCH 105/447] Fix allowed-ellipsis detection (#5174) ## Summary We weren't resetting the `allow_ellipsis` flag properly, which ultimately caused us to treat the semicolon as "unnecessary" rather than "creating a multi-statement line". Closes #5154. --- .../resources/test/fixtures/pycodestyle/E70.py | 3 +++ .../rules/pycodestyle/rules/compound_statements.rs | 14 +++++++++----- ...ff__rules__pycodestyle__tests__E702_E70.py.snap | 8 ++++++++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pycodestyle/E70.py b/crates/ruff/resources/test/fixtures/pycodestyle/E70.py index bfbec79124..2fa6fa4813 100644 --- a/crates/ruff/resources/test/fixtures/pycodestyle/E70.py +++ b/crates/ruff/resources/test/fixtures/pycodestyle/E70.py @@ -60,3 +60,6 @@ match *0, 1, *2: #: class Foo: match: Optional[Match] = None +#: E702:2:4 +while 1: + 1;... diff --git a/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs b/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs index 6f095d8272..fd9b57b6e9 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs @@ -145,6 +145,12 @@ pub(crate) fn compound_statements(lxr: &[LexResult], settings: &Settings) -> Vec Tok::Rbrace => { brace_count = brace_count.saturating_sub(1); } + Tok::Ellipsis => { + if allow_ellipsis { + allow_ellipsis = false; + continue; + } + } _ => {} } @@ -195,17 +201,15 @@ pub(crate) fn compound_statements(lxr: &[LexResult], settings: &Settings) -> Vec || with.is_some() { colon = Some((range.start(), range.end())); - allow_ellipsis = true; + + // Allow `class C: ...`-style definitions in stubs. + allow_ellipsis = class.is_some(); } } Tok::Semi => { semi = Some((range.start(), range.end())); } Tok::Comment(..) | Tok::Indent | Tok::Dedent | Tok::NonLogicalNewline => {} - Tok::Ellipsis if allow_ellipsis => { - // Allow `class C: ...`-style definitions in stubs. - allow_ellipsis = false; - } _ => { if let Some((start, end)) = semi { diagnostics.push(Diagnostic::new( diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E702_E70.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E702_E70.py.snap index 5bca9b1e53..20d62cd44b 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E702_E70.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E702_E70.py.snap @@ -61,4 +61,12 @@ E70.py:56:13: E702 Multiple statements on one line (semicolon) 58 | match *0, 1, *2: | +E70.py:65:4: E702 Multiple statements on one line (semicolon) + | +63 | #: E702:2:4 +64 | while 1: +65 | 1;... + | ^ E702 + | + From 361d45f2b2f7e69d37d4e6d8270e448f72cae9a7 Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 19 Jun 2023 11:40:09 +0200 Subject: [PATCH 106/447] Add `cargo dev repeat` for profiling (#5144) ## Summary This adds a new subcommand that can be used as ```shell cargo build --bin ruff_dev --profile=release-debug perf record -g -F 999 target/release-debug/ruff_dev repeat --repeat 30 --exit-zero --no-cache path/to/cpython > /dev/null flamegraph --perfdata perf.data ``` ## Test Plan This is a ruff internal script. I successfully used it to profile cpython with the instructions above --- .gitignore | 5 +++++ crates/ruff_cli/src/args.rs | 2 +- crates/ruff_cli/src/lib.rs | 2 +- crates/ruff_dev/src/main.rs | 24 ++++++++++++++++++++++++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 55de3ba557..562d1b4c5a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,11 @@ github_search*.jsonl schemastore .venv* scratch.py +perf.data +perf.data.old +flamegraph.svg +# Additional target directories that don't invalidate the main compile cache when changing linker settings +/target* ### # Rust.gitignore diff --git a/crates/ruff_cli/src/args.rs b/crates/ruff_cli/src/args.rs index 09662c8272..eb3efc09a5 100644 --- a/crates/ruff_cli/src/args.rs +++ b/crates/ruff_cli/src/args.rs @@ -68,7 +68,7 @@ pub enum Command { }, } -#[derive(Debug, clap::Args)] +#[derive(Clone, Debug, clap::Args)] #[allow(clippy::struct_excessive_bools, clippy::module_name_repetitions)] pub struct CheckArgs { /// List of files or directories to check. diff --git a/crates/ruff_cli/src/lib.rs b/crates/ruff_cli/src/lib.rs index a9b53365fb..4307f74290 100644 --- a/crates/ruff_cli/src/lib.rs +++ b/crates/ruff_cli/src/lib.rs @@ -159,7 +159,7 @@ fn format(files: &[PathBuf]) -> Result { Ok(ExitStatus::Success) } -fn check(args: CheckArgs, log_level: LogLevel) -> Result { +pub fn check(args: CheckArgs, log_level: LogLevel) -> Result { let (cli, overrides) = args.partition(); // Construct the "default" settings. These are used when no `pyproject.toml` diff --git a/crates/ruff_dev/src/main.rs b/crates/ruff_dev/src/main.rs index c8beed5fb1..dbdfb45466 100644 --- a/crates/ruff_dev/src/main.rs +++ b/crates/ruff_dev/src/main.rs @@ -4,6 +4,8 @@ use anyhow::Result; use clap::{Parser, Subcommand}; +use ruff::logging::{set_up_logging, LogLevel}; +use ruff_cli::check; mod generate_all; mod generate_cli_help; @@ -27,6 +29,7 @@ struct Args { } #[derive(Subcommand)] +#[allow(clippy::large_enum_variant)] enum Command { /// Run all code and documentation generation steps. GenerateAll(generate_all::Args), @@ -48,6 +51,16 @@ enum Command { PrintTokens(print_tokens::Args), /// Run round-trip source code generation on a given Python file. RoundTrip(round_trip::Args), + /// Run a ruff command n times for profiling/benchmarking + Repeat { + #[clap(flatten)] + args: ruff_cli::args::CheckArgs, + #[clap(flatten)] + log_level_args: ruff_cli::args::LogLevelArgs, + /// Run this many times + #[clap(long, short = 'n')] + repeat: usize, + }, } fn main() -> Result<()> { @@ -64,6 +77,17 @@ fn main() -> Result<()> { Command::PrintCST(args) => print_cst::main(args)?, Command::PrintTokens(args) => print_tokens::main(args)?, Command::RoundTrip(args) => round_trip::main(args)?, + Command::Repeat { + args, + repeat, + log_level_args, + } => { + let log_level = LogLevel::from(log_level_args); + set_up_logging(&log_level)?; + for _ in 0..*repeat { + check(args.clone(), log_level)?; + } + } } Ok(()) } From 0e028142f46935c1e8d63a4cb0d575158835ae94 Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 19 Jun 2023 14:24:45 +0200 Subject: [PATCH 107/447] Explain dangling comments in the formatter (#5170) This documentation change improves the section on dangling comments in the formatter. --------- Co-authored-by: David Szotten Co-authored-by: Micha Reiser --- crates/ruff_python_formatter/README.md | 106 ++++++++++++++++++++++--- 1 file changed, 97 insertions(+), 9 deletions(-) diff --git a/crates/ruff_python_formatter/README.md b/crates/ruff_python_formatter/README.md index 8d693a3aab..0dde0b475f 100644 --- a/crates/ruff_python_formatter/README.md +++ b/crates/ruff_python_formatter/README.md @@ -94,18 +94,32 @@ write!(f, [if_group_breaks(&text(","))]) If you need avoid second mutable borrows with a builder, you can use `format_with(|f| { ... })` as a formattable element similar to `text()` or `group()`. -The generic comment formatting in `FormatNodeRule` handles comments correctly for most nodes, e.g. -preceding and end-of-line comments depending on the node range. Sometimes however, you may have -dangling comments that are not before or after a node but inside of it, e.g. +## Comments + +Comments can either be own line or end-of-line and can be marked as `Leading`, `Trailing` and `Dangling`. + +```python +# Leading comment (always own line) +print("hello world") # Trailing comment (end-of-line) +# Trailing comment (own line) +``` + +Comments are automatically attached as `Leading` or `Trailing` to a node close to them, or `Dangling` +if there are only tokens and no nodes surrounding it. Categorization is automatic but sometimes +needs to be overridden in +[`place_comment`](https://github.com/astral-sh/ruff/blob/be11cae619d5a24adb4da34e64d3c5f270f9727b/crates/ruff_python_formatter/src/comments/placement.rs#L13) +in `placement.rs`, which this section is about. ```Python [ - # here we use an empty list + # This needs to be handled as a dangling comment ] ``` -Here, you have to call `dangling_comments` manually and stubbing out `fmt_dangling_comments` in list -formatting. +Here, the comment is dangling because it is preceded by `[`, which is a non-trivia token but not a +node, and followed by `]`, which is also a non-trivia token but not a node. In the `FormatExprList` +implementation, we have to call `dangling_comments` manually and stub out the +`fmt_dangling_comments` default from `FormatNodeRule`. ```rust impl FormatNodeRule for FormatExprList { @@ -116,7 +130,7 @@ impl FormatNodeRule for FormatExprList { f, [group(&format_args![ text("["), - dangling_comments(dangling), + dangling_comments(dangling), // Gets all the comments marked as dangling for the node soft_block_indent(&items), text("]") ])] @@ -130,8 +144,82 @@ impl FormatNodeRule for FormatExprList { } ``` -Comments are categorized into `Leading`, `Trailing` and `Dangling`, you can override this in -`place_comment`. +A related common challenge is that we want to attach comments to tokens (think keywords and +syntactically meaningful characters such as `:`) that have no node on their own. A slightly +simplified version of the `while` node in our AST looks like the following: + +```rust +pub struct StmtWhile { + pub range: TextRange, + pub test: Box>, + pub body: Vec>, + pub orelse: Vec>, +} +``` + +That means in + +```python +while True: # Trailing condition comment + if f(): + break + # trailing while comment +# leading else comment +else: + print("while-else") +``` + +the `else` has no node, we're just getting the statements in its body. + +The preceding token of the leading else comment is the `break`, which has a node, the following +token is the `else`, which lacks a node, so by default the comment would be marked as trailing +the `break` and wrongly formatted as such. We can identify these cases by looking for comments +between two bodies that have the same indentation level as the keyword, e.g. in our case the +leading else comment is inside the `while` node (which spans the entire snippet) and on the same +level as the `else`. We identify those case in +[`handle_in_between_bodies_own_line_comment`](https://github.com/astral-sh/ruff/blob/be11cae619d5a24adb4da34e64d3c5f270f9727b/crates/ruff_python_formatter/src/comments/placement.rs#L196) +and mark them as dangling for manual formatting later. Similarly, we find and mark comment after +the colon(s) in +[`handle_trailing_end_of_line_condition_comment`](https://github.com/astral-sh/ruff/blob/main/crates/ruff_python_formatter/src/comments/placement.rs#L518) +. + +The comments don't carry any extra information such as why we marked the comment as trailing, +instead they are sorted into one list of leading, one list of trailing and one list of dangling +comments per node. In `FormatStmtWhile`, we can have multiple types of dangling comments, so we +have to split the dangling list into after-colon-comments, before-else-comments, etc. by some +element separating them (e.g. all comments trailing the colon come before the first statement in +the body) and manually insert them in the right position. + +A simplified implementation with only those two kinds of comments: + +```rust +fn fmt_fields(&self, item: &StmtWhile, f: &mut PyFormatter) -> FormatResult<()> { + + // ... + + // See FormatStmtWhile for the real, more complex implementation + let first_while_body_stmt = item.body.first().unwrap().end(); + let trailing_condition_comments_end = + dangling_comments.partition_point(|comment| comment.slice().end() < first_while_body_stmt); + let (trailing_condition_comments, or_else_comments) = + dangling_comments.split_at(trailing_condition_comments_end); + + write!( + f, + [ + text("while"), + space(), + test.format(), + text(":"), + trailing_comments(trailing_condition_comments), + block_indent(&body.format()) + leading_comments(or_else_comments), + text("else:"), + block_indent(&orelse.format()) + ] + )?; +} +``` ## Development notes From b8d378b0a3f4d3c5e372447abbdd2bc79e06853f Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 19 Jun 2023 16:13:38 +0200 Subject: [PATCH 108/447] Add a script that tests formatter stability on repositories (#5055) ## Summary We want to ensure that once formatted content stays the same when formatted again, which is known as formatter stability or formatter idempotency, and that the formatter prints syntactically valid code. As our test cases cover only a limited amount of code, this allows checking entire repositories. This adds a new subcommand to `ruff_dev` which can be invoked as `cargo run --bin ruff_dev -- check-formatter-stability `. While initially only intended to check stability, it has also found cases where the formatter printed invalid syntax or panicked. ## Test Plan Running this on cpython is already identifying bugs (https://github.com/astral-sh/ruff/pull/5089) --- Cargo.lock | 3 + Cargo.toml | 2 +- crates/ruff/Cargo.toml | 2 +- crates/ruff_cli/src/lib.rs | 2 +- crates/ruff_cli/src/resolve.rs | 2 +- crates/ruff_dev/Cargo.toml | 3 + .../ruff_dev/src/check_formatter_stability.rs | 277 ++++++++++++++++++ crates/ruff_dev/src/main.rs | 35 ++- 8 files changed, 309 insertions(+), 17 deletions(-) create mode 100644 crates/ruff_dev/src/check_formatter_stability.rs diff --git a/Cargo.lock b/Cargo.lock index 8ffd30015d..856f7c9fea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1942,17 +1942,20 @@ dependencies = [ "clap", "itertools", "libcst", + "log", "once_cell", "pretty_assertions", "regex", "ruff", "ruff_cli", "ruff_diagnostics", + "ruff_python_formatter", "ruff_textwrap", "rustpython-format", "rustpython-parser", "schemars", "serde_json", + "similar", "strum", "strum_macros", ] diff --git a/Cargo.toml b/Cargo.toml index 29941786c9..3051a7e693 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ schemars = { version = "0.8.12" } serde = { version = "1.0.152", features = ["derive"] } serde_json = { version = "1.0.93", features = ["preserve_order"] } shellexpand = { version = "3.0.0" } -similar = { version = "2.2.1" } +similar = { version = "2.2.1", features = ["inline"] } smallvec = { version = "1.10.0" } strum = { version = "0.24.1", features = ["strum_macros"] } strum_macros = { version = "0.24.3" } diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 306a689ae6..cfea318ddc 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -66,7 +66,7 @@ semver = { version = "1.0.16" } serde = { workspace = true } serde_json = { workspace = true } serde_with = { version = "3.0.0" } -similar = { workspace = true, features = ["inline"] } +similar = { workspace = true } shellexpand = { workspace = true } smallvec = { workspace = true } strum = { workspace = true } diff --git a/crates/ruff_cli/src/lib.rs b/crates/ruff_cli/src/lib.rs index 4307f74290..01b3e229e1 100644 --- a/crates/ruff_cli/src/lib.rs +++ b/crates/ruff_cli/src/lib.rs @@ -24,7 +24,7 @@ mod commands; mod diagnostics; mod panic; mod printer; -mod resolve; +pub mod resolve; #[derive(Copy, Clone)] pub enum ExitStatus { diff --git a/crates/ruff_cli/src/resolve.rs b/crates/ruff_cli/src/resolve.rs index 7952d70158..0bf5db6670 100644 --- a/crates/ruff_cli/src/resolve.rs +++ b/crates/ruff_cli/src/resolve.rs @@ -14,7 +14,7 @@ use crate::args::Overrides; /// Resolve the relevant settings strategy and defaults for the current /// invocation. -pub(crate) fn resolve( +pub fn resolve( isolated: bool, config: Option<&Path>, overrides: &Overrides, diff --git a/crates/ruff_dev/Cargo.toml b/crates/ruff_dev/Cargo.toml index 376bb09b8e..fa8ee5d7c5 100644 --- a/crates/ruff_dev/Cargo.toml +++ b/crates/ruff_dev/Cargo.toml @@ -14,12 +14,14 @@ license = { workspace = true } ruff = { path = "../ruff", features = ["schemars"] } ruff_cli = { path = "../ruff_cli" } ruff_diagnostics = { path = "../ruff_diagnostics" } +ruff_python_formatter = { path = "../ruff_python_formatter" } ruff_textwrap = { path = "../ruff_textwrap" } anyhow = { workspace = true } clap = { workspace = true } itertools = { workspace = true } libcst = { workspace = true } +log = { workspace = true } once_cell = { workspace = true } pretty_assertions = { version = "1.3.0" } regex = { workspace = true } @@ -27,5 +29,6 @@ rustpython-format = { workspace = true } rustpython-parser = { workspace = true } schemars = { workspace = true } serde_json = { workspace = true } +similar = { workspace = true } strum = { workspace = true } strum_macros = { workspace = true } diff --git a/crates/ruff_dev/src/check_formatter_stability.rs b/crates/ruff_dev/src/check_formatter_stability.rs new file mode 100644 index 0000000000..26b1d68902 --- /dev/null +++ b/crates/ruff_dev/src/check_formatter_stability.rs @@ -0,0 +1,277 @@ +//! We want to ensure that once formatted content stays the same when formatted again, which is +//! known as formatter stability or formatter idempotency, and that the formatter prints +//! syntactically valid code. As our test cases cover only a limited amount of code, this allows +//! checking entire repositories. +#![allow(clippy::print_stdout)] + +use anyhow::Context; +use clap::Parser; +use log::debug; +use ruff::resolver::python_files_in_path; +use ruff::settings::types::{FilePattern, FilePatternSet}; +use ruff_cli::args::CheckArgs; +use ruff_cli::resolve::resolve; +use ruff_python_formatter::format_module; +use similar::{ChangeTag, TextDiff}; +use std::io::Write; +use std::panic::catch_unwind; +use std::path::{Path, PathBuf}; +use std::process::ExitCode; +use std::time::Instant; +use std::{fs, io, iter}; + +/// Control the verbosity of the output +#[derive(Copy, Clone, PartialEq, Eq, clap::ValueEnum, Default)] +pub(crate) enum Format { + /// Filenames only + Minimal, + /// Filenames and reduced diff + #[default] + Default, + /// Full diff and invalid code + Full, +} + +#[derive(clap::Args)] +pub(crate) struct Args { + /// Like `ruff check`'s files + pub(crate) files: Vec, + /// Control the verbosity of the output + #[arg(long, default_value_t, value_enum)] + pub(crate) format: Format, + /// Print only the first error and exit, `-x` is same as pytest + #[arg(long, short = 'x')] + pub(crate) exit_first_error: bool, +} + +/// Generate ourself a `try_parse_from` impl for `CheckArgs`. This is a strange way to use clap but +/// we want the same behaviour as `ruff_cli` and clap seems to lack a way to parse directly to +/// `Args` instead of a `Parser` +#[derive(Debug, clap::Parser)] +struct WrapperArgs { + #[clap(flatten)] + check_args: CheckArgs, +} + +pub(crate) fn main(args: &Args) -> anyhow::Result { + let start = Instant::now(); + + // Find files to check (or in this case, format twice). Adapted from ruff_cli + // First argument is ignored + let dummy = PathBuf::from("check"); + let check_args_input = iter::once(&dummy).chain(&args.files); + let check_args: CheckArgs = WrapperArgs::try_parse_from(check_args_input)?.check_args; + let (cli, overrides) = check_args.partition(); + let mut pyproject_config = resolve( + cli.isolated, + cli.config.as_deref(), + &overrides, + cli.stdin_filename.as_deref(), + )?; + // We don't want to format pyproject.toml + pyproject_config.settings.lib.include = FilePatternSet::try_from_vec(vec![ + FilePattern::Builtin("*.py"), + FilePattern::Builtin("*.pyi"), + ]) + .unwrap(); + let (paths, _resolver) = python_files_in_path(&cli.files, &pyproject_config, &overrides)?; + assert!(!paths.is_empty(), "no python files in {:?}", cli.files); + + let errors = paths + .into_iter() + .map(|dir_entry| { + // Doesn't make sense to recover here in this test script + let file = dir_entry + .expect("Iterating the files in the repository failed") + .into_path(); + // Handle panics (mostly in `debug_assert!`) + let result = match catch_unwind(|| check_file(&file)) { + Ok(result) => result, + Err(panic) => { + if let Ok(message) = panic.downcast::() { + Err(FormatterStabilityError::Panic { message: *message }) + } else { + Err(FormatterStabilityError::Panic { + // This should not happen, but it can + message: "(Panic didn't set a string message)".to_string(), + }) + } + } + }; + (result, file) + }) + // We only care about the errors + .filter_map(|(result, file)| match result { + Err(err) => Some((err, file)), + Ok(()) => None, + }); + + let mut any_errors = false; + + // Don't collect the iterator so we already see errors while it's still processing + for (error, file) in errors { + any_errors = true; + match error { + FormatterStabilityError::Unstable { + formatted, + reformatted, + } => { + println!("Unstable formatting {}", file.display()); + match args.format { + Format::Minimal => {} + Format::Default => { + diff_show_only_changes( + io::stdout().lock().by_ref(), + &formatted, + &reformatted, + )?; + } + Format::Full => { + let diff = TextDiff::from_lines(&formatted, &reformatted) + .unified_diff() + .header("Formatted once", "Formatted twice") + .to_string(); + println!( + r#"Reformatting the formatted code a second time resulted in formatting changes. +--- +{diff}--- + +Formatted once: +--- +{formatted}--- + +Formatted twice: +--- +{reformatted}---"#, + ); + } + } + } + FormatterStabilityError::InvalidSyntax { err, formatted } => { + println!( + "Formatter generated invalid syntax {}: {}", + file.display(), + err + ); + if args.format == Format::Full { + println!("---\n{formatted}\n---\n"); + } + } + FormatterStabilityError::Panic { message } => { + println!("Panic {}: {}", file.display(), message); + } + FormatterStabilityError::Other(err) => { + println!("Uncategorized error {}: {}", file.display(), err); + } + } + + if args.exit_first_error { + return Ok(ExitCode::FAILURE); + } + } + let duration = start.elapsed(); + println!( + "Formatting {} files twice took {:.2}s", + cli.files.len(), + duration.as_secs_f32() + ); + + if any_errors { + Ok(ExitCode::FAILURE) + } else { + Ok(ExitCode::SUCCESS) + } +} + +/// A compact diff that only shows a header and changes, but nothing unchanged. This makes viewing +/// multiple errors easier. +fn diff_show_only_changes( + writer: &mut impl Write, + formatted: &str, + reformatted: &str, +) -> io::Result<()> { + for changes in TextDiff::from_lines(formatted, reformatted) + .unified_diff() + .iter_hunks() + { + for (idx, change) in changes + .iter_changes() + .filter(|change| change.tag() != ChangeTag::Equal) + .enumerate() + { + if idx == 0 { + writeln!(writer, "{}", changes.header())?; + } + write!(writer, "{}", change.tag())?; + writer.write_all(change.value().as_bytes())?; + } + } + Ok(()) +} + +#[derive(Debug)] +enum FormatterStabilityError { + /// First and second pass of the formatter are different + Unstable { + formatted: String, + reformatted: String, + }, + /// The formatter printed invalid code + InvalidSyntax { + err: anyhow::Error, + formatted: String, + }, + /// From `catch_unwind` + Panic { + message: String, + }, + Other(anyhow::Error), +} + +impl From for FormatterStabilityError { + fn from(error: anyhow::Error) -> Self { + Self::Other(error) + } +} + +/// Run the formatter twice on the given file. Does not write back to the file +fn check_file(input_path: &Path) -> Result<(), FormatterStabilityError> { + let content = fs::read_to_string(input_path).context("Failed to read file")?; + let printed = match format_module(&content) { + Ok(printed) => printed, + Err(err) => { + return if err + .to_string() + .starts_with("Source contains syntax errors ") + { + debug!( + "Skipping {} with invalid first pass {}", + input_path.display(), + err + ); + Ok(()) + } else { + Err(err.into()) + }; + } + }; + let formatted = printed.as_code(); + + let reformatted = match format_module(formatted) { + Ok(reformatted) => reformatted, + Err(err) => { + return Err(FormatterStabilityError::InvalidSyntax { + err, + formatted: formatted.to_string(), + }); + } + }; + + if reformatted.as_code() != formatted { + return Err(FormatterStabilityError::Unstable { + formatted: formatted.to_string(), + reformatted: reformatted.into_code(), + }); + } + Ok(()) +} diff --git a/crates/ruff_dev/src/main.rs b/crates/ruff_dev/src/main.rs index dbdfb45466..0327211a20 100644 --- a/crates/ruff_dev/src/main.rs +++ b/crates/ruff_dev/src/main.rs @@ -6,7 +6,9 @@ use anyhow::Result; use clap::{Parser, Subcommand}; use ruff::logging::{set_up_logging, LogLevel}; use ruff_cli::check; +use std::process::ExitCode; +mod check_formatter_stability; mod generate_all; mod generate_cli_help; mod generate_docs; @@ -61,33 +63,40 @@ enum Command { #[clap(long, short = 'n')] repeat: usize, }, + /// Format a repository twice and ensure that it looks that the first and second formatting + /// look the same. Same arguments as `ruff check` + CheckFormatterStability(check_formatter_stability::Args), } -fn main() -> Result<()> { +fn main() -> Result { let args = Args::parse(); #[allow(clippy::print_stdout)] - match &args.command { - Command::GenerateAll(args) => generate_all::main(args)?, - Command::GenerateJSONSchema(args) => generate_json_schema::main(args)?, + match args.command { + Command::GenerateAll(args) => generate_all::main(&args)?, + Command::GenerateJSONSchema(args) => generate_json_schema::main(&args)?, Command::GenerateRulesTable => println!("{}", generate_rules_table::generate()), Command::GenerateOptions => println!("{}", generate_options::generate()), - Command::GenerateCliHelp(args) => generate_cli_help::main(args)?, - Command::GenerateDocs(args) => generate_docs::main(args)?, - Command::PrintAST(args) => print_ast::main(args)?, - Command::PrintCST(args) => print_cst::main(args)?, - Command::PrintTokens(args) => print_tokens::main(args)?, - Command::RoundTrip(args) => round_trip::main(args)?, + Command::GenerateCliHelp(args) => generate_cli_help::main(&args)?, + Command::GenerateDocs(args) => generate_docs::main(&args)?, + Command::PrintAST(args) => print_ast::main(&args)?, + Command::PrintCST(args) => print_cst::main(&args)?, + Command::PrintTokens(args) => print_tokens::main(&args)?, + Command::RoundTrip(args) => round_trip::main(&args)?, Command::Repeat { args, repeat, log_level_args, } => { - let log_level = LogLevel::from(log_level_args); + let log_level = LogLevel::from(&log_level_args); set_up_logging(&log_level)?; - for _ in 0..*repeat { + for _ in 0..repeat { check(args.clone(), log_level)?; } } + Command::CheckFormatterStability(args) => { + let exit_code = check_formatter_stability::main(&args)?; + return Ok(exit_code); + } } - Ok(()) + Ok(ExitCode::SUCCESS) } From e3c12764f82124264cf6886004f89c7bc49a568a Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Mon, 19 Jun 2023 17:46:13 +0200 Subject: [PATCH 109/447] Only use a single cache file per Python package (#5117) ## Summary This changes the caching design from one cache file per source file, to one cache file per package. This greatly reduces the amount of cache files that are opened and written, while maintaining roughly the same (combined) size as bincode is very compact. Below are some very much not scientific performance tests. It uses projects/sources to check: * small.py: single, 31 bytes Python file with 2 errors. * test.py: single, 43k Python file with 8 errors. * fastapi: FastAPI repo, 1134 files checked, 0 errors. Source | Before # files | After # files | Before size | After size -------|-------|-------|-------|------- small.py | 1 | 1 | 20 K | 20 K test.py | 1 | 1 | 60 K | 60 K fastapi | 1134 | 518 | 4.5 M | 2.3 M One question that might come up is why fastapi still has 518 cache files and not 1? That is because this is using the existing package resolution, which sees examples, docs, etc. as separate from the "main" source code (in the fastapi directory in the repo). In this future it might be worth consider switching to a one cache file per repo strategy. This new design is not perfect and does have a number of known issues. First, like the old design it doesn't remove the cache for a source file that has been (re)moved until `ruff clean` is called. Second, this currently uses a large mutex around the mutation of the package cache (e.g. inserting result). This could be (or become) a bottleneck. It's future work to test and improve this (if needed). Third, currently the packages and opened and stored in a sequential loop, this could be done parallel. This is also future work. ## Test Plan Run `ruff check` (with caching enabled) twice on any Python source code and it should produce the same results. --- Cargo.lock | 1 + crates/ruff_cache/src/lib.rs | 3 +- crates/ruff_cli/Cargo.toml | 1 + crates/ruff_cli/src/cache.rs | 422 +++++++++--------- crates/ruff_cli/src/commands/run.rs | 60 ++- crates/ruff_cli/src/diagnostics.rs | 45 +- crates/ruff_python_ast/src/source_code/mod.rs | 2 +- 7 files changed, 285 insertions(+), 249 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 856f7c9fea..7b248b608e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1908,6 +1908,7 @@ dependencies = [ "glob", "ignore", "itertools", + "itoa", "log", "mimalloc", "notify", diff --git a/crates/ruff_cache/src/lib.rs b/crates/ruff_cache/src/lib.rs index 3d17f88336..c07c415134 100644 --- a/crates/ruff_cache/src/lib.rs +++ b/crates/ruff_cache/src/lib.rs @@ -8,8 +8,7 @@ pub mod globset; pub const CACHE_DIR_NAME: &str = ".ruff_cache"; -/// Return the cache directory for a given project root. Defers to the -/// `RUFF_CACHE_DIR` environment variable, if set. +/// Return the cache directory for a given project root. pub fn cache_dir(project_root: &Path) -> PathBuf { project_root.join(CACHE_DIR_NAME) } diff --git a/crates/ruff_cli/Cargo.toml b/crates/ruff_cli/Cargo.toml index 0cdd5b765f..166e4676c6 100644 --- a/crates/ruff_cli/Cargo.toml +++ b/crates/ruff_cli/Cargo.toml @@ -47,6 +47,7 @@ filetime = { workspace = true } glob = { workspace = true } ignore = { workspace = true } itertools = { workspace = true } +itoa = { version = "1.0.6" } log = { workspace = true } notify = { version = "5.1.0" } path-absolutize = { workspace = true, features = ["once_cell_cache"] } diff --git a/crates/ruff_cli/src/cache.rs b/crates/ruff_cli/src/cache.rs index 0147d9ccb5..d52ade04ec 100644 --- a/crates/ruff_cli/src/cache.rs +++ b/crates/ruff_cli/src/cache.rs @@ -1,171 +1,247 @@ -use std::cell::RefCell; -use std::fs; +use std::collections::HashMap; +use std::fs::{self, File}; use std::hash::Hasher; -use std::io::Write; -#[cfg(unix)] -use std::os::unix::fs::PermissionsExt; -use std::path::Path; +use std::io::{self, BufReader, BufWriter, Write}; +use std::path::{Path, PathBuf}; +use std::sync::Mutex; +use std::time::SystemTime; -use anyhow::Result; -use filetime::FileTime; -use log::error; -use path_absolutize::Absolutize; -use ruff_text_size::{TextRange, TextSize}; -use serde::ser::{SerializeSeq, SerializeStruct}; -use serde::{Deserialize, Serialize, Serializer}; +use anyhow::{anyhow, Context, Result}; +use serde::{Deserialize, Serialize}; use ruff::message::Message; -use ruff::settings::{AllSettings, Settings}; +use ruff::settings::Settings; use ruff_cache::{CacheKey, CacheKeyHasher}; use ruff_diagnostics::{DiagnosticKind, Fix}; use ruff_python_ast::imports::ImportMap; use ruff_python_ast::source_code::SourceFileBuilder; +use ruff_text_size::{TextRange, TextSize}; -const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); +use crate::diagnostics::Diagnostics; -/// Vec storing all source files. The tuple is (filename, source code). -type Files<'a> = Vec<(&'a str, &'a str)>; -type FilesBuf = Vec<(String, String)>; - -struct CheckResultRef<'a> { - imports: &'a ImportMap, - messages: &'a [Message], +/// On disk representation of a cache of a package. +#[derive(Deserialize, Debug, Serialize)] +pub(crate) struct PackageCache { + /// Location of the cache. + /// + /// Not stored on disk, just used as a storage location. + #[serde(skip)] + path: PathBuf, + /// Path to the root of the package. + /// + /// Usually this is a directory, but it can also be a single file in case of + /// single file "packages", e.g. scripts. + package_root: PathBuf, + /// Mapping of source file path to it's cached data. + // TODO: look into concurrent hashmap or similar instead of a mutex. + files: Mutex>, } -impl Serialize for CheckResultRef<'_> { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - let mut s = serializer.serialize_struct("CheckResultRef", 3)?; +impl PackageCache { + /// Open or create a new package cache. + /// + /// `package_root` must be canonicalized. + pub(crate) fn open( + cache_dir: &Path, + package_root: PathBuf, + settings: &Settings, + ) -> Result { + debug_assert!(package_root.is_absolute(), "package root not canonicalized"); - s.serialize_field("imports", &self.imports)?; + let mut buf = itoa::Buffer::new(); + let key = Path::new(buf.format(cache_key(&package_root, settings))); + let path = PathBuf::from_iter([cache_dir, Path::new("content"), key]); - let serialize_messages = SerializeMessages { - messages: self.messages, - files: RefCell::default(), + let file = match File::open(&path) { + Ok(file) => file, + Err(err) if err.kind() == io::ErrorKind::NotFound => { + // No cache exist yet, return an empty cache. + return Ok(PackageCache { + path, + package_root, + files: Mutex::new(HashMap::new()), + }); + } + Err(err) => { + return Err(err) + .with_context(|| format!("Failed to open cache file '{}'", path.display()))? + } }; - s.serialize_field("messages", &serialize_messages)?; + let mut cache: PackageCache = bincode::deserialize_from(BufReader::new(file)) + .with_context(|| format!("Failed parse cache file '{}'", path.display()))?; - let files = serialize_messages.files.take(); - - s.serialize_field("files", &files)?; - - s.end() - } -} - -struct SerializeMessages<'a> { - messages: &'a [Message], - files: RefCell>, -} - -impl Serialize for SerializeMessages<'_> { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - let mut s = serializer.serialize_seq(Some(self.messages.len()))?; - let mut files = self.files.borrow_mut(); - - for message in self.messages { - // Using a Vec instead of a HashMap because the cache is per file and the large majority of - // files have exactly one source file. - let file_id = if let Some(position) = files - .iter() - .position(|(filename, _)| *filename == message.filename()) - { - position - } else { - let index = files.len(); - files.push((message.filename(), message.file.source_text())); - index - }; - - s.serialize_element(&SerializeMessage { message, file_id })?; + // Sanity check. + if cache.package_root != package_root { + return Err(anyhow!( + "Different package root in cache: expected '{}', got '{}'", + package_root.display(), + cache.package_root.display(), + )); } - s.end() + cache.path = path; + Ok(cache) + } + + /// Store the cache to disk. + pub(crate) fn store(&self) -> Result<()> { + let file = File::create(&self.path) + .with_context(|| format!("Failed to create cache file '{}'", self.path.display()))?; + let writer = BufWriter::new(file); + bincode::serialize_into(writer, &self).with_context(|| { + format!( + "Failed to serialise cache to file '{}'", + self.path.display() + ) + }) + } + + /// Returns the relative path based on `path` and the package root. + /// + /// Returns `None` if `path` is not within the package. + pub(crate) fn relative_path<'a>(&self, path: &'a Path) -> Option<&'a RelativePath> { + path.strip_prefix(&self.package_root).ok() + } + + /// Get the cached results for a single file at relative `path`. This uses + /// `file_last_modified` to determine if the results are still accurate + /// (i.e. if the file hasn't been modified since the cached run). + /// + /// This returns `None` if `file_last_modified` differs from the cached + /// timestamp or if the cache doesn't contain results for the file. + pub(crate) fn get( + &self, + path: &RelativePath, + file_last_modified: SystemTime, + ) -> Option { + let files = self.files.lock().unwrap(); + let file = files.get(path)?; + + // Make sure the file hasn't changed since the cached run. + if file.last_modified != file_last_modified { + return None; + } + + Some(file.clone()) + } + + /// Add or update a file cache at `path` relative to the package root. + pub(crate) fn update(&self, path: RelativePathBuf, file: FileCache) { + self.files.lock().unwrap().insert(path, file); + } + + /// Remove a file cache at `path` relative to the package root. + pub(crate) fn remove(&self, path: &RelativePath) { + self.files.lock().unwrap().remove(path); } } -struct SerializeMessage<'a> { - message: &'a Message, - file_id: usize, +/// [`Path`] that is relative to the package root in [`PackageCache`]. +pub(crate) type RelativePath = Path; +/// [`PathBuf`] that is relative to the package root in [`PackageCache`]. +pub(crate) type RelativePathBuf = PathBuf; + +/// On disk representation of the cache per source file. +#[derive(Clone, Deserialize, Debug, Serialize)] +pub(crate) struct FileCache { + /// Timestamp when the file was last modified before the (cached) check. + last_modified: SystemTime, + /// Imports made. + imports: ImportMap, + /// Diagnostic messages. + messages: Vec, + /// Source code of the file. + /// + /// # Notes + /// + /// This will be empty if `messages` is empty. + source: String, } -impl Serialize for SerializeMessage<'_> { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let Message { - kind, - range, - fix, - // Serialized manually for all files - file: _, - noqa_offset: noqa_row, - } = self.message; +impl FileCache { + /// Create a new source file cache. + pub(crate) fn new( + last_modified: SystemTime, + messages: &[Message], + imports: &ImportMap, + ) -> FileCache { + let source = if let Some(msg) = messages.first() { + msg.file.source_text().to_owned() + } else { + String::new() // No messages, no need to keep the source! + }; - let mut s = serializer.serialize_struct("Message", 5)?; + let messages = messages + .iter() + .map(|msg| { + // Make sure that all message use the same source file. + assert!( + msg.file == messages.first().unwrap().file, + "message uses a different source file" + ); + CacheMessage { + kind: msg.kind.clone(), + range: msg.range, + fix: msg.fix.clone(), + noqa_offset: msg.noqa_offset, + } + }) + .collect(); - s.serialize_field("kind", &kind)?; - s.serialize_field("range", &range)?; - s.serialize_field("fix", &fix)?; - s.serialize_field("file_id", &self.file_id)?; - s.serialize_field("noqa_row", &noqa_row)?; + FileCache { + last_modified, + imports: imports.clone(), + messages, + source, + } + } - s.end() + /// Convert the file cache into `Diagnostics`, using `path` as file name. + pub(crate) fn into_diagnostics(self, path: &Path) -> Diagnostics { + let messages = if self.messages.is_empty() { + Vec::new() + } else { + let file = SourceFileBuilder::new(path.to_string_lossy(), self.source).finish(); + self.messages + .into_iter() + .map(|msg| Message { + kind: msg.kind, + range: msg.range, + fix: msg.fix, + file: file.clone(), + noqa_offset: msg.noqa_offset, + }) + .collect() + }; + Diagnostics::new(messages, self.imports) } } -#[derive(Deserialize)] -struct MessageHeader { +/// On disk representation of a diagnostic message. +#[derive(Clone, Deserialize, Debug, Serialize)] +struct CacheMessage { kind: DiagnosticKind, + /// Range into the message's [`FileCache::source`]. range: TextRange, fix: Option, - file_id: usize, - noqa_row: TextSize, + noqa_offset: TextSize, } -#[derive(Deserialize)] -struct CheckResult { - imports: ImportMap, - messages: Vec, - files: FilesBuf, -} - -fn content_dir() -> &'static Path { - Path::new("content") -} - -fn cache_key( - path: &Path, - package: Option<&Path>, - metadata: &fs::Metadata, - settings: &Settings, -) -> u64 { +/// Returns a hash key based on the `package_root`, `settings` and the crate +/// version. +fn cache_key(package_root: &Path, settings: &Settings) -> u64 { let mut hasher = CacheKeyHasher::new(); - CARGO_PKG_VERSION.cache_key(&mut hasher); - path.absolutize().unwrap().cache_key(&mut hasher); - package - .as_ref() - .map(|path| path.absolutize().unwrap()) - .cache_key(&mut hasher); - FileTime::from_last_modification_time(metadata).cache_key(&mut hasher); - #[cfg(unix)] - metadata.permissions().mode().cache_key(&mut hasher); + env!("CARGO_PKG_VERSION").cache_key(&mut hasher); + package_root.cache_key(&mut hasher); settings.cache_key(&mut hasher); hasher.finish() } -#[allow(dead_code)] /// Initialize the cache at the specified `Path`. pub(crate) fn init(path: &Path) -> Result<()> { // Create the cache directories. - fs::create_dir_all(path.join(content_dir()))?; + fs::create_dir_all(path.join("content"))?; // Add the CACHEDIR.TAG. if !cachedir::is_tagged(path)? { @@ -181,99 +257,3 @@ pub(crate) fn init(path: &Path) -> Result<()> { Ok(()) } - -fn write_sync(cache_dir: &Path, key: u64, value: &[u8]) -> Result<(), std::io::Error> { - fs::write( - cache_dir.join(content_dir()).join(format!("{key:x}")), - value, - ) -} - -fn read_sync(cache_dir: &Path, key: u64) -> Result, std::io::Error> { - fs::read(cache_dir.join(content_dir()).join(format!("{key:x}"))) -} - -fn del_sync(cache_dir: &Path, key: u64) -> Result<(), std::io::Error> { - fs::remove_file(cache_dir.join(content_dir()).join(format!("{key:x}"))) -} - -/// Get a value from the cache. -pub(crate) fn get( - path: &Path, - package: Option<&Path>, - metadata: &fs::Metadata, - settings: &AllSettings, -) -> Option<(Vec, ImportMap)> { - let encoded = read_sync( - &settings.cli.cache_dir, - cache_key(path, package, metadata, &settings.lib), - ) - .ok()?; - match bincode::deserialize::(&encoded[..]) { - Ok(CheckResult { - messages: headers, - imports, - files: sources, - }) => { - let mut messages = Vec::with_capacity(headers.len()); - - let source_files: Vec<_> = sources - .into_iter() - .map(|(filename, text)| SourceFileBuilder::new(filename, text).finish()) - .collect(); - - for header in headers { - let Some(source_file) = source_files.get(header.file_id) else { - error!("Failed to retrieve source file for cached entry"); - return None; - }; - - messages.push(Message { - kind: header.kind, - range: header.range, - fix: header.fix, - file: source_file.clone(), - noqa_offset: header.noqa_row, - }); - } - - Some((messages, imports)) - } - Err(e) => { - error!("Failed to deserialize encoded cache entry: {e:?}"); - None - } - } -} - -/// Set a value in the cache. -pub(crate) fn set( - path: &Path, - package: Option<&Path>, - metadata: &fs::Metadata, - settings: &AllSettings, - messages: &[Message], - imports: &ImportMap, -) { - let check_result = CheckResultRef { imports, messages }; - if let Err(e) = write_sync( - &settings.cli.cache_dir, - cache_key(path, package, metadata, &settings.lib), - &bincode::serialize(&check_result).unwrap(), - ) { - error!("Failed to write to cache: {e:?}"); - } -} - -/// Delete a value from the cache. -pub(crate) fn del( - path: &Path, - package: Option<&Path>, - metadata: &fs::Metadata, - settings: &AllSettings, -) { - drop(del_sync( - &settings.cli.cache_dir, - cache_key(path, package, metadata, &settings.lib), - )); -} diff --git a/crates/ruff_cli/src/commands/run.rs b/crates/ruff_cli/src/commands/run.rs index c299be0c09..533b7f120f 100644 --- a/crates/ruff_cli/src/commands/run.rs +++ b/crates/ruff_cli/src/commands/run.rs @@ -1,3 +1,5 @@ +use std::collections::{hash_map, HashMap}; +use std::fmt::Write; use std::io; use std::path::{Path, PathBuf}; use std::time::Instant; @@ -20,7 +22,7 @@ use ruff_python_ast::imports::ImportMap; use ruff_python_ast::source_code::SourceFileBuilder; use crate::args::Overrides; -use crate::cache; +use crate::cache::{self, PackageCache}; use crate::diagnostics::Diagnostics; use crate::panic::catch_unwind; @@ -75,6 +77,38 @@ pub(crate) fn run( pyproject_config, ); + // Create a cache per package, if enabled. + let package_caches = if cache.into() { + let mut caches = HashMap::new(); + // TODO(thomas): try to merge this with the detection of package roots + // above or with the parallel iteration below. + for entry in &paths { + let Ok(entry) = entry else { continue }; + let path = entry.path(); + let package = path + .parent() + .and_then(|parent| package_roots.get(parent)) + .and_then(|package| *package); + // For paths not in a package, e.g. scripts, we use the path as + // the package root. + let package_root = package.unwrap_or(path); + + let settings = resolver.resolve_all(path, pyproject_config); + + if let hash_map::Entry::Vacant(entry) = caches.entry(package_root) { + let cache = PackageCache::open( + &settings.cli.cache_dir, + package_root.to_owned(), + &settings.lib, + )?; + entry.insert(cache); + } + } + Some(caches) + } else { + None + }; + let start = Instant::now(); let mut diagnostics: Diagnostics = paths .par_iter() @@ -86,13 +120,22 @@ pub(crate) fn run( .parent() .and_then(|parent| package_roots.get(parent)) .and_then(|package| *package); + + let package_cache = package_caches.as_ref().map(|package_caches| { + let package_root = package.unwrap_or(path); + let package_cache = package_caches + .get(package_root) + .expect("failed to get package cache"); + package_cache + }); + let settings = resolver.resolve_all(path, pyproject_config); - lint_path(path, package, settings, cache, noqa, autofix).map_err(|e| { + lint_path(path, package, settings, package_cache, noqa, autofix).map_err(|e| { (Some(path.to_owned()), { let mut error = e.to_string(); for cause in e.chain() { - error += &format!("\n Caused by: {cause}"); + write!(&mut error, "\n Caused by: {cause}").unwrap(); } error }) @@ -145,6 +188,13 @@ pub(crate) fn run( diagnostics.messages.sort(); + // Store the package caches. + if let Some(package_caches) = package_caches { + for package_cache in package_caches.values() { + package_cache.store()?; + } + } + let duration = start.elapsed(); debug!("Checked {:?} files in: {:?}", paths.len(), duration); @@ -157,12 +207,12 @@ fn lint_path( path: &Path, package: Option<&Path>, settings: &AllSettings, - cache: flags::Cache, + package_cache: Option<&PackageCache>, noqa: flags::Noqa, autofix: flags::FixMode, ) -> Result { let result = catch_unwind(|| { - crate::diagnostics::lint_path(path, package, settings, cache, noqa, autofix) + crate::diagnostics::lint_path(path, package, settings, package_cache, noqa, autofix) }); match result { diff --git a/crates/ruff_cli/src/diagnostics.rs b/crates/ruff_cli/src/diagnostics.rs index f8efe174c2..68c110851e 100644 --- a/crates/ruff_cli/src/diagnostics.rs +++ b/crates/ruff_cli/src/diagnostics.rs @@ -25,7 +25,7 @@ use ruff_python_ast::imports::ImportMap; use ruff_python_ast::source_code::{LineIndex, SourceCode, SourceFileBuilder}; use ruff_python_stdlib::path::is_project_toml; -use crate::cache; +use crate::cache::{FileCache, PackageCache}; #[derive(Debug, Default, PartialEq)] pub(crate) struct Diagnostics { @@ -100,7 +100,7 @@ pub(crate) fn lint_path( path: &Path, package: Option<&Path>, settings: &AllSettings, - cache: flags::Cache, + package_cache: Option<&PackageCache>, noqa: flags::Noqa, autofix: flags::FixMode, ) -> Result { @@ -110,15 +110,19 @@ pub(crate) fn lint_path( // to cache `fixer::Mode::Apply`, since a file either has no fixes, or we'll // write the fixes to disk, thus invalidating the cache. But it's a bit hard // to reason about. We need to come up with a better solution here.) - let metadata = if cache.into() && noqa.into() && autofix.is_generate() { - let metadata = path.metadata()?; - if let Some((messages, imports)) = cache::get(path, package, &metadata, settings) { - debug!("Cache hit for: {}", path.display()); - return Ok(Diagnostics::new(messages, imports)); + let caching = match package_cache { + Some(package_cache) if noqa.into() && autofix.is_generate() => { + let relative_path = package_cache + .relative_path(path) + .expect("wrong package cache for file"); + let last_modified = path.metadata()?.modified()?; + if let Some(cache) = package_cache.get(relative_path, last_modified) { + return Ok(cache.into_diagnostics(path)); + } + + Some((package_cache, relative_path, last_modified)) } - Some(metadata) - } else { - None + _ => None, }; debug!("Checking: {}", path.display()); @@ -203,6 +207,17 @@ pub(crate) fn lint_path( let imports = imports.unwrap_or_default(); + if let Some((package_cache, relative_path, file_last_modified)) = caching { + if parse_error.is_some() { + // We don't cache parsing error, so we remove the old file cache (if + // any). + package_cache.remove(relative_path); + } else { + let file_cache = FileCache::new(file_last_modified, &messages, &imports); + package_cache.update(relative_path.to_owned(), file_cache); + } + } + if let Some(err) = parse_error { error!( "{}", @@ -212,16 +227,6 @@ pub(crate) fn lint_path( Some(&source_kind), ) ); - - // Purge the cache. - if let Some(metadata) = metadata { - cache::del(path, package, &metadata, settings); - } - } else { - // Re-populate the cache. - if let Some(metadata) = metadata { - cache::set(path, package, &metadata, settings, &messages, &imports); - } } Ok(Diagnostics { diff --git a/crates/ruff_python_ast/src/source_code/mod.rs b/crates/ruff_python_ast/src/source_code/mod.rs index acd988c02e..829cc98515 100644 --- a/crates/ruff_python_ast/src/source_code/mod.rs +++ b/crates/ruff_python_ast/src/source_code/mod.rs @@ -202,7 +202,7 @@ impl SourceFile { .get_or_init(|| LineIndex::from_source_text(self.source_text())) } - /// Returns `Some` with the source text if set, or `None`. + /// Returns the source code. #[inline] pub fn source_text(&self) -> &str { &self.inner.code From 94abf7f08841d972f4f6045cb7301fac748c442f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 19 Jun 2023 12:09:10 -0400 Subject: [PATCH 110/447] Rename `*Importation` structs to `*Import` (#5185) ## Summary I find "Importation" a bit awkward, it may not even be grammatically correct here. --- crates/ruff/src/checkers/ast/mod.rs | 29 +++++----- crates/ruff/src/renamer.rs | 6 +-- .../unaliased_collections_abc_set_import.rs | 4 +- .../src/rules/flake8_type_checking/helpers.rs | 4 +- crates/ruff/src/rules/pandas_vet/helpers.rs | 4 +- .../pandas_vet/rules/inplace_argument.rs | 4 +- crates/ruff_python_semantic/src/binding.rs | 54 +++++++++---------- crates/ruff_python_semantic/src/model.rs | 16 +++--- crates/ruff_python_semantic/src/scope.rs | 8 +-- 9 files changed, 61 insertions(+), 68 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 67237eb737..f3a805bfdb 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -22,9 +22,8 @@ use ruff_python_ast::{cast, helpers, identifier, str, visitor}; use ruff_python_semantic::analyze::{branch_detection, typing, visibility}; use ruff_python_semantic::{ Binding, BindingFlags, BindingId, BindingKind, ContextualizedDefinition, Exceptions, - ExecutionContext, Export, FromImportation, Globals, Importation, Module, ModuleKind, - ResolvedRead, Scope, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImportation, - SubmoduleImportation, + ExecutionContext, Export, FromImport, Globals, Import, Module, ModuleKind, ResolvedRead, Scope, + ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport, SubmoduleImport, }; use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS}; use ruff_python_stdlib::path::is_python_stub_file; @@ -815,9 +814,7 @@ where self.add_binding( name, alias.identifier(self.locator), - BindingKind::SubmoduleImportation(SubmoduleImportation { - qualified_name, - }), + BindingKind::SubmoduleImport(SubmoduleImport { qualified_name }), BindingFlags::EXTERNAL, ); } else { @@ -838,7 +835,7 @@ where self.add_binding( name, alias.identifier(self.locator), - BindingKind::Importation(Importation { qualified_name }), + BindingKind::Import(Import { qualified_name }), flags, ); @@ -1056,7 +1053,7 @@ where self.add_binding( name, alias.identifier(self.locator), - BindingKind::FutureImportation, + BindingKind::FutureImport, BindingFlags::empty(), ); @@ -1074,7 +1071,7 @@ where } else if &alias.name == "*" { self.semantic .scope_mut() - .add_star_import(StarImportation { level, module }); + .add_star_import(StarImport { level, module }); if self.enabled(Rule::UndefinedLocalWithNestedImportStarUsage) { let scope = self.semantic.scope(); @@ -1127,7 +1124,7 @@ where self.add_binding( name, alias.identifier(self.locator), - BindingKind::FromImportation(FromImportation { qualified_name }), + BindingKind::FromImport(FromImport { qualified_name }), flags, ); } @@ -4190,10 +4187,10 @@ impl<'a> Checker<'a> { { let shadows_import = matches!( shadowed.kind, - BindingKind::Importation(..) - | BindingKind::FromImportation(..) - | BindingKind::SubmoduleImportation(..) - | BindingKind::FutureImportation + BindingKind::Import(..) + | BindingKind::FromImport(..) + | BindingKind::SubmoduleImport(..) + | BindingKind::FutureImport ); if binding.kind.is_loop_var() && shadows_import { if self.enabled(Rule::ImportShadowedByLoopVar) { @@ -4314,7 +4311,7 @@ impl<'a> Checker<'a> { .scopes .iter() .flat_map(Scope::star_imports) - .map(|StarImportation { level, module }| { + .map(|StarImport { level, module }| { helpers::format_import_from(*level, *module) }) .sorted() @@ -4792,7 +4789,7 @@ impl<'a> Checker<'a> { if self.enabled(Rule::UndefinedLocalWithImportStarUsage) { let sources: Vec = scope .star_imports() - .map(|StarImportation { level, module }| { + .map(|StarImport { level, module }| { helpers::format_import_from(*level, *module) }) .sorted() diff --git a/crates/ruff/src/renamer.rs b/crates/ruff/src/renamer.rs index 6c75d9176d..fe31104198 100644 --- a/crates/ruff/src/renamer.rs +++ b/crates/ruff/src/renamer.rs @@ -217,7 +217,7 @@ impl Renamer { /// Rename a [`Binding`] reference. fn rename_binding(binding: &Binding, name: &str, target: &str) -> Option { match &binding.kind { - BindingKind::Importation(_) | BindingKind::FromImportation(_) => { + BindingKind::Import(_) | BindingKind::FromImport(_) => { if binding.is_alias() { // Ex) Rename `import pandas as alias` to `import pandas as pd`. Some(Edit::range_replacement(target.to_string(), binding.range)) @@ -229,7 +229,7 @@ impl Renamer { )) } } - BindingKind::SubmoduleImportation(import) => { + BindingKind::SubmoduleImport(import) => { // Ex) Rename `import pandas.core` to `import pandas as pd`. let module_name = import.qualified_name.split('.').next().unwrap(); Some(Edit::range_replacement( @@ -238,7 +238,7 @@ impl Renamer { )) } // Avoid renaming builtins and other "special" bindings. - BindingKind::FutureImportation | BindingKind::Builtin | BindingKind::Export(_) => None, + BindingKind::FutureImport | BindingKind::Builtin | BindingKind::Export(_) => None, // By default, replace the binding's name with the target name. BindingKind::Annotation | BindingKind::Argument diff --git a/crates/ruff/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs b/crates/ruff/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs index 07a3039083..5fb0a3f5f6 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs @@ -1,6 +1,6 @@ use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::{BindingKind, FromImportation, Scope}; +use ruff_python_semantic::{BindingKind, FromImport, Scope}; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -53,7 +53,7 @@ pub(crate) fn unaliased_collections_abc_set_import( ) { for (name, binding_id) in scope.all_bindings() { let binding = checker.semantic().binding(binding_id); - let BindingKind::FromImportation(FromImportation { qualified_name }) = &binding.kind else { + let BindingKind::FromImport(FromImport { qualified_name }) = &binding.kind else { continue; }; if qualified_name.as_str() != "collections.abc.Set" { diff --git a/crates/ruff/src/rules/flake8_type_checking/helpers.rs b/crates/ruff/src/rules/flake8_type_checking/helpers.rs index d9c3ef6075..48bda7481b 100644 --- a/crates/ruff/src/rules/flake8_type_checking/helpers.rs +++ b/crates/ruff/src/rules/flake8_type_checking/helpers.rs @@ -7,9 +7,7 @@ use ruff_python_semantic::{Binding, BindingKind, ScopeKind, SemanticModel}; pub(crate) fn is_valid_runtime_import(binding: &Binding, semantic: &SemanticModel) -> bool { if matches!( binding.kind, - BindingKind::Importation(..) - | BindingKind::FromImportation(..) - | BindingKind::SubmoduleImportation(..) + BindingKind::Import(..) | BindingKind::FromImport(..) | BindingKind::SubmoduleImport(..) ) { binding.context.is_runtime() && binding diff --git a/crates/ruff/src/rules/pandas_vet/helpers.rs b/crates/ruff/src/rules/pandas_vet/helpers.rs index 1b92dd9867..b849af1f80 100644 --- a/crates/ruff/src/rules/pandas_vet/helpers.rs +++ b/crates/ruff/src/rules/pandas_vet/helpers.rs @@ -1,7 +1,7 @@ use rustpython_parser::ast; use rustpython_parser::ast::Expr; -use ruff_python_semantic::{BindingKind, Importation, SemanticModel}; +use ruff_python_semantic::{BindingKind, Import, SemanticModel}; pub(super) enum Resolution { /// The expression resolves to an irrelevant expression type (e.g., a constant). @@ -39,7 +39,7 @@ pub(super) fn test_expression(expr: &Expr, semantic: &SemanticModel) -> Resoluti | BindingKind::LoopVar | BindingKind::Global | BindingKind::Nonlocal(_) => Resolution::RelevantLocal, - BindingKind::Importation(Importation { + BindingKind::Import(Import { qualified_name: module, }) if module == "pandas" => Resolution::PandasModule, _ => Resolution::IrrelevantBinding, 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 39b2cca800..cf5983213d 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged}; use ruff_diagnostics::{AutofixKind, Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::{BindingKind, Importation}; +use ruff_python_semantic::{BindingKind, Import}; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -70,7 +70,7 @@ pub(crate) fn inplace_argument( .map_or(false, |binding| { matches!( binding.kind, - BindingKind::Importation(Importation { + BindingKind::Import(Import { qualified_name: "pandas" }) ) diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index 1eb256e089..75841038f9 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -82,33 +82,33 @@ impl<'a> Binding<'a> { /// Return `true` if this binding redefines the given binding. pub fn redefines(&self, existing: &'a Binding) -> bool { match &self.kind { - BindingKind::Importation(Importation { qualified_name }) => { - if let BindingKind::SubmoduleImportation(SubmoduleImportation { + BindingKind::Import(Import { qualified_name }) => { + if let BindingKind::SubmoduleImport(SubmoduleImport { qualified_name: existing, }) = &existing.kind { return qualified_name == existing; } } - BindingKind::FromImportation(FromImportation { qualified_name }) => { - if let BindingKind::SubmoduleImportation(SubmoduleImportation { + BindingKind::FromImport(FromImport { qualified_name }) => { + if let BindingKind::SubmoduleImport(SubmoduleImport { qualified_name: existing, }) = &existing.kind { return qualified_name == existing; } } - BindingKind::SubmoduleImportation(SubmoduleImportation { qualified_name }) => { + BindingKind::SubmoduleImport(SubmoduleImport { qualified_name }) => { match &existing.kind { - BindingKind::Importation(Importation { + BindingKind::Import(Import { qualified_name: existing, }) - | BindingKind::SubmoduleImportation(SubmoduleImportation { + | BindingKind::SubmoduleImport(SubmoduleImport { qualified_name: existing, }) => { return qualified_name == existing; } - BindingKind::FromImportation(FromImportation { + BindingKind::FromImport(FromImport { qualified_name: existing, }) => { return qualified_name == existing; @@ -118,7 +118,7 @@ impl<'a> Binding<'a> { } BindingKind::Deletion | BindingKind::Annotation - | BindingKind::FutureImportation + | BindingKind::FutureImport | BindingKind::Builtin => { return false; } @@ -128,20 +128,18 @@ impl<'a> Binding<'a> { existing.kind, BindingKind::ClassDefinition | BindingKind::FunctionDefinition - | BindingKind::Importation(..) - | BindingKind::FromImportation(..) - | BindingKind::SubmoduleImportation(..) + | BindingKind::Import(..) + | BindingKind::FromImport(..) + | BindingKind::SubmoduleImport(..) ) } /// Returns the fully-qualified symbol name, if this symbol was imported from another module. pub fn qualified_name(&self) -> Option<&str> { match &self.kind { - BindingKind::Importation(Importation { qualified_name }) => Some(qualified_name), - BindingKind::FromImportation(FromImportation { qualified_name }) => { - Some(qualified_name) - } - BindingKind::SubmoduleImportation(SubmoduleImportation { qualified_name }) => { + BindingKind::Import(Import { qualified_name }) => Some(qualified_name), + BindingKind::FromImport(FromImport { qualified_name }) => Some(qualified_name), + BindingKind::SubmoduleImport(SubmoduleImport { qualified_name }) => { Some(qualified_name) } _ => None, @@ -152,11 +150,11 @@ impl<'a> Binding<'a> { /// symbol was imported from another module. pub fn module_name(&self) -> Option<&str> { match &self.kind { - BindingKind::Importation(Importation { qualified_name }) - | BindingKind::SubmoduleImportation(SubmoduleImportation { qualified_name }) => { + BindingKind::Import(Import { qualified_name }) + | BindingKind::SubmoduleImport(SubmoduleImport { qualified_name }) => { Some(qualified_name.split('.').next().unwrap_or(qualified_name)) } - BindingKind::FromImportation(FromImportation { qualified_name }) => Some( + BindingKind::FromImport(FromImport { qualified_name }) => Some( qualified_name .rsplit_once('.') .map_or(qualified_name, |(module, _)| module), @@ -275,7 +273,7 @@ impl<'a> FromIterator> for Bindings<'a> { } #[derive(Debug, Clone)] -pub struct StarImportation<'a> { +pub struct StarImport<'a> { /// The level of the import. `None` or `Some(0)` indicate an absolute import. pub level: Option, /// The module being imported. `None` indicates a wildcard import. @@ -292,7 +290,7 @@ pub struct Export<'a> { /// Ex) `import foo` would be keyed on "foo". /// Ex) `import foo as bar` would be keyed on "bar". #[derive(Debug, Clone)] -pub struct Importation<'a> { +pub struct Import<'a> { /// The full name of the module being imported. /// Ex) Given `import foo`, `qualified_name` would be "foo". /// Ex) Given `import foo as bar`, `qualified_name` would be "foo". @@ -303,7 +301,7 @@ pub struct Importation<'a> { /// Ex) `from foo import bar` would be keyed on "bar". /// Ex) `from foo import bar as baz` would be keyed on "baz". #[derive(Debug, Clone)] -pub struct FromImportation { +pub struct FromImport { /// The full name of the member being imported. /// Ex) Given `from foo import bar`, `qualified_name` would be "foo.bar". /// Ex) Given `from foo import bar as baz`, `qualified_name` would be "foo.bar". @@ -313,7 +311,7 @@ pub struct FromImportation { /// A binding for a submodule imported from a module, keyed on the name of the parent module. /// Ex) `import foo.bar` would be keyed on "foo". #[derive(Debug, Clone)] -pub struct SubmoduleImportation<'a> { +pub struct SubmoduleImport<'a> { /// The full name of the submodule being imported. /// Ex) Given `import foo.bar`, `qualified_name` would be "foo.bar". pub qualified_name: &'a str, @@ -401,25 +399,25 @@ pub enum BindingKind<'a> { /// ```python /// from __future__ import annotations /// ``` - FutureImportation, + FutureImport, /// A binding for a straight `import`, like `foo` in: /// ```python /// import foo /// ``` - Importation(Importation<'a>), + Import(Import<'a>), /// A binding for a member imported from a module, like `bar` in: /// ```python /// from foo import bar /// ``` - FromImportation(FromImportation), + FromImport(FromImport), /// A binding for a submodule imported from a module, like `bar` in: /// ```python /// import foo.bar /// ``` - SubmoduleImportation(SubmoduleImportation<'a>), + SubmoduleImport(SubmoduleImport<'a>), /// A binding for a deletion, like `x` in: /// ```python diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index c69e52f999..3ea95b2dee 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -13,8 +13,8 @@ use ruff_python_stdlib::path::is_python_stub_file; use ruff_python_stdlib::typing::is_typing_extension; use crate::binding::{ - Binding, BindingFlags, BindingId, BindingKind, Bindings, Exceptions, FromImportation, - Importation, SubmoduleImportation, + Binding, BindingFlags, BindingId, BindingKind, Bindings, Exceptions, FromImport, Import, + SubmoduleImport, }; use crate::context::ExecutionContext; use crate::definition::{Definition, DefinitionId, Definitions, Member, Module}; @@ -417,7 +417,7 @@ impl<'a> SemanticModel<'a> { let head = call_path.first()?; let binding = self.find_binding(head)?; match &binding.kind { - BindingKind::Importation(Importation { + BindingKind::Import(Import { qualified_name: name, }) => { if name.starts_with('.') { @@ -434,7 +434,7 @@ impl<'a> SemanticModel<'a> { Some(source_path) } } - BindingKind::SubmoduleImportation(SubmoduleImportation { + BindingKind::SubmoduleImport(SubmoduleImport { qualified_name: name, }) => { let name = name.split('.').next().unwrap_or(name); @@ -442,7 +442,7 @@ impl<'a> SemanticModel<'a> { source_path.extend(call_path.into_iter().skip(1)); Some(source_path) } - BindingKind::FromImportation(FromImportation { + BindingKind::FromImport(FromImport { qualified_name: name, }) => { if name.starts_with('.') { @@ -493,7 +493,7 @@ impl<'a> SemanticModel<'a> { // Ex) Given `module="sys"` and `object="exit"`: // `import sys` -> `sys.exit` // `import sys as sys2` -> `sys2.exit` - BindingKind::Importation(Importation { qualified_name }) => { + BindingKind::Import(Import { qualified_name }) => { if qualified_name == &module { if let Some(source) = binding.source { // Verify that `sys` isn't bound in an inner scope. @@ -514,7 +514,7 @@ impl<'a> SemanticModel<'a> { // Ex) Given `module="os.path"` and `object="join"`: // `from os.path import join` -> `join` // `from os.path import join as join2` -> `join2` - BindingKind::FromImportation(FromImportation { qualified_name }) => { + BindingKind::FromImport(FromImport { qualified_name }) => { if let Some((target_module, target_member)) = qualified_name.split_once('.') { if target_module == module && target_member == member { @@ -537,7 +537,7 @@ impl<'a> SemanticModel<'a> { } // Ex) Given `module="os"` and `object="name"`: // `import os.path ` -> `os.name` - BindingKind::SubmoduleImportation(SubmoduleImportation { .. }) => { + BindingKind::SubmoduleImport(SubmoduleImport { .. }) => { if name == module { if let Some(source) = binding.source { // Verify that `os` isn't bound in an inner scope. diff --git a/crates/ruff_python_semantic/src/scope.rs b/crates/ruff_python_semantic/src/scope.rs index 885452e7f5..711572f861 100644 --- a/crates/ruff_python_semantic/src/scope.rs +++ b/crates/ruff_python_semantic/src/scope.rs @@ -8,7 +8,7 @@ use rustpython_parser::ast; use ruff_index::{newtype_index, Idx, IndexSlice, IndexVec}; -use crate::binding::{BindingId, StarImportation}; +use crate::binding::{BindingId, StarImport}; use crate::globals::GlobalsId; #[derive(Debug)] @@ -21,7 +21,7 @@ pub struct Scope<'a> { /// A list of star imports in this scope. These represent _module_ imports (e.g., `sys` in /// `from sys import *`), rather than individual bindings (e.g., individual members in `sys`). - star_imports: Vec>, + star_imports: Vec>, /// A map from bound name to binding ID. bindings: FxHashMap<&'a str, BindingId>, @@ -126,7 +126,7 @@ impl<'a> Scope<'a> { } /// Adds a reference to a star import (e.g., `from sys import *`) to this scope. - pub fn add_star_import(&mut self, import: StarImportation<'a>) { + pub fn add_star_import(&mut self, import: StarImport<'a>) { self.star_imports.push(import); } @@ -136,7 +136,7 @@ impl<'a> Scope<'a> { } /// Returns an iterator over all star imports (e.g., `from sys import *`) in this scope. - pub fn star_imports(&self) -> impl Iterator> { + pub fn star_imports(&self) -> impl Iterator> { self.star_imports.iter() } From 48f4f2d63de7296d12b20893f452b876b2c64f76 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Mon, 19 Jun 2023 23:47:56 +0530 Subject: [PATCH 111/447] Maintain consistency when deserializing to JSON (#5114) ## Summary Maintain consistency while deserializing Jupyter notebook to JSON. The following changes were made: 1. Use string array to store the source value as that's the default (https://github.com/jupyter/nbformat/blob/57817204230a8f7173b8bec380e571b5d410de0d/nbformat/v4/nbjson.py#L56-L57) 2. Remove unused structs and enums 3. Reorder the keys in alphabetical order as that's the default. (https://github.com/jupyter/nbformat/blob/57817204230a8f7173b8bec380e571b5d410de0d/nbformat/v4/nbjson.py#L51) ### Side effect Removing the `preserve_order` feature means that the order of keys in JSON output (`--format json`) will be in alphabetical order. This is because the value is represented using `serde_json::Value` which internally is a `BTreeMap`, thus sorting it as per the string key. For posterity if this turns out to be not ideal, then we could define a struct representing the JSON object and the order of struct fields will determine the order in the JSON string. ## Test Plan Add a test case to assert the raw JSON string. --- Cargo.lock | 1 - Cargo.toml | 2 +- .../test/fixtures/jupyter/after_fix.ipynb | 37 +++ .../test/fixtures/jupyter/before_fix.ipynb | 38 +++ .../fixtures/jupyter/cell/code_and_magic.json | 3 + .../test/fixtures/jupyter/cell/markdown.json | 1 + .../test/fixtures/jupyter/cell/only_code.json | 3 + .../fixtures/jupyter/cell/only_magic.json | 3 + crates/ruff/src/jupyter/notebook.rs | 106 +++++-- crates/ruff/src/jupyter/schema.rs | 288 +++++++----------- .../ruff__message__gitlab__tests__output.snap | 24 +- .../ruff__message__json__tests__output.snap | 80 ++--- ...f__message__json_lines__tests__output.snap | 8 +- crates/ruff_cli/src/commands/run.rs | 90 ------ crates/ruff_cli/tests/integration_test.rs | 32 +- 15 files changed, 346 insertions(+), 370 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/jupyter/after_fix.ipynb create mode 100644 crates/ruff/resources/test/fixtures/jupyter/before_fix.ipynb diff --git a/Cargo.lock b/Cargo.lock index 7b248b608e..09fac32aaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2368,7 +2368,6 @@ version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ - "indexmap", "itoa", "ryu", "serde", diff --git a/Cargo.toml b/Cargo.toml index 3051a7e693..42c0957d67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "0dc8fdf52d146698c5bcf0b842fddc9e398ad8db", default-features = false, features = ["full-lexer", "all-nodes-with-ranges"] } schemars = { version = "0.8.12" } serde = { version = "1.0.152", features = ["derive"] } -serde_json = { version = "1.0.93", features = ["preserve_order"] } +serde_json = { version = "1.0.93" } shellexpand = { version = "3.0.0" } similar = { version = "2.2.1", features = ["inline"] } smallvec = { version = "1.10.0" } diff --git a/crates/ruff/resources/test/fixtures/jupyter/after_fix.ipynb b/crates/ruff/resources/test/fixtures/jupyter/after_fix.ipynb new file mode 100644 index 0000000000..ef9bf6614f --- /dev/null +++ b/crates/ruff/resources/test/fixtures/jupyter/after_fix.ipynb @@ -0,0 +1,37 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "\n", + "math.pi" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (ruff)", + "language": "python", + "name": "ruff" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/crates/ruff/resources/test/fixtures/jupyter/before_fix.ipynb b/crates/ruff/resources/test/fixtures/jupyter/before_fix.ipynb new file mode 100644 index 0000000000..fdaaa2819c --- /dev/null +++ b/crates/ruff/resources/test/fixtures/jupyter/before_fix.ipynb @@ -0,0 +1,38 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "import os\n", + "\n", + "math.pi" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (ruff)", + "language": "python", + "name": "ruff" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/crates/ruff/resources/test/fixtures/jupyter/cell/code_and_magic.json b/crates/ruff/resources/test/fixtures/jupyter/cell/code_and_magic.json index b1acfb8be0..5fc3d268f4 100644 --- a/crates/ruff/resources/test/fixtures/jupyter/cell/code_and_magic.json +++ b/crates/ruff/resources/test/fixtures/jupyter/cell/code_and_magic.json @@ -1,5 +1,8 @@ { + "execution_count": null, "cell_type": "code", + "id": "1", "metadata": {}, + "outputs": [], "source": ["def foo():\n", " pass\n", "\n", "%timeit foo()"] } diff --git a/crates/ruff/resources/test/fixtures/jupyter/cell/markdown.json b/crates/ruff/resources/test/fixtures/jupyter/cell/markdown.json index f6880ebbf0..00b7245742 100644 --- a/crates/ruff/resources/test/fixtures/jupyter/cell/markdown.json +++ b/crates/ruff/resources/test/fixtures/jupyter/cell/markdown.json @@ -1,5 +1,6 @@ { "cell_type": "markdown", + "id": "1", "metadata": {}, "source": ["This is a markdown cell\n", "Some more content"] } diff --git a/crates/ruff/resources/test/fixtures/jupyter/cell/only_code.json b/crates/ruff/resources/test/fixtures/jupyter/cell/only_code.json index 89904fbd93..c36b77bbeb 100644 --- a/crates/ruff/resources/test/fixtures/jupyter/cell/only_code.json +++ b/crates/ruff/resources/test/fixtures/jupyter/cell/only_code.json @@ -1,5 +1,8 @@ { + "execution_count": null, "cell_type": "code", + "id": "1", "metadata": {}, + "outputs": [], "source": ["def foo():\n", " pass"] } diff --git a/crates/ruff/resources/test/fixtures/jupyter/cell/only_magic.json b/crates/ruff/resources/test/fixtures/jupyter/cell/only_magic.json index 183923bde1..515ba814fc 100644 --- a/crates/ruff/resources/test/fixtures/jupyter/cell/only_magic.json +++ b/crates/ruff/resources/test/fixtures/jupyter/cell/only_magic.json @@ -1,5 +1,8 @@ { + "execution_count": null, "cell_type": "code", + "id": "1", "metadata": {}, + "outputs": [], "source": "%timeit print('hello world')" } diff --git a/crates/ruff/src/jupyter/notebook.rs b/crates/ruff/src/jupyter/notebook.rs index d7381cf56f..2eb8f92bc5 100644 --- a/crates/ruff/src/jupyter/notebook.rs +++ b/crates/ruff/src/jupyter/notebook.rs @@ -1,6 +1,6 @@ use std::cmp::Ordering; use std::fs::File; -use std::io::{BufReader, BufWriter, Cursor, Write}; +use std::io::{BufReader, BufWriter, Write}; use std::iter; use std::path::Path; @@ -10,12 +10,12 @@ use serde::Serialize; use serde_json::error::Category; use ruff_diagnostics::Diagnostic; -use ruff_python_whitespace::NewlineWithTrailingNewline; +use ruff_python_whitespace::{NewlineWithTrailingNewline, UniversalNewlineIterator}; use ruff_text_size::{TextRange, TextSize}; use crate::autofix::source_map::{SourceMap, SourceMarker}; use crate::jupyter::index::JupyterIndex; -use crate::jupyter::{Cell, CellType, RawNotebook, SourceValue}; +use crate::jupyter::schema::{Cell, RawNotebook, SortAlphabetically, SourceValue}; use crate::rules::pycodestyle::rules::SyntaxError; use crate::IOError; @@ -34,9 +34,9 @@ pub fn round_trip(path: &Path) -> anyhow::Result { })?; let code = notebook.content().to_string(); notebook.update_cell_content(&code); - let mut buffer = Cursor::new(Vec::new()); + let mut buffer = BufWriter::new(Vec::new()); notebook.write_inner(&mut buffer)?; - Ok(String::from_utf8(buffer.into_inner())?) + Ok(String::from_utf8(buffer.into_inner()?)?) } /// Return `true` if the [`Path`] appears to be that of a jupyter notebook file (`.ipynb`). @@ -49,18 +49,37 @@ pub fn is_jupyter_notebook(path: &Path) -> bool { } impl Cell { + /// Return the [`SourceValue`] of the cell. + fn source(&self) -> &SourceValue { + match self { + Cell::Code(cell) => &cell.source, + Cell::Markdown(cell) => &cell.source, + Cell::Raw(cell) => &cell.source, + } + } + + /// Update the [`SourceValue`] of the cell. + fn set_source(&mut self, source: SourceValue) { + match self { + Cell::Code(cell) => cell.source = source, + Cell::Markdown(cell) => cell.source = source, + Cell::Raw(cell) => cell.source = source, + } + } + /// Return `true` if it's a valid code cell. /// - /// A valid code cell is a cell where the type is [`CellType::Code`] and the + /// A valid code cell is a cell where the cell type is [`Cell::Code`] and the /// source doesn't contain a magic, shell or help command. fn is_valid_code_cell(&self) -> bool { - if self.cell_type != CellType::Code { - return false; - } + let source = match self { + Cell::Code(cell) => &cell.source, + _ => return false, + }; // Ignore a cell if it contains a magic command. There could be valid // Python code as well, but we'll ignore that for now. // TODO(dhruvmanila): https://github.com/psf/black/blob/main/src/black/handle_ipynb_magics.py - !match &self.source { + !match source { SourceValue::String(string) => string.lines().any(|line| { MAGIC_PREFIX .iter() @@ -92,7 +111,7 @@ pub struct Notebook { /// The offsets of each cell in the concatenated source code. This includes /// the first and last character offsets as well. cell_offsets: Vec, - /// The cell numbers of all valid code cells in the notebook. + /// The cell index of all valid code cells in the notebook. valid_code_cells: Vec, } @@ -108,7 +127,7 @@ impl Notebook { TextRange::default(), ) })?); - let notebook: RawNotebook = match serde_json::from_reader(reader) { + let raw_notebook: RawNotebook = match serde_json::from_reader(reader) { Ok(notebook) => notebook, Err(err) => { // Translate the error into a diagnostic @@ -176,34 +195,34 @@ impl Notebook { }; // v4 is what everybody uses - if notebook.nbformat != 4 { + if raw_notebook.nbformat != 4 { // bail because we should have already failed at the json schema stage return Err(Box::new(Diagnostic::new( SyntaxError { message: format!( "Expected Jupyter Notebook format 4, found {}", - notebook.nbformat + raw_notebook.nbformat ), }, TextRange::default(), ))); } - let valid_code_cells = notebook + let valid_code_cells = raw_notebook .cells .iter() .enumerate() .filter(|(_, cell)| cell.is_valid_code_cell()) - .map(|(pos, _)| u32::try_from(pos).unwrap()) + .map(|(idx, _)| u32::try_from(idx).unwrap()) .collect::>(); let mut contents = Vec::with_capacity(valid_code_cells.len()); let mut current_offset = TextSize::from(0); - let mut cell_offsets = Vec::with_capacity(notebook.cells.len()); + let mut cell_offsets = Vec::with_capacity(valid_code_cells.len()); cell_offsets.push(TextSize::from(0)); - for &pos in &valid_code_cells { - let cell_contents = match ¬ebook.cells[pos as usize].source { + for &idx in &valid_code_cells { + let cell_contents = match &raw_notebook.cells[idx as usize].source() { SourceValue::String(string) => string.clone(), SourceValue::StringArray(string_array) => string_array.join(""), }; @@ -213,7 +232,7 @@ impl Notebook { } Ok(Self { - raw: notebook, + raw: raw_notebook, index: OnceCell::new(), // The additional newline at the end is to maintain consistency for // all cells. These newlines will be removed before updating the @@ -267,7 +286,7 @@ impl Notebook { /// can happen only if the cell offsets were not updated before calling /// this method or the offsets were updated incorrectly. fn update_cell_content(&mut self, transformed: &str) { - for (&pos, (start, end)) in self + for (&idx, (start, end)) in self .valid_code_cells .iter() .zip(self.cell_offsets.iter().tuple_windows::<(_, _)>()) @@ -275,22 +294,25 @@ impl Notebook { let cell_content = transformed .get(start.to_usize()..end.to_usize()) .unwrap_or_else(|| { - panic!("Transformed content out of bounds ({start:?}..{end:?}) for cell {pos}"); + panic!( + "Transformed content out of bounds ({start:?}..{end:?}) for cell at {idx:?}" + ); }); - self.raw.cells[pos as usize].source = SourceValue::String( - cell_content + self.raw.cells[idx as usize].set_source(SourceValue::StringArray( + UniversalNewlineIterator::from( // We only need to strip the trailing newline which we added // while concatenating the cell contents. - .strip_suffix('\n') - .unwrap_or(cell_content) - .to_string(), - ); + cell_content.strip_suffix('\n').unwrap_or(cell_content), + ) + .map(|line| line.as_full_str().to_string()) + .collect::>(), + )); } } /// Build and return the [`JupyterIndex`]. /// - /// # Notes + /// ## Notes /// /// Empty cells don't have any newlines, but there's a single visible line /// in the UI. That single line needs to be accounted for. @@ -317,8 +339,8 @@ impl Notebook { let mut row_to_cell = vec![0]; let mut row_to_row_in_cell = vec![0]; - for &pos in &self.valid_code_cells { - let line_count = match &self.raw.cells[pos as usize].source { + for &idx in &self.valid_code_cells { + let line_count = match &self.raw.cells[idx as usize].source() { SourceValue::String(string) => { if string.is_empty() { 1 @@ -336,7 +358,7 @@ impl Notebook { } } }; - row_to_cell.extend(iter::repeat(pos + 1).take(line_count as usize)); + row_to_cell.extend(iter::repeat(idx + 1).take(line_count as usize)); row_to_row_in_cell.extend(1..=line_count); } @@ -390,7 +412,7 @@ impl Notebook { // https://github.com/psf/black/blob/69ca0a4c7a365c5f5eea519a90980bab72cab764/src/black/__init__.py#LL1041 let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); let mut ser = serde_json::Serializer::with_formatter(writer, formatter); - self.raw.serialize(&mut ser)?; + SortAlphabetically(&self.raw).serialize(&mut ser)?; Ok(()) } @@ -404,6 +426,7 @@ impl Notebook { #[cfg(test)] mod test { + use std::io::BufWriter; use std::path::Path; use anyhow::Result; @@ -536,4 +559,21 @@ print("after empty cells") assert_messages!(diagnostics, path, source_kind); Ok(()) } + + #[test] + fn test_json_consistency() -> Result<()> { + let path = "before_fix.ipynb".to_string(); + let (_, source_kind) = test_notebook_path( + path, + Path::new("after_fix.ipynb"), + &settings::Settings::for_rule(Rule::UnusedImport), + )?; + let mut writer = BufWriter::new(Vec::new()); + source_kind.expect_jupyter().write_inner(&mut writer)?; + let actual = String::from_utf8(writer.into_inner()?)?; + let expected = + std::fs::read_to_string(test_resource_path("fixtures/jupyter/after_fix.ipynb"))?; + assert_eq!(actual, expected); + Ok(()) + } } diff --git a/crates/ruff/src/jupyter/schema.rs b/crates/ruff/src/jupyter/schema.rs index 34e168c901..b6f9ed3c47 100644 --- a/crates/ruff/src/jupyter/schema.rs +++ b/crates/ruff/src/jupyter/schema.rs @@ -5,6 +5,7 @@ //! Jupyter Notebook v4.5 JSON schema. //! //! The following changes were made to the generated version: +//! * Only keep the required structs and enums. //! * `Cell::id` is optional because it wasn't required ( + value: &T, + serializer: S, +) -> Result { + let value = serde_json::to_value(value).map_err(serde::ser::Error::custom)?; + value.serialize(serializer) +} + +/// This is used to serialize any value implementing [`Serialize`] alphabetically. +/// +/// The reason for this is to maintain consistency in the generated JSON string, +/// which is useful for diffing. The default serializer keeps the order of the +/// fields as they are defined in the struct, which will not be consistent when +/// there are `extra` fields. +/// +/// # Example +/// +/// ``` +/// use std::collections::BTreeMap; +/// +/// use serde::Serialize; +/// +/// use ruff::jupyter::SortAlphabetically; +/// +/// #[derive(Serialize)] +/// struct MyStruct { +/// a: String, +/// #[serde(flatten)] +/// extra: BTreeMap, +/// b: String, +/// } +/// +/// let my_struct = MyStruct { +/// a: "a".to_string(), +/// extra: BTreeMap::from([ +/// ("d".to_string(), "d".to_string()), +/// ("c".to_string(), "c".to_string()), +/// ]), +/// b: "b".to_string(), +/// }; +/// +/// let serialized = serde_json::to_string_pretty(&SortAlphabetically(&my_struct)).unwrap(); +/// assert_eq!( +/// serialized, +/// r#"{ +/// "a": "a", +/// "b": "b", +/// "c": "c", +/// "d": "d" +/// }"# +/// ); +/// ``` +#[derive(Serialize)] +pub struct SortAlphabetically(#[serde(serialize_with = "sort_alphabetically")] pub T); + /// The root of the JSON of a Jupyter Notebook /// /// Generated by from /// /// Jupyter Notebook v4.5 JSON schema. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] pub struct RawNotebook { /// Array of cells of the current notebook. pub cells: Vec, /// Notebook root-level metadata. - pub metadata: JupyterNotebookMetadata, + pub metadata: RawNotebookMetadata, /// Notebook format (major number). Incremented between backwards incompatible changes to the /// notebook format. pub nbformat: i64, @@ -40,119 +99,73 @@ pub struct RawNotebook { pub nbformat_minor: i64, } +/// String identifying the type of cell. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(tag = "cell_type")] +pub enum Cell { + #[serde(rename = "code")] + Code(CodeCell), + #[serde(rename = "markdown")] + Markdown(MarkdownCell), + #[serde(rename = "raw")] + Raw(RawCell), +} + /// Notebook raw nbconvert cell. -/// -/// Notebook markdown cell. -/// -/// Notebook code cell. #[skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] -pub struct Cell { - pub attachments: Option>>, - /// String identifying the type of cell. - pub cell_type: CellType, +pub struct RawCell { + pub attachments: Option, /// Technically, id isn't required (it's not even present) in schema v4.0 through v4.4, but /// it's required in v4.5. Main issue is that pycharm creates notebooks without an id /// pub id: Option, /// Cell-level metadata. - pub metadata: CellMetadata, + pub metadata: Value, pub source: SourceValue, +} + +/// Notebook markdown cell. +#[skip_serializing_none] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct MarkdownCell { + pub attachments: Option, + /// Technically, id isn't required (it's not even present) in schema v4.0 through v4.4, but + /// it's required in v4.5. Main issue is that pycharm creates notebooks without an id + /// + pub id: Option, + /// Cell-level metadata. + pub metadata: Value, + pub source: SourceValue, +} + +/// Notebook code cell. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct CodeCell { /// The code cell's prompt number. Will be null if the cell has not been run. pub execution_count: Option, + /// Technically, id isn't required (it's not even present) in schema v4.0 through v4.4, but + /// it's required in v4.5. Main issue is that pycharm creates notebooks without an id + /// + pub id: Option, + /// Cell-level metadata. + pub metadata: Value, /// Execution, display, or stream outputs. - pub outputs: Option>, -} - -/// Cell-level metadata. -#[skip_serializing_none] -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct CellMetadata { - /// Raw cell metadata format for nbconvert. - pub format: Option, - /// Official Jupyter Metadata for Raw Cells - /// - /// Official Jupyter Metadata for Markdown Cells - /// - /// Official Jupyter Metadata for Code Cells - pub jupyter: Option>>, - pub name: Option, - pub tags: Option>, - /// Whether the cell's output is collapsed/expanded. - pub collapsed: Option, - /// Execution time for the code in the cell. This tracks time at which messages are received - /// from iopub or shell channels - pub execution: Option, - /// Whether the cell's output is scrolled, unscrolled, or autoscrolled. - pub scrolled: Option, - /// Custom added: round-trip support - #[serde(flatten)] - pub other: BTreeMap, -} - -/// Execution time for the code in the cell. This tracks time at which messages are received -/// from iopub or shell channels -#[skip_serializing_none] -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(deny_unknown_fields)] -pub struct Execution { - /// header.date (in ISO 8601 format) of iopub channel's execute_input message. It indicates - /// the time at which the kernel broadcasts an execute_input message to connected frontends - #[serde(rename = "iopub.execute_input")] - pub iopub_execute_input: Option, - /// header.date (in ISO 8601 format) of iopub channel's kernel status message when the status - /// is 'busy' - #[serde(rename = "iopub.status.busy")] - pub iopub_status_busy: Option, - /// header.date (in ISO 8601 format) of iopub channel's kernel status message when the status - /// is 'idle'. It indicates the time at which kernel finished processing the associated - /// request - #[serde(rename = "iopub.status.idle")] - pub iopub_status_idle: Option, - /// header.date (in ISO 8601 format) of the shell channel's execute_reply message. It - /// indicates the time at which the execute_reply message was created - #[serde(rename = "shell.execute_reply")] - pub shell_execute_reply: Option, -} - -/// Result of executing a code cell. -/// -/// Data displayed as a result of code cell execution. -/// -/// Stream output from a code cell. -/// -/// Output of an error that occurred during code cell execution. -#[skip_serializing_none] -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(deny_unknown_fields)] -pub struct Output { - pub data: Option>, - /// A result's prompt number. - pub execution_count: Option, - pub metadata: Option>>, - /// Type of cell output. - pub output_type: OutputType, - /// The name of the stream (stdout, stderr). - pub name: Option, - /// The stream's text output, represented as an array of strings. - pub text: Option, - /// The name of the error. - pub ename: Option, - /// The value, or message, of the error. - pub evalue: Option, - /// The error's traceback, represented as an array of strings. - pub traceback: Option>, + pub outputs: Vec, + pub source: SourceValue, } /// Notebook root-level metadata. #[skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct JupyterNotebookMetadata { +pub struct RawNotebookMetadata { /// The author(s) of the notebook document - pub authors: Option>>, + pub authors: Option, /// Kernel information. - pub kernelspec: Option, + pub kernelspec: Option, /// Kernel information. pub language_info: Option, /// Original notebook format (major number) before converting the notebook between versions. @@ -160,21 +173,9 @@ pub struct JupyterNotebookMetadata { pub orig_nbformat: Option, /// The title of the notebook document pub title: Option, - /// Custom added: round-trip support + /// For additional properties. #[serde(flatten)] - pub other: BTreeMap, -} - -/// Kernel information. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct Kernelspec { - /// Name to display in UI. - pub display_name: String, - /// Name of the kernel specification. - pub name: String, - /// Custom added: round-trip support - #[serde(flatten)] - pub other: BTreeMap, + pub extra: BTreeMap, } /// Kernel information. @@ -182,7 +183,7 @@ pub struct Kernelspec { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct LanguageInfo { /// The codemirror mode to use for code in this language. - pub codemirror_mode: Option, + pub codemirror_mode: Option, /// The file extension for files in this language. pub file_extension: Option, /// The mimetype corresponding to files in this language. @@ -191,9 +192,9 @@ pub struct LanguageInfo { pub name: String, /// The pygments lexer to use for code in this language. pub pygments_lexer: Option, - /// Custom added: round-trip support + /// For additional properties. #[serde(flatten)] - pub other: BTreeMap, + pub extra: BTreeMap, } /// mimetype output (e.g. text/plain), represented as either an array of strings or a @@ -208,62 +209,3 @@ pub enum SourceValue { String(String), StringArray(Vec), } - -/// Whether the cell's output is scrolled, unscrolled, or autoscrolled. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(untagged)] -pub enum ScrolledUnion { - Bool(bool), - Enum(ScrolledEnum), -} - -/// mimetype output (e.g. text/plain), represented as either an array of strings or a -/// string. -/// -/// Contents of the cell, represented as an array of lines. -/// -/// The stream's text output, represented as an array of strings. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(untagged)] -pub enum TextUnion { - String(String), - StringArray(Vec), -} - -/// The codemirror mode to use for code in this language. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(untagged)] -pub enum CodemirrorMode { - AnythingMap(HashMap>), - String(String), -} - -/// String identifying the type of cell. -#[derive(Debug, Serialize, Deserialize, PartialEq, Copy, Clone)] -pub enum CellType { - #[serde(rename = "code")] - Code, - #[serde(rename = "markdown")] - Markdown, - #[serde(rename = "raw")] - Raw, -} - -#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq)] -pub enum ScrolledEnum { - #[serde(rename = "auto")] - Auto, -} - -/// Type of cell output. -#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq)] -pub enum OutputType { - #[serde(rename = "display_data")] - DisplayData, - #[serde(rename = "error")] - Error, - #[serde(rename = "execute_result")] - ExecuteResult, - #[serde(rename = "stream")] - Stream, -} diff --git a/crates/ruff/src/message/snapshots/ruff__message__gitlab__tests__output.snap b/crates/ruff/src/message/snapshots/ruff__message__gitlab__tests__output.snap index e83d0d23d9..43e03eb1b2 100644 --- a/crates/ruff/src/message/snapshots/ruff__message__gitlab__tests__output.snap +++ b/crates/ruff/src/message/snapshots/ruff__message__gitlab__tests__output.snap @@ -5,38 +5,38 @@ expression: redact_fingerprint(&content) [ { "description": "(F401) `os` imported but unused", - "severity": "major", "fingerprint": "", "location": { - "path": "fib.py", "lines": { "begin": 1, "end": 1 - } - } + }, + "path": "fib.py" + }, + "severity": "major" }, { "description": "(F841) Local variable `x` is assigned to but never used", - "severity": "major", "fingerprint": "", "location": { - "path": "fib.py", "lines": { "begin": 6, "end": 6 - } - } + }, + "path": "fib.py" + }, + "severity": "major" }, { "description": "(F821) Undefined name `a`", - "severity": "major", "fingerprint": "", "location": { - "path": "undef.py", "lines": { "begin": 1, "end": 1 - } - } + }, + "path": "undef.py" + }, + "severity": "major" } ] diff --git a/crates/ruff/src/message/snapshots/ruff__message__json__tests__output.snap b/crates/ruff/src/message/snapshots/ruff__message__json__tests__output.snap index e9272931d6..e0daded40c 100644 --- a/crates/ruff/src/message/snapshots/ruff__message__json__tests__output.snap +++ b/crates/ruff/src/message/snapshots/ruff__message__json__tests__output.snap @@ -5,79 +5,79 @@ expression: content [ { "code": "F401", - "message": "`os` imported but unused", + "end_location": { + "column": 10, + "row": 1 + }, + "filename": "fib.py", "fix": { "applicability": "Suggested", - "message": "Remove unused import: `os`", "edits": [ { "content": "", - "location": { - "row": 1, - "column": 1 - }, "end_location": { - "row": 2, - "column": 1 + "column": 1, + "row": 2 + }, + "location": { + "column": 1, + "row": 1 } } - ] + ], + "message": "Remove unused import: `os`" }, "location": { - "row": 1, - "column": 8 + "column": 8, + "row": 1 }, - "end_location": { - "row": 1, - "column": 10 - }, - "filename": "fib.py", + "message": "`os` imported but unused", "noqa_row": 1 }, { "code": "F841", - "message": "Local variable `x` is assigned to but never used", + "end_location": { + "column": 6, + "row": 6 + }, + "filename": "fib.py", "fix": { "applicability": "Suggested", - "message": "Remove assignment to unused variable `x`", "edits": [ { "content": "", - "location": { - "row": 6, - "column": 5 - }, "end_location": { - "row": 6, - "column": 10 + "column": 10, + "row": 6 + }, + "location": { + "column": 5, + "row": 6 } } - ] + ], + "message": "Remove assignment to unused variable `x`" }, "location": { - "row": 6, - "column": 5 + "column": 5, + "row": 6 }, - "end_location": { - "row": 6, - "column": 6 - }, - "filename": "fib.py", + "message": "Local variable `x` is assigned to but never used", "noqa_row": 6 }, { "code": "F821", - "message": "Undefined name `a`", - "fix": null, - "location": { - "row": 1, - "column": 4 - }, "end_location": { - "row": 1, - "column": 5 + "column": 5, + "row": 1 }, "filename": "undef.py", + "fix": null, + "location": { + "column": 4, + "row": 1 + }, + "message": "Undefined name `a`", "noqa_row": 1 } ] diff --git a/crates/ruff/src/message/snapshots/ruff__message__json_lines__tests__output.snap b/crates/ruff/src/message/snapshots/ruff__message__json_lines__tests__output.snap index b6edd32a10..d4433a8e7d 100644 --- a/crates/ruff/src/message/snapshots/ruff__message__json_lines__tests__output.snap +++ b/crates/ruff/src/message/snapshots/ruff__message__json_lines__tests__output.snap @@ -1,8 +1,8 @@ --- -source: crates/ruff/src/message/jsonlines.rs +source: crates/ruff/src/message/json_lines.rs expression: content --- -{"code":"F401","message":"`os` imported but unused","fix":{"applicability":"Suggested","message":"Remove unused import: `os`","edits":[{"content":"","location":{"row":1,"column":1},"end_location":{"row":2,"column":1}}]},"location":{"row":1,"column":8},"end_location":{"row":1,"column":10},"filename":"fib.py","noqa_row":1} -{"code":"F841","message":"Local variable `x` is assigned to but never used","fix":{"applicability":"Suggested","message":"Remove assignment to unused variable `x`","edits":[{"content":"","location":{"row":6,"column":5},"end_location":{"row":6,"column":10}}]},"location":{"row":6,"column":5},"end_location":{"row":6,"column":6},"filename":"fib.py","noqa_row":6} -{"code":"F821","message":"Undefined name `a`","fix":null,"location":{"row":1,"column":4},"end_location":{"row":1,"column":5},"filename":"undef.py","noqa_row":1} +{"code":"F401","end_location":{"column":10,"row":1},"filename":"fib.py","fix":{"applicability":"Suggested","edits":[{"content":"","end_location":{"column":1,"row":2},"location":{"column":1,"row":1}}],"message":"Remove unused import: `os`"},"location":{"column":8,"row":1},"message":"`os` imported but unused","noqa_row":1} +{"code":"F841","end_location":{"column":6,"row":6},"filename":"fib.py","fix":{"applicability":"Suggested","edits":[{"content":"","end_location":{"column":10,"row":6},"location":{"column":5,"row":6}}],"message":"Remove assignment to unused variable `x`"},"location":{"column":5,"row":6},"message":"Local variable `x` is assigned to but never used","noqa_row":6} +{"code":"F821","end_location":{"column":5,"row":1},"filename":"undef.py","fix":null,"location":{"column":4,"row":1},"message":"Undefined name `a`","noqa_row":1} diff --git a/crates/ruff_cli/src/commands/run.rs b/crates/ruff_cli/src/commands/run.rs index 533b7f120f..62a37352f5 100644 --- a/crates/ruff_cli/src/commands/run.rs +++ b/crates/ruff_cli/src/commands/run.rs @@ -236,93 +236,3 @@ with the relevant file contents, the `pyproject.toml` settings, and the followin } } } - -#[cfg(test)] -#[cfg(feature = "jupyter_notebook")] -mod test { - use std::path::PathBuf; - use std::str::FromStr; - - use anyhow::Result; - use path_absolutize::Absolutize; - - use ruff::logging::LogLevel; - use ruff::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy}; - use ruff::settings::configuration::{Configuration, RuleSelection}; - use ruff::settings::flags::FixMode; - use ruff::settings::flags::{Cache, Noqa}; - use ruff::settings::types::SerializationFormat; - use ruff::settings::AllSettings; - use ruff::RuleSelector; - - use crate::args::Overrides; - use crate::printer::{Flags, Printer}; - - use super::run; - - #[test] - fn test_jupyter_notebook_integration() -> Result<()> { - let overrides: Overrides = Overrides { - select: Some(vec![ - RuleSelector::from_str("B")?, - RuleSelector::from_str("F")?, - ]), - ..Default::default() - }; - - let mut configuration = Configuration::default(); - configuration.rule_selections.push(RuleSelection { - select: Some(vec![ - RuleSelector::from_str("B")?, - RuleSelector::from_str("F")?, - ]), - ..Default::default() - }); - - let root_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("ruff") - .join("resources") - .join("test") - .join("fixtures") - .join("jupyter"); - - let diagnostics = run( - &[root_path.join("valid.ipynb")], - &PyprojectConfig::new( - PyprojectDiscoveryStrategy::Fixed, - AllSettings::from_configuration(configuration, &root_path)?, - None, - ), - &overrides, - Cache::Disabled, - Noqa::Enabled, - FixMode::Generate, - )?; - - let printer = Printer::new( - SerializationFormat::Text, - LogLevel::Default, - FixMode::Generate, - Flags::SHOW_VIOLATIONS, - ); - let mut writer: Vec = Vec::new(); - // Mute the terminal color codes. - colored::control::set_override(false); - printer.write_once(&diagnostics, &mut writer)?; - // TODO(konstin): Set jupyter notebooks as none-fixable for now - // TODO(konstin): Make jupyter notebooks fixable - let expected = format!( - "{valid_ipynb}:cell 1:2:5: F841 [*] Local variable `x` is assigned to but never used -{valid_ipynb}:cell 3:1:24: B006 Do not use mutable data structures for argument defaults -Found 2 errors. -[*] 1 potentially fixable with the --fix option. -", - valid_ipynb = root_path.join("valid.ipynb").absolutize()?.display() - ); - - assert_eq!(expected, String::from_utf8(writer)?); - - Ok(()) - } -} diff --git a/crates/ruff_cli/tests/integration_test.rs b/crates/ruff_cli/tests/integration_test.rs index 033839d28f..14c49dcd36 100644 --- a/crates/ruff_cli/tests/integration_test.rs +++ b/crates/ruff_cli/tests/integration_test.rs @@ -91,33 +91,33 @@ fn stdin_json() -> Result<()> { r#"[ {{ "code": "F401", - "message": "`os` imported but unused", + "end_location": {{ + "column": 10, + "row": 1 + }}, + "filename": "{file_path}", "fix": {{ "applicability": "Automatic", - "message": "Remove unused import: `os`", "edits": [ {{ "content": "", - "location": {{ - "row": 1, - "column": 1 - }}, "end_location": {{ - "row": 2, - "column": 1 + "column": 1, + "row": 2 + }}, + "location": {{ + "column": 1, + "row": 1 }} }} - ] + ], + "message": "Remove unused import: `os`" }}, "location": {{ - "row": 1, - "column": 8 + "column": 8, + "row": 1 }}, - "end_location": {{ - "row": 1, - "column": 10 - }}, - "filename": "{file_path}", + "message": "`os` imported but unused", "noqa_row": 1 }} ]"# From ddfdc3bb01094bc908887c483e6961ef1671bac4 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 19 Jun 2023 17:09:15 -0400 Subject: [PATCH 112/447] Add rule documentation URL to JSON output (#5187) ## Summary I want to include URLs to the rule documentation in the LSP (the LSP has a native `code_description` field for this, which, if specified, causes the source to be rendered as a link to the docs). This PR exposes the URL to the documentation in the Ruff JSON output. --- Cargo.toml | 4 ++-- crates/ruff/src/message/json.rs | 1 + .../snapshots/ruff__message__json__tests__output.snap | 9 ++++++--- .../ruff__message__json_lines__tests__output.snap | 6 +++--- crates/ruff/src/registry.rs | 7 +++++++ crates/ruff_cli/tests/integration_test.rs | 3 ++- 6 files changed, 21 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 42c0957d67..bed4b47b45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,8 +5,8 @@ resolver = "2" [workspace.package] edition = "2021" rust-version = "1.70" -homepage = "https://beta.ruff.rs/docs/" -documentation = "https://beta.ruff.rs/docs/" +homepage = "https://beta.ruff.rs/docs" +documentation = "https://beta.ruff.rs/docs" repository = "https://github.com/astral-sh/ruff" authors = ["Charlie Marsh "] license = "MIT" diff --git a/crates/ruff/src/message/json.rs b/crates/ruff/src/message/json.rs index 47008eb64a..835d6ec067 100644 --- a/crates/ruff/src/message/json.rs +++ b/crates/ruff/src/message/json.rs @@ -63,6 +63,7 @@ pub(crate) fn message_to_json_value(message: &Message) -> Value { json!({ "code": message.kind.rule().noqa_code().to_string(), + "url": message.kind.rule().url(), "message": message.kind.body, "fix": fix, "location": start_location, diff --git a/crates/ruff/src/message/snapshots/ruff__message__json__tests__output.snap b/crates/ruff/src/message/snapshots/ruff__message__json__tests__output.snap index e0daded40c..de7b932dbe 100644 --- a/crates/ruff/src/message/snapshots/ruff__message__json__tests__output.snap +++ b/crates/ruff/src/message/snapshots/ruff__message__json__tests__output.snap @@ -32,7 +32,8 @@ expression: content "row": 1 }, "message": "`os` imported but unused", - "noqa_row": 1 + "noqa_row": 1, + "url": "https://beta.ruff.rs/docs/rules/unused-import" }, { "code": "F841", @@ -63,7 +64,8 @@ expression: content "row": 6 }, "message": "Local variable `x` is assigned to but never used", - "noqa_row": 6 + "noqa_row": 6, + "url": "https://beta.ruff.rs/docs/rules/unused-variable" }, { "code": "F821", @@ -78,6 +80,7 @@ expression: content "row": 1 }, "message": "Undefined name `a`", - "noqa_row": 1 + "noqa_row": 1, + "url": "https://beta.ruff.rs/docs/rules/undefined-name" } ] diff --git a/crates/ruff/src/message/snapshots/ruff__message__json_lines__tests__output.snap b/crates/ruff/src/message/snapshots/ruff__message__json_lines__tests__output.snap index d4433a8e7d..90749c6753 100644 --- a/crates/ruff/src/message/snapshots/ruff__message__json_lines__tests__output.snap +++ b/crates/ruff/src/message/snapshots/ruff__message__json_lines__tests__output.snap @@ -2,7 +2,7 @@ source: crates/ruff/src/message/json_lines.rs expression: content --- -{"code":"F401","end_location":{"column":10,"row":1},"filename":"fib.py","fix":{"applicability":"Suggested","edits":[{"content":"","end_location":{"column":1,"row":2},"location":{"column":1,"row":1}}],"message":"Remove unused import: `os`"},"location":{"column":8,"row":1},"message":"`os` imported but unused","noqa_row":1} -{"code":"F841","end_location":{"column":6,"row":6},"filename":"fib.py","fix":{"applicability":"Suggested","edits":[{"content":"","end_location":{"column":10,"row":6},"location":{"column":5,"row":6}}],"message":"Remove assignment to unused variable `x`"},"location":{"column":5,"row":6},"message":"Local variable `x` is assigned to but never used","noqa_row":6} -{"code":"F821","end_location":{"column":5,"row":1},"filename":"undef.py","fix":null,"location":{"column":4,"row":1},"message":"Undefined name `a`","noqa_row":1} +{"code":"F401","end_location":{"column":10,"row":1},"filename":"fib.py","fix":{"applicability":"Suggested","edits":[{"content":"","end_location":{"column":1,"row":2},"location":{"column":1,"row":1}}],"message":"Remove unused import: `os`"},"location":{"column":8,"row":1},"message":"`os` imported but unused","noqa_row":1,"url":"https://beta.ruff.rs/docs/rules/unused-import"} +{"code":"F841","end_location":{"column":6,"row":6},"filename":"fib.py","fix":{"applicability":"Suggested","edits":[{"content":"","end_location":{"column":10,"row":6},"location":{"column":5,"row":6}}],"message":"Remove assignment to unused variable `x`"},"location":{"column":5,"row":6},"message":"Local variable `x` is assigned to but never used","noqa_row":6,"url":"https://beta.ruff.rs/docs/rules/unused-variable"} +{"code":"F821","end_location":{"column":5,"row":1},"filename":"undef.py","fix":null,"location":{"column":4,"row":1},"message":"Undefined name `a`","noqa_row":1,"url":"https://beta.ruff.rs/docs/rules/undefined-name"} diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 7e0fff6b37..dd47f24815 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -347,6 +347,13 @@ impl Rule { _ => LintSource::Ast, } } + + /// Return the URL for the rule documentation, if it exists. + pub fn url(&self) -> Option { + self.explanation() + .is_some() + .then(|| format!("{}/rules/{}", env!("CARGO_PKG_HOMEPAGE"), self.as_ref())) + } } /// Pairs of checks that shouldn't be enabled together. diff --git a/crates/ruff_cli/tests/integration_test.rs b/crates/ruff_cli/tests/integration_test.rs index 14c49dcd36..674b2fd5a2 100644 --- a/crates/ruff_cli/tests/integration_test.rs +++ b/crates/ruff_cli/tests/integration_test.rs @@ -118,7 +118,8 @@ fn stdin_json() -> Result<()> { "row": 1 }}, "message": "`os` imported but unused", - "noqa_row": 1 + "noqa_row": 1, + "url": "https://beta.ruff.rs/docs/rules/unused-import" }} ]"# ) From 36e01ad6eb928580545376e40742cb31334527fd Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 19 Jun 2023 17:09:53 -0400 Subject: [PATCH 113/447] Upgrade RustPython (#5192) ## Summary This PR upgrade RustPython to pull in the changes to `Arguments` (zip defaults with their identifiers) and all the renames to `CmpOp` and friends. --- Cargo.lock | 13 +- Cargo.toml | 10 +- crates/ruff/src/autofix/edits.rs | 4 +- crates/ruff/src/checkers/ast/mod.rs | 105 +++++----- .../src/rules/flake8_2020/rules/compare.rs | 12 +- .../flake8_annotations/rules/definition.rs | 48 +++-- .../rules/hardcoded_password_default.rs | 32 ++- .../rules/try_except_continue.rs | 6 +- .../flake8_bandit/rules/try_except_pass.rs | 6 +- ...an_default_value_in_function_definition.rs | 18 +- .../rules/check_positional_boolean_in_def.rs | 15 +- .../rules/assert_raises_exception.rs | 4 +- .../rules/duplicate_exceptions.rs | 6 +- .../rules/except_with_empty_tuple.rs | 14 +- .../except_with_non_exception_classes.rs | 7 +- .../rules/function_call_argument_default.rs | 17 +- .../rules/loop_variable_overrides_iterator.rs | 15 +- .../rules/mutable_argument_default.rs | 28 +-- .../redundant_tuple_in_exception_handler.rs | 6 +- .../rules/unary_prefix_increment.rs | 8 +- .../ruff/src/rules/flake8_builtins/helpers.rs | 8 +- .../rules/unnecessary_subscript_reversal.rs | 4 +- .../rules/multiple_starts_ends_with.rs | 6 +- .../flake8_pyi/rules/any_eq_ne_annotation.rs | 2 +- .../rules/bad_version_info_comparison.rs | 6 +- .../rules/no_return_argument_annotation.rs | 13 +- .../rules/flake8_pyi/rules/simple_defaults.rs | 176 ++++++---------- .../flake8_pyi/rules/unrecognized_platform.rs | 6 +- .../flake8_pytest_style/rules/assertion.rs | 26 +-- .../flake8_pytest_style/rules/fixture.rs | 37 ++-- .../rules/flake8_pytest_style/rules/raises.rs | 4 +- .../rules/unittest_assert.rs | 48 ++--- .../flake8_simplify/rules/ast_bool_op.rs | 32 +-- .../src/rules/flake8_simplify/rules/ast_if.rs | 10 +- .../rules/flake8_simplify/rules/ast_ifexp.rs | 4 +- .../flake8_simplify/rules/ast_unary_op.rs | 24 +-- .../rules/flake8_simplify/rules/ast_with.rs | 4 +- .../flake8_simplify/rules/key_in_dict.rs | 6 +- .../rules/reimplemented_builtin.rs | 28 +-- .../rules/return_in_try_except_finally.rs | 6 +- .../rules/suppressible_exception.rs | 6 +- .../flake8_simplify/rules/yoda_conditions.rs | 8 +- .../rules/unused_arguments.rs | 10 +- crates/ruff/src/rules/isort/block.rs | 12 +- .../mccabe/rules/function_is_too_complex.rs | 4 +- ...id_first_argument_name_for_class_method.rs | 13 +- .../invalid_first_argument_name_for_method.rs | 4 +- crates/ruff/src/rules/pycodestyle/helpers.rs | 4 +- .../rules/pycodestyle/rules/bare_except.rs | 4 +- .../pycodestyle/rules/lambda_assignment.rs | 28 ++- .../pycodestyle/rules/literal_comparisons.rs | 80 ++++---- .../src/rules/pycodestyle/rules/not_tests.rs | 16 +- .../pycodestyle/rules/type_comparison.rs | 6 +- .../src/rules/pydocstyle/rules/sections.rs | 14 +- crates/ruff/src/rules/pyflakes/fixes.rs | 8 +- .../pyflakes/rules/default_except_not_last.rs | 6 +- .../rules/invalid_literal_comparisons.rs | 46 ++--- .../rules/pyflakes/rules/unused_variable.rs | 4 +- crates/ruff/src/rules/pylint/helpers.rs | 34 ++-- .../rules/pylint/rules/binary_op_exception.rs | 23 ++- .../pylint/rules/compare_to_empty_string.rs | 26 +-- .../pylint/rules/comparison_of_constant.rs | 10 +- .../pylint/rules/comparison_with_itself.rs | 10 +- .../pylint/rules/magic_value_comparison.rs | 4 +- .../pylint/rules/property_with_parameters.rs | 16 +- .../rules/pylint/rules/redefined_loop_name.rs | 4 +- .../pylint/rules/repeated_isinstance_calls.rs | 4 +- .../rules/pylint/rules/too_many_arguments.rs | 14 +- .../rules/pylint/rules/too_many_branches.rs | 6 +- .../rules/pylint/rules/too_many_statements.rs | 4 +- .../unexpected_special_method_signature.rs | 3 +- .../pylint/rules/useless_else_on_loop.rs | 4 +- .../rules/pyupgrade/rules/os_error_alias.rs | 6 +- .../pyupgrade/rules/outdated_version_block.rs | 14 +- .../rules/super_call_with_parameters.rs | 7 +- .../src/rules/ruff/rules/implicit_optional.rs | 31 ++- .../rules/ruff/rules/pairwise_over_zipped.rs | 4 +- .../rules/error_instead_of_exception.rs | 6 +- .../tryceratops/rules/raise_within_try.rs | 4 +- .../tryceratops/rules/try_consider_else.rs | 4 +- .../tryceratops/rules/useless_try_except.rs | 8 +- .../tryceratops/rules/verbose_log_message.rs | 6 +- .../rules/tryceratops/rules/verbose_raise.rs | 6 +- crates/ruff_python_ast/src/comparable.rs | 115 ++++++----- crates/ruff_python_ast/src/helpers.rs | 148 +++++++------- crates/ruff_python_ast/src/identifier.rs | 31 ++- crates/ruff_python_ast/src/node.rs | 190 ++++++++++++------ .../src/source_code/generator.rs | 76 ++++--- .../ruff_python_ast/src/statement_visitor.rs | 22 +- crates/ruff_python_ast/src/visitor.rs | 95 +++++---- .../ruff_python_ast/src/visitor/preorder.rs | 126 ++++++------ .../src/comments/placement.rs | 47 ++--- ...ormatter__comments__tests__try_except.snap | 2 +- ...ments__tests__try_except_finally_else.snap | 2 +- .../src/comments/visitor.rs | 16 +- .../src/expression/expr_bin_op.rs | 4 +- crates/ruff_python_formatter/src/generated.rs | 88 +++++--- .../src/other/arg_with_default.rs | 28 +++ .../src/other/arguments.rs | 86 +++----- ...er.rs => except_handler_except_handler.rs} | 8 +- crates/ruff_python_formatter/src/other/mod.rs | 5 +- .../src/other/{withitem.rs => with_item.rs} | 8 +- .../src/analyze/branch_detection.rs | 4 +- 103 files changed, 1291 insertions(+), 1165 deletions(-) create mode 100644 crates/ruff_python_formatter/src/other/arg_with_default.rs rename crates/ruff_python_formatter/src/other/{excepthandler_except_handler.rs => except_handler_except_handler.rs} (54%) rename crates/ruff_python_formatter/src/other/{withitem.rs => with_item.rs} (53%) diff --git a/Cargo.lock b/Cargo.lock index 09fac32aaf..62ddd69515 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2105,7 +2105,7 @@ dependencies = [ [[package]] name = "ruff_text_size" version = "0.0.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=0dc8fdf52d146698c5bcf0b842fddc9e398ad8db#0dc8fdf52d146698c5bcf0b842fddc9e398ad8db" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8d74eee75031b68d2204219963fae54a3f31a394#8d74eee75031b68d2204219963fae54a3f31a394" dependencies = [ "schemars", "serde", @@ -2183,7 +2183,7 @@ dependencies = [ [[package]] name = "rustpython-ast" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=0dc8fdf52d146698c5bcf0b842fddc9e398ad8db#0dc8fdf52d146698c5bcf0b842fddc9e398ad8db" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8d74eee75031b68d2204219963fae54a3f31a394#8d74eee75031b68d2204219963fae54a3f31a394" dependencies = [ "is-macro", "num-bigint", @@ -2194,7 +2194,7 @@ dependencies = [ [[package]] name = "rustpython-format" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=0dc8fdf52d146698c5bcf0b842fddc9e398ad8db#0dc8fdf52d146698c5bcf0b842fddc9e398ad8db" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8d74eee75031b68d2204219963fae54a3f31a394#8d74eee75031b68d2204219963fae54a3f31a394" dependencies = [ "bitflags 2.3.1", "itertools", @@ -2206,7 +2206,7 @@ dependencies = [ [[package]] name = "rustpython-literal" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=0dc8fdf52d146698c5bcf0b842fddc9e398ad8db#0dc8fdf52d146698c5bcf0b842fddc9e398ad8db" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8d74eee75031b68d2204219963fae54a3f31a394#8d74eee75031b68d2204219963fae54a3f31a394" dependencies = [ "hexf-parse", "is-macro", @@ -2218,7 +2218,7 @@ dependencies = [ [[package]] name = "rustpython-parser" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=0dc8fdf52d146698c5bcf0b842fddc9e398ad8db#0dc8fdf52d146698c5bcf0b842fddc9e398ad8db" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8d74eee75031b68d2204219963fae54a3f31a394#8d74eee75031b68d2204219963fae54a3f31a394" dependencies = [ "anyhow", "is-macro", @@ -2241,9 +2241,10 @@ dependencies = [ [[package]] name = "rustpython-parser-core" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=0dc8fdf52d146698c5bcf0b842fddc9e398ad8db#0dc8fdf52d146698c5bcf0b842fddc9e398ad8db" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8d74eee75031b68d2204219963fae54a3f31a394#8d74eee75031b68d2204219963fae54a3f31a394" dependencies = [ "is-macro", + "memchr", "ruff_text_size", ] diff --git a/Cargo.toml b/Cargo.toml index bed4b47b45..b1bd3edc84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,11 +36,11 @@ proc-macro2 = { version = "1.0.51" } quote = { version = "1.0.23" } regex = { version = "1.7.1" } rustc-hash = { version = "1.1.0" } -ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "0dc8fdf52d146698c5bcf0b842fddc9e398ad8db" } -rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "0dc8fdf52d146698c5bcf0b842fddc9e398ad8db", default-features = false, features = ["all-nodes-with-ranges"]} -rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "0dc8fdf52d146698c5bcf0b842fddc9e398ad8db" } -rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "0dc8fdf52d146698c5bcf0b842fddc9e398ad8db" } -rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "0dc8fdf52d146698c5bcf0b842fddc9e398ad8db", default-features = false, features = ["full-lexer", "all-nodes-with-ranges"] } +ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8d74eee75031b68d2204219963fae54a3f31a394" } +rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8d74eee75031b68d2204219963fae54a3f31a394" , default-features = false, features = ["all-nodes-with-ranges", "num-bigint"]} +rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8d74eee75031b68d2204219963fae54a3f31a394", default-features = false, features = ["num-bigint"] } +rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8d74eee75031b68d2204219963fae54a3f31a394" } +rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8d74eee75031b68d2204219963fae54a3f31a394" , default-features = false, features = ["full-lexer", "all-nodes-with-ranges", "num-bigint"] } schemars = { version = "0.8.12" } serde = { version = "1.0.152", features = ["derive"] } serde_json = { version = "1.0.93" } diff --git a/crates/ruff/src/autofix/edits.rs b/crates/ruff/src/autofix/edits.rs index 623e1876ed..c6218e5060 100644 --- a/crates/ruff/src/autofix/edits.rs +++ b/crates/ruff/src/autofix/edits.rs @@ -1,7 +1,7 @@ //! Interface for generating autofix edits from higher-level actions (e.g., "remove an argument"). use anyhow::{bail, Result}; use ruff_text_size::{TextLen, TextRange, TextSize}; -use rustpython_parser::ast::{self, Excepthandler, Expr, Keyword, Ranged, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, Expr, Keyword, Ranged, Stmt}; use rustpython_parser::{lexer, Mode, Tok}; use ruff_diagnostics::Edit; @@ -218,7 +218,7 @@ fn is_lone_child(child: &Stmt, parent: &Stmt) -> bool { || is_only(orelse, child) || is_only(finalbody, child) || handlers.iter().any(|handler| match handler { - Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { + ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) => is_only(body, child), }) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index f3a805bfdb..155d2f8c1f 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -5,8 +5,8 @@ use log::error; use ruff_text_size::{TextRange, TextSize}; use rustpython_format::cformat::{CFormatError, CFormatErrorType}; use rustpython_parser::ast::{ - self, Arg, Arguments, Comprehension, Constant, Excepthandler, Expr, ExprContext, Keyword, - Operator, Pattern, Ranged, Stmt, Suite, Unaryop, + self, Arg, ArgWithDefault, Arguments, Comprehension, Constant, ExceptHandler, Expr, + ExprContext, Keyword, Operator, Pattern, Ranged, Stmt, Suite, UnaryOp, }; use ruff_diagnostics::{Diagnostic, Fix, IsolationLevel}; @@ -17,7 +17,7 @@ use ruff_python_ast::source_code::{Generator, Indexer, Locator, Quote, Stylist}; use ruff_python_ast::str::trailing_quote; use ruff_python_ast::types::Node; use ruff_python_ast::typing::{parse_type_annotation, AnnotationKind}; -use ruff_python_ast::visitor::{walk_excepthandler, walk_pattern, Visitor}; +use ruff_python_ast::visitor::{walk_except_handler, walk_pattern, Visitor}; use ruff_python_ast::{cast, helpers, identifier, str, visitor}; use ruff_python_semantic::analyze::{branch_detection, typing, visibility}; use ruff_python_semantic::{ @@ -1759,22 +1759,21 @@ where // are enabled. let runtime_annotation = !self.semantic.future_annotations(); - for arg in &args.posonlyargs { - if let Some(expr) = &arg.annotation { + for arg_with_default in args + .posonlyargs + .iter() + .chain(&args.args) + .chain(&args.kwonlyargs) + { + if let Some(expr) = &arg_with_default.def.annotation { if runtime_annotation { self.visit_type_definition(expr); } else { self.visit_annotation(expr); }; } - } - for arg in &args.args { - if let Some(expr) = &arg.annotation { - if runtime_annotation { - self.visit_type_definition(expr); - } else { - self.visit_annotation(expr); - }; + if let Some(expr) = &arg_with_default.default { + self.visit_expr(expr); } } if let Some(arg) = &args.vararg { @@ -1786,15 +1785,6 @@ where }; } } - for arg in &args.kwonlyargs { - if let Some(expr) = &arg.annotation { - if runtime_annotation { - self.visit_type_definition(expr); - } else { - self.visit_annotation(expr); - }; - } - } if let Some(arg) = &args.kwarg { if let Some(expr) = &arg.annotation { if runtime_annotation { @@ -1811,12 +1801,6 @@ where self.visit_annotation(expr); }; } - for expr in &args.kw_defaults { - self.visit_expr(expr); - } - for expr in &args.defaults { - self.visit_expr(expr); - } self.add_binding( name, @@ -1929,8 +1913,8 @@ where self.semantic.handled_exceptions.pop(); self.semantic.flags |= SemanticModelFlags::EXCEPTION_HANDLER; - for excepthandler in handlers { - self.visit_excepthandler(excepthandler); + for except_handler in handlers { + self.visit_except_handler(except_handler); } self.visit_body(orelse); @@ -2100,7 +2084,7 @@ where expr, Expr::BoolOp(_) | Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::Not, + op: UnaryOp::Not, .. }) ) { @@ -3301,12 +3285,21 @@ where } // Visit the default arguments, but avoid the body, which will be deferred. - for expr in &args.kw_defaults { - self.visit_expr(expr); - } - for expr in &args.defaults { - self.visit_expr(expr); + for ArgWithDefault { + default, + def: _, + range: _, + } in args + .posonlyargs + .iter() + .chain(&args.args) + .chain(&args.kwonlyargs) + { + if let Some(expr) = &default { + self.visit_expr(expr); + } } + self.semantic.push_scope(ScopeKind::Lambda(lambda)); } Expr::IfExp(ast::ExprIfExp { @@ -3794,9 +3787,9 @@ where self.semantic.pop_expr(); } - fn visit_excepthandler(&mut self, excepthandler: &'b Excepthandler) { - match excepthandler { - Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { + fn visit_except_handler(&mut self, except_handler: &'b ExceptHandler) { + match except_handler { + ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, name, body, @@ -3807,7 +3800,7 @@ where if let Some(diagnostic) = pycodestyle::rules::bare_except( type_.as_deref(), body, - excepthandler, + except_handler, self.locator, ) { self.diagnostics.push(diagnostic); @@ -3822,7 +3815,7 @@ where if self.enabled(Rule::TryExceptPass) { flake8_bandit::rules::try_except_pass( self, - excepthandler, + except_handler, type_.as_deref(), name, body, @@ -3832,7 +3825,7 @@ where if self.enabled(Rule::TryExceptContinue) { flake8_bandit::rules::try_except_continue( self, - excepthandler, + except_handler, type_.as_deref(), name, body, @@ -3840,20 +3833,20 @@ where ); } if self.enabled(Rule::ExceptWithEmptyTuple) { - flake8_bugbear::rules::except_with_empty_tuple(self, excepthandler); + flake8_bugbear::rules::except_with_empty_tuple(self, except_handler); } if self.enabled(Rule::ExceptWithNonExceptionClasses) { - flake8_bugbear::rules::except_with_non_exception_classes(self, excepthandler); + flake8_bugbear::rules::except_with_non_exception_classes(self, except_handler); } if self.enabled(Rule::ReraiseNoCause) { tryceratops::rules::reraise_no_cause(self, body); } if self.enabled(Rule::BinaryOpException) { - pylint::rules::binary_op_exception(self, excepthandler); + pylint::rules::binary_op_exception(self, except_handler); } match name { Some(name) => { - let range = excepthandler.try_identifier(self.locator).unwrap(); + let range = except_handler.try_identifier(self.locator).unwrap(); if self.enabled(Rule::AmbiguousVariableName) { if let Some(diagnostic) = @@ -3866,7 +3859,7 @@ where flake8_builtins::rules::builtin_variable_shadowing( self, name, - AnyShadowing::from(excepthandler), + AnyShadowing::from(except_handler), ); } @@ -3878,7 +3871,7 @@ where BindingFlags::empty(), ); - walk_excepthandler(self, excepthandler); + walk_except_handler(self, except_handler); // Remove it from the scope immediately after. self.add_binding( @@ -3898,7 +3891,7 @@ where if self.patch(Rule::UnusedVariable) { diagnostic.try_set_fix(|| { pyflakes::fixes::remove_exception_handler_assignment( - excepthandler, + except_handler, self.locator, ) .map(Fix::automatic) @@ -3908,7 +3901,7 @@ where } } } - None => walk_excepthandler(self, excepthandler), + None => walk_except_handler(self, except_handler), } } } @@ -3946,17 +3939,17 @@ where // Bind, but intentionally avoid walking default expressions, as we handle them // upstream. - for arg in &arguments.posonlyargs { - self.visit_arg(arg); + for arg_with_default in &arguments.posonlyargs { + self.visit_arg(&arg_with_default.def); } - for arg in &arguments.args { - self.visit_arg(arg); + for arg_with_default in &arguments.args { + self.visit_arg(&arg_with_default.def); } if let Some(arg) = &arguments.vararg { self.visit_arg(arg); } - for arg in &arguments.kwonlyargs { - self.visit_arg(arg); + for arg_with_default in &arguments.kwonlyargs { + self.visit_arg(&arg_with_default.def); } if let Some(arg) = &arguments.kwarg { self.visit_arg(arg); diff --git a/crates/ruff/src/rules/flake8_2020/rules/compare.rs b/crates/ruff/src/rules/flake8_2020/rules/compare.rs index 185551481c..1e083b48a1 100644 --- a/crates/ruff/src/rules/flake8_2020/rules/compare.rs +++ b/crates/ruff/src/rules/flake8_2020/rules/compare.rs @@ -1,5 +1,5 @@ use num_bigint::BigInt; -use rustpython_parser::ast::{self, Cmpop, Constant, Expr, Ranged}; +use rustpython_parser::ast::{self, CmpOp, Constant, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -66,7 +66,7 @@ impl Violation for SysVersionCmpStr10 { } /// YTT103, YTT201, YTT203, YTT204, YTT302 -pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &[Expr]) { +pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[CmpOp], comparators: &[Expr]) { match left { Expr::Subscript(ast::ExprSubscript { value, slice, .. }) if is_sys(value, "version_info", checker.semantic()) => @@ -78,7 +78,7 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], compara { if *i == BigInt::from(0) { if let ( - [Cmpop::Eq | Cmpop::NotEq], + [CmpOp::Eq | CmpOp::NotEq], [Expr::Constant(ast::ExprConstant { value: Constant::Int(n), .. @@ -93,7 +93,7 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], compara } } else if *i == BigInt::from(1) { if let ( - [Cmpop::Lt | Cmpop::LtE | Cmpop::Gt | Cmpop::GtE], + [CmpOp::Lt | CmpOp::LtE | CmpOp::Gt | CmpOp::GtE], [Expr::Constant(ast::ExprConstant { value: Constant::Int(_), .. @@ -114,7 +114,7 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], compara if is_sys(value, "version_info", checker.semantic()) && attr == "minor" => { if let ( - [Cmpop::Lt | Cmpop::LtE | Cmpop::Gt | Cmpop::GtE], + [CmpOp::Lt | CmpOp::LtE | CmpOp::Gt | CmpOp::GtE], [Expr::Constant(ast::ExprConstant { value: Constant::Int(_), .. @@ -134,7 +134,7 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], compara if is_sys(left, "version", checker.semantic()) { if let ( - [Cmpop::Lt | Cmpop::LtE | Cmpop::Gt | Cmpop::GtE], + [CmpOp::Lt | CmpOp::LtE | CmpOp::Gt | CmpOp::GtE], [Expr::Constant(ast::ExprConstant { value: Constant::Str(s), .. diff --git a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs index 22d63d89a3..b22555ce78 100644 --- a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{Expr, Ranged, Stmt}; +use rustpython_parser::ast::{ArgWithDefault, Expr, Ranged, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -471,7 +471,7 @@ pub(crate) fn definition( _ => return vec![], }; - let (name, args, returns, body, decorator_list) = match_function_def(stmt); + let (name, arguments, returns, body, decorator_list) = match_function_def(stmt); // Keep track of whether we've seen any typed arguments or return values. let mut has_any_typed_arg = false; // Any argument has been typed? let mut has_typed_return = false; // Return value has been typed? @@ -484,11 +484,15 @@ pub(crate) fn definition( let is_overridden = visibility::is_override(decorator_list, checker.semantic()); // ANN001, ANN401 - for arg in args + for ArgWithDefault { + def, + default: _, + range: _, + } in arguments .posonlyargs .iter() - .chain(args.args.iter()) - .chain(args.kwonlyargs.iter()) + .chain(&arguments.args) + .chain(&arguments.kwonlyargs) .skip( // If this is a non-static method, skip `cls` or `self`. usize::from( @@ -498,12 +502,12 @@ pub(crate) fn definition( ) { // ANN401 for dynamically typed arguments - if let Some(annotation) = &arg.annotation { + if let Some(annotation) = &def.annotation { has_any_typed_arg = true; if checker.enabled(Rule::AnyType) { check_dynamically_typed( annotation, - || arg.arg.to_string(), + || def.arg.to_string(), &mut diagnostics, is_overridden, checker.semantic(), @@ -511,14 +515,14 @@ pub(crate) fn definition( } } else { if !(checker.settings.flake8_annotations.suppress_dummy_args - && checker.settings.dummy_variable_rgx.is_match(&arg.arg)) + && checker.settings.dummy_variable_rgx.is_match(&def.arg)) { if checker.enabled(Rule::MissingTypeFunctionArgument) { diagnostics.push(Diagnostic::new( MissingTypeFunctionArgument { - name: arg.arg.to_string(), + name: def.arg.to_string(), }, - arg.range(), + def.range(), )); } } @@ -526,7 +530,7 @@ pub(crate) fn definition( } // ANN002, ANN401 - if let Some(arg) = &args.vararg { + if let Some(arg) = &arguments.vararg { if let Some(expr) = &arg.annotation { has_any_typed_arg = true; if !checker.settings.flake8_annotations.allow_star_arg_any { @@ -558,7 +562,7 @@ pub(crate) fn definition( } // ANN003, ANN401 - if let Some(arg) = &args.kwarg { + if let Some(arg) = &arguments.kwarg { if let Some(expr) = &arg.annotation { has_any_typed_arg = true; if !checker.settings.flake8_annotations.allow_star_arg_any { @@ -591,24 +595,32 @@ pub(crate) fn definition( // ANN101, ANN102 if is_method && !visibility::is_staticmethod(cast::decorator_list(stmt), checker.semantic()) { - if let Some(arg) = args.posonlyargs.first().or_else(|| args.args.first()) { - if arg.annotation.is_none() { + if let Some(ArgWithDefault { + def, + default: _, + range: _, + }) = arguments + .posonlyargs + .first() + .or_else(|| arguments.args.first()) + { + if def.annotation.is_none() { if visibility::is_classmethod(cast::decorator_list(stmt), checker.semantic()) { if checker.enabled(Rule::MissingTypeCls) { diagnostics.push(Diagnostic::new( MissingTypeCls { - name: arg.arg.to_string(), + name: def.arg.to_string(), }, - arg.range(), + def.range(), )); } } else { if checker.enabled(Rule::MissingTypeSelf) { diagnostics.push(Diagnostic::new( MissingTypeSelf { - name: arg.arg.to_string(), + name: def.arg.to_string(), }, - arg.range(), + def.range(), )); } } 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 0f61c414df..127ce31199 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,4 +1,4 @@ -use rustpython_parser::ast::{Arg, Arguments, Expr, Ranged}; +use rustpython_parser::ast::{Arg, ArgWithDefault, Arguments, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -39,29 +39,21 @@ fn check_password_kwarg(arg: &Arg, default: &Expr) -> Option { pub(crate) fn hardcoded_password_default(arguments: &Arguments) -> Vec { let mut diagnostics: Vec = Vec::new(); - let defaults_start = - arguments.posonlyargs.len() + arguments.args.len() - arguments.defaults.len(); - for (i, arg) in arguments + for ArgWithDefault { + def, + default, + range: _, + } in arguments .posonlyargs .iter() .chain(&arguments.args) - .enumerate() + .chain(&arguments.kwonlyargs) { - if let Some(i) = i.checked_sub(defaults_start) { - let default = &arguments.defaults[i]; - if let Some(diagnostic) = check_password_kwarg(arg, default) { - diagnostics.push(diagnostic); - } - } - } - - let defaults_start = arguments.kwonlyargs.len() - arguments.kw_defaults.len(); - for (i, kwarg) in arguments.kwonlyargs.iter().enumerate() { - if let Some(i) = i.checked_sub(defaults_start) { - let default = &arguments.kw_defaults[i]; - if let Some(diagnostic) = check_password_kwarg(kwarg, default) { - diagnostics.push(diagnostic); - } + let Some(default) = default else { + continue; + }; + if let Some(diagnostic) = check_password_kwarg(def, default) { + diagnostics.push(diagnostic); } } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/try_except_continue.rs b/crates/ruff/src/rules/flake8_bandit/rules/try_except_continue.rs index e2ce1f3cfa..5a4e4faec4 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/try_except_continue.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/try_except_continue.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{Excepthandler, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{ExceptHandler, Expr, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -19,7 +19,7 @@ impl Violation for TryExceptContinue { /// S112 pub(crate) fn try_except_continue( checker: &mut Checker, - excepthandler: &Excepthandler, + except_handler: &ExceptHandler, type_: Option<&Expr>, _name: Option<&str>, body: &[Stmt], @@ -31,6 +31,6 @@ pub(crate) fn try_except_continue( { checker .diagnostics - .push(Diagnostic::new(TryExceptContinue, excepthandler.range())); + .push(Diagnostic::new(TryExceptContinue, except_handler.range())); } } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/try_except_pass.rs b/crates/ruff/src/rules/flake8_bandit/rules/try_except_pass.rs index a89c41b240..ee4bd533d3 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/try_except_pass.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/try_except_pass.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{Excepthandler, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{ExceptHandler, Expr, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -19,7 +19,7 @@ impl Violation for TryExceptPass { /// S110 pub(crate) fn try_except_pass( checker: &mut Checker, - excepthandler: &Excepthandler, + except_handler: &ExceptHandler, type_: Option<&Expr>, _name: Option<&str>, body: &[Stmt], @@ -31,6 +31,6 @@ pub(crate) fn try_except_pass( { checker .diagnostics - .push(Diagnostic::new(TryExceptPass, excepthandler.range())); + .push(Diagnostic::new(TryExceptPass, except_handler.range())); } } diff --git a/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_default_value_in_function_definition.rs b/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_default_value_in_function_definition.rs index a920ffb129..fda0ff5af4 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_default_value_in_function_definition.rs +++ b/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_default_value_in_function_definition.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{Arguments, Decorator}; +use rustpython_parser::ast::{ArgWithDefault, Arguments, Decorator}; use ruff_diagnostics::Violation; @@ -75,7 +75,19 @@ pub(crate) fn check_boolean_default_value_in_function_definition( return; } - for arg in &arguments.defaults { - add_if_boolean(checker, arg, BooleanDefaultValueInFunctionDefinition.into()); + for ArgWithDefault { + def: _, + default, + range: _, + } in arguments.args.iter().chain(&arguments.posonlyargs) + { + let Some(default) = default else { + continue; + }; + add_if_boolean( + checker, + default, + BooleanDefaultValueInFunctionDefinition.into(), + ); } } diff --git a/crates/ruff/src/rules/flake8_boolean_trap/rules/check_positional_boolean_in_def.rs b/crates/ruff/src/rules/flake8_boolean_trap/rules/check_positional_boolean_in_def.rs index 20352a888e..57e50e7be7 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/rules/check_positional_boolean_in_def.rs +++ b/crates/ruff/src/rules/flake8_boolean_trap/rules/check_positional_boolean_in_def.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Arguments, Constant, Decorator, Expr, Ranged}; +use rustpython_parser::ast::{self, ArgWithDefault, Arguments, Constant, Decorator, Expr, Ranged}; use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Violation; @@ -93,11 +93,16 @@ pub(crate) fn check_positional_boolean_in_def( return; } - for arg in arguments.posonlyargs.iter().chain(arguments.args.iter()) { - if arg.annotation.is_none() { + for ArgWithDefault { + def, + default: _, + range: _, + } in arguments.posonlyargs.iter().chain(&arguments.args) + { + if def.annotation.is_none() { continue; } - let Some(expr) = &arg.annotation else { + let Some(expr) = &def.annotation else { continue; }; @@ -115,7 +120,7 @@ pub(crate) fn check_positional_boolean_in_def( } checker.diagnostics.push(Diagnostic::new( BooleanPositionalArgInFunctionDefinition, - arg.range(), + def.range(), )); } } 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 aae68f9b41..7dfb6c6d91 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,4 +1,4 @@ -use rustpython_parser::ast::{self, Expr, Ranged, Stmt, Withitem}; +use rustpython_parser::ast::{self, Expr, Ranged, Stmt, WithItem}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -50,7 +50,7 @@ impl Violation for AssertRaisesException { } /// B017 -pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: &[Withitem]) { +pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: &[WithItem]) { let Some(item) = items.first() else { return; }; 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 39b20692a1..8a7e90df79 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs @@ -1,7 +1,7 @@ use itertools::Itertools; use ruff_text_size::TextRange; use rustc_hash::{FxHashMap, FxHashSet}; -use rustpython_parser::ast::{self, Excepthandler, Expr, ExprContext, Ranged}; +use rustpython_parser::ast::{self, ExceptHandler, Expr, ExprContext, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix}; @@ -105,11 +105,11 @@ fn duplicate_handler_exceptions<'a>( seen } -pub(crate) fn duplicate_exceptions(checker: &mut Checker, handlers: &[Excepthandler]) { +pub(crate) fn duplicate_exceptions(checker: &mut Checker, handlers: &[ExceptHandler]) { 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_empty_tuple.rs b/crates/ruff/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs index 9ee4188a46..594ee99932 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs @@ -1,5 +1,5 @@ use rustpython_parser::ast::{self, Ranged}; -use rustpython_parser::ast::{Excepthandler, Expr}; +use rustpython_parser::ast::{ExceptHandler, Expr}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -17,8 +17,9 @@ impl Violation for ExceptWithEmptyTuple { } /// B029 -pub(crate) fn except_with_empty_tuple(checker: &mut Checker, excepthandler: &Excepthandler) { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { type_, .. }) = excepthandler; +pub(crate) fn except_with_empty_tuple(checker: &mut Checker, except_handler: &ExceptHandler) { + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, .. }) = + except_handler; let Some(type_) = type_ else { return; }; @@ -26,8 +27,9 @@ pub(crate) fn except_with_empty_tuple(checker: &mut Checker, excepthandler: &Exc return; }; if elts.is_empty() { - checker - .diagnostics - .push(Diagnostic::new(ExceptWithEmptyTuple, excepthandler.range())); + checker.diagnostics.push(Diagnostic::new( + ExceptWithEmptyTuple, + except_handler.range(), + )); } } 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 4bfab07f94..7b7df1fcc7 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 @@ -1,6 +1,6 @@ use std::collections::VecDeque; -use rustpython_parser::ast::{self, Excepthandler, Expr, Ranged}; +use rustpython_parser::ast::{self, ExceptHandler, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -44,9 +44,10 @@ fn flatten_starred_iterables(expr: &Expr) -> Vec<&Expr> { /// B030 pub(crate) fn except_with_non_exception_classes( checker: &mut Checker, - excepthandler: &Excepthandler, + except_handler: &ExceptHandler, ) { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { type_, .. }) = excepthandler; + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, .. }) = + except_handler; let Some(type_) = type_ else { return; }; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs b/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs index 6d0fcc4e21..d85d0d19e5 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs @@ -1,5 +1,5 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Arguments, Expr, Ranged}; +use rustpython_parser::ast::{self, ArgWithDefault, Arguments, Expr, Ranged}; use ruff_diagnostics::Violation; use ruff_diagnostics::{Diagnostic, DiagnosticKind}; @@ -114,12 +114,19 @@ pub(crate) fn function_call_argument_default(checker: &mut Checker, arguments: & .collect(); let diagnostics = { let mut visitor = ArgumentDefaultVisitor::new(checker.semantic(), extend_immutable_calls); - for expr in arguments - .defaults + for ArgWithDefault { + default, + def: _, + range: _, + } in arguments + .posonlyargs .iter() - .chain(arguments.kw_defaults.iter()) + .chain(&arguments.args) + .chain(&arguments.kwonlyargs) { - visitor.visit_expr(expr); + if let Some(expr) = &default { + visitor.visit_expr(expr); + } } visitor.diagnostics }; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs b/crates/ruff/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs index f8d37590dd..22012d80b7 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs @@ -1,5 +1,5 @@ use rustc_hash::FxHashMap; -use rustpython_parser::ast::{self, Expr, Ranged}; +use rustpython_parser::ast::{self, ArgWithDefault, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -49,8 +49,17 @@ where range: _, }) => { visitor::walk_expr(self, body); - for arg in &args.args { - self.names.remove(arg.arg.as_str()); + for ArgWithDefault { + def, + default: _, + range: _, + } in args + .posonlyargs + .iter() + .chain(&args.args) + .chain(&args.kwonlyargs) + { + self.names.remove(def.arg.as_str()); } } _ => visitor::walk_expr(self, expr), 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 986aca83d4..bdae234986 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 @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{Arguments, Ranged}; +use rustpython_parser::ast::{ArgWithDefault, Arguments, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -19,22 +19,22 @@ impl Violation for MutableArgumentDefault { /// B006 pub(crate) fn mutable_argument_default(checker: &mut Checker, arguments: &Arguments) { // Scan in reverse order to right-align zip(). - for (arg, default) in arguments - .kwonlyargs + for ArgWithDefault { + def, + default, + range: _, + } in arguments + .posonlyargs .iter() - .rev() - .zip(arguments.kw_defaults.iter().rev()) - .chain( - arguments - .args - .iter() - .rev() - .chain(arguments.posonlyargs.iter().rev()) - .zip(arguments.defaults.iter().rev()), - ) + .chain(&arguments.args) + .chain(&arguments.kwonlyargs) { + let Some(default)= default else { + continue; + }; + if is_mutable_expr(default, checker.semantic()) - && !arg.annotation.as_ref().map_or(false, |expr| { + && !def.annotation.as_ref().map_or(false, |expr| { is_immutable_annotation(expr, checker.semantic()) }) { 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 9b0bbe6f29..7802ccf42f 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 @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Excepthandler, Expr, Ranged}; +use rustpython_parser::ast::{self, ExceptHandler, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -30,10 +30,10 @@ impl AlwaysAutofixableViolation for RedundantTupleInExceptionHandler { /// B013 pub(crate) fn redundant_tuple_in_exception_handler( checker: &mut Checker, - handlers: &[Excepthandler], + 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/unary_prefix_increment.rs b/crates/ruff/src/rules/flake8_bugbear/rules/unary_prefix_increment.rs index 12f70b9044..7a5d978ecc 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 @@ -17,7 +17,7 @@ //! n += 1 //! ``` -use rustpython_parser::ast::{self, Expr, Ranged, Unaryop}; +use rustpython_parser::ast::{self, Expr, Ranged, UnaryOp}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -38,16 +38,16 @@ impl Violation for UnaryPrefixIncrement { pub(crate) fn unary_prefix_increment( checker: &mut Checker, expr: &Expr, - op: Unaryop, + op: UnaryOp, operand: &Expr, ) { - if !matches!(op, Unaryop::UAdd) { + if !matches!(op, UnaryOp::UAdd) { return; } let Expr::UnaryOp(ast::ExprUnaryOp { op, .. })= operand else { return; }; - if !matches!(op, Unaryop::UAdd) { + if !matches!(op, UnaryOp::UAdd) { return; } checker diff --git a/crates/ruff/src/rules/flake8_builtins/helpers.rs b/crates/ruff/src/rules/flake8_builtins/helpers.rs index 5b6c45761e..2280f084c5 100644 --- a/crates/ruff/src/rules/flake8_builtins/helpers.rs +++ b/crates/ruff/src/rules/flake8_builtins/helpers.rs @@ -1,5 +1,5 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{Excepthandler, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{ExceptHandler, Expr, Ranged, Stmt}; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; @@ -13,7 +13,7 @@ pub(super) fn shadows_builtin(name: &str, ignorelist: &[String]) -> bool { pub(crate) enum AnyShadowing<'a> { Expression(&'a Expr), Statement(&'a Stmt), - ExceptHandler(&'a Excepthandler), + ExceptHandler(&'a ExceptHandler), } impl AnyShadowing<'_> { @@ -38,8 +38,8 @@ impl<'a> From<&'a Expr> for AnyShadowing<'a> { } } -impl<'a> From<&'a Excepthandler> for AnyShadowing<'a> { - fn from(value: &'a Excepthandler) -> Self { +impl<'a> From<&'a ExceptHandler> for AnyShadowing<'a> { + fn from(value: &'a ExceptHandler) -> Self { AnyShadowing::ExceptHandler(value) } } 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 fc9e678e44..5a2c0879cc 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 @@ -1,5 +1,5 @@ use num_bigint::BigInt; -use rustpython_parser::ast::{self, Constant, Expr, Ranged, Unaryop}; +use rustpython_parser::ast::{self, Constant, Expr, Ranged, UnaryOp}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -74,7 +74,7 @@ pub(crate) fn unnecessary_subscript_reversal( return; }; let Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::USub, + op: UnaryOp::USub, operand, range: _, }) = step.as_ref() else { 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 3846b11ac8..708df4e241 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 @@ -5,7 +5,7 @@ use itertools::Either::{Left, Right}; use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Boolop, Expr, ExprContext, Ranged}; +use rustpython_parser::ast::{self, BoolOp, Expr, ExprContext, Ranged}; use ruff_diagnostics::AlwaysAutofixableViolation; use ruff_diagnostics::{Diagnostic, Edit, Fix}; @@ -60,7 +60,7 @@ 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; }; @@ -155,7 +155,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) { // Generate the combined `BoolOp`. let mut call = Some(call); let node = Expr::BoolOp(ast::ExprBoolOp { - op: Boolop::Or, + op: BoolOp::Or, values: values .iter() .enumerate() diff --git a/crates/ruff/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs b/crates/ruff/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs index 1f52670794..43a47e5c24 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs @@ -62,7 +62,7 @@ pub(crate) fn any_eq_ne_annotation(checker: &mut Checker, name: &str, args: &Arg return; } - let Some(annotation) = &args.args[1].annotation else { + let Some(annotation) = &args.args[1].def.annotation else { return; }; 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 f660f196e5..305e1ba2c7 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::{CmpOp, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -61,7 +61,7 @@ pub(crate) fn bad_version_info_comparison( checker: &mut Checker, expr: &Expr, left: &Expr, - ops: &[Cmpop], + ops: &[CmpOp], comparators: &[Expr], ) { let ([op], [_right]) = (ops, comparators) else { @@ -78,7 +78,7 @@ pub(crate) fn bad_version_info_comparison( return; } - if !matches!(op, Cmpop::Lt | Cmpop::GtE) { + if !matches!(op, CmpOp::Lt | CmpOp::GtE) { let diagnostic = Diagnostic::new(BadVersionInfoComparison, expr.range()); checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs b/crates/ruff/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs index c45275f9db..354067c031 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs @@ -1,6 +1,5 @@ use std::fmt; -use itertools::chain; use rustpython_parser::ast::Ranged; use ruff_diagnostics::{Diagnostic, Violation}; @@ -49,12 +48,12 @@ impl Violation for NoReturnArgumentAnnotationInStub { /// PYI050 pub(crate) fn no_return_argument_annotation(checker: &mut Checker, args: &Arguments) { - for annotation in chain!( - args.args.iter(), - args.posonlyargs.iter(), - args.kwonlyargs.iter() - ) - .filter_map(|arg| arg.annotation.as_ref()) + for annotation in args + .posonlyargs + .iter() + .chain(&args.args) + .chain(&args.kwonlyargs) + .filter_map(|arg| arg.def.annotation.as_ref()) { if checker.semantic().match_typing_expr(annotation, "NoReturn") { checker.diagnostics.push(Diagnostic::new( 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 14df881ff0..eacd3a98dd 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs @@ -1,4 +1,6 @@ -use rustpython_parser::ast::{self, Arguments, Constant, Expr, Operator, Ranged, Stmt, Unaryop}; +use rustpython_parser::ast::{ + self, ArgWithDefault, Arguments, Constant, Expr, Operator, Ranged, Stmt, UnaryOp, +}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -156,7 +158,7 @@ fn is_valid_default_value_with_annotation( }); } Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::USub, + op: UnaryOp::USub, operand, range: _, }) => { @@ -199,7 +201,7 @@ fn is_valid_default_value_with_annotation( { return locator.slice(left.range()).len() <= 10; } else if let Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::USub, + op: UnaryOp::USub, operand, range: _, }) = left.as_ref() @@ -312,130 +314,74 @@ fn is_enum(bases: &[Expr], semantic: &SemanticModel) -> bool { } /// PYI011 -pub(crate) fn typed_argument_simple_defaults(checker: &mut Checker, args: &Arguments) { - if !args.defaults.is_empty() { - let defaults_start = args.posonlyargs.len() + args.args.len() - args.defaults.len(); - for (i, arg) in args.posonlyargs.iter().chain(&args.args).enumerate() { - if let Some(default) = i - .checked_sub(defaults_start) - .and_then(|i| args.defaults.get(i)) - { - if arg.annotation.is_some() { - if !is_valid_default_value_with_annotation( - default, - true, - checker.locator, - checker.semantic(), - ) { - let mut diagnostic = - Diagnostic::new(TypedArgumentDefaultInStub, default.range()); +pub(crate) fn typed_argument_simple_defaults(checker: &mut Checker, arguments: &Arguments) { + for ArgWithDefault { + def, + default, + range: _, + } in arguments + .posonlyargs + .iter() + .chain(&arguments.args) + .chain(&arguments.kwonlyargs) + { + let Some(default) = default else { + continue; + }; + if def.annotation.is_some() { + if !is_valid_default_value_with_annotation( + default, + true, + checker.locator, + checker.semantic(), + ) { + let mut diagnostic = Diagnostic::new(TypedArgumentDefaultInStub, default.range()); - if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Fix::suggested(Edit::range_replacement( - "...".to_string(), - default.range(), - ))); - } - - checker.diagnostics.push(diagnostic); - } + if checker.patch(diagnostic.kind.rule()) { + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( + "...".to_string(), + default.range(), + ))); } - } - } - } - if !args.kw_defaults.is_empty() { - let defaults_start = args.kwonlyargs.len() - args.kw_defaults.len(); - for (i, kwarg) in args.kwonlyargs.iter().enumerate() { - if let Some(default) = i - .checked_sub(defaults_start) - .and_then(|i| args.kw_defaults.get(i)) - { - if kwarg.annotation.is_some() { - if !is_valid_default_value_with_annotation( - default, - true, - checker.locator, - checker.semantic(), - ) { - let mut diagnostic = - Diagnostic::new(TypedArgumentDefaultInStub, default.range()); - - if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Fix::suggested(Edit::range_replacement( - "...".to_string(), - default.range(), - ))); - } - - checker.diagnostics.push(diagnostic); - } - } + checker.diagnostics.push(diagnostic); } } } } /// PYI014 -pub(crate) fn argument_simple_defaults(checker: &mut Checker, args: &Arguments) { - if !args.defaults.is_empty() { - let defaults_start = args.posonlyargs.len() + args.args.len() - args.defaults.len(); - for (i, arg) in args.posonlyargs.iter().chain(&args.args).enumerate() { - if let Some(default) = i - .checked_sub(defaults_start) - .and_then(|i| args.defaults.get(i)) - { - if arg.annotation.is_none() { - if !is_valid_default_value_with_annotation( - default, - true, - checker.locator, - checker.semantic(), - ) { - let mut diagnostic = - Diagnostic::new(ArgumentDefaultInStub, default.range()); +pub(crate) fn argument_simple_defaults(checker: &mut Checker, arguments: &Arguments) { + for ArgWithDefault { + def, + default, + range: _, + } in arguments + .posonlyargs + .iter() + .chain(&arguments.args) + .chain(&arguments.kwonlyargs) + { + let Some(default) = default else { + continue; + }; + if def.annotation.is_none() { + if !is_valid_default_value_with_annotation( + default, + true, + checker.locator, + checker.semantic(), + ) { + let mut diagnostic = Diagnostic::new(ArgumentDefaultInStub, default.range()); - if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Fix::suggested(Edit::range_replacement( - "...".to_string(), - default.range(), - ))); - } - - checker.diagnostics.push(diagnostic); - } + if checker.patch(diagnostic.kind.rule()) { + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( + "...".to_string(), + default.range(), + ))); } - } - } - } - if !args.kw_defaults.is_empty() { - let defaults_start = args.kwonlyargs.len() - args.kw_defaults.len(); - for (i, kwarg) in args.kwonlyargs.iter().enumerate() { - if let Some(default) = i - .checked_sub(defaults_start) - .and_then(|i| args.kw_defaults.get(i)) - { - if kwarg.annotation.is_none() { - if !is_valid_default_value_with_annotation( - default, - true, - checker.locator, - checker.semantic(), - ) { - let mut diagnostic = - Diagnostic::new(ArgumentDefaultInStub, default.range()); - - if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Fix::suggested(Edit::range_replacement( - "...".to_string(), - default.range(), - ))); - } - - checker.diagnostics.push(diagnostic); - } - } + checker.diagnostics.push(diagnostic); } } } 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 36088621d0..af876baa0c 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Cmpop, Constant, Expr, Ranged}; +use rustpython_parser::ast::{self, CmpOp, Constant, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -93,7 +93,7 @@ pub(crate) fn unrecognized_platform( checker: &mut Checker, expr: &Expr, left: &Expr, - ops: &[Cmpop], + ops: &[CmpOp], comparators: &[Expr], ) { let ([op], [right]) = (ops, comparators) else { @@ -113,7 +113,7 @@ 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) { + if !matches!(op, CmpOp::Eq | CmpOp::NotEq) && checker.enabled(Rule::UnrecognizedPlatformCheck) { checker .diagnostics .push(diagnostic_unrecognized_platform_check); 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 ef73693881..9c3fa25046 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs @@ -3,11 +3,11 @@ use std::borrow::Cow; use anyhow::bail; use anyhow::Result; use libcst_native::{ - Assert, BooleanOp, CompoundStatement, Expression, ParenthesizableWhitespace, ParenthesizedNode, - SimpleStatementLine, SimpleWhitespace, SmallStatement, Statement, TrailingWhitespace, UnaryOp, - UnaryOperation, + self, Assert, BooleanOp, CompoundStatement, Expression, ParenthesizableWhitespace, + ParenthesizedNode, SimpleStatementLine, SimpleWhitespace, SmallStatement, Statement, + TrailingWhitespace, UnaryOperation, }; -use rustpython_parser::ast::{self, Boolop, Excepthandler, Expr, Keyword, Ranged, Stmt, Unaryop}; +use rustpython_parser::ast::{self, BoolOp, ExceptHandler, Expr, Keyword, Ranged, Stmt, UnaryOp}; use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; @@ -227,11 +227,11 @@ pub(crate) fn assert_falsy(checker: &mut Checker, stmt: &Stmt, test: &Expr) { } /// PT017 -pub(crate) fn assert_in_exception_handler(handlers: &[Excepthandler]) -> Vec { +pub(crate) fn assert_in_exception_handler(handlers: &[ExceptHandler]) -> Vec { handlers .iter() .flat_map(|handler| match handler { - Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { + ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { name, body, .. }) => { if let Some(name) = name { @@ -262,17 +262,17 @@ enum CompositionKind { fn is_composite_condition(test: &Expr) -> CompositionKind { match test { Expr::BoolOp(ast::ExprBoolOp { - op: Boolop::And, .. + op: BoolOp::And, .. }) => { return CompositionKind::Simple; } Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::Not, + op: UnaryOp::Not, operand, range: _, }) => { if let Expr::BoolOp(ast::ExprBoolOp { - op: Boolop::Or, + op: BoolOp::Or, values, range: _, }) = operand.as_ref() @@ -282,7 +282,7 @@ fn is_composite_condition(test: &Expr) -> CompositionKind { !matches!( expr, Expr::BoolOp(ast::ExprBoolOp { - op: Boolop::And, + op: BoolOp::And, .. }) ) @@ -301,12 +301,12 @@ fn is_composite_condition(test: &Expr) -> CompositionKind { /// Negate a condition, i.e., `a` => `not a` and `not a` => `a`. fn negate<'a>(expression: &Expression<'a>) -> Expression<'a> { if let Expression::UnaryOperation(ref expression) = expression { - if matches!(expression.operator, UnaryOp::Not { .. }) { + if matches!(expression.operator, libcst_native::UnaryOp::Not { .. }) { return *expression.expression.clone(); } } Expression::UnaryOperation(Box::new(UnaryOperation { - operator: UnaryOp::Not { + operator: libcst_native::UnaryOp::Not { whitespace_after: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(" ")), }, expression: Box::new(expression.clone()), @@ -371,7 +371,7 @@ fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> let mut conditions: Vec = Vec::with_capacity(2); match &assert_statement.test { Expression::UnaryOperation(op) => { - if matches!(op.operator, UnaryOp::Not { .. }) { + if matches!(op.operator, libcst_native::UnaryOp::Not { .. }) { if let Expression::BooleanOperation(op) = &*op.expression { if matches!(op.operator, BooleanOp::Or { .. }) { conditions.push(negate(&op.left)); 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 760aa8f4b6..502cfb59ea 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs @@ -2,7 +2,7 @@ use std::fmt; use anyhow::Result; use ruff_text_size::{TextLen, TextRange, TextSize}; -use rustpython_parser::ast::{self, Arguments, Expr, Keyword, Ranged, Stmt}; +use rustpython_parser::ast::{self, ArgWithDefault, Arguments, Expr, Keyword, Ranged, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix}; @@ -421,18 +421,29 @@ fn check_fixture_returns(checker: &mut Checker, stmt: &Stmt, name: &str, body: & } /// PT019 -fn check_test_function_args(checker: &mut Checker, args: &Arguments) { - args.args.iter().chain(&args.kwonlyargs).for_each(|arg| { - let name = &arg.arg; - if name.starts_with('_') { - checker.diagnostics.push(Diagnostic::new( - PytestFixtureParamWithoutValue { - name: name.to_string(), - }, - arg.range(), - )); - } - }); +fn check_test_function_args(checker: &mut Checker, arguments: &Arguments) { + arguments + .posonlyargs + .iter() + .chain(&arguments.args) + .chain(&arguments.kwonlyargs) + .for_each( + |ArgWithDefault { + def, + default: _, + range: _, + }| { + let name = &def.arg; + if name.starts_with('_') { + checker.diagnostics.push(Diagnostic::new( + PytestFixtureParamWithoutValue { + name: name.to_string(), + }, + def.range(), + )); + } + }, + ); } /// PT020 diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs index fe8bdf7ac6..78296a99e4 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Expr, Identifier, Keyword, Ranged, Stmt, Withitem}; +use rustpython_parser::ast::{self, Expr, Identifier, Keyword, Ranged, Stmt, WithItem}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -94,7 +94,7 @@ pub(crate) fn raises_call(checker: &mut Checker, func: &Expr, args: &[Expr], key pub(crate) fn complex_raises( checker: &mut Checker, stmt: &Stmt, - items: &[Withitem], + items: &[WithItem], body: &[Stmt], ) { let mut is_too_complex = false; diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/unittest_assert.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/unittest_assert.rs index 8c70114e67..0c032ce84c 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/unittest_assert.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/unittest_assert.rs @@ -3,7 +3,7 @@ use std::hash::BuildHasherDefault; use anyhow::{anyhow, bail, Result}; use ruff_text_size::TextRange; use rustc_hash::FxHashMap; -use rustpython_parser::ast::{self, Cmpop, Constant, Expr, ExprContext, Keyword, Stmt, Unaryop}; +use rustpython_parser::ast::{self, CmpOp, Constant, Expr, ExprContext, Keyword, Stmt, UnaryOp}; /// An enum to represent the different types of assertions present in the /// `unittest` module. Note: any variants that can't be replaced with plain @@ -149,10 +149,10 @@ fn assert(expr: &Expr, msg: Option<&Expr>) -> Stmt { }) } -fn compare(left: &Expr, cmpop: Cmpop, right: &Expr) -> Expr { +fn compare(left: &Expr, cmp_op: CmpOp, right: &Expr) -> Expr { Expr::Compare(ast::ExprCompare { left: Box::new(left.clone()), - ops: vec![cmpop], + ops: vec![cmp_op], comparators: vec![right.clone()], range: TextRange::default(), }) @@ -263,7 +263,7 @@ impl UnittestAssert { Ok(if matches!(self, UnittestAssert::False) { assert( &Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::Not, + op: UnaryOp::Not, operand: Box::new(expr.clone()), range: TextRange::default(), }), @@ -290,18 +290,18 @@ impl UnittestAssert { .get("second") .ok_or_else(|| anyhow!("Missing argument `second`"))?; let msg = args.get("msg").copied(); - let cmpop = match self { - UnittestAssert::Equal | UnittestAssert::Equals => Cmpop::Eq, - UnittestAssert::NotEqual | UnittestAssert::NotEquals => Cmpop::NotEq, - UnittestAssert::Greater => Cmpop::Gt, - UnittestAssert::GreaterEqual => Cmpop::GtE, - UnittestAssert::Less => Cmpop::Lt, - UnittestAssert::LessEqual => Cmpop::LtE, - UnittestAssert::Is => Cmpop::Is, - UnittestAssert::IsNot => Cmpop::IsNot, + let cmp_op = match self { + UnittestAssert::Equal | UnittestAssert::Equals => CmpOp::Eq, + UnittestAssert::NotEqual | UnittestAssert::NotEquals => CmpOp::NotEq, + UnittestAssert::Greater => CmpOp::Gt, + UnittestAssert::GreaterEqual => CmpOp::GtE, + UnittestAssert::Less => CmpOp::Lt, + UnittestAssert::LessEqual => CmpOp::LtE, + UnittestAssert::Is => CmpOp::Is, + UnittestAssert::IsNot => CmpOp::IsNot, _ => unreachable!(), }; - let expr = compare(first, cmpop, second); + let expr = compare(first, cmp_op, second); Ok(assert(&expr, msg)) } UnittestAssert::In | UnittestAssert::NotIn => { @@ -312,12 +312,12 @@ impl UnittestAssert { .get("container") .ok_or_else(|| anyhow!("Missing argument `container`"))?; let msg = args.get("msg").copied(); - let cmpop = if matches!(self, UnittestAssert::In) { - Cmpop::In + let cmp_op = if matches!(self, UnittestAssert::In) { + CmpOp::In } else { - Cmpop::NotIn + CmpOp::NotIn }; - let expr = compare(member, cmpop, container); + let expr = compare(member, cmp_op, container); Ok(assert(&expr, msg)) } UnittestAssert::IsNone | UnittestAssert::IsNotNone => { @@ -325,17 +325,17 @@ impl UnittestAssert { .get("expr") .ok_or_else(|| anyhow!("Missing argument `expr`"))?; let msg = args.get("msg").copied(); - let cmpop = if matches!(self, UnittestAssert::IsNone) { - Cmpop::Is + let cmp_op = if matches!(self, UnittestAssert::IsNone) { + CmpOp::Is } else { - Cmpop::IsNot + CmpOp::IsNot }; let node = Expr::Constant(ast::ExprConstant { value: Constant::None, kind: None, range: TextRange::default(), }); - let expr = compare(expr, cmpop, &node); + let expr = compare(expr, cmp_op, &node); Ok(assert(&expr, msg)) } UnittestAssert::IsInstance | UnittestAssert::NotIsInstance => { @@ -362,7 +362,7 @@ impl UnittestAssert { Ok(assert(&isinstance, msg)) } else { let node = ast::ExprUnaryOp { - op: Unaryop::Not, + op: UnaryOp::Not, operand: Box::new(isinstance), range: TextRange::default(), }; @@ -403,7 +403,7 @@ impl UnittestAssert { Ok(assert(&re_search, msg)) } else { let node = ast::ExprUnaryOp { - op: Unaryop::Not, + op: UnaryOp::Not, operand: Box::new(re_search), range: TextRange::default(), }; 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 4c7fdd4cb0..ebf3fbc285 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 @@ -5,7 +5,7 @@ use itertools::Either::{Left, Right}; use itertools::Itertools; use ruff_text_size::TextRange; use rustc_hash::FxHashMap; -use rustpython_parser::ast::{self, Boolop, Cmpop, Expr, ExprContext, Ranged, Unaryop}; +use rustpython_parser::ast::{self, BoolOp, CmpOp, Expr, ExprContext, Ranged, UnaryOp}; use ruff_diagnostics::{AlwaysAutofixableViolation, AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -299,7 +299,7 @@ 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; }; @@ -402,7 +402,7 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) { // Generate the combined `BoolOp`. let node = ast::ExprBoolOp { - op: Boolop::Or, + op: BoolOp::Or, values: iter::once(call) .chain( values @@ -437,7 +437,7 @@ fn match_eq_target(expr: &Expr) -> Option<(&str, &Expr)> { if ops.len() != 1 || comparators.len() != 1 { return None; } - if !matches!(&ops[0], Cmpop::Eq) { + if !matches!(&ops[0], CmpOp::Eq) { return None; } let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() else { @@ -452,7 +452,7 @@ 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; }; @@ -501,7 +501,7 @@ pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) { }; let node2 = ast::ExprCompare { left: Box::new(node1.into()), - ops: vec![Cmpop::In], + ops: vec![CmpOp::In], comparators: vec![node.into()], range: TextRange::default(), }; @@ -524,7 +524,7 @@ pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) { } else { // Wrap in a `x in (a, b) or ...` boolean operation. let node = ast::ExprBoolOp { - op: Boolop::Or, + op: BoolOp::Or, values: iter::once(in_expr).chain(unmatched).collect(), range: TextRange::default(), }; @@ -542,7 +542,7 @@ 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 { @@ -554,7 +554,7 @@ pub(crate) fn expr_and_not_expr(checker: &mut Checker, expr: &Expr) { let mut non_negated_expr = vec![]; for expr in values { if let Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::Not, + op: UnaryOp::Not, operand, range: _, }) = expr @@ -597,7 +597,7 @@ 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 { @@ -609,7 +609,7 @@ pub(crate) fn expr_or_not_expr(checker: &mut Checker, expr: &Expr) { let mut non_negated_expr = vec![]; for expr in values { if let Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::Not, + op: UnaryOp::Not, operand, range: _, }) = expr @@ -673,7 +673,7 @@ pub(crate) fn get_short_circuit_edit( fn is_short_circuit( expr: &Expr, - expected_op: Boolop, + expected_op: BoolOp, checker: &Checker, ) -> Option<(Edit, ContentAround)> { let Expr::BoolOp(ast::ExprBoolOp { op, values, range: _, }) = expr else { @@ -683,8 +683,8 @@ fn is_short_circuit( return None; } let short_circuit_truthiness = match op { - Boolop::And => Truthiness::Falsey, - Boolop::Or => Truthiness::Truthy, + BoolOp::And => Truthiness::Falsey, + BoolOp::Or => Truthiness::Truthy, }; let mut location = expr.start(); @@ -753,7 +753,7 @@ fn is_short_circuit( /// SIM222 pub(crate) fn expr_or_true(checker: &mut Checker, expr: &Expr) { - if let Some((edit, remove)) = is_short_circuit(expr, Boolop::Or, checker) { + if let Some((edit, remove)) = is_short_circuit(expr, BoolOp::Or, checker) { let mut diagnostic = Diagnostic::new( ExprOrTrue { expr: edit.content().unwrap_or_default().to_string(), @@ -771,7 +771,7 @@ pub(crate) fn expr_or_true(checker: &mut Checker, expr: &Expr) { /// SIM223 pub(crate) fn expr_and_false(checker: &mut Checker, expr: &Expr) { - if let Some((edit, remove)) = is_short_circuit(expr, Boolop::And, checker) { + if let Some((edit, remove)) = is_short_circuit(expr, BoolOp::And, checker) { let mut diagnostic = Diagnostic::new( ExprAndFalse { expr: edit.content().unwrap_or_default().to_string(), 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 14da927c36..3b5d4a4bb9 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs @@ -1,7 +1,7 @@ use log::error; use ruff_text_size::TextRange; use rustc_hash::FxHashSet; -use rustpython_parser::ast::{self, Cmpop, Constant, Expr, ExprContext, Ranged, Stmt}; +use rustpython_parser::ast::{self, CmpOp, Constant, Expr, ExprContext, Ranged, Stmt}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -734,7 +734,7 @@ pub(crate) fn manual_dict_lookup( if orelse.len() != 1 { return; } - if !(ops.len() == 1 && ops[0] == Cmpop::Eq) { + if !(ops.len() == 1 && ops[0] == CmpOp::Eq) { return; } if comparators.len() != 1 { @@ -807,7 +807,7 @@ pub(crate) fn manual_dict_lookup( let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() else { return; }; - if !(id == target && ops.len() == 1 && ops[0] == Cmpop::Eq) { + if !(id == target && ops.len() == 1 && ops[0] == CmpOp::Eq) { return; } if comparators.len() != 1 { @@ -882,8 +882,8 @@ pub(crate) fn use_dict_get_with_default( return; } let (expected_var, expected_value, default_var, default_value) = match ops[..] { - [Cmpop::In] => (&body_var[0], body_value, &orelse_var[0], orelse_value), - [Cmpop::NotIn] => (&orelse_var[0], orelse_value, &body_var[0], body_value), + [CmpOp::In] => (&body_var[0], body_value, &orelse_var[0], orelse_value), + [CmpOp::NotIn] => (&orelse_var[0], orelse_value, &body_var[0], body_value), _ => { 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 7210e6b202..ffbc5d2032 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs @@ -1,5 +1,5 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Ranged, Unaryop}; +use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Ranged, UnaryOp}; use ruff_diagnostics::{AlwaysAutofixableViolation, AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -219,7 +219,7 @@ pub(crate) fn explicit_false_true_in_ifexpr( if checker.patch(diagnostic.kind.rule()) { let node = test.clone(); let node1 = ast::ExprUnaryOp { - op: Unaryop::Not, + op: UnaryOp::Not, operand: Box::new(node), range: TextRange::default(), }; 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 e303b1974f..a24505d9e7 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 @@ -1,5 +1,5 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Cmpop, Expr, ExprContext, Ranged, Stmt, Unaryop}; +use rustpython_parser::ast::{self, CmpOp, Expr, ExprContext, Ranged, Stmt, UnaryOp}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -138,16 +138,16 @@ fn is_exception_check(stmt: &Stmt) -> bool { pub(crate) fn negation_with_equal_op( checker: &mut Checker, expr: &Expr, - op: Unaryop, + op: UnaryOp, operand: &Expr, ) { - if !matches!(op, Unaryop::Not) { + if !matches!(op, UnaryOp::Not) { return; } let Expr::Compare(ast::ExprCompare { left, ops, comparators, range: _}) = operand else { return; }; - if !matches!(&ops[..], [Cmpop::Eq]) { + if !matches!(&ops[..], [CmpOp::Eq]) { return; } if is_exception_check(checker.semantic().stmt()) { @@ -174,7 +174,7 @@ pub(crate) fn negation_with_equal_op( if checker.patch(diagnostic.kind.rule()) { let node = ast::ExprCompare { left: left.clone(), - ops: vec![Cmpop::NotEq], + ops: vec![CmpOp::NotEq], comparators: comparators.clone(), range: TextRange::default(), }; @@ -191,16 +191,16 @@ pub(crate) fn negation_with_equal_op( pub(crate) fn negation_with_not_equal_op( checker: &mut Checker, expr: &Expr, - op: Unaryop, + op: UnaryOp, operand: &Expr, ) { - if !matches!(op, Unaryop::Not) { + if !matches!(op, UnaryOp::Not) { return; } let Expr::Compare(ast::ExprCompare { left, ops, comparators, range: _}) = operand else { return; }; - if !matches!(&ops[..], [Cmpop::NotEq]) { + if !matches!(&ops[..], [CmpOp::NotEq]) { return; } if is_exception_check(checker.semantic().stmt()) { @@ -227,7 +227,7 @@ pub(crate) fn negation_with_not_equal_op( if checker.patch(diagnostic.kind.rule()) { let node = ast::ExprCompare { left: left.clone(), - ops: vec![Cmpop::Eq], + ops: vec![CmpOp::Eq], comparators: comparators.clone(), range: TextRange::default(), }; @@ -241,14 +241,14 @@ pub(crate) fn negation_with_not_equal_op( } /// SIM208 -pub(crate) fn double_negation(checker: &mut Checker, expr: &Expr, op: Unaryop, operand: &Expr) { - if !matches!(op, Unaryop::Not) { +pub(crate) fn double_negation(checker: &mut Checker, expr: &Expr, op: UnaryOp, operand: &Expr) { + if !matches!(op, UnaryOp::Not) { return; } let Expr::UnaryOp(ast::ExprUnaryOp { op: operand_op, operand, range: _ }) = operand else { return; }; - if !matches!(operand_op, Unaryop::Not) { + if !matches!(operand_op, UnaryOp::Not) { return; } diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs index 72dbce6b09..d3c1642a70 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs @@ -1,6 +1,6 @@ use log::error; use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Ranged, Stmt, Withitem}; +use rustpython_parser::ast::{self, Ranged, Stmt, WithItem}; use ruff_diagnostics::{AutofixKind, Violation}; use ruff_diagnostics::{Diagnostic, Fix}; @@ -62,7 +62,7 @@ impl Violation for MultipleWithStatements { /// Returns a boolean indicating whether it's an async with statement, the items /// and body. -fn next_with(body: &[Stmt]) -> Option<(bool, &[Withitem], &[Stmt])> { +fn next_with(body: &[Stmt]) -> Option<(bool, &[WithItem], &[Stmt])> { match body { [Stmt::With(ast::StmtWith { items, body, .. })] => Some((false, items, body)), [Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. })] => Some((true, items, body)), 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 c8c8f4021a..7bdd4b2365 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 @@ -1,7 +1,7 @@ use anyhow::Result; use log::error; use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Cmpop, Expr, Ranged}; +use rustpython_parser::ast::{self, CmpOp, Expr, Ranged}; use ruff_diagnostics::Edit; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix}; @@ -128,10 +128,10 @@ pub(crate) fn key_in_dict_compare( checker: &mut Checker, expr: &Expr, left: &Expr, - ops: &[Cmpop], + ops: &[CmpOp], comparators: &[Expr], ) { - if !matches!(ops[..], [Cmpop::In]) { + if !matches!(ops[..], [CmpOp::In]) { return; } 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 20678787c2..340ad9f956 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs @@ -1,6 +1,6 @@ use ruff_text_size::{TextRange, TextSize}; use rustpython_parser::ast::{ - self, Cmpop, Comprehension, Constant, Expr, ExprContext, Ranged, Stmt, Unaryop, + self, CmpOp, Comprehension, Constant, Expr, ExprContext, Ranged, Stmt, UnaryOp, }; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; @@ -117,7 +117,7 @@ pub(crate) fn convert_for_loop_to_any_all( // Invert the condition. let test = { if let Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::Not, + op: UnaryOp::Not, operand, range: _, }) = &loop_info.test @@ -132,16 +132,16 @@ pub(crate) fn convert_for_loop_to_any_all( { if let ([op], [comparator]) = (ops.as_slice(), comparators.as_slice()) { let op = match op { - Cmpop::Eq => Cmpop::NotEq, - Cmpop::NotEq => Cmpop::Eq, - Cmpop::Lt => Cmpop::GtE, - Cmpop::LtE => Cmpop::Gt, - Cmpop::Gt => Cmpop::LtE, - Cmpop::GtE => Cmpop::Lt, - Cmpop::Is => Cmpop::IsNot, - Cmpop::IsNot => Cmpop::Is, - Cmpop::In => Cmpop::NotIn, - Cmpop::NotIn => Cmpop::In, + CmpOp::Eq => CmpOp::NotEq, + CmpOp::NotEq => CmpOp::Eq, + CmpOp::Lt => CmpOp::GtE, + CmpOp::LtE => CmpOp::Gt, + CmpOp::Gt => CmpOp::LtE, + CmpOp::GtE => CmpOp::Lt, + CmpOp::Is => CmpOp::IsNot, + CmpOp::IsNot => CmpOp::Is, + CmpOp::In => CmpOp::NotIn, + CmpOp::NotIn => CmpOp::In, }; let node = ast::ExprCompare { left: left.clone(), @@ -152,7 +152,7 @@ pub(crate) fn convert_for_loop_to_any_all( node.into() } else { let node = ast::ExprUnaryOp { - op: Unaryop::Not, + op: UnaryOp::Not, operand: Box::new(loop_info.test.clone()), range: TextRange::default(), }; @@ -160,7 +160,7 @@ pub(crate) fn convert_for_loop_to_any_all( } } else { let node = ast::ExprUnaryOp { - op: Unaryop::Not, + op: UnaryOp::Not, operand: Box::new(loop_info.test.clone()), range: TextRange::default(), }; diff --git a/crates/ruff/src/rules/flake8_simplify/rules/return_in_try_except_finally.rs b/crates/ruff/src/rules/flake8_simplify/rules/return_in_try_except_finally.rs index ac8ae87bd0..ce6872778f 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/return_in_try_except_finally.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/return_in_try_except_finally.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Excepthandler, Ranged, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -57,12 +57,12 @@ fn find_return(stmts: &[Stmt]) -> Option<&Stmt> { pub(crate) fn return_in_try_except_finally( checker: &mut Checker, body: &[Stmt], - handlers: &[Excepthandler], + handlers: &[ExceptHandler], finalbody: &[Stmt], ) { let try_has_return = find_return(body).is_some(); let except_has_return = handlers.iter().any(|handler| { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { body, .. }) = handler; + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) = handler; find_return(body).is_some() }); diff --git a/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs b/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs index 6f738754a7..9d5d984f88 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs @@ -1,5 +1,5 @@ use ruff_text_size::{TextLen, TextRange}; -use rustpython_parser::ast::{self, Constant, Excepthandler, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, Constant, ExceptHandler, Expr, Ranged, Stmt}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -68,7 +68,7 @@ pub(crate) fn suppressible_exception( checker: &mut Checker, stmt: &Stmt, try_body: &[Stmt], - handlers: &[Excepthandler], + handlers: &[ExceptHandler], orelse: &[Stmt], finalbody: &[Stmt], ) { @@ -90,7 +90,7 @@ pub(crate) fn suppressible_exception( return; } let handler = &handlers[0]; - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { body, .. }) = handler; + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) = handler; if body.len() == 1 { let node = &body[0]; if node.is_pass_stmt() diff --git a/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs b/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs index b95681958f..cadf0af99d 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs @@ -1,6 +1,6 @@ use anyhow::Result; use libcst_native::CompOp; -use rustpython_parser::ast::{self, Cmpop, Expr, Ranged, Unaryop}; +use rustpython_parser::ast::{self, CmpOp, Expr, Ranged, UnaryOp}; use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; @@ -76,7 +76,7 @@ fn is_constant_like(expr: &Expr) -> bool { Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().all(is_constant_like), Expr::Name(ast::ExprName { id, .. }) => str::is_cased_uppercase(id), Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::UAdd | Unaryop::USub | Unaryop::Invert, + op: UnaryOp::UAdd | UnaryOp::USub | UnaryOp::Invert, operand, range: _, }) => operand.is_constant_expr(), @@ -156,7 +156,7 @@ pub(crate) fn yoda_conditions( checker: &mut Checker, expr: &Expr, left: &Expr, - ops: &[Cmpop], + ops: &[CmpOp], comparators: &[Expr], ) { let ([op], [right]) = (ops, comparators) else { @@ -165,7 +165,7 @@ pub(crate) fn yoda_conditions( if !matches!( op, - Cmpop::Eq | Cmpop::NotEq | Cmpop::Lt | Cmpop::LtE | Cmpop::Gt | Cmpop::GtE, + CmpOp::Eq | CmpOp::NotEq | CmpOp::Lt | CmpOp::LtE | CmpOp::Gt | CmpOp::GtE, ) { return; } diff --git a/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs b/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs index 76c2d589f8..9c4d406474 100644 --- a/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs +++ b/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs @@ -225,8 +225,9 @@ fn function( let args = args .posonlyargs .iter() - .chain(args.args.iter()) - .chain(args.kwonlyargs.iter()) + .chain(&args.args) + .chain(&args.kwonlyargs) + .map(|arg_with_default| &arg_with_default.def) .chain( iter::once::>(args.vararg.as_deref()) .flatten() @@ -252,9 +253,10 @@ fn method( let args = args .posonlyargs .iter() - .chain(args.args.iter()) + .chain(&args.args) + .chain(&args.kwonlyargs) .skip(1) - .chain(args.kwonlyargs.iter()) + .map(|arg_with_default| &arg_with_default.def) .chain( iter::once::>(args.vararg.as_deref()) .flatten() diff --git a/crates/ruff/src/rules/isort/block.rs b/crates/ruff/src/rules/isort/block.rs index 8b8203bd3d..73ee365186 100644 --- a/crates/ruff/src/rules/isort/block.rs +++ b/crates/ruff/src/rules/isort/block.rs @@ -1,5 +1,5 @@ use ruff_text_size::{TextRange, TextSize}; -use rustpython_parser::ast::{self, Excepthandler, MatchCase, Ranged, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, MatchCase, Ranged, Stmt}; use ruff_python_ast::source_code::Locator; use ruff_python_ast::statement_visitor::StatementVisitor; @@ -267,8 +267,8 @@ where finalbody, range: _, }) => { - for excepthandler in handlers { - self.visit_excepthandler(excepthandler); + for except_handler in handlers { + self.visit_except_handler(except_handler); } for stmt in body { @@ -291,12 +291,12 @@ where self.nested = prev_nested; } - fn visit_excepthandler(&mut self, excepthandler: &'b Excepthandler) { + fn visit_except_handler(&mut self, except_handler: &'b ExceptHandler) { let prev_nested = self.nested; self.nested = true; - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { body, .. }) = - excepthandler; + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) = + except_handler; for stmt in body { self.visit_stmt(stmt); } diff --git a/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs b/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs index 6b27ba7d62..de18d78153 100644 --- a/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs +++ b/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Excepthandler, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -117,7 +117,7 @@ fn get_complexity_number(stmts: &[Stmt]) -> usize { complexity += get_complexity_number(finalbody); for handler in handlers { complexity += 1; - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) = handler; complexity += get_complexity_number(body); diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs index e7c07e9e82..cc17ffe817 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{Arguments, Decorator, Ranged}; +use rustpython_parser::ast::{ArgWithDefault, Arguments, Decorator, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -74,8 +74,13 @@ pub(crate) fn invalid_first_argument_name_for_class_method( ) { return None; } - if let Some(arg) = args.posonlyargs.first().or_else(|| args.args.first()) { - if &arg.arg != "cls" { + if let Some(ArgWithDefault { + def, + default: _, + range: _, + }) = args.posonlyargs.first().or_else(|| args.args.first()) + { + if &def.arg != "cls" { if checker .settings .pep8_naming @@ -87,7 +92,7 @@ pub(crate) fn invalid_first_argument_name_for_class_method( } return Some(Diagnostic::new( InvalidFirstArgumentNameForClassMethod, - arg.range(), + def.range(), )); } } diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs index 96a826be71..55dd34d104 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs @@ -73,7 +73,7 @@ pub(crate) fn invalid_first_argument_name_for_method( return None; } let arg = args.posonlyargs.first().or_else(|| args.args.first())?; - if &arg.arg == "self" { + if &arg.def.arg == "self" { return None; } if checker @@ -87,6 +87,6 @@ pub(crate) fn invalid_first_argument_name_for_method( } Some(Diagnostic::new( InvalidFirstArgumentNameForMethod, - arg.range(), + arg.def.range(), )) } diff --git a/crates/ruff/src/rules/pycodestyle/helpers.rs b/crates/ruff/src/rules/pycodestyle/helpers.rs index 967efe60b2..ab8b04537a 100644 --- a/crates/ruff/src/rules/pycodestyle/helpers.rs +++ b/crates/ruff/src/rules/pycodestyle/helpers.rs @@ -1,5 +1,5 @@ use ruff_text_size::{TextLen, TextRange}; -use rustpython_parser::ast::{self, Cmpop, Expr}; +use rustpython_parser::ast::{self, CmpOp, Expr}; use unicode_width::UnicodeWidthStr; use ruff_python_ast::source_code::Generator; @@ -13,7 +13,7 @@ pub(crate) fn is_ambiguous_name(name: &str) -> bool { pub(crate) fn compare( left: &Expr, - ops: &[Cmpop], + ops: &[CmpOp], comparators: &[Expr], generator: Generator, ) -> String { diff --git a/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs b/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs index 457bdd6347..beac9e84fc 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Excepthandler, Expr, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -58,7 +58,7 @@ impl Violation for BareExcept { pub(crate) fn bare_except( type_: Option<&Expr>, body: &[Stmt], - handler: &Excepthandler, + handler: &ExceptHandler, locator: &Locator, ) -> Option { if type_.is_none() diff --git a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs index 30968ecfab..d6126674c7 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs @@ -1,5 +1,5 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Arg, Arguments, Constant, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, Arg, ArgWithDefault, Arguments, Constant, Expr, Ranged, Stmt}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -176,22 +176,28 @@ fn function( .posonlyargs .iter() .enumerate() - .map(|(idx, arg)| Arg { - annotation: arg_types - .get(idx) - .map(|arg_type| Box::new(arg_type.clone())), - ..arg.clone() + .map(|(idx, arg_with_default)| ArgWithDefault { + def: Arg { + annotation: arg_types + .get(idx) + .map(|arg_type| Box::new(arg_type.clone())), + ..arg_with_default.def.clone() + }, + ..arg_with_default.clone() }) .collect::>(); let new_args = args .args .iter() .enumerate() - .map(|(idx, arg)| Arg { - annotation: arg_types - .get(idx + new_posonlyargs.len()) - .map(|arg_type| Box::new(arg_type.clone())), - ..arg.clone() + .map(|(idx, arg_with_default)| ArgWithDefault { + def: Arg { + annotation: arg_types + .get(idx + new_posonlyargs.len()) + .map(|arg_type| Box::new(arg_type.clone())), + ..arg_with_default.def.clone() + }, + ..arg_with_default.clone() }) .collect::>(); let func = Stmt::FunctionDef(ast::StmtFunctionDef { diff --git a/crates/ruff/src/rules/pycodestyle/rules/literal_comparisons.rs b/crates/ruff/src/rules/pycodestyle/rules/literal_comparisons.rs index 53c67aaca9..872c515b0a 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/literal_comparisons.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/literal_comparisons.rs @@ -1,6 +1,6 @@ use itertools::izip; use rustc_hash::FxHashMap; -use rustpython_parser::ast::{self, Cmpop, Constant, Expr, Ranged}; +use rustpython_parser::ast::{self, CmpOp, Constant, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -12,16 +12,16 @@ use crate::registry::AsRule; use crate::rules::pycodestyle::helpers::compare; #[derive(Debug, PartialEq, Eq, Copy, Clone)] -enum EqCmpop { +enum EqCmpOp { Eq, NotEq, } -impl EqCmpop { - fn try_from(value: Cmpop) -> Option { +impl EqCmpOp { + fn try_from(value: CmpOp) -> Option { match value { - Cmpop::Eq => Some(EqCmpop::Eq), - Cmpop::NotEq => Some(EqCmpop::NotEq), + CmpOp::Eq => Some(EqCmpOp::Eq), + CmpOp::NotEq => Some(EqCmpOp::NotEq), _ => None, } } @@ -50,23 +50,23 @@ impl EqCmpop { /// /// [PEP 8]: https://peps.python.org/pep-0008/#programming-recommendations #[violation] -pub struct NoneComparison(EqCmpop); +pub struct NoneComparison(EqCmpOp); impl AlwaysAutofixableViolation for NoneComparison { #[derive_message_formats] fn message(&self) -> String { let NoneComparison(op) = self; match op { - EqCmpop::Eq => format!("Comparison to `None` should be `cond is None`"), - EqCmpop::NotEq => format!("Comparison to `None` should be `cond is not None`"), + EqCmpOp::Eq => format!("Comparison to `None` should be `cond is None`"), + EqCmpOp::NotEq => format!("Comparison to `None` should be `cond is not None`"), } } fn autofix_title(&self) -> String { let NoneComparison(op) = self; match op { - EqCmpop::Eq => "Replace with `cond is None`".to_string(), - EqCmpop::NotEq => "Replace with `cond is not None`".to_string(), + EqCmpOp::Eq => "Replace with `cond is None`".to_string(), + EqCmpOp::NotEq => "Replace with `cond is not None`".to_string(), } } } @@ -96,23 +96,23 @@ impl AlwaysAutofixableViolation for NoneComparison { /// /// [PEP 8]: https://peps.python.org/pep-0008/#programming-recommendations #[violation] -pub struct TrueFalseComparison(bool, EqCmpop); +pub struct TrueFalseComparison(bool, EqCmpOp); impl AlwaysAutofixableViolation for TrueFalseComparison { #[derive_message_formats] fn message(&self) -> String { let TrueFalseComparison(value, op) = self; match (value, op) { - (true, EqCmpop::Eq) => { + (true, EqCmpOp::Eq) => { format!("Comparison to `True` should be `cond is True` or `if cond:`") } - (true, EqCmpop::NotEq) => { + (true, EqCmpOp::NotEq) => { format!("Comparison to `True` should be `cond is not True` or `if not cond:`") } - (false, EqCmpop::Eq) => { + (false, EqCmpOp::Eq) => { format!("Comparison to `False` should be `cond is False` or `if not cond:`") } - (false, EqCmpop::NotEq) => { + (false, EqCmpOp::NotEq) => { format!("Comparison to `False` should be `cond is not False` or `if cond:`") } } @@ -121,10 +121,10 @@ impl AlwaysAutofixableViolation for TrueFalseComparison { fn autofix_title(&self) -> String { let TrueFalseComparison(value, op) = self; match (value, op) { - (true, EqCmpop::Eq) => "Replace with `cond is True`".to_string(), - (true, EqCmpop::NotEq) => "Replace with `cond is not True`".to_string(), - (false, EqCmpop::Eq) => "Replace with `cond is False`".to_string(), - (false, EqCmpop::NotEq) => "Replace with `cond is not False`".to_string(), + (true, EqCmpOp::Eq) => "Replace with `cond is True`".to_string(), + (true, EqCmpOp::NotEq) => "Replace with `cond is not True`".to_string(), + (false, EqCmpOp::Eq) => "Replace with `cond is False`".to_string(), + (false, EqCmpOp::NotEq) => "Replace with `cond is not False`".to_string(), } } } @@ -134,7 +134,7 @@ pub(crate) fn literal_comparisons( checker: &mut Checker, expr: &Expr, left: &Expr, - ops: &[Cmpop], + ops: &[CmpOp], comparators: &[Expr], check_none_comparisons: bool, check_true_false_comparisons: bool, @@ -143,7 +143,7 @@ pub(crate) fn literal_comparisons( // through the list of operators, we apply "dummy" fixes for each error, // then replace the entire expression at the end with one "real" fix, to // avoid conflicts. - let mut bad_ops: FxHashMap = FxHashMap::default(); + let mut bad_ops: FxHashMap = FxHashMap::default(); let mut diagnostics: Vec = vec![]; let op = ops.first().unwrap(); @@ -153,20 +153,20 @@ pub(crate) fn literal_comparisons( let next = &comparators[0]; if !helpers::is_constant_non_singleton(next) { - if let Some(op) = EqCmpop::try_from(*op) { + if let Some(op) = EqCmpOp::try_from(*op) { if check_none_comparisons && is_const_none(comparator) { match op { - EqCmpop::Eq => { + EqCmpOp::Eq => { let diagnostic = Diagnostic::new(NoneComparison(op), comparator.range()); if checker.patch(diagnostic.kind.rule()) { - bad_ops.insert(0, Cmpop::Is); + bad_ops.insert(0, CmpOp::Is); } diagnostics.push(diagnostic); } - EqCmpop::NotEq => { + EqCmpOp::NotEq => { let diagnostic = Diagnostic::new(NoneComparison(op), comparator.range()); if checker.patch(diagnostic.kind.rule()) { - bad_ops.insert(0, Cmpop::IsNot); + bad_ops.insert(0, CmpOp::IsNot); } diagnostics.push(diagnostic); } @@ -181,23 +181,23 @@ pub(crate) fn literal_comparisons( }) = comparator { match op { - EqCmpop::Eq => { + EqCmpOp::Eq => { let diagnostic = Diagnostic::new( TrueFalseComparison(*value, op), comparator.range(), ); if checker.patch(diagnostic.kind.rule()) { - bad_ops.insert(0, Cmpop::Is); + bad_ops.insert(0, CmpOp::Is); } diagnostics.push(diagnostic); } - EqCmpop::NotEq => { + EqCmpOp::NotEq => { let diagnostic = Diagnostic::new( TrueFalseComparison(*value, op), comparator.range(), ); if checker.patch(diagnostic.kind.rule()) { - bad_ops.insert(0, Cmpop::IsNot); + bad_ops.insert(0, CmpOp::IsNot); } diagnostics.push(diagnostic); } @@ -214,20 +214,20 @@ pub(crate) fn literal_comparisons( continue; } - if let Some(op) = EqCmpop::try_from(*op) { + if let Some(op) = EqCmpOp::try_from(*op) { if check_none_comparisons && is_const_none(next) { match op { - EqCmpop::Eq => { + EqCmpOp::Eq => { let diagnostic = Diagnostic::new(NoneComparison(op), next.range()); if checker.patch(diagnostic.kind.rule()) { - bad_ops.insert(idx, Cmpop::Is); + bad_ops.insert(idx, CmpOp::Is); } diagnostics.push(diagnostic); } - EqCmpop::NotEq => { + EqCmpOp::NotEq => { let diagnostic = Diagnostic::new(NoneComparison(op), next.range()); if checker.patch(diagnostic.kind.rule()) { - bad_ops.insert(idx, Cmpop::IsNot); + bad_ops.insert(idx, CmpOp::IsNot); } diagnostics.push(diagnostic); } @@ -242,19 +242,19 @@ pub(crate) fn literal_comparisons( }) = next { match op { - EqCmpop::Eq => { + EqCmpOp::Eq => { let diagnostic = Diagnostic::new(TrueFalseComparison(*value, op), next.range()); if checker.patch(diagnostic.kind.rule()) { - bad_ops.insert(idx, Cmpop::Is); + bad_ops.insert(idx, CmpOp::Is); } diagnostics.push(diagnostic); } - EqCmpop::NotEq => { + EqCmpOp::NotEq => { let diagnostic = Diagnostic::new(TrueFalseComparison(*value, op), next.range()); if checker.patch(diagnostic.kind.rule()) { - bad_ops.insert(idx, Cmpop::IsNot); + bad_ops.insert(idx, CmpOp::IsNot); } diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pycodestyle/rules/not_tests.rs b/crates/ruff/src/rules/pycodestyle/rules/not_tests.rs index caa76365bf..af2808c9db 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/not_tests.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/not_tests.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Cmpop, Expr, Ranged, Unaryop}; +use rustpython_parser::ast::{self, CmpOp, Expr, Ranged, UnaryOp}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -77,12 +77,12 @@ impl AlwaysAutofixableViolation for NotIsTest { pub(crate) fn not_tests( checker: &mut Checker, expr: &Expr, - op: Unaryop, + op: UnaryOp, operand: &Expr, check_not_in: bool, check_not_is: bool, ) { - if matches!(op, Unaryop::Not) { + if matches!(op, UnaryOp::Not) { if let Expr::Compare(ast::ExprCompare { left, ops, @@ -90,19 +90,19 @@ pub(crate) fn not_tests( range: _, }) = operand { - if !matches!(&ops[..], [Cmpop::In | Cmpop::Is]) { + if !matches!(&ops[..], [CmpOp::In | CmpOp::Is]) { return; } for op in ops.iter() { match op { - Cmpop::In => { + CmpOp::In => { if check_not_in { let mut diagnostic = Diagnostic::new(NotInTest, operand.range()); if checker.patch(diagnostic.kind.rule()) { diagnostic.set_fix(Fix::automatic(Edit::range_replacement( compare( left, - &[Cmpop::NotIn], + &[CmpOp::NotIn], comparators, checker.generator(), ), @@ -112,14 +112,14 @@ pub(crate) fn not_tests( checker.diagnostics.push(diagnostic); } } - Cmpop::Is => { + CmpOp::Is => { if check_not_is { let mut diagnostic = Diagnostic::new(NotIsTest, operand.range()); if checker.patch(diagnostic.kind.rule()) { diagnostic.set_fix(Fix::automatic(Edit::range_replacement( compare( left, - &[Cmpop::IsNot], + &[CmpOp::IsNot], comparators, checker.generator(), ), diff --git a/crates/ruff/src/rules/pycodestyle/rules/type_comparison.rs b/crates/ruff/src/rules/pycodestyle/rules/type_comparison.rs index 1af0cc98f8..4b9d7535b7 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/type_comparison.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/type_comparison.rs @@ -1,5 +1,5 @@ use itertools::izip; -use rustpython_parser::ast::{self, Cmpop, Constant, Expr, Ranged}; +use rustpython_parser::ast::{self, CmpOp, Constant, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -41,11 +41,11 @@ impl Violation for TypeComparison { pub(crate) fn type_comparison( checker: &mut Checker, expr: &Expr, - ops: &[Cmpop], + ops: &[CmpOp], comparators: &[Expr], ) { for (op, right) in izip!(ops, comparators) { - if !matches!(op, Cmpop::Is | Cmpop::IsNot | Cmpop::Eq | Cmpop::NotEq) { + if !matches!(op, CmpOp::Is | CmpOp::IsNot | CmpOp::Eq | CmpOp::NotEq) { continue; } match right { diff --git a/crates/ruff/src/rules/pydocstyle/rules/sections.rs b/crates/ruff/src/rules/pydocstyle/rules/sections.rs index 26d3f06109..464b87ded5 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/sections.rs @@ -3,7 +3,7 @@ use once_cell::sync::Lazy; use regex::Regex; use ruff_text_size::{TextLen, TextRange, TextSize}; use rustc_hash::FxHashSet; -use rustpython_parser::ast::{self, Stmt}; +use rustpython_parser::ast::{self, ArgWithDefault, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix}; @@ -713,11 +713,15 @@ fn missing_args(checker: &mut Checker, docstring: &Docstring, docstrings_args: & // Look for arguments that weren't included in the docstring. let mut missing_arg_names: FxHashSet = FxHashSet::default(); - for arg in arguments + for ArgWithDefault { + def, + default: _, + range: _, + } in arguments .posonlyargs .iter() - .chain(arguments.args.iter()) - .chain(arguments.kwonlyargs.iter()) + .chain(&arguments.args) + .chain(&arguments.kwonlyargs) .skip( // If this is a non-static method, skip `cls` or `self`. usize::from( @@ -726,7 +730,7 @@ fn missing_args(checker: &mut Checker, docstring: &Docstring, docstrings_args: & ), ) { - let arg_name = arg.arg.as_str(); + let arg_name = def.arg.as_str(); if !arg_name.starts_with('_') && !docstrings_args.contains(arg_name) { missing_arg_names.insert(arg_name.to_string()); } diff --git a/crates/ruff/src/rules/pyflakes/fixes.rs b/crates/ruff/src/rules/pyflakes/fixes.rs index 24a7ef82e7..49796e9bd4 100644 --- a/crates/ruff/src/rules/pyflakes/fixes.rs +++ b/crates/ruff/src/rules/pyflakes/fixes.rs @@ -1,6 +1,6 @@ use anyhow::{bail, Ok, Result}; use ruff_text_size::TextRange; -use rustpython_parser::ast::{Excepthandler, Expr, Ranged}; +use rustpython_parser::ast::{ExceptHandler, Expr, Ranged}; use rustpython_parser::{lexer, Mode, Tok}; use ruff_diagnostics::Edit; @@ -90,17 +90,17 @@ pub(crate) fn remove_unused_positional_arguments_from_format_call( /// Generate a [`Edit`] to remove the binding from an exception handler. pub(crate) fn remove_exception_handler_assignment( - excepthandler: &Excepthandler, + except_handler: &ExceptHandler, locator: &Locator, ) -> Result { - let contents = locator.slice(excepthandler.range()); + let contents = locator.slice(except_handler.range()); let mut fix_start = None; let mut fix_end = None; // End of the token just before the `as` to the semicolon. let mut prev = None; for (tok, range) in - lexer::lex_starts_at(contents, Mode::Module, excepthandler.start()).flatten() + lexer::lex_starts_at(contents, Mode::Module, except_handler.start()).flatten() { if matches!(tok, Tok::As) { fix_start = prev; diff --git a/crates/ruff/src/rules/pyflakes/rules/default_except_not_last.rs b/crates/ruff/src/rules/pyflakes/rules/default_except_not_last.rs index 26a760d2b7..49fc869109 100644 --- a/crates/ruff/src/rules/pyflakes/rules/default_except_not_last.rs +++ b/crates/ruff/src/rules/pyflakes/rules/default_except_not_last.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Excepthandler}; +use rustpython_parser::ast::{self, ExceptHandler}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -55,11 +55,11 @@ impl Violation for DefaultExceptNotLast { /// F707 pub(crate) fn default_except_not_last( - handlers: &[Excepthandler], + handlers: &[ExceptHandler], locator: &Locator, ) -> Option { for (idx, handler) in handlers.iter().enumerate() { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { type_, .. }) = handler; + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, .. }) = handler; if type_.is_none() && idx < handlers.len() - 1 { return Some(Diagnostic::new( DefaultExceptNotLast, diff --git a/crates/ruff/src/rules/pyflakes/rules/invalid_literal_comparisons.rs b/crates/ruff/src/rules/pyflakes/rules/invalid_literal_comparisons.rs index 42cfc34b9a..f01023a4e9 100644 --- a/crates/ruff/src/rules/pyflakes/rules/invalid_literal_comparisons.rs +++ b/crates/ruff/src/rules/pyflakes/rules/invalid_literal_comparisons.rs @@ -1,7 +1,7 @@ use itertools::izip; use log::error; use once_cell::unsync::Lazy; -use rustpython_parser::ast::{Cmpop, Expr, Ranged}; +use rustpython_parser::ast::{CmpOp, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -47,24 +47,24 @@ use crate::registry::AsRule; /// - [_Why does Python log a SyntaxWarning for ‘is’ with literals?_ by Adam Johnson](https://adamj.eu/tech/2020/01/21/why-does-python-3-8-syntaxwarning-for-is-literal/) #[violation] pub struct IsLiteral { - cmpop: IsCmpop, + cmp_op: IsCmpOp, } impl AlwaysAutofixableViolation for IsLiteral { #[derive_message_formats] fn message(&self) -> String { - let IsLiteral { cmpop } = self; - match cmpop { - IsCmpop::Is => format!("Use `==` to compare constant literals"), - IsCmpop::IsNot => format!("Use `!=` to compare constant literals"), + let IsLiteral { cmp_op } = self; + match cmp_op { + IsCmpOp::Is => format!("Use `==` to compare constant literals"), + IsCmpOp::IsNot => format!("Use `!=` to compare constant literals"), } } fn autofix_title(&self) -> String { - let IsLiteral { cmpop } = self; - match cmpop { - IsCmpop::Is => "Replace `is` with `==`".to_string(), - IsCmpop::IsNot => "Replace `is not` with `!=`".to_string(), + let IsLiteral { cmp_op } = self; + match cmp_op { + IsCmpOp::Is => "Replace `is` with `==`".to_string(), + IsCmpOp::IsNot => "Replace `is not` with `!=`".to_string(), } } } @@ -73,24 +73,24 @@ impl AlwaysAutofixableViolation for IsLiteral { pub(crate) fn invalid_literal_comparison( checker: &mut Checker, left: &Expr, - ops: &[Cmpop], + ops: &[CmpOp], comparators: &[Expr], expr: &Expr, ) { - let located = Lazy::new(|| helpers::locate_cmpops(expr, checker.locator)); + let located = Lazy::new(|| helpers::locate_cmp_ops(expr, checker.locator)); let mut left = left; for (index, (op, right)) in izip!(ops, comparators).enumerate() { - if matches!(op, Cmpop::Is | Cmpop::IsNot) + if matches!(op, CmpOp::Is | CmpOp::IsNot) && (helpers::is_constant_non_singleton(left) || helpers::is_constant_non_singleton(right)) { - let mut diagnostic = Diagnostic::new(IsLiteral { cmpop: op.into() }, expr.range()); + let mut diagnostic = Diagnostic::new(IsLiteral { cmp_op: op.into() }, expr.range()); if checker.patch(diagnostic.kind.rule()) { if let Some(located_op) = &located.get(index) { assert_eq!(located_op.op, *op); if let Some(content) = match located_op.op { - Cmpop::Is => Some("==".to_string()), - Cmpop::IsNot => Some("!=".to_string()), + CmpOp::Is => Some("==".to_string()), + CmpOp::IsNot => Some("!=".to_string()), node => { error!("Failed to fix invalid comparison: {node:?}"); None @@ -112,17 +112,17 @@ pub(crate) fn invalid_literal_comparison( } #[derive(Debug, PartialEq, Eq, Copy, Clone)] -enum IsCmpop { +enum IsCmpOp { Is, IsNot, } -impl From<&Cmpop> for IsCmpop { - fn from(cmpop: &Cmpop) -> Self { - match cmpop { - Cmpop::Is => IsCmpop::Is, - Cmpop::IsNot => IsCmpop::IsNot, - _ => panic!("Expected Cmpop::Is | Cmpop::IsNot"), +impl From<&CmpOp> for IsCmpOp { + fn from(cmp_op: &CmpOp) -> Self { + match cmp_op { + CmpOp::Is => IsCmpOp::Is, + CmpOp::IsNot => IsCmpOp::IsNot, + _ => panic!("Expected CmpOp::Is | CmpOp::IsNot"), } } } diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs b/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs index de24c4d795..f9ede7b63b 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs @@ -253,10 +253,10 @@ fn remove_unused_variable( } } - // Third case: withitem (`with foo() as x:`) + // Third case: with_item (`with foo() as x:`) if let Stmt::With(ast::StmtWith { items, .. }) = stmt { // Find the binding that matches the given `Range`. - // TODO(charlie): Store the `Withitem` in the `Binding`. + // TODO(charlie): Store the `WithItem` in the `Binding`. for item in items { if let Some(optional_vars) = &item.optional_vars { if optional_vars.range() == range { diff --git a/crates/ruff/src/rules/pylint/helpers.rs b/crates/ruff/src/rules/pylint/helpers.rs index 37d48f5104..8af22107a3 100644 --- a/crates/ruff/src/rules/pylint/helpers.rs +++ b/crates/ruff/src/rules/pylint/helpers.rs @@ -1,7 +1,7 @@ use std::fmt; use rustpython_parser::ast; -use rustpython_parser::ast::Cmpop; +use rustpython_parser::ast::CmpOp; use ruff_python_semantic::analyze::function_type; use ruff_python_semantic::{ScopeKind, SemanticModel}; @@ -47,29 +47,29 @@ pub(super) fn in_dunder_init(semantic: &SemanticModel, settings: &Settings) -> b true } -/// A wrapper around [`Cmpop`] that implements `Display`. +/// A wrapper around [`CmpOp`] that implements `Display`. #[derive(Debug)] -pub(super) struct CmpopExt(Cmpop); +pub(super) struct CmpOpExt(CmpOp); -impl From<&Cmpop> for CmpopExt { - fn from(cmpop: &Cmpop) -> Self { - CmpopExt(*cmpop) +impl From<&CmpOp> for CmpOpExt { + fn from(cmp_op: &CmpOp) -> Self { + CmpOpExt(*cmp_op) } } -impl fmt::Display for CmpopExt { +impl fmt::Display for CmpOpExt { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let representation = match self.0 { - Cmpop::Eq => "==", - Cmpop::NotEq => "!=", - Cmpop::Lt => "<", - Cmpop::LtE => "<=", - Cmpop::Gt => ">", - Cmpop::GtE => ">=", - Cmpop::Is => "is", - Cmpop::IsNot => "is not", - Cmpop::In => "in", - Cmpop::NotIn => "not in", + CmpOp::Eq => "==", + CmpOp::NotEq => "!=", + CmpOp::Lt => "<", + CmpOp::LtE => "<=", + CmpOp::Gt => ">", + CmpOp::GtE => ">=", + CmpOp::Is => "is", + CmpOp::IsNot => "is not", + CmpOp::In => "in", + CmpOp::NotIn => "not in", }; write!(f, "{representation}") } diff --git a/crates/ruff/src/rules/pylint/rules/binary_op_exception.rs b/crates/ruff/src/rules/pylint/rules/binary_op_exception.rs index 8ce1fc2bfd..fc871a02ec 100644 --- a/crates/ruff/src/rules/pylint/rules/binary_op_exception.rs +++ b/crates/ruff/src/rules/pylint/rules/binary_op_exception.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Excepthandler, Expr, Ranged}; +use rustpython_parser::ast::{self, ExceptHandler, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -6,16 +6,16 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; #[derive(Debug, PartialEq, Eq, Copy, Clone)] -enum Boolop { +enum BoolOp { And, Or, } -impl From<&ast::Boolop> for Boolop { - fn from(op: &ast::Boolop) -> Self { +impl From<&ast::BoolOp> for BoolOp { + fn from(op: &ast::BoolOp) -> Self { match op { - ast::Boolop::And => Boolop::And, - ast::Boolop::Or => Boolop::Or, + ast::BoolOp::And => BoolOp::And, + ast::BoolOp::Or => BoolOp::Or, } } } @@ -47,7 +47,7 @@ impl From<&ast::Boolop> for Boolop { /// ``` #[violation] pub struct BinaryOpException { - op: Boolop, + op: BoolOp, } impl Violation for BinaryOpException { @@ -55,15 +55,16 @@ impl Violation for BinaryOpException { fn message(&self) -> String { let BinaryOpException { op } = self; match op { - Boolop::And => format!("Exception to catch is the result of a binary `and` operation"), - Boolop::Or => format!("Exception to catch is the result of a binary `or` operation"), + BoolOp::And => format!("Exception to catch is the result of a binary `and` operation"), + BoolOp::Or => format!("Exception to catch is the result of a binary `or` operation"), } } } /// PLW0711 -pub(crate) fn binary_op_exception(checker: &mut Checker, excepthandler: &Excepthandler) { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { type_, .. }) = excepthandler; +pub(crate) fn binary_op_exception(checker: &mut Checker, except_handler: &ExceptHandler) { + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, .. }) = + except_handler; let Some(type_) = type_ else { return; diff --git a/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs b/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs index f79ae2e74f..da44e79fe3 100644 --- a/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs +++ b/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs @@ -1,6 +1,6 @@ use anyhow::bail; use itertools::Itertools; -use rustpython_parser::ast::{self, Cmpop, Constant, Expr, Ranged}; +use rustpython_parser::ast::{self, CmpOp, Constant, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -8,28 +8,28 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; #[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub(crate) enum EmptyStringCmpop { +pub(crate) enum EmptyStringCmpOp { Is, IsNot, Eq, NotEq, } -impl TryFrom<&Cmpop> for EmptyStringCmpop { +impl TryFrom<&CmpOp> for EmptyStringCmpOp { type Error = anyhow::Error; - fn try_from(value: &Cmpop) -> Result { + fn try_from(value: &CmpOp) -> Result { match value { - Cmpop::Is => Ok(Self::Is), - Cmpop::IsNot => Ok(Self::IsNot), - Cmpop::Eq => Ok(Self::Eq), - Cmpop::NotEq => Ok(Self::NotEq), - _ => bail!("{value:?} cannot be converted to EmptyStringCmpop"), + CmpOp::Is => Ok(Self::Is), + CmpOp::IsNot => Ok(Self::IsNot), + CmpOp::Eq => Ok(Self::Eq), + CmpOp::NotEq => Ok(Self::NotEq), + _ => bail!("{value:?} cannot be converted to EmptyStringCmpOp"), } } } -impl EmptyStringCmpop { +impl EmptyStringCmpOp { pub(crate) fn into_unary(self) -> &'static str { match self { Self::Is | Self::Eq => "not ", @@ -38,7 +38,7 @@ impl EmptyStringCmpop { } } -impl std::fmt::Display for EmptyStringCmpop { +impl std::fmt::Display for EmptyStringCmpOp { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let repr = match self { Self::Is => "is", @@ -93,7 +93,7 @@ impl Violation for CompareToEmptyString { pub(crate) fn compare_to_empty_string( checker: &mut Checker, left: &Expr, - ops: &[Cmpop], + ops: &[CmpOp], comparators: &[Expr], ) { // Omit string comparison rules within subscripts. This is mostly commonly used within @@ -110,7 +110,7 @@ pub(crate) fn compare_to_empty_string( .tuple_windows::<(&Expr<_>, &Expr<_>)>() .zip(ops) { - if let Ok(op) = EmptyStringCmpop::try_from(op) { + if let Ok(op) = EmptyStringCmpOp::try_from(op) { if std::mem::take(&mut first) { // Check the left-most expression. if let Expr::Constant(ast::ExprConstant { value, .. }) = &lhs { diff --git a/crates/ruff/src/rules/pylint/rules/comparison_of_constant.rs b/crates/ruff/src/rules/pylint/rules/comparison_of_constant.rs index 8930264d1e..0caac387c4 100644 --- a/crates/ruff/src/rules/pylint/rules/comparison_of_constant.rs +++ b/crates/ruff/src/rules/pylint/rules/comparison_of_constant.rs @@ -1,11 +1,11 @@ use itertools::Itertools; -use rustpython_parser::ast::{self, Cmpop, Expr, Ranged}; +use rustpython_parser::ast::{self, CmpOp, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; -use crate::rules::pylint::helpers::CmpopExt; +use crate::rules::pylint::helpers::CmpOpExt; /// ## What it does /// Checks for comparisons between constants. @@ -30,7 +30,7 @@ use crate::rules::pylint::helpers::CmpopExt; #[violation] pub struct ComparisonOfConstant { left_constant: String, - op: Cmpop, + op: CmpOp, right_constant: String, } @@ -45,7 +45,7 @@ impl Violation for ComparisonOfConstant { format!( "Two constants compared in a comparison, consider replacing `{left_constant} {} {right_constant}`", - CmpopExt::from(op) + CmpOpExt::from(op) ) } } @@ -54,7 +54,7 @@ impl Violation for ComparisonOfConstant { pub(crate) fn comparison_of_constant( checker: &mut Checker, left: &Expr, - ops: &[Cmpop], + ops: &[CmpOp], comparators: &[Expr], ) { for ((left, right), op) in std::iter::once(left) diff --git a/crates/ruff/src/rules/pylint/rules/comparison_with_itself.rs b/crates/ruff/src/rules/pylint/rules/comparison_with_itself.rs index 12824054cd..7758eb2989 100644 --- a/crates/ruff/src/rules/pylint/rules/comparison_with_itself.rs +++ b/crates/ruff/src/rules/pylint/rules/comparison_with_itself.rs @@ -1,11 +1,11 @@ use itertools::Itertools; -use rustpython_parser::ast::{Cmpop, Expr, Ranged}; +use rustpython_parser::ast::{CmpOp, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; -use crate::rules::pylint::helpers::CmpopExt; +use crate::rules::pylint::helpers::CmpOpExt; /// ## What it does /// Checks for operations that compare a name to itself. @@ -24,7 +24,7 @@ use crate::rules::pylint::helpers::CmpopExt; #[violation] pub struct ComparisonWithItself { left: String, - op: Cmpop, + op: CmpOp, right: String, } @@ -34,7 +34,7 @@ impl Violation for ComparisonWithItself { let ComparisonWithItself { left, op, right } = self; format!( "Name compared with itself, consider replacing `{left} {} {right}`", - CmpopExt::from(op) + CmpOpExt::from(op) ) } } @@ -43,7 +43,7 @@ impl Violation for ComparisonWithItself { pub(crate) fn comparison_with_itself( checker: &mut Checker, left: &Expr, - ops: &[Cmpop], + ops: &[CmpOp], comparators: &[Expr], ) { for ((left, right), op) in std::iter::once(left) diff --git a/crates/ruff/src/rules/pylint/rules/magic_value_comparison.rs b/crates/ruff/src/rules/pylint/rules/magic_value_comparison.rs index c337641c79..0051f8907e 100644 --- a/crates/ruff/src/rules/pylint/rules/magic_value_comparison.rs +++ b/crates/ruff/src/rules/pylint/rules/magic_value_comparison.rs @@ -1,5 +1,5 @@ use itertools::Itertools; -use rustpython_parser::ast::{self, Constant, Expr, Ranged, Unaryop}; +use rustpython_parser::ast::{self, Constant, Expr, Ranged, UnaryOp}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -55,7 +55,7 @@ fn as_constant(expr: &Expr) -> Option<&Constant> { match expr { Expr::Constant(ast::ExprConstant { value, .. }) => Some(value), Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::UAdd | Unaryop::USub | Unaryop::Invert, + op: UnaryOp::UAdd | UnaryOp::USub | UnaryOp::Invert, operand, range: _, }) => match operand.as_ref() { diff --git a/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs b/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs index 1029a9d97f..9f826bf3a3 100644 --- a/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs +++ b/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs @@ -59,14 +59,14 @@ pub(crate) fn property_with_parameters( { return; } - if checker.semantic().is_builtin("property") - && args - .args - .iter() - .chain(args.posonlyargs.iter()) - .chain(args.kwonlyargs.iter()) - .count() - > 1 + if args + .posonlyargs + .iter() + .chain(&args.args) + .chain(&args.kwonlyargs) + .count() + > 1 + && checker.semantic().is_builtin("property") { checker.diagnostics.push(Diagnostic::new( PropertyWithParameters, diff --git a/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs b/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs index 0bdcb3ce9e..c0536daee1 100644 --- a/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs +++ b/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs @@ -1,7 +1,7 @@ use std::{fmt, iter}; use regex::Regex; -use rustpython_parser::ast::{self, Expr, ExprContext, Ranged, Stmt, Withitem}; +use rustpython_parser::ast::{self, Expr, ExprContext, Ranged, Stmt, WithItem}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -308,7 +308,7 @@ fn assignment_targets_from_expr<'a>( } fn assignment_targets_from_with_items<'a>( - items: &'a [Withitem], + items: &'a [WithItem], dummy_variable_rgx: &'a Regex, ) -> impl Iterator + 'a { items diff --git a/crates/ruff/src/rules/pylint/rules/repeated_isinstance_calls.rs b/crates/ruff/src/rules/pylint/rules/repeated_isinstance_calls.rs index 4ddc1cbf55..c4038767cc 100644 --- a/crates/ruff/src/rules/pylint/rules/repeated_isinstance_calls.rs +++ b/crates/ruff/src/rules/pylint/rules/repeated_isinstance_calls.rs @@ -1,6 +1,6 @@ use itertools::Itertools; use rustc_hash::{FxHashMap, FxHashSet}; -use rustpython_parser::ast::{self, Boolop, Expr, Ranged}; +use rustpython_parser::ast::{self, BoolOp, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -63,7 +63,7 @@ impl AlwaysAutofixableViolation for RepeatedIsinstanceCalls { pub(crate) fn repeated_isinstance_calls( checker: &mut Checker, expr: &Expr, - op: Boolop, + op: BoolOp, values: &[Expr], ) { if !op.is_or() { diff --git a/crates/ruff/src/rules/pylint/rules/too_many_arguments.rs b/crates/ruff/src/rules/pylint/rules/too_many_arguments.rs index 2d050ee75c..c5b396588e 100644 --- a/crates/ruff/src/rules/pylint/rules/too_many_arguments.rs +++ b/crates/ruff/src/rules/pylint/rules/too_many_arguments.rs @@ -58,18 +58,18 @@ impl Violation for TooManyArguments { } /// PLR0913 -pub(crate) fn too_many_arguments(checker: &mut Checker, args: &Arguments, stmt: &Stmt) { - let num_args = args +pub(crate) fn too_many_arguments(checker: &mut Checker, arguments: &Arguments, stmt: &Stmt) { + let num_arguments = arguments .args .iter() - .chain(args.kwonlyargs.iter()) - .chain(args.posonlyargs.iter()) - .filter(|arg| !checker.settings.dummy_variable_rgx.is_match(&arg.arg)) + .chain(&arguments.kwonlyargs) + .chain(&arguments.posonlyargs) + .filter(|arg| !checker.settings.dummy_variable_rgx.is_match(&arg.def.arg)) .count(); - if num_args > checker.settings.pylint.max_args { + if num_arguments > checker.settings.pylint.max_args { checker.diagnostics.push(Diagnostic::new( TooManyArguments { - c_args: num_args, + c_args: num_arguments, max_args: checker.settings.pylint.max_args, }, stmt.identifier(checker.locator), diff --git a/crates/ruff/src/rules/pylint/rules/too_many_branches.rs b/crates/ruff/src/rules/pylint/rules/too_many_branches.rs index 8a9b0be78f..a0330cb8cf 100644 --- a/crates/ruff/src/rules/pylint/rules/too_many_branches.rs +++ b/crates/ruff/src/rules/pylint/rules/too_many_branches.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Excepthandler, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -147,8 +147,8 @@ fn num_branches(stmts: &[Stmt]) -> usize { .iter() .map(|handler| { 1 + { - let Excepthandler::ExceptHandler( - ast::ExcepthandlerExceptHandler { body, .. }, + let ExceptHandler::ExceptHandler( + ast::ExceptHandlerExceptHandler { body, .. }, ) = handler; num_branches(body) } diff --git a/crates/ruff/src/rules/pylint/rules/too_many_statements.rs b/crates/ruff/src/rules/pylint/rules/too_many_statements.rs index 755daab2b0..2fd832459f 100644 --- a/crates/ruff/src/rules/pylint/rules/too_many_statements.rs +++ b/crates/ruff/src/rules/pylint/rules/too_many_statements.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Excepthandler, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -123,7 +123,7 @@ fn num_statements(stmts: &[Stmt]) -> usize { } for handler in handlers { count += 1; - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) = handler; count += num_statements(body); 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 8277cb699e..e7b25da4a5 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,8 +160,7 @@ pub(crate) fn unexpected_special_method_signature( } let actual_params = args.args.len(); - let optional_params = args.defaults.len(); - let mandatory_params = actual_params - optional_params; + 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 { return; diff --git a/crates/ruff/src/rules/pylint/rules/useless_else_on_loop.rs b/crates/ruff/src/rules/pylint/rules/useless_else_on_loop.rs index ab6f737cfb..32fb1b8b59 100644 --- a/crates/ruff/src/rules/pylint/rules/useless_else_on_loop.rs +++ b/crates/ruff/src/rules/pylint/rules/useless_else_on_loop.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Excepthandler, MatchCase, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, MatchCase, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -79,7 +79,7 @@ fn loop_exits_early(body: &[Stmt]) -> bool { || loop_exits_early(orelse) || loop_exits_early(finalbody) || handlers.iter().any(|handler| match handler { - Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { + ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) => loop_exits_early(body), }) diff --git a/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs index 0493a63e2a..09f7821919 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs @@ -1,5 +1,5 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Excepthandler, Expr, ExprContext, Ranged}; +use rustpython_parser::ast::{self, ExceptHandler, Expr, ExprContext, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -151,9 +151,9 @@ fn tuple_diagnostic(checker: &mut Checker, target: &Expr, aliases: &[&Expr]) { } /// UP024 -pub(crate) fn os_error_alias_handlers(checker: &mut Checker, handlers: &[Excepthandler]) { +pub(crate) fn os_error_alias_handlers(checker: &mut Checker, handlers: &[ExceptHandler]) { for handler in handlers { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { type_, .. }) = handler; + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, .. }) = handler; let Some(expr) = type_.as_ref() else { continue; }; 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 626c8b5ba0..5598b2a76d 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs @@ -2,7 +2,7 @@ use std::cmp::Ordering; use num_bigint::{BigInt, Sign}; use ruff_text_size::{TextRange, TextSize}; -use rustpython_parser::ast::{self, Cmpop, Constant, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, CmpOp, Constant, Expr, Ranged, Stmt}; use rustpython_parser::{lexer, Mode, Tok}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; @@ -370,8 +370,8 @@ pub(crate) fn outdated_version_block( Expr::Tuple(ast::ExprTuple { elts, .. }) => { let version = extract_version(elts); let target = checker.settings.target_version; - if op == &Cmpop::Lt || op == &Cmpop::LtE { - if compare_version(&version, target, op == &Cmpop::LtE) { + if op == &CmpOp::Lt || op == &CmpOp::LtE { + if compare_version(&version, target, op == &CmpOp::LtE) { let mut diagnostic = Diagnostic::new(OutdatedVersionBlock, stmt.range()); if checker.patch(diagnostic.kind.rule()) { if let Some(block) = metadata(checker.locator, stmt, body) { @@ -382,8 +382,8 @@ pub(crate) fn outdated_version_block( } checker.diagnostics.push(diagnostic); } - } else if op == &Cmpop::Gt || op == &Cmpop::GtE { - if compare_version(&version, target, op == &Cmpop::GtE) { + } else if op == &CmpOp::Gt || op == &CmpOp::GtE { + if compare_version(&version, target, op == &CmpOp::GtE) { let mut diagnostic = Diagnostic::new(OutdatedVersionBlock, stmt.range()); if checker.patch(diagnostic.kind.rule()) { if let Some(block) = metadata(checker.locator, stmt, body) { @@ -402,7 +402,7 @@ pub(crate) fn outdated_version_block( .. }) => { let version_number = bigint_to_u32(number); - if version_number == 2 && op == &Cmpop::Eq { + if version_number == 2 && op == &CmpOp::Eq { let mut diagnostic = Diagnostic::new(OutdatedVersionBlock, stmt.range()); if checker.patch(diagnostic.kind.rule()) { if let Some(block) = metadata(checker.locator, stmt, body) { @@ -412,7 +412,7 @@ pub(crate) fn outdated_version_block( } } checker.diagnostics.push(diagnostic); - } else if version_number == 3 && op == &Cmpop::Eq { + } else if version_number == 3 && op == &CmpOp::Eq { let mut diagnostic = Diagnostic::new(OutdatedVersionBlock, stmt.range()); if checker.patch(diagnostic.kind.rule()) { if let Some(block) = metadata(checker.locator, stmt, body) { 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 898ef4f364..cdde59a7e0 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 @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Arg, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, Arg, ArgWithDefault, Expr, Ranged, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -104,8 +104,9 @@ pub(crate) fn super_call_with_parameters( }; // Extract the name of the first argument to the enclosing function. - let Some(Arg { - arg: parent_arg, .. + let Some(ArgWithDefault { + def: Arg { arg: parent_arg, .. }, + .. }) = parent_args.args.first() else { return; }; diff --git a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs index 573122e469..105d076112 100644 --- a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs +++ b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs @@ -1,13 +1,13 @@ use std::fmt; use anyhow::Result; -use rustpython_parser::ast::{self, Arguments, Constant, Expr, Operator, Ranged}; +use ruff_text_size::TextRange; +use rustpython_parser::ast::{self, ArgWithDefault, Arguments, Constant, Expr, Operator, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::is_const_none; use ruff_python_semantic::SemanticModel; -use ruff_text_size::TextRange; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; @@ -307,24 +307,23 @@ fn generate_fix(checker: &Checker, conversion_type: ConversionType, expr: &Expr) /// RUF011 pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) { - let arguments_with_defaults = arguments - .kwonlyargs + for ArgWithDefault { + def, + default, + range: _, + } in arguments + .posonlyargs .iter() - .rev() - .zip(arguments.kw_defaults.iter().rev()) - .chain( - arguments - .args - .iter() - .rev() - .chain(arguments.posonlyargs.iter().rev()) - .zip(arguments.defaults.iter().rev()), - ); - for (arg, default) in arguments_with_defaults { + .chain(&arguments.args) + .chain(&arguments.kwonlyargs) + { + let Some(default) = default else { + continue + }; if !is_const_none(default) { continue; } - let Some(annotation) = &arg.annotation else { + let Some(annotation) = &def.annotation else { continue }; let Some(expr) = type_hint_explicitly_allows_none(annotation, checker.semantic()) else { 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 3e6f60fd54..5aec8dc085 100644 --- a/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs +++ b/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs @@ -1,5 +1,5 @@ use num_traits::ToPrimitive; -use rustpython_parser::ast::{self, Constant, Expr, Ranged, Unaryop}; +use rustpython_parser::ast::{self, Constant, Expr, Ranged, UnaryOp}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -69,7 +69,7 @@ fn to_bound(expr: &Expr) -> Option { .. }) => value.to_i64(), Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::USub | Unaryop::Invert, + op: UnaryOp::USub | UnaryOp::Invert, operand, range: _, }) => { diff --git a/crates/ruff/src/rules/tryceratops/rules/error_instead_of_exception.rs b/crates/ruff/src/rules/tryceratops/rules/error_instead_of_exception.rs index a335c103f4..4e111e7e99 100644 --- a/crates/ruff/src/rules/tryceratops/rules/error_instead_of_exception.rs +++ b/crates/ruff/src/rules/tryceratops/rules/error_instead_of_exception.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Excepthandler, Expr, Ranged}; +use rustpython_parser::ast::{self, ExceptHandler, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -54,9 +54,9 @@ impl Violation for ErrorInsteadOfException { } /// TRY400 -pub(crate) fn error_instead_of_exception(checker: &mut Checker, handlers: &[Excepthandler]) { +pub(crate) fn error_instead_of_exception(checker: &mut Checker, handlers: &[ExceptHandler]) { for handler in handlers { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { body, .. }) = handler; + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) = handler; let calls = { let mut visitor = LoggerCandidateVisitor::new(checker.semantic()); visitor.visit_body(body); diff --git a/crates/ruff/src/rules/tryceratops/rules/raise_within_try.rs b/crates/ruff/src/rules/tryceratops/rules/raise_within_try.rs index 5dcdcc117e..f980a21dee 100644 --- a/crates/ruff/src/rules/tryceratops/rules/raise_within_try.rs +++ b/crates/ruff/src/rules/tryceratops/rules/raise_within_try.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{Excepthandler, Ranged, Stmt}; +use rustpython_parser::ast::{ExceptHandler, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -72,7 +72,7 @@ where } /// TRY301 -pub(crate) fn raise_within_try(checker: &mut Checker, body: &[Stmt], handlers: &[Excepthandler]) { +pub(crate) fn raise_within_try(checker: &mut Checker, body: &[Stmt], handlers: &[ExceptHandler]) { if handlers.is_empty() { return; } diff --git a/crates/ruff/src/rules/tryceratops/rules/try_consider_else.rs b/crates/ruff/src/rules/tryceratops/rules/try_consider_else.rs index 10f30c1e25..0ed6b3b27c 100644 --- a/crates/ruff/src/rules/tryceratops/rules/try_consider_else.rs +++ b/crates/ruff/src/rules/tryceratops/rules/try_consider_else.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Excepthandler, Ranged, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -60,7 +60,7 @@ pub(crate) fn try_consider_else( checker: &mut Checker, body: &[Stmt], orelse: &[Stmt], - handler: &[Excepthandler], + handler: &[ExceptHandler], ) { if body.len() > 1 && orelse.is_empty() && !handler.is_empty() { if let Some(stmt) = body.last() { 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 8869e4eccd..c2620b9515 100644 --- a/crates/ruff/src/rules/tryceratops/rules/useless_try_except.rs +++ b/crates/ruff/src/rules/tryceratops/rules/useless_try_except.rs @@ -1,5 +1,4 @@ -use rustpython_parser::ast::Excepthandler::ExceptHandler; -use rustpython_parser::ast::{self, Excepthandler, ExcepthandlerExceptHandler, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, ExceptHandlerExceptHandler, Expr, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -39,11 +38,12 @@ impl Violation for UselessTryExcept { } /// TRY302 -pub(crate) fn useless_try_except(checker: &mut Checker, handlers: &[Excepthandler]) { +pub(crate) fn useless_try_except(checker: &mut Checker, handlers: &[ExceptHandler]) { if let Some(diagnostics) = handlers .iter() .map(|handler| { - let ExceptHandler(ExcepthandlerExceptHandler { name, body, .. }) = handler; + let ExceptHandler::ExceptHandler(ExceptHandlerExceptHandler { name, body, .. }) = + handler; let Some(Stmt::Raise(ast::StmtRaise { exc, cause: None, .. })) = &body.first() else { return None; }; diff --git a/crates/ruff/src/rules/tryceratops/rules/verbose_log_message.rs b/crates/ruff/src/rules/tryceratops/rules/verbose_log_message.rs index 7153fe800a..4852bac33f 100644 --- a/crates/ruff/src/rules/tryceratops/rules/verbose_log_message.rs +++ b/crates/ruff/src/rules/tryceratops/rules/verbose_log_message.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Excepthandler, Expr, Ranged}; +use rustpython_parser::ast::{self, ExceptHandler, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -57,9 +57,9 @@ impl<'a> Visitor<'a> for NameVisitor<'a> { } /// TRY401 -pub(crate) fn verbose_log_message(checker: &mut Checker, handlers: &[Excepthandler]) { +pub(crate) fn verbose_log_message(checker: &mut Checker, handlers: &[ExceptHandler]) { for handler in handlers { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { name, body, .. }) = + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { name, body, .. }) = handler; let Some(target) = name else { continue; diff --git a/crates/ruff/src/rules/tryceratops/rules/verbose_raise.rs b/crates/ruff/src/rules/tryceratops/rules/verbose_raise.rs index b740212215..56123b43ad 100644 --- a/crates/ruff/src/rules/tryceratops/rules/verbose_raise.rs +++ b/crates/ruff/src/rules/tryceratops/rules/verbose_raise.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Excepthandler, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, Expr, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -74,10 +74,10 @@ where } /// TRY201 -pub(crate) fn verbose_raise(checker: &mut Checker, handlers: &[Excepthandler]) { +pub(crate) fn verbose_raise(checker: &mut Checker, handlers: &[ExceptHandler]) { for handler in handlers { // If the handler assigned a name to the exception... - if let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { + if let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { name: Some(exception_name), body, .. diff --git a/crates/ruff_python_ast/src/comparable.rs b/crates/ruff_python_ast/src/comparable.rs index b69f6f31a0..ed246f137e 100644 --- a/crates/ruff_python_ast/src/comparable.rs +++ b/crates/ruff_python_ast/src/comparable.rs @@ -22,16 +22,16 @@ impl From<&ast::ExprContext> for ComparableExprContext { } #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] -pub enum ComparableBoolop { +pub enum ComparableBoolOp { And, Or, } -impl From for ComparableBoolop { - fn from(op: ast::Boolop) -> Self { +impl From for ComparableBoolOp { + fn from(op: ast::BoolOp) -> Self { match op { - ast::Boolop::And => Self::And, - ast::Boolop::Or => Self::Or, + ast::BoolOp::And => Self::And, + ast::BoolOp::Or => Self::Or, } } } @@ -74,26 +74,26 @@ impl From for ComparableOperator { } #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] -pub enum ComparableUnaryop { +pub enum ComparableUnaryOp { Invert, Not, UAdd, USub, } -impl From for ComparableUnaryop { - fn from(op: ast::Unaryop) -> Self { +impl From for ComparableUnaryOp { + fn from(op: ast::UnaryOp) -> Self { match op { - ast::Unaryop::Invert => Self::Invert, - ast::Unaryop::Not => Self::Not, - ast::Unaryop::UAdd => Self::UAdd, - ast::Unaryop::USub => Self::USub, + ast::UnaryOp::Invert => Self::Invert, + ast::UnaryOp::Not => Self::Not, + ast::UnaryOp::UAdd => Self::UAdd, + ast::UnaryOp::USub => Self::USub, } } } #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] -pub enum ComparableCmpop { +pub enum ComparableCmpOp { Eq, NotEq, Lt, @@ -106,19 +106,19 @@ pub enum ComparableCmpop { NotIn, } -impl From for ComparableCmpop { - fn from(op: ast::Cmpop) -> Self { +impl From for ComparableCmpOp { + fn from(op: ast::CmpOp) -> Self { match op { - ast::Cmpop::Eq => Self::Eq, - ast::Cmpop::NotEq => Self::NotEq, - ast::Cmpop::Lt => Self::Lt, - ast::Cmpop::LtE => Self::LtE, - ast::Cmpop::Gt => Self::Gt, - ast::Cmpop::GtE => Self::GtE, - ast::Cmpop::Is => Self::Is, - ast::Cmpop::IsNot => Self::IsNot, - ast::Cmpop::In => Self::In, - ast::Cmpop::NotIn => Self::NotIn, + ast::CmpOp::Eq => Self::Eq, + ast::CmpOp::NotEq => Self::NotEq, + ast::CmpOp::Lt => Self::Lt, + ast::CmpOp::LtE => Self::LtE, + ast::CmpOp::Gt => Self::Gt, + ast::CmpOp::GtE => Self::GtE, + ast::CmpOp::Is => Self::Is, + ast::CmpOp::IsNot => Self::IsNot, + ast::CmpOp::In => Self::In, + ast::CmpOp::NotIn => Self::NotIn, } } } @@ -139,16 +139,16 @@ impl<'a> From<&'a ast::Alias> for ComparableAlias<'a> { } #[derive(Debug, PartialEq, Eq, Hash)] -pub struct ComparableWithitem<'a> { +pub struct ComparableWithItem<'a> { context_expr: ComparableExpr<'a>, optional_vars: Option>, } -impl<'a> From<&'a ast::Withitem> for ComparableWithitem<'a> { - fn from(withitem: &'a ast::Withitem) -> Self { +impl<'a> From<&'a ast::WithItem> for ComparableWithItem<'a> { + fn from(with_item: &'a ast::WithItem) -> Self { Self { - context_expr: (&withitem.context_expr).into(), - optional_vars: withitem.optional_vars.as_ref().map(Into::into), + context_expr: (&with_item.context_expr).into(), + optional_vars: with_item.optional_vars.as_ref().map(Into::into), } } } @@ -342,13 +342,11 @@ impl<'a> From<&'a ast::Constant> for ComparableConstant<'a> { #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableArguments<'a> { - posonlyargs: Vec>, - args: Vec>, + posonlyargs: Vec>, + args: Vec>, vararg: Option>, - kwonlyargs: Vec>, - kw_defaults: Vec>, + kwonlyargs: Vec>, kwarg: Option>, - defaults: Vec>, } impl<'a> From<&'a ast::Arguments> for ComparableArguments<'a> { @@ -358,9 +356,7 @@ impl<'a> From<&'a ast::Arguments> for ComparableArguments<'a> { args: arguments.args.iter().map(Into::into).collect(), vararg: arguments.vararg.as_ref().map(Into::into), kwonlyargs: arguments.kwonlyargs.iter().map(Into::into).collect(), - kw_defaults: arguments.kw_defaults.iter().map(Into::into).collect(), kwarg: arguments.kwarg.as_ref().map(Into::into), - defaults: arguments.defaults.iter().map(Into::into).collect(), } } } @@ -394,6 +390,21 @@ impl<'a> From<&'a ast::Arg> for ComparableArg<'a> { } } +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ComparableArgWithDefault<'a> { + def: ComparableArg<'a>, + default: Option>, +} + +impl<'a> From<&'a ast::ArgWithDefault> for ComparableArgWithDefault<'a> { + fn from(arg: &'a ast::ArgWithDefault) -> Self { + Self { + def: (&arg.def).into(), + default: arg.default.as_ref().map(Into::into), + } + } +} + #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableKeyword<'a> { arg: Option<&'a str>, @@ -429,26 +440,26 @@ impl<'a> From<&'a ast::Comprehension> for ComparableComprehension<'a> { } #[derive(Debug, PartialEq, Eq, Hash)] -pub struct ExcepthandlerExceptHandler<'a> { +pub struct ExceptHandlerExceptHandler<'a> { type_: Option>>, name: Option<&'a str>, body: Vec>, } #[derive(Debug, PartialEq, Eq, Hash)] -pub enum ComparableExcepthandler<'a> { - ExceptHandler(ExcepthandlerExceptHandler<'a>), +pub enum ComparableExceptHandler<'a> { + ExceptHandler(ExceptHandlerExceptHandler<'a>), } -impl<'a> From<&'a ast::Excepthandler> for ComparableExcepthandler<'a> { - fn from(excepthandler: &'a ast::Excepthandler) -> Self { - let ast::Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { +impl<'a> From<&'a ast::ExceptHandler> for ComparableExceptHandler<'a> { + fn from(except_handler: &'a ast::ExceptHandler) -> Self { + let ast::ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, name, body, .. - }) = excepthandler; - Self::ExceptHandler(ExcepthandlerExceptHandler { + }) = except_handler; + Self::ExceptHandler(ExceptHandlerExceptHandler { type_: type_.as_ref().map(Into::into), name: name.as_deref(), body: body.iter().map(Into::into).collect(), @@ -458,7 +469,7 @@ impl<'a> From<&'a ast::Excepthandler> for ComparableExcepthandler<'a> { #[derive(Debug, PartialEq, Eq, Hash)] pub struct ExprBoolOp<'a> { - op: ComparableBoolop, + op: ComparableBoolOp, values: Vec>, } @@ -477,7 +488,7 @@ pub struct ExprBinOp<'a> { #[derive(Debug, PartialEq, Eq, Hash)] pub struct ExprUnaryOp<'a> { - op: ComparableUnaryop, + op: ComparableUnaryOp, operand: Box>, } @@ -548,7 +559,7 @@ pub struct ExprYieldFrom<'a> { #[derive(Debug, PartialEq, Eq, Hash)] pub struct ExprCompare<'a> { left: Box>, - ops: Vec, + ops: Vec, comparators: Vec>, } @@ -994,14 +1005,14 @@ pub struct StmtIf<'a> { #[derive(Debug, PartialEq, Eq, Hash)] pub struct StmtWith<'a> { - items: Vec>, + items: Vec>, body: Vec>, type_comment: Option<&'a str>, } #[derive(Debug, PartialEq, Eq, Hash)] pub struct StmtAsyncWith<'a> { - items: Vec>, + items: Vec>, body: Vec>, type_comment: Option<&'a str>, } @@ -1021,7 +1032,7 @@ pub struct StmtRaise<'a> { #[derive(Debug, PartialEq, Eq, Hash)] pub struct StmtTry<'a> { body: Vec>, - handlers: Vec>, + handlers: Vec>, orelse: Vec>, finalbody: Vec>, } @@ -1029,7 +1040,7 @@ pub struct StmtTry<'a> { #[derive(Debug, PartialEq, Eq, Hash)] pub struct StmtTryStar<'a> { body: Vec>, - handlers: Vec>, + handlers: Vec>, orelse: Vec>, finalbody: Vec>, } diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index f8ab144d28..d9a64049a8 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -5,9 +5,9 @@ use std::path::Path; use num_traits::Zero; use ruff_text_size::{TextRange, TextSize}; use rustc_hash::{FxHashMap, FxHashSet}; -use rustpython_ast::Cmpop; +use rustpython_ast::CmpOp; use rustpython_parser::ast::{ - self, Arguments, Constant, Excepthandler, Expr, Keyword, MatchCase, Pattern, Ranged, Stmt, + self, Arguments, Constant, ExceptHandler, Expr, Keyword, MatchCase, Pattern, Ranged, Stmt, }; use rustpython_parser::{lexer, Mode, Tok}; use smallvec::SmallVec; @@ -333,25 +333,19 @@ where returns, .. }) => { - args.defaults.iter().any(|expr| any_over_expr(expr, func)) - || args - .kw_defaults - .iter() - .any(|expr| any_over_expr(expr, func)) - || args.args.iter().any(|arg| { - arg.annotation - .as_ref() - .map_or(false, |expr| any_over_expr(expr, func)) - }) - || args.kwonlyargs.iter().any(|arg| { - arg.annotation - .as_ref() - .map_or(false, |expr| any_over_expr(expr, func)) - }) - || args.posonlyargs.iter().any(|arg| { - arg.annotation + args.posonlyargs + .iter() + .chain(args.args.iter().chain(args.kwonlyargs.iter())) + .any(|arg_with_default| { + arg_with_default + .default .as_ref() .map_or(false, |expr| any_over_expr(expr, func)) + || arg_with_default + .def + .annotation + .as_ref() + .map_or(false, |expr| any_over_expr(expr, func)) }) || args.vararg.as_ref().map_or(false, |arg| { arg.annotation @@ -448,9 +442,9 @@ where }) => any_over_expr(test, func) || any_over_body(body, func) || any_over_body(orelse, func), Stmt::With(ast::StmtWith { items, body, .. }) | Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. }) => { - items.iter().any(|withitem| { - any_over_expr(&withitem.context_expr, func) - || withitem + items.iter().any(|with_item| { + any_over_expr(&with_item.context_expr, func) + || with_item .optional_vars .as_ref() .map_or(false, |expr| any_over_expr(expr, func)) @@ -483,7 +477,7 @@ where }) => { any_over_body(body, func) || handlers.iter().any(|handler| { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, body, .. @@ -655,11 +649,11 @@ pub fn has_non_none_keyword(keywords: &[Keyword], keyword: &str) -> bool { } /// Extract the names of all handled exceptions. -pub fn extract_handled_exceptions(handlers: &[Excepthandler]) -> Vec<&Expr> { +pub fn extract_handled_exceptions(handlers: &[ExceptHandler]) -> Vec<&Expr> { let mut handled_exceptions = Vec::new(); for handler in handlers { match handler { - Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { type_, .. }) => { + ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, .. }) => { if let Some(type_) = type_ { if let Expr::Tuple(ast::ExprTuple { elts, .. }) = &type_.as_ref() { for type_ in elts { @@ -678,17 +672,17 @@ pub fn extract_handled_exceptions(handlers: &[Excepthandler]) -> Vec<&Expr> { /// 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 in &arguments.posonlyargs { - arg_names.insert(arg.arg.as_str()); + for arg_with_default in &arguments.posonlyargs { + arg_names.insert(arg_with_default.def.arg.as_str()); } - for arg in &arguments.args { - arg_names.insert(arg.arg.as_str()); + for arg_with_default in &arguments.args { + arg_names.insert(arg_with_default.def.arg.as_str()); } if let Some(arg) = &arguments.vararg { arg_names.insert(arg.arg.as_str()); } - for arg in &arguments.kwonlyargs { - 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 let Some(arg) = &arguments.kwarg { arg_names.insert(arg.arg.as_str()); @@ -1409,13 +1403,13 @@ impl Truthiness { } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct LocatedCmpop { +pub struct LocatedCmpOp { pub range: TextRange, - pub op: Cmpop, + pub op: CmpOp, } -impl LocatedCmpop { - fn new>(range: T, op: Cmpop) -> Self { +impl LocatedCmpOp { + fn new>(range: T, op: CmpOp) -> Self { Self { range: range.into(), op, @@ -1423,13 +1417,13 @@ impl LocatedCmpop { } } -/// Extract all [`Cmpop`] operators from an expression snippet, with appropriate +/// Extract all [`CmpOp`] operators from an expression snippet, with appropriate /// ranges. /// -/// `RustPython` doesn't include line and column information on [`Cmpop`] nodes. +/// `RustPython` doesn't include line and column information on [`CmpOp`] nodes. /// `CPython` doesn't either. This method iterates over the token stream and -/// re-identifies [`Cmpop`] nodes, annotating them with valid ranges. -pub fn locate_cmpops(expr: &Expr, locator: &Locator) -> Vec { +/// re-identifies [`CmpOp`] nodes, annotating them with valid ranges. +pub fn locate_cmp_ops(expr: &Expr, locator: &Locator) -> Vec { // If `Expr` is a multi-line expression, we need to parenthesize it to // ensure that it's lexed correctly. let contents = locator.slice(expr.range()); @@ -1441,7 +1435,7 @@ pub fn locate_cmpops(expr: &Expr, locator: &Locator) -> Vec { .filter(|(tok, _)| !matches!(tok, Tok::NonLogicalNewline | Tok::Comment(_))) .peekable(); - let mut ops: Vec = vec![]; + let mut ops: Vec = vec![]; let mut count = 0u32; loop { let Some((tok, range)) = tok_iter.next() else { @@ -1460,45 +1454,45 @@ pub fn locate_cmpops(expr: &Expr, locator: &Locator) -> Vec { if let Some((_, next_range)) = tok_iter.next_if(|(tok, _)| matches!(tok, Tok::In)) { - ops.push(LocatedCmpop::new( + ops.push(LocatedCmpOp::new( TextRange::new(range.start(), next_range.end()), - Cmpop::NotIn, + CmpOp::NotIn, )); } } Tok::In => { - ops.push(LocatedCmpop::new(range, Cmpop::In)); + ops.push(LocatedCmpOp::new(range, CmpOp::In)); } Tok::Is => { let op = if let Some((_, next_range)) = tok_iter.next_if(|(tok, _)| matches!(tok, Tok::Not)) { - LocatedCmpop::new( + LocatedCmpOp::new( TextRange::new(range.start(), next_range.end()), - Cmpop::IsNot, + CmpOp::IsNot, ) } else { - LocatedCmpop::new(range, Cmpop::Is) + LocatedCmpOp::new(range, CmpOp::Is) }; ops.push(op); } Tok::NotEqual => { - ops.push(LocatedCmpop::new(range, Cmpop::NotEq)); + ops.push(LocatedCmpOp::new(range, CmpOp::NotEq)); } Tok::EqEqual => { - ops.push(LocatedCmpop::new(range, Cmpop::Eq)); + ops.push(LocatedCmpOp::new(range, CmpOp::Eq)); } Tok::GreaterEqual => { - ops.push(LocatedCmpop::new(range, Cmpop::GtE)); + ops.push(LocatedCmpOp::new(range, CmpOp::GtE)); } Tok::Greater => { - ops.push(LocatedCmpop::new(range, Cmpop::Gt)); + ops.push(LocatedCmpOp::new(range, CmpOp::Gt)); } Tok::LessEqual => { - ops.push(LocatedCmpop::new(range, Cmpop::LtE)); + ops.push(LocatedCmpOp::new(range, CmpOp::LtE)); } Tok::Less => { - ops.push(LocatedCmpop::new(range, Cmpop::Lt)); + ops.push(LocatedCmpOp::new(range, CmpOp::Lt)); } _ => {} } @@ -1513,13 +1507,13 @@ mod tests { use anyhow::Result; use ruff_text_size::{TextLen, TextRange, TextSize}; - use rustpython_ast::{Cmpop, Expr, Stmt}; + use rustpython_ast::{CmpOp, Expr, Stmt}; use rustpython_parser::ast::Suite; use rustpython_parser::Parse; use crate::helpers::{ - elif_else_range, first_colon_range, has_trailing_content, locate_cmpops, - resolve_imported_module_path, LocatedCmpop, + elif_else_range, first_colon_range, has_trailing_content, locate_cmp_ops, + resolve_imported_module_path, LocatedCmpOp, }; use crate::source_code::Locator; @@ -1642,15 +1636,15 @@ else: } #[test] - fn extract_cmpop_location() -> Result<()> { + fn extract_cmp_op_location() -> Result<()> { let contents = "x == 1"; let expr = Expr::parse(contents, "")?; let locator = Locator::new(contents); assert_eq!( - locate_cmpops(&expr, &locator), - vec![LocatedCmpop::new( + locate_cmp_ops(&expr, &locator), + vec![LocatedCmpOp::new( TextSize::from(2)..TextSize::from(4), - Cmpop::Eq + CmpOp::Eq )] ); @@ -1658,10 +1652,10 @@ else: let expr = Expr::parse(contents, "")?; let locator = Locator::new(contents); assert_eq!( - locate_cmpops(&expr, &locator), - vec![LocatedCmpop::new( + locate_cmp_ops(&expr, &locator), + vec![LocatedCmpOp::new( TextSize::from(2)..TextSize::from(4), - Cmpop::NotEq + CmpOp::NotEq )] ); @@ -1669,10 +1663,10 @@ else: let expr = Expr::parse(contents, "")?; let locator = Locator::new(contents); assert_eq!( - locate_cmpops(&expr, &locator), - vec![LocatedCmpop::new( + locate_cmp_ops(&expr, &locator), + vec![LocatedCmpOp::new( TextSize::from(2)..TextSize::from(4), - Cmpop::Is + CmpOp::Is )] ); @@ -1680,10 +1674,10 @@ else: let expr = Expr::parse(contents, "")?; let locator = Locator::new(contents); assert_eq!( - locate_cmpops(&expr, &locator), - vec![LocatedCmpop::new( + locate_cmp_ops(&expr, &locator), + vec![LocatedCmpOp::new( TextSize::from(2)..TextSize::from(8), - Cmpop::IsNot + CmpOp::IsNot )] ); @@ -1691,10 +1685,10 @@ else: let expr = Expr::parse(contents, "")?; let locator = Locator::new(contents); assert_eq!( - locate_cmpops(&expr, &locator), - vec![LocatedCmpop::new( + locate_cmp_ops(&expr, &locator), + vec![LocatedCmpOp::new( TextSize::from(2)..TextSize::from(4), - Cmpop::In + CmpOp::In )] ); @@ -1702,10 +1696,10 @@ else: let expr = Expr::parse(contents, "")?; let locator = Locator::new(contents); assert_eq!( - locate_cmpops(&expr, &locator), - vec![LocatedCmpop::new( + locate_cmp_ops(&expr, &locator), + vec![LocatedCmpOp::new( TextSize::from(2)..TextSize::from(8), - Cmpop::NotIn + CmpOp::NotIn )] ); @@ -1713,10 +1707,10 @@ else: let expr = Expr::parse(contents, "")?; let locator = Locator::new(contents); assert_eq!( - locate_cmpops(&expr, &locator), - vec![LocatedCmpop::new( + locate_cmp_ops(&expr, &locator), + vec![LocatedCmpOp::new( TextSize::from(2)..TextSize::from(4), - Cmpop::NotEq + CmpOp::NotEq )] ); diff --git a/crates/ruff_python_ast/src/identifier.rs b/crates/ruff_python_ast/src/identifier.rs index c1cb6c4a32..4babb87717 100644 --- a/crates/ruff_python_ast/src/identifier.rs +++ b/crates/ruff_python_ast/src/identifier.rs @@ -20,8 +20,8 @@ use std::ops::{Add, Sub}; use std::str::Chars; use ruff_text_size::{TextLen, TextRange, TextSize}; -use rustpython_ast::{Alias, Arg, Pattern}; -use rustpython_parser::ast::{self, Excepthandler, Ranged, Stmt}; +use rustpython_ast::{Alias, Arg, ArgWithDefault, Pattern}; +use rustpython_parser::ast::{self, ExceptHandler, Ranged, Stmt}; use ruff_python_whitespace::is_python_whitespace; @@ -94,7 +94,7 @@ impl Identifier for Arg { /// /// For example, return the range of `x` in: /// ```python - /// def f(x: int = 0): + /// def f(x: int): /// ... /// ``` fn identifier(&self, locator: &Locator) -> TextRange { @@ -104,6 +104,19 @@ impl Identifier for Arg { } } +impl Identifier for ArgWithDefault { + /// Return the [`TextRange`] for the identifier defining an [`ArgWithDefault`]. + /// + /// For example, return the range of `x` in: + /// ```python + /// def f(x: int = 0): + /// ... + /// ``` + fn identifier(&self, locator: &Locator) -> TextRange { + self.def.identifier(locator) + } +} + impl Identifier for Alias { /// Return the [`TextRange`] for the identifier defining an [`Alias`]. /// @@ -239,8 +252,8 @@ impl TryIdentifier for Pattern { } } -impl TryIdentifier for Excepthandler { - /// Return the [`TextRange`] of a named exception in an [`Excepthandler`]. +impl TryIdentifier for ExceptHandler { + /// Return the [`TextRange`] of a named exception in an [`ExceptHandler`]. /// /// For example, return the range of `e` in: /// ```python @@ -250,7 +263,7 @@ impl TryIdentifier for Excepthandler { /// ... /// ``` fn try_identifier(&self, locator: &Locator) -> Option { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { type_, name, .. }) = + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, name, .. }) = self; if name.is_none() { @@ -284,11 +297,11 @@ pub fn names<'a>(stmt: &Stmt, locator: &'a Locator<'a>) -> impl Iterator TextRange { +/// Return the [`TextRange`] of the `except` token in an [`ExceptHandler`]. +pub fn except(handler: &ExceptHandler, locator: &Locator) -> TextRange { IdentifierTokenizer::new(locator.contents(), handler.range()) .next() - .expect("Failed to find `except` token in `Excepthandler`") + .expect("Failed to find `except` token in `ExceptHandler`") } /// Return the [`TextRange`] of the `else` token in a `For`, `AsyncFor`, or `While` statement. diff --git a/crates/ruff_python_ast/src/node.rs b/crates/ruff_python_ast/src/node.rs index d60be5b284..b9a6654753 100644 --- a/crates/ruff_python_ast/src/node.rs +++ b/crates/ruff_python_ast/src/node.rs @@ -75,7 +75,7 @@ pub enum AnyNode { ExprList(ExprList), ExprTuple(ExprTuple), ExprSlice(ExprSlice), - ExcepthandlerExceptHandler(ExcepthandlerExceptHandler), + ExceptHandlerExceptHandler(ExceptHandlerExceptHandler), PatternMatchValue(PatternMatchValue), PatternMatchSingleton(PatternMatchSingleton), PatternMatchSequence(PatternMatchSequence), @@ -88,9 +88,10 @@ pub enum AnyNode { Comprehension(Comprehension), Arguments(Arguments), Arg(Arg), + ArgWithDefault(ArgWithDefault), Keyword(Keyword), Alias(Alias), - Withitem(Withitem), + WithItem(WithItem), MatchCase(MatchCase), Decorator(Decorator), } @@ -157,7 +158,7 @@ impl AnyNode { | AnyNode::ExprList(_) | AnyNode::ExprTuple(_) | AnyNode::ExprSlice(_) - | AnyNode::ExcepthandlerExceptHandler(_) + | AnyNode::ExceptHandlerExceptHandler(_) | AnyNode::PatternMatchValue(_) | AnyNode::PatternMatchSingleton(_) | AnyNode::PatternMatchSequence(_) @@ -170,9 +171,10 @@ impl AnyNode { | AnyNode::Comprehension(_) | AnyNode::Arguments(_) | AnyNode::Arg(_) + | AnyNode::ArgWithDefault(_) | AnyNode::Keyword(_) | AnyNode::Alias(_) - | AnyNode::Withitem(_) + | AnyNode::WithItem(_) | AnyNode::MatchCase(_) | AnyNode::Decorator(_) => None, } @@ -239,7 +241,7 @@ impl AnyNode { | AnyNode::StmtPass(_) | AnyNode::StmtBreak(_) | AnyNode::StmtContinue(_) - | AnyNode::ExcepthandlerExceptHandler(_) + | AnyNode::ExceptHandlerExceptHandler(_) | AnyNode::PatternMatchValue(_) | AnyNode::PatternMatchSingleton(_) | AnyNode::PatternMatchSequence(_) @@ -252,9 +254,10 @@ impl AnyNode { | AnyNode::Comprehension(_) | AnyNode::Arguments(_) | AnyNode::Arg(_) + | AnyNode::ArgWithDefault(_) | AnyNode::Keyword(_) | AnyNode::Alias(_) - | AnyNode::Withitem(_) + | AnyNode::WithItem(_) | AnyNode::MatchCase(_) | AnyNode::Decorator(_) => None, } @@ -321,7 +324,7 @@ impl AnyNode { | AnyNode::ExprList(_) | AnyNode::ExprTuple(_) | AnyNode::ExprSlice(_) - | AnyNode::ExcepthandlerExceptHandler(_) + | AnyNode::ExceptHandlerExceptHandler(_) | AnyNode::PatternMatchValue(_) | AnyNode::PatternMatchSingleton(_) | AnyNode::PatternMatchSequence(_) @@ -334,9 +337,10 @@ impl AnyNode { | AnyNode::Comprehension(_) | AnyNode::Arguments(_) | AnyNode::Arg(_) + | AnyNode::ArgWithDefault(_) | AnyNode::Keyword(_) | AnyNode::Alias(_) - | AnyNode::Withitem(_) + | AnyNode::WithItem(_) | AnyNode::MatchCase(_) | AnyNode::Decorator(_) => None, } @@ -411,22 +415,23 @@ impl AnyNode { | AnyNode::ExprList(_) | AnyNode::ExprTuple(_) | AnyNode::ExprSlice(_) - | AnyNode::ExcepthandlerExceptHandler(_) + | AnyNode::ExceptHandlerExceptHandler(_) | AnyNode::TypeIgnoreTypeIgnore(_) | AnyNode::Comprehension(_) | AnyNode::Arguments(_) | AnyNode::Arg(_) + | AnyNode::ArgWithDefault(_) | AnyNode::Keyword(_) | AnyNode::Alias(_) - | AnyNode::Withitem(_) + | AnyNode::WithItem(_) | AnyNode::MatchCase(_) | AnyNode::Decorator(_) => None, } } - pub fn except_handler(self) -> Option { + pub fn except_handler(self) -> Option { match self { - AnyNode::ExcepthandlerExceptHandler(node) => Some(Excepthandler::ExceptHandler(node)), + AnyNode::ExceptHandlerExceptHandler(node) => Some(ExceptHandler::ExceptHandler(node)), AnyNode::ModModule(_) | AnyNode::ModInteractive(_) @@ -498,9 +503,10 @@ impl AnyNode { | AnyNode::Comprehension(_) | AnyNode::Arguments(_) | AnyNode::Arg(_) + | AnyNode::ArgWithDefault(_) | AnyNode::Keyword(_) | AnyNode::Alias(_) - | AnyNode::Withitem(_) + | AnyNode::WithItem(_) | AnyNode::MatchCase(_) | AnyNode::Decorator(_) => None, } @@ -576,13 +582,14 @@ impl AnyNode { | AnyNode::PatternMatchStar(_) | AnyNode::PatternMatchAs(_) | AnyNode::PatternMatchOr(_) - | AnyNode::ExcepthandlerExceptHandler(_) + | AnyNode::ExceptHandlerExceptHandler(_) | AnyNode::Comprehension(_) | AnyNode::Arguments(_) | AnyNode::Arg(_) + | AnyNode::ArgWithDefault(_) | AnyNode::Keyword(_) | AnyNode::Alias(_) - | AnyNode::Withitem(_) + | AnyNode::WithItem(_) | AnyNode::MatchCase(_) | AnyNode::Decorator(_) => None, } @@ -672,7 +679,7 @@ impl AnyNode { Self::ExprList(node) => AnyNodeRef::ExprList(node), Self::ExprTuple(node) => AnyNodeRef::ExprTuple(node), Self::ExprSlice(node) => AnyNodeRef::ExprSlice(node), - Self::ExcepthandlerExceptHandler(node) => AnyNodeRef::ExcepthandlerExceptHandler(node), + Self::ExceptHandlerExceptHandler(node) => AnyNodeRef::ExceptHandlerExceptHandler(node), Self::PatternMatchValue(node) => AnyNodeRef::PatternMatchValue(node), Self::PatternMatchSingleton(node) => AnyNodeRef::PatternMatchSingleton(node), Self::PatternMatchSequence(node) => AnyNodeRef::PatternMatchSequence(node), @@ -685,9 +692,10 @@ impl AnyNode { Self::Comprehension(node) => AnyNodeRef::Comprehension(node), Self::Arguments(node) => AnyNodeRef::Arguments(node), Self::Arg(node) => AnyNodeRef::Arg(node), + Self::ArgWithDefault(node) => AnyNodeRef::ArgWithDefault(node), Self::Keyword(node) => AnyNodeRef::Keyword(node), Self::Alias(node) => AnyNodeRef::Alias(node), - Self::Withitem(node) => AnyNodeRef::Withitem(node), + Self::WithItem(node) => AnyNodeRef::WithItem(node), Self::MatchCase(node) => AnyNodeRef::MatchCase(node), Self::Decorator(node) => AnyNodeRef::Decorator(node), } @@ -2323,12 +2331,12 @@ impl AstNode for ExprSlice { AnyNode::from(self) } } -impl AstNode for ExcepthandlerExceptHandler { +impl AstNode for ExceptHandlerExceptHandler { fn cast(kind: AnyNode) -> Option where Self: Sized, { - if let AnyNode::ExcepthandlerExceptHandler(node) = kind { + if let AnyNode::ExceptHandlerExceptHandler(node) = kind { Some(node) } else { None @@ -2336,7 +2344,7 @@ impl AstNode for ExcepthandlerExceptHandler { } fn cast_ref(kind: AnyNodeRef) -> Option<&Self> { - if let AnyNodeRef::ExcepthandlerExceptHandler(node) = kind { + if let AnyNodeRef::ExceptHandlerExceptHandler(node) = kind { Some(node) } else { None @@ -2688,6 +2696,34 @@ impl AstNode for Arg { AnyNode::from(self) } } +impl AstNode for ArgWithDefault { + fn cast(kind: AnyNode) -> Option + where + Self: Sized, + { + if let AnyNode::ArgWithDefault(node) = kind { + Some(node) + } else { + None + } + } + + fn cast_ref(kind: AnyNodeRef) -> Option<&Self> { + if let AnyNodeRef::ArgWithDefault(node) = kind { + Some(node) + } else { + None + } + } + + fn as_any_node_ref(&self) -> AnyNodeRef { + AnyNodeRef::from(self) + } + + fn into_any_node(self) -> AnyNode { + AnyNode::from(self) + } +} impl AstNode for Keyword { fn cast(kind: AnyNode) -> Option where @@ -2744,12 +2780,12 @@ impl AstNode for Alias { AnyNode::from(self) } } -impl AstNode for Withitem { +impl AstNode for WithItem { fn cast(kind: AnyNode) -> Option where Self: Sized, { - if let AnyNode::Withitem(node) = kind { + if let AnyNode::WithItem(node) = kind { Some(node) } else { None @@ -2757,7 +2793,7 @@ impl AstNode for Withitem { } fn cast_ref(kind: AnyNodeRef) -> Option<&Self> { - if let AnyNodeRef::Withitem(node) = kind { + if let AnyNodeRef::WithItem(node) = kind { Some(node) } else { None @@ -2924,10 +2960,10 @@ impl From for AnyNode { } } -impl From for AnyNode { - fn from(handler: Excepthandler) -> Self { +impl From for AnyNode { + fn from(handler: ExceptHandler) -> Self { match handler { - Excepthandler::ExceptHandler(handler) => AnyNode::ExcepthandlerExceptHandler(handler), + ExceptHandler::ExceptHandler(handler) => AnyNode::ExceptHandlerExceptHandler(handler), } } } @@ -3288,9 +3324,9 @@ impl From for AnyNode { } } -impl From for AnyNode { - fn from(node: ExcepthandlerExceptHandler) -> Self { - AnyNode::ExcepthandlerExceptHandler(node) +impl From for AnyNode { + fn from(node: ExceptHandlerExceptHandler) -> Self { + AnyNode::ExceptHandlerExceptHandler(node) } } @@ -3363,6 +3399,11 @@ impl From for AnyNode { AnyNode::Arg(node) } } +impl From for AnyNode { + fn from(node: ArgWithDefault) -> Self { + AnyNode::ArgWithDefault(node) + } +} impl From for AnyNode { fn from(node: Keyword) -> Self { AnyNode::Keyword(node) @@ -3373,9 +3414,9 @@ impl From for AnyNode { AnyNode::Alias(node) } } -impl From for AnyNode { - fn from(node: Withitem) -> Self { - AnyNode::Withitem(node) +impl From for AnyNode { + fn from(node: WithItem) -> Self { + AnyNode::WithItem(node) } } impl From for AnyNode { @@ -3450,7 +3491,7 @@ impl Ranged for AnyNode { AnyNode::ExprList(node) => node.range(), AnyNode::ExprTuple(node) => node.range(), AnyNode::ExprSlice(node) => node.range(), - AnyNode::ExcepthandlerExceptHandler(node) => node.range(), + AnyNode::ExceptHandlerExceptHandler(node) => node.range(), AnyNode::PatternMatchValue(node) => node.range(), AnyNode::PatternMatchSingleton(node) => node.range(), AnyNode::PatternMatchSequence(node) => node.range(), @@ -3463,9 +3504,10 @@ impl Ranged for AnyNode { AnyNode::Comprehension(node) => node.range(), AnyNode::Arguments(node) => node.range(), AnyNode::Arg(node) => node.range(), + AnyNode::ArgWithDefault(node) => node.range(), AnyNode::Keyword(node) => node.range(), AnyNode::Alias(node) => node.range(), - AnyNode::Withitem(node) => node.range(), + AnyNode::WithItem(node) => node.range(), AnyNode::MatchCase(node) => node.range(), AnyNode::Decorator(node) => node.range(), } @@ -3532,7 +3574,7 @@ pub enum AnyNodeRef<'a> { ExprList(&'a ExprList), ExprTuple(&'a ExprTuple), ExprSlice(&'a ExprSlice), - ExcepthandlerExceptHandler(&'a ExcepthandlerExceptHandler), + ExceptHandlerExceptHandler(&'a ExceptHandlerExceptHandler), PatternMatchValue(&'a PatternMatchValue), PatternMatchSingleton(&'a PatternMatchSingleton), PatternMatchSequence(&'a PatternMatchSequence), @@ -3545,9 +3587,10 @@ pub enum AnyNodeRef<'a> { Comprehension(&'a Comprehension), Arguments(&'a Arguments), Arg(&'a Arg), + ArgWithDefault(&'a ArgWithDefault), Keyword(&'a Keyword), Alias(&'a Alias), - Withitem(&'a Withitem), + WithItem(&'a WithItem), MatchCase(&'a MatchCase), Decorator(&'a Decorator), } @@ -3613,7 +3656,7 @@ impl AnyNodeRef<'_> { AnyNodeRef::ExprList(node) => NonNull::from(*node).cast(), AnyNodeRef::ExprTuple(node) => NonNull::from(*node).cast(), AnyNodeRef::ExprSlice(node) => NonNull::from(*node).cast(), - AnyNodeRef::ExcepthandlerExceptHandler(node) => NonNull::from(*node).cast(), + AnyNodeRef::ExceptHandlerExceptHandler(node) => NonNull::from(*node).cast(), AnyNodeRef::PatternMatchValue(node) => NonNull::from(*node).cast(), AnyNodeRef::PatternMatchSingleton(node) => NonNull::from(*node).cast(), AnyNodeRef::PatternMatchSequence(node) => NonNull::from(*node).cast(), @@ -3626,9 +3669,10 @@ impl AnyNodeRef<'_> { AnyNodeRef::Comprehension(node) => NonNull::from(*node).cast(), AnyNodeRef::Arguments(node) => NonNull::from(*node).cast(), AnyNodeRef::Arg(node) => NonNull::from(*node).cast(), + AnyNodeRef::ArgWithDefault(node) => NonNull::from(*node).cast(), AnyNodeRef::Keyword(node) => NonNull::from(*node).cast(), AnyNodeRef::Alias(node) => NonNull::from(*node).cast(), - AnyNodeRef::Withitem(node) => NonNull::from(*node).cast(), + AnyNodeRef::WithItem(node) => NonNull::from(*node).cast(), AnyNodeRef::MatchCase(node) => NonNull::from(*node).cast(), AnyNodeRef::Decorator(node) => NonNull::from(*node).cast(), } @@ -3700,7 +3744,7 @@ impl AnyNodeRef<'_> { AnyNodeRef::ExprList(_) => NodeKind::ExprList, AnyNodeRef::ExprTuple(_) => NodeKind::ExprTuple, AnyNodeRef::ExprSlice(_) => NodeKind::ExprSlice, - AnyNodeRef::ExcepthandlerExceptHandler(_) => NodeKind::ExcepthandlerExceptHandler, + AnyNodeRef::ExceptHandlerExceptHandler(_) => NodeKind::ExceptHandlerExceptHandler, AnyNodeRef::PatternMatchValue(_) => NodeKind::PatternMatchValue, AnyNodeRef::PatternMatchSingleton(_) => NodeKind::PatternMatchSingleton, AnyNodeRef::PatternMatchSequence(_) => NodeKind::PatternMatchSequence, @@ -3713,9 +3757,10 @@ impl AnyNodeRef<'_> { AnyNodeRef::Comprehension(_) => NodeKind::Comprehension, AnyNodeRef::Arguments(_) => NodeKind::Arguments, AnyNodeRef::Arg(_) => NodeKind::Arg, + AnyNodeRef::ArgWithDefault(_) => NodeKind::ArgWithDefault, AnyNodeRef::Keyword(_) => NodeKind::Keyword, AnyNodeRef::Alias(_) => NodeKind::Alias, - AnyNodeRef::Withitem(_) => NodeKind::Withitem, + AnyNodeRef::WithItem(_) => NodeKind::WithItem, AnyNodeRef::MatchCase(_) => NodeKind::MatchCase, AnyNodeRef::Decorator(_) => NodeKind::Decorator, } @@ -3782,7 +3827,7 @@ impl AnyNodeRef<'_> { | AnyNodeRef::ExprList(_) | AnyNodeRef::ExprTuple(_) | AnyNodeRef::ExprSlice(_) - | AnyNodeRef::ExcepthandlerExceptHandler(_) + | AnyNodeRef::ExceptHandlerExceptHandler(_) | AnyNodeRef::PatternMatchValue(_) | AnyNodeRef::PatternMatchSingleton(_) | AnyNodeRef::PatternMatchSequence(_) @@ -3795,9 +3840,10 @@ impl AnyNodeRef<'_> { | AnyNodeRef::Comprehension(_) | AnyNodeRef::Arguments(_) | AnyNodeRef::Arg(_) + | AnyNodeRef::ArgWithDefault(_) | AnyNodeRef::Keyword(_) | AnyNodeRef::Alias(_) - | AnyNodeRef::Withitem(_) + | AnyNodeRef::WithItem(_) | AnyNodeRef::MatchCase(_) | AnyNodeRef::Decorator(_) => false, } @@ -3864,7 +3910,7 @@ impl AnyNodeRef<'_> { | AnyNodeRef::StmtPass(_) | AnyNodeRef::StmtBreak(_) | AnyNodeRef::StmtContinue(_) - | AnyNodeRef::ExcepthandlerExceptHandler(_) + | AnyNodeRef::ExceptHandlerExceptHandler(_) | AnyNodeRef::PatternMatchValue(_) | AnyNodeRef::PatternMatchSingleton(_) | AnyNodeRef::PatternMatchSequence(_) @@ -3877,9 +3923,10 @@ impl AnyNodeRef<'_> { | AnyNodeRef::Comprehension(_) | AnyNodeRef::Arguments(_) | AnyNodeRef::Arg(_) + | AnyNodeRef::ArgWithDefault(_) | AnyNodeRef::Keyword(_) | AnyNodeRef::Alias(_) - | AnyNodeRef::Withitem(_) + | AnyNodeRef::WithItem(_) | AnyNodeRef::MatchCase(_) | AnyNodeRef::Decorator(_) => false, } @@ -3946,7 +3993,7 @@ impl AnyNodeRef<'_> { | AnyNodeRef::ExprList(_) | AnyNodeRef::ExprTuple(_) | AnyNodeRef::ExprSlice(_) - | AnyNodeRef::ExcepthandlerExceptHandler(_) + | AnyNodeRef::ExceptHandlerExceptHandler(_) | AnyNodeRef::PatternMatchValue(_) | AnyNodeRef::PatternMatchSingleton(_) | AnyNodeRef::PatternMatchSequence(_) @@ -3959,9 +4006,10 @@ impl AnyNodeRef<'_> { | AnyNodeRef::Comprehension(_) | AnyNodeRef::Arguments(_) | AnyNodeRef::Arg(_) + | AnyNodeRef::ArgWithDefault(_) | AnyNodeRef::Keyword(_) | AnyNodeRef::Alias(_) - | AnyNodeRef::Withitem(_) + | AnyNodeRef::WithItem(_) | AnyNodeRef::MatchCase(_) | AnyNodeRef::Decorator(_) => false, } @@ -4036,14 +4084,15 @@ impl AnyNodeRef<'_> { | AnyNodeRef::ExprList(_) | AnyNodeRef::ExprTuple(_) | AnyNodeRef::ExprSlice(_) - | AnyNodeRef::ExcepthandlerExceptHandler(_) + | AnyNodeRef::ExceptHandlerExceptHandler(_) | AnyNodeRef::TypeIgnoreTypeIgnore(_) | AnyNodeRef::Comprehension(_) | AnyNodeRef::Arguments(_) | AnyNodeRef::Arg(_) + | AnyNodeRef::ArgWithDefault(_) | AnyNodeRef::Keyword(_) | AnyNodeRef::Alias(_) - | AnyNodeRef::Withitem(_) + | AnyNodeRef::WithItem(_) | AnyNodeRef::MatchCase(_) | AnyNodeRef::Decorator(_) => false, } @@ -4051,7 +4100,7 @@ impl AnyNodeRef<'_> { pub const fn is_except_handler(self) -> bool { match self { - AnyNodeRef::ExcepthandlerExceptHandler(_) => true, + AnyNodeRef::ExceptHandlerExceptHandler(_) => true, AnyNodeRef::ModModule(_) | AnyNodeRef::ModInteractive(_) @@ -4123,9 +4172,10 @@ impl AnyNodeRef<'_> { | AnyNodeRef::Comprehension(_) | AnyNodeRef::Arguments(_) | AnyNodeRef::Arg(_) + | AnyNodeRef::ArgWithDefault(_) | AnyNodeRef::Keyword(_) | AnyNodeRef::Alias(_) - | AnyNodeRef::Withitem(_) + | AnyNodeRef::WithItem(_) | AnyNodeRef::MatchCase(_) | AnyNodeRef::Decorator(_) => false, } @@ -4201,13 +4251,14 @@ impl AnyNodeRef<'_> { | AnyNodeRef::PatternMatchStar(_) | AnyNodeRef::PatternMatchAs(_) | AnyNodeRef::PatternMatchOr(_) - | AnyNodeRef::ExcepthandlerExceptHandler(_) + | AnyNodeRef::ExceptHandlerExceptHandler(_) | AnyNodeRef::Comprehension(_) | AnyNodeRef::Arguments(_) | AnyNodeRef::Arg(_) + | AnyNodeRef::ArgWithDefault(_) | AnyNodeRef::Keyword(_) | AnyNodeRef::Alias(_) - | AnyNodeRef::Withitem(_) + | AnyNodeRef::WithItem(_) | AnyNodeRef::MatchCase(_) | AnyNodeRef::Decorator(_) => false, } @@ -4578,9 +4629,9 @@ impl<'a> From<&'a ExprSlice> for AnyNodeRef<'a> { } } -impl<'a> From<&'a ExcepthandlerExceptHandler> for AnyNodeRef<'a> { - fn from(node: &'a ExcepthandlerExceptHandler) -> Self { - AnyNodeRef::ExcepthandlerExceptHandler(node) +impl<'a> From<&'a ExceptHandlerExceptHandler> for AnyNodeRef<'a> { + fn from(node: &'a ExceptHandlerExceptHandler) -> Self { + AnyNodeRef::ExceptHandlerExceptHandler(node) } } @@ -4738,11 +4789,11 @@ impl<'a> From<&'a Pattern> for AnyNodeRef<'a> { } } -impl<'a> From<&'a Excepthandler> for AnyNodeRef<'a> { - fn from(handler: &'a Excepthandler) -> Self { +impl<'a> From<&'a ExceptHandler> for AnyNodeRef<'a> { + fn from(handler: &'a ExceptHandler) -> Self { match handler { - Excepthandler::ExceptHandler(handler) => { - AnyNodeRef::ExcepthandlerExceptHandler(handler) + ExceptHandler::ExceptHandler(handler) => { + AnyNodeRef::ExceptHandlerExceptHandler(handler) } } } @@ -4771,6 +4822,11 @@ impl<'a> From<&'a Arg> for AnyNodeRef<'a> { AnyNodeRef::Arg(node) } } +impl<'a> From<&'a ArgWithDefault> for AnyNodeRef<'a> { + fn from(node: &'a ArgWithDefault) -> Self { + AnyNodeRef::ArgWithDefault(node) + } +} impl<'a> From<&'a Keyword> for AnyNodeRef<'a> { fn from(node: &'a Keyword) -> Self { AnyNodeRef::Keyword(node) @@ -4781,9 +4837,9 @@ impl<'a> From<&'a Alias> for AnyNodeRef<'a> { AnyNodeRef::Alias(node) } } -impl<'a> From<&'a Withitem> for AnyNodeRef<'a> { - fn from(node: &'a Withitem) -> Self { - AnyNodeRef::Withitem(node) +impl<'a> From<&'a WithItem> for AnyNodeRef<'a> { + fn from(node: &'a WithItem) -> Self { + AnyNodeRef::WithItem(node) } } impl<'a> From<&'a MatchCase> for AnyNodeRef<'a> { @@ -4853,7 +4909,7 @@ impl Ranged for AnyNodeRef<'_> { AnyNodeRef::ExprList(node) => node.range(), AnyNodeRef::ExprTuple(node) => node.range(), AnyNodeRef::ExprSlice(node) => node.range(), - AnyNodeRef::ExcepthandlerExceptHandler(node) => node.range(), + AnyNodeRef::ExceptHandlerExceptHandler(node) => node.range(), AnyNodeRef::PatternMatchValue(node) => node.range(), AnyNodeRef::PatternMatchSingleton(node) => node.range(), AnyNodeRef::PatternMatchSequence(node) => node.range(), @@ -4866,9 +4922,10 @@ impl Ranged for AnyNodeRef<'_> { AnyNodeRef::Comprehension(node) => node.range(), AnyNodeRef::Arguments(node) => node.range(), AnyNodeRef::Arg(node) => node.range(), + AnyNodeRef::ArgWithDefault(node) => node.range(), AnyNodeRef::Keyword(node) => node.range(), AnyNodeRef::Alias(node) => node.range(), - AnyNodeRef::Withitem(node) => node.range(), + AnyNodeRef::WithItem(node) => node.range(), AnyNodeRef::MatchCase(node) => node.range(), AnyNodeRef::Decorator(node) => node.range(), } @@ -4935,7 +4992,7 @@ pub enum NodeKind { ExprList, ExprTuple, ExprSlice, - ExcepthandlerExceptHandler, + ExceptHandlerExceptHandler, PatternMatchValue, PatternMatchSingleton, PatternMatchSequence, @@ -4948,9 +5005,10 @@ pub enum NodeKind { Comprehension, Arguments, Arg, + ArgWithDefault, Keyword, Alias, - Withitem, + WithItem, MatchCase, Decorator, } diff --git a/crates/ruff_python_ast/src/source_code/generator.rs b/crates/ruff_python_ast/src/source_code/generator.rs index 7f7e64047c..08d552f8ec 100644 --- a/crates/ruff_python_ast/src/source_code/generator.rs +++ b/crates/ruff_python_ast/src/source_code/generator.rs @@ -1,11 +1,12 @@ //! Generate Python source code from an abstract syntax tree (AST). +use rustpython_ast::ArgWithDefault; use std::ops::Deref; use rustpython_literal::escape::{AsciiEscape, Escape, UnicodeEscape}; use rustpython_parser::ast::{ - self, Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, ConversionFlag, - Excepthandler, Expr, Identifier, MatchCase, Operator, Pattern, Stmt, Suite, Withitem, + self, Alias, Arg, Arguments, BoolOp, CmpOp, Comprehension, Constant, ConversionFlag, + ExceptHandler, Expr, Identifier, MatchCase, Operator, Pattern, Stmt, Suite, WithItem, }; use ruff_python_whitespace::LineEnding; @@ -501,7 +502,7 @@ impl<'a> Generator<'a> { let mut first = true; for item in items { self.p_delim(&mut first, ", "); - self.unparse_withitem(item); + self.unparse_with_item(item); } self.p(":"); }); @@ -513,7 +514,7 @@ impl<'a> Generator<'a> { let mut first = true; for item in items { self.p_delim(&mut first, ", "); - self.unparse_withitem(item); + self.unparse_with_item(item); } self.p(":"); }); @@ -568,7 +569,7 @@ impl<'a> Generator<'a> { for handler in handlers { statement!({ - self.unparse_excepthandler(handler, false); + self.unparse_except_handler(handler, false); }); } @@ -599,7 +600,7 @@ impl<'a> Generator<'a> { for handler in handlers { statement!({ - self.unparse_excepthandler(handler, true); + self.unparse_except_handler(handler, true); }); } @@ -717,9 +718,9 @@ impl<'a> Generator<'a> { } } - fn unparse_excepthandler(&mut self, ast: &Excepthandler, star: bool) { + fn unparse_except_handler(&mut self, ast: &ExceptHandler, star: bool) { match ast { - Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { + ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, name, body, @@ -870,7 +871,7 @@ impl<'a> Generator<'a> { values, range: _range, }) => { - let (op, prec) = opprec!(bin, op, Boolop, And("and", AND), Or("or", OR)); + let (op, prec) = opprec!(bin, op, BoolOp, And("and", AND), Or("or", OR)); group_if!(prec, { let mut first = true; for val in values { @@ -929,7 +930,7 @@ impl<'a> Generator<'a> { let (op, prec) = opprec!( un, op, - rustpython_parser::ast::Unaryop, + rustpython_parser::ast::UnaryOp, Invert("~", INVERT), Not("not ", NOT), UAdd("+", UADD), @@ -1087,16 +1088,16 @@ impl<'a> Generator<'a> { self.unparse_expr(left, new_lvl); for (op, cmp) in ops.iter().zip(comparators) { let op = match op { - Cmpop::Eq => " == ", - Cmpop::NotEq => " != ", - Cmpop::Lt => " < ", - Cmpop::LtE => " <= ", - Cmpop::Gt => " > ", - Cmpop::GtE => " >= ", - Cmpop::Is => " is ", - Cmpop::IsNot => " is not ", - Cmpop::In => " in ", - Cmpop::NotIn => " not in ", + CmpOp::Eq => " == ", + CmpOp::NotEq => " != ", + CmpOp::Lt => " < ", + CmpOp::LtE => " <= ", + CmpOp::Gt => " > ", + CmpOp::GtE => " >= ", + CmpOp::Is => " is ", + CmpOp::IsNot => " is not ", + CmpOp::In => " in ", + CmpOp::NotIn => " not in ", }; self.p(op); self.unparse_expr(cmp, new_lvl); @@ -1290,14 +1291,9 @@ impl<'a> Generator<'a> { fn unparse_args(&mut self, args: &Arguments) { let mut first = true; - let defaults_start = args.posonlyargs.len() + args.args.len() - args.defaults.len(); - for (i, arg) in args.posonlyargs.iter().chain(&args.args).enumerate() { + for (i, arg_with_default) in args.posonlyargs.iter().chain(&args.args).enumerate() { self.p_delim(&mut first, ", "); - self.unparse_arg(arg); - if let Some(i) = i.checked_sub(defaults_start) { - self.p("="); - self.unparse_expr(&args.defaults[i], precedence::COMMA); - } + self.unparse_arg_with_default(arg_with_default); self.p_if(i + 1 == args.posonlyargs.len(), ", /"); } if args.vararg.is_some() || !args.kwonlyargs.is_empty() { @@ -1307,17 +1303,9 @@ impl<'a> Generator<'a> { if let Some(vararg) = &args.vararg { self.unparse_arg(vararg); } - let defaults_start = args.kwonlyargs.len() - args.kw_defaults.len(); - for (i, kwarg) in args.kwonlyargs.iter().enumerate() { + for kwarg in &args.kwonlyargs { self.p_delim(&mut first, ", "); - self.unparse_arg(kwarg); - if let Some(default) = i - .checked_sub(defaults_start) - .and_then(|i| args.kw_defaults.get(i)) - { - self.p("="); - self.unparse_expr(default, precedence::COMMA); - } + self.unparse_arg_with_default(kwarg); } if let Some(kwarg) = &args.kwarg { self.p_delim(&mut first, ", "); @@ -1334,6 +1322,14 @@ impl<'a> Generator<'a> { } } + fn unparse_arg_with_default(&mut self, arg_with_default: &ArgWithDefault) { + self.unparse_arg(&arg_with_default.def); + if let Some(default) = &arg_with_default.default { + self.p("="); + self.unparse_expr(default, precedence::COMMA); + } + } + fn unparse_comp(&mut self, generators: &[Comprehension]) { for comp in generators { self.p(if comp.is_async { @@ -1445,9 +1441,9 @@ impl<'a> Generator<'a> { } } - fn unparse_withitem(&mut self, withitem: &Withitem) { - self.unparse_expr(&withitem.context_expr, precedence::MAX); - if let Some(optional_vars) = &withitem.optional_vars { + fn unparse_with_item(&mut self, with_item: &WithItem) { + self.unparse_expr(&with_item.context_expr, precedence::MAX); + if let Some(optional_vars) = &with_item.optional_vars { self.p(" as "); self.unparse_expr(optional_vars, precedence::MAX); } diff --git a/crates/ruff_python_ast/src/statement_visitor.rs b/crates/ruff_python_ast/src/statement_visitor.rs index 805da33b42..df35b6bb2e 100644 --- a/crates/ruff_python_ast/src/statement_visitor.rs +++ b/crates/ruff_python_ast/src/statement_visitor.rs @@ -1,6 +1,6 @@ //! Specialized AST visitor trait and walk functions that only visit statements. -use rustpython_parser::ast::{self, Excepthandler, MatchCase, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, MatchCase, Stmt}; /// A trait for AST visitors that only need to visit statements. pub trait StatementVisitor<'a> { @@ -10,8 +10,8 @@ pub trait StatementVisitor<'a> { fn visit_stmt(&mut self, stmt: &'a Stmt) { walk_stmt(self, stmt); } - fn visit_excepthandler(&mut self, excepthandler: &'a Excepthandler) { - walk_excepthandler(self, excepthandler); + fn visit_except_handler(&mut self, except_handler: &'a ExceptHandler) { + walk_except_handler(self, except_handler); } fn visit_match_case(&mut self, match_case: &'a MatchCase) { walk_match_case(self, match_case); @@ -70,8 +70,8 @@ pub fn walk_stmt<'a, V: StatementVisitor<'a> + ?Sized>(visitor: &mut V, stmt: &' range: _range, }) => { visitor.visit_body(body); - for excepthandler in handlers { - visitor.visit_excepthandler(excepthandler); + for except_handler in handlers { + visitor.visit_except_handler(except_handler); } visitor.visit_body(orelse); visitor.visit_body(finalbody); @@ -84,8 +84,8 @@ pub fn walk_stmt<'a, V: StatementVisitor<'a> + ?Sized>(visitor: &mut V, stmt: &' range: _range, }) => { visitor.visit_body(body); - for excepthandler in handlers { - visitor.visit_excepthandler(excepthandler); + for except_handler in handlers { + visitor.visit_except_handler(except_handler); } visitor.visit_body(orelse); visitor.visit_body(finalbody); @@ -94,12 +94,12 @@ pub fn walk_stmt<'a, V: StatementVisitor<'a> + ?Sized>(visitor: &mut V, stmt: &' } } -pub fn walk_excepthandler<'a, V: StatementVisitor<'a> + ?Sized>( +pub fn walk_except_handler<'a, V: StatementVisitor<'a> + ?Sized>( visitor: &mut V, - excepthandler: &'a Excepthandler, + except_handler: &'a ExceptHandler, ) { - match excepthandler { - Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { body, .. }) => { + match except_handler { + ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) => { visitor.visit_body(body); } } diff --git a/crates/ruff_python_ast/src/visitor.rs b/crates/ruff_python_ast/src/visitor.rs index 2864df5acb..e9f506fc8a 100644 --- a/crates/ruff_python_ast/src/visitor.rs +++ b/crates/ruff_python_ast/src/visitor.rs @@ -2,10 +2,10 @@ pub mod preorder; -use rustpython_ast::Decorator; +use rustpython_ast::{ArgWithDefault, Decorator}; use rustpython_parser::ast::{ - self, Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, Excepthandler, Expr, - ExprContext, Keyword, MatchCase, Operator, Pattern, Stmt, Unaryop, Withitem, + self, Alias, Arg, Arguments, BoolOp, CmpOp, Comprehension, Constant, ExceptHandler, Expr, + ExprContext, Keyword, MatchCase, Operator, Pattern, Stmt, UnaryOp, WithItem, }; /// A trait for AST visitors. Visits all nodes in the AST recursively in evaluation-order. @@ -34,23 +34,23 @@ pub trait Visitor<'a> { fn visit_expr_context(&mut self, expr_context: &'a ExprContext) { walk_expr_context(self, expr_context); } - fn visit_boolop(&mut self, boolop: &'a Boolop) { - walk_boolop(self, boolop); + fn visit_bool_op(&mut self, bool_op: &'a BoolOp) { + walk_bool_op(self, bool_op); } fn visit_operator(&mut self, operator: &'a Operator) { walk_operator(self, operator); } - fn visit_unaryop(&mut self, unaryop: &'a Unaryop) { - walk_unaryop(self, unaryop); + fn visit_unary_op(&mut self, unary_op: &'a UnaryOp) { + walk_unary_op(self, unary_op); } - fn visit_cmpop(&mut self, cmpop: &'a Cmpop) { - walk_cmpop(self, cmpop); + fn visit_cmp_op(&mut self, cmp_op: &'a CmpOp) { + walk_cmp_op(self, cmp_op); } fn visit_comprehension(&mut self, comprehension: &'a Comprehension) { walk_comprehension(self, comprehension); } - fn visit_excepthandler(&mut self, excepthandler: &'a Excepthandler) { - walk_excepthandler(self, excepthandler); + fn visit_except_handler(&mut self, except_handler: &'a ExceptHandler) { + walk_except_handler(self, except_handler); } fn visit_format_spec(&mut self, format_spec: &'a Expr) { walk_expr(self, format_spec); @@ -61,14 +61,17 @@ pub trait Visitor<'a> { fn visit_arg(&mut self, arg: &'a Arg) { walk_arg(self, arg); } + fn visit_arg_with_default(&mut self, arg_with_default: &'a ArgWithDefault) { + walk_arg_with_default(self, arg_with_default); + } fn visit_keyword(&mut self, keyword: &'a Keyword) { walk_keyword(self, keyword); } fn visit_alias(&mut self, alias: &'a Alias) { walk_alias(self, alias); } - fn visit_withitem(&mut self, withitem: &'a Withitem) { - walk_withitem(self, withitem); + fn visit_with_item(&mut self, with_item: &'a WithItem) { + walk_with_item(self, with_item); } fn visit_match_case(&mut self, match_case: &'a MatchCase) { walk_match_case(self, match_case); @@ -228,14 +231,14 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { visitor.visit_body(orelse); } Stmt::With(ast::StmtWith { items, body, .. }) => { - for withitem in items { - visitor.visit_withitem(withitem); + for with_item in items { + visitor.visit_with_item(with_item); } visitor.visit_body(body); } Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. }) => { - for withitem in items { - visitor.visit_withitem(withitem); + for with_item in items { + visitor.visit_with_item(with_item); } visitor.visit_body(body); } @@ -269,8 +272,8 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { range: _range, }) => { visitor.visit_body(body); - for excepthandler in handlers { - visitor.visit_excepthandler(excepthandler); + for except_handler in handlers { + visitor.visit_except_handler(except_handler); } visitor.visit_body(orelse); visitor.visit_body(finalbody); @@ -283,8 +286,8 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { range: _range, }) => { visitor.visit_body(body); - for excepthandler in handlers { - visitor.visit_excepthandler(excepthandler); + for except_handler in handlers { + visitor.visit_except_handler(except_handler); } visitor.visit_body(orelse); visitor.visit_body(finalbody); @@ -333,7 +336,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { values, range: _range, }) => { - visitor.visit_boolop(op); + visitor.visit_bool_op(op); for expr in values { visitor.visit_expr(expr); } @@ -361,7 +364,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { operand, range: _range, }) => { - visitor.visit_unaryop(op); + visitor.visit_unary_op(op); visitor.visit_expr(operand); } Expr::Lambda(ast::ExprLambda { @@ -467,8 +470,8 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { range: _range, }) => { visitor.visit_expr(left); - for cmpop in ops { - visitor.visit_cmpop(cmpop); + for cmp_op in ops { + visitor.visit_cmp_op(cmp_op); } for expr in comparators { visitor.visit_expr(expr); @@ -588,12 +591,12 @@ pub fn walk_comprehension<'a, V: Visitor<'a> + ?Sized>( } } -pub fn walk_excepthandler<'a, V: Visitor<'a> + ?Sized>( +pub fn walk_except_handler<'a, V: Visitor<'a> + ?Sized>( visitor: &mut V, - excepthandler: &'a Excepthandler, + except_handler: &'a ExceptHandler, ) { - match excepthandler { - Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { type_, body, .. }) => { + match except_handler { + ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, body, .. }) => { if let Some(expr) = type_ { visitor.visit_expr(expr); } @@ -604,26 +607,20 @@ pub fn walk_excepthandler<'a, V: Visitor<'a> + ?Sized>( pub fn walk_arguments<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arguments: &'a Arguments) { for arg in &arguments.posonlyargs { - visitor.visit_arg(arg); + visitor.visit_arg_with_default(arg); } for arg in &arguments.args { - visitor.visit_arg(arg); + visitor.visit_arg_with_default(arg); } if let Some(arg) = &arguments.vararg { visitor.visit_arg(arg); } for arg in &arguments.kwonlyargs { - visitor.visit_arg(arg); - } - for expr in &arguments.kw_defaults { - visitor.visit_expr(expr); + visitor.visit_arg_with_default(arg); } if let Some(arg) = &arguments.kwarg { visitor.visit_arg(arg); } - for expr in &arguments.defaults { - visitor.visit_expr(expr); - } } pub fn walk_arg<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arg: &'a Arg) { @@ -632,13 +629,23 @@ pub fn walk_arg<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arg: &'a Arg) { } } +pub fn walk_arg_with_default<'a, V: Visitor<'a> + ?Sized>( + visitor: &mut V, + arg_with_default: &'a ArgWithDefault, +) { + visitor.visit_arg(&arg_with_default.def); + if let Some(expr) = &arg_with_default.default { + visitor.visit_expr(expr); + } +} + pub fn walk_keyword<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, keyword: &'a Keyword) { visitor.visit_expr(&keyword.value); } -pub fn walk_withitem<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, withitem: &'a Withitem) { - visitor.visit_expr(&withitem.context_expr); - if let Some(expr) = &withitem.optional_vars { +pub fn walk_with_item<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, with_item: &'a WithItem) { + visitor.visit_expr(&with_item.context_expr); + if let Some(expr) = &with_item.optional_vars { visitor.visit_expr(expr); } } @@ -719,16 +726,16 @@ pub fn walk_expr_context<'a, V: Visitor<'a> + ?Sized>( } #[allow(unused_variables)] -pub fn walk_boolop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, boolop: &'a Boolop) {} +pub fn walk_bool_op<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, bool_op: &'a BoolOp) {} #[allow(unused_variables)] pub fn walk_operator<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, operator: &'a Operator) {} #[allow(unused_variables)] -pub fn walk_unaryop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, unaryop: &'a Unaryop) {} +pub fn walk_unary_op<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, unary_op: &'a UnaryOp) {} #[allow(unused_variables)] -pub fn walk_cmpop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, cmpop: &'a Cmpop) {} +pub fn walk_cmp_op<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, cmp_op: &'a CmpOp) {} #[allow(unused_variables)] pub fn walk_alias<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, alias: &'a Alias) {} diff --git a/crates/ruff_python_ast/src/visitor/preorder.rs b/crates/ruff_python_ast/src/visitor/preorder.rs index 248070ec4d..544a0c8402 100644 --- a/crates/ruff_python_ast/src/visitor/preorder.rs +++ b/crates/ruff_python_ast/src/visitor/preorder.rs @@ -26,28 +26,28 @@ pub trait PreorderVisitor<'a> { walk_constant(self, constant); } - fn visit_boolop(&mut self, boolop: &'a Boolop) { - walk_boolop(self, boolop); + fn visit_bool_op(&mut self, bool_op: &'a BoolOp) { + walk_bool_op(self, bool_op); } fn visit_operator(&mut self, operator: &'a Operator) { walk_operator(self, operator); } - fn visit_unaryop(&mut self, unaryop: &'a Unaryop) { - walk_unaryop(self, unaryop); + fn visit_unary_op(&mut self, unary_op: &'a UnaryOp) { + walk_unary_op(self, unary_op); } - fn visit_cmpop(&mut self, cmpop: &'a Cmpop) { - walk_cmpop(self, cmpop); + fn visit_cmp_op(&mut self, cmp_op: &'a CmpOp) { + walk_cmp_op(self, cmp_op); } fn visit_comprehension(&mut self, comprehension: &'a Comprehension) { walk_comprehension(self, comprehension); } - fn visit_excepthandler(&mut self, excepthandler: &'a Excepthandler) { - walk_excepthandler(self, excepthandler); + fn visit_except_handler(&mut self, except_handler: &'a ExceptHandler) { + walk_except_handler(self, except_handler); } fn visit_format_spec(&mut self, format_spec: &'a Expr) { @@ -62,6 +62,10 @@ pub trait PreorderVisitor<'a> { walk_arg(self, arg); } + fn visit_arg_with_default(&mut self, arg_with_default: &'a ArgWithDefault) { + walk_arg_with_default(self, arg_with_default); + } + fn visit_keyword(&mut self, keyword: &'a Keyword) { walk_keyword(self, keyword); } @@ -70,8 +74,8 @@ pub trait PreorderVisitor<'a> { walk_alias(self, alias); } - fn visit_withitem(&mut self, withitem: &'a Withitem) { - walk_withitem(self, withitem); + fn visit_with_item(&mut self, with_item: &'a WithItem) { + walk_with_item(self, with_item); } fn visit_match_case(&mut self, match_case: &'a MatchCase) { @@ -300,8 +304,8 @@ where type_comment: _, range: _, }) => { - for withitem in items { - visitor.visit_withitem(withitem); + for with_item in items { + visitor.visit_with_item(with_item); } visitor.visit_body(body); } @@ -345,8 +349,8 @@ where range: _range, }) => { visitor.visit_body(body); - for excepthandler in handlers { - visitor.visit_excepthandler(excepthandler); + for except_handler in handlers { + visitor.visit_except_handler(except_handler); } visitor.visit_body(orelse); visitor.visit_body(finalbody); @@ -410,13 +414,13 @@ where }) => match values.as_slice() { [left, rest @ ..] => { visitor.visit_expr(left); - visitor.visit_boolop(op); + visitor.visit_bool_op(op); for expr in rest { visitor.visit_expr(expr); } } [] => { - visitor.visit_boolop(op); + visitor.visit_bool_op(op); } }, @@ -445,7 +449,7 @@ where operand, range: _range, }) => { - visitor.visit_unaryop(op); + visitor.visit_unary_op(op); visitor.visit_expr(operand); } @@ -565,7 +569,7 @@ where visitor.visit_expr(left); for (op, comparator) in ops.iter().zip(comparators) { - visitor.visit_cmpop(op); + visitor.visit_cmp_op(op); visitor.visit_expr(comparator); } } @@ -703,12 +707,12 @@ where } } -pub fn walk_excepthandler<'a, V>(visitor: &mut V, excepthandler: &'a Excepthandler) +pub fn walk_except_handler<'a, V>(visitor: &mut V, except_handler: &'a ExceptHandler) where V: PreorderVisitor<'a> + ?Sized, { - match excepthandler { - Excepthandler::ExceptHandler(ExcepthandlerExceptHandler { + match except_handler { + ExceptHandler::ExceptHandler(ExceptHandlerExceptHandler { range: _, type_, name: _, @@ -726,34 +730,16 @@ pub fn walk_arguments<'a, V>(visitor: &mut V, arguments: &'a Arguments) where V: PreorderVisitor<'a> + ?Sized, { - let non_default_args_len = - arguments.posonlyargs.len() + arguments.args.len() - arguments.defaults.len(); - - let mut args_iter = arguments.posonlyargs.iter().chain(&arguments.args); - - for _ in 0..non_default_args_len { - visitor.visit_arg(args_iter.next().unwrap()); - } - - for (arg, default) in args_iter.zip(&arguments.defaults) { - visitor.visit_arg(arg); - visitor.visit_expr(default); + for arg in arguments.posonlyargs.iter().chain(&arguments.args) { + visitor.visit_arg_with_default(arg); } if let Some(arg) = &arguments.vararg { visitor.visit_arg(arg); } - let non_default_kwargs_len = arguments.kwonlyargs.len() - arguments.kw_defaults.len(); - let mut kwargsonly_iter = arguments.kwonlyargs.iter(); - - for _ in 0..non_default_kwargs_len { - visitor.visit_arg(kwargsonly_iter.next().unwrap()); - } - - for (arg, default) in kwargsonly_iter.zip(&arguments.kw_defaults) { - visitor.visit_arg(arg); - visitor.visit_expr(default); + for arg in &arguments.kwonlyargs { + visitor.visit_arg_with_default(arg); } if let Some(arg) = &arguments.kwarg { @@ -770,6 +756,16 @@ where } } +pub fn walk_arg_with_default<'a, V>(visitor: &mut V, arg_with_default: &'a ArgWithDefault) +where + V: PreorderVisitor<'a> + ?Sized, +{ + visitor.visit_arg(&arg_with_default.def); + if let Some(expr) = &arg_with_default.default { + visitor.visit_expr(expr); + } +} + #[inline] pub fn walk_keyword<'a, V>(visitor: &mut V, keyword: &'a Keyword) where @@ -778,13 +774,13 @@ where visitor.visit_expr(&keyword.value); } -pub fn walk_withitem<'a, V>(visitor: &mut V, withitem: &'a Withitem) +pub fn walk_with_item<'a, V>(visitor: &mut V, with_item: &'a WithItem) where V: PreorderVisitor<'a> + ?Sized, { - visitor.visit_expr(&withitem.context_expr); + visitor.visit_expr(&with_item.context_expr); - if let Some(expr) = &withitem.optional_vars { + if let Some(expr) = &with_item.optional_vars { visitor.visit_expr(expr); } } @@ -885,7 +881,7 @@ where { } -pub fn walk_boolop<'a, V>(_visitor: &mut V, _boolop: &'a Boolop) +pub fn walk_bool_op<'a, V>(_visitor: &mut V, _bool_op: &'a BoolOp) where V: PreorderVisitor<'a> + ?Sized, { @@ -899,14 +895,14 @@ where } #[inline] -pub fn walk_unaryop<'a, V>(_visitor: &mut V, _unaryop: &'a Unaryop) +pub fn walk_unary_op<'a, V>(_visitor: &mut V, _unary_op: &'a UnaryOp) where V: PreorderVisitor<'a> + ?Sized, { } #[inline] -pub fn walk_cmpop<'a, V>(_visitor: &mut V, _cmpop: &'a Cmpop) +pub fn walk_cmp_op<'a, V>(_visitor: &mut V, _cmp_op: &'a CmpOp) where V: PreorderVisitor<'a> + ?Sized, { @@ -923,11 +919,11 @@ where mod tests { use crate::node::AnyNodeRef; use crate::visitor::preorder::{ - walk_alias, walk_arg, walk_arguments, walk_comprehension, walk_excepthandler, walk_expr, + walk_alias, walk_arg, walk_arguments, walk_comprehension, walk_except_handler, walk_expr, walk_keyword, walk_match_case, walk_module, walk_pattern, walk_stmt, walk_type_ignore, - walk_withitem, Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, - Excepthandler, Expr, Keyword, MatchCase, Mod, Operator, Pattern, PreorderVisitor, Stmt, - String, TypeIgnore, Unaryop, Withitem, + walk_with_item, Alias, Arg, Arguments, BoolOp, CmpOp, Comprehension, Constant, + ExceptHandler, Expr, Keyword, MatchCase, Mod, Operator, Pattern, PreorderVisitor, Stmt, + String, TypeIgnore, UnaryOp, WithItem, }; use insta::assert_snapshot; use rustpython_parser::lexer::lex; @@ -1089,20 +1085,20 @@ class A: self.emit(&constant); } - fn visit_boolop(&mut self, boolop: &Boolop) { - self.emit(&boolop); + fn visit_bool_op(&mut self, bool_op: &BoolOp) { + self.emit(&bool_op); } fn visit_operator(&mut self, operator: &Operator) { self.emit(&operator); } - fn visit_unaryop(&mut self, unaryop: &Unaryop) { - self.emit(&unaryop); + fn visit_unary_op(&mut self, unary_op: &UnaryOp) { + self.emit(&unary_op); } - fn visit_cmpop(&mut self, cmpop: &Cmpop) { - self.emit(&cmpop); + fn visit_cmp_op(&mut self, cmp_op: &CmpOp) { + self.emit(&cmp_op); } fn visit_comprehension(&mut self, comprehension: &Comprehension) { @@ -1111,9 +1107,9 @@ class A: self.exit_node(); } - fn visit_excepthandler(&mut self, excepthandler: &Excepthandler) { - self.enter_node(excepthandler); - walk_excepthandler(self, excepthandler); + fn visit_except_handler(&mut self, except_handler: &ExceptHandler) { + self.enter_node(except_handler); + walk_except_handler(self, except_handler); self.exit_node(); } @@ -1147,9 +1143,9 @@ class A: self.exit_node(); } - fn visit_withitem(&mut self, withitem: &Withitem) { - self.enter_node(withitem); - walk_withitem(self, withitem); + fn visit_with_item(&mut self, with_item: &WithItem) { + self.enter_node(with_item); + walk_with_item(self, with_item); self.exit_node(); } diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 3c1166b1f5..2bfe71690a 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -14,21 +14,21 @@ pub(super) fn place_comment<'a>( comment: DecoratedComment<'a>, locator: &Locator, ) -> CommentPlacement<'a> { - handle_in_between_excepthandlers_or_except_handler_and_else_or_finally_comment(comment, locator) - .or_else(|comment| handle_match_comment(comment, locator)) - .or_else(|comment| handle_in_between_bodies_own_line_comment(comment, locator)) - .or_else(|comment| handle_in_between_bodies_end_of_line_comment(comment, locator)) - .or_else(|comment| handle_trailing_body_comment(comment, locator)) - .or_else(handle_trailing_end_of_line_body_comment) - .or_else(|comment| handle_trailing_end_of_line_condition_comment(comment, locator)) - .or_else(|comment| { - handle_module_level_own_line_comment_before_class_or_function_comment(comment, locator) - }) - .or_else(|comment| handle_positional_only_arguments_separator_comment(comment, locator)) - .or_else(|comment| { - handle_trailing_binary_expression_left_or_operator_comment(comment, locator) - }) - .or_else(handle_leading_function_with_decorators_comment) + handle_in_between_except_handlers_or_except_handler_and_else_or_finally_comment( + comment, locator, + ) + .or_else(|comment| handle_match_comment(comment, locator)) + .or_else(|comment| handle_in_between_bodies_own_line_comment(comment, locator)) + .or_else(|comment| handle_in_between_bodies_end_of_line_comment(comment, locator)) + .or_else(|comment| handle_trailing_body_comment(comment, locator)) + .or_else(handle_trailing_end_of_line_body_comment) + .or_else(|comment| handle_trailing_end_of_line_condition_comment(comment, locator)) + .or_else(|comment| { + handle_module_level_own_line_comment_before_class_or_function_comment(comment, locator) + }) + .or_else(|comment| handle_positional_only_arguments_separator_comment(comment, locator)) + .or_else(|comment| handle_trailing_binary_expression_left_or_operator_comment(comment, locator)) + .or_else(handle_leading_function_with_decorators_comment) } /// Handles leading comments in front of a match case or a trailing comment of the `match` statement. @@ -138,8 +138,8 @@ fn handle_match_comment<'a>( } } -/// Handles comments between excepthandlers and between the last except handler and any following `else` or `finally` block. -fn handle_in_between_excepthandlers_or_except_handler_and_else_or_finally_comment<'a>( +/// Handles comments between except handlers and between the last except handler and any following `else` or `finally` block. +fn handle_in_between_except_handlers_or_except_handler_and_else_or_finally_comment<'a>( comment: DecoratedComment<'a>, locator: &Locator, ) -> CommentPlacement<'a> { @@ -147,7 +147,7 @@ fn handle_in_between_excepthandlers_or_except_handler_and_else_or_finally_commen return CommentPlacement::Default(comment); } - if let Some(AnyNodeRef::ExcepthandlerExceptHandler(except_handler)) = comment.preceding_node() { + if let Some(AnyNodeRef::ExceptHandlerExceptHandler(except_handler)) = comment.preceding_node() { // it now depends on the indentation level of the comment if it is a leading comment for e.g. // the following `elif` or indeed a trailing comment of the previous body's last statement. let comment_indentation = @@ -627,11 +627,8 @@ fn handle_positional_only_arguments_separator_comment<'a>( // ```python // def test(a=10, /, b): pass // ``` - || arguments - .defaults - .iter() - .position(|default| AnyNodeRef::from(default).ptr_eq(last_argument_or_default)) - == Some(arguments.posonlyargs.len().saturating_sub(1)); + || are_same_optional(last_argument_or_default, arguments + .posonlyargs.last().and_then(|arg| arg.default.as_deref())); if !is_last_positional_argument { return CommentPlacement::Default(comment); @@ -906,7 +903,7 @@ fn last_child_in_body(node: AnyNodeRef) -> Option { | AnyNodeRef::StmtWith(StmtWith { body, .. }) | AnyNodeRef::StmtAsyncWith(StmtAsyncWith { body, .. }) | AnyNodeRef::MatchCase(MatchCase { body, .. }) - | AnyNodeRef::ExcepthandlerExceptHandler(ExcepthandlerExceptHandler { body, .. }) => body, + | AnyNodeRef::ExceptHandlerExceptHandler(ExceptHandlerExceptHandler { body, .. }) => body, AnyNodeRef::StmtIf(StmtIf { body, orelse, .. }) | AnyNodeRef::StmtFor(StmtFor { body, orelse, .. }) @@ -988,7 +985,7 @@ fn is_first_statement_in_enclosing_alternate_body( }) => { are_same_optional(following, handlers.first()) // Comments between the handlers and the `else`, or comments between the `handlers` and the `finally` - // are already handled by `handle_in_between_excepthandlers_or_except_handler_and_else_or_finally_comment` + // are already handled by `handle_in_between_except_handlers_or_except_handler_and_else_or_finally_comment` || handlers.is_empty() && are_same_optional(following, orelse.first()) || (handlers.is_empty() || !orelse.is_empty()) && are_same_optional(following, finalbody.first()) diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__try_except.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__try_except.snap index 1bcf35e95c..f72858e288 100644 --- a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__try_except.snap +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__try_except.snap @@ -34,7 +34,7 @@ expression: comments.debug(test_case.source_code) ], }, Node { - kind: ExcepthandlerExceptHandler, + kind: ExceptHandlerExceptHandler, range: 100..136, source: `except Exception as ex:⏎`, }: { diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__try_except_finally_else.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__try_except_finally_else.snap index d5b6bbcb7f..004b7a2de2 100644 --- a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__try_except_finally_else.snap +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__try_except_finally_else.snap @@ -39,7 +39,7 @@ expression: comments.debug(test_case.source_code) ], }, Node { - kind: ExcepthandlerExceptHandler, + kind: ExceptHandlerExceptHandler, range: 68..100, source: `except Exception as ex:⏎`, }: { diff --git a/crates/ruff_python_formatter/src/comments/visitor.rs b/crates/ruff_python_formatter/src/comments/visitor.rs index 4035cd05fe..26850b409c 100644 --- a/crates/ruff_python_formatter/src/comments/visitor.rs +++ b/crates/ruff_python_formatter/src/comments/visitor.rs @@ -208,11 +208,11 @@ impl<'ast> PreorderVisitor<'ast> for CommentsVisitor<'ast> { self.finish_node(comprehension); } - fn visit_excepthandler(&mut self, excepthandler: &'ast Excepthandler) { - if self.start_node(excepthandler).is_traverse() { - walk_excepthandler(self, excepthandler); + fn visit_except_handler(&mut self, except_handler: &'ast ExceptHandler) { + if self.start_node(except_handler).is_traverse() { + walk_except_handler(self, except_handler); } - self.finish_node(excepthandler); + self.finish_node(except_handler); } fn visit_format_spec(&mut self, format_spec: &'ast Expr) { @@ -250,12 +250,12 @@ impl<'ast> PreorderVisitor<'ast> for CommentsVisitor<'ast> { self.finish_node(alias); } - fn visit_withitem(&mut self, withitem: &'ast Withitem) { - if self.start_node(withitem).is_traverse() { - walk_withitem(self, withitem); + fn visit_with_item(&mut self, with_item: &'ast WithItem) { + if self.start_node(with_item).is_traverse() { + walk_with_item(self, with_item); } - self.finish_node(withitem); + self.finish_node(with_item); } fn visit_match_case(&mut self, match_case: &'ast MatchCase) { diff --git a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs index cd9618b28c..92461b4dfe 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs @@ -10,7 +10,7 @@ use ruff_formatter::{ }; use ruff_python_ast::node::AstNode; use rustpython_parser::ast::{ - Constant, Expr, ExprAttribute, ExprBinOp, ExprConstant, ExprUnaryOp, Operator, Unaryop, + Constant, Expr, ExprAttribute, ExprBinOp, ExprConstant, ExprUnaryOp, Operator, UnaryOp, }; #[derive(Default)] @@ -116,7 +116,7 @@ const fn is_simple_power_expression(expr: &ExprBinOp) -> bool { const fn is_simple_power_operand(expr: &Expr) -> bool { match expr { Expr::UnaryOp(ExprUnaryOp { - op: Unaryop::Not, .. + op: UnaryOp::Not, .. }) => false, Expr::Constant(ExprConstant { value: Constant::Complex { .. } | Constant::Float(_) | Constant::Int(_), diff --git a/crates/ruff_python_formatter/src/generated.rs b/crates/ruff_python_formatter/src/generated.rs index d491ef0065..d9bf041c64 100644 --- a/crates/ruff_python_formatter/src/generated.rs +++ b/crates/ruff_python_formatter/src/generated.rs @@ -2253,42 +2253,44 @@ impl<'ast> IntoFormat> for ast::ExprSlice { } } -impl FormatRule> - for crate::other::excepthandler_except_handler::FormatExcepthandlerExceptHandler +impl FormatRule> + for crate::other::except_handler_except_handler::FormatExceptHandlerExceptHandler { #[inline] fn fmt( &self, - node: &ast::ExcepthandlerExceptHandler, + node: &ast::ExceptHandlerExceptHandler, f: &mut Formatter>, ) -> FormatResult<()> { - FormatNodeRule::::fmt(self, node, f) + FormatNodeRule::::fmt(self, node, f) } } -impl<'ast> AsFormat> for ast::ExcepthandlerExceptHandler { +impl<'ast> AsFormat> for ast::ExceptHandlerExceptHandler { type Format<'a> = FormatRefWithRule< 'a, - ast::ExcepthandlerExceptHandler, - crate::other::excepthandler_except_handler::FormatExcepthandlerExceptHandler, + ast::ExceptHandlerExceptHandler, + crate::other::except_handler_except_handler::FormatExceptHandlerExceptHandler, PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::other::excepthandler_except_handler::FormatExcepthandlerExceptHandler::default(), + crate::other::except_handler_except_handler::FormatExceptHandlerExceptHandler::default( + ), ) } } -impl<'ast> IntoFormat> for ast::ExcepthandlerExceptHandler { +impl<'ast> IntoFormat> for ast::ExceptHandlerExceptHandler { type Format = FormatOwnedWithRule< - ast::ExcepthandlerExceptHandler, - crate::other::excepthandler_except_handler::FormatExcepthandlerExceptHandler, + ast::ExceptHandlerExceptHandler, + crate::other::except_handler_except_handler::FormatExceptHandlerExceptHandler, PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::other::excepthandler_except_handler::FormatExcepthandlerExceptHandler::default(), + crate::other::except_handler_except_handler::FormatExceptHandlerExceptHandler::default( + ), ) } } @@ -2746,6 +2748,46 @@ impl<'ast> IntoFormat> for ast::Arg { } } +impl FormatRule> + for crate::other::arg_with_default::FormatArgWithDefault +{ + #[inline] + fn fmt( + &self, + node: &ast::ArgWithDefault, + f: &mut Formatter>, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl<'ast> AsFormat> for ast::ArgWithDefault { + type Format<'a> = FormatRefWithRule< + 'a, + ast::ArgWithDefault, + crate::other::arg_with_default::FormatArgWithDefault, + PyFormatContext<'ast>, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::other::arg_with_default::FormatArgWithDefault::default(), + ) + } +} +impl<'ast> IntoFormat> for ast::ArgWithDefault { + type Format = FormatOwnedWithRule< + ast::ArgWithDefault, + crate::other::arg_with_default::FormatArgWithDefault, + PyFormatContext<'ast>, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::other::arg_with_default::FormatArgWithDefault::default(), + ) + } +} + impl FormatRule> for crate::other::keyword::FormatKeyword { #[inline] fn fmt(&self, node: &ast::Keyword, f: &mut Formatter>) -> FormatResult<()> { @@ -2795,35 +2837,35 @@ impl<'ast> IntoFormat> for ast::Alias { } } -impl FormatRule> for crate::other::withitem::FormatWithitem { +impl FormatRule> for crate::other::with_item::FormatWithItem { #[inline] fn fmt( &self, - node: &ast::Withitem, + node: &ast::WithItem, f: &mut Formatter>, ) -> FormatResult<()> { - FormatNodeRule::::fmt(self, node, f) + FormatNodeRule::::fmt(self, node, f) } } -impl<'ast> AsFormat> for ast::Withitem { +impl<'ast> AsFormat> for ast::WithItem { type Format<'a> = FormatRefWithRule< 'a, - ast::Withitem, - crate::other::withitem::FormatWithitem, + ast::WithItem, + crate::other::with_item::FormatWithItem, PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::other::withitem::FormatWithitem::default()) + FormatRefWithRule::new(self, crate::other::with_item::FormatWithItem::default()) } } -impl<'ast> IntoFormat> for ast::Withitem { +impl<'ast> IntoFormat> for ast::WithItem { type Format = FormatOwnedWithRule< - ast::Withitem, - crate::other::withitem::FormatWithitem, + ast::WithItem, + crate::other::with_item::FormatWithItem, PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::other::withitem::FormatWithitem::default()) + FormatOwnedWithRule::new(self, crate::other::with_item::FormatWithItem::default()) } } diff --git a/crates/ruff_python_formatter/src/other/arg_with_default.rs b/crates/ruff_python_formatter/src/other/arg_with_default.rs new file mode 100644 index 0000000000..2dc9993407 --- /dev/null +++ b/crates/ruff_python_formatter/src/other/arg_with_default.rs @@ -0,0 +1,28 @@ +use rustpython_parser::ast::ArgWithDefault; + +use ruff_formatter::write; + +use crate::prelude::*; +use crate::FormatNodeRule; + +#[derive(Default)] +pub struct FormatArgWithDefault; + +impl FormatNodeRule for FormatArgWithDefault { + fn fmt_fields(&self, item: &ArgWithDefault, f: &mut PyFormatter) -> FormatResult<()> { + let ArgWithDefault { + range: _, + def, + default, + } = item; + + write!(f, [def.format()])?; + + if let Some(default) = default { + let space = def.annotation.is_some().then_some(space()); + write!(f, [space, text("="), space, default.format()])?; + } + + Ok(()) + } +} diff --git a/crates/ruff_python_formatter/src/other/arguments.rs b/crates/ruff_python_formatter/src/other/arguments.rs index 9e0e88e214..63cc7859b9 100644 --- a/crates/ruff_python_formatter/src/other/arguments.rs +++ b/crates/ruff_python_formatter/src/other/arguments.rs @@ -1,12 +1,15 @@ +use std::usize; + +use rustpython_parser::ast::{Arguments, Ranged}; + +use ruff_formatter::{format_args, write}; +use ruff_python_ast::node::{AnyNodeRef, AstNode}; + use crate::comments::{dangling_node_comments, leading_node_comments}; use crate::context::NodeLevel; use crate::prelude::*; use crate::trivia::{first_non_trivia_token, SimpleTokenizer, Token, TokenKind}; use crate::FormatNodeRule; -use ruff_formatter::{format_args, write, FormatError}; -use ruff_python_ast::node::{AnyNodeRef, AstNode}; -use rustpython_parser::ast::{Arg, Arguments, Expr, Ranged}; -use std::usize; #[derive(Default)] pub struct FormatArguments; @@ -17,10 +20,8 @@ impl FormatNodeRule for FormatArguments { range: _, posonlyargs, args, - defaults, vararg, kwonlyargs, - kw_defaults, kwarg, } = item; @@ -32,30 +33,30 @@ impl FormatNodeRule for FormatArguments { let mut joiner = f.join_with(separator); let mut last_node: Option = None; - let mut defaults = std::iter::repeat(None) - .take(posonlyargs.len() + args.len() - defaults.len()) - .chain(defaults.iter().map(Some)); + for arg_with_default in posonlyargs { + joiner.entry(&arg_with_default.into_format()); - for positional in posonlyargs { - let default = defaults.next().ok_or(FormatError::SyntaxError)?; - joiner.entry(&ArgumentWithDefault { - argument: positional, - default, - }); - - last_node = Some(default.map_or_else(|| positional.into(), AnyNodeRef::from)); + last_node = Some( + arg_with_default + .default + .as_deref() + .map_or_else(|| (&arg_with_default.def).into(), AnyNodeRef::from), + ); } if !posonlyargs.is_empty() { joiner.entry(&text("/")); } - for argument in args { - let default = defaults.next().ok_or(FormatError::SyntaxError)?; + for arg_with_default in args { + joiner.entry(&arg_with_default.into_format()); - joiner.entry(&ArgumentWithDefault { argument, default }); - - last_node = Some(default.map_or_else(|| argument.into(), AnyNodeRef::from)); + last_node = Some( + arg_with_default + .default + .as_deref() + .map_or_else(|| (&arg_with_default.def).into(), AnyNodeRef::from), + ); } // kw only args need either a `*args` ahead of them capturing all var args or a `*` @@ -72,24 +73,17 @@ impl FormatNodeRule for FormatArguments { joiner.entry(&text("*")); } - debug_assert!(defaults.next().is_none()); + for arg_with_default in kwonlyargs { + joiner.entry(&arg_with_default.into_format()); - let mut defaults = std::iter::repeat(None) - .take(kwonlyargs.len() - kw_defaults.len()) - .chain(kw_defaults.iter().map(Some)); - - for keyword_argument in kwonlyargs { - let default = defaults.next().ok_or(FormatError::SyntaxError)?; - joiner.entry(&ArgumentWithDefault { - argument: keyword_argument, - default, - }); - - last_node = Some(default.map_or_else(|| keyword_argument.into(), AnyNodeRef::from)); + last_node = Some( + arg_with_default + .default + .as_deref() + .map_or_else(|| (&arg_with_default.def).into(), AnyNodeRef::from), + ); } - debug_assert!(defaults.next().is_none()); - if let Some(kwarg) = kwarg { joiner.entry(&format_args![ leading_node_comments(kwarg.as_ref()), @@ -173,21 +167,3 @@ impl FormatNodeRule for FormatArguments { Ok(()) } } - -struct ArgumentWithDefault<'a> { - argument: &'a Arg, - default: Option<&'a Expr>, -} - -impl Format> for ArgumentWithDefault<'_> { - fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { - write!(f, [self.argument.format()])?; - - if let Some(default) = self.default { - let space = self.argument.annotation.is_some().then_some(space()); - write!(f, [space, text("="), space, default.format()])?; - } - - Ok(()) - } -} diff --git a/crates/ruff_python_formatter/src/other/excepthandler_except_handler.rs b/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs similarity index 54% rename from crates/ruff_python_formatter/src/other/excepthandler_except_handler.rs rename to crates/ruff_python_formatter/src/other/except_handler_except_handler.rs index a5826413fc..338b686bc7 100644 --- a/crates/ruff_python_formatter/src/other/excepthandler_except_handler.rs +++ b/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs @@ -1,14 +1,14 @@ use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::ExcepthandlerExceptHandler; +use rustpython_parser::ast::ExceptHandlerExceptHandler; #[derive(Default)] -pub struct FormatExcepthandlerExceptHandler; +pub struct FormatExceptHandlerExceptHandler; -impl FormatNodeRule for FormatExcepthandlerExceptHandler { +impl FormatNodeRule for FormatExceptHandlerExceptHandler { fn fmt_fields( &self, - item: &ExcepthandlerExceptHandler, + item: &ExceptHandlerExceptHandler, f: &mut PyFormatter, ) -> FormatResult<()> { write!(f, [not_yet_implemented(item)]) diff --git a/crates/ruff_python_formatter/src/other/mod.rs b/crates/ruff_python_formatter/src/other/mod.rs index b7f843d7bc..fc2530e7c2 100644 --- a/crates/ruff_python_formatter/src/other/mod.rs +++ b/crates/ruff_python_formatter/src/other/mod.rs @@ -1,10 +1,11 @@ pub(crate) mod alias; pub(crate) mod arg; +pub(crate) mod arg_with_default; pub(crate) mod arguments; pub(crate) mod comprehension; pub(crate) mod decorator; -pub(crate) mod excepthandler_except_handler; +pub(crate) mod except_handler_except_handler; pub(crate) mod keyword; pub(crate) mod match_case; pub(crate) mod type_ignore_type_ignore; -pub(crate) mod withitem; +pub(crate) mod with_item; diff --git a/crates/ruff_python_formatter/src/other/withitem.rs b/crates/ruff_python_formatter/src/other/with_item.rs similarity index 53% rename from crates/ruff_python_formatter/src/other/withitem.rs rename to crates/ruff_python_formatter/src/other/with_item.rs index 7edf9ea116..3b13b582a0 100644 --- a/crates/ruff_python_formatter/src/other/withitem.rs +++ b/crates/ruff_python_formatter/src/other/with_item.rs @@ -1,12 +1,12 @@ use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::Withitem; +use rustpython_parser::ast::WithItem; #[derive(Default)] -pub struct FormatWithitem; +pub struct FormatWithItem; -impl FormatNodeRule for FormatWithitem { - fn fmt_fields(&self, item: &Withitem, f: &mut PyFormatter) -> FormatResult<()> { +impl FormatNodeRule for FormatWithItem { + fn fmt_fields(&self, item: &WithItem, f: &mut PyFormatter) -> FormatResult<()> { write!(f, [not_yet_implemented(item)]) } } diff --git a/crates/ruff_python_semantic/src/analyze/branch_detection.rs b/crates/ruff_python_semantic/src/analyze/branch_detection.rs index 7d1f1c2284..62ffb8e116 100644 --- a/crates/ruff_python_semantic/src/analyze/branch_detection.rs +++ b/crates/ruff_python_semantic/src/analyze/branch_detection.rs @@ -1,6 +1,6 @@ use std::cmp::Ordering; -use rustpython_parser::ast::{self, Excepthandler, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, Stmt}; use crate::node::{NodeId, Nodes}; @@ -57,7 +57,7 @@ fn alternatives(stmt: &Stmt) -> Vec> { }) => vec![body.iter().chain(orelse.iter()).collect()] .into_iter() .chain(handlers.iter().map(|handler| { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { body, .. }) = + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) = handler; body.iter().collect() })) From 015895bcae21225c2a3092a07d3ebb4e99c9061d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 19 Jun 2023 21:41:47 -0400 Subject: [PATCH 114/447] Move copyright rule to nursery (#5197) ## Summary I want this to be explicitly opted-into. --- crates/ruff/src/codes.rs | 2 +- ruff.schema.json | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 57985dd762..78356ead86 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -375,7 +375,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Simplify, "910") => (RuleGroup::Unspecified, rules::flake8_simplify::rules::DictGetWithNoneDefault), // copyright - (Copyright, "001") => (RuleGroup::Unspecified, rules::copyright::rules::MissingCopyrightNotice), + (Copyright, "001") => (RuleGroup::Nursery, rules::copyright::rules::MissingCopyrightNotice), // pyupgrade (Pyupgrade, "001") => (RuleGroup::Unspecified, rules::pyupgrade::rules::UselessMetaclassType), diff --git a/ruff.schema.json b/ruff.schema.json index e0342d8e3e..c74c74e26b 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1720,8 +1720,6 @@ "COM818", "COM819", "CPY", - "CPY0", - "CPY00", "CPY001", "D", "D1", From 8e06140d1d3646b51e1fc572043916f03e15d034 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 19 Jun 2023 22:04:28 -0400 Subject: [PATCH 115/447] Remove continuations when deleting statements (#5198) ## Summary This PR modifies our statement deletion logic to delete any preceding continuation lines. For example, given: ```py x = 1; \ import os ``` We'll now rewrite to: ```py x = 1; ``` In addition, the logic can now handle multiple preceding continuations (which is unlikely, but valid). --- .../pyflakes/multi_statement_lines.py | 23 +- crates/ruff/src/autofix/edits.rs | 48 +-- .../rules/duplicate_class_field_definition.rs | 1 - .../flake8_pie/rules/no_unnecessary_pass.rs | 8 +- .../rules/ellipsis_in_non_empty_class_body.rs | 9 +- .../flake8_pyi/rules/pass_in_class_body.rs | 9 +- .../rules/str_or_repr_defined_in_stub.rs | 8 +- .../src/rules/flake8_return/rules/function.rs | 9 +- .../rules/empty_type_checking_block.rs | 8 +- .../pycodestyle/rules/lambda_assignment.rs | 4 +- .../rules/pyflakes/rules/unused_variable.rs | 16 +- ...yflakes__tests__multi_statement_lines.snap | 388 ++++++++++-------- crates/ruff/src/rules/pylint/rules/logging.rs | 4 +- .../src/rules/pylint/rules/useless_return.rs | 9 +- .../pyupgrade/rules/outdated_version_block.rs | 1 - .../pyupgrade/rules/useless_metaclass_type.rs | 8 +- crates/ruff_python_ast/src/helpers.rs | 158 ++++--- 17 files changed, 394 insertions(+), 317 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pyflakes/multi_statement_lines.py b/crates/ruff/resources/test/fixtures/pyflakes/multi_statement_lines.py index d4e8712933..16233c83db 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/multi_statement_lines.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/multi_statement_lines.py @@ -1,4 +1,3 @@ - if True: import foo1; x = 1 import foo2; x = 1 @@ -11,7 +10,6 @@ if True: import foo4 \ ; x = 1 - if True: x = 1; import foo5 @@ -20,12 +18,10 @@ if True: x = 1; \ import foo6 - if True: x = 1 \ ; import foo7 - if True: x = 1; import foo8; x = 1 x = 1; import foo9; x = 1 @@ -40,12 +36,27 @@ if True: ;import foo11 \ ;x = 1 +if True: + x = 1; \ + \ + import foo12 + +if True: + x = 1; \ +\ + import foo13 + + +if True: + x = 1; \ + # \ + import foo14 # Continuation, but not as the last content in the file. x = 1; \ -import foo12 +import foo15 # Continuation, followed by end-of-file. (Removing `import foo` would cause a syntax # error.) x = 1; \ -import foo13 +import foo16 \ No newline at end of file diff --git a/crates/ruff/src/autofix/edits.rs b/crates/ruff/src/autofix/edits.rs index c6218e5060..285d42c258 100644 --- a/crates/ruff/src/autofix/edits.rs +++ b/crates/ruff/src/autofix/edits.rs @@ -29,7 +29,6 @@ pub(crate) fn delete_stmt( parent: Option<&Stmt>, locator: &Locator, indexer: &Indexer, - stylist: &Stylist, ) -> Edit { if parent .map(|parent| is_lone_child(stmt, parent)) @@ -39,18 +38,15 @@ pub(crate) fn delete_stmt( // it with a `pass`. Edit::range_replacement("pass".to_string(), stmt.range()) } else { - if let Some(semicolon) = trailing_semicolon(stmt, locator) { + if let Some(semicolon) = trailing_semicolon(stmt.end(), locator) { let next = next_stmt_break(semicolon, locator); Edit::deletion(stmt.start(), next) - } else if helpers::has_leading_content(stmt, locator) { + } else if helpers::has_leading_content(stmt.start(), locator) { Edit::range_deletion(stmt.range()) - } else if helpers::preceded_by_continuation(stmt, indexer, locator) { - if is_end_of_file(stmt, locator) && locator.is_at_start_of_line(stmt.start()) { - // Special-case: a file can't end in a continuation. - Edit::range_replacement(stylist.line_ending().to_string(), stmt.range()) - } else { - Edit::range_deletion(stmt.range()) - } + } else if let Some(start) = + helpers::preceded_by_continuations(stmt.start(), locator, indexer) + { + Edit::range_deletion(TextRange::new(start, stmt.end())) } else { let range = locator.full_lines_range(stmt.range()); Edit::range_deletion(range) @@ -68,7 +64,7 @@ pub(crate) fn remove_unused_imports<'a>( stylist: &Stylist, ) -> Result { match codemods::remove_imports(unused_imports, stmt, locator, stylist)? { - None => Ok(delete_stmt(stmt, parent, locator, indexer, stylist)), + None => Ok(delete_stmt(stmt, parent, locator, indexer)), Some(content) => Ok(Edit::range_replacement(content, stmt.range())), } } @@ -238,15 +234,15 @@ fn is_lone_child(child: &Stmt, parent: &Stmt) -> bool { /// Return the location of a trailing semicolon following a `Stmt`, if it's part /// of a multi-statement line. -fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option { - let contents = locator.after(stmt.end()); +fn trailing_semicolon(offset: TextSize, locator: &Locator) -> Option { + let contents = locator.after(offset); for line in NewlineWithTrailingNewline::from(contents) { let trimmed = line.trim_whitespace_start(); if trimmed.starts_with(';') { let colon_offset = line.text_len() - trimmed.text_len(); - return Some(stmt.end() + line.start() + colon_offset); + return Some(offset + line.start() + colon_offset); } if !trimmed.starts_with('\\') { @@ -284,16 +280,11 @@ fn next_stmt_break(semicolon: TextSize, locator: &Locator) -> TextSize { locator.line_end(start_location) } -/// Return `true` if a `Stmt` occurs at the end of a file. -fn is_end_of_file(stmt: &Stmt, locator: &Locator) -> bool { - stmt.end() == locator.contents().text_len() -} - #[cfg(test)] mod tests { use anyhow::Result; use ruff_text_size::TextSize; - use rustpython_parser::ast::Suite; + use rustpython_parser::ast::{Ranged, Suite}; use rustpython_parser::Parse; use ruff_python_ast::source_code::Locator; @@ -306,19 +297,25 @@ mod tests { let program = Suite::parse(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert_eq!(trailing_semicolon(stmt, &locator), None); + assert_eq!(trailing_semicolon(stmt.end(), &locator), None); let contents = "x = 1; y = 1"; let program = Suite::parse(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(5))); + assert_eq!( + trailing_semicolon(stmt.end(), &locator), + Some(TextSize::from(5)) + ); let contents = "x = 1 ; y = 1"; let program = Suite::parse(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(6))); + assert_eq!( + trailing_semicolon(stmt.end(), &locator), + Some(TextSize::from(6)) + ); let contents = r#" x = 1 \ @@ -328,7 +325,10 @@ x = 1 \ let program = Suite::parse(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(10))); + assert_eq!( + trailing_semicolon(stmt.end(), &locator), + Some(TextSize::from(10)) + ); Ok(()) } diff --git a/crates/ruff/src/rules/flake8_pie/rules/duplicate_class_field_definition.rs b/crates/ruff/src/rules/flake8_pie/rules/duplicate_class_field_definition.rs index ce0991ac98..6351778925 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/duplicate_class_field_definition.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/duplicate_class_field_definition.rs @@ -93,7 +93,6 @@ pub(crate) fn duplicate_class_field_definition<'a, 'b>( Some(parent), checker.locator, checker.indexer, - checker.stylist, ); diagnostic.set_fix(Fix::suggested(edit).isolate(checker.isolation(Some(parent)))); } diff --git a/crates/ruff/src/rules/flake8_pie/rules/no_unnecessary_pass.rs b/crates/ruff/src/rules/flake8_pie/rules/no_unnecessary_pass.rs index f9047c5004..1cc40d052e 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/no_unnecessary_pass.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/no_unnecessary_pass.rs @@ -67,13 +67,7 @@ pub(crate) fn no_unnecessary_pass(checker: &mut Checker, body: &[Stmt]) { let edit = if let Some(index) = trailing_comment_start_offset(stmt, checker.locator) { Edit::range_deletion(stmt.range().add_end(index)) } else { - autofix::edits::delete_stmt( - stmt, - None, - checker.locator, - checker.indexer, - checker.stylist, - ) + autofix::edits::delete_stmt(stmt, None, checker.locator, checker.indexer) }; diagnostic.set_fix(Fix::automatic(edit)); } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs b/crates/ruff/src/rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs index 3648d27345..5239611c0f 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs @@ -69,13 +69,8 @@ pub(crate) fn ellipsis_in_non_empty_class_body<'a>( let mut diagnostic = Diagnostic::new(EllipsisInNonEmptyClassBody, stmt.range()); if checker.patch(diagnostic.kind.rule()) { - let edit = autofix::edits::delete_stmt( - stmt, - Some(parent), - checker.locator, - checker.indexer, - checker.stylist, - ); + let edit = + autofix::edits::delete_stmt(stmt, Some(parent), checker.locator, checker.indexer); diagnostic.set_fix(Fix::automatic(edit).isolate(checker.isolation(Some(parent)))); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/flake8_pyi/rules/pass_in_class_body.rs b/crates/ruff/src/rules/flake8_pyi/rules/pass_in_class_body.rs index 43f1829e27..d6f947fd34 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/pass_in_class_body.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/pass_in_class_body.rs @@ -39,13 +39,8 @@ pub(crate) fn pass_in_class_body<'a>( let mut diagnostic = Diagnostic::new(PassInClassBody, stmt.range()); if checker.patch(diagnostic.kind.rule()) { - let edit = autofix::edits::delete_stmt( - stmt, - Some(parent), - checker.locator, - checker.indexer, - checker.stylist, - ); + let edit = + autofix::edits::delete_stmt(stmt, Some(parent), checker.locator, checker.indexer); diagnostic.set_fix(Fix::automatic(edit).isolate(checker.isolation(Some(parent)))); } checker.diagnostics.push(diagnostic); 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 3ae9eab2f8..54611b75e3 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 @@ -95,13 +95,7 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) { if checker.patch(diagnostic.kind.rule()) { let stmt = checker.semantic().stmt(); let parent = checker.semantic().stmt_parent(); - let edit = delete_stmt( - stmt, - parent, - checker.locator, - checker.indexer, - checker.stylist, - ); + let edit = delete_stmt(stmt, parent, checker.locator, checker.indexer); diagnostic.set_fix( Fix::automatic(edit).isolate(checker.isolation(checker.semantic().stmt_parent())), ); diff --git a/crates/ruff/src/rules/flake8_return/rules/function.rs b/crates/ruff/src/rules/flake8_return/rules/function.rs index 1fb986ca2b..11b5582579 100644 --- a/crates/ruff/src/rules/flake8_return/rules/function.rs +++ b/crates/ruff/src/rules/flake8_return/rules/function.rs @@ -517,13 +517,8 @@ fn unnecessary_assign(checker: &mut Checker, stack: &Stack) { // Delete the `return` statement. There's no need to treat this as an isolated // edit, since we're editing the preceding statement, so no conflicting edit would // be allowed to remove that preceding statement. - let delete_return = edits::delete_stmt( - stmt, - None, - checker.locator, - checker.indexer, - checker.stylist, - ); + let delete_return = + edits::delete_stmt(stmt, None, checker.locator, checker.indexer); // Replace the `x = 1` statement with `return 1`. let content = checker.locator.slice(assign.range()); diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs b/crates/ruff/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs index 88ed829041..f3d0929615 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs @@ -62,13 +62,7 @@ pub(crate) fn empty_type_checking_block(checker: &mut Checker, stmt: &ast::StmtI // Delete the entire type-checking block. let stmt = checker.semantic().stmt(); let parent = checker.semantic().stmt_parent(); - let edit = autofix::edits::delete_stmt( - stmt, - parent, - checker.locator, - checker.indexer, - checker.stylist, - ); + let edit = autofix::edits::delete_stmt(stmt, parent, checker.locator, checker.indexer); diagnostic.set_fix(Fix::automatic(edit).isolate(checker.isolation(parent))); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs index d6126674c7..9d65aa6692 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs @@ -78,8 +78,8 @@ pub(crate) fn lambda_assignment( // See https://github.com/astral-sh/ruff/issues/3046 if checker.patch(diagnostic.kind.rule()) && !checker.semantic().scope().kind.is_class() - && !has_leading_content(stmt, checker.locator) - && !has_trailing_content(stmt, checker.locator) + && !has_leading_content(stmt.start(), checker.locator) + && !has_trailing_content(stmt.end(), checker.locator) { let first_line = checker.locator.line(stmt.start()); let indentation = leading_indentation(first_line); diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs b/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs index f9ede7b63b..a611eb8375 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs @@ -210,13 +210,7 @@ fn remove_unused_variable( Some(Fix::suggested(edit)) } else { // If (e.g.) assigning to a constant (`x = 1`), delete the entire statement. - let edit = delete_stmt( - stmt, - parent, - checker.locator, - checker.indexer, - checker.stylist, - ); + let edit = delete_stmt(stmt, parent, checker.locator, checker.indexer); Some(Fix::suggested(edit).isolate(checker.isolation(parent))) }; } @@ -241,13 +235,7 @@ fn remove_unused_variable( Some(Fix::suggested(edit)) } else { // If (e.g.) assigning to a constant (`x = 1`), delete the entire statement. - let edit = delete_stmt( - stmt, - parent, - checker.locator, - checker.indexer, - checker.stylist, - ); + let edit = delete_stmt(stmt, parent, checker.locator, checker.indexer); Some(Fix::suggested(edit).isolate(checker.isolation(parent))) }; } diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__multi_statement_lines.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__multi_statement_lines.snap index 00f0e38f00..f384bf81ca 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__multi_statement_lines.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__multi_statement_lines.snap @@ -1,257 +1,325 @@ --- source: crates/ruff/src/rules/pyflakes/mod.rs --- -multi_statement_lines.py:3:12: F401 [*] `foo1` imported but unused +multi_statement_lines.py:2:12: F401 [*] `foo1` imported but unused | -2 | if True: -3 | import foo1; x = 1 +1 | if True: +2 | import foo1; x = 1 | ^^^^ F401 -4 | import foo2; x = 1 +3 | import foo2; x = 1 | = help: Remove unused import: `foo1` ℹ Fix -1 1 | -2 2 | if True: -3 |- import foo1; x = 1 - 3 |+ x = 1 -4 4 | import foo2; x = 1 -5 5 | -6 6 | if True: +1 1 | if True: +2 |- import foo1; x = 1 + 2 |+ x = 1 +3 3 | import foo2; x = 1 +4 4 | +5 5 | if True: -multi_statement_lines.py:4:12: F401 [*] `foo2` imported but unused +multi_statement_lines.py:3:12: F401 [*] `foo2` imported but unused | -2 | if True: -3 | import foo1; x = 1 -4 | import foo2; x = 1 +1 | if True: +2 | import foo1; x = 1 +3 | import foo2; x = 1 | ^^^^ F401 -5 | -6 | if True: +4 | +5 | if True: | = help: Remove unused import: `foo2` ℹ Fix -1 1 | -2 2 | if True: -3 3 | import foo1; x = 1 -4 |- import foo2; x = 1 - 4 |+ x = 1 -5 5 | -6 6 | if True: -7 7 | import foo3; \ +1 1 | if True: +2 2 | import foo1; x = 1 +3 |- import foo2; x = 1 + 3 |+ x = 1 +4 4 | +5 5 | if True: +6 6 | import foo3; \ -multi_statement_lines.py:7:12: F401 [*] `foo3` imported but unused +multi_statement_lines.py:6:12: F401 [*] `foo3` imported but unused | -6 | if True: -7 | import foo3; \ +5 | if True: +6 | import foo3; \ | ^^^^ F401 -8 | x = 1 +7 | x = 1 | = help: Remove unused import: `foo3` ℹ Fix -4 4 | import foo2; x = 1 -5 5 | -6 6 | if True: -7 |- import foo3; \ -8 |-x = 1 - 7 |+ x = 1 -9 8 | -10 9 | if True: -11 10 | import foo4 \ +3 3 | import foo2; x = 1 +4 4 | +5 5 | if True: +6 |- import foo3; \ +7 |-x = 1 + 6 |+ x = 1 +8 7 | +9 8 | if True: +10 9 | import foo4 \ -multi_statement_lines.py:11:12: F401 [*] `foo4` imported but unused +multi_statement_lines.py:10:12: F401 [*] `foo4` imported but unused | -10 | if True: -11 | import foo4 \ + 9 | if True: +10 | import foo4 \ | ^^^^ F401 -12 | ; x = 1 +11 | ; x = 1 | = help: Remove unused import: `foo4` ℹ Fix -8 8 | x = 1 -9 9 | -10 10 | if True: -11 |- import foo4 \ -12 |- ; x = 1 - 11 |+ x = 1 -13 12 | -14 13 | -15 14 | if True: +7 7 | x = 1 +8 8 | +9 9 | if True: +10 |- import foo4 \ +11 |- ; x = 1 + 10 |+ x = 1 +12 11 | +13 12 | if True: +14 13 | x = 1; import foo5 -multi_statement_lines.py:16:19: F401 [*] `foo5` imported but unused +multi_statement_lines.py:14:19: F401 [*] `foo5` imported but unused | -15 | if True: -16 | x = 1; import foo5 +13 | if True: +14 | x = 1; import foo5 | ^^^^ F401 | = help: Remove unused import: `foo5` ℹ Fix -13 13 | -14 14 | -15 15 | if True: -16 |- x = 1; import foo5 - 16 |+ x = 1; -17 17 | -18 18 | -19 19 | if True: +11 11 | ; x = 1 +12 12 | +13 13 | if True: +14 |- x = 1; import foo5 + 14 |+ x = 1; +15 15 | +16 16 | +17 17 | if True: -multi_statement_lines.py:21:17: F401 [*] `foo6` imported but unused +multi_statement_lines.py:19:17: F401 [*] `foo6` imported but unused | -19 | if True: -20 | x = 1; \ -21 | import foo6 +17 | if True: +18 | x = 1; \ +19 | import foo6 | ^^^^ F401 +20 | +21 | if True: | = help: Remove unused import: `foo6` ℹ Fix -18 18 | -19 19 | if True: -20 20 | x = 1; \ -21 |- import foo6 - 21 |+ -22 22 | -23 23 | -24 24 | if True: +15 15 | +16 16 | +17 17 | if True: +18 |- x = 1; \ +19 |- import foo6 + 18 |+ x = 1; +20 19 | +21 20 | if True: +22 21 | x = 1 \ -multi_statement_lines.py:26:18: F401 [*] `foo7` imported but unused +multi_statement_lines.py:23:18: F401 [*] `foo7` imported but unused | -24 | if True: -25 | x = 1 \ -26 | ; import foo7 +21 | if True: +22 | x = 1 \ +23 | ; import foo7 | ^^^^ F401 +24 | +25 | if True: | = help: Remove unused import: `foo7` ℹ Fix -23 23 | -24 24 | if True: -25 25 | x = 1 \ -26 |- ; import foo7 - 26 |+ ; -27 27 | -28 28 | -29 29 | if True: +20 20 | +21 21 | if True: +22 22 | x = 1 \ +23 |- ; import foo7 + 23 |+ ; +24 24 | +25 25 | if True: +26 26 | x = 1; import foo8; x = 1 -multi_statement_lines.py:30:19: F401 [*] `foo8` imported but unused +multi_statement_lines.py:26:19: F401 [*] `foo8` imported but unused | -29 | if True: -30 | x = 1; import foo8; x = 1 +25 | if True: +26 | x = 1; import foo8; x = 1 | ^^^^ F401 -31 | x = 1; import foo9; x = 1 +27 | x = 1; import foo9; x = 1 | = help: Remove unused import: `foo8` ℹ Fix -27 27 | +23 23 | ; import foo7 +24 24 | +25 25 | if True: +26 |- x = 1; import foo8; x = 1 + 26 |+ x = 1; x = 1 +27 27 | x = 1; import foo9; x = 1 28 28 | 29 29 | if True: -30 |- x = 1; import foo8; x = 1 - 30 |+ x = 1; x = 1 -31 31 | x = 1; import foo9; x = 1 -32 32 | -33 33 | if True: -multi_statement_lines.py:31:23: F401 [*] `foo9` imported but unused +multi_statement_lines.py:27:23: F401 [*] `foo9` imported but unused | -29 | if True: -30 | x = 1; import foo8; x = 1 -31 | x = 1; import foo9; x = 1 +25 | if True: +26 | x = 1; import foo8; x = 1 +27 | x = 1; import foo9; x = 1 | ^^^^ F401 -32 | -33 | if True: +28 | +29 | if True: | = help: Remove unused import: `foo9` ℹ Fix +24 24 | +25 25 | if True: +26 26 | x = 1; import foo8; x = 1 +27 |- x = 1; import foo9; x = 1 + 27 |+ x = 1; x = 1 28 28 | 29 29 | if True: -30 30 | x = 1; import foo8; x = 1 -31 |- x = 1; import foo9; x = 1 - 31 |+ x = 1; x = 1 -32 32 | -33 33 | if True: -34 34 | x = 1; \ +30 30 | x = 1; \ -multi_statement_lines.py:35:16: F401 [*] `foo10` imported but unused +multi_statement_lines.py:31:16: F401 [*] `foo10` imported but unused | -33 | if True: -34 | x = 1; \ -35 | import foo10; \ +29 | if True: +30 | x = 1; \ +31 | import foo10; \ | ^^^^^ F401 -36 | x = 1 +32 | x = 1 | = help: Remove unused import: `foo10` ℹ Fix -32 32 | -33 33 | if True: -34 34 | x = 1; \ -35 |- import foo10; \ -36 |- x = 1 - 35 |+ x = 1 -37 36 | -38 37 | if True: -39 38 | x = 1 \ +28 28 | +29 29 | if True: +30 30 | x = 1; \ +31 |- import foo10; \ +32 |- x = 1 + 31 |+ x = 1 +33 32 | +34 33 | if True: +35 34 | x = 1 \ -multi_statement_lines.py:40:17: F401 [*] `foo11` imported but unused +multi_statement_lines.py:36:17: F401 [*] `foo11` imported but unused | -38 | if True: -39 | x = 1 \ -40 | ;import foo11 \ +34 | if True: +35 | x = 1 \ +36 | ;import foo11 \ | ^^^^^ F401 -41 | ;x = 1 +37 | ;x = 1 | = help: Remove unused import: `foo11` ℹ Fix -37 37 | -38 38 | if True: -39 39 | x = 1 \ -40 |- ;import foo11 \ -41 40 | ;x = 1 -42 41 | -43 42 | +33 33 | +34 34 | if True: +35 35 | x = 1 \ +36 |- ;import foo11 \ +37 36 | ;x = 1 +38 37 | +39 38 | if True: -multi_statement_lines.py:46:8: F401 [*] `foo12` imported but unused +multi_statement_lines.py:42:16: F401 [*] `foo12` imported but unused | -44 | # Continuation, but not as the last content in the file. -45 | x = 1; \ -46 | import foo12 - | ^^^^^ F401 -47 | -48 | # Continuation, followed by end-of-file. (Removing `import foo` would cause a syntax +40 | x = 1; \ +41 | \ +42 | import foo12 + | ^^^^^ F401 +43 | +44 | if True: | = help: Remove unused import: `foo12` ℹ Fix -43 43 | -44 44 | # Continuation, but not as the last content in the file. -45 45 | x = 1; \ -46 |-import foo12 -47 46 | - 47 |+ -48 48 | # Continuation, followed by end-of-file. (Removing `import foo` would cause a syntax -49 49 | # error.) -50 50 | x = 1; \ +37 37 | ;x = 1 +38 38 | +39 39 | if True: +40 |- x = 1; \ +41 |- \ +42 |- import foo12 + 40 |+ x = 1; +43 41 | +44 42 | if True: +45 43 | x = 1; \ -multi_statement_lines.py:51:8: F401 [*] `foo13` imported but unused +multi_statement_lines.py:47:12: F401 [*] `foo13` imported but unused | -49 | # error.) -50 | x = 1; \ -51 | import foo13 - | ^^^^^ F401 +45 | x = 1; \ +46 | \ +47 | import foo13 + | ^^^^^ F401 | = help: Remove unused import: `foo13` ℹ Fix -48 48 | # Continuation, followed by end-of-file. (Removing `import foo` would cause a syntax -49 49 | # error.) -50 50 | x = 1; \ -51 |-import foo13 - 51 |+ +42 42 | import foo12 +43 43 | +44 44 | if True: +45 |- x = 1; \ +46 |-\ +47 |- import foo13 + 45 |+ x = 1; +48 46 | +49 47 | +50 48 | if True: + +multi_statement_lines.py:53:12: F401 [*] `foo14` imported but unused + | +51 | x = 1; \ +52 | # \ +53 | import foo14 + | ^^^^^ F401 +54 | +55 | # Continuation, but not as the last content in the file. + | + = help: Remove unused import: `foo14` + +ℹ Fix +50 50 | if True: +51 51 | x = 1; \ +52 52 | # \ +53 |- import foo14 +54 53 | +55 54 | # Continuation, but not as the last content in the file. +56 55 | x = 1; \ + +multi_statement_lines.py:57:8: F401 [*] `foo15` imported but unused + | +55 | # Continuation, but not as the last content in the file. +56 | x = 1; \ +57 | import foo15 + | ^^^^^ F401 +58 | +59 | # Continuation, followed by end-of-file. (Removing `import foo` would cause a syntax + | + = help: Remove unused import: `foo15` + +ℹ Fix +53 53 | import foo14 +54 54 | +55 55 | # Continuation, but not as the last content in the file. +56 |-x = 1; \ +57 |-import foo15 + 56 |+x = 1; +58 57 | +59 58 | # Continuation, followed by end-of-file. (Removing `import foo` would cause a syntax +60 59 | # error.) + +multi_statement_lines.py:62:8: F401 [*] `foo16` imported but unused + | +60 | # error.) +61 | x = 1; \ +62 | import foo16 + | ^^^^^ F401 + | + = help: Remove unused import: `foo16` + +ℹ Fix +58 58 | +59 59 | # Continuation, followed by end-of-file. (Removing `import foo` would cause a syntax +60 60 | # error.) +61 |-x = 1; \ +62 |-import foo16 + 61 |+x = 1; diff --git a/crates/ruff/src/rules/pylint/rules/logging.rs b/crates/ruff/src/rules/pylint/rules/logging.rs index 157844d818..8391113612 100644 --- a/crates/ruff/src/rules/pylint/rules/logging.rs +++ b/crates/ruff/src/rules/pylint/rules/logging.rs @@ -122,7 +122,7 @@ pub(crate) fn logging_call( return; } - let message_args = call_args.args.len() - 1; + let message_args = call_args.num_args() - 1; if checker.enabled(Rule::LoggingTooManyArgs) { if summary.num_positional < message_args { @@ -134,7 +134,7 @@ pub(crate) fn logging_call( if checker.enabled(Rule::LoggingTooFewArgs) { if message_args > 0 - && call_args.kwargs.is_empty() + && call_args.num_kwargs() == 0 && summary.num_positional > message_args { checker diff --git a/crates/ruff/src/rules/pylint/rules/useless_return.rs b/crates/ruff/src/rules/pylint/rules/useless_return.rs index 82436e976e..58b6b1b46e 100644 --- a/crates/ruff/src/rules/pylint/rules/useless_return.rs +++ b/crates/ruff/src/rules/pylint/rules/useless_return.rs @@ -103,13 +103,8 @@ pub(crate) fn useless_return<'a>( let mut diagnostic = Diagnostic::new(UselessReturn, last_stmt.range()); if checker.patch(diagnostic.kind.rule()) { - let edit = autofix::edits::delete_stmt( - last_stmt, - Some(stmt), - checker.locator, - checker.indexer, - checker.stylist, - ); + let edit = + autofix::edits::delete_stmt(last_stmt, Some(stmt), checker.locator, checker.indexer); diagnostic.set_fix(Fix::automatic(edit).isolate(checker.isolation(Some(stmt)))); } checker.diagnostics.push(diagnostic); 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 5598b2a76d..8fad415be1 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs @@ -226,7 +226,6 @@ fn fix_py2_block( if matches!(block.leading_token.tok, StartTok::If) { parent } else { None }, checker.locator, checker.indexer, - checker.stylist, ); return Some(Fix::suggested(edit)); }; 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 a739ccad3e..bb54016701 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs @@ -68,13 +68,7 @@ pub(crate) fn useless_metaclass_type( if checker.patch(diagnostic.kind.rule()) { let stmt = checker.semantic().stmt(); let parent = checker.semantic().stmt_parent(); - let edit = autofix::edits::delete_stmt( - stmt, - parent, - checker.locator, - checker.indexer, - checker.stylist, - ); + let edit = autofix::edits::delete_stmt(stmt, parent, checker.locator, checker.indexer); diagnostic.set_fix(Fix::automatic(edit).isolate(checker.isolation(parent))); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index d9a64049a8..1c6c65e0d5 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -12,7 +12,7 @@ use rustpython_parser::ast::{ use rustpython_parser::{lexer, Mode, Tok}; use smallvec::SmallVec; -use ruff_python_whitespace::{PythonWhitespace, UniversalNewlineIterator}; +use ruff_python_whitespace::{is_python_whitespace, PythonWhitespace, UniversalNewlineIterator}; use crate::call_path::CallPath; use crate::source_code::{Indexer, Locator}; @@ -717,19 +717,19 @@ pub fn map_subscript(expr: &Expr) -> &Expr { } /// Returns `true` if a statement or expression includes at least one comment. -pub fn has_comments(located: &T, locator: &Locator) -> bool +pub fn has_comments(node: &T, locator: &Locator) -> bool where T: Ranged, { - let start = if has_leading_content(located, locator) { - located.start() + let start = if has_leading_content(node.start(), locator) { + node.start() } else { - locator.line_start(located.start()) + locator.line_start(node.start()) }; - let end = if has_trailing_content(located, locator) { - located.end() + let end = if has_trailing_content(node.end(), locator) { + node.end() } else { - locator.line_end(located.end()) + locator.line_end(node.end()) }; has_comments_in(TextRange::new(start, end), locator) @@ -927,7 +927,7 @@ where { fn visit_stmt(&mut self, stmt: &'b Stmt) { match stmt { - Stmt::FunctionDef(_) | Stmt::AsyncFunctionDef(_) => { + Stmt::FunctionDef(_) | Stmt::AsyncFunctionDef(_) | Stmt::ClassDef(_) => { // Don't recurse. } Stmt::Return(stmt) => self.returns.push(stmt), @@ -982,29 +982,23 @@ where } } -/// Return `true` if a [`Ranged`] has leading content. -pub fn has_leading_content(located: &T, locator: &Locator) -> bool -where - T: Ranged, -{ - let line_start = locator.line_start(located.start()); - let leading = &locator.contents()[TextRange::new(line_start, located.start())]; - leading.chars().any(|char| !char.is_whitespace()) +/// Return `true` if the node starting the given [`TextSize`] has leading content. +pub fn has_leading_content(offset: TextSize, locator: &Locator) -> bool { + let line_start = locator.line_start(offset); + let leading = &locator.contents()[TextRange::new(line_start, offset)]; + leading.chars().any(|char| !is_python_whitespace(char)) } -/// Return `true` if a [`Ranged`] has trailing content. -pub fn has_trailing_content(located: &T, locator: &Locator) -> bool -where - T: Ranged, -{ - let line_end = locator.line_end(located.end()); - let trailing = &locator.contents()[TextRange::new(located.end(), line_end)]; +/// Return `true` if the node ending at the given [`TextSize`] has trailing content. +pub fn has_trailing_content(offset: TextSize, locator: &Locator) -> bool { + let line_end = locator.line_end(offset); + let trailing = &locator.contents()[TextRange::new(offset, line_end)]; for char in trailing.chars() { if char == '#' { return false; } - if !char.is_whitespace() { + if !is_python_whitespace(char) { return true; } } @@ -1020,11 +1014,11 @@ where let trailing = &locator.contents()[TextRange::new(located.end(), line_end)]; - for (i, char) in trailing.chars().enumerate() { + for (index, char) in trailing.char_indices() { if char == '#' { - return TextSize::try_from(i).ok(); + return TextSize::try_from(index).ok(); } - if !char.is_whitespace() { + if !is_python_whitespace(char) { return None; } } @@ -1040,7 +1034,7 @@ pub fn trailing_lines_end(stmt: &Stmt, locator: &Locator) -> TextSize { UniversalNewlineIterator::with_offset(rest, line_end) .take_while(|line| line.trim_whitespace().is_empty()) .last() - .map_or(line_end, |l| l.full_end()) + .map_or(line_end, |line| line.full_end()) } /// Return the range of the first parenthesis pair after a given [`TextSize`]. @@ -1081,7 +1075,7 @@ pub fn first_colon_range(range: TextRange, locator: &Locator) -> Option Option bool { - let previous_line_end = locator.line_start(stmt.start()); - let newline_pos = usize::from(previous_line_end).saturating_sub(1); +/// Given an offset at the end of a line (including newlines), return the offset of the +/// continuation at the end of that line. +fn find_continuation(offset: TextSize, locator: &Locator, indexer: &Indexer) -> Option { + let newline_pos = usize::from(offset).saturating_sub(1); - // Compute start of preceding line + // Skip the newline. let newline_len = match locator.contents().as_bytes()[newline_pos] { b'\n' => { if locator @@ -1126,24 +1119,77 @@ pub fn preceded_by_continuation(stmt: &Stmt, indexer: &Indexer, locator: &Locato } } b'\r' => 1, - // No preceding line - _ => return false, + // No preceding line. + _ => return None, }; - // See if the position is in the continuation line starts - indexer.is_continuation(previous_line_end - TextSize::from(newline_len), locator) + indexer + .is_continuation(offset - TextSize::from(newline_len), locator) + .then(|| offset - TextSize::from(newline_len) - TextSize::from(1)) +} + +/// If the node starting at the given [`TextSize`] is preceded by at least one continuation line +/// (i.e., a line ending in a backslash), return the starting offset of the first such continuation +/// character. +/// +/// For example, given: +/// ```python +/// x = 1; \ +/// y = 2 +/// ``` +/// +/// When passed the offset of `y`, this function will return the offset of the backslash at the end +/// of the first line. +/// +/// Similarly, given: +/// ```python +/// x = 1; \ +/// \ +/// y = 2; +/// ``` +/// +/// When passed the offset of `y`, this function will again return the offset of the backslash at +/// the end of the first line. +pub fn preceded_by_continuations( + offset: TextSize, + locator: &Locator, + indexer: &Indexer, +) -> Option { + // Find the first preceding continuation. + let mut continuation = find_continuation(locator.line_start(offset), locator, indexer)?; + + // Continue searching for continuations, in the unlikely event that we have multiple + // continuations in a row. + loop { + let previous_line_end = locator.line_start(continuation); + if locator + .slice(TextRange::new(previous_line_end, continuation)) + .chars() + .all(is_python_whitespace) + { + if let Some(next_continuation) = find_continuation(previous_line_end, locator, indexer) + { + continuation = next_continuation; + continue; + } + } + break; + } + + Some(continuation) } /// Return `true` if a `Stmt` appears to be part of a multi-statement line, with /// other statements preceding it. pub fn preceded_by_multi_statement_line(stmt: &Stmt, locator: &Locator, indexer: &Indexer) -> bool { - has_leading_content(stmt, locator) || preceded_by_continuation(stmt, indexer, locator) + has_leading_content(stmt.start(), locator) + || preceded_by_continuations(stmt.start(), locator, indexer).is_some() } /// Return `true` if a `Stmt` appears to be part of a multi-statement line, with /// other statements following it. pub fn followed_by_multi_statement_line(stmt: &Stmt, locator: &Locator) -> bool { - has_trailing_content(stmt, locator) + has_trailing_content(stmt.end(), locator) } /// Return `true` if a `Stmt` is a docstring. @@ -1165,11 +1211,11 @@ pub fn is_docstring_stmt(stmt: &Stmt) -> bool { } } -#[derive(Default)] /// A simple representation of a call's positional and keyword arguments. +#[derive(Default)] pub struct SimpleCallArgs<'a> { - pub args: Vec<&'a Expr>, - pub kwargs: FxHashMap<&'a str, &'a Expr>, + args: Vec<&'a Expr>, + kwargs: FxHashMap<&'a str, &'a Expr>, } impl<'a> SimpleCallArgs<'a> { @@ -1213,6 +1259,16 @@ impl<'a> SimpleCallArgs<'a> { self.args.len() + self.kwargs.len() } + /// Return the number of positional arguments. + pub fn num_args(&self) -> usize { + self.args.len() + } + + /// Return the number of keyword arguments. + pub fn num_kwargs(&self) -> usize { + self.kwargs.len() + } + /// Return `true` if there are no positional or keyword arguments. pub fn is_empty(&self) -> bool { self.len() == 0 @@ -1507,7 +1563,7 @@ mod tests { use anyhow::Result; use ruff_text_size::{TextLen, TextRange, TextSize}; - use rustpython_ast::{CmpOp, Expr, Stmt}; + use rustpython_ast::{CmpOp, Expr, Ranged, Stmt}; use rustpython_parser::ast::Suite; use rustpython_parser::Parse; @@ -1523,25 +1579,25 @@ mod tests { let program = Suite::parse(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert!(!has_trailing_content(stmt, &locator)); + assert!(!has_trailing_content(stmt.end(), &locator)); let contents = "x = 1; y = 2"; let program = Suite::parse(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert!(has_trailing_content(stmt, &locator)); + assert!(has_trailing_content(stmt.end(), &locator)); let contents = "x = 1 "; let program = Suite::parse(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert!(!has_trailing_content(stmt, &locator)); + assert!(!has_trailing_content(stmt.end(), &locator)); let contents = "x = 1 # Comment"; let program = Suite::parse(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert!(!has_trailing_content(stmt, &locator)); + assert!(!has_trailing_content(stmt.end(), &locator)); let contents = r#" x = 1 @@ -1551,7 +1607,7 @@ y = 2 let program = Suite::parse(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert!(!has_trailing_content(stmt, &locator)); + assert!(!has_trailing_content(stmt.end(), &locator)); Ok(()) } From 64bd955c582ca890ef1ebb72f0afdafe6f167340 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 19 Jun 2023 22:22:32 -0400 Subject: [PATCH 116/447] Remove continuations before trailing semicolons (#5199) ## Summary Closes #4828. --- .../test/fixtures/pycodestyle/E70.py | 3 +++ crates/ruff/src/checkers/tokens.rs | 2 +- .../pycodestyle/rules/compound_statements.rs | 16 ++++++++++--- ...ules__pycodestyle__tests__E702_E70.py.snap | 2 ++ ...ules__pycodestyle__tests__E703_E70.py.snap | 23 ++++++++++++++++--- 5 files changed, 39 insertions(+), 7 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pycodestyle/E70.py b/crates/ruff/resources/test/fixtures/pycodestyle/E70.py index 2fa6fa4813..9b9fde824c 100644 --- a/crates/ruff/resources/test/fixtures/pycodestyle/E70.py +++ b/crates/ruff/resources/test/fixtures/pycodestyle/E70.py @@ -63,3 +63,6 @@ class Foo: #: E702:2:4 while 1: 1;... +#: E703:2:1 +0\ +; diff --git a/crates/ruff/src/checkers/tokens.rs b/crates/ruff/src/checkers/tokens.rs index 4f71610a7c..07bc525a4a 100644 --- a/crates/ruff/src/checkers/tokens.rs +++ b/crates/ruff/src/checkers/tokens.rs @@ -141,7 +141,7 @@ pub(crate) fn check_tokens( // E701, E702, E703 if enforce_compound_statements { diagnostics.extend( - pycodestyle::rules::compound_statements(tokens, settings) + pycodestyle::rules::compound_statements(tokens, indexer, locator, settings) .into_iter() .filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())), ); diff --git a/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs b/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs index fd9b57b6e9..b484d9cd09 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs @@ -5,6 +5,8 @@ use rustpython_parser::Tok; use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers; +use ruff_python_ast::source_code::{Indexer, Locator}; use crate::registry::Rule; use crate::settings::Settings; @@ -97,7 +99,12 @@ impl AlwaysAutofixableViolation for UselessSemicolon { } /// E701, E702, E703 -pub(crate) fn compound_statements(lxr: &[LexResult], settings: &Settings) -> Vec { +pub(crate) fn compound_statements( + lxr: &[LexResult], + indexer: &Indexer, + locator: &Locator, + settings: &Settings, +) -> Vec { let mut diagnostics = vec![]; // Track the last seen instance of a variety of tokens. @@ -164,8 +171,11 @@ pub(crate) fn compound_statements(lxr: &[LexResult], settings: &Settings) -> Vec let mut diagnostic = Diagnostic::new(UselessSemicolon, TextRange::new(start, end)); if settings.rules.should_fix(Rule::UselessSemicolon) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::deletion(start, end))); + diagnostic.set_fix(Fix::automatic(Edit::deletion( + helpers::preceded_by_continuations(start, locator, indexer) + .unwrap_or(start), + end, + ))); }; diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E702_E70.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E702_E70.py.snap index 20d62cd44b..a0e09543c1 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E702_E70.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E702_E70.py.snap @@ -67,6 +67,8 @@ E70.py:65:4: E702 Multiple statements on one line (semicolon) 64 | while 1: 65 | 1;... | ^ E702 +66 | #: E703:2:1 +67 | 0\ | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E703_E70.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E703_E70.py.snap index 6e5b240773..a612503351 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E703_E70.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E703_E70.py.snap @@ -12,7 +12,7 @@ E70.py:10:13: E703 [*] Statement ends with an unnecessary semicolon | = help: Remove unnecessary semicolon -ℹ Suggested fix +ℹ Fix 7 7 | #: E702:1:17 8 8 | import bdist_egg; bdist_egg.write_safety_flag(cmd.egg_info, safe) 9 9 | #: E703:1:13 @@ -33,7 +33,7 @@ E70.py:12:23: E703 [*] Statement ends with an unnecessary semicolon | = help: Remove unnecessary semicolon -ℹ Suggested fix +ℹ Fix 9 9 | #: E703:1:13 10 10 | import shlex; 11 11 | #: E702:1:9 E703:1:23 @@ -54,7 +54,7 @@ E70.py:25:14: E703 [*] Statement ends with an unnecessary semicolon | = help: Remove unnecessary semicolon -ℹ Suggested fix +ℹ Fix 22 22 | while all is round: 23 23 | def f(x): return 2*x 24 24 | #: E704:1:8 E702:1:11 E703:1:14 @@ -64,4 +64,21 @@ E70.py:25:14: E703 [*] Statement ends with an unnecessary semicolon 27 27 | if True: lambda a: b 28 28 | #: E701:1:10 +E70.py:68:1: E703 [*] Statement ends with an unnecessary semicolon + | +66 | #: E703:2:1 +67 | 0\ +68 | ; + | ^ E703 + | + = help: Remove unnecessary semicolon + +ℹ Fix +64 64 | while 1: +65 65 | 1;... +66 66 | #: E703:2:1 +67 |-0\ +68 |-; + 67 |+0 + From 62aa77df311c913295a1ba0289d8a6e6ce7ef6de Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Mon, 19 Jun 2023 21:57:24 -0500 Subject: [PATCH 117/447] Fix corner case involving terminal backslash after fixing `W293` (#5172) ## Summary Fixes #4404. Consider this file: ```python if True: x = 1; \ ``` The current implementation of W293 removes the 3 spaces on line 2. This fix changes the file to: ```python if True: x = 1; \ ``` A file can't end in a `\`, according to Python's [lexical analysis](https://docs.python.org/3/reference/lexical_analysis.html), so subsequent iterations of the autofixer fail (the AST-based ones specifically, since they depend on a valid syntax tree and get re-parsed). This patch examines the line before the line checked in `W293`. If its first non-whitespace character is a `\`, the patch will extend the diagnostic's fix range to all whitespace up until the previous line's *second* non-whitespace character; that is, it deletes all spaces and potential `\`s up until the next non-whitespace character on the previous line. ## Test Plan Ran `cargo run -p ruff_cli -- ~/Downloads/aa.py --fix --select W293,D100 --no-cache` against the above file. This resulted in: ``` /Users/evan/Downloads/aa.py:1:1: D100 Missing docstring in public module Found 2 errors (1 fixed, 1 remaining). ``` The file's contents, after the fix: ```python if True: x = 1; ``` The `\` was removed, leaving the terminal space. The space should be handled by `Rule::TrailingWhitespace`, not `BlankLineWithWhitespace`. --- crates/ruff/src/checkers/physical_lines.rs | 7 ++++--- .../pycodestyle/rules/trailing_whitespace.rs | 21 ++++++++++++++----- ...ules__pycodestyle__tests__W291_W29.py.snap | 6 +++--- ...ules__pycodestyle__tests__W293_W29.py.snap | 2 +- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/crates/ruff/src/checkers/physical_lines.rs b/crates/ruff/src/checkers/physical_lines.rs index 8934a75762..9882bc86ef 100644 --- a/crates/ruff/src/checkers/physical_lines.rs +++ b/crates/ruff/src/checkers/physical_lines.rs @@ -1,7 +1,7 @@ //! Lint rules based on checking physical lines. +use std::path::Path; use ruff_text_size::TextSize; -use std::path::Path; use ruff_diagnostics::Diagnostic; use ruff_python_ast::source_code::{Indexer, Locator, Stylist}; @@ -146,7 +146,7 @@ pub(crate) fn check_physical_lines( } if enforce_trailing_whitespace || enforce_blank_line_contains_whitespace { - if let Some(diagnostic) = trailing_whitespace(&line, settings) { + if let Some(diagnostic) = trailing_whitespace(&line, locator, indexer, settings) { diagnostics.push(diagnostic); } } @@ -185,9 +185,10 @@ pub(crate) fn check_physical_lines( #[cfg(test)] mod tests { + use std::path::Path; + use rustpython_parser::lexer::lex; use rustpython_parser::Mode; - use std::path::Path; use ruff_python_ast::source_code::{Indexer, Locator, Stylist}; diff --git a/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs b/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs index ff7082c9c7..79452236c4 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs @@ -2,6 +2,8 @@ use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers; +use ruff_python_ast::source_code::{Indexer, Locator}; use ruff_python_whitespace::Line; use crate::registry::Rule; @@ -72,7 +74,12 @@ impl AlwaysAutofixableViolation for BlankLineWithWhitespace { } /// W291, W293 -pub(crate) fn trailing_whitespace(line: &Line, settings: &Settings) -> Option { +pub(crate) fn trailing_whitespace( + line: &Line, + locator: &Locator, + indexer: &Indexer, + settings: &Settings, +) -> Option { let whitespace_len: TextSize = line .chars() .rev() @@ -86,16 +93,20 @@ pub(crate) fn trailing_whitespace(line: &Line, settings: &Settings) -> Option Date: Mon, 19 Jun 2023 22:59:51 -0400 Subject: [PATCH 118/447] Use a consistent argument ordering for `Indexer` (#5200) --- crates/ruff/src/autofix/edits.rs | 2 +- crates/ruff/src/checkers/tokens.rs | 8 ++++---- .../ruff/src/rules/eradicate/rules/commented_out_code.rs | 2 +- .../src/rules/flake8_pyi/rules/type_comment_in_stub.rs | 2 +- crates/ruff/src/rules/flake8_todos/rules/todos.rs | 2 +- .../rules/runtime_import_in_type_checking_block.rs | 2 +- .../rules/typing_only_runtime_import.rs | 2 +- .../src/rules/pycodestyle/rules/compound_statements.rs | 2 +- crates/ruff/src/rules/pyflakes/rules/unused_import.rs | 2 +- .../rules/pyupgrade/rules/unnecessary_builtin_import.rs | 2 +- .../rules/pyupgrade/rules/unnecessary_future_import.rs | 2 +- 11 files changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/ruff/src/autofix/edits.rs b/crates/ruff/src/autofix/edits.rs index 285d42c258..aeb9c8114a 100644 --- a/crates/ruff/src/autofix/edits.rs +++ b/crates/ruff/src/autofix/edits.rs @@ -60,8 +60,8 @@ pub(crate) fn remove_unused_imports<'a>( stmt: &Stmt, parent: Option<&Stmt>, locator: &Locator, - indexer: &Indexer, stylist: &Stylist, + indexer: &Indexer, ) -> Result { match codemods::remove_imports(unused_imports, stmt, locator, stylist)? { None => Ok(delete_stmt(stmt, parent, locator, indexer)), diff --git a/crates/ruff/src/checkers/tokens.rs b/crates/ruff/src/checkers/tokens.rs index 07bc525a4a..812bf455a2 100644 --- a/crates/ruff/src/checkers/tokens.rs +++ b/crates/ruff/src/checkers/tokens.rs @@ -109,7 +109,7 @@ pub(crate) fn check_tokens( // ERA001 if enforce_commented_out_code { diagnostics.extend(eradicate::rules::commented_out_code( - indexer, locator, settings, + locator, indexer, settings, )); } @@ -141,7 +141,7 @@ pub(crate) fn check_tokens( // E701, E702, E703 if enforce_compound_statements { diagnostics.extend( - pycodestyle::rules::compound_statements(tokens, indexer, locator, settings) + pycodestyle::rules::compound_statements(tokens, locator, indexer, settings) .into_iter() .filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())), ); @@ -187,7 +187,7 @@ pub(crate) fn check_tokens( // PYI033 if enforce_type_comment_in_stub && is_stub { - diagnostics.extend(flake8_pyi::rules::type_comment_in_stub(indexer, locator)); + diagnostics.extend(flake8_pyi::rules::type_comment_in_stub(locator, indexer)); } // TD001, TD002, TD003, TD004, TD005, TD006, TD007 @@ -204,7 +204,7 @@ pub(crate) fn check_tokens( .collect(); diagnostics.extend( - flake8_todos::rules::todos(&todo_comments, indexer, locator, settings) + flake8_todos::rules::todos(&todo_comments, locator, indexer, settings) .into_iter() .filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())), ); diff --git a/crates/ruff/src/rules/eradicate/rules/commented_out_code.rs b/crates/ruff/src/rules/eradicate/rules/commented_out_code.rs index 1e8aec5126..7864eb99bf 100644 --- a/crates/ruff/src/rules/eradicate/rules/commented_out_code.rs +++ b/crates/ruff/src/rules/eradicate/rules/commented_out_code.rs @@ -48,8 +48,8 @@ fn is_standalone_comment(line: &str) -> bool { /// ERA001 pub(crate) fn commented_out_code( - indexer: &Indexer, locator: &Locator, + indexer: &Indexer, settings: &Settings, ) -> Vec { let mut diagnostics = vec![]; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs index a4ca3bbc14..9d5dc3d1df 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs @@ -34,7 +34,7 @@ impl Violation for TypeCommentInStub { } /// PYI033 -pub(crate) fn type_comment_in_stub(indexer: &Indexer, locator: &Locator) -> Vec { +pub(crate) fn type_comment_in_stub(locator: &Locator, indexer: &Indexer) -> Vec { let mut diagnostics = vec![]; for range in indexer.comment_ranges() { diff --git a/crates/ruff/src/rules/flake8_todos/rules/todos.rs b/crates/ruff/src/rules/flake8_todos/rules/todos.rs index b032c111fd..ce6c1ee9fa 100644 --- a/crates/ruff/src/rules/flake8_todos/rules/todos.rs +++ b/crates/ruff/src/rules/flake8_todos/rules/todos.rs @@ -236,8 +236,8 @@ static ISSUE_LINK_REGEX_SET: Lazy = Lazy::new(|| { pub(crate) fn todos( todo_comments: &[TodoComment], - indexer: &Indexer, locator: &Locator, + indexer: &Indexer, settings: &Settings, ) -> Vec { let mut diagnostics: Vec = vec![]; diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs index 6863584901..da9795bec7 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs @@ -211,8 +211,8 @@ fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result stmt, parent, checker.locator, - checker.indexer, checker.stylist, + checker.indexer, )?; // Step 2) Add the import to the top-level. diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index 34e6eccbdf..5d1ee695bd 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -435,8 +435,8 @@ fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result stmt, parent, checker.locator, - checker.indexer, checker.stylist, + checker.indexer, )?; // Step 2) Add the import to a `TYPE_CHECKING` block. diff --git a/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs b/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs index b484d9cd09..4192e84db3 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs @@ -101,8 +101,8 @@ impl AlwaysAutofixableViolation for UselessSemicolon { /// E701, E702, E703 pub(crate) fn compound_statements( lxr: &[LexResult], - indexer: &Indexer, locator: &Locator, + indexer: &Indexer, settings: &Settings, ) -> Vec { let mut diagnostics = vec![]; diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs index ca76ccfda2..4ad8f1c719 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs @@ -234,8 +234,8 @@ fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result stmt, parent, checker.locator, - checker.indexer, checker.stylist, + checker.indexer, )?; Ok(Fix::automatic(edit).isolate(checker.isolation(parent))) } diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs index 070a1c4aaa..b66ed7eaaf 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs @@ -137,8 +137,8 @@ pub(crate) fn unnecessary_builtin_import( stmt, parent, checker.locator, - checker.indexer, checker.stylist, + checker.indexer, )?; Ok(Fix::suggested(edit).isolate(checker.isolation(parent))) }); diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs index 97eb9e1269..14894f2626 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs @@ -122,8 +122,8 @@ pub(crate) fn unnecessary_future_import(checker: &mut Checker, stmt: &Stmt, name stmt, parent, checker.locator, - checker.indexer, checker.stylist, + checker.indexer, )?; Ok(Fix::suggested(edit).isolate(checker.isolation(parent))) }); From 4cc3cdba16846955a1b7a9f4abf7f83fddd26359 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 19 Jun 2023 23:21:08 -0400 Subject: [PATCH 119/447] Use some more wildcard imports in rules (#5201) --- crates/ruff/src/rules/flake8_gettext/rules/mod.rs | 2 +- crates/ruff/src/rules/flake8_logging_format/rules/mod.rs | 2 +- crates/ruff/src/rules/flake8_use_pathlib/rules/mod.rs | 2 +- crates/ruff/src/rules/pylint/rules/mod.rs | 4 ++-- crates/ruff/src/rules/ruff/rules/mod.rs | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/ruff/src/rules/flake8_gettext/rules/mod.rs b/crates/ruff/src/rules/flake8_gettext/rules/mod.rs index 833309bd21..490011e5fb 100644 --- a/crates/ruff/src/rules/flake8_gettext/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_gettext/rules/mod.rs @@ -1,6 +1,6 @@ pub(crate) use f_string_in_gettext_func_call::*; pub(crate) use format_in_gettext_func_call::*; -pub(crate) use is_gettext_func_call::is_gettext_func_call; +pub(crate) use is_gettext_func_call::*; pub(crate) use printf_in_gettext_func_call::*; mod f_string_in_gettext_func_call; diff --git a/crates/ruff/src/rules/flake8_logging_format/rules/mod.rs b/crates/ruff/src/rules/flake8_logging_format/rules/mod.rs index c0cce2dd57..94db04d8df 100644 --- a/crates/ruff/src/rules/flake8_logging_format/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_logging_format/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use logging_call::logging_call; +pub(crate) use logging_call::*; mod logging_call; diff --git a/crates/ruff/src/rules/flake8_use_pathlib/rules/mod.rs b/crates/ruff/src/rules/flake8_use_pathlib/rules/mod.rs index 33eac782e4..dcf45f615c 100644 --- a/crates/ruff/src/rules/flake8_use_pathlib/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_use_pathlib/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use replaceable_by_pathlib::replaceable_by_pathlib; +pub(crate) use replaceable_by_pathlib::*; mod replaceable_by_pathlib; diff --git a/crates/ruff/src/rules/pylint/rules/mod.rs b/crates/ruff/src/rules/pylint/rules/mod.rs index 1047bad813..dbbca73e1a 100644 --- a/crates/ruff/src/rules/pylint/rules/mod.rs +++ b/crates/ruff/src/rules/pylint/rules/mod.rs @@ -11,7 +11,7 @@ pub(crate) use comparison_with_itself::*; pub(crate) use continue_in_finally::*; pub(crate) use duplicate_bases::*; pub(crate) use global_statement::*; -pub(crate) use global_variable_not_assigned::GlobalVariableNotAssigned; +pub(crate) use global_variable_not_assigned::*; pub(crate) use import_self::*; pub(crate) use invalid_all_format::*; pub(crate) use invalid_all_object::*; @@ -26,7 +26,7 @@ pub(crate) use magic_value_comparison::*; pub(crate) use manual_import_from::*; pub(crate) use named_expr_without_context::*; pub(crate) use nested_min_max::*; -pub(crate) use nonlocal_without_binding::NonlocalWithoutBinding; +pub(crate) use nonlocal_without_binding::*; pub(crate) use property_with_parameters::*; pub(crate) use redefined_loop_name::*; pub(crate) use repeated_isinstance_calls::*; diff --git a/crates/ruff/src/rules/ruff/rules/mod.rs b/crates/ruff/src/rules/ruff/rules/mod.rs index c75237d500..6ec0eda590 100644 --- a/crates/ruff/src/rules/ruff/rules/mod.rs +++ b/crates/ruff/src/rules/ruff/rules/mod.rs @@ -4,7 +4,7 @@ pub(crate) use collection_literal_concatenation::*; pub(crate) use explicit_f_string_type_conversion::*; pub(crate) use function_call_in_dataclass_default::*; pub(crate) use implicit_optional::*; -pub(crate) use invalid_pyproject_toml::InvalidPyprojectToml; +pub(crate) use invalid_pyproject_toml::*; pub(crate) use mutable_class_default::*; pub(crate) use mutable_dataclass_default::*; pub(crate) use pairwise_over_zipped::*; From 5c5d2815af091f8eb2e2463d7faf597b208e5160 Mon Sep 17 00:00:00 2001 From: konstin Date: Tue, 20 Jun 2023 08:07:30 +0200 Subject: [PATCH 120/447] Document gitignore (#5191) This docs-only change adds explanations to all custom .gitignore entries --- .gitignore | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 562d1b4c5a..57a5b8f8ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,25 @@ +# Benchmarking cpython (CONTRIBUTING.md) crates/ruff/resources/test/cpython +# generate_mkdocs.py mkdocs.yml .overrides +# check_ecosystem.py ruff-old github_search*.jsonl +# update_schemastore.py schemastore +# `maturin develop` and ecosystem_all_check.sh .venv* +# Formatter debugging (crates/ruff_python_formatter/README.md) scratch.py +# Created by `perf` (CONTRIBUTING.md) perf.data perf.data.old +# Created by `flamegraph` (CONTRIBUTING.md) flamegraph.svg -# Additional target directories that don't invalidate the main compile cache when changing linker settings +# Additional target directories that don't invalidate the main compile cache when changing linker settings, +# e.g. `CARGO_TARGET_DIR=target-maturin maturin build --release --strip` or +# `CARGO_TARGET_DIR=target-llvm-lines RUSTFLAGS="-Csymbol-mangling-version=v0" cargo llvm-lines -p ruff --lib` /target* ### From dfb04e679e81cd0c0236d12e705e75ef5a7fa318 Mon Sep 17 00:00:00 2001 From: Logan Hunt <39638017+dosisod@users.noreply.github.com> Date: Mon, 19 Jun 2023 23:47:01 -0700 Subject: [PATCH 121/447] Small binary size optimization (#5203) --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index b1bd3edc84..23414c705f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ toml = { version = "0.7.2" } [profile.release] lto = "fat" +codegen-units = 1 [profile.dev.package.insta] opt-level = 3 From 773e79b481278ae74f225db1ae2927017716bd15 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Tue, 20 Jun 2023 10:25:08 +0100 Subject: [PATCH 122/447] basic formatting for ExprDict (#5167) --- .../test/fixtures/ruff/expression/dict.py | 50 ++++++ .../src/comments/placement.rs | 73 ++++++++- .../src/expression/expr_dict.rs | 101 +++++++++++- ...er__tests__black_test__collections_py.snap | 32 ++-- ...atter__tests__black_test__comments_py.snap | 36 +++-- ...ter__tests__black_test__expression_py.snap | 144 +++++++++++------- ...atter__tests__black_test__fmtonoff_py.snap | 47 +++--- ...atter__tests__black_test__function_py.snap | 38 ++--- ...lack_test__function_trailing_comma_py.snap | 51 ++++--- ...ests__black_test__power_op_spacing_py.snap | 10 +- ...t__trailing_comma_optional_parens3_py.snap | 16 +- ...sts__ruff_test__expression__binary_py.snap | 7 +- ...tests__ruff_test__expression__dict_py.snap | 117 ++++++++++++++ crates/ruff_python_formatter/src/trivia.rs | 4 + 14 files changed, 556 insertions(+), 170 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/dict.py create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__dict_py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/dict.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/dict.py new file mode 100644 index 0000000000..f8cf5a9351 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/dict.py @@ -0,0 +1,50 @@ +# before +{ # open + key# key + : # colon + value# value +} # close +# after + + +{**d} + +{**a, # leading +** # middle +b # trailing +} + +{ +** # middle with single item +b +} + +{ + # before + ** # between + b, +} + +{ + **a # comment before preceeding node's comma + , + # before + ** # between + b, +} + +{} + +{1:2,} + +{1:2, + 3:4,} + +{asdfsadfalsdkjfhalsdkjfhalskdjfhlaksjdfhlaskjdfhlaskjdfhlaksdjfh: 1, adsfadsflasdflasdfasdfasdasdf: 2} + +mapping = { + A: 0.25 * (10.0 / 12), + B: 0.1 * (10.0 / 12), + C: 0.1 * (10.0 / 12), + D: 0.1 * (10.0 / 12), +} diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 2bfe71690a..3c6742508f 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -1,6 +1,6 @@ use crate::comments::visitor::{CommentPlacement, DecoratedComment}; use crate::comments::CommentTextPosition; -use crate::trivia::{SimpleTokenizer, TokenKind}; +use crate::trivia::{SimpleTokenizer, Token, TokenKind}; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::source_code::Locator; use ruff_python_ast::whitespace; @@ -29,6 +29,7 @@ pub(super) fn place_comment<'a>( .or_else(|comment| handle_positional_only_arguments_separator_comment(comment, locator)) .or_else(|comment| handle_trailing_binary_expression_left_or_operator_comment(comment, locator)) .or_else(handle_leading_function_with_decorators_comment) + .or_else(|comment| handle_dict_unpacking_comment(comment, locator)) } /// Handles leading comments in front of a match case or a trailing comment of the `match` statement. @@ -885,6 +886,76 @@ fn handle_leading_function_with_decorators_comment(comment: DecoratedComment) -> } } +/// Handles comments between `**` and the variable name in dict unpacking +/// It attaches these to the appropriate value node +/// +/// ```python +/// { +/// ** # comment between `**` and the variable name +/// value +/// ... +/// } +/// ``` +fn handle_dict_unpacking_comment<'a>( + comment: DecoratedComment<'a>, + locator: &Locator, +) -> CommentPlacement<'a> { + match comment.enclosing_node() { + // TODO: can maybe also add AnyNodeRef::Arguments here, but tricky to test due to + // https://github.com/astral-sh/ruff/issues/5176 + AnyNodeRef::ExprDict(_) => {} + _ => { + return CommentPlacement::Default(comment); + } + }; + + // no node after our comment so we can't be between `**` and the name (node) + let Some(following) = comment.following_node() else { + return CommentPlacement::Default(comment); + }; + + // we look at tokens between the previous node (or the start of the dict) + // and the comment + let preceding_end = match comment.preceding_node() { + Some(preceding) => preceding.end(), + None => comment.enclosing_node().start(), + }; + if preceding_end > comment.slice().start() { + return CommentPlacement::Default(comment); + } + let mut tokens = SimpleTokenizer::new( + locator.contents(), + TextRange::new(preceding_end, comment.slice().start()), + ) + .skip_trivia(); + + // we start from the preceding node but we skip its token + if let Some(first) = tokens.next() { + debug_assert!(matches!( + first, + Token { + kind: TokenKind::LBrace | TokenKind::Comma | TokenKind::Colon, + .. + } + )); + } + + // if the remaining tokens from the previous node is exactly `**`, + // re-assign the comment to the one that follows the stars + let mut count = 0; + for token in tokens { + if token.kind != TokenKind::Star { + return CommentPlacement::Default(comment); + } + count += 1; + } + if count == 2 { + return CommentPlacement::trailing(following, comment); + } + + CommentPlacement::Default(comment) +} + /// Returns `true` if `right` is `Some` and `left` and `right` are referentially equal. fn are_same_optional<'a, T>(left: AnyNodeRef, right: Option) -> bool where diff --git a/crates/ruff_python_formatter/src/expression/expr_dict.rs b/crates/ruff_python_formatter/src/expression/expr_dict.rs index 3ccc6a4dc8..11d1a3ca85 100644 --- a/crates/ruff_python_formatter/src/expression/expr_dict.rs +++ b/crates/ruff_python_formatter/src/expression/expr_dict.rs @@ -1,21 +1,108 @@ -use crate::comments::Comments; +use crate::comments::{dangling_node_comments, leading_comments, Comments}; +use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; -use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; +use crate::prelude::*; +use crate::trivia::Token; +use crate::trivia::{first_non_trivia_token, TokenKind}; +use crate::USE_MAGIC_TRAILING_COMMA; +use crate::{FormatNodeRule, PyFormatter}; +use ruff_formatter::format_args; use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::ExprDict; +use ruff_python_ast::prelude::Ranged; +use rustpython_parser::ast::{Expr, ExprDict}; #[derive(Default)] pub struct FormatExprDict; +struct KeyValuePair<'a> { + key: &'a Option, + value: &'a Expr, +} + +impl Format> for KeyValuePair<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + if let Some(key) = self.key { + write!( + f, + [group(&format_args![ + key.format(), + text(":"), + space(), + self.value.format() + ])] + ) + } else { + let comments = f.context().comments().clone(); + let leading_value_comments = comments.leading_comments(self.value.into()); + write!( + f, + [ + // make sure the leading comments are hoisted past the `**` + leading_comments(leading_value_comments), + group(&format_args![text("**"), self.value.format()]) + ] + ) + } + } +} + impl FormatNodeRule for FormatExprDict { - fn fmt_fields(&self, _item: &ExprDict, f: &mut PyFormatter) -> FormatResult<()> { + fn fmt_fields(&self, item: &ExprDict, f: &mut PyFormatter) -> FormatResult<()> { + let ExprDict { + range: _, + keys, + values, + } = item; + + let last = match &values[..] { + [] => { + return write!( + f, + [ + &text("{"), + block_indent(&dangling_node_comments(item)), + &text("}"), + ] + ); + } + [.., last] => last, + }; + let magic_trailing_comma = USE_MAGIC_TRAILING_COMMA + && matches!( + first_non_trivia_token(last.range().end(), f.context().contents()), + Some(Token { + kind: TokenKind::Comma, + .. + }) + ); + + debug_assert_eq!(keys.len(), values.len()); + + let joined = format_with(|f| { + f.join_with(format_args!(text(","), soft_line_break_or_space())) + .entries( + keys.iter() + .zip(values) + .map(|(key, value)| KeyValuePair { key, value }), + ) + .finish() + }); + + let block = if magic_trailing_comma { + block_indent + } else { + soft_block_indent + }; + write!( f, - [not_yet_implemented_custom_text( - "{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}" - )] + [group(&format_args![ + text("{"), + block(&format_args![joined, if_group_breaks(&text(",")),]), + text("}") + ])] ) } diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap index 6a6c2179d6..0b9b054e8a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap @@ -84,7 +84,7 @@ if True: ```diff --- Black +++ Ruff -@@ -1,99 +1,52 @@ +@@ -1,99 +1,56 @@ -import core, time, a +NOT_YET_IMPLEMENTED_StmtImport @@ -143,24 +143,24 @@ if True: - "dddddddddddddddddddddddddddddddddddddddd", + "NOT_YET_IMPLEMENTED_STRING", ] --{ + { - "oneple": (1,), --} ++ "NOT_YET_IMPLEMENTED_STRING": (1,), + } -{"oneple": (1,)} -["ls", "lsoneple/%s" % (foo,)] -x = {"oneple": (1,)} --y = { ++{"NOT_YET_IMPLEMENTED_STRING": (1,)} ++["NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING" % (foo,)] ++x = {"NOT_YET_IMPLEMENTED_STRING": (1,)} + y = { - "oneple": (1,), --} ++ "NOT_YET_IMPLEMENTED_STRING": (1,), + } -assert False, ( - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa wraps %s" - % bar -) -+{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -+{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -+["NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING" % (foo,)] -+x = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -+y = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} +NOT_YET_IMPLEMENTED_StmtAssert # looping over a 1-tuple should also not get wrapped @@ -245,11 +245,15 @@ nested_long_lines = [ (1, 2, 3), "NOT_YET_IMPLEMENTED_STRING", ] -{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} +{ + "NOT_YET_IMPLEMENTED_STRING": (1,), +} +{"NOT_YET_IMPLEMENTED_STRING": (1,)} ["NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING" % (foo,)] -x = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -y = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} +x = {"NOT_YET_IMPLEMENTED_STRING": (1,)} +y = { + "NOT_YET_IMPLEMENTED_STRING": (1,), +} NOT_YET_IMPLEMENTED_StmtAssert # looping over a 1-tuple should also not get wrapped diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap index ec61d99846..5a2dcb790f 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap @@ -114,24 +114,24 @@ async def wat(): # Has many lines. Many, many lines. # Many, many, many lines. -"""Module docstring. -- ++"NOT_YET_IMPLEMENTED_STRING" + -Possibly also many, many lines. -""" -- ++NOT_YET_IMPLEMENTED_StmtImport ++NOT_YET_IMPLEMENTED_StmtImport + -import os.path -import sys - -import a -from b.c import X # some noqa comment -+"NOT_YET_IMPLEMENTED_STRING" - +- -try: - import fast -except ImportError: - import slow as fast -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImport - +- +NOT_YET_IMPLEMENTED_StmtImport +NOT_YET_IMPLEMENTED_StmtImportFrom # some noqa comment @@ -140,7 +140,7 @@ async def wat(): y = 1 ( # some strings -@@ -30,67 +21,46 @@ +@@ -30,67 +21,50 @@ def function(default=None): @@ -168,22 +168,26 @@ async def wat(): # Explains why we use global state. -GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} -+GLOBAL_STATE = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} ++GLOBAL_STATE = { ++ "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(), ++ "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(), ++ "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(), ++} # 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.""" - @@ -264,7 +268,11 @@ def function(default=None): # Explains why we use global state. -GLOBAL_STATE = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} +GLOBAL_STATE = { + "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(), + "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(), + "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(), +} # Another comment! diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap index b2cc26d4f2..bb84670f91 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap @@ -276,7 +276,7 @@ last_call() Name None True -@@ -7,294 +8,236 @@ +@@ -7,226 +8,225 @@ 1 1.0 1j @@ -339,9 +339,6 @@ last_call() -) -{"2.7": dead, "3.7": (long_live or die_hard)} -{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} --{**a, **b, **c} --{"2.7", "3.6", "3.7", "3.8", "3.9", ("4.0" if gilectomy else "3.10")} --({"a": "b"}, (True or False), (+value), "string", b"bytes") or None +NOT_YET_IMPLEMENTED_ExprUnaryOp +NOT_YET_IMPLEMENTED_ExprUnaryOp +NOT_YET_IMPLEMENTED_ExprUnaryOp @@ -363,9 +360,22 @@ last_call() +NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false +NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false +(NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false) -+{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -+{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -+{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} ++{ ++ "NOT_YET_IMPLEMENTED_STRING": dead, ++ "NOT_YET_IMPLEMENTED_STRING": ( ++ NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 ++ ), ++} ++{ ++ "NOT_YET_IMPLEMENTED_STRING": dead, ++ "NOT_YET_IMPLEMENTED_STRING": ( ++ NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 ++ ), ++ **{"NOT_YET_IMPLEMENTED_STRING": verygood}, ++} + {**a, **b, **c} +-{"2.7", "3.6", "3.7", "3.8", "3.9", ("4.0" if gilectomy else "3.10")} +-({"a": "b"}, (True or False), (+value), "string", b"bytes") or None +{ + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", @@ -392,6 +402,11 @@ last_call() - *a, 4, 5, +-] +-[ +- 4, +- *a, +- 5, + 6, + 7, + 8, @@ -400,11 +415,6 @@ last_call() + (NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2), + (NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2), ] --[ -- 4, -- *a, -- 5, --] +[1, 2, 3] +[NOT_YET_IMPLEMENTED_ExprStarred] +[NOT_YET_IMPLEMENTED_ExprStarred] @@ -495,16 +505,11 @@ last_call() +NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] +NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] +NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -+{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} { - k: v - for k, v in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, ++ "NOT_YET_IMPLEMENTED_STRING": dead, ++ "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2, } -Python3 > Python2 > COBOL -Life is Life @@ -541,6 +546,14 @@ last_call() -] -very_long_variable_name_filters: t.List[ - t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], ++{ ++ "NOT_YET_IMPLEMENTED_STRING", ++ "NOT_YET_IMPLEMENTED_STRING", ++ "NOT_YET_IMPLEMENTED_STRING", ++ "NOT_YET_IMPLEMENTED_STRING", ++ "NOT_YET_IMPLEMENTED_STRING", ++ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, ++} +[ + 1, + 2, @@ -602,21 +615,26 @@ 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_ExprStarred,) + { - "id": "1", - "type": "type", - "started_at": now(), - "ended_at": now() + timedelta(days=10), - "priority": 1, - "import_session_id": 1, -- **kwargs, --} -+(i for i in []) -+(i for i in []) -+(i for i in []) -+(i for i in []) -+(NOT_YET_IMPLEMENTED_ExprStarred,) -+{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} ++ "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", ++ "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", ++ "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(), ++ "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(), ++ "NOT_YET_IMPLEMENTED_STRING": 1, ++ "NOT_YET_IMPLEMENTED_STRING": 1, + **kwargs, + } a = (1,) b = (1,) c = 1 @@ -659,17 +677,14 @@ last_call() ) -Ø = set() -authors.łukasz.say_thanks() --mapping = { -- A: 0.25 * (10.0 / 12), -- B: 0.1 * (10.0 / 12), -- C: 0.1 * (10.0 / 12), -- D: 0.1 * (10.0 / 12), --} +result = NOT_IMPLEMENTED_call() +result = NOT_IMPLEMENTED_call() +Ø = NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call() -+mapping = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} + mapping = { + A: 0.25 * (10.0 / 12), + B: 0.1 * (10.0 / 12), +@@ -236,65 +236,35 @@ def gen(): @@ -699,14 +714,6 @@ last_call() -for (x,) in (1,), (2,), (3,): - ... -for y in (): -- ... --for z in (i for i in (1, 2, 3)): -- ... --for i in call(): -- ... --for j in 1 + (2 + 3): -- ... --while this and that: +NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call() @@ -720,6 +727,14 @@ last_call() +NOT_YET_IMPLEMENTED_StmtFor +while NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: ... +-for z in (i for i in (1, 2, 3)): +- ... +-for i in call(): +- ... +-for j in 1 + (2 + 3): +- ... +-while this and that: +- ... -for ( - addr_family, - addr_type, @@ -758,7 +773,7 @@ last_call() return True if ( aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -@@ -327,24 +270,44 @@ +@@ -327,24 +297,44 @@ ): return True if ( @@ -815,7 +830,7 @@ last_call() ): return True ( -@@ -363,8 +326,9 @@ +@@ -363,8 +353,9 @@ bbbb >> bbbb * bbbb ( aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa @@ -880,9 +895,20 @@ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false (NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false) -{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} +{ + "NOT_YET_IMPLEMENTED_STRING": dead, + "NOT_YET_IMPLEMENTED_STRING": ( + NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 + ), +} +{ + "NOT_YET_IMPLEMENTED_STRING": dead, + "NOT_YET_IMPLEMENTED_STRING": ( + NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 + ), + **{"NOT_YET_IMPLEMENTED_STRING": verygood}, +} +{**a, **b, **c} { "NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING", @@ -988,7 +1014,10 @@ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} +{ + "NOT_YET_IMPLEMENTED_STRING": dead, + "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2, +} { "NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING", @@ -1019,7 +1048,15 @@ SomeName (i for i in []) (i for i in []) (NOT_YET_IMPLEMENTED_ExprStarred,) -{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} +{ + "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(), + "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(), + "NOT_YET_IMPLEMENTED_STRING": 1, + "NOT_YET_IMPLEMENTED_STRING": 1, + **kwargs, +} a = (1,) b = (1,) c = 1 @@ -1039,7 +1076,12 @@ result = NOT_IMPLEMENTED_call() result = NOT_IMPLEMENTED_call() Ø = NOT_IMPLEMENTED_call() NOT_IMPLEMENTED_call() -mapping = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} +mapping = { + A: 0.25 * (10.0 / 12), + B: 0.1 * (10.0 / 12), + C: 0.1 * (10.0 / 12), + D: 0.1 * (10.0 / 12), +} def gen(): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap index 183ed478e1..2dcad260b6 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap @@ -222,7 +222,7 @@ d={'a':1, # Comment 1 # Comment 2 -@@ -18,109 +16,112 @@ +@@ -18,30 +16,51 @@ # fmt: off def func_no_args(): @@ -284,7 +284,7 @@ d={'a':1, + a=1, + b=(), + c=[], -+ d={NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, ++ d={}, + e=True, + f=NOT_YET_IMPLEMENTED_ExprUnaryOp, + g=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, @@ -296,11 +296,9 @@ d={'a':1, def spaces_types( - a: int = 1, - b: tuple = (), +@@ -50,77 +69,62 @@ c: list = [], -- d: dict = {}, -+ d: dict = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, + d: dict = {}, e: bool = True, - f: int = -1, - g: int = 1 if False else 2, @@ -319,11 +317,11 @@ d={'a':1, ... --something = { -- # fmt: off + something = { + # fmt: off - key: 'value', --} -+something = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} ++ key: "NOT_YET_IMPLEMENTED_STRING", + } def subscriptlist(): @@ -394,7 +392,7 @@ d={'a':1, # fmt: off # hey, that won't work -@@ -130,13 +131,15 @@ +@@ -130,13 +134,15 @@ def on_and_off_broken(): @@ -415,7 +413,7 @@ d={'a':1, # fmt: on # fmt: off # ...but comments still get reformatted even though they should not be -@@ -145,80 +148,21 @@ +@@ -145,80 +151,21 @@ def long_lines(): if True: @@ -427,11 +425,13 @@ d={'a':1, - implicit_default=True, - ) - ) -- # fmt: off ++ NOT_IMPLEMENTED_call() + # fmt: off - a = ( - unnecessary_bracket() - ) -- # fmt: on ++ a = NOT_IMPLEMENTED_call() + # fmt: on - _type_comment_re = re.compile( - r""" - ^ @@ -452,11 +452,9 @@ d={'a':1, - ) - $ - """, -+ NOT_IMPLEMENTED_call() - # fmt: off +- # fmt: off - re.MULTILINE|re.VERBOSE -+ a = NOT_IMPLEMENTED_call() - # fmt: on +- # fmt: on - ) + _type_comment_re = NOT_IMPLEMENTED_call() @@ -503,7 +501,7 @@ d={'a':1, -d={'a':1, - 'b':2} +l = [1, 2, 3] -+d = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} ++d = {"NOT_YET_IMPLEMENTED_STRING": 1, "NOT_YET_IMPLEMENTED_STRING": 2} ``` ## Ruff Output @@ -563,7 +561,7 @@ def spaces( a=1, b=(), c=[], - d={NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, + d={}, e=True, f=NOT_YET_IMPLEMENTED_ExprUnaryOp, g=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, @@ -578,7 +576,7 @@ def spaces_types( a: int = 1, b: tuple = (), c: list = [], - d: dict = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, + d: dict = {}, e: bool = True, f: int = NOT_YET_IMPLEMENTED_ExprUnaryOp, g: int = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, @@ -592,7 +590,10 @@ def spaces2(result=NOT_IMPLEMENTED_call()): ... -something = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} +something = { + # fmt: off + key: "NOT_YET_IMPLEMENTED_STRING", +} def subscriptlist(): @@ -676,7 +677,7 @@ NOT_IMPLEMENTED_call() NOT_YET_IMPLEMENTED_ExprYield # No formatting to the end of the file l = [1, 2, 3] -d = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} +d = {"NOT_YET_IMPLEMENTED_STRING": 1, "NOT_YET_IMPLEMENTED_STRING": 2} ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap index 9f5f58ae76..a82537bcef 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap @@ -112,21 +112,21 @@ 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 def func_no_args(): -@@ -14,135 +13,87 @@ +@@ -14,39 +13,46 @@ b c if True: @@ -177,7 +177,7 @@ def __await__(): return (yield) + a=1, + b=(), + c=[], -+ d={NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, ++ d={}, + e=True, + f=NOT_YET_IMPLEMENTED_ExprUnaryOp, + g=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, @@ -189,11 +189,9 @@ def __await__(): return (yield) def spaces_types( - a: int = 1, - b: tuple = (), +@@ -55,71 +61,27 @@ c: list = [], -- d: dict = {}, -+ d: dict = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, + d: dict = {}, e: bool = True, - f: int = -1, - g: int = 1 if False else 2, @@ -273,16 +271,7 @@ def __await__(): return (yield) def trailing_comma(): -- mapping = { -- A: 0.25 * (10.0 / 12), -- B: 0.1 * (10.0 / 12), -- C: 0.1 * (10.0 / 12), -- D: 0.1 * (10.0 / 12), -- } -+ mapping = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} - - - def f( +@@ -135,14 +97,8 @@ a, **kwargs, ) -> A: @@ -350,7 +339,7 @@ def spaces( a=1, b=(), c=[], - d={NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, + d={}, e=True, f=NOT_YET_IMPLEMENTED_ExprUnaryOp, g=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, @@ -365,7 +354,7 @@ def spaces_types( a: int = 1, b: tuple = (), c: list = [], - d: dict = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, + d: dict = {}, e: bool = True, f: int = NOT_YET_IMPLEMENTED_ExprUnaryOp, g: int = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, @@ -391,7 +380,12 @@ def long_lines(): def trailing_comma(): - mapping = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} + mapping = { + A: 0.25 * (10.0 / 12), + B: 0.1 * (10.0 / 12), + C: 0.1 * (10.0 / 12), + D: 0.1 * (10.0 / 12), + } def f( diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap index 3e472e8729..89e72cdcc0 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap @@ -74,30 +74,27 @@ some_module.some_function( ```diff --- Black +++ Ruff -@@ -1,9 +1,7 @@ - def f( +@@ -2,7 +2,7 @@ a, ): -- d = { + d = { - "key": "value", -- } -+ d = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} ++ "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", + } tup = (1,) - -@@ -11,10 +9,7 @@ - a, +@@ -12,8 +12,8 @@ b, ): -- d = { + d = { - "key": "value", - "key2": "value2", -- } -+ d = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} ++ "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", ++ "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", + } tup = ( 1, - 2, -@@ -24,46 +19,15 @@ +@@ -24,45 +24,18 @@ def f( a: int = 1, ): @@ -136,7 +133,8 @@ some_module.some_function( -def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> ( - Set["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"] -): -- json = { ++def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]: + json = { - "k": { - "k2": { - "k3": [ @@ -144,13 +142,13 @@ some_module.some_function( - ] - } - } -- } -+def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]: -+ json = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} ++ "NOT_YET_IMPLEMENTED_STRING": { ++ "NOT_YET_IMPLEMENTED_STRING": {"NOT_YET_IMPLEMENTED_STRING": [1]}, ++ }, + } - # The type annotation shouldn't get a trailing comma since that would change its type. -@@ -80,35 +44,16 @@ +@@ -80,35 +53,16 @@ pass @@ -198,7 +196,9 @@ some_module.some_function( def f( a, ): - d = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} + d = { + "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", + } tup = (1,) @@ -206,7 +206,10 @@ def f2( a, b, ): - d = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} + d = { + "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", + } tup = ( 1, 2, @@ -224,7 +227,11 @@ def f( def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]: - json = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} + json = { + "NOT_YET_IMPLEMENTED_STRING": { + "NOT_YET_IMPLEMENTED_STRING": {"NOT_YET_IMPLEMENTED_STRING": [1]}, + }, + } # The type annotation shouldn't get a trailing comma since that would change its type. diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap index 4a41269a87..6358e8d7e3 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap @@ -76,12 +76,8 @@ return np.divide( ```diff --- Black +++ Ruff -@@ -8,56 +8,49 @@ - - - def function_dont_replace_spaces(): -- {**a, **b, **c} -+ {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} +@@ -11,53 +11,46 @@ + {**a, **b, **c} -a = 5**~4 @@ -184,7 +180,7 @@ def function_replace_spaces(**kwargs): def function_dont_replace_spaces(): - {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} + {**a, **b, **c} a = 5**NOT_YET_IMPLEMENTED_ExprUnaryOp diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens3_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens3_py.snap index 889aaf9c58..ea49d311ae 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens3_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens3_py.snap @@ -30,10 +30,10 @@ if True: - + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", - "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", - ) % {"reported_username": reported_username, "report_reason": report_reason} -+ return ( -+ NOT_IMPLEMENTED_call() -+ % {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -+ ) ++ return NOT_IMPLEMENTED_call() % { ++ "NOT_YET_IMPLEMENTED_STRING": reported_username, ++ "NOT_YET_IMPLEMENTED_STRING": report_reason, ++ } ``` ## Ruff Output @@ -42,10 +42,10 @@ if True: if True: if True: if True: - return ( - NOT_IMPLEMENTED_call() - % {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} - ) + return NOT_IMPLEMENTED_call() % { + "NOT_YET_IMPLEMENTED_STRING": reported_username, + "NOT_YET_IMPLEMENTED_STRING": report_reason, + } ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap index 47374f6ad8..6f7abbf1a6 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap @@ -92,7 +92,12 @@ aaaaaaaaaaaaaa + ( dddddddddddddddd, eeeeeee, ) -aaaaaaaaaaaaaa + {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} +aaaaaaaaaaaaaa + { + key1: bbbbbbbbbbbbbbbbbbbbbb, + key2: ccccccccccccccccccccc, + key3: dddddddddddddddd, + key4: eeeeeee, +} aaaaaaaaaaaaaa + { bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__dict_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__dict_py.snap new file mode 100644 index 0000000000..2c0cba1259 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__dict_py.snap @@ -0,0 +1,117 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +--- +## Input +```py +# before +{ # open + key# key + : # colon + value# value +} # close +# after + + +{**d} + +{**a, # leading +** # middle +b # trailing +} + +{ +** # middle with single item +b +} + +{ + # before + ** # between + b, +} + +{ + **a # comment before preceeding node's comma + , + # before + ** # between + b, +} + +{} + +{1:2,} + +{1:2, + 3:4,} + +{asdfsadfalsdkjfhalsdkjfhalskdjfhlaksjdfhlaskjdfhlaskjdfhlaksdjfh: 1, adsfadsflasdflasdfasdfasdasdf: 2} + +mapping = { + A: 0.25 * (10.0 / 12), + B: 0.1 * (10.0 / 12), + C: 0.1 * (10.0 / 12), + D: 0.1 * (10.0 / 12), +} +``` + + + +## Output +```py +# before +{ + # open + key: value, # key # colon # value +} # close +# after + + +{**d} + +{ + **a, # leading + **b, # middle # trailing +} + +{ + **b, # middle with single item +} + +{ + # before + **b, # between +} + +{ + **a, # comment before preceeding node's comma + # before + **b, # between +} + +{} + +{ + 1: 2, +} + +{ + 1: 2, + 3: 4, +} + +{ + asdfsadfalsdkjfhalsdkjfhalskdjfhlaksjdfhlaskjdfhlaskjdfhlaksdjfh: 1, + adsfadsflasdflasdfasdfasdasdf: 2, +} + +mapping = { + A: 0.25 * (10.0 / 12), + B: 0.1 * (10.0 / 12), + C: 0.1 * (10.0 / 12), + D: 0.1 * (10.0 / 12), +} +``` + + diff --git a/crates/ruff_python_formatter/src/trivia.rs b/crates/ruff_python_formatter/src/trivia.rs index 01415e89b1..04a06dd404 100644 --- a/crates/ruff_python_formatter/src/trivia.rs +++ b/crates/ruff_python_formatter/src/trivia.rs @@ -162,6 +162,9 @@ pub(crate) enum TokenKind { /// '/' Slash, + /// '*' + Star, + /// Any other non trivia token. Always has a length of 1 Other, @@ -181,6 +184,7 @@ impl TokenKind { ',' => TokenKind::Comma, ':' => TokenKind::Colon, '/' => TokenKind::Slash, + '*' => TokenKind::Star, _ => TokenKind::Other, } } From 062b6e5c2bf72d06925391454eab2da5b972ca90 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Tue, 20 Jun 2023 15:49:11 +0530 Subject: [PATCH 123/447] Handle trailing newline in Jupyter notebook JSON string (#5202) ## Summary Handle trailing newline in Jupyter Notebook JSON string similar to how `black` does it. ## Test Plan Add test cases when the JSON string for notebook ends with and without a newline. resolves: #5190 --- .../test/fixtures/jupyter/after_fix.ipynb | 2 +- .../jupyter/no_trailing_newline.ipynb | 38 +++++++++++ crates/ruff/src/jupyter/notebook.rs | 67 ++++++++++++++----- crates/ruff/src/test.rs | 13 ++-- 4 files changed, 94 insertions(+), 26 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/jupyter/no_trailing_newline.ipynb diff --git a/crates/ruff/resources/test/fixtures/jupyter/after_fix.ipynb b/crates/ruff/resources/test/fixtures/jupyter/after_fix.ipynb index ef9bf6614f..407665029b 100644 --- a/crates/ruff/resources/test/fixtures/jupyter/after_fix.ipynb +++ b/crates/ruff/resources/test/fixtures/jupyter/after_fix.ipynb @@ -34,4 +34,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/crates/ruff/resources/test/fixtures/jupyter/no_trailing_newline.ipynb b/crates/ruff/resources/test/fixtures/jupyter/no_trailing_newline.ipynb new file mode 100644 index 0000000000..4c7df0a39e --- /dev/null +++ b/crates/ruff/resources/test/fixtures/jupyter/no_trailing_newline.ipynb @@ -0,0 +1,38 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "4cec6161-f594-446c-ab65-37395bbb3127", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "import os\n", + "\n", + "_ = math.pi" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (ruff)", + "language": "python", + "name": "ruff" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/crates/ruff/src/jupyter/notebook.rs b/crates/ruff/src/jupyter/notebook.rs index 2eb8f92bc5..0c206ace46 100644 --- a/crates/ruff/src/jupyter/notebook.rs +++ b/crates/ruff/src/jupyter/notebook.rs @@ -1,6 +1,6 @@ use std::cmp::Ordering; use std::fs::File; -use std::io::{BufReader, BufWriter, Write}; +use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}; use std::iter; use std::path::Path; @@ -34,9 +34,9 @@ pub fn round_trip(path: &Path) -> anyhow::Result { })?; let code = notebook.content().to_string(); notebook.update_cell_content(&code); - let mut buffer = BufWriter::new(Vec::new()); - notebook.write_inner(&mut buffer)?; - Ok(String::from_utf8(buffer.into_inner()?)?) + let mut writer = Vec::new(); + notebook.write_inner(&mut writer)?; + Ok(String::from_utf8(writer)?) } /// Return `true` if the [`Path`] appears to be that of a jupyter notebook file (`.ipynb`). @@ -113,13 +113,17 @@ pub struct Notebook { cell_offsets: Vec, /// The cell index of all valid code cells in the notebook. valid_code_cells: Vec, + /// Flag to indicate if the JSON string of the notebook has a trailing newline. + trailing_newline: bool, } impl Notebook { + /// Read the Jupyter Notebook from the given [`Path`]. + /// /// See also the black implementation /// pub fn read(path: &Path) -> Result> { - let reader = BufReader::new(File::open(path).map_err(|err| { + let mut reader = BufReader::new(File::open(path).map_err(|err| { Diagnostic::new( IOError { message: format!("{err}"), @@ -127,6 +131,18 @@ impl Notebook { TextRange::default(), ) })?); + let trailing_newline = reader.seek(SeekFrom::End(-1)).is_ok_and(|_| { + let mut buf = [0; 1]; + reader.read_exact(&mut buf).is_ok_and(|_| buf[0] == b'\n') + }); + reader.rewind().map_err(|err| { + Diagnostic::new( + IOError { + message: format!("{err}"), + }, + TextRange::default(), + ) + })?; let raw_notebook: RawNotebook = match serde_json::from_reader(reader) { Ok(notebook) => notebook, Err(err) => { @@ -240,6 +256,7 @@ impl Notebook { content: contents.join("\n") + "\n", cell_offsets, valid_code_cells, + trailing_newline, }) } @@ -411,8 +428,11 @@ impl Notebook { fn write_inner(&self, writer: &mut impl Write) -> anyhow::Result<()> { // https://github.com/psf/black/blob/69ca0a4c7a365c5f5eea519a90980bab72cab764/src/black/__init__.py#LL1041 let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); - let mut ser = serde_json::Serializer::with_formatter(writer, formatter); - SortAlphabetically(&self.raw).serialize(&mut ser)?; + let mut serializer = serde_json::Serializer::with_formatter(writer, formatter); + SortAlphabetically(&self.raw).serialize(&mut serializer)?; + if self.trailing_newline { + writeln!(serializer.into_inner())?; + } Ok(()) } @@ -426,7 +446,6 @@ impl Notebook { #[cfg(test)] mod test { - use std::io::BufWriter; use std::path::Path; use anyhow::Result; @@ -438,7 +457,7 @@ mod test { use crate::jupyter::schema::Cell; use crate::jupyter::Notebook; use crate::registry::Rule; - use crate::test::{test_notebook_path, test_resource_path}; + use crate::test::{read_jupyter_notebook, test_notebook_path, test_resource_path}; use crate::{assert_messages, settings}; /// Read a Jupyter cell from the `resources/test/fixtures/jupyter/cell` directory. @@ -450,15 +469,13 @@ mod test { #[test] fn test_valid() { - let path = Path::new("resources/test/fixtures/jupyter/valid.ipynb"); - assert!(Notebook::read(path).is_ok()); + assert!(read_jupyter_notebook(Path::new("valid.ipynb")).is_ok()); } #[test] fn test_r() { // We can load this, it will be filtered out later - let path = Path::new("resources/test/fixtures/jupyter/R.ipynb"); - assert!(Notebook::read(path).is_ok()); + assert!(read_jupyter_notebook(Path::new("R.ipynb")).is_ok()); } #[test] @@ -506,9 +523,8 @@ mod test { } #[test] - fn test_concat_notebook() { - let path = Path::new("resources/test/fixtures/jupyter/valid.ipynb"); - let notebook = Notebook::read(path).unwrap(); + fn test_concat_notebook() -> Result<()> { + let notebook = read_jupyter_notebook(Path::new("valid.ipynb"))?; assert_eq!( notebook.content, r#"def unused_variable(): @@ -546,6 +562,7 @@ print("after empty cells") 198.into() ] ); + Ok(()) } #[test] @@ -568,12 +585,26 @@ print("after empty cells") Path::new("after_fix.ipynb"), &settings::Settings::for_rule(Rule::UnusedImport), )?; - let mut writer = BufWriter::new(Vec::new()); + let mut writer = Vec::new(); source_kind.expect_jupyter().write_inner(&mut writer)?; - let actual = String::from_utf8(writer.into_inner()?)?; + let actual = String::from_utf8(writer)?; let expected = std::fs::read_to_string(test_resource_path("fixtures/jupyter/after_fix.ipynb"))?; assert_eq!(actual, expected); Ok(()) } + + #[test_case(Path::new("before_fix.ipynb"), true; "trailing_newline")] + #[test_case(Path::new("no_trailing_newline.ipynb"), false; "no_trailing_newline")] + fn test_trailing_newline(path: &Path, trailing_newline: bool) -> Result<()> { + let notebook = read_jupyter_notebook(path)?; + assert_eq!(notebook.trailing_newline, trailing_newline); + + let mut writer = Vec::new(); + notebook.write_inner(&mut writer)?; + let string = String::from_utf8(writer)?; + assert_eq!(string.ends_with('\n'), trailing_newline); + + Ok(()) + } } diff --git a/crates/ruff/src/test.rs b/crates/ruff/src/test.rs index 1e46037db7..d1682617d1 100644 --- a/crates/ruff/src/test.rs +++ b/crates/ruff/src/test.rs @@ -25,8 +25,9 @@ use crate::settings::{flags, Settings}; use crate::source_kind::SourceKind; #[cfg(not(fuzzing))] -fn read_jupyter_notebook(path: &Path) -> Result { - Notebook::read(path).map_err(|err| { +pub(crate) fn read_jupyter_notebook(path: &Path) -> Result { + let path = test_resource_path("fixtures/jupyter").join(path); + Notebook::read(&path).map_err(|err| { anyhow::anyhow!( "Failed to read notebook file `{}`: {:?}", path.display(), @@ -58,11 +59,9 @@ pub(crate) fn test_notebook_path( expected: impl AsRef, settings: &Settings, ) -> Result<(Vec, SourceKind)> { - let path = test_resource_path("fixtures/jupyter").join(path); - let mut source_kind = SourceKind::Jupyter(read_jupyter_notebook(&path)?); - let messages = test_contents(&mut source_kind, &path, settings); - let expected_notebook = - read_jupyter_notebook(&test_resource_path("fixtures/jupyter").join(expected))?; + let mut source_kind = SourceKind::Jupyter(read_jupyter_notebook(path.as_ref())?); + let messages = test_contents(&mut source_kind, path.as_ref(), settings); + let expected_notebook = read_jupyter_notebook(expected.as_ref())?; if let SourceKind::Jupyter(notebook) = &source_kind { assert_eq!(notebook.cell_offsets(), expected_notebook.cell_offsets()); assert_eq!(notebook.index(), expected_notebook.index()); From 17f1ecd56e376a75d66e01be47029f1b1b29c0aa Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Tue, 20 Jun 2023 17:43:09 +0200 Subject: [PATCH 124/447] Open cache files in parallel (#5120) ## Summary Open cache files in parallel (again), brings the performance back to be roughly equal to the old implementation. ## Test Plan Existing tests should keep working. --- .github/workflows/ci.yaml | 3 + .pre-commit-config.yaml | 1 + crates/ruff/src/settings/mod.rs | 2 +- .../test/fixtures/cache_mutable/.gitignore | 2 + .../test/fixtures/cache_mutable/source.py | 4 + crates/ruff_cli/src/cache.rs | 365 +++++++++++++++--- crates/ruff_cli/src/commands/run.rs | 74 ++-- crates/ruff_cli/src/diagnostics.rs | 27 +- 8 files changed, 353 insertions(+), 125 deletions(-) create mode 100644 crates/ruff_cli/resources/test/fixtures/cache_mutable/.gitignore create mode 100644 crates/ruff_cli/resources/test/fixtures/cache_mutable/source.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 46ad6cd981..6cae13b9e9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -76,6 +76,9 @@ jobs: cargo insta test --all --all-features git diff --exit-code - run: cargo test --package ruff_cli --test black_compatibility_test -- --ignored + # Skipped as it's currently broken. The resource were moved from the + # ruff_cli to ruff crate, but this test was not updated. + if: false # Check for broken links in the documentation. - run: cargo doc --all --no-deps env: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 16ffe82286..3331ffbd7f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,6 +4,7 @@ exclude: | (?x)^( crates/ruff/resources/.*| crates/ruff/src/rules/.*/snapshots/.*| + crates/ruff_cli/resources/.*| crates/ruff_python_formatter/resources/.*| crates/ruff_python_formatter/src/snapshots/.* )$ diff --git a/crates/ruff/src/settings/mod.rs b/crates/ruff/src/settings/mod.rs index fb5d6f8894..9a36b59908 100644 --- a/crates/ruff/src/settings/mod.rs +++ b/crates/ruff/src/settings/mod.rs @@ -40,7 +40,7 @@ pub mod types; const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); -#[derive(Debug)] +#[derive(Debug, Default)] pub struct AllSettings { pub cli: CliSettings, pub lib: Settings, diff --git a/crates/ruff_cli/resources/test/fixtures/cache_mutable/.gitignore b/crates/ruff_cli/resources/test/fixtures/cache_mutable/.gitignore new file mode 100644 index 0000000000..92d4d36a24 --- /dev/null +++ b/crates/ruff_cli/resources/test/fixtures/cache_mutable/.gitignore @@ -0,0 +1,2 @@ +# Modified by the cache tests. +source.py diff --git a/crates/ruff_cli/resources/test/fixtures/cache_mutable/source.py b/crates/ruff_cli/resources/test/fixtures/cache_mutable/source.py new file mode 100644 index 0000000000..7e397f06e5 --- /dev/null +++ b/crates/ruff_cli/resources/test/fixtures/cache_mutable/source.py @@ -0,0 +1,4 @@ +# NOTE: sync with cache::invalidation test +a = 1 + +__all__ = list(["a", "b"]) diff --git a/crates/ruff_cli/src/cache.rs b/crates/ruff_cli/src/cache.rs index d52ade04ec..d543a8bf94 100644 --- a/crates/ruff_cli/src/cache.rs +++ b/crates/ruff_cli/src/cache.rs @@ -6,11 +6,12 @@ use std::path::{Path, PathBuf}; use std::sync::Mutex; use std::time::SystemTime; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use ruff::message::Message; use ruff::settings::Settings; +use ruff::warn_user; use ruff_cache::{CacheKey, CacheKeyHasher}; use ruff_diagnostics::{DiagnosticKind, Fix}; use ruff_python_ast::imports::ImportMap; @@ -19,33 +20,45 @@ use ruff_text_size::{TextRange, TextSize}; use crate::diagnostics::Diagnostics; -/// On disk representation of a cache of a package. -#[derive(Deserialize, Debug, Serialize)] -pub(crate) struct PackageCache { +/// [`Path`] that is relative to the package root in [`PackageCache`]. +pub(crate) type RelativePath = Path; +/// [`PathBuf`] that is relative to the package root in [`PackageCache`]. +pub(crate) type RelativePathBuf = PathBuf; + +/// Cache. +/// +/// `Cache` holds everything required to display the diagnostics for a single +/// package. The on-disk representation is represented in [`PackageCache`] (and +/// related) types. +/// +/// This type manages the cache file, reading it from disk and writing it back +/// to disk (if required). +pub(crate) struct Cache { /// Location of the cache. - /// - /// Not stored on disk, just used as a storage location. - #[serde(skip)] path: PathBuf, - /// Path to the root of the package. + /// Package cache read from disk. + package: PackageCache, + /// Changes made compared to the (current) `package`. /// - /// Usually this is a directory, but it can also be a single file in case of - /// single file "packages", e.g. scripts. - package_root: PathBuf, - /// Mapping of source file path to it's cached data. - // TODO: look into concurrent hashmap or similar instead of a mutex. - files: Mutex>, + /// Files that are linted, but are not in `package.files` or are in + /// `package.files` but are outdated. This gets merged with `package.files` + /// when the cache is written back to disk in [`Cache::store`]. + new_files: Mutex>, } -impl PackageCache { - /// Open or create a new package cache. +impl Cache { + /// Open or create a new cache. /// - /// `package_root` must be canonicalized. - pub(crate) fn open( - cache_dir: &Path, - package_root: PathBuf, - settings: &Settings, - ) -> Result { + /// `cache_dir` is considered the root directory of the cache, which can be + /// local to the project, global or otherwise set by the user. + /// + /// `package_root` is the path to root of the package that is contained + /// within this cache and must be canonicalized (to avoid considering `./` + /// and `../project` being different). + /// + /// Finally `settings` is used to ensure we don't open a cache for different + /// settings. + pub(crate) fn open(cache_dir: &Path, package_root: PathBuf, settings: &Settings) -> Cache { debug_assert!(package_root.is_absolute(), "package root not canonicalized"); let mut buf = itoa::Buffer::new(); @@ -56,40 +69,66 @@ impl PackageCache { Ok(file) => file, Err(err) if err.kind() == io::ErrorKind::NotFound => { // No cache exist yet, return an empty cache. - return Ok(PackageCache { - path, - package_root, - files: Mutex::new(HashMap::new()), - }); + return Cache::empty(path, package_root); } Err(err) => { - return Err(err) - .with_context(|| format!("Failed to open cache file '{}'", path.display()))? + warn_user!("Failed to open cache file '{}': {err}", path.display()); + return Cache::empty(path, package_root); } }; - let mut cache: PackageCache = bincode::deserialize_from(BufReader::new(file)) - .with_context(|| format!("Failed parse cache file '{}'", path.display()))?; + let mut package: PackageCache = match bincode::deserialize_from(BufReader::new(file)) { + Ok(package) => package, + Err(err) => { + warn_user!("Failed parse cache file '{}': {err}", path.display()); + return Cache::empty(path, package_root); + } + }; // Sanity check. - if cache.package_root != package_root { - return Err(anyhow!( + if package.package_root != package_root { + warn_user!( "Different package root in cache: expected '{}', got '{}'", package_root.display(), - cache.package_root.display(), - )); + package.package_root.display(), + ); + package.files.clear(); + } + Cache { + path, + package, + new_files: Mutex::new(HashMap::new()), } - - cache.path = path; - Ok(cache) } - /// Store the cache to disk. - pub(crate) fn store(&self) -> Result<()> { + /// Create an empty `Cache`. + fn empty(path: PathBuf, package_root: PathBuf) -> Cache { + Cache { + path, + package: PackageCache { + package_root, + files: HashMap::new(), + }, + new_files: Mutex::new(HashMap::new()), + } + } + + /// Store the cache to disk, if it has been changed. + pub(crate) fn store(mut self) -> Result<()> { + let new_files = self.new_files.into_inner().unwrap(); + if new_files.is_empty() { + // No changes made, no need to write the same cache file back to + // disk. + return Ok(()); + } + + // Add/overwrite the changes made. + self.package.files.extend(new_files); + let file = File::create(&self.path) .with_context(|| format!("Failed to create cache file '{}'", self.path.display()))?; let writer = BufWriter::new(file); - bincode::serialize_into(writer, &self).with_context(|| { + bincode::serialize_into(writer, &self.package).with_context(|| { format!( "Failed to serialise cache to file '{}'", self.path.display() @@ -101,7 +140,7 @@ impl PackageCache { /// /// Returns `None` if `path` is not within the package. pub(crate) fn relative_path<'a>(&self, path: &'a Path) -> Option<&'a RelativePath> { - path.strip_prefix(&self.package_root).ok() + path.strip_prefix(&self.package.package_root).ok() } /// Get the cached results for a single file at relative `path`. This uses @@ -114,33 +153,34 @@ impl PackageCache { &self, path: &RelativePath, file_last_modified: SystemTime, - ) -> Option { - let files = self.files.lock().unwrap(); - let file = files.get(path)?; + ) -> Option<&FileCache> { + let file = self.package.files.get(path)?; // Make sure the file hasn't changed since the cached run. if file.last_modified != file_last_modified { return None; } - Some(file.clone()) + Some(file) } /// Add or update a file cache at `path` relative to the package root. pub(crate) fn update(&self, path: RelativePathBuf, file: FileCache) { - self.files.lock().unwrap().insert(path, file); - } - - /// Remove a file cache at `path` relative to the package root. - pub(crate) fn remove(&self, path: &RelativePath) { - self.files.lock().unwrap().remove(path); + self.new_files.lock().unwrap().insert(path, file); } } -/// [`Path`] that is relative to the package root in [`PackageCache`]. -pub(crate) type RelativePath = Path; -/// [`PathBuf`] that is relative to the package root in [`PackageCache`]. -pub(crate) type RelativePathBuf = PathBuf; +/// On disk representation of a cache of a package. +#[derive(Deserialize, Debug, Serialize)] +struct PackageCache { + /// Path to the root of the package. + /// + /// Usually this is a directory, but it can also be a single file in case of + /// single file "packages", e.g. scripts. + package_root: PathBuf, + /// Mapping of source file path to it's cached data. + files: HashMap, +} /// On disk representation of the cache per source file. #[derive(Clone, Deserialize, Debug, Serialize)] @@ -198,23 +238,23 @@ impl FileCache { } /// Convert the file cache into `Diagnostics`, using `path` as file name. - pub(crate) fn into_diagnostics(self, path: &Path) -> Diagnostics { + pub(crate) fn as_diagnostics(&self, path: &Path) -> Diagnostics { let messages = if self.messages.is_empty() { Vec::new() } else { - let file = SourceFileBuilder::new(path.to_string_lossy(), self.source).finish(); + let file = SourceFileBuilder::new(path.to_string_lossy(), &*self.source).finish(); self.messages - .into_iter() + .iter() .map(|msg| Message { - kind: msg.kind, + kind: msg.kind.clone(), range: msg.range, - fix: msg.fix, + fix: msg.fix.clone(), file: file.clone(), noqa_offset: msg.noqa_offset, }) .collect() }; - Diagnostics::new(messages, self.imports) + Diagnostics::new(messages, self.imports.clone()) } } @@ -257,3 +297,204 @@ pub(crate) fn init(path: &Path) -> Result<()> { Ok(()) } + +#[cfg(test)] +mod test { + use std::env::temp_dir; + use std::fs; + use std::io::{self, Write}; + use std::path::Path; + + use ruff::settings::{flags, AllSettings}; + use ruff_cache::CACHE_DIR_NAME; + + use crate::cache::{self, Cache}; + use crate::diagnostics::{lint_path, Diagnostics}; + + #[test] + fn same_results() { + let mut cache_dir = temp_dir(); + cache_dir.push("ruff_tests/cache_same_results"); + let _ = fs::remove_dir_all(&cache_dir); + cache::init(&cache_dir).unwrap(); + + let settings = AllSettings::default(); + + let package_root = fs::canonicalize("../ruff/resources/test/fixtures").unwrap(); + let cache = Cache::open(&cache_dir, package_root.clone(), &settings.lib); + assert_eq!(cache.new_files.lock().unwrap().len(), 0); + + let mut paths = Vec::new(); + let mut parse_errors = Vec::new(); + let mut expected_diagnostics = Diagnostics::default(); + for entry in fs::read_dir(&package_root).unwrap() { + let entry = entry.unwrap(); + if !entry.file_type().unwrap().is_dir() { + continue; + } + + let dir_path = entry.path(); + if dir_path.ends_with(CACHE_DIR_NAME) { + continue; + } + + for entry in fs::read_dir(dir_path).unwrap() { + let entry = entry.unwrap(); + if !entry.file_type().unwrap().is_file() { + continue; + } + + let path = entry.path(); + if path.ends_with("pyproject.toml") || path.ends_with("R.ipynb") { + continue; + } + + let diagnostics = lint_path( + &path, + Some(&package_root), + &settings, + Some(&cache), + flags::Noqa::Enabled, + flags::FixMode::Generate, + ) + .unwrap(); + if diagnostics + .messages + .iter() + .any(|m| m.kind.name == "SyntaxError") + { + parse_errors.push(path.clone()); + } + paths.push(path); + expected_diagnostics += diagnostics; + } + } + assert_ne!(paths, &[] as &[std::path::PathBuf], "no files checked"); + + cache.store().unwrap(); + + let cache = Cache::open(&cache_dir, package_root.clone(), &settings.lib); + assert_ne!(cache.package.files.len(), 0); + + parse_errors.sort(); + + for path in &paths { + if parse_errors.binary_search(path).is_ok() { + continue; // We don't cache parsing errors. + } + + let relative_path = cache.relative_path(path).unwrap(); + + assert!( + cache.package.files.contains_key(relative_path), + "missing file from cache: '{}'", + relative_path.display() + ); + } + + let mut got_diagnostics = Diagnostics::default(); + for path in paths { + got_diagnostics += lint_path( + &path, + Some(&package_root), + &settings, + Some(&cache), + flags::Noqa::Enabled, + flags::FixMode::Generate, + ) + .unwrap(); + } + + // Not stored in the cache. + expected_diagnostics.source_kind.clear(); + got_diagnostics.source_kind.clear(); + assert!(expected_diagnostics == got_diagnostics); + } + + #[test] + fn invalidation() { + // NOTE: keep in sync with actual file. + const SOURCE: &[u8] = b"# NOTE: sync with cache::invalidation test\na = 1\n\n__all__ = list([\"a\", \"b\"])\n"; + + let mut cache_dir = temp_dir(); + cache_dir.push("ruff_tests/cache_invalidation"); + let _ = fs::remove_dir_all(&cache_dir); + cache::init(&cache_dir).unwrap(); + + let settings = AllSettings::default(); + let package_root = fs::canonicalize("resources/test/fixtures/cache_mutable").unwrap(); + let cache = Cache::open(&cache_dir, package_root.clone(), &settings.lib); + assert_eq!(cache.new_files.lock().unwrap().len(), 0); + + let path = package_root.join("source.py"); + let mut expected_diagnostics = lint_path( + &path, + Some(&package_root), + &settings, + Some(&cache), + flags::Noqa::Enabled, + flags::FixMode::Generate, + ) + .unwrap(); + assert_eq!(cache.new_files.lock().unwrap().len(), 1); + + cache.store().unwrap(); + + let tests = [ + // File change. + (|path| { + let mut file = fs::OpenOptions::new() + .write(true) + .truncate(true) + .open(path)?; + file.write_all(SOURCE)?; + file.sync_data()?; + Ok(|_| Ok(())) + }) as fn(&Path) -> io::Result io::Result<()>>, + // Regression for issue #3086. + #[cfg(unix)] + |path| { + flip_execute_permission_bit(path)?; + Ok(flip_execute_permission_bit) + }, + ]; + + #[cfg(unix)] + #[allow(clippy::items_after_statements)] + fn flip_execute_permission_bit(path: &Path) -> io::Result<()> { + use std::os::unix::fs::PermissionsExt; + let file = fs::OpenOptions::new().write(true).open(path)?; + let perms = file.metadata()?.permissions(); + file.set_permissions(PermissionsExt::from_mode(perms.mode() ^ 0o111)) + } + + for change_file in tests { + let cleanup = change_file(&path).unwrap(); + + let cache = Cache::open(&cache_dir, package_root.clone(), &settings.lib); + + let mut got_diagnostics = lint_path( + &path, + Some(&package_root), + &settings, + Some(&cache), + flags::Noqa::Enabled, + flags::FixMode::Generate, + ) + .unwrap(); + + cleanup(&path).unwrap(); + + assert_eq!( + cache.new_files.lock().unwrap().len(), + 1, + "cache must not be used" + ); + + // Not store in the cache. + expected_diagnostics.source_kind.clear(); + got_diagnostics.source_kind.clear(); + assert!(expected_diagnostics == got_diagnostics); + } + } +} diff --git a/crates/ruff_cli/src/commands/run.rs b/crates/ruff_cli/src/commands/run.rs index 62a37352f5..9ff134f2c9 100644 --- a/crates/ruff_cli/src/commands/run.rs +++ b/crates/ruff_cli/src/commands/run.rs @@ -1,4 +1,4 @@ -use std::collections::{hash_map, HashMap}; +use std::collections::HashMap; use std::fmt::Write; use std::io; use std::path::{Path, PathBuf}; @@ -22,7 +22,7 @@ use ruff_python_ast::imports::ImportMap; use ruff_python_ast::source_code::SourceFileBuilder; use crate::args::Overrides; -use crate::cache::{self, PackageCache}; +use crate::cache::{self, Cache}; use crate::diagnostics::Diagnostics; use crate::panic::catch_unwind; @@ -77,37 +77,21 @@ pub(crate) fn run( pyproject_config, ); - // Create a cache per package, if enabled. - let package_caches = if cache.into() { - let mut caches = HashMap::new(); - // TODO(thomas): try to merge this with the detection of package roots - // above or with the parallel iteration below. - for entry in &paths { - let Ok(entry) = entry else { continue }; - let path = entry.path(); - let package = path - .parent() - .and_then(|parent| package_roots.get(parent)) - .and_then(|package| *package); - // For paths not in a package, e.g. scripts, we use the path as - // the package root. - let package_root = package.unwrap_or(path); - - let settings = resolver.resolve_all(path, pyproject_config); - - if let hash_map::Entry::Vacant(entry) = caches.entry(package_root) { - let cache = PackageCache::open( + // Load the caches. + let caches = bool::from(cache).then(|| { + package_roots + .par_iter() + .map(|(package_root, _)| { + let settings = resolver.resolve_all(package_root, pyproject_config); + let cache = Cache::open( &settings.cli.cache_dir, - package_root.to_owned(), + package_root.to_path_buf(), &settings.lib, - )?; - entry.insert(cache); - } - } - Some(caches) - } else { - None - }; + ); + (&**package_root, cache) + }) + .collect::>() + }); let start = Instant::now(); let mut diagnostics: Diagnostics = paths @@ -121,17 +105,13 @@ pub(crate) fn run( .and_then(|parent| package_roots.get(parent)) .and_then(|package| *package); - let package_cache = package_caches.as_ref().map(|package_caches| { - let package_root = package.unwrap_or(path); - let package_cache = package_caches - .get(package_root) - .expect("failed to get package cache"); - package_cache - }); - let settings = resolver.resolve_all(path, pyproject_config); + let package_root = package.unwrap_or_else(|| path.parent().unwrap_or(path)); + let cache = caches + .as_ref() + .map(|caches| caches.get(&package_root).unwrap()); - lint_path(path, package, settings, package_cache, noqa, autofix).map_err(|e| { + lint_path(path, package, settings, cache, noqa, autofix).map_err(|e| { (Some(path.to_owned()), { let mut error = e.to_string(); for cause in e.chain() { @@ -188,11 +168,11 @@ pub(crate) fn run( diagnostics.messages.sort(); - // Store the package caches. - if let Some(package_caches) = package_caches { - for package_cache in package_caches.values() { - package_cache.store()?; - } + // Store the caches. + if let Some(caches) = caches { + caches + .into_par_iter() + .try_for_each(|(_, cache)| cache.store())?; } let duration = start.elapsed(); @@ -207,12 +187,12 @@ fn lint_path( path: &Path, package: Option<&Path>, settings: &AllSettings, - package_cache: Option<&PackageCache>, + cache: Option<&Cache>, noqa: flags::Noqa, autofix: flags::FixMode, ) -> Result { let result = catch_unwind(|| { - crate::diagnostics::lint_path(path, package, settings, package_cache, noqa, autofix) + crate::diagnostics::lint_path(path, package, settings, cache, noqa, autofix) }); match result { diff --git a/crates/ruff_cli/src/diagnostics.rs b/crates/ruff_cli/src/diagnostics.rs index 68c110851e..7fabf26141 100644 --- a/crates/ruff_cli/src/diagnostics.rs +++ b/crates/ruff_cli/src/diagnostics.rs @@ -25,7 +25,7 @@ use ruff_python_ast::imports::ImportMap; use ruff_python_ast::source_code::{LineIndex, SourceCode, SourceFileBuilder}; use ruff_python_stdlib::path::is_project_toml; -use crate::cache::{FileCache, PackageCache}; +use crate::cache::{Cache, FileCache}; #[derive(Debug, Default, PartialEq)] pub(crate) struct Diagnostics { @@ -100,7 +100,7 @@ pub(crate) fn lint_path( path: &Path, package: Option<&Path>, settings: &AllSettings, - package_cache: Option<&PackageCache>, + cache: Option<&Cache>, noqa: flags::Noqa, autofix: flags::FixMode, ) -> Result { @@ -110,17 +110,17 @@ pub(crate) fn lint_path( // to cache `fixer::Mode::Apply`, since a file either has no fixes, or we'll // write the fixes to disk, thus invalidating the cache. But it's a bit hard // to reason about. We need to come up with a better solution here.) - let caching = match package_cache { - Some(package_cache) if noqa.into() && autofix.is_generate() => { - let relative_path = package_cache + let caching = match cache { + Some(cache) if noqa.into() && autofix.is_generate() => { + let relative_path = cache .relative_path(path) .expect("wrong package cache for file"); let last_modified = path.metadata()?.modified()?; - if let Some(cache) = package_cache.get(relative_path, last_modified) { - return Ok(cache.into_diagnostics(path)); + if let Some(cache) = cache.get(relative_path, last_modified) { + return Ok(cache.as_diagnostics(path)); } - Some((package_cache, relative_path, last_modified)) + Some((cache, relative_path, last_modified)) } _ => None, }; @@ -207,14 +207,11 @@ pub(crate) fn lint_path( let imports = imports.unwrap_or_default(); - if let Some((package_cache, relative_path, file_last_modified)) = caching { - if parse_error.is_some() { - // We don't cache parsing error, so we remove the old file cache (if - // any). - package_cache.remove(relative_path); - } else { + if let Some((cache, relative_path, file_last_modified)) = caching { + if parse_error.is_none() { + // We don't cache parsing error. let file_cache = FileCache::new(file_last_modified, &messages, &imports); - package_cache.update(relative_path.to_owned(), file_cache); + cache.update(relative_path.to_owned(), file_cache); } } From 633159851158a959b684be143c1d4f5d3d883e34 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 20 Jun 2023 11:43:38 -0400 Subject: [PATCH 125/447] Upgrade `RustPython` to access ranged names (#5194) ## Summary In https://github.com/astral-sh/RustPython-Parser/pull/8, we modified RustPython to include ranges for any identifiers that aren't `Expr::Name` (which already has an identifier). For example, the `e` in `except ValueError as e` was previously un-ranged. To extract its range, we had to do some lexing of our own. This change should improve performance and let us remove a bunch of code. ## Test Plan `cargo test` --- Cargo.lock | 12 +++++----- Cargo.toml | 19 ++++++++++----- crates/ruff/src/checkers/ast/mod.rs | 4 ++-- .../rules/getattr_with_constant.rs | 4 ++-- .../rules/setattr_with_constant.rs | 4 ++-- .../rules/is_gettext_func_call.rs | 2 +- .../rules/multiple_starts_ends_with.rs | 4 ++-- .../rules/flake8_pytest_style/rules/raises.rs | 11 +++++---- .../rules/unittest_assert.rs | 6 +++-- .../src/rules/flake8_simplify/rules/ast_if.rs | 4 ++-- .../rules/relative_imports.rs | 7 ++++-- .../pycodestyle/rules/lambda_assignment.rs | 8 ++++--- .../src/rules/pylint/rules/duplicate_bases.rs | 4 ++-- .../rules/pylint/rules/manual_import_from.rs | 4 ++-- .../pylint/rules/useless_import_alias.rs | 2 +- ...convert_named_tuple_functional_to_class.rs | 6 +++-- .../convert_typed_dict_functional_to_class.rs | 6 +++-- .../rules/super_call_with_parameters.rs | 2 +- .../src/rules/ruff/rules/implicit_optional.rs | 2 +- .../tryceratops/rules/useless_try_except.rs | 2 +- .../tryceratops/rules/verbose_log_message.rs | 2 +- .../rules/tryceratops/rules/verbose_raise.rs | 2 +- .../src/source_code/generator.rs | 2 +- .../src/comments/placement.rs | 24 ++++++++++++------- 24 files changed, 86 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62ddd69515..b2eb7daa03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2105,7 +2105,7 @@ dependencies = [ [[package]] name = "ruff_text_size" version = "0.0.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8d74eee75031b68d2204219963fae54a3f31a394#8d74eee75031b68d2204219963fae54a3f31a394" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=ed3b4eb72b6e497bbdb4d19dec6621074d724130#ed3b4eb72b6e497bbdb4d19dec6621074d724130" dependencies = [ "schemars", "serde", @@ -2183,7 +2183,7 @@ dependencies = [ [[package]] name = "rustpython-ast" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8d74eee75031b68d2204219963fae54a3f31a394#8d74eee75031b68d2204219963fae54a3f31a394" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=ed3b4eb72b6e497bbdb4d19dec6621074d724130#ed3b4eb72b6e497bbdb4d19dec6621074d724130" dependencies = [ "is-macro", "num-bigint", @@ -2194,7 +2194,7 @@ dependencies = [ [[package]] name = "rustpython-format" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8d74eee75031b68d2204219963fae54a3f31a394#8d74eee75031b68d2204219963fae54a3f31a394" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=ed3b4eb72b6e497bbdb4d19dec6621074d724130#ed3b4eb72b6e497bbdb4d19dec6621074d724130" dependencies = [ "bitflags 2.3.1", "itertools", @@ -2206,7 +2206,7 @@ dependencies = [ [[package]] name = "rustpython-literal" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8d74eee75031b68d2204219963fae54a3f31a394#8d74eee75031b68d2204219963fae54a3f31a394" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=ed3b4eb72b6e497bbdb4d19dec6621074d724130#ed3b4eb72b6e497bbdb4d19dec6621074d724130" dependencies = [ "hexf-parse", "is-macro", @@ -2218,7 +2218,7 @@ dependencies = [ [[package]] name = "rustpython-parser" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8d74eee75031b68d2204219963fae54a3f31a394#8d74eee75031b68d2204219963fae54a3f31a394" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=ed3b4eb72b6e497bbdb4d19dec6621074d724130#ed3b4eb72b6e497bbdb4d19dec6621074d724130" dependencies = [ "anyhow", "is-macro", @@ -2241,7 +2241,7 @@ dependencies = [ [[package]] name = "rustpython-parser-core" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8d74eee75031b68d2204219963fae54a3f31a394#8d74eee75031b68d2204219963fae54a3f31a394" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=ed3b4eb72b6e497bbdb4d19dec6621074d724130#ed3b4eb72b6e497bbdb4d19dec6621074d724130" dependencies = [ "is-macro", "memchr", diff --git a/Cargo.toml b/Cargo.toml index 23414c705f..7a9e8d929b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,6 @@ ignore = { version = "0.4.20" } insta = { version = "1.28.0" } is-macro = { version = "0.2.2" } itertools = { version = "0.10.5" } -libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "80e4c1399f95e5beb532fdd1e209ad2dbb470438" } log = { version = "0.4.17" } memchr = "2.5.0" nohash-hasher = { version = "0.2.0" } @@ -36,11 +35,6 @@ proc-macro2 = { version = "1.0.51" } quote = { version = "1.0.23" } regex = { version = "1.7.1" } rustc-hash = { version = "1.1.0" } -ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8d74eee75031b68d2204219963fae54a3f31a394" } -rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8d74eee75031b68d2204219963fae54a3f31a394" , default-features = false, features = ["all-nodes-with-ranges", "num-bigint"]} -rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8d74eee75031b68d2204219963fae54a3f31a394", default-features = false, features = ["num-bigint"] } -rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8d74eee75031b68d2204219963fae54a3f31a394" } -rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8d74eee75031b68d2204219963fae54a3f31a394" , default-features = false, features = ["full-lexer", "all-nodes-with-ranges", "num-bigint"] } schemars = { version = "0.8.12" } serde = { version = "1.0.152", features = ["derive"] } serde_json = { version = "1.0.93" } @@ -53,6 +47,19 @@ syn = { version = "2.0.15" } test-case = { version = "3.0.0" } toml = { version = "0.7.2" } +# v0.0.1 +libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "80e4c1399f95e5beb532fdd1e209ad2dbb470438" } +# v0.0.3 +ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "ed3b4eb72b6e497bbdb4d19dec6621074d724130" } +# v0.0.3 +rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "ed3b4eb72b6e497bbdb4d19dec6621074d724130" , default-features = false, features = ["all-nodes-with-ranges", "num-bigint"]} +# v0.0.3 +rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "ed3b4eb72b6e497bbdb4d19dec6621074d724130", default-features = false, features = ["num-bigint"] } +# v0.0.3 +rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "ed3b4eb72b6e497bbdb4d19dec6621074d724130", default-features = false } +# v0.0.3 +rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "ed3b4eb72b6e497bbdb4d19dec6621074d724130" , default-features = false, features = ["full-lexer", "all-nodes-with-ranges", "num-bigint"] } + [profile.release] lto = "fat" codegen-units = 1 diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 155d2f8c1f..54948a23cd 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -825,7 +825,7 @@ where if alias .asname .as_ref() - .map_or(false, |asname| asname == &alias.name) + .map_or(false, |asname| asname.as_str() == alias.name.as_str()) { flags |= BindingFlags::EXPLICIT_EXPORT; } @@ -1110,7 +1110,7 @@ where if alias .asname .as_ref() - .map_or(false, |asname| asname == &alias.name) + .map_or(false, |asname| asname.as_str() == alias.name.as_str()) { flags |= BindingFlags::EXPLICIT_EXPORT; } 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 ff57ce600f..29a667eb15 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 @@ -1,5 +1,5 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Ranged}; +use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Identifier, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -27,7 +27,7 @@ impl AlwaysAutofixableViolation for GetAttrWithConstant { fn attribute(value: &Expr, attr: &str) -> Expr { ast::ExprAttribute { value: Box::new(value.clone()), - attr: attr.into(), + attr: Identifier::new(attr.to_string(), TextRange::default()), ctx: ExprContext::Load, range: TextRange::default(), } 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 4866a9ae7c..0708af6b87 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 @@ -1,5 +1,5 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Ranged, Stmt}; +use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Identifier, Ranged, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -30,7 +30,7 @@ fn assignment(obj: &Expr, name: &str, value: &Expr, generator: Generator) -> Str let stmt = Stmt::Assign(ast::StmtAssign { targets: vec![Expr::Attribute(ast::ExprAttribute { value: Box::new(obj.clone()), - attr: name.into(), + attr: Identifier::new(name.to_string(), TextRange::default()), ctx: ExprContext::Store, range: TextRange::default(), })], diff --git a/crates/ruff/src/rules/flake8_gettext/rules/is_gettext_func_call.rs b/crates/ruff/src/rules/flake8_gettext/rules/is_gettext_func_call.rs index 0e539ead9a..e2f78c4476 100644 --- a/crates/ruff/src/rules/flake8_gettext/rules/is_gettext_func_call.rs +++ b/crates/ruff/src/rules/flake8_gettext/rules/is_gettext_func_call.rs @@ -3,7 +3,7 @@ use rustpython_parser::ast::{self, Expr}; /// Returns true if the [`Expr`] is an internationalization function call. pub(crate) fn is_gettext_func_call(func: &Expr, functions_names: &[String]) -> bool { if let Expr::Name(ast::ExprName { id, .. }) = func { - functions_names.contains(id.as_ref()) + functions_names.contains(id) } else { false } 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 708df4e241..2905bf8d34 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 @@ -5,7 +5,7 @@ use itertools::Either::{Left, Right}; use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, BoolOp, Expr, ExprContext, Ranged}; +use rustpython_parser::ast::{self, BoolOp, Expr, ExprContext, Identifier, Ranged}; use ruff_diagnostics::AlwaysAutofixableViolation; use ruff_diagnostics::{Diagnostic, Edit, Fix}; @@ -140,7 +140,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) { }); let node2 = Expr::Attribute(ast::ExprAttribute { value: Box::new(node1), - attr: attr_name.into(), + attr: Identifier::new(attr_name.to_string(), TextRange::default()), ctx: ExprContext::Load, range: TextRange::default(), }); diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs index 78296a99e4..74e13767a7 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Expr, Identifier, Keyword, Ranged, Stmt, WithItem}; +use rustpython_parser::ast::{self, Expr, Keyword, Ranged, Stmt, WithItem}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -74,9 +74,12 @@ pub(crate) fn raises_call(checker: &mut Checker, func: &Expr, args: &[Expr], key } if checker.enabled(Rule::PytestRaisesTooBroad) { - let match_keyword = keywords - .iter() - .find(|kw| kw.arg == Some(Identifier::new("match"))); + let match_keyword = keywords.iter().find(|keyword| { + keyword + .arg + .as_ref() + .map_or(false, |arg| arg.as_str() == "match") + }); if let Some(exception) = args.first() { if let Some(match_keyword) = match_keyword { diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/unittest_assert.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/unittest_assert.rs index 0c032ce84c..106bf9ec51 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/unittest_assert.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/unittest_assert.rs @@ -3,7 +3,9 @@ use std::hash::BuildHasherDefault; use anyhow::{anyhow, bail, Result}; use ruff_text_size::TextRange; use rustc_hash::FxHashMap; -use rustpython_parser::ast::{self, CmpOp, Constant, Expr, ExprContext, Keyword, Stmt, UnaryOp}; +use rustpython_parser::ast::{ + self, CmpOp, Constant, Expr, ExprContext, Identifier, Keyword, Stmt, UnaryOp, +}; /// An enum to represent the different types of assertions present in the /// `unittest` module. Note: any variants that can't be replaced with plain @@ -388,7 +390,7 @@ impl UnittestAssert { }; let node1 = ast::ExprAttribute { value: Box::new(node.into()), - attr: "search".into(), + attr: Identifier::new("search".to_string(), TextRange::default()), ctx: ExprContext::Load, range: TextRange::default(), }; 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 3b5d4a4bb9..8a3105919d 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs @@ -1,7 +1,7 @@ use log::error; use ruff_text_size::TextRange; use rustc_hash::FxHashSet; -use rustpython_parser::ast::{self, CmpOp, Constant, Expr, ExprContext, Ranged, Stmt}; +use rustpython_parser::ast::{self, CmpOp, Constant, Expr, ExprContext, Identifier, Ranged, Stmt}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -941,7 +941,7 @@ pub(crate) fn use_dict_get_with_default( let node1 = *test_key.clone(); let node2 = ast::ExprAttribute { value: expected_subscript.clone(), - attr: "get".into(), + attr: Identifier::new("get".to_string(), TextRange::default()), ctx: ExprContext::Load, range: TextRange::default(), }; diff --git a/crates/ruff/src/rules/flake8_tidy_imports/rules/relative_imports.rs b/crates/ruff/src/rules/flake8_tidy_imports/rules/relative_imports.rs index 6b8227be28..5012d69950 100644 --- a/crates/ruff/src/rules/flake8_tidy_imports/rules/relative_imports.rs +++ b/crates/ruff/src/rules/flake8_tidy_imports/rules/relative_imports.rs @@ -1,5 +1,5 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Int, Ranged, Stmt}; +use rustpython_parser::ast::{self, Identifier, Int, Ranged, Stmt}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -94,7 +94,10 @@ fn fix_banned_relative_import( panic!("Expected Stmt::ImportFrom"); }; let node = ast::StmtImportFrom { - module: Some(module_path.to_string().into()), + module: Some(Identifier::new( + module_path.to_string(), + TextRange::default(), + )), names: names.clone(), level: Some(Int::new(0)), range: TextRange::default(), diff --git a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs index 9d65aa6692..8cc70a26c9 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs @@ -1,5 +1,7 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Arg, ArgWithDefault, Arguments, Constant, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{ + self, Arg, ArgWithDefault, Arguments, Constant, Expr, Identifier, Ranged, Stmt, +}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -201,7 +203,7 @@ fn function( }) .collect::>(); let func = Stmt::FunctionDef(ast::StmtFunctionDef { - name: name.into(), + name: Identifier::new(name.to_string(), TextRange::default()), args: Box::new(Arguments { posonlyargs: new_posonlyargs, args: new_args, @@ -217,7 +219,7 @@ fn function( } } let func = Stmt::FunctionDef(ast::StmtFunctionDef { - name: name.into(), + name: Identifier::new(name.to_string(), TextRange::default()), args: Box::new(args.clone()), body: vec![body], decorator_list: vec![], diff --git a/crates/ruff/src/rules/pylint/rules/duplicate_bases.rs b/crates/ruff/src/rules/pylint/rules/duplicate_bases.rs index bd5a6b5c25..860e393565 100644 --- a/crates/ruff/src/rules/pylint/rules/duplicate_bases.rs +++ b/crates/ruff/src/rules/pylint/rules/duplicate_bases.rs @@ -1,7 +1,7 @@ use std::hash::BuildHasherDefault; use rustc_hash::FxHashSet; -use rustpython_parser::ast::{self, Expr, Identifier, Ranged}; +use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -52,7 +52,7 @@ impl Violation for DuplicateBases { /// PLE0241 pub(crate) fn duplicate_bases(checker: &mut Checker, name: &str, bases: &[Expr]) { - let mut seen: FxHashSet<&Identifier> = + let mut seen: FxHashSet<&str> = FxHashSet::with_capacity_and_hasher(bases.len(), BuildHasherDefault::default()); for base in bases { if let Expr::Name(ast::ExprName { id, .. }) = base { diff --git a/crates/ruff/src/rules/pylint/rules/manual_import_from.rs b/crates/ruff/src/rules/pylint/rules/manual_import_from.rs index bac9cf746b..0343cd1eb7 100644 --- a/crates/ruff/src/rules/pylint/rules/manual_import_from.rs +++ b/crates/ruff/src/rules/pylint/rules/manual_import_from.rs @@ -1,5 +1,5 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Alias, Int, Ranged, Stmt}; +use rustpython_parser::ast::{self, Alias, Identifier, Int, Ranged, Stmt}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -74,7 +74,7 @@ pub(crate) fn manual_from_import( if checker.patch(diagnostic.kind.rule()) { if names.len() == 1 { let node = ast::StmtImportFrom { - module: Some(module.into()), + module: Some(Identifier::new(module.to_string(), TextRange::default())), names: vec![Alias { name: asname.clone(), asname: None, diff --git a/crates/ruff/src/rules/pylint/rules/useless_import_alias.rs b/crates/ruff/src/rules/pylint/rules/useless_import_alias.rs index 7c657a5840..708de5926b 100644 --- a/crates/ruff/src/rules/pylint/rules/useless_import_alias.rs +++ b/crates/ruff/src/rules/pylint/rules/useless_import_alias.rs @@ -43,7 +43,7 @@ pub(crate) fn useless_import_alias(checker: &mut Checker, alias: &Alias) { if alias.name.contains('.') { return; } - if &alias.name != asname { + if alias.name.as_str() != asname.as_str() { 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 9e83d864cc..284342608f 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 @@ -1,7 +1,9 @@ use anyhow::{bail, Result}; use log::debug; use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Keyword, Ranged, Stmt}; +use rustpython_parser::ast::{ + self, Constant, Expr, ExprContext, Identifier, Keyword, Ranged, Stmt, +}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -185,7 +187,7 @@ fn create_properties_from_args(args: &[Expr], defaults: &[Expr]) -> Result, base_class: &Expr) -> Stmt { ast::StmtClassDef { - name: typename.into(), + name: Identifier::new(typename.to_string(), TextRange::default()), bases: vec![base_class.clone()], keywords: vec![], body, diff --git a/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs b/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs index c1d2f5c58d..2c7f2827af 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs @@ -1,7 +1,9 @@ use anyhow::{bail, Result}; use log::debug; use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Keyword, Ranged, Stmt}; +use rustpython_parser::ast::{ + self, Constant, Expr, ExprContext, Identifier, Keyword, Ranged, Stmt, +}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -120,7 +122,7 @@ fn create_class_def_stmt( None => vec![], }; ast::StmtClassDef { - name: class_name.into(), + name: Identifier::new(class_name.to_string(), TextRange::default()), bases: vec![base_class.clone()], keywords, body, 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 cdde59a7e0..fab3d45868 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 @@ -129,7 +129,7 @@ pub(crate) fn super_call_with_parameters( return; }; - if !(first_arg_id == parent_name && second_arg_id == parent_arg) { + if !(first_arg_id == parent_name.as_str() && second_arg_id == parent_arg.as_str()) { return; } diff --git a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs index 105d076112..2cc6f1dbc0 100644 --- a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs +++ b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs @@ -289,7 +289,7 @@ fn generate_fix(checker: &Checker, conversion_type: ConversionType, expr: &Expr) let new_expr = Expr::Subscript(ast::ExprSubscript { range: TextRange::default(), value: Box::new(Expr::Name(ast::ExprName { - id: binding.into(), + id: binding, ctx: ast::ExprContext::Store, range: TextRange::default(), })), 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 c2620b9515..db26e7588f 100644 --- a/crates/ruff/src/rules/tryceratops/rules/useless_try_except.rs +++ b/crates/ruff/src/rules/tryceratops/rules/useless_try_except.rs @@ -50,7 +50,7 @@ pub(crate) fn useless_try_except(checker: &mut Checker, handlers: &[ExceptHandle if let Some(expr) = exc { // E.g., `except ... as e: raise e` if let Expr::Name(ast::ExprName { id, .. }) = expr.as_ref() { - if Some(id) == name.as_ref() { + if name.as_ref().map_or(false, |name| name.as_str() == id) { return Some(Diagnostic::new(UselessTryExcept, handler.range())); } } diff --git a/crates/ruff/src/rules/tryceratops/rules/verbose_log_message.rs b/crates/ruff/src/rules/tryceratops/rules/verbose_log_message.rs index 4852bac33f..da2652d9ed 100644 --- a/crates/ruff/src/rules/tryceratops/rules/verbose_log_message.rs +++ b/crates/ruff/src/rules/tryceratops/rules/verbose_log_message.rs @@ -86,7 +86,7 @@ pub(crate) fn verbose_log_message(checker: &mut Checker, handlers: &[ExceptHandl names }; for expr in names { - if expr.id == *target { + if expr.id == target.as_str() { checker .diagnostics .push(Diagnostic::new(VerboseLogMessage, expr.range())); diff --git a/crates/ruff/src/rules/tryceratops/rules/verbose_raise.rs b/crates/ruff/src/rules/tryceratops/rules/verbose_raise.rs index 56123b43ad..c2829ca707 100644 --- a/crates/ruff/src/rules/tryceratops/rules/verbose_raise.rs +++ b/crates/ruff/src/rules/tryceratops/rules/verbose_raise.rs @@ -95,7 +95,7 @@ pub(crate) fn verbose_raise(checker: &mut Checker, handlers: &[ExceptHandler]) { if let Some(exc) = exc { // ...and the raised object is bound to the same name... if let Expr::Name(ast::ExprName { id, .. }) = exc { - if id == exception_name { + if id == exception_name.as_str() { checker .diagnostics .push(Diagnostic::new(VerboseRaise, exc.range())); diff --git a/crates/ruff_python_ast/src/source_code/generator.rs b/crates/ruff_python_ast/src/source_code/generator.rs index 08d552f8ec..0b3ec2f5d5 100644 --- a/crates/ruff_python_ast/src/source_code/generator.rs +++ b/crates/ruff_python_ast/src/source_code/generator.rs @@ -1191,7 +1191,7 @@ impl<'a> Generator<'a> { self.p("*"); self.unparse_expr(value, precedence::MAX); } - Expr::Name(ast::ExprName { id, .. }) => self.p_id(id), + Expr::Name(ast::ExprName { id, .. }) => self.p(id.as_str()), Expr::List(ast::ExprList { elts, .. }) => { self.p("["); let mut first = true; diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 3c6742508f..eb845933af 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -1,13 +1,16 @@ -use crate::comments::visitor::{CommentPlacement, DecoratedComment}; -use crate::comments::CommentTextPosition; -use crate::trivia::{SimpleTokenizer, Token, TokenKind}; +use std::cmp::Ordering; + +use ruff_text_size::{TextRange, TextSize}; +use rustpython_parser::ast::Ranged; + use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::source_code::Locator; use ruff_python_ast::whitespace; use ruff_python_whitespace::{PythonWhitespace, UniversalNewlines}; -use ruff_text_size::{TextRange, TextSize}; -use rustpython_parser::ast::Ranged; -use std::cmp::Ordering; + +use crate::comments::visitor::{CommentPlacement, DecoratedComment}; +use crate::comments::CommentTextPosition; +use crate::trivia::{SimpleTokenizer, Token, TokenKind}; /// Implements the custom comment placement logic. pub(super) fn place_comment<'a>( @@ -623,8 +626,13 @@ fn handle_positional_only_arguments_separator_comment<'a>( return CommentPlacement::Default(comment); }; - let is_last_positional_argument = are_same_optional(last_argument_or_default, arguments.posonlyargs.last()) - // If the preceding node is the default for the last positional argument + let is_last_positional_argument = + // If the preceding node is the identifier for the last positional argument (`a`). + // ```python + // def test(a, /, b): pass + // ``` + are_same_optional(last_argument_or_default, arguments.posonlyargs.last().map(|arg| &arg.def)) + // If the preceding node is the default for the last positional argument (`10`). // ```python // def test(a=10, /, b): pass // ``` From 7bc33a8d5fc69fb42f8dca174dfd9eb3a7861a2b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 20 Jun 2023 12:07:29 -0400 Subject: [PATCH 126/447] Remove identifier lexing in favor of parser ranges (#5195) ## Summary Now that all identifiers include ranges (#5194), we can remove a ton of this "custom lexing" code that we have to sketchily extract identifier ranges from source. ## Test Plan `cargo test` --- crates/ruff/src/checkers/ast/mod.rs | 37 +-- .../flake8_annotations/rules/definition.rs | 12 +- .../rules/abstract_base_class.rs | 2 +- .../rules/f_string_docstring.rs | 7 +- .../ruff/src/rules/flake8_builtins/helpers.rs | 11 +- .../rules/builtin_attribute_shadowing.rs | 3 +- .../rules/builtin_variable_shadowing.rs | 3 +- ..._flake8_builtins__tests__A001_A001.py.snap | 14 +- ...sts__A001_A001.py_builtins_ignorelist.snap | 14 +- ...ke8_import_conventions__tests__custom.snap | 8 +- ...8_import_conventions__tests__defaults.snap | 2 +- ...port_conventions__tests__from_imports.snap | 2 +- ..._conventions__tests__override_default.snap | 2 +- ...rt_conventions__tests__remove_default.snap | 2 +- .../flake8_pyi/rules/non_self_return_type.rs | 10 +- .../rules/str_or_repr_defined_in_stub.rs | 2 +- .../rules/stub_body_multiple_statements.rs | 2 +- .../flake8_pytest_style/rules/fixture.rs | 4 +- .../rules/no_slots_in_namedtuple_subclass.rs | 2 +- .../rules/no_slots_in_str_subclass.rs | 7 +- .../rules/no_slots_in_tuple_subclass.rs | 7 +- .../mccabe/rules/function_is_too_complex.rs | 4 +- .../pep8_naming/rules/dunder_function_name.rs | 7 +- .../rules/error_suffix_on_exception_name.rs | 4 +- .../pep8_naming/rules/invalid_class_name.rs | 4 +- .../rules/invalid_function_name.rs | 4 +- .../src/rules/pydocstyle/rules/if_needed.rs | 7 +- .../src/rules/pydocstyle/rules/not_missing.rs | 39 +-- .../src/rules/pydocstyle/rules/sections.rs | 2 +- ...ules__pyflakes__tests__F401_F401_0.py.snap | 2 +- ...ules__pyflakes__tests__F401_F401_5.py.snap | 2 +- .../pylint/rules/property_with_parameters.rs | 7 +- .../rules/pylint/rules/too_many_arguments.rs | 2 +- .../rules/pylint/rules/too_many_branches.rs | 4 +- .../rules/too_many_return_statements.rs | 4 +- .../rules/pylint/rules/too_many_statements.rs | 4 +- .../unexpected_special_method_signature.rs | 4 +- .../rules/unnecessary_class_parentheses.rs | 2 +- .../rules/useless_object_inheritance.rs | 2 +- crates/ruff_python_ast/src/identifier.rs | 305 ++---------------- 40 files changed, 134 insertions(+), 428 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 54948a23cd..33e59c3841 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -366,9 +366,7 @@ where } if self.enabled(Rule::AmbiguousFunctionName) { if let Some(diagnostic) = - pycodestyle::rules::ambiguous_function_name(name, || { - stmt.identifier(self.locator) - }) + pycodestyle::rules::ambiguous_function_name(name, || stmt.identifier()) { self.diagnostics.push(diagnostic); } @@ -383,7 +381,6 @@ where decorator_list, &self.settings.pep8_naming.ignore_names, &self.semantic, - self.locator, ) { self.diagnostics.push(diagnostic); } @@ -452,7 +449,6 @@ where stmt, name, &self.settings.pep8_naming.ignore_names, - self.locator, ) { self.diagnostics.push(diagnostic); } @@ -503,7 +499,6 @@ where name, body, self.settings.mccabe.max_complexity, - self.locator, ) { self.diagnostics.push(diagnostic); } @@ -523,7 +518,6 @@ where stmt, body, self.settings.pylint.max_returns, - self.locator, ) { self.diagnostics.push(diagnostic); } @@ -533,7 +527,6 @@ where stmt, body, self.settings.pylint.max_branches, - self.locator, ) { self.diagnostics.push(diagnostic); } @@ -543,7 +536,6 @@ where stmt, body, self.settings.pylint.max_statements, - self.locator, ) { self.diagnostics.push(diagnostic); } @@ -605,7 +597,6 @@ where name, decorator_list, args, - self.locator, ); } if self.enabled(Rule::FStringDocstring) { @@ -693,9 +684,9 @@ where pyupgrade::rules::unnecessary_class_parentheses(self, class_def, stmt); } if self.enabled(Rule::AmbiguousClassName) { - if let Some(diagnostic) = pycodestyle::rules::ambiguous_class_name(name, || { - stmt.identifier(self.locator) - }) { + if let Some(diagnostic) = + pycodestyle::rules::ambiguous_class_name(name, || stmt.identifier()) + { self.diagnostics.push(diagnostic); } } @@ -704,7 +695,6 @@ where stmt, name, &self.settings.pep8_naming.ignore_names, - self.locator, ) { self.diagnostics.push(diagnostic); } @@ -714,7 +704,6 @@ where stmt, bases, name, - self.locator, &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); @@ -813,7 +802,7 @@ where let qualified_name = &alias.name; self.add_binding( name, - alias.identifier(self.locator), + alias.identifier(), BindingKind::SubmoduleImport(SubmoduleImport { qualified_name }), BindingFlags::EXTERNAL, ); @@ -834,7 +823,7 @@ where let qualified_name = &alias.name; self.add_binding( name, - alias.identifier(self.locator), + alias.identifier(), BindingKind::Import(Import { qualified_name }), flags, ); @@ -1052,7 +1041,7 @@ where self.add_binding( name, - alias.identifier(self.locator), + alias.identifier(), BindingKind::FutureImport, BindingFlags::empty(), ); @@ -1123,7 +1112,7 @@ where helpers::format_import_from_member(level, module, &alias.name); self.add_binding( name, - alias.identifier(self.locator), + alias.identifier(), BindingKind::FromImport(FromImport { qualified_name }), flags, ); @@ -1804,7 +1793,7 @@ where self.add_binding( name, - stmt.identifier(self.locator), + stmt.identifier(), BindingKind::FunctionDefinition, BindingFlags::empty(), ); @@ -2029,7 +2018,7 @@ where self.semantic.pop_definition(); self.add_binding( name, - stmt.identifier(self.locator), + stmt.identifier(), BindingKind::ClassDefinition, BindingFlags::empty(), ); @@ -3846,7 +3835,7 @@ where } match name { Some(name) => { - let range = except_handler.try_identifier(self.locator).unwrap(); + let range = except_handler.try_identifier().unwrap(); if self.enabled(Rule::AmbiguousVariableName) { if let Some(diagnostic) = @@ -3961,7 +3950,7 @@ where // upstream. self.add_binding( &arg.arg, - arg.identifier(self.locator), + arg.identifier(), BindingKind::Argument, BindingFlags::empty(), ); @@ -4001,7 +3990,7 @@ where { self.add_binding( name, - pattern.try_identifier(self.locator).unwrap(), + pattern.try_identifier().unwrap(), BindingKind::Assignment, BindingFlags::empty(), ); diff --git a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs index b22555ce78..5fdb814dab 100644 --- a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs @@ -653,7 +653,7 @@ pub(crate) fn definition( MissingReturnTypeClassMethod { name: name.to_string(), }, - stmt.identifier(checker.locator), + stmt.identifier(), )); } } else if is_method @@ -664,7 +664,7 @@ pub(crate) fn definition( MissingReturnTypeStaticMethod { name: name.to_string(), }, - stmt.identifier(checker.locator), + stmt.identifier(), )); } } else if is_method && visibility::is_init(name) { @@ -676,7 +676,7 @@ pub(crate) fn definition( MissingReturnTypeSpecialMethod { name: name.to_string(), }, - stmt.identifier(checker.locator), + stmt.identifier(), ); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { @@ -693,7 +693,7 @@ pub(crate) fn definition( MissingReturnTypeSpecialMethod { name: name.to_string(), }, - stmt.identifier(checker.locator), + stmt.identifier(), ); if checker.patch(diagnostic.kind.rule()) { if let Some(return_type) = simple_magic_return_type(name) { @@ -713,7 +713,7 @@ pub(crate) fn definition( MissingReturnTypeUndocumentedPublicFunction { name: name.to_string(), }, - stmt.identifier(checker.locator), + stmt.identifier(), )); } } @@ -723,7 +723,7 @@ pub(crate) fn definition( MissingReturnTypePrivateFunction { name: name.to_string(), }, - stmt.identifier(checker.locator), + stmt.identifier(), )); } } 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 16606d08ff..679e68e782 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 @@ -134,7 +134,7 @@ pub(crate) fn abstract_base_class( AbstractBaseClassWithoutAbstractMethod { name: name.to_string(), }, - stmt.identifier(checker.locator), + stmt.identifier(), )); } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs b/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs index 8556160ba5..ad48483bda 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs @@ -29,8 +29,7 @@ pub(crate) fn f_string_docstring(checker: &mut Checker, body: &[Stmt]) { let Expr::JoinedStr ( _) = value.as_ref() else { return; }; - checker.diagnostics.push(Diagnostic::new( - FStringDocstring, - stmt.identifier(checker.locator), - )); + checker + .diagnostics + .push(Diagnostic::new(FStringDocstring, stmt.identifier())); } diff --git a/crates/ruff/src/rules/flake8_builtins/helpers.rs b/crates/ruff/src/rules/flake8_builtins/helpers.rs index 2280f084c5..1fe69e0cb4 100644 --- a/crates/ruff/src/rules/flake8_builtins/helpers.rs +++ b/crates/ruff/src/rules/flake8_builtins/helpers.rs @@ -1,8 +1,7 @@ use ruff_text_size::TextRange; use rustpython_parser::ast::{ExceptHandler, Expr, Ranged, Stmt}; -use ruff_python_ast::identifier::Identifier; -use ruff_python_ast::source_code::Locator; +use ruff_python_ast::identifier::{Identifier, TryIdentifier}; use ruff_python_stdlib::builtins::BUILTINS; pub(super) fn shadows_builtin(name: &str, ignorelist: &[String]) -> bool { @@ -16,12 +15,12 @@ pub(crate) enum AnyShadowing<'a> { ExceptHandler(&'a ExceptHandler), } -impl AnyShadowing<'_> { - pub(crate) fn range(self, locator: &Locator) -> TextRange { +impl Identifier for AnyShadowing<'_> { + fn identifier(&self) -> TextRange { match self { AnyShadowing::Expression(expr) => expr.range(), - AnyShadowing::Statement(stmt) => stmt.identifier(locator), - AnyShadowing::ExceptHandler(handler) => handler.range(), + AnyShadowing::Statement(stmt) => stmt.identifier(), + AnyShadowing::ExceptHandler(handler) => handler.try_identifier().unwrap(), } } } diff --git a/crates/ruff/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs b/crates/ruff/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs index 2adb257426..45edcb2ef7 100644 --- a/crates/ruff/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs +++ b/crates/ruff/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs @@ -1,6 +1,7 @@ use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::identifier::Identifier; use rustpython_parser::ast; use crate::checkers::ast::Checker; @@ -83,7 +84,7 @@ pub(crate) fn builtin_attribute_shadowing( BuiltinAttributeShadowing { name: name.to_string(), }, - shadowing.range(checker.locator), + shadowing.identifier(), )); } } diff --git a/crates/ruff/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs b/crates/ruff/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs index d604a29df2..d61aff2e57 100644 --- a/crates/ruff/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs +++ b/crates/ruff/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs @@ -1,6 +1,7 @@ use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; @@ -68,7 +69,7 @@ pub(crate) fn builtin_variable_shadowing( BuiltinVariableShadowing { name: name.to_string(), }, - shadowing.range(checker.locator), + shadowing.identifier(), )); } } diff --git a/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A001_A001.py.snap b/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A001_A001.py.snap index f62577f0f3..4aa57c119f 100644 --- a/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A001_A001.py.snap +++ b/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A001_A001.py.snap @@ -122,15 +122,13 @@ A001.py:16:7: A001 Variable `slice` is shadowing a Python builtin 17 | pass | -A001.py:21:1: A001 Variable `ValueError` is shadowing a Python builtin +A001.py:21:23: A001 Variable `ValueError` is shadowing a Python builtin | -19 | try: -20 | ... -21 | / except ImportError as ValueError: -22 | | ... - | |_______^ A001 -23 | -24 | for memoryview, *bytearray in []: +19 | try: +20 | ... +21 | except ImportError as ValueError: + | ^^^^^^^^^^ A001 +22 | ... | A001.py:24:5: A001 Variable `memoryview` is shadowing a Python builtin diff --git a/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A001_A001.py_builtins_ignorelist.snap b/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A001_A001.py_builtins_ignorelist.snap index 4c47bb5f0c..591ee6781f 100644 --- a/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A001_A001.py_builtins_ignorelist.snap +++ b/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A001_A001.py_builtins_ignorelist.snap @@ -102,15 +102,13 @@ A001.py:16:7: A001 Variable `slice` is shadowing a Python builtin 17 | pass | -A001.py:21:1: A001 Variable `ValueError` is shadowing a Python builtin +A001.py:21:23: A001 Variable `ValueError` is shadowing a Python builtin | -19 | try: -20 | ... -21 | / except ImportError as ValueError: -22 | | ... - | |_______^ A001 -23 | -24 | for memoryview, *bytearray in []: +19 | try: +20 | ... +21 | except ImportError as ValueError: + | ^^^^^^^^^^ A001 +22 | ... | A001.py:24:5: A001 Variable `memoryview` is shadowing a Python builtin diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__custom.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__custom.snap index 8552fb09ea..1b7779d90d 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__custom.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__custom.snap @@ -16,7 +16,7 @@ custom.py:4:8: ICN001 `dask.array` should be imported as `da` | 3 | import altair # unconventional 4 | import dask.array # unconventional - | ^^^^ ICN001 + | ^^^^^^^^^^ ICN001 5 | import dask.dataframe # unconventional 6 | import matplotlib.pyplot # unconventional | @@ -27,7 +27,7 @@ custom.py:5:8: ICN001 `dask.dataframe` should be imported as `dd` 3 | import altair # unconventional 4 | import dask.array # unconventional 5 | import dask.dataframe # unconventional - | ^^^^ ICN001 + | ^^^^^^^^^^^^^^ ICN001 6 | import matplotlib.pyplot # unconventional 7 | import numpy # unconventional | @@ -38,7 +38,7 @@ custom.py:6:8: ICN001 `matplotlib.pyplot` should be imported as `plt` 4 | import dask.array # unconventional 5 | import dask.dataframe # unconventional 6 | import matplotlib.pyplot # unconventional - | ^^^^^^^^^^ ICN001 + | ^^^^^^^^^^^^^^^^^ ICN001 7 | import numpy # unconventional 8 | import pandas # unconventional | @@ -115,7 +115,7 @@ custom.py:13:8: ICN001 `plotly.express` should be imported as `px` 11 | import holoviews # unconventional 12 | import panel # unconventional 13 | import plotly.express # unconventional - | ^^^^^^ ICN001 + | ^^^^^^^^^^^^^^ ICN001 14 | import matplotlib # unconventional 15 | import polars # unconventional | diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__defaults.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__defaults.snap index 89782e181f..42f1d32909 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__defaults.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__defaults.snap @@ -16,7 +16,7 @@ defaults.py:4:8: ICN001 `matplotlib.pyplot` should be imported as `plt` | 3 | import altair # unconventional 4 | import matplotlib.pyplot # unconventional - | ^^^^^^^^^^ ICN001 + | ^^^^^^^^^^^^^^^^^ ICN001 5 | import numpy # unconventional 6 | import pandas # unconventional | diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__from_imports.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__from_imports.snap index f69e23b91c..bda73c8ac3 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__from_imports.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__from_imports.snap @@ -6,7 +6,7 @@ from_imports.py:3:8: ICN001 `xml.dom.minidom` should be imported as `md` 1 | # Test absolute imports 2 | # Violation cases 3 | import xml.dom.minidom - | ^^^ ICN001 + | ^^^^^^^^^^^^^^^ ICN001 4 | import xml.dom.minidom as wrong 5 | from xml.dom import minidom as wrong | diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__override_default.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__override_default.snap index 267ed836dd..a0e6bef88c 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__override_default.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__override_default.snap @@ -16,7 +16,7 @@ override_default.py:4:8: ICN001 `matplotlib.pyplot` should be imported as `plt` | 3 | import altair # unconventional 4 | import matplotlib.pyplot # unconventional - | ^^^^^^^^^^ ICN001 + | ^^^^^^^^^^^^^^^^^ ICN001 5 | import numpy # unconventional 6 | import pandas # unconventional | diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__remove_default.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__remove_default.snap index 60ce87e652..b02135a6cb 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__remove_default.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__remove_default.snap @@ -16,7 +16,7 @@ remove_default.py:4:8: ICN001 `matplotlib.pyplot` should be imported as `plt` | 3 | import altair # unconventional 4 | import matplotlib.pyplot # unconventional - | ^^^^^^^^^^ ICN001 + | ^^^^^^^^^^^^^^^^^ ICN001 5 | import numpy # not checked 6 | import pandas # unconventional | diff --git a/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs b/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs index 78155686c9..04d76b4fb0 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs @@ -148,7 +148,7 @@ pub(crate) fn non_self_return_type( class_name: class_def.name.to_string(), method_name: name.to_string(), }, - stmt.identifier(checker.locator), + stmt.identifier(), )); } return; @@ -162,7 +162,7 @@ pub(crate) fn non_self_return_type( class_name: class_def.name.to_string(), method_name: name.to_string(), }, - stmt.identifier(checker.locator), + stmt.identifier(), )); } return; @@ -177,7 +177,7 @@ pub(crate) fn non_self_return_type( class_name: class_def.name.to_string(), method_name: name.to_string(), }, - stmt.identifier(checker.locator), + stmt.identifier(), )); } return; @@ -193,7 +193,7 @@ pub(crate) fn non_self_return_type( class_name: class_def.name.to_string(), method_name: name.to_string(), }, - stmt.identifier(checker.locator), + stmt.identifier(), )); } } @@ -206,7 +206,7 @@ pub(crate) fn non_self_return_type( class_name: class_def.name.to_string(), method_name: name.to_string(), }, - stmt.identifier(checker.locator), + stmt.identifier(), )); } } 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 54611b75e3..47d1f9f177 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 @@ -90,7 +90,7 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) { StrOrReprDefinedInStub { name: name.to_string(), }, - stmt.identifier(checker.locator), + stmt.identifier(), ); if checker.patch(diagnostic.kind.rule()) { let stmt = checker.semantic().stmt(); diff --git a/crates/ruff/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs b/crates/ruff/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs index bfc0e70857..62872ecc9a 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs @@ -32,6 +32,6 @@ pub(crate) fn stub_body_multiple_statements(checker: &mut Checker, stmt: &Stmt, checker.diagnostics.push(Diagnostic::new( StubBodyMultipleStatements, - stmt.identifier(checker.locator), + stmt.identifier(), )); } 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 502cfb59ea..a34832a93f 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs @@ -379,7 +379,7 @@ fn check_fixture_returns(checker: &mut Checker, stmt: &Stmt, name: &str, body: & PytestIncorrectFixtureNameUnderscore { function: name.to_string(), }, - stmt.identifier(checker.locator), + stmt.identifier(), )); } else if checker.enabled(Rule::PytestMissingFixtureNameUnderscore) && !visitor.has_return_with_value @@ -390,7 +390,7 @@ fn check_fixture_returns(checker: &mut Checker, stmt: &Stmt, name: &str, body: & PytestMissingFixtureNameUnderscore { function: name.to_string(), }, - stmt.identifier(checker.locator), + stmt.identifier(), )); } diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs index 3a56fc07c6..37ec291eab 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs @@ -77,7 +77,7 @@ pub(crate) fn no_slots_in_namedtuple_subclass( if !has_slots(&class.body) { checker.diagnostics.push(Diagnostic::new( NoSlotsInNamedtupleSubclass, - stmt.identifier(checker.locator), + stmt.identifier(), )); } } diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs index a1a5182c61..8d5aa7f886 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs @@ -59,10 +59,9 @@ pub(crate) fn no_slots_in_str_subclass(checker: &mut Checker, stmt: &Stmt, class }) }) { if !has_slots(&class.body) { - checker.diagnostics.push(Diagnostic::new( - NoSlotsInStrSubclass, - stmt.identifier(checker.locator), - )); + checker + .diagnostics + .push(Diagnostic::new(NoSlotsInStrSubclass, stmt.identifier())); } } } diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs index c57cd510b1..bb0cc7e376 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs @@ -63,10 +63,9 @@ pub(crate) fn no_slots_in_tuple_subclass(checker: &mut Checker, stmt: &Stmt, cla }) }) { if !has_slots(&class.body) { - checker.diagnostics.push(Diagnostic::new( - NoSlotsInTupleSubclass, - stmt.identifier(checker.locator), - )); + checker + .diagnostics + .push(Diagnostic::new(NoSlotsInTupleSubclass, stmt.identifier())); } } } diff --git a/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs b/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs index de18d78153..d67de8efc1 100644 --- a/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs +++ b/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{self, ExceptHandler, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::identifier::Identifier; -use ruff_python_ast::source_code::Locator; /// ## What it does /// Checks for functions with a high `McCabe` complexity. @@ -142,7 +141,6 @@ pub(crate) fn function_is_too_complex( name: &str, body: &[Stmt], max_complexity: usize, - locator: &Locator, ) -> Option { let complexity = get_complexity_number(body) + 1; if complexity > max_complexity { @@ -152,7 +150,7 @@ pub(crate) fn function_is_too_complex( complexity, max_complexity, }, - stmt.identifier(locator), + stmt.identifier(), )) } else { None diff --git a/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs b/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs index 4f248b2040..2e0b708469 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::identifier::Identifier; -use ruff_python_ast::source_code::Locator; use ruff_python_semantic::{Scope, ScopeKind}; use crate::settings::types::IdentifierPattern; @@ -48,7 +47,6 @@ pub(crate) fn dunder_function_name( stmt: &Stmt, name: &str, ignore_names: &[IdentifierPattern], - locator: &Locator, ) -> Option { if matches!(scope.kind, ScopeKind::Class(_)) { return None; @@ -67,8 +65,5 @@ pub(crate) fn dunder_function_name( return None; } - Some(Diagnostic::new( - DunderFunctionName, - stmt.identifier(locator), - )) + Some(Diagnostic::new(DunderFunctionName, stmt.identifier())) } diff --git a/crates/ruff/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs b/crates/ruff/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs index 182ba1e404..82edff98eb 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{self, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::identifier::Identifier; -use ruff_python_ast::source_code::Locator; use crate::settings::types::IdentifierPattern; @@ -48,7 +47,6 @@ pub(crate) fn error_suffix_on_exception_name( class_def: &Stmt, bases: &[Expr], name: &str, - locator: &Locator, ignore_names: &[IdentifierPattern], ) -> Option { if ignore_names @@ -75,6 +73,6 @@ pub(crate) fn error_suffix_on_exception_name( ErrorSuffixOnExceptionName { name: name.to_string(), }, - class_def.identifier(locator), + class_def.identifier(), )) } diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_class_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_class_name.rs index c6d7e72e99..e1bd799d9f 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_class_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_class_name.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::identifier::Identifier; -use ruff_python_ast::source_code::Locator; use crate::settings::types::IdentifierPattern; @@ -54,7 +53,6 @@ pub(crate) fn invalid_class_name( class_def: &Stmt, name: &str, ignore_names: &[IdentifierPattern], - locator: &Locator, ) -> Option { if ignore_names .iter() @@ -69,7 +67,7 @@ pub(crate) fn invalid_class_name( InvalidClassName { name: name.to_string(), }, - class_def.identifier(locator), + class_def.identifier(), )); } None diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs index f14ba2c709..71ca191d40 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Decorator, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::identifier::Identifier; -use ruff_python_ast::source_code::Locator; use ruff_python_semantic::analyze::visibility; use ruff_python_semantic::SemanticModel; use ruff_python_stdlib::str; @@ -57,7 +56,6 @@ pub(crate) fn invalid_function_name( decorator_list: &[Decorator], ignore_names: &[IdentifierPattern], semantic: &SemanticModel, - locator: &Locator, ) -> Option { // Ignore any explicitly-ignored function names. if ignore_names @@ -82,6 +80,6 @@ pub(crate) fn invalid_function_name( InvalidFunctionName { name: name.to_string(), }, - stmt.identifier(locator), + stmt.identifier(), )) } diff --git a/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs b/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs index 0f531d8d74..a109e51ea5 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs @@ -30,8 +30,7 @@ pub(crate) fn if_needed(checker: &mut Checker, docstring: &Docstring) { if !is_overload(cast::decorator_list(stmt), checker.semantic()) { return; } - checker.diagnostics.push(Diagnostic::new( - OverloadWithDocstring, - stmt.identifier(checker.locator), - )); + checker + .diagnostics + .push(Diagnostic::new(OverloadWithDocstring, stmt.identifier())); } diff --git a/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs b/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs index 1d78d99444..9db7d71e23 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs @@ -133,10 +133,9 @@ pub(crate) fn not_missing( .. }) => { if checker.enabled(Rule::UndocumentedPublicClass) { - checker.diagnostics.push(Diagnostic::new( - UndocumentedPublicClass, - stmt.identifier(checker.locator), - )); + checker + .diagnostics + .push(Diagnostic::new(UndocumentedPublicClass, stmt.identifier())); } false } @@ -148,7 +147,7 @@ pub(crate) fn not_missing( if checker.enabled(Rule::UndocumentedPublicNestedClass) { checker.diagnostics.push(Diagnostic::new( UndocumentedPublicNestedClass, - stmt.identifier(checker.locator), + stmt.identifier(), )); } false @@ -164,7 +163,7 @@ pub(crate) fn not_missing( if checker.enabled(Rule::UndocumentedPublicFunction) { checker.diagnostics.push(Diagnostic::new( UndocumentedPublicFunction, - stmt.identifier(checker.locator), + stmt.identifier(), )); } false @@ -181,34 +180,30 @@ pub(crate) fn not_missing( true } else if is_init(cast::name(stmt)) { if checker.enabled(Rule::UndocumentedPublicInit) { - checker.diagnostics.push(Diagnostic::new( - UndocumentedPublicInit, - stmt.identifier(checker.locator), - )); + checker + .diagnostics + .push(Diagnostic::new(UndocumentedPublicInit, stmt.identifier())); } true } else if is_new(cast::name(stmt)) || is_call(cast::name(stmt)) { if checker.enabled(Rule::UndocumentedPublicMethod) { - checker.diagnostics.push(Diagnostic::new( - UndocumentedPublicMethod, - stmt.identifier(checker.locator), - )); + checker + .diagnostics + .push(Diagnostic::new(UndocumentedPublicMethod, stmt.identifier())); } true } else if is_magic(cast::name(stmt)) { if checker.enabled(Rule::UndocumentedMagicMethod) { - checker.diagnostics.push(Diagnostic::new( - UndocumentedMagicMethod, - stmt.identifier(checker.locator), - )); + checker + .diagnostics + .push(Diagnostic::new(UndocumentedMagicMethod, stmt.identifier())); } true } else { if checker.enabled(Rule::UndocumentedPublicMethod) { - checker.diagnostics.push(Diagnostic::new( - UndocumentedPublicMethod, - stmt.identifier(checker.locator), - )); + checker + .diagnostics + .push(Diagnostic::new(UndocumentedPublicMethod, stmt.identifier())); } true } diff --git a/crates/ruff/src/rules/pydocstyle/rules/sections.rs b/crates/ruff/src/rules/pydocstyle/rules/sections.rs index 464b87ded5..2050c5f973 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/sections.rs @@ -763,7 +763,7 @@ fn missing_args(checker: &mut Checker, docstring: &Docstring, docstrings_args: & let names = missing_arg_names.into_iter().sorted().collect(); checker.diagnostics.push(Diagnostic::new( UndocumentedParam { names }, - stmt.identifier(checker.locator), + stmt.identifier(), )); } } diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_0.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_0.py.snap index d7ce627b24..67c97bb6ba 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_0.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_0.py.snap @@ -44,7 +44,7 @@ F401_0.py:12:8: F401 [*] `logging.handlers` imported but unused 10 | import multiprocessing.process 11 | import logging.config 12 | import logging.handlers - | ^^^^^^^ F401 + | ^^^^^^^^^^^^^^^^ F401 13 | from typing import ( 14 | TYPE_CHECKING, | diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_5.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_5.py.snap index 2800a738ed..7be6dbbe89 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_5.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_5.py.snap @@ -41,7 +41,7 @@ F401_5.py:4:8: F401 [*] `h.i` imported but unused 2 | from a.b import c 3 | from d.e import f as g 4 | import h.i - | ^ F401 + | ^^^ F401 5 | import j.k as l | = help: Remove unused import: `h.i` diff --git a/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs b/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs index 9f826bf3a3..f88f99d50e 100644 --- a/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs +++ b/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs @@ -68,9 +68,8 @@ pub(crate) fn property_with_parameters( > 1 && checker.semantic().is_builtin("property") { - checker.diagnostics.push(Diagnostic::new( - PropertyWithParameters, - stmt.identifier(checker.locator), - )); + checker + .diagnostics + .push(Diagnostic::new(PropertyWithParameters, stmt.identifier())); } } diff --git a/crates/ruff/src/rules/pylint/rules/too_many_arguments.rs b/crates/ruff/src/rules/pylint/rules/too_many_arguments.rs index c5b396588e..4db54ced09 100644 --- a/crates/ruff/src/rules/pylint/rules/too_many_arguments.rs +++ b/crates/ruff/src/rules/pylint/rules/too_many_arguments.rs @@ -72,7 +72,7 @@ pub(crate) fn too_many_arguments(checker: &mut Checker, arguments: &Arguments, s c_args: num_arguments, max_args: checker.settings.pylint.max_args, }, - stmt.identifier(checker.locator), + stmt.identifier(), )); } } diff --git a/crates/ruff/src/rules/pylint/rules/too_many_branches.rs b/crates/ruff/src/rules/pylint/rules/too_many_branches.rs index a0330cb8cf..5d997ab843 100644 --- a/crates/ruff/src/rules/pylint/rules/too_many_branches.rs +++ b/crates/ruff/src/rules/pylint/rules/too_many_branches.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{self, ExceptHandler, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::identifier::Identifier; -use ruff_python_ast::source_code::Locator; /// ## What it does /// Checks for functions or methods with too many branches. @@ -166,7 +165,6 @@ pub(crate) fn too_many_branches( stmt: &Stmt, body: &[Stmt], max_branches: usize, - locator: &Locator, ) -> Option { let branches = num_branches(body); if branches > max_branches { @@ -175,7 +173,7 @@ pub(crate) fn too_many_branches( branches, max_branches, }, - stmt.identifier(locator), + stmt.identifier(), )) } else { None diff --git a/crates/ruff/src/rules/pylint/rules/too_many_return_statements.rs b/crates/ruff/src/rules/pylint/rules/too_many_return_statements.rs index ef5e45d2d3..6b7f7de10a 100644 --- a/crates/ruff/src/rules/pylint/rules/too_many_return_statements.rs +++ b/crates/ruff/src/rules/pylint/rules/too_many_return_statements.rs @@ -4,7 +4,6 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::ReturnStatementVisitor; use ruff_python_ast::identifier::Identifier; -use ruff_python_ast::source_code::Locator; use ruff_python_ast::statement_visitor::StatementVisitor; /// ## What it does @@ -81,7 +80,6 @@ pub(crate) fn too_many_return_statements( stmt: &Stmt, body: &[Stmt], max_returns: usize, - locator: &Locator, ) -> Option { let returns = num_returns(body); if returns > max_returns { @@ -90,7 +88,7 @@ pub(crate) fn too_many_return_statements( returns, max_returns, }, - stmt.identifier(locator), + stmt.identifier(), )) } else { None diff --git a/crates/ruff/src/rules/pylint/rules/too_many_statements.rs b/crates/ruff/src/rules/pylint/rules/too_many_statements.rs index 2fd832459f..b22bffdbd8 100644 --- a/crates/ruff/src/rules/pylint/rules/too_many_statements.rs +++ b/crates/ruff/src/rules/pylint/rules/too_many_statements.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{self, ExceptHandler, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::identifier::Identifier; -use ruff_python_ast::source_code::Locator; /// ## What it does /// Checks for functions or methods with too many statements. @@ -149,7 +148,6 @@ pub(crate) fn too_many_statements( stmt: &Stmt, body: &[Stmt], max_statements: usize, - locator: &Locator, ) -> Option { let statements = num_statements(body); if statements > max_statements { @@ -158,7 +156,7 @@ pub(crate) fn too_many_statements( statements, max_statements, }, - stmt.identifier(locator), + stmt.identifier(), )) } else { None 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 e7b25da4a5..eb02b3cc9f 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 @@ -5,7 +5,6 @@ use rustpython_parser::ast::{Arguments, Decorator, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::identifier::Identifier; -use ruff_python_ast::source_code::Locator; use ruff_python_semantic::analyze::visibility::is_staticmethod; use crate::checkers::ast::Checker; @@ -143,7 +142,6 @@ pub(crate) fn unexpected_special_method_signature( name: &str, decorator_list: &[Decorator], args: &Arguments, - locator: &Locator, ) { if !checker.semantic().scope().kind.is_class() { return; @@ -188,7 +186,7 @@ pub(crate) fn unexpected_special_method_signature( expected_params, actual_params, }, - stmt.identifier(locator), + stmt.identifier(), )); } } 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 cd2483fd65..5fcb60fa8c 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs @@ -53,7 +53,7 @@ pub(crate) fn unnecessary_class_parentheses( return; } - let offset = stmt.identifier(checker.locator).start(); + let offset = stmt.identifier().start(); 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 ad61c35c8b..aa6ec0e31b 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs @@ -73,7 +73,7 @@ pub(crate) fn useless_object_inheritance( diagnostic.try_set_fix(|| { let edit = remove_argument( checker.locator, - stmt.identifier(checker.locator).start(), + stmt.identifier().start(), expr.range(), &class_def.bases, &class_def.keywords, diff --git a/crates/ruff_python_ast/src/identifier.rs b/crates/ruff_python_ast/src/identifier.rs index 4babb87717..4af761ae83 100644 --- a/crates/ruff_python_ast/src/identifier.rs +++ b/crates/ruff_python_ast/src/identifier.rs @@ -29,13 +29,13 @@ use crate::source_code::Locator; pub trait Identifier { /// Return the [`TextRange`] of the identifier in the given AST node. - fn identifier(&self, locator: &Locator) -> TextRange; + fn identifier(&self) -> TextRange; } pub trait TryIdentifier { /// Return the [`TextRange`] of the identifier in the given AST node, or `None` if /// the node does not have an identifier. - fn try_identifier(&self, locator: &Locator) -> Option; + fn try_identifier(&self) -> Option; } impl Identifier for Stmt { @@ -46,44 +46,11 @@ impl Identifier for Stmt { /// def f(): /// ... /// ``` - fn identifier(&self, locator: &Locator) -> TextRange { + fn identifier(&self) -> TextRange { match self { - Stmt::ClassDef(ast::StmtClassDef { - decorator_list, - range, - .. - }) - | Stmt::FunctionDef(ast::StmtFunctionDef { - decorator_list, - range, - .. - }) => { - let range = decorator_list.last().map_or(*range, |last_decorator| { - TextRange::new(last_decorator.end(), range.end()) - }); - - // The first "identifier" is the `def` or `class` keyword. - // The second "identifier" is the function or class name. - IdentifierTokenizer::starts_at(range.start(), locator.contents()) - .nth(1) - .expect("Unable to identify identifier in function or class definition") - } - Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { - decorator_list, - range, - .. - }) => { - let range = decorator_list.last().map_or(*range, |last_decorator| { - TextRange::new(last_decorator.end(), range.end()) - }); - - // The first "identifier" is the `async` keyword. - // The second "identifier" is the `def` or `class` keyword. - // The third "identifier" is the function or class name. - IdentifierTokenizer::starts_at(range.start(), locator.contents()) - .nth(2) - .expect("Unable to identify identifier in function or class definition") - } + Stmt::ClassDef(ast::StmtClassDef { name, .. }) + | Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) + | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { name, .. }) => name.range(), _ => self.range(), } } @@ -97,10 +64,8 @@ impl Identifier for Arg { /// def f(x: int): /// ... /// ``` - fn identifier(&self, locator: &Locator) -> TextRange { - IdentifierTokenizer::new(locator.contents(), self.range()) - .next() - .expect("Failed to find argument identifier") + fn identifier(&self) -> TextRange { + self.arg.range() } } @@ -112,8 +77,8 @@ impl Identifier for ArgWithDefault { /// def f(x: int = 0): /// ... /// ``` - fn identifier(&self, locator: &Locator) -> TextRange { - self.def.identifier(locator) + fn identifier(&self) -> TextRange { + self.def.identifier() } } @@ -124,22 +89,10 @@ impl Identifier for Alias { /// ```python /// from foo import bar as x /// ``` - fn identifier(&self, locator: &Locator) -> TextRange { - if matches!(self.name.as_str(), "*") { - self.range() - } else if self.asname.is_none() { - // The first identifier is the module name. - IdentifierTokenizer::new(locator.contents(), self.range()) - .next() - .expect("Failed to find alias identifier") - } else { - // The first identifier is the module name. - // The second identifier is the "as" keyword. - // The third identifier is the alias name. - IdentifierTokenizer::new(locator.contents(), self.range()) - .last() - .expect("Failed to find alias identifier") - } + fn identifier(&self) -> TextRange { + self.asname + .as_ref() + .map_or_else(|| self.name.range(), Ranged::range) } } @@ -177,78 +130,20 @@ impl TryIdentifier for Pattern { /// case *z: /// ... /// ``` - fn try_identifier(&self, locator: &Locator) -> Option { - match self { + fn try_identifier(&self) -> Option { + let name = match self { Pattern::MatchAs(ast::PatternMatchAs { - name: Some(_), - pattern, - range, - }) => { - Some(if let Some(pattern) = pattern { - // Identify `z` in: - // ```python - // match x: - // case Foo(bar) as z: - // ... - // ``` - IdentifierTokenizer::starts_at(pattern.end(), locator.contents()) - .nth(1) - .expect("Unable to identify identifier in pattern") - } else { - // Identify `z` in: - // ```python - // match x: - // case z: - // ... - // ``` - *range - }) - } + name: Some(name), .. + }) => Some(name), Pattern::MatchMapping(ast::PatternMatchMapping { - patterns, - rest: Some(_), - .. - }) => { - Some(if let Some(pattern) = patterns.last() { - // Identify `z` in: - // ```python - // match x: - // case {"a": 1, **z} - // ... - // ``` - // - // A mapping pattern can contain at most one double-star pattern, - // and it must be the last pattern in the mapping. - IdentifierTokenizer::starts_at(pattern.end(), locator.contents()) - .next() - .expect("Unable to identify identifier in pattern") - } else { - // Identify `z` in: - // ```python - // match x: - // case {**z} - // ... - // ``` - IdentifierTokenizer::starts_at(self.start(), locator.contents()) - .next() - .expect("Unable to identify identifier in pattern") - }) - } - Pattern::MatchStar(ast::PatternMatchStar { name: Some(_), .. }) => { - // Identify `z` in: - // ```python - // match x: - // case *z: - // ... - // ``` - Some( - IdentifierTokenizer::starts_at(self.start(), locator.contents()) - .next() - .expect("Unable to identify identifier in pattern"), - ) - } + rest: Some(rest), .. + }) => Some(rest), + Pattern::MatchStar(ast::PatternMatchStar { + name: Some(name), .. + }) => Some(name), _ => None, - } + }; + name.map(Ranged::range) } } @@ -262,24 +157,9 @@ impl TryIdentifier for ExceptHandler { /// except ValueError as e: /// ... /// ``` - fn try_identifier(&self, locator: &Locator) -> Option { - let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, name, .. }) = - self; - - if name.is_none() { - return None; - } - - let Some(type_) = type_ else { - return None; - }; - - // The exception name is the first identifier token after the `as` keyword. - Some( - IdentifierTokenizer::starts_at(type_.end(), locator.contents()) - .nth(1) - .expect("Failed to find exception identifier in exception handler"), - ) + fn try_identifier(&self) -> Option { + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { name, .. }) = self; + name.as_ref().map(Ranged::range) } } @@ -481,137 +361,8 @@ mod tests { use rustpython_parser::Parse; use crate::identifier; - use crate::identifier::Identifier; use crate::source_code::Locator; - #[test] - fn extract_arg_range() -> Result<()> { - let contents = "def f(x): pass".trim(); - let stmt = Stmt::parse(contents, "")?; - let function_def = stmt.as_function_def_stmt().unwrap(); - let args = &function_def.args.args; - let arg = &args[0]; - let locator = Locator::new(contents); - assert_eq!( - arg.identifier(&locator), - TextRange::new(TextSize::from(6), TextSize::from(7)) - ); - - let contents = "def f(x: int): pass".trim(); - let stmt = Stmt::parse(contents, "")?; - let function_def = stmt.as_function_def_stmt().unwrap(); - let args = &function_def.args.args; - let arg = &args[0]; - let locator = Locator::new(contents); - assert_eq!( - arg.identifier(&locator), - TextRange::new(TextSize::from(6), TextSize::from(7)) - ); - - let contents = r#" -def f( - x: int, # Comment -): - pass -"# - .trim(); - let stmt = Stmt::parse(contents, "")?; - let function_def = stmt.as_function_def_stmt().unwrap(); - let args = &function_def.args.args; - let arg = &args[0]; - let locator = Locator::new(contents); - assert_eq!( - arg.identifier(&locator), - TextRange::new(TextSize::from(11), TextSize::from(12)) - ); - - Ok(()) - } - - #[test] - fn extract_identifier_range() -> Result<()> { - let contents = "def f(): pass".trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - stmt.identifier(&locator), - TextRange::new(TextSize::from(4), TextSize::from(5)) - ); - - let contents = "async def f(): pass".trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - stmt.identifier(&locator), - TextRange::new(TextSize::from(10), TextSize::from(11)) - ); - - let contents = r#" -def \ - f(): - pass -"# - .trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - stmt.identifier(&locator), - TextRange::new(TextSize::from(8), TextSize::from(9)) - ); - - let contents = "class Class(): pass".trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - stmt.identifier(&locator), - TextRange::new(TextSize::from(6), TextSize::from(11)) - ); - - let contents = "class Class: pass".trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - stmt.identifier(&locator), - TextRange::new(TextSize::from(6), TextSize::from(11)) - ); - - let contents = r#" -@decorator() -class Class(): - pass -"# - .trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - stmt.identifier(&locator), - TextRange::new(TextSize::from(19), TextSize::from(24)) - ); - - let contents = r#" -@decorator() # Comment -class Class(): - pass -"# - .trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - stmt.identifier(&locator), - TextRange::new(TextSize::from(30), TextSize::from(35)) - ); - - let contents = r#"x = y + 1"#.trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - stmt.identifier(&locator), - TextRange::new(TextSize::from(0), TextSize::from(9)) - ); - - Ok(()) - } - #[test] fn extract_else_range() -> Result<()> { let contents = r#" From 6929fcc55f402ed716345dc1a8f2d3c288ee32f7 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Tue, 20 Jun 2023 17:10:58 +0100 Subject: [PATCH 127/447] Complete `flake8-bugbear` documentation (#5178) ## Summary Completes the documentation for the `flake8-bugbear` ruleset. Related to #2646. ## Test Plan `python scripts/check_docs_formatted.py` --------- Co-authored-by: Charlie Marsh --- .../rules/abstract_base_class.rs | 68 +++++++++++++++++++ .../flake8_bugbear/rules/assert_false.rs | 22 ++++++ .../rules/assignment_to_os_environ.rs | 33 +++++++++ .../rules/cached_instance_method.rs | 51 ++++++++++++++ .../rules/duplicate_exceptions.rs | 57 ++++++++++++++++ .../rules/except_with_empty_tuple.rs | 26 +++++++ .../except_with_non_exception_classes.rs | 26 +++++++ .../rules/f_string_docstring.rs | 24 +++++++ .../rules/function_uses_loop_variable.rs | 33 +++++++++ .../rules/getattr_with_constant.rs | 23 +++++++ .../rules/jump_statement_in_finally.rs | 34 ++++++++++ .../rules/loop_variable_overrides_iterator.rs | 27 ++++++++ .../rules/mutable_argument_default.rs | 40 +++++++++++ .../flake8_bugbear/rules/raise_literal.rs | 20 ++++++ .../rules/raise_without_from_inside_except.rs | 38 +++++++++++ .../redundant_tuple_in_exception_handler.rs | 26 +++++++ .../rules/setattr_with_constant.rs | 22 ++++++ .../star_arg_unpacking_after_keyword_arg.rs | 48 ++++++++++--- .../rules/strip_with_multi_characters.rs | 26 +++++++ .../rules/unary_prefix_increment.rs | 39 +++++------ .../rules/unreliable_callable_check.rs | 26 +++++++ .../rules/unused_loop_control_variable.rs | 45 ++++++------ .../rules/useless_comparison.rs | 20 ++++++ .../rules/useless_contextlib_suppress.rs | 30 ++++++++ .../rules/useless_expression.rs | 29 ++++++-- .../rules/zip_without_explicit_strict.rs | 23 +++++++ .../explicit_f_string_type_conversion.rs | 2 +- 27 files changed, 803 insertions(+), 55 deletions(-) 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 679e68e782..796bcd17df 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 @@ -9,6 +9,40 @@ use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; use crate::registry::Rule; +/// ## What it does +/// Checks for abstract classes without abstract methods. +/// +/// ## Why is this bad? +/// Abstract base classes are used to define interfaces. If they have no abstract +/// methods, they are not useful. +/// +/// If the class is not meant to be used as an interface, it should not be an +/// abstract base class. Remove the `ABC` base class from the class definition, +/// or add an abstract method to the class. +/// +/// ## Example +/// ```python +/// from abc import ABC +/// +/// +/// class Foo(ABC): +/// def method(self): +/// bar() +/// ``` +/// +/// Use instead: +/// ```python +/// from abc import ABC, abstractmethod +/// +/// +/// class Foo(ABC): +/// @abstractmethod +/// def method(self): +/// bar() +/// ``` +/// +/// ## References +/// - [Python documentation: `abc`](https://docs.python.org/3/library/abc.html) #[violation] pub struct AbstractBaseClassWithoutAbstractMethod { name: String, @@ -21,6 +55,40 @@ impl Violation for AbstractBaseClassWithoutAbstractMethod { format!("`{name}` is an abstract base class, but it has no abstract methods") } } +/// ## What it does +/// Checks for empty methods in abstract base classes without an abstract +/// decorator. +/// +/// ## Why is this bad? +/// Empty methods in abstract base classes without an abstract decorator are +/// indicative of unfinished code or a mistake. +/// +/// Instead, add an abstract method decorated to indicate that it is abstract, +/// or implement the method. +/// +/// ## Example +/// ```python +/// from abc import ABC +/// +/// +/// class Foo(ABC): +/// def method(self): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// from abc import ABC, abstractmethod +/// +/// +/// class Foo(ABC): +/// @abstractmethod +/// def method(self): +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: abc](https://docs.python.org/3/library/abc.html) #[violation] pub struct EmptyMethodWithoutAbstractDecorator { name: String, diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs b/crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs index 54b9912265..56b43f77df 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs @@ -8,6 +8,28 @@ use ruff_python_ast::helpers::is_const_false; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of `assert False`. +/// +/// ## Why is this bad? +/// Python removes `assert` statements when running in optimized mode +/// (`python -O`), making `assert False` an unreliable means of +/// raising an `AssertionError`. +/// +/// Instead, raise an `AssertionError` directly. +/// +/// ## Example +/// ```python +/// assert False +/// ``` +/// +/// Use instead: +/// ```python +/// raise AssertionError +/// ``` +/// +/// ## References +/// - [Python documentation: `assert`](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement) #[violation] pub struct AssertFalse; 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 fcaa3d6373..dbd9ad3d0e 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 @@ -5,6 +5,39 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for assignments to `os.environ`. +/// +/// ## Why is this bad? +/// In Python, `os.environ` is a mapping that represents the environment of the +/// current process. +/// +/// However, reassigning to `os.environ` does not clear the environment. Instead, +/// it merely updates the `os.environ` for the current process. This can lead to +/// unexpected behavior, especially when running the program in a subprocess. +/// +/// Instead, use `os.environ.clear()` to clear the environment, or use the +/// `env` argument of `subprocess.Popen` to pass a custom environment to +/// a subprocess. +/// +/// ## Example +/// ```python +/// import os +/// +/// os.environ = {"foo": "bar"} +/// ``` +/// +/// Use instead: +/// ```python +/// import os +/// +/// os.environ.clear() +/// os.environ["foo"] = "bar" +/// ``` +/// +/// ## References +/// - [Python documentation: `os.environ`](https://docs.python.org/3/library/os.html#os.environ) +/// - [Python documentation: `subprocess.Popen`](https://docs.python.org/3/library/subprocess.html#subprocess.Popen) #[violation] pub struct AssignmentToOsEnviron; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs b/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs index df291f3725..b12482599d 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs @@ -6,6 +6,57 @@ use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for uses of the `functools.lru_cache` and `functools.cache` +/// decorators on methods. +/// +/// ## Why is this bad? +/// Using the `functools.lru_cache` and `functools.cache` decorators on methods +/// can lead to memory leaks, as the global cache will retain a reference to +/// the instance, preventing it from being garbage collected. +/// +/// Instead, refactor the method to depend only on its arguments and not on the +/// instance of the class, or use the `@lru_cache` decorator on a function +/// outside of the class. +/// +/// ## Example +/// ```python +/// from functools import lru_cache +/// +/// +/// def square(x: int) -> int: +/// return x * x +/// +/// +/// class Number: +/// value: int +/// +/// @lru_cache +/// def squared(self): +/// return square(self.value) +/// ``` +/// +/// Use instead: +/// ```python +/// from functools import lru_cache +/// +/// +/// @lru_cache +/// def square(x: int) -> int: +/// return x * x +/// +/// +/// class Number: +/// value: int +/// +/// def squared(self): +/// return square(self.value) +/// ``` +/// +/// ## References +/// - [Python documentation: `functools.lru_cache`](https://docs.python.org/3/library/functools.html#functools.lru_cache) +/// - [Python documentation: `functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache) +/// - [don't lru_cache methods!](https://www.youtube.com/watch?v=sVjtp6tGo0g) #[violation] pub struct CachedInstanceMethod; 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 8a7e90df79..1e8f8edb18 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs @@ -12,6 +12,33 @@ use ruff_python_ast::call_path::CallPath; use crate::checkers::ast::Checker; use crate::registry::{AsRule, Rule}; +/// ## What it does +/// Checks for `try-except` blocks with duplicate exception handlers. +/// +/// ## Why is this bad? +/// Duplicate exception handlers are redundant, as the first handler will catch +/// the exception, making the second handler unreachable. +/// +/// ## Example +/// ```python +/// try: +/// ... +/// except ValueError: +/// ... +/// except ValueError: +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// try: +/// ... +/// except ValueError: +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) #[violation] pub struct DuplicateTryBlockException { name: String, @@ -24,6 +51,36 @@ impl Violation for DuplicateTryBlockException { format!("try-except block with duplicate exception `{name}`") } } + +/// ## What it does +/// Checks for exception handlers that catch duplicate exceptions. +/// +/// ## Why is this bad? +/// Including the same exception multiple times in the same handler is redundant, +/// as the first exception will catch the exception, making the second exception +/// unreachable. The same applies to exception hierarchies, as a handler for a +/// parent exception (like `Exception`) will also catch child exceptions (like +/// `ValueError`). +/// +/// ## Example +/// ```python +/// try: +/// ... +/// except (Exception, ValueError): # `Exception` includes `ValueError`. +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// try: +/// ... +/// except Exception: +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) +/// - [Python documentation: Exception hierarchy](https://docs.python.org/3/library/exceptions.html#exception-hierarchy) #[violation] pub struct DuplicateHandlerException { pub names: Vec, diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs b/crates/ruff/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs index 594ee99932..1ee0f711ee 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs @@ -6,6 +6,32 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for exception handlers that catch an empty tuple. +/// +/// ## Why is this bad? +/// An exception handler that catches an empty tuple will not catch anything, +/// and is indicative of a mistake. Instead, add exceptions to the `except` +/// clause. +/// +/// ## Example +/// ```python +/// try: +/// 1 / 0 +/// except (): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// try: +/// 1 / 0 +/// except ZeroDivisionError: +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) #[violation] pub struct ExceptWithEmptyTuple; 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 7b7df1fcc7..ab362356af 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 @@ -7,6 +7,32 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for exception handlers that catch non-exception classes. +/// +/// ## Why is this bad? +/// Catching classes that do not inherit from `BaseException` will raise a +/// `TypeError`. +/// +/// ## Example +/// ```python +/// try: +/// 1 / 0 +/// except 1: +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// try: +/// 1 / 0 +/// except ZeroDivisionError: +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) +/// - [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions) #[violation] pub struct ExceptWithNonExceptionClasses; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs b/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs index ad48483bda..02bfbe804c 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs @@ -6,6 +6,30 @@ use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for docstrings that are written via f-strings. +/// +/// ## Why is this bad? +/// Python will interpret the f-string as a joined string, rather than as a +/// docstring. As such, the "docstring" will not be accessible via the +/// `__doc__` attribute, nor will it be picked up by any automated +/// documentation tooling. +/// +/// ## Example +/// ```python +/// def foo(): +/// f"""Not a docstring.""" +/// ``` +/// +/// Use instead: +/// ```python +/// def foo(): +/// """A docstring.""" +/// ``` +/// +/// ## References +/// - [PEP 257](https://peps.python.org/pep-0257/) +/// - [Python documentation: Formatted string literals](https://docs.python.org/3/reference/lexical_analysis.html#f-strings) #[violation] pub struct FStringDocstring; 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 d6d061534f..9741b37c7e 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 @@ -10,6 +10,39 @@ use ruff_python_ast::visitor::Visitor; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for function definitions that use a loop variable. +/// +/// ## Why is this bad? +/// The loop variable is not bound in the function definition, so it will always +/// have the value it had in the last iteration when the function is called. +/// +/// Instead, consider using a default argument to bind the loop variable at +/// function definition time. Or, use `functools.partial`. +/// +/// ## Example +/// ```python +/// adders = [lambda x: x + i for i in range(3)] +/// values = [adder(1) for adder in adders] # [3, 3, 3] +/// ``` +/// +/// Use instead: +/// ```python +/// adders = [lambda x, i=i: x + i for i in range(3)] +/// values = [adder(1) for adder in adders] # [1, 2, 3] +/// ``` +/// +/// Or: +/// ```python +/// from functools import partial +/// +/// adders = [partial(lambda x, i: x + i, i) for i in range(3)] +/// values = [adder(1) for adder in adders] # [1, 2, 3] +/// ``` +/// +/// ## References +/// - [The Hitchhiker's Guide to Python: Late Binding Closures](https://docs.python-guide.org/writing/gotchas/#late-binding-closures) +/// - [Python documentation: functools.partial](https://docs.python.org/3/library/functools.html#functools.partial) #[violation] pub struct FunctionUsesLoopVariable { name: String, 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 29a667eb15..1fa1cd2634 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 @@ -8,6 +8,29 @@ use ruff_python_stdlib::identifiers::{is_identifier, is_mangled_private}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of `getattr` that take a constant attribute value as an +/// argument (e.g., `getattr(obj, "foo")`). +/// +/// ## Why is this bad? +/// `getattr` is used to access attributes dynamically. If the attribute is +/// defined as a constant, it is no safer than a typical property access. When +/// possible, prefer property access over `getattr` calls, as the former is +/// more concise and idiomatic. +/// +/// +/// ## Example +/// ```python +/// getattr(obj, "foo") +/// ``` +/// +/// Use instead: +/// ```python +/// obj.foo +/// ``` +/// +/// ## References +/// - [Python documentation: `getattr`](https://docs.python.org/3/library/functions.html#getattr) #[violation] pub struct GetAttrWithConstant; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/jump_statement_in_finally.rs b/crates/ruff/src/rules/flake8_bugbear/rules/jump_statement_in_finally.rs index df656cafed..15b8aca374 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/jump_statement_in_finally.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/jump_statement_in_finally.rs @@ -5,6 +5,40 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for `break`, `continue`, and `return` statements in `finally` +/// blocks. +/// +/// ## Why is this bad? +/// The use of `break`, `continue`, and `return` statements in `finally` blocks +/// can cause exceptions to be silenced. +/// +/// `finally` blocks execute regardless of whether an exception is raised. If a +/// `break`, `continue`, or `return` statement is reached in a `finally` block, +/// any exception raised in the `try` or `except` blocks will be silenced. +/// +/// ## Example +/// ```python +/// def speed(distance, time): +/// try: +/// return distance / time +/// except ZeroDivisionError: +/// raise ValueError("Time cannot be zero") +/// finally: +/// return 299792458 # `ValueError` is silenced +/// ``` +/// +/// Use instead: +/// ```python +/// def speed(distance, time): +/// try: +/// return distance / time +/// except ZeroDivisionError: +/// raise ValueError("Time cannot be zero") +/// ``` +/// +/// ## References +/// - [Python documentation: The `try` statement](https://docs.python.org/3/reference/compound_stmts.html#the-try-statement) #[violation] pub struct JumpStatementInFinally { name: String, diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs b/crates/ruff/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs index 22012d80b7..8b7efb83ec 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs @@ -8,6 +8,33 @@ use ruff_python_ast::visitor::Visitor; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for loop control variables that override the loop iterable. +/// +/// ## Why is this bad? +/// Loop control variables should not override the loop iterable, as this can +/// lead to confusing behavior. +/// +/// Instead, use a distinct variable name for any loop control variables. +/// +/// ## Example +/// ```python +/// items = [1, 2, 3] +/// +/// for items in items: +/// print(items) +/// ``` +/// +/// Use instead: +/// ```python +/// items = [1, 2, 3] +/// +/// for item in items: +/// print(item) +/// ``` +/// +/// ## References +/// - [Python documentation: The `for` statement](https://docs.python.org/3/reference/compound_stmts.html#the-for-statement) #[violation] pub struct LoopVariableOverridesIterator { name: String, 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 bdae234986..dc2a5ffbfe 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 @@ -6,6 +6,46 @@ use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_ use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for uses of mutable objects as function argument defaults. +/// +/// ## Why is this bad? +/// Function defaults are evaluated once, when the function is defined. +/// +/// The same mutable object is then shared across all calls to the function. +/// If the object is modified, those modifications will persist across calls, +/// which can lead to unexpected behavior. +/// +/// Instead, prefer to use immutable data structures, or take `None` as a +/// default, and initialize a new mutable object inside the function body +/// for each call. +/// +/// ## Example +/// ```python +/// def add_to_list(item, some_list=[]): +/// some_list.append(item) +/// return some_list +/// +/// +/// l1 = add_to_list(0) # [0] +/// l2 = add_to_list(1) # [0, 1] +/// ``` +/// +/// Use instead: +/// ```python +/// def add_to_list(item, some_list=None): +/// if some_list is None: +/// some_list = [] +/// some_list.append(item) +/// return some_list +/// +/// +/// l1 = add_to_list(0) # [0] +/// l2 = add_to_list(1) # [1] +/// ``` +/// +/// ## References +/// - [Python documentation: Default Argument Values](https://docs.python.org/3/tutorial/controlflow.html#default-argument-values) #[violation] pub struct MutableArgumentDefault; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/raise_literal.rs b/crates/ruff/src/rules/flake8_bugbear/rules/raise_literal.rs index 202d4c1c0c..8adc4f0150 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/raise_literal.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/raise_literal.rs @@ -5,6 +5,26 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for `raise` statements that raise a literal value. +/// +/// ## Why is this bad? +/// `raise` must be followed by an exception instance or an exception class, +/// and exceptions must be instances of `BaseException` or a subclass thereof. +/// Raising a literal will raise a `TypeError` at runtime. +/// +/// ## Example +/// ```python +/// raise "foo" +/// ``` +/// +/// Use instead: +/// ```python +/// raise Exception("foo") +/// ``` +/// +/// ## References +/// - [Python documentation: `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement) #[violation] pub struct RaiseLiteral; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs b/crates/ruff/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs index 01a0e2ad4b..3495c16065 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs @@ -8,6 +8,44 @@ use ruff_python_stdlib::str::is_cased_lowercase; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for `raise` statements in exception handlers that lack a `from` +/// clause. +/// +/// ## Why is this bad? +/// In Python, `raise` can be used with or without an exception from which the +/// current exception is derived. This is known as exception chaining. When +/// printing the stack trace, chained exceptions are displayed in such a way +/// so as make it easier to trace the exception back to its root cause. +/// +/// When raising an exception from within an `except` clause, always include a +/// `from` clause to facilitate exception chaining. If the exception is not +/// chained, it will be difficult to trace the exception back to its root cause. +/// +/// ## Example +/// ```python +/// try: +/// ... +/// except FileNotFoundError: +/// if ...: +/// raise RuntimeError("...") +/// else: +/// raise UserWarning("...") +/// ``` +/// +/// Use instead: +/// ```python +/// try: +/// ... +/// except FileNotFoundError as exc: +/// if ...: +/// raise RuntimeError("...") from None +/// else: +/// raise UserWarning("...") from exc +/// ``` +/// +/// ## References +/// - [Python documentation: `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement) #[violation] pub struct RaiseWithoutFromInsideExcept; 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 7802ccf42f..225807a217 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 @@ -6,6 +6,32 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for single-element tuples in exception handlers (e.g., +/// `except (ValueError,):`). +/// +/// ## Why is this bad? +/// A tuple with a single element can be more concisely and idiomatically +/// expressed as a single value. +/// +/// ## Example +/// ```python +/// try: +/// ... +/// except (ValueError,): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// try: +/// ... +/// except ValueError: +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) #[violation] pub struct RedundantTupleInExceptionHandler { name: String, 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 0708af6b87..7d1ddcaf4c 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 @@ -9,6 +9,28 @@ use ruff_python_stdlib::identifiers::{is_identifier, is_mangled_private}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of `setattr` that take a constant attribute value as an +/// argument (e.g., `setattr(obj, "foo", 42)`). +/// +/// ## Why is this bad? +/// `setattr` is used to set attributes dynamically. If the attribute is +/// defined as a constant, it is no safer than a typical property access. When +/// possible, prefer property access over `setattr` calls, as the former is +/// more concise and idiomatic. +/// +/// ## Example +/// ```python +/// setattr(obj, "foo", 42) +/// ``` +/// +/// Use instead: +/// ```python +/// obj.foo = 42 +/// ``` +/// +/// ## References +/// - [Python documentation: `setattr`](https://docs.python.org/3/library/functions.html#setattr) #[violation] pub struct SetAttrWithConstant; 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 287f67035f..f8cb8fa12c 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 @@ -1,12 +1,3 @@ -//! Checks for `f(x=0, *(1, 2))`. -//! -//! ## Why is this bad? -//! -//! Star-arg unpacking after a keyword argument is strongly discouraged. It only -//! works when the keyword parameter is declared after all parameters supplied -//! by the unpacked sequence, and this change of ordering can surprise and -//! mislead readers. - use rustpython_parser::ast::{Expr, Keyword, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; @@ -14,6 +5,45 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for function calls that use star-argument unpacking after providing a +/// keyword argument +/// +/// ## Why is this bad? +/// In Python, you can use star-argument unpacking to pass a list or tuple of +/// arguments to a function. +/// +/// Providing a star-argument after a keyword argument can lead to confusing +/// behavior, and is only supported for backwards compatibility. +/// +/// ## Example +/// ```python +/// def foo(x, y, z): +/// return x, y, z +/// +/// +/// foo(1, 2, 3) # (1, 2, 3) +/// foo(1, *[2, 3]) # (1, 2, 3) +/// # foo(x=1, *[2, 3]) # TypeError +/// # foo(y=2, *[1, 3]) # TypeError +/// foo(z=3, *[1, 2]) # (1, 2, 3) # No error, but confusing! +/// ``` +/// +/// Use instead: +/// ```python +/// def foo(x, y, z): +/// return x, y, z +/// +/// +/// foo(1, 2, 3) # (1, 2, 3) +/// foo(x=1, y=2, z=3) # (1, 2, 3) +/// foo(*[1, 2, 3]) # (1, 2, 3) +/// foo(*[1, 2], 3) # (1, 2, 3) +/// ``` +/// +/// ## References +/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls) +/// - [Disallow iterable argument unpacking after a keyword argument?](https://github.com/python/cpython/issues/82741) #[violation] pub struct StarArgUnpackingAfterKeywordArg; 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 1fd78729c9..75349ba3d9 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 @@ -6,6 +6,32 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for uses of multi-character strings in `.strip()`, `.lstrip()`, and +/// `.rstrip()` calls. +/// +/// ## Why is this bad? +/// All characters in the call to `.strip()`, `.lstrip()`, or `.rstrip()` are +/// removed from the leading and trailing ends of the string. If the string +/// contains multiple characters, the reader may be misled into thinking that +/// a prefix or suffix is being removed, rather than a set of characters. +/// +/// In Python 3.9 and later, you can use `str#removeprefix` and +/// `str#removesuffix` to remove an exact prefix or suffix from a string, +/// respectively, which should be preferred when possible. +/// +/// ## Example +/// ```python +/// "abcba".strip("ab") # "c" +/// ``` +/// +/// Use instead: +/// ```python +/// "abcba".removeprefix("ab").removesuffix("ba") # "c" +/// ``` +/// +/// ## References +/// - [Python documentation: `str.strip`](https://docs.python.org/3/library/stdtypes.html#str.strip) #[violation] pub struct StripWithMultiCharacters; 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 7a5d978ecc..43356e2fa4 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 @@ -1,22 +1,3 @@ -//! Checks for `++n`. -//! -//! ## Why is this bad? -//! -//! Python does not support the unary prefix increment. Writing `++n` is -//! equivalent to `+(+(n))`, which equals `n`. -//! -//! ## Example -//! -//! ```python -//! ++n; -//! ``` -//! -//! Use instead: -//! -//! ```python -//! n += 1 -//! ``` - use rustpython_parser::ast::{self, Expr, Ranged, UnaryOp}; use ruff_diagnostics::{Diagnostic, Violation}; @@ -24,6 +5,26 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for uses of the unary prefix increment operator (e.g., `++n`). +/// +/// ## Why is this bad? +/// Python does not support the unary prefix increment operator. Writing `++n` +/// is equivalent to `+(+(n))`, which is equivalent to `n`. +/// +/// ## Example +/// ```python +/// ++n +/// ``` +/// +/// Use instead: +/// ```python +/// n += 1 +/// ``` +/// +/// ## References +/// - [Python documentation: Unary arithmetic and bitwise operations](https://docs.python.org/3/reference/expressions.html#unary-arithmetic-and-bitwise-operations) +/// - [Python documentation: Augmented assignment statements](https://docs.python.org/3/reference/simple_stmts.html#augmented-assignment-statements) #[violation] pub struct UnaryPrefixIncrement; 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 ec1cff4635..58cfafa186 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 @@ -5,6 +5,32 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for uses of `hasattr` to test if an object is callable (e.g., +/// `hasattr(obj, "__call__")`). +/// +/// ## Why is this bad? +/// Using `hasattr` is an unreliable mechanism for testing if an object is +/// callable. If `obj` implements a custom `__getattr__`, or if its `__call__` +/// is itself not callable, you may get misleading results. +/// +/// Instead, use `callable(obj)` to test if `obj` is callable. +/// +/// ## Example +/// ```python +/// hasattr(obj, "__call__") +/// ``` +/// +/// Use instead: +/// ```python +/// callable(obj) +/// ``` +/// +/// ## References +/// - [Python documentation: `callable`](https://docs.python.org/3/library/functions.html#callable) +/// - [Python documentation: `hasattr`](https://docs.python.org/3/library/functions.html#hasattr) +/// - [Python documentation: `__getattr__`](https://docs.python.org/3/reference/datamodel.html#object.__getattr__) +/// - [Python documentation: `__call__`](https://docs.python.org/3/reference/datamodel.html#object.__call__) #[violation] pub struct UnreliableCallableCheck; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs b/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs index 19432aceed..5eb68d0119 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs @@ -1,23 +1,3 @@ -//! Checks for unused loop variables. -//! -//! ## Why is this bad? -//! -//! Unused variables may signal a mistake or unfinished code. -//! -//! ## Example -//! -//! ```python -//! for x in range(10): -//! method() -//! ``` -//! -//! Prefix the variable with an underscore: -//! -//! ```python -//! for _x in range(10): -//! method() -//! ``` - use rustc_hash::FxHashMap; use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; @@ -35,6 +15,31 @@ enum Certainty { Uncertain, } +/// ## What it does +/// Checks for unused variables in loops (e.g., `for` and `while` statements). +/// +/// ## Why is this bad? +/// Defining a variable in a loop statement that is never used can confuse +/// readers. +/// +/// If the variable is intended to be unused (e.g., to facilitate +/// destructuring of a tuple or other object), prefix it with an underscore +/// to indicate the intent. Otherwise, remove the variable entirely. +/// +/// ## Example +/// ```python +/// for i, j in foo: +/// bar(i) +/// ``` +/// +/// Use instead: +/// ```python +/// for i, _j in foo: +/// bar(i) +/// ``` +/// +/// ## References +/// - [PEP 8: Naming Conventions](https://peps.python.org/pep-0008/#naming-conventions) #[violation] pub struct UnusedLoopControlVariable { /// The name of the loop control variable. diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/useless_comparison.rs b/crates/ruff/src/rules/flake8_bugbear/rules/useless_comparison.rs index 746acc8ec9..1654ef13ce 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/useless_comparison.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/useless_comparison.rs @@ -5,6 +5,26 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for useless comparisons. +/// +/// ## Why is this bad? +/// Useless comparisons have no effect on the program, and are often included +/// by mistake. If the comparison is intended to enforce an invariant, prepend +/// the comparison with an `assert`. Otherwise, remove it entirely. +/// +/// ## Example +/// ```python +/// foo == bar +/// ``` +/// +/// Use instead: +/// ```python +/// assert foo == bar, "`foo` and `bar` should be equal." +/// ``` +/// +/// ## References +/// - [Python documentation: `assert` statement](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement) #[violation] pub struct UselessComparison; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs b/crates/ruff/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs index 5c023d79ca..8e734d70cb 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs @@ -5,6 +5,36 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for `contextlib.suppress` without arguments. +/// +/// ## Why is this bad? +/// `contextlib.suppress` is a context manager that suppresses exceptions. It takes, +/// as arguments, the exceptions to suppress within the enclosed block. If no +/// exceptions are specified, then the context manager won't suppress any +/// exceptions, and is thus redundant. +/// +/// Consider adding exceptions to the `contextlib.suppress` call, or removing the +/// context manager entirely. +/// +/// ## Example +/// ```python +/// import contextlib +/// +/// with contextlib.suppress(): +/// foo() +/// ``` +/// +/// Use instead: +/// ```python +/// import contextlib +/// +/// with contextlib.suppress(Exception): +/// foo() +/// ``` +/// +/// ## References +/// - [Python documentation: contextlib.suppress](https://docs.python.org/3/library/contextlib.html#contextlib.suppress) #[violation] pub struct UselessContextlibSuppress; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/useless_expression.rs b/crates/ruff/src/rules/flake8_bugbear/rules/useless_expression.rs index 9d5f1083d2..24361c1895 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/useless_expression.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/useless_expression.rs @@ -6,12 +6,23 @@ use ruff_python_ast::helpers::contains_effect; use crate::checkers::ast::Checker; -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub(crate) enum Kind { - Expression, - Attribute, -} - +/// ## What it does +/// Checks for useless expressions. +/// +/// ## Why is this bad? +/// Useless expressions have no effect on the program, and are often included +/// by mistake. Assign a useless expression to a variable, or remove it +/// entirely. +/// +/// ## Example +/// ```python +/// 1 + 1 +/// ``` +/// +/// Use instead: +/// ```python +/// foo = 1 + 1 +/// ``` #[violation] pub struct UselessExpression { kind: Kind, @@ -74,3 +85,9 @@ pub(crate) fn useless_expression(checker: &mut Checker, value: &Expr) { value.range(), )); } + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum Kind { + Expression, + Attribute, +} 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 3a605071b5..83854bc6bc 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 @@ -7,6 +7,29 @@ use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for `zip` calls without an explicit `strict` parameter. +/// +/// ## Why is this bad? +/// By default, if the iterables passed to `zip` are of different lengths, the +/// resulting iterator will be silently truncated to the length of the shortest +/// iterable. This can lead to subtle bugs. +/// +/// Use the `strict` parameter to raise a `ValueError` if the iterables are of +/// non-uniform length. +/// +/// ## Example +/// ```python +/// zip(a, b) +/// ``` +/// +/// Use instead: +/// ```python +/// zip(a, b, strict=True) +/// ``` +/// +/// ## References +/// - [Python documentation: `zip`](https://docs.python.org/3/library/functions.html#zip) #[violation] pub struct ZipWithoutExplicitStrict; 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 7561fc24ca..513843feff 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 @@ -15,7 +15,7 @@ use crate::cst::matchers::{match_call_mut, match_expression, match_name}; use crate::registry::AsRule; /// ## What it does -/// Checks for usages of `str()`, `repr()`, and `ascii()` as explicit type +/// Checks for uses of `str()`, `repr()`, and `ascii()` as explicit type /// conversions within f-strings. /// /// ## Why is this bad? From d9e59b21cd850819134e07458bb9846f80c86707 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 20 Jun 2023 18:16:01 +0200 Subject: [PATCH 128/447] Add BestFittingMode (#5184) ## Summary Black supports for layouts when it comes to breaking binary expressions: ```rust #[derive(Copy, Clone, Debug, Eq, PartialEq)] enum BinaryLayout { /// Put each operand on their own line if either side expands Default, /// Try to expand the left to make it fit. Add parentheses if the left or right don't fit. /// ///```python /// [ /// a, /// b /// ] & c ///``` ExpandLeft, /// Try to expand the right to make it fix. Add parentheses if the left or right don't fit. /// /// ```python /// a & [ /// b, /// c /// ] /// ``` ExpandRight, /// Both the left and right side can be expanded. Try in the following order: /// * expand the right side /// * expand the left side /// * expand both sides /// /// to make the expression fit /// /// ```python /// [ /// a, /// b /// ] & [ /// c, /// d /// ] /// ``` ExpandRightThenLeft, } ``` Our current implementation only handles `ExpandRight` and `Default` correctly. `ExpandLeft` turns out to be surprisingly hard. This PR adds a new `BestFittingMode` parameter to `BestFitting` to support `ExpandLeft`. There are 3 variants that `ExpandLeft` must support: **Variant 1**: Everything fits on the line (easy) ```python [a, b] + c ``` **Variant 2**: Left breaks, but right fits on the line. Doesn't need parentheses ```python [ a, b ] + c ``` **Variant 3**: The left breaks, but there's still not enough space for the right hand side. Parenthesize the whole expression: ```python ( [ a, b ] + ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc ) ``` Solving Variant 1 and 2 on their own is straightforward The printer gives us this behavior by nesting right inside of the group of left: ``` group(&format_args![ if_group_breaks(&text("(")), soft_block_indent(&group(&format_args![ left, soft_line_break_or_space(), op, space(), group(&right) ])), if_group_breaks(&text(")")) ]) ``` The fundamental problem is that the outer group, which adds the parentheses, always breaks if the left side breaks. That means, we end up with ```python ( [ a, b ] + c ) ``` which is not what we want (we only want parentheses if the right side doesn't fit). Okay, so nesting groups don't work because of the outer parentheses. Sequencing groups doesn't work because it results in a right-to-left breaking which is the opposite of what we want. Could we use best fitting? Almost! ``` best_fitting![ // All flat format_args![left, space(), op, space(), right], // Break left format_args!(group(&left).should_expand(true), space(), op, space(), right], // Break all format_args![ text("("), block_indent!(&format_args![ left, hard_line_break(), op, space() right ]) ] ] ``` I hope I managed to write this up correctly. The problem is that the printer never reaches the 3rd variant because the second variant always fits: * The `group(&left).should_expand(true)` changes the group so that all `soft_line_breaks` are turned into hard line breaks. This is necessary because we want to test if the content fits if we break after the `[`. * Now, the whole idea of `best_fitting` is that you can pretend that some content fits on the line when it actually does not. The way this works is that the printer **only** tests if all the content of the variant **up to** the first line break fits on the line (we insert that line break by using `should_expand(true))`. The printer doesn't care whether the rest `a\n, b\n ] + c` all fits on (multiple?) lines. Why does breaking right work but not breaking the left? The difference is that we can make the decision whether to parenthesis the expression based on the left expression. We can't do this for breaking left because the decision whether to insert parentheses or not would depend on a lookahead: will the right side break. We simply don't know this yet when printing the parentheses (it would work for the right parentheses but not for the left and indent). What we kind of want here is to tell the printer: Look, what comes here may or may not fit on a single line but we don't care. Simply test that what comes **after** fits on a line. This PR adds a new `BestFittingMode` that has a new `AllLines` option that gives us the desired behavior of testing all content and not just up to the first line break. ## Test Plan I added a new example to `BestFitting::with_mode` --- crates/ruff_formatter/src/builders.rs | 126 ++++++++++++- crates/ruff_formatter/src/format_element.rs | 99 +++++++--- .../src/format_element/document.rs | 18 +- .../ruff_formatter/src/format_element/tag.rs | 10 +- crates/ruff_formatter/src/lib.rs | 2 +- crates/ruff_formatter/src/macros.rs | 6 +- .../ruff_formatter/src/printer/call_stack.rs | 13 +- crates/ruff_formatter/src/printer/mod.rs | 175 +++++++++++------- 8 files changed, 329 insertions(+), 120 deletions(-) diff --git a/crates/ruff_formatter/src/builders.rs b/crates/ruff_formatter/src/builders.rs index 24879eb868..1f3ef8fff1 100644 --- a/crates/ruff_formatter/src/builders.rs +++ b/crates/ruff_formatter/src/builders.rs @@ -2131,11 +2131,12 @@ impl<'a, 'buf, Context> FillBuilder<'a, 'buf, Context> { /// The first variant is the most flat, and the last is the most expanded variant. /// See [`best_fitting!`] macro for a more in-detail documentation #[derive(Copy, Clone)] -pub struct FormatBestFitting<'a, Context> { +pub struct BestFitting<'a, Context> { variants: Arguments<'a, Context>, + mode: BestFittingMode, } -impl<'a, Context> FormatBestFitting<'a, Context> { +impl<'a, Context> BestFitting<'a, Context> { /// Creates a new best fitting IR with the given variants. The method itself isn't unsafe /// but it is to discourage people from using it because the printer will panic if /// the slice doesn't contain at least the least and most expanded variants. @@ -2150,11 +2151,119 @@ impl<'a, Context> FormatBestFitting<'a, Context> { "Requires at least the least expanded and most expanded variants" ); - Self { variants } + Self { + variants, + mode: BestFittingMode::default(), + } + } + + /// Changes the mode used by this best fitting element to determine whether a variant fits. + /// + /// ## Examples + /// + /// ### All Lines + /// + /// ``` + /// use ruff_formatter::{Formatted, LineWidth, format, format_args, SimpleFormatOptions}; + /// use ruff_formatter::prelude::*; + /// + /// # fn main() -> FormatResult<()> { + /// let formatted = format!( + /// SimpleFormatContext::default(), + /// [ + /// best_fitting!( + /// // Everything fits on a single line + /// format_args!( + /// group(&format_args![ + /// text("["), + /// soft_block_indent(&format_args![ + /// text("1,"), + /// soft_line_break_or_space(), + /// text("2,"), + /// soft_line_break_or_space(), + /// text("3"), + /// ]), + /// text("]") + /// ]), + /// space(), + /// text("+"), + /// space(), + /// text("aVeryLongIdentifier") + /// ), + /// + /// // Breaks after `[` and prints each elements on a single line + /// // The group is necessary because the variant, by default is printed in flat mode and a + /// // hard line break indicates that the content doesn't fit. + /// format_args!( + /// text("["), + /// group(&block_indent(&format_args![text("1,"), hard_line_break(), text("2,"), hard_line_break(), text("3")])).should_expand(true), + /// text("]"), + /// space(), + /// text("+"), + /// space(), + /// text("aVeryLongIdentifier") + /// ), + /// + /// // Adds parentheses and indents the body, breaks after the operator + /// format_args!( + /// text("("), + /// block_indent(&format_args![ + /// text("["), + /// block_indent(&format_args![ + /// text("1,"), + /// hard_line_break(), + /// text("2,"), + /// hard_line_break(), + /// text("3"), + /// ]), + /// text("]"), + /// hard_line_break(), + /// text("+"), + /// space(), + /// text("aVeryLongIdentifier") + /// ]), + /// text(")") + /// ) + /// ).with_mode(BestFittingMode::AllLines) + /// ] + /// )?; + /// + /// let document = formatted.into_document(); + /// + /// // Takes the first variant if everything fits on a single line + /// assert_eq!( + /// "[1, 2, 3] + aVeryLongIdentifier", + /// Formatted::new(document.clone(), SimpleFormatContext::default()) + /// .print()? + /// .as_code() + /// ); + /// + /// // It takes the second if the first variant doesn't fit on a single line. The second variant + /// // has some additional line breaks to make sure inner groups don't break + /// assert_eq!( + /// "[\n\t1,\n\t2,\n\t3\n] + aVeryLongIdentifier", + /// Formatted::new(document.clone(), SimpleFormatContext::new(SimpleFormatOptions { line_width: 23.try_into().unwrap(), ..SimpleFormatOptions::default() })) + /// .print()? + /// .as_code() + /// ); + /// + /// // Prints the last option as last resort + /// assert_eq!( + /// "(\n\t[\n\t\t1,\n\t\t2,\n\t\t3\n\t]\n\t+ aVeryLongIdentifier\n)", + /// Formatted::new(document.clone(), SimpleFormatContext::new(SimpleFormatOptions { line_width: 22.try_into().unwrap(), ..SimpleFormatOptions::default() })) + /// .print()? + /// .as_code() + /// ); + /// # Ok(()) + /// # } + /// ``` + pub fn with_mode(mut self, mode: BestFittingMode) -> Self { + self.mode = mode; + self } } -impl Format for FormatBestFitting<'_, Context> { +impl Format for BestFitting<'_, Context> { fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { let mut buffer = VecBuffer::new(f.state_mut()); let variants = self.variants.items(); @@ -2172,9 +2281,12 @@ impl Format for FormatBestFitting<'_, Context> { // SAFETY: The constructor guarantees that there are always at least two variants. It's, therefore, // safe to call into the unsafe `from_vec_unchecked` function let element = unsafe { - FormatElement::BestFitting(format_element::BestFitting::from_vec_unchecked( - formatted_variants, - )) + FormatElement::BestFitting { + variants: format_element::BestFittingVariants::from_vec_unchecked( + formatted_variants, + ), + mode: self.mode, + } }; f.write_element(element) diff --git a/crates/ruff_formatter/src/format_element.rs b/crates/ruff_formatter/src/format_element.rs index a7dd7dedae..5c5b3ff3e9 100644 --- a/crates/ruff_formatter/src/format_element.rs +++ b/crates/ruff_formatter/src/format_element.rs @@ -6,7 +6,7 @@ use std::hash::{Hash, Hasher}; use std::ops::Deref; use std::rc::Rc; -use crate::format_element::tag::{LabelId, Tag}; +use crate::format_element::tag::{GroupMode, LabelId, Tag}; use crate::source_code::SourceCodeSlice; use crate::TagKind; use ruff_text_size::TextSize; @@ -57,7 +57,10 @@ pub enum FormatElement { /// A list of different variants representing the same content. The printer picks the best fitting content. /// Line breaks inside of a best fitting don't propagate to parent groups. - BestFitting(BestFitting), + BestFitting { + variants: BestFittingVariants, + mode: BestFittingMode, + }, /// A [Tag] that marks the start/end of some content to which some special formatting is applied. Tag(Tag), @@ -84,9 +87,11 @@ impl std::fmt::Debug for FormatElement { .field(contains_newlines) .finish(), FormatElement::LineSuffixBoundary => write!(fmt, "LineSuffixBoundary"), - FormatElement::BestFitting(best_fitting) => { - fmt.debug_tuple("BestFitting").field(&best_fitting).finish() - } + FormatElement::BestFitting { variants, mode } => fmt + .debug_struct("BestFitting") + .field("variants", variants) + .field("mode", &mode) + .finish(), FormatElement::Interned(interned) => { fmt.debug_list().entries(interned.deref()).finish() } @@ -134,6 +139,15 @@ impl PrintMode { } } +impl From for PrintMode { + fn from(value: GroupMode) -> Self { + match value { + GroupMode::Flat => PrintMode::Flat, + GroupMode::Expand | GroupMode::Propagated => PrintMode::Expanded, + } + } +} + #[derive(Clone)] pub struct Interned(Rc<[FormatElement]>); @@ -256,7 +270,10 @@ impl FormatElements for FormatElement { FormatElement::Interned(interned) => interned.will_break(), // Traverse into the most flat version because the content is guaranteed to expand when even // the most flat version contains some content that forces a break. - FormatElement::BestFitting(best_fitting) => best_fitting.most_flat().will_break(), + FormatElement::BestFitting { + variants: best_fitting, + .. + } => best_fitting.most_flat().will_break(), FormatElement::LineSuffixBoundary | FormatElement::Space | FormatElement::Tag(_) @@ -284,19 +301,36 @@ impl FormatElements for FormatElement { } } -/// Provides the printer with different representations for the same element so that the printer -/// can pick the best fitting variant. -/// -/// Best fitting is defined as the variant that takes the most horizontal space but fits on the line. -#[derive(Clone, Eq, PartialEq)] -pub struct BestFitting { - /// The different variants for this element. - /// The first element is the one that takes up the most space horizontally (the most flat), - /// The last element takes up the least space horizontally (but most horizontal space). - variants: Box<[Box<[FormatElement]>]>, +/// Mode used to determine if any variant (except the most expanded) fits for [`BestFittingVariants`]. +#[repr(u8)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] +pub enum BestFittingMode { + /// The variant fits if the content up to the first hard or a soft line break inside a [`Group`] with + /// [`PrintMode::Expanded`] fits on the line. The default mode. + /// + /// [`Group`]: tag::Group + #[default] + FirstLine, + + /// A variant fits if all lines fit into the configured print width. A line ends if by any + /// hard or a soft line break inside a [`Group`] with [`PrintMode::Expanded`]. + /// The content doesn't fit if there's any hard line break outside a [`Group`] with [`PrintMode::Expanded`] + /// (a hard line break in content that should be considered in [`PrintMode::Flat`]. + /// + /// Use this mode with caution as it requires measuring all content of the variant which is more + /// expensive than using [`BestFittingMode::FirstLine`]. + /// + /// [`Group`]: tag::Group + AllLines, } -impl BestFitting { +/// The different variants for this element. +/// The first element is the one that takes up the most space horizontally (the most flat), +/// The last element takes up the least space horizontally (but most horizontal space). +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct BestFittingVariants(Box<[Box<[FormatElement]>]>); + +impl BestFittingVariants { /// Creates a new best fitting IR with the given variants. The method itself isn't unsafe /// but it is to discourage people from using it because the printer will panic if /// the slice doesn't contain at least the least and most expanded variants. @@ -312,33 +346,42 @@ impl BestFitting { "Requires at least the least expanded and most expanded variants" ); - Self { - variants: variants.into_boxed_slice(), - } + Self(variants.into_boxed_slice()) } /// Returns the most expanded variant pub fn most_expanded(&self) -> &[FormatElement] { - self.variants.last().expect( + self.0.last().expect( "Most contain at least two elements, as guaranteed by the best fitting builder.", ) } - pub fn variants(&self) -> &[Box<[FormatElement]>] { - &self.variants + pub fn as_slice(&self) -> &[Box<[FormatElement]>] { + &self.0 } /// Returns the least expanded variant pub fn most_flat(&self) -> &[FormatElement] { - self.variants.first().expect( + self.0.first().expect( "Most contain at least two elements, as guaranteed by the best fitting builder.", ) } } -impl std::fmt::Debug for BestFitting { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_list().entries(&*self.variants).finish() +impl Deref for BestFittingVariants { + type Target = [Box<[FormatElement]>]; + + fn deref(&self) -> &Self::Target { + self.as_slice() + } +} + +impl<'a> IntoIterator for &'a BestFittingVariants { + type Item = &'a Box<[FormatElement]>; + type IntoIter = std::slice::Iter<'a, Box<[FormatElement]>>; + + fn into_iter(self) -> Self::IntoIter { + self.as_slice().iter() } } @@ -397,7 +440,7 @@ mod sizes { assert_eq_size!(ruff_text_size::TextRange, [u8; 8]); assert_eq_size!(crate::prelude::tag::VerbatimKind, [u8; 8]); assert_eq_size!(crate::prelude::Interned, [u8; 16]); - assert_eq_size!(crate::format_element::BestFitting, [u8; 16]); + assert_eq_size!(crate::format_element::BestFittingVariants, [u8; 16]); #[cfg(not(debug_assertions))] assert_eq_size!(crate::SourceCodeSlice, [u8; 8]); diff --git a/crates/ruff_formatter/src/format_element/document.rs b/crates/ruff_formatter/src/format_element/document.rs index 2b83cb9907..9799511fc5 100644 --- a/crates/ruff_formatter/src/format_element/document.rs +++ b/crates/ruff_formatter/src/format_element/document.rs @@ -67,10 +67,10 @@ impl Document { interned_expands } }, - FormatElement::BestFitting(best_fitting) => { + FormatElement::BestFitting { variants, mode: _ } => { enclosing.push(Enclosing::BestFitting); - for variant in best_fitting.variants() { + for variant in variants { propagate_expands(variant, enclosing, checked_interned); } @@ -280,14 +280,14 @@ impl Format> for &[FormatElement] { write!(f, [text("line_suffix_boundary")])?; } - FormatElement::BestFitting(best_fitting) => { + FormatElement::BestFitting { variants, mode } => { write!(f, [text("best_fitting([")])?; f.write_elements([ FormatElement::Tag(StartIndent), FormatElement::Line(LineMode::Hard), ])?; - for variant in best_fitting.variants() { + for variant in variants { write!(f, [variant.deref(), hard_line_break()])?; } @@ -296,6 +296,16 @@ impl Format> for &[FormatElement] { FormatElement::Line(LineMode::Hard), ])?; + if *mode != BestFittingMode::AllLines { + write!( + f, + [ + dynamic_text(&std::format!("mode: {mode:?},"), None), + space() + ] + )?; + } + write!(f, [text("])")])?; } diff --git a/crates/ruff_formatter/src/format_element/tag.rs b/crates/ruff_formatter/src/format_element/tag.rs index 38ebbaf1c4..6c8d03c20b 100644 --- a/crates/ruff_formatter/src/format_element/tag.rs +++ b/crates/ruff_formatter/src/format_element/tag.rs @@ -203,8 +203,8 @@ pub enum DedentMode { #[derive(Debug, Clone, Eq, PartialEq)] pub struct Condition { - /// - Flat -> Omitted if the enclosing group is a multiline group, printed for groups fitting on a single line - /// - Multiline -> Omitted if the enclosing group fits on a single line, printed if the group breaks over multiple lines. + /// - `Flat` -> Omitted if the enclosing group is a multiline group, printed for groups fitting on a single line + /// - `Expanded` -> Omitted if the enclosing group fits on a single line, printed if the group breaks over multiple lines. pub(crate) mode: PrintMode, /// The id of the group for which it should check if it breaks or not. The group must appear in the document @@ -213,7 +213,7 @@ pub struct Condition { } impl Condition { - pub fn new(mode: PrintMode) -> Self { + pub(crate) fn new(mode: PrintMode) -> Self { Self { mode, group_id: None, @@ -224,10 +224,6 @@ impl Condition { self.group_id = id; self } - - pub fn mode(&self) -> PrintMode { - self.mode - } } #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/crates/ruff_formatter/src/lib.rs b/crates/ruff_formatter/src/lib.rs index d322b77a7a..4e40deb77d 100644 --- a/crates/ruff_formatter/src/lib.rs +++ b/crates/ruff_formatter/src/lib.rs @@ -48,7 +48,7 @@ pub use buffer::{ Buffer, BufferExtensions, BufferSnapshot, Inspect, PreambleBuffer, RemoveSoftLinesBuffer, VecBuffer, }; -pub use builders::FormatBestFitting; +pub use builders::BestFitting; pub use source_code::{SourceCode, SourceCodeSlice}; pub use crate::diagnostics::{ActualStart, FormatError, InvalidDocumentError, PrintError}; diff --git a/crates/ruff_formatter/src/macros.rs b/crates/ruff_formatter/src/macros.rs index fb6c66e6fa..4f5bcdf050 100644 --- a/crates/ruff_formatter/src/macros.rs +++ b/crates/ruff_formatter/src/macros.rs @@ -320,17 +320,17 @@ macro_rules! format { /// the content up to the first non-soft line break without exceeding the configured print width. /// This definition differs from groups as that non-soft line breaks make group expand. /// -/// [crate::FormatBestFitting] acts as a "break" boundary, meaning that it is considered to fit +/// [crate::BestFitting] acts as a "break" boundary, meaning that it is considered to fit /// /// /// [`Flat`]: crate::format_element::PrintMode::Flat /// [`Expanded`]: crate::format_element::PrintMode::Expanded -/// [`MostExpanded`]: crate::format_element::BestFitting::most_expanded +/// [`MostExpanded`]: crate::format_element::BestFittingVariants::most_expanded #[macro_export] macro_rules! best_fitting { ($least_expanded:expr, $($tail:expr),+ $(,)?) => {{ unsafe { - $crate::FormatBestFitting::from_arguments_unchecked($crate::format_args!($least_expanded, $($tail),+)) + $crate::BestFitting::from_arguments_unchecked($crate::format_args!($least_expanded, $($tail),+)) } }} } diff --git a/crates/ruff_formatter/src/printer/call_stack.rs b/crates/ruff_formatter/src/printer/call_stack.rs index 8bedc9f783..a262f210a7 100644 --- a/crates/ruff_formatter/src/printer/call_stack.rs +++ b/crates/ruff_formatter/src/printer/call_stack.rs @@ -1,7 +1,7 @@ use crate::format_element::tag::TagKind; use crate::format_element::PrintMode; use crate::printer::stack::{Stack, StackedStack}; -use crate::printer::Indention; +use crate::printer::{Indention, MeasureMode}; use crate::{IndentStyle, InvalidDocumentError, PrintError, PrintResult}; use std::fmt::Debug; use std::num::NonZeroU8; @@ -28,6 +28,7 @@ pub(super) struct StackFrame { pub(super) struct PrintElementArgs { indent: Indention, mode: PrintMode, + measure_mode: MeasureMode, } impl PrintElementArgs { @@ -42,6 +43,10 @@ impl PrintElementArgs { self.mode } + pub(super) fn measure_mode(&self) -> MeasureMode { + self.measure_mode + } + pub(super) fn indention(&self) -> Indention { self.indent } @@ -70,6 +75,11 @@ impl PrintElementArgs { self.mode = mode; self } + + pub(crate) fn with_measure_mode(mut self, mode: MeasureMode) -> Self { + self.measure_mode = mode; + self + } } impl Default for PrintElementArgs { @@ -77,6 +87,7 @@ impl Default for PrintElementArgs { Self { indent: Indention::Level(0), mode: PrintMode::Expanded, + measure_mode: MeasureMode::FirstLine, } } } diff --git a/crates/ruff_formatter/src/printer/mod.rs b/crates/ruff_formatter/src/printer/mod.rs index 223a42005d..df4608f733 100644 --- a/crates/ruff_formatter/src/printer/mod.rs +++ b/crates/ruff_formatter/src/printer/mod.rs @@ -4,18 +4,10 @@ mod printer_options; mod queue; mod stack; -pub use printer_options::*; - -use crate::format_element::{BestFitting, LineMode, PrintMode}; -use crate::{ - ActualStart, FormatElement, GroupId, IndentStyle, InvalidDocumentError, PrintError, - PrintResult, Printed, SourceMarker, TextRange, -}; - use crate::format_element::document::Document; -use crate::format_element::tag::Condition; +use crate::format_element::tag::{Condition, GroupMode}; +use crate::format_element::{BestFittingMode, BestFittingVariants, LineMode, PrintMode}; use crate::prelude::tag::{DedentMode, Tag, TagKind, VerbatimKind}; -use crate::prelude::Tag::EndFill; use crate::printer::call_stack::{ CallStack, FitsCallStack, PrintCallStack, PrintElementArgs, StackFrame, }; @@ -24,7 +16,12 @@ use crate::printer::queue::{ AllPredicate, FitsEndPredicate, FitsQueue, PrintQueue, Queue, SingleEntryPredicate, }; use crate::source_code::SourceCode; +use crate::{ + ActualStart, FormatElement, GroupId, IndentStyle, InvalidDocumentError, PrintError, + PrintResult, Printed, SourceMarker, TextRange, +}; use drop_bomb::DebugDropBomb; +pub use printer_options::*; use ruff_text_size::{TextLen, TextSize}; use std::num::NonZeroU8; use unicode_width::UnicodeWidthChar; @@ -137,8 +134,8 @@ impl<'a> Printer<'a> { self.flush_line_suffixes(queue, stack, Some(HARD_BREAK)); } - FormatElement::BestFitting(best_fitting) => { - self.print_best_fitting(best_fitting, queue, stack)?; + FormatElement::BestFitting { variants, mode } => { + self.print_best_fitting(variants, *mode, queue, stack)?; } FormatElement::Interned(content) => { @@ -146,30 +143,31 @@ impl<'a> Printer<'a> { } FormatElement::Tag(StartGroup(group)) => { - let group_mode = if !group.mode().is_flat() { - PrintMode::Expanded - } else { - match args.mode() { - PrintMode::Flat if self.state.measured_group_fits => { - // A parent group has already verified that this group fits on a single line - // Thus, just continue in flat mode - PrintMode::Flat - } - // The printer is either in expanded mode or it's necessary to re-measure if the group fits - // because the printer printed a line break - _ => { - self.state.measured_group_fits = true; - - // Measure to see if the group fits up on a single line. If that's the case, - // print the group in "flat" mode, otherwise continue in expanded mode - stack.push(TagKind::Group, args.with_print_mode(PrintMode::Flat)); - let fits = self.fits(queue, stack)?; - stack.pop(TagKind::Group)?; - - if fits { + let group_mode = match group.mode() { + GroupMode::Expand | GroupMode::Propagated => PrintMode::Expanded, + GroupMode::Flat => { + match args.mode() { + PrintMode::Flat if self.state.measured_group_fits => { + // A parent group has already verified that this group fits on a single line + // Thus, just continue in flat mode PrintMode::Flat - } else { - PrintMode::Expanded + } + // The printer is either in expanded mode or it's necessary to re-measure if the group fits + // because the printer printed a line break + _ => { + self.state.measured_group_fits = true; + + // Measure to see if the group fits up on a single line. If that's the case, + // print the group in "flat" mode, otherwise continue in expanded mode + stack.push(TagKind::Group, args.with_print_mode(PrintMode::Flat)); + let fits = self.fits(queue, stack)?; + stack.pop(TagKind::Group)?; + + if fits { + PrintMode::Flat + } else { + PrintMode::Expanded + } } } } @@ -211,10 +209,10 @@ impl<'a> Printer<'a> { Some(id) => self.state.group_modes.unwrap_print_mode(*id, element), }; - if group_mode != *mode { - queue.skip_content(TagKind::ConditionalContent); - } else { + if *mode == group_mode { stack.push(TagKind::ConditionalContent, args); + } else { + queue.skip_content(TagKind::ConditionalContent); } } @@ -249,6 +247,7 @@ impl<'a> Printer<'a> { FormatElement::Tag(tag @ (StartLabelled(_) | StartEntry)) => { stack.push(tag.kind(), args); } + FormatElement::Tag( tag @ (EndLabelled | EndEntry @@ -371,19 +370,19 @@ impl<'a> Printer<'a> { fn print_best_fitting( &mut self, - best_fitting: &'a BestFitting, + variants: &'a BestFittingVariants, + mode: BestFittingMode, queue: &mut PrintQueue<'a>, stack: &mut PrintCallStack, ) -> PrintResult<()> { let args = stack.top(); if args.mode().is_flat() && self.state.measured_group_fits { - queue.extend_back(best_fitting.most_flat()); + queue.extend_back(variants.most_flat()); self.print_entry(queue, stack, args) } else { self.state.measured_group_fits = true; - - let normal_variants = &best_fitting.variants()[..best_fitting.variants().len() - 1]; + let normal_variants = &variants[..variants.len() - 1]; for variant in normal_variants.iter() { // Test if this variant fits and if so, use it. Otherwise try the next @@ -394,12 +393,14 @@ impl<'a> Printer<'a> { return invalid_start_tag(TagKind::Entry, variant.first()); } - let entry_args = args.with_print_mode(PrintMode::Flat); - // Skip the first element because we want to override the args for the entry and the // args must be popped from the stack as soon as it sees the matching end entry. let content = &variant[1..]; + let entry_args = args + .with_print_mode(PrintMode::Flat) + .with_measure_mode(MeasureMode::from(mode)); + queue.extend_back(content); stack.push(TagKind::Entry, entry_args); let variant_fits = self.fits(queue, stack)?; @@ -411,12 +412,12 @@ impl<'a> Printer<'a> { if variant_fits { queue.extend_back(variant); - return self.print_entry(queue, stack, entry_args); + return self.print_entry(queue, stack, args.with_print_mode(PrintMode::Flat)); } } // No variant fits, take the last (most expanded) as fallback - let most_expanded = best_fitting.most_expanded(); + let most_expanded = variants.most_expanded(); queue.extend_back(most_expanded); self.print_entry(queue, stack, args.with_print_mode(PrintMode::Expanded)) } @@ -555,7 +556,7 @@ impl<'a> Printer<'a> { } } - if queue.top() == Some(&FormatElement::Tag(EndFill)) { + if queue.top() == Some(&FormatElement::Tag(Tag::EndFill)) { Ok(()) } else { invalid_end_tag(TagKind::Fill, stack.top_kind()) @@ -959,8 +960,8 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { FormatElement::Space => return Ok(self.fits_text(" ")), FormatElement::Line(line_mode) => { - if args.mode().is_flat() { - match line_mode { + match args.mode() { + PrintMode::Flat => match line_mode { LineMode::SoftOrSpace => return Ok(self.fits_text(" ")), LineMode::Soft => {} LineMode::Hard | LineMode::Empty => { @@ -970,13 +971,22 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { Fits::Yes }); } + }, + PrintMode::Expanded => { + match args.measure_mode() { + MeasureMode::FirstLine => { + // Reachable if the restQueue contains an element with mode expanded because Expanded + // is what the mode's initialized to by default + // This means, the printer is outside of the current element at this point and any + // line break should be printed as regular line break + return Ok(Fits::Yes); + } + MeasureMode::AllLines => { + // Continue measuring on the next line + self.state.line_width = 0; + } + } } - } else { - // Reachable if the restQueue contains an element with mode expanded because Expanded - // is what the mode's initialized to by default - // This means, the printer is outside of the current element at this point and any - // line break should be printed as regular line break -> Fits - return Ok(Fits::Yes); } } @@ -1000,17 +1010,21 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { FormatElement::SourcePosition(_) => {} - FormatElement::BestFitting(best_fitting) => { - let slice = match args.mode() { - PrintMode::Flat => best_fitting.most_flat(), - PrintMode::Expanded => best_fitting.most_expanded(), + FormatElement::BestFitting { variants, mode } => { + let (slice, args) = match args.mode() { + PrintMode::Flat => ( + variants.most_flat(), + args.with_measure_mode(MeasureMode::from(*mode)), + ), + PrintMode::Expanded => (variants.most_expanded(), args), }; if !matches!(slice.first(), Some(FormatElement::Tag(Tag::StartEntry))) { return invalid_start_tag(TagKind::Entry, slice.first()); } - self.queue.extend_back(slice); + self.stack.push(TagKind::Entry, args); + self.queue.extend_back(&slice[1..]); } FormatElement::Interned(content) => self.queue.extend_back(content), @@ -1040,22 +1054,23 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { return Ok(Fits::No); } - let group_mode = if !group.mode().is_flat() { + // Continue printing groups in expanded mode if measuring a `fits_expanded` element + let print_mode = if !group.mode().is_flat() { PrintMode::Expanded } else { args.mode() }; self.stack - .push(TagKind::Group, args.with_print_mode(group_mode)); + .push(TagKind::Group, args.with_print_mode(print_mode)); if let Some(id) = group.id() { - self.group_modes_mut().insert_print_mode(id, group_mode); + self.group_modes_mut().insert_print_mode(id, print_mode); } } FormatElement::Tag(StartConditionalContent(condition)) => { - let group_mode = match condition.group_id { + let print_mode = match condition.group_id { None => args.mode(), Some(group_id) => self .group_modes() @@ -1063,20 +1078,20 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { .unwrap_or_else(|| args.mode()), }; - if group_mode != condition.mode { - self.queue.skip_content(TagKind::ConditionalContent); - } else { + if condition.mode == print_mode { self.stack.push(TagKind::ConditionalContent, args); + } else { + self.queue.skip_content(TagKind::ConditionalContent); } } FormatElement::Tag(StartIndentIfGroupBreaks(id)) => { - let group_mode = self + let print_mode = self .group_modes() .get_print_mode(*id) .unwrap_or_else(|| args.mode()); - match group_mode { + match print_mode { PrintMode::Flat => { self.stack.push(TagKind::IndentIfGroupBreaks, args); } @@ -1103,6 +1118,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { ) => { self.stack.push(tag.kind(), args); } + FormatElement::Tag( tag @ (EndFill | EndVerbatim @@ -1234,6 +1250,27 @@ struct FitsState { line_width: usize, } +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum MeasureMode { + /// The content fits if a hard line break or soft line break in [`PrintMode::Expanded`] is seen + /// before exceeding the configured print width. + /// Returns + FirstLine, + + /// The content only fits if non of the lines exceed the print width. Lines are terminated by either + /// a hard line break or a soft line break in [`PrintMode::Expanded`]. + AllLines, +} + +impl From for MeasureMode { + fn from(value: BestFittingMode) -> Self { + match value { + BestFittingMode::FirstLine => Self::FirstLine, + BestFittingMode::AllLines => Self::AllLines, + } + } +} + #[cfg(test)] mod tests { use crate::prelude::*; From 6f7d3cc79848a2b0d831775a7ecba27130fd2979 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Tue, 20 Jun 2023 22:16:49 +0530 Subject: [PATCH 129/447] Add option (`-o`/`--output-file`) to write output to a file (#4950) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary A new CLI option (`-o`/`--output-file`) to write output to a file instead of stdout. Major change is to remove the lock acquired on stdout. The argument is that the output is buffered and thus the lock is acquired only when writing a block (8kb). As per the benchmark below there is a slight performance penalty. Reference: https://rustmagazine.org/issue-3/javascript-compiler/#printing-is-slow ## Benchmarks _Output is truncated to only contain useful information:_ Command: `check --isolated --no-cache --select=ALL --show-source ./test-repos/cpython"` Latest HEAD (361d45f2b2f7e69d37d4e6d8270e448f72cae9a7) with and without the manual lock on stdout: ```console Benchmark 1: With lock Time (mean ± σ): 5.687 s ± 0.075 s [User: 17.110 s, System: 0.486 s] Range (min … max): 5.615 s … 5.860 s 10 runs Benchmark 2: Without lock Time (mean ± σ): 5.719 s ± 0.064 s [User: 17.095 s, System: 0.491 s] Range (min … max): 5.640 s … 5.865 s 10 runs Summary (1) ran 1.01 ± 0.02 times faster than (2) ``` This PR: ```console Benchmark 1: This PR Time (mean ± σ): 5.855 s ± 0.058 s [User: 17.197 s, System: 0.491 s] Range (min … max): 5.786 s … 5.987 s 10 runs Benchmark 2: Latest HEAD with lock Time (mean ± σ): 5.645 s ± 0.033 s [User: 16.922 s, System: 0.495 s] Range (min … max): 5.600 s … 5.712 s 10 runs Summary (2) ran 1.04 ± 0.01 times faster than (1) ``` ## Test Plan Run all of the commands which gives output with and without the `--output-file=ruff.out` option: * `--show-settings` * `--show-files` * `--show-fixes` * `--diff` * `--select=ALL` * `--select=All --show-source` * `--watch` (only stdout allowed) resolves: #4754 --- crates/ruff_cli/src/args.rs | 5 ++ crates/ruff_cli/src/commands/show_files.rs | 6 +-- crates/ruff_cli/src/commands/show_settings.rs | 10 ++-- crates/ruff_cli/src/lib.rs | 28 +++++++--- crates/ruff_cli/src/printer.rs | 51 ++++++++++--------- docs/configuration.md | 2 + 6 files changed, 64 insertions(+), 38 deletions(-) diff --git a/crates/ruff_cli/src/args.rs b/crates/ruff_cli/src/args.rs index eb3efc09a5..0199d2f552 100644 --- a/crates/ruff_cli/src/args.rs +++ b/crates/ruff_cli/src/args.rs @@ -107,6 +107,9 @@ pub struct CheckArgs { /// Output serialization format for violations. #[arg(long, value_enum, env = "RUFF_FORMAT")] pub format: Option, + /// Specify file to write the linter output to (default: stdout). + #[arg(short, long)] + pub output_file: Option, /// The minimum Python version that should be supported. #[arg(long, value_enum)] pub target_version: Option, @@ -399,6 +402,7 @@ impl CheckArgs { ignore_noqa: self.ignore_noqa, isolated: self.isolated, no_cache: self.no_cache, + output_file: self.output_file, show_files: self.show_files, show_settings: self.show_settings, statistics: self.statistics, @@ -465,6 +469,7 @@ pub struct Arguments { pub ignore_noqa: bool, pub isolated: bool, pub no_cache: bool, + pub output_file: Option, pub show_files: bool, pub show_settings: bool, pub statistics: bool, diff --git a/crates/ruff_cli/src/commands/show_files.rs b/crates/ruff_cli/src/commands/show_files.rs index 802501b578..7b13474e93 100644 --- a/crates/ruff_cli/src/commands/show_files.rs +++ b/crates/ruff_cli/src/commands/show_files.rs @@ -1,4 +1,4 @@ -use std::io::{self, BufWriter, Write}; +use std::io::Write; use std::path::PathBuf; use anyhow::Result; @@ -14,6 +14,7 @@ pub(crate) fn show_files( files: &[PathBuf], pyproject_config: &PyprojectConfig, overrides: &Overrides, + writer: &mut impl Write, ) -> Result<()> { // Collect all files in the hierarchy. let (paths, _resolver) = resolver::python_files_in_path(files, pyproject_config, overrides)?; @@ -24,13 +25,12 @@ pub(crate) fn show_files( } // Print the list of files. - let mut stdout = BufWriter::new(io::stdout().lock()); for entry in paths .iter() .flatten() .sorted_by(|a, b| a.path().cmp(b.path())) { - writeln!(stdout, "{}", entry.path().to_string_lossy())?; + writeln!(writer, "{}", entry.path().to_string_lossy())?; } Ok(()) diff --git a/crates/ruff_cli/src/commands/show_settings.rs b/crates/ruff_cli/src/commands/show_settings.rs index 48a88b811d..8f91668be0 100644 --- a/crates/ruff_cli/src/commands/show_settings.rs +++ b/crates/ruff_cli/src/commands/show_settings.rs @@ -1,4 +1,4 @@ -use std::io::{self, BufWriter, Write}; +use std::io::Write; use std::path::PathBuf; use anyhow::{bail, Result}; @@ -14,6 +14,7 @@ pub(crate) fn show_settings( files: &[PathBuf], pyproject_config: &PyprojectConfig, overrides: &Overrides, + writer: &mut impl Write, ) -> Result<()> { // Collect all files in the hierarchy. let (paths, resolver) = resolver::python_files_in_path(files, pyproject_config, overrides)?; @@ -28,12 +29,11 @@ pub(crate) fn show_settings( let path = entry.path(); let settings = resolver.resolve(path, pyproject_config); - let mut stdout = BufWriter::new(io::stdout().lock()); - writeln!(stdout, "Resolved settings for: {path:?}")?; + writeln!(writer, "Resolved settings for: {path:?}")?; if let Some(settings_path) = pyproject_config.path.as_ref() { - writeln!(stdout, "Settings path: {settings_path:?}")?; + writeln!(writer, "Settings path: {settings_path:?}")?; } - writeln!(stdout, "{settings:#?}")?; + writeln!(writer, "{settings:#?}")?; Ok(()) } diff --git a/crates/ruff_cli/src/lib.rs b/crates/ruff_cli/src/lib.rs index 01b3e229e1..4b63ad4a07 100644 --- a/crates/ruff_cli/src/lib.rs +++ b/crates/ruff_cli/src/lib.rs @@ -1,3 +1,4 @@ +use std::fs::File; use std::io::{self, stdout, BufWriter, Write}; use std::path::{Path, PathBuf}; use std::process::ExitCode; @@ -171,12 +172,26 @@ pub fn check(args: CheckArgs, log_level: LogLevel) -> Result { cli.stdin_filename.as_deref(), )?; + let mut writer: Box = match cli.output_file { + Some(path) if !cli.watch => { + colored::control::set_override(false); + let file = File::create(path)?; + Box::new(BufWriter::new(file)) + } + _ => Box::new(BufWriter::new(io::stdout())), + }; + if cli.show_settings { - commands::show_settings::show_settings(&cli.files, &pyproject_config, &overrides)?; + commands::show_settings::show_settings( + &cli.files, + &pyproject_config, + &overrides, + &mut writer, + )?; return Ok(ExitStatus::Success); } if cli.show_files { - commands::show_files::show_files(&cli.files, &pyproject_config, &overrides)?; + commands::show_files::show_files(&cli.files, &pyproject_config, &overrides, &mut writer)?; return Ok(ExitStatus::Success); } @@ -276,7 +291,7 @@ pub fn check(args: CheckArgs, log_level: LogLevel) -> Result { noqa.into(), autofix, )?; - printer.write_continuously(&messages)?; + printer.write_continuously(&mut writer, &messages)?; // In watch mode, we may need to re-resolve the configuration. // TODO(charlie): Re-compute other derivative values, like the `printer`. @@ -308,7 +323,7 @@ pub fn check(args: CheckArgs, log_level: LogLevel) -> Result { noqa.into(), autofix, )?; - printer.write_continuously(&messages)?; + printer.write_continuously(&mut writer, &messages)?; } Err(err) => return Err(err.into()), } @@ -341,10 +356,9 @@ pub fn check(args: CheckArgs, log_level: LogLevel) -> Result { // source code goes to stdout). if !(is_stdin && matches!(autofix, flags::FixMode::Apply | flags::FixMode::Diff)) { if cli.statistics { - printer.write_statistics(&diagnostics)?; + printer.write_statistics(&diagnostics, &mut writer)?; } else { - let mut stdout = BufWriter::new(io::stdout().lock()); - printer.write_once(&diagnostics, &mut stdout)?; + printer.write_once(&diagnostics, &mut writer)?; } } diff --git a/crates/ruff_cli/src/printer.rs b/crates/ruff_cli/src/printer.rs index 2062b9e823..2774706703 100644 --- a/crates/ruff_cli/src/printer.rs +++ b/crates/ruff_cli/src/printer.rs @@ -1,8 +1,7 @@ use std::cmp::Reverse; use std::fmt::Display; use std::hash::Hash; -use std::io; -use std::io::{BufWriter, Write}; +use std::io::Write; use anyhow::Result; use bitflags::bitflags; @@ -98,7 +97,7 @@ impl Printer { } } - fn write_summary_text(&self, stdout: &mut dyn Write, diagnostics: &Diagnostics) -> Result<()> { + fn write_summary_text(&self, writer: &mut dyn Write, diagnostics: &Diagnostics) -> Result<()> { if self.log_level >= LogLevel::Default { if self.flags.contains(Flags::SHOW_VIOLATIONS) { let fixed = diagnostics @@ -111,12 +110,12 @@ impl Printer { if fixed > 0 { let s = if total == 1 { "" } else { "s" }; writeln!( - stdout, + writer, "Found {total} error{s} ({fixed} fixed, {remaining} remaining)." )?; } else if remaining > 0 { let s = if remaining == 1 { "" } else { "s" }; - writeln!(stdout, "Found {remaining} error{s}.")?; + writeln!(writer, "Found {remaining} error{s}.")?; } if show_fix_status(self.autofix_level) { @@ -127,7 +126,7 @@ impl Printer { .count(); if num_fixable > 0 { writeln!( - stdout, + writer, "[{}] {num_fixable} potentially fixable with the --fix option.", "*".cyan(), )?; @@ -142,9 +141,9 @@ impl Printer { if fixed > 0 { let s = if fixed == 1 { "" } else { "s" }; if self.autofix_level.is_apply() { - writeln!(stdout, "Fixed {fixed} error{s}.")?; + writeln!(writer, "Fixed {fixed} error{s}.")?; } else { - writeln!(stdout, "Would fix {fixed} error{s}.")?; + writeln!(writer, "Would fix {fixed} error{s}.")?; } } } @@ -155,7 +154,7 @@ impl Printer { pub(crate) fn write_once( &self, diagnostics: &Diagnostics, - writer: &mut impl Write, + writer: &mut dyn Write, ) -> Result<()> { if matches!(self.log_level, LogLevel::Silent) { return Ok(()); @@ -241,7 +240,11 @@ impl Printer { Ok(()) } - pub(crate) fn write_statistics(&self, diagnostics: &Diagnostics) -> Result<()> { + pub(crate) fn write_statistics( + &self, + diagnostics: &Diagnostics, + writer: &mut dyn Write, + ) -> Result<()> { let statistics: Vec = diagnostics .messages .iter() @@ -277,7 +280,6 @@ impl Printer { return Ok(()); } - let mut stdout = BufWriter::new(io::stdout().lock()); match self.format { SerializationFormat::Text => { // Compute the maximum number of digits in the count and code, for all messages, @@ -302,7 +304,7 @@ impl Printer { // By default, we mimic Flake8's `--statistics` format. for statistic in statistics { writeln!( - stdout, + writer, "{:>count_width$}\t{: { - writeln!(stdout, "{}", serde_json::to_string_pretty(&statistics)?)?; + writeln!(writer, "{}", serde_json::to_string_pretty(&statistics)?)?; } _ => { anyhow::bail!( @@ -331,12 +333,16 @@ impl Printer { } } - stdout.flush()?; + writer.flush()?; Ok(()) } - pub(crate) fn write_continuously(&self, diagnostics: &Diagnostics) -> Result<()> { + pub(crate) fn write_continuously( + &self, + writer: &mut dyn Write, + diagnostics: &Diagnostics, + ) -> Result<()> { if matches!(self.log_level, LogLevel::Silent) { return Ok(()); } @@ -353,19 +359,18 @@ impl Printer { ); } - let mut stdout = BufWriter::new(io::stdout().lock()); if !diagnostics.messages.is_empty() { if self.log_level >= LogLevel::Default { - writeln!(stdout)?; + writeln!(writer)?; } let context = EmitterContext::new(&diagnostics.source_kind); TextEmitter::default() .with_show_fix_status(show_fix_status(self.autofix_level)) .with_show_source(self.flags.contains(Flags::SHOW_SOURCE)) - .emit(&mut stdout, &diagnostics.messages, &context)?; + .emit(writer, &diagnostics.messages, &context)?; } - stdout.flush()?; + writer.flush()?; Ok(()) } @@ -394,7 +399,7 @@ const fn show_fix_status(autofix_level: flags::FixMode) -> bool { !autofix_level.is_apply() } -fn print_fix_summary(stdout: &mut T, fixed: &FxHashMap) -> Result<()> { +fn print_fix_summary(writer: &mut dyn Write, fixed: &FxHashMap) -> Result<()> { let total = fixed .values() .map(|table| table.values().sum::()) @@ -410,14 +415,14 @@ fn print_fix_summary(stdout: &mut T, fixed: &FxHashMap(stdout: &mut T, fixed: &FxHashMapnum_digits$} × {} ({})", rule.noqa_code().to_string().red().bold(), rule.as_ref(), diff --git a/docs/configuration.md b/docs/configuration.md index 91897bac80..538e19ce8f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -212,6 +212,8 @@ Options: Ignore any `# noqa` comments --format Output serialization format for violations [env: RUFF_FORMAT=] [possible values: text, json, json-lines, junit, grouped, github, gitlab, pylint, azure] + -o, --output-file + Specify file to write the linter output to (default: stdout) --target-version The minimum Python version that should be supported [possible values: py37, py38, py39, py310, py311] --config From b36928883329ffcf7d11899a195ad13aa9dbfab3 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 20 Jun 2023 18:49:21 +0200 Subject: [PATCH 130/447] Accept any `Into` as `Comments` arguments (#5205) --- .../ruff_python_formatter/src/comments/mod.rs | 71 ++++++++++++++----- .../src/expression/expr_bin_op.rs | 2 +- .../src/expression/expr_dict.rs | 2 +- .../src/expression/expr_list.rs | 2 +- crates/ruff_python_formatter/src/lib.rs | 14 ++-- crates/ruff_python_formatter/src/prelude.rs | 4 +- .../src/statement/stmt_function_def.rs | 2 +- .../src/statement/stmt_if.rs | 4 +- .../src/statement/suite.rs | 13 ++-- 9 files changed, 73 insertions(+), 41 deletions(-) diff --git a/crates/ruff_python_formatter/src/comments/mod.rs b/crates/ruff_python_formatter/src/comments/mod.rs index 171073d565..837ce8266e 100644 --- a/crates/ruff_python_formatter/src/comments/mod.rs +++ b/crates/ruff_python_formatter/src/comments/mod.rs @@ -260,76 +260,109 @@ impl<'a> Comments<'a> { } #[inline] - pub(crate) fn has_comments(&self, node: AnyNodeRef) -> bool { - self.data.comments.has(&NodeRefEqualityKey::from_ref(node)) + pub(crate) fn has_comments(&self, node: T) -> bool + where + T: Into>, + { + self.data + .comments + .has(&NodeRefEqualityKey::from_ref(node.into())) } /// Returns `true` if the given `node` has any [leading comments](self#leading-comments). #[inline] - pub(crate) fn has_leading_comments(&self, node: AnyNodeRef) -> bool { + pub(crate) fn has_leading_comments(&self, node: T) -> bool + where + T: Into>, + { !self.leading_comments(node).is_empty() } /// Returns the `node`'s [leading comments](self#leading-comments). #[inline] - pub(crate) fn leading_comments(&self, node: AnyNodeRef<'a>) -> &[SourceComment] { + pub(crate) fn leading_comments(&self, node: T) -> &[SourceComment] + where + T: Into>, + { self.data .comments - .leading(&NodeRefEqualityKey::from_ref(node)) + .leading(&NodeRefEqualityKey::from_ref(node.into())) } /// Returns `true` if node has any [dangling comments](self#dangling-comments). - pub(crate) fn has_dangling_comments(&self, node: AnyNodeRef<'a>) -> bool { + pub(crate) fn has_dangling_comments(&self, node: T) -> bool + where + T: Into>, + { !self.dangling_comments(node).is_empty() } /// Returns the [dangling comments](self#dangling-comments) of `node` - pub(crate) fn dangling_comments(&self, node: AnyNodeRef<'a>) -> &[SourceComment] { + pub(crate) fn dangling_comments(&self, node: T) -> &[SourceComment] + where + T: Into>, + { self.data .comments - .dangling(&NodeRefEqualityKey::from_ref(node)) + .dangling(&NodeRefEqualityKey::from_ref(node.into())) } /// Returns the `node`'s [trailing comments](self#trailing-comments). #[inline] - pub(crate) fn trailing_comments(&self, node: AnyNodeRef<'a>) -> &[SourceComment] { + pub(crate) fn trailing_comments(&self, node: T) -> &[SourceComment] + where + T: Into>, + { self.data .comments - .trailing(&NodeRefEqualityKey::from_ref(node)) + .trailing(&NodeRefEqualityKey::from_ref(node.into())) } /// Returns `true` if the given `node` has any [trailing comments](self#trailing-comments). #[inline] - pub(crate) fn has_trailing_comments(&self, node: AnyNodeRef) -> bool { + pub(crate) fn has_trailing_comments(&self, node: T) -> bool + where + T: Into>, + { !self.trailing_comments(node).is_empty() } /// Returns `true` if the given `node` has any [trailing own line comments](self#trailing-comments). #[inline] - pub(crate) fn has_trailing_own_line_comments(&self, node: AnyNodeRef) -> bool { + pub(crate) fn has_trailing_own_line_comments(&self, node: T) -> bool + where + T: Into>, + { self.trailing_comments(node) .iter() .any(|comment| comment.position().is_own_line()) } /// Returns an iterator over the [leading](self#leading-comments) and [trailing comments](self#trailing-comments) of `node`. - pub(crate) fn leading_trailing_comments( + pub(crate) fn leading_trailing_comments( &self, - node: AnyNodeRef<'a>, - ) -> impl Iterator { + node: T, + ) -> impl Iterator + where + T: Into>, + { + let node = node.into(); self.leading_comments(node) .iter() .chain(self.trailing_comments(node).iter()) } /// Returns an iterator over the [leading](self#leading-comments), [dangling](self#dangling-comments), and [trailing](self#trailing) comments of `node`. - pub(crate) fn leading_dangling_trailing_comments( + pub(crate) fn leading_dangling_trailing_comments( &self, - node: AnyNodeRef<'a>, - ) -> impl Iterator { + node: T, + ) -> impl Iterator + where + T: Into>, + { self.data .comments - .parts(&NodeRefEqualityKey::from_ref(node)) + .parts(&NodeRefEqualityKey::from_ref(node.into())) } #[inline(always)] diff --git a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs index 92461b4dfe..f9a8723728 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs @@ -91,7 +91,7 @@ impl FormatNodeRule for FormatExprBinOp { )?; // Format the operator on its own line if the right side has any leading comments. - if comments.has_leading_comments(right.as_ref().into()) { + if comments.has_leading_comments(right.as_ref()) { write!(f, [hard_line_break()])?; } else if needs_space { write!(f, [space()])?; diff --git a/crates/ruff_python_formatter/src/expression/expr_dict.rs b/crates/ruff_python_formatter/src/expression/expr_dict.rs index 11d1a3ca85..e40574ee24 100644 --- a/crates/ruff_python_formatter/src/expression/expr_dict.rs +++ b/crates/ruff_python_formatter/src/expression/expr_dict.rs @@ -35,7 +35,7 @@ impl Format> for KeyValuePair<'_> { ) } else { let comments = f.context().comments().clone(); - let leading_value_comments = comments.leading_comments(self.value.into()); + let leading_value_comments = comments.leading_comments(self.value); write!( f, [ diff --git a/crates/ruff_python_formatter/src/expression/expr_list.rs b/crates/ruff_python_formatter/src/expression/expr_list.rs index a269853579..2e6c055315 100644 --- a/crates/ruff_python_formatter/src/expression/expr_list.rs +++ b/crates/ruff_python_formatter/src/expression/expr_list.rs @@ -19,7 +19,7 @@ impl FormatNodeRule for FormatExprList { } = item; let comments = f.context().comments().clone(); - let dangling = comments.dangling_comments(item.into()); + let dangling = comments.dangling_comments(item); // The empty list is special because there can be dangling comments, and they can be in two // positions: diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 6ad37c5665..3e38b1d487 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -1,3 +1,7 @@ +use crate::comments::{ + dangling_node_comments, leading_node_comments, trailing_node_comments, Comments, +}; +use crate::context::PyFormatContext; use anyhow::{anyhow, Context, Result}; use ruff_formatter::prelude::*; use ruff_formatter::{format, write}; @@ -10,11 +14,6 @@ use rustpython_parser::lexer::lex; use rustpython_parser::{parse_tokens, Mode}; use std::borrow::Cow; -use crate::comments::{ - dangling_node_comments, leading_node_comments, trailing_node_comments, Comments, -}; -use crate::context::PyFormatContext; - pub(crate) mod builders; pub mod cli; mod comments; @@ -437,9 +436,10 @@ def with_leading_comment(): ... // Uncomment the `dbg` to print the IR. // Use `dbg_write!(f, []) instead of `write!(f, [])` in your formatting code to print some IR // inside of a `Format` implementation - // dbg!(formatted + // use ruff_formatter::FormatContext; + // formatted // .document() - // .display(formatted.context().source_code())); + // .display(formatted.context().source_code()); // dbg!(formatted // .context() diff --git a/crates/ruff_python_formatter/src/prelude.rs b/crates/ruff_python_formatter/src/prelude.rs index 7382684dba..f3f88f6145 100644 --- a/crates/ruff_python_formatter/src/prelude.rs +++ b/crates/ruff_python_formatter/src/prelude.rs @@ -1,7 +1,7 @@ #[allow(unused_imports)] pub(crate) use crate::{ - builders::PyFormatterExtensions, AsFormat, FormattedIterExt as _, IntoFormat, PyFormatContext, - PyFormatter, + builders::PyFormatterExtensions, AsFormat, FormatNodeRule, FormattedIterExt as _, IntoFormat, + PyFormatContext, PyFormatter, }; #[allow(unused_imports)] pub(crate) use ruff_formatter::prelude::*; diff --git a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs index ed48d4820d..12432c8b1d 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs @@ -37,7 +37,7 @@ impl FormatRule, PyFormatContext<'_>> for FormatAnyFun ) -> FormatResult<()> { let comments = f.context().comments().clone(); - let dangling_comments = comments.dangling_comments(item.into()); + let dangling_comments = comments.dangling_comments(item); let trailing_definition_comments_start = dangling_comments.partition_point(|comment| comment.position().is_own_line()); diff --git a/crates/ruff_python_formatter/src/statement/stmt_if.rs b/crates/ruff_python_formatter/src/statement/stmt_if.rs index 33fad5466f..85a9fabee1 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_if.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_if.rs @@ -23,7 +23,7 @@ impl FormatNodeRule for FormatStmtIf { } = current_statement; let first_statement = body.first().ok_or(FormatError::SyntaxError)?; - let trailing = comments.dangling_comments(current_statement.into()); + let trailing = comments.dangling_comments(current_statement); let trailing_if_comments_end = trailing .partition_point(|comment| comment.slice().start() < first_statement.start()); @@ -32,7 +32,7 @@ impl FormatNodeRule for FormatStmtIf { trailing.split_at(trailing_if_comments_end); if current.is_elif() { - let elif_leading = comments.leading_comments(current_statement.into()); + let elif_leading = comments.leading_comments(current_statement); // Manually format the leading comments because the formatting bypasses `NodeRule::fmt` write!( f, diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index 3c075cf862..0a264a6214 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -96,13 +96,12 @@ impl FormatRule> for FormatSuite { // the leading comment. This is why the suite handling counts the lines before the // start of the next statement or before the first leading comments for compound statements. let separator = format_with(|f| { - let start = if let Some(first_leading) = - comments.leading_comments(statement.into()).first() - { - first_leading.slice().start() - } else { - statement.start() - }; + let start = + if let Some(first_leading) = comments.leading_comments(statement).first() { + first_leading.slice().start() + } else { + statement.start() + }; match lines_before(start, source) { 0 | 1 => hard_line_break().fmt(f), From 310abc769d52b62bafda6a0ba8b0a152bd0792bc Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 20 Jun 2023 13:12:46 -0400 Subject: [PATCH 131/447] Move `StarImport` to its own module (#5186) --- crates/ruff_python_semantic/src/binding.rs | 8 -------- crates/ruff_python_semantic/src/lib.rs | 2 ++ crates/ruff_python_semantic/src/scope.rs | 3 ++- crates/ruff_python_semantic/src/star_import.rs | 7 +++++++ 4 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 crates/ruff_python_semantic/src/star_import.rs diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index 75841038f9..833ad4effe 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -272,14 +272,6 @@ impl<'a> FromIterator> for Bindings<'a> { } } -#[derive(Debug, Clone)] -pub struct StarImport<'a> { - /// The level of the import. `None` or `Some(0)` indicate an absolute import. - pub level: Option, - /// The module being imported. `None` indicates a wildcard import. - pub module: Option<&'a str>, -} - #[derive(Debug, Clone)] pub struct Export<'a> { /// The names of the bindings exported via `__all__`. diff --git a/crates/ruff_python_semantic/src/lib.rs b/crates/ruff_python_semantic/src/lib.rs index e47b73ca16..3b7ce9a7a5 100644 --- a/crates/ruff_python_semantic/src/lib.rs +++ b/crates/ruff_python_semantic/src/lib.rs @@ -7,6 +7,7 @@ mod model; mod node; mod reference; mod scope; +mod star_import; pub use binding::*; pub use context::*; @@ -16,3 +17,4 @@ pub use model::*; pub use node::*; pub use reference::*; pub use scope::*; +pub use star_import::*; diff --git a/crates/ruff_python_semantic/src/scope.rs b/crates/ruff_python_semantic/src/scope.rs index 711572f861..f64148b56e 100644 --- a/crates/ruff_python_semantic/src/scope.rs +++ b/crates/ruff_python_semantic/src/scope.rs @@ -8,8 +8,9 @@ use rustpython_parser::ast; use ruff_index::{newtype_index, Idx, IndexSlice, IndexVec}; -use crate::binding::{BindingId, StarImport}; +use crate::binding::BindingId; use crate::globals::GlobalsId; +use crate::star_import::StarImport; #[derive(Debug)] pub struct Scope<'a> { diff --git a/crates/ruff_python_semantic/src/star_import.rs b/crates/ruff_python_semantic/src/star_import.rs new file mode 100644 index 0000000000..53055a53b8 --- /dev/null +++ b/crates/ruff_python_semantic/src/star_import.rs @@ -0,0 +1,7 @@ +#[derive(Debug, Clone)] +pub struct StarImport<'a> { + /// The level of the import. `None` or `Some(0)` indicate an absolute import. + pub level: Option, + /// The module being imported. `None` indicates a wildcard import. + pub module: Option<&'a str>, +} From 4547002eb7b81c909c77d691b74119ddad7456bd Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 20 Jun 2023 13:16:00 -0400 Subject: [PATCH 132/447] Remove defaults from fixtures/pyproject.toml (#5217) ## Summary These should be encoded in the tests themselves, rather than here. In fact, I think they're all unused? --- .../resources/test/fixtures/pyproject.toml | 45 ---------- crates/ruff/src/rules/ruff/mod.rs | 17 ++-- .../ruff__rules__ruff__tests__ruf100_0.snap | 4 +- crates/ruff/src/settings/pyproject.rs | 85 ------------------- 4 files changed, 12 insertions(+), 139 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pyproject.toml b/crates/ruff/resources/test/fixtures/pyproject.toml index 950e30dfb8..5525b69dea 100644 --- a/crates/ruff/resources/test/fixtures/pyproject.toml +++ b/crates/ruff/resources/test/fixtures/pyproject.toml @@ -1,53 +1,8 @@ [tool.ruff] -allowed-confusables = ["−", "ρ", "∗"] line-length = 88 extend-exclude = [ "excluded_file.py", "migrations", "with_excluded_file/other_excluded_file.py", ] -external = ["V101"] per-file-ignores = { "__init__.py" = ["F401"] } - -[tool.ruff.flake8-bugbear] -extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"] - -[tool.ruff.flake8-builtins] -builtins-ignorelist = ["id", "dir"] - -[tool.ruff.flake8-quotes] -inline-quotes = "single" -multiline-quotes = "double" -docstring-quotes = "double" -avoid-escape = true - -[tool.ruff.mccabe] -max-complexity = 10 - -[tool.ruff.pep8-naming] -classmethod-decorators = ["pydantic.validator"] - -[tool.ruff.flake8-tidy-imports] -ban-relative-imports = "parents" - -[tool.ruff.flake8-tidy-imports.banned-api] -"cgi".msg = "The cgi module is deprecated." -"typing.TypedDict".msg = "Use typing_extensions.TypedDict instead." - -[tool.ruff.flake8-errmsg] -max-string-length = 20 - -[tool.ruff.flake8-import-conventions.aliases] -pandas = "pd" - -[tool.ruff.flake8-import-conventions.extend-aliases] -"dask.dataframe" = "dd" - -[tool.ruff.flake8-pytest-style] -fixture-parentheses = false -parametrize-names-type = "csv" -parametrize-values-type = "tuple" -parametrize-values-row-type = "list" -raises-require-match-for = ["Exception", "TypeError", "KeyError"] -raises-extend-require-match-for = ["requests.RequestException"] -mark-parentheses = false diff --git a/crates/ruff/src/rules/ruff/mod.rs b/crates/ruff/src/rules/ruff/mod.rs index 3d5c312ce4..f0aa41c570 100644 --- a/crates/ruff/src/rules/ruff/mod.rs +++ b/crates/ruff/src/rules/ruff/mod.rs @@ -90,13 +90,16 @@ mod tests { fn ruf100_0() -> Result<()> { let diagnostics = test_path( Path::new("ruff/RUF100_0.py"), - &settings::Settings::for_rules(vec![ - Rule::UnusedNOQA, - Rule::LineTooLong, - Rule::UnusedImport, - Rule::UnusedVariable, - Rule::TabIndentation, - ]), + &settings::Settings { + external: FxHashSet::from_iter(vec!["V101".to_string()]), + ..settings::Settings::for_rules(vec![ + Rule::UnusedNOQA, + Rule::LineTooLong, + Rule::UnusedImport, + Rule::UnusedVariable, + Rule::TabIndentation, + ]) + }, )?; assert_messages!(diagnostics); Ok(()) diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruf100_0.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruf100_0.snap index b581a21c34..96a0369c89 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruf100_0.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruf100_0.snap @@ -80,7 +80,7 @@ RUF100_0.py:19:12: RUF100 [*] Unused `noqa` directive (unused: `F841`, `W191`; n 21 21 | # Invalid (but external) 22 22 | d = 1 # noqa: F841, V101 -RUF100_0.py:22:12: RUF100 [*] Unused `noqa` directive (unused: `F841`; unknown: `V101`) +RUF100_0.py:22:12: RUF100 [*] Unused `noqa` directive (unused: `F841`) | 21 | # Invalid (but external) 22 | d = 1 # noqa: F841, V101 @@ -95,7 +95,7 @@ RUF100_0.py:22:12: RUF100 [*] Unused `noqa` directive (unused: `F841`; unknown: 20 20 | 21 21 | # Invalid (but external) 22 |- d = 1 # noqa: F841, V101 - 22 |+ d = 1 + 22 |+ d = 1 # noqa: V101 23 23 | 24 24 | # fmt: off 25 25 | # Invalid - no space before # diff --git a/crates/ruff/src/settings/pyproject.rs b/crates/ruff/src/settings/pyproject.rs index 6577dedd5c..6c6df45ed7 100644 --- a/crates/ruff/src/settings/pyproject.rs +++ b/crates/ruff/src/settings/pyproject.rs @@ -152,12 +152,6 @@ mod tests { use crate::codes::{self, RuleCodePrefix}; use crate::line_width::LineLength; - use crate::rules::flake8_quotes::settings::Quote; - use crate::rules::flake8_tidy_imports::settings::{ApiBan, Strictness}; - use crate::rules::{ - flake8_bugbear, flake8_builtins, flake8_errmsg, flake8_import_conventions, - flake8_pytest_style, flake8_quotes, flake8_tidy_imports, mccabe, pep8_naming, - }; use crate::settings::pyproject::{ find_settings_toml, parse_pyproject_toml, Options, Pyproject, Tools, }; @@ -300,95 +294,16 @@ other-attribute = 1 assert_eq!( config, Options { - allowed_confusables: Some(vec!['−', 'ρ', '∗']), line_length: Some(LineLength::from(88)), extend_exclude: Some(vec![ "excluded_file.py".to_string(), "migrations".to_string(), "with_excluded_file/other_excluded_file.py".to_string(), ]), - external: Some(vec!["V101".to_string()]), per_file_ignores: Some(FxHashMap::from_iter([( "__init__.py".to_string(), vec![RuleCodePrefix::Pyflakes(codes::Pyflakes::_401).into()] )])), - flake8_bugbear: Some(flake8_bugbear::settings::Options { - extend_immutable_calls: Some(vec![ - "fastapi.Depends".to_string(), - "fastapi.Query".to_string(), - ]), - }), - flake8_builtins: Some(flake8_builtins::settings::Options { - builtins_ignorelist: Some(vec!["id".to_string(), "dir".to_string(),]), - }), - flake8_errmsg: Some(flake8_errmsg::settings::Options { - max_string_length: Some(20), - }), - flake8_pytest_style: Some(flake8_pytest_style::settings::Options { - fixture_parentheses: Some(false), - parametrize_names_type: Some( - flake8_pytest_style::types::ParametrizeNameType::Csv - ), - parametrize_values_type: Some( - flake8_pytest_style::types::ParametrizeValuesType::Tuple, - ), - parametrize_values_row_type: Some( - flake8_pytest_style::types::ParametrizeValuesRowType::List, - ), - raises_require_match_for: Some(vec![ - "Exception".to_string(), - "TypeError".to_string(), - "KeyError".to_string(), - ]), - raises_extend_require_match_for: Some(vec![ - "requests.RequestException".to_string(), - ]), - mark_parentheses: Some(false), - }), - flake8_implicit_str_concat: None, - flake8_quotes: Some(flake8_quotes::settings::Options { - inline_quotes: Some(Quote::Single), - multiline_quotes: Some(Quote::Double), - docstring_quotes: Some(Quote::Double), - avoid_escape: Some(true), - }), - flake8_tidy_imports: Some(flake8_tidy_imports::options::Options { - ban_relative_imports: Some(Strictness::Parents), - banned_api: Some(FxHashMap::from_iter([ - ( - "cgi".to_string(), - ApiBan { - msg: "The cgi module is deprecated.".to_string() - } - ), - ( - "typing.TypedDict".to_string(), - ApiBan { - msg: "Use typing_extensions.TypedDict instead.".to_string() - } - ) - ])) - }), - flake8_import_conventions: Some(flake8_import_conventions::settings::Options { - aliases: Some(FxHashMap::from_iter([( - "pandas".to_string(), - "pd".to_string(), - )])), - extend_aliases: Some(FxHashMap::from_iter([( - "dask.dataframe".to_string(), - "dd".to_string(), - )])), - banned_aliases: None, - banned_from: None, - }), - mccabe: Some(mccabe::settings::Options { - max_complexity: Some(10), - }), - pep8_naming: Some(pep8_naming::settings::Options { - ignore_names: None, - classmethod_decorators: Some(vec!["pydantic.validator".to_string()]), - staticmethod_decorators: None, - }), ..Options::default() } ); From 30734f06fd62af07217c4c6e5e2168b827b6e52f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 20 Jun 2023 13:47:01 -0400 Subject: [PATCH 133/447] Support parenthesized expressions when splitting compound assertions (#5219) ## Summary I'm looking into the Black stability tests, and here's one failing case. We split `assert a and (b and c)` into: ```python assert a assert (b and c) ``` We fail to split `assert (b and c)` due to the parentheses. But Black then removes then, and when running Ruff again, we get: ```python assert a assert b assert c ``` This PR just enables us to fix to this in one pass. --- .../fixtures/flake8_pytest_style/PT018.py | 15 +- .../flake8_pytest_style/rules/assertion.rs | 62 ++++- ...es__flake8_pytest_style__tests__PT018.snap | 241 +++++++++--------- 3 files changed, 187 insertions(+), 131 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT018.py b/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT018.py index 9bc5fbe877..cedd7ba170 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT018.py +++ b/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT018.py @@ -21,6 +21,13 @@ def test_error(): assert something and something_else == """error message """ + assert ( + something + and something_else + == """error +message +""" + ) # recursive case assert not (a or not (b or c)) @@ -31,14 +38,6 @@ def test_error(): assert not (something or something_else and something_third), "with message" # detected, but no autofix for mixed conditions (e.g. `a or b and c`) assert not (something or something_else and something_third) - # detected, but no autofix for parenthesized conditions - assert ( - something - and something_else - == """error -message -""" - ) assert something # OK 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 9c3fa25046..e6984f2456 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs @@ -9,7 +9,6 @@ use libcst_native::{ }; use rustpython_parser::ast::{self, BoolOp, ExceptHandler, Expr, Keyword, Ranged, Stmt, UnaryOp}; -use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{has_comments_in, Truthiness}; @@ -17,6 +16,7 @@ use ruff_python_ast::source_code::{Locator, Stylist}; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{visitor, whitespace}; +use crate::autofix::codemods::CodegenStylist; use crate::checkers::ast::Checker; use crate::cst::matchers::match_indented_block; use crate::cst::matchers::match_module; @@ -315,6 +315,54 @@ fn negate<'a>(expression: &Expression<'a>) -> Expression<'a> { })) } +/// Propagate parentheses from a parent to a child expression, if necessary. +/// +/// For example, when splitting: +/// ```python +/// assert (a and b == +/// """) +/// ``` +/// +/// The parentheses need to be propagated to the right-most expression: +/// ```python +/// assert a +/// assert (b == +/// "") +/// ``` +fn parenthesize<'a>(expression: Expression<'a>, parent: &Expression<'a>) -> Expression<'a> { + if matches!( + expression, + Expression::Comparison(_) + | Expression::UnaryOperation(_) + | Expression::BinaryOperation(_) + | Expression::BooleanOperation(_) + | Expression::Attribute(_) + | Expression::Tuple(_) + | Expression::Call(_) + | Expression::GeneratorExp(_) + | Expression::ListComp(_) + | Expression::SetComp(_) + | Expression::DictComp(_) + | Expression::List(_) + | Expression::Set(_) + | Expression::Dict(_) + | Expression::Subscript(_) + | Expression::StarredElement(_) + | Expression::IfExp(_) + | Expression::Lambda(_) + | Expression::Yield(_) + | Expression::Await(_) + | Expression::ConcatenatedString(_) + | Expression::FormattedString(_) + | Expression::NamedExpr(_) + ) { + if let (Some(left), Some(right)) = (parent.lpar().first(), parent.rpar().first()) { + return expression.with_parens(left.clone(), right.clone()); + } + } + expression +} + /// Replace composite condition `assert a == "hello" and b == "world"` with two statements /// `assert a == "hello"` and `assert b == "world"`. fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> Result { @@ -363,10 +411,6 @@ fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> bail!("Expected simple statement to be an assert") }; - if !(assert_statement.test.lpar().is_empty() && assert_statement.test.rpar().is_empty()) { - bail!("Unable to split parenthesized condition"); - } - // Extract the individual conditions. let mut conditions: Vec = Vec::with_capacity(2); match &assert_statement.test { @@ -374,8 +418,8 @@ fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> if matches!(op.operator, libcst_native::UnaryOp::Not { .. }) { if let Expression::BooleanOperation(op) = &*op.expression { if matches!(op.operator, BooleanOp::Or { .. }) { - conditions.push(negate(&op.left)); - conditions.push(negate(&op.right)); + conditions.push(parenthesize(negate(&op.left), &assert_statement.test)); + conditions.push(parenthesize(negate(&op.right), &assert_statement.test)); } else { bail!("Expected assert statement to be a composite condition"); } @@ -386,8 +430,8 @@ fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> } Expression::BooleanOperation(op) => { if matches!(op.operator, BooleanOp::And { .. }) { - conditions.push(*op.left.clone()); - conditions.push(*op.right.clone()); + conditions.push(parenthesize(*op.left.clone(), &assert_statement.test)); + conditions.push(parenthesize(*op.right.clone(), &assert_statement.test)); } else { bail!("Expected assert statement to be a composite condition"); } diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT018.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT018.snap index 8d9c69ab49..cbfde1160d 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT018.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT018.snap @@ -163,8 +163,8 @@ PT018.py:21:5: PT018 [*] Assertion should be broken down into multiple parts 22 | | message 23 | | """ | |_______^ PT018 -24 | -25 | # recursive case +24 | assert ( +25 | something | = help: Break down assertion into multiple parts @@ -177,131 +177,144 @@ PT018.py:21:5: PT018 [*] Assertion should be broken down into multiple parts 22 |+ assert something_else == """error 22 23 | message 23 24 | """ -24 25 | +24 25 | assert ( -PT018.py:26:5: PT018 [*] Assertion should be broken down into multiple parts +PT018.py:24:5: PT018 [*] Assertion should be broken down into multiple parts | -25 | # recursive case -26 | assert not (a or not (b or c)) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 -27 | assert not (a or not (b and c)) - | - = help: Break down assertion into multiple parts - -ℹ Suggested fix -23 23 | """ -24 24 | -25 25 | # recursive case -26 |- assert not (a or not (b or c)) - 26 |+ assert not a - 27 |+ assert (b or c) -27 28 | assert not (a or not (b and c)) -28 29 | -29 30 | # detected, but no autofix for messages - -PT018.py:27:5: PT018 [*] Assertion should be broken down into multiple parts - | -25 | # recursive case -26 | assert not (a or not (b or c)) -27 | assert not (a or not (b and c)) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 -28 | -29 | # detected, but no autofix for messages - | - = help: Break down assertion into multiple parts - -ℹ Suggested fix -24 24 | -25 25 | # recursive case -26 26 | assert not (a or not (b or c)) -27 |- assert not (a or not (b and c)) - 27 |+ assert not a - 28 |+ assert (b and c) -28 29 | -29 30 | # detected, but no autofix for messages -30 31 | assert something and something_else, "error message" - -PT018.py:30:5: PT018 Assertion should be broken down into multiple parts - | -29 | # detected, but no autofix for messages -30 | assert something and something_else, "error message" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 -31 | assert not (something or something_else and something_third), "with message" -32 | # detected, but no autofix for mixed conditions (e.g. `a or b and c`) - | - = help: Break down assertion into multiple parts - -PT018.py:31:5: PT018 Assertion should be broken down into multiple parts - | -29 | # detected, but no autofix for messages -30 | assert something and something_else, "error message" -31 | assert not (something or something_else and something_third), "with message" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 -32 | # detected, but no autofix for mixed conditions (e.g. `a or b and c`) -33 | assert not (something or something_else and something_third) - | - = help: Break down assertion into multiple parts - -PT018.py:33:5: PT018 Assertion should be broken down into multiple parts - | -31 | assert not (something or something_else and something_third), "with message" -32 | # detected, but no autofix for mixed conditions (e.g. `a or b and c`) -33 | assert not (something or something_else and something_third) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 -34 | # detected, but no autofix for parenthesized conditions -35 | assert ( - | - = help: Break down assertion into multiple parts - -PT018.py:35:5: PT018 Assertion should be broken down into multiple parts - | -33 | assert not (something or something_else and something_third) -34 | # detected, but no autofix for parenthesized conditions -35 | assert ( +22 | message +23 | """ +24 | assert ( | _____^ -36 | | something -37 | | and something_else -38 | | == """error -39 | | message -40 | | """ -41 | | ) +25 | | something +26 | | and something_else +27 | | == """error +28 | | message +29 | | """ +30 | | ) | |_____^ PT018 +31 | +32 | # recursive case | = help: Break down assertion into multiple parts +ℹ Suggested fix +21 21 | assert something and something_else == """error +22 22 | message +23 23 | """ + 24 |+ assert something +24 25 | assert ( +25 |- something +26 |- and something_else + 26 |+ something_else +27 27 | == """error +28 28 | message +29 29 | """ + +PT018.py:33:5: PT018 [*] Assertion should be broken down into multiple parts + | +32 | # recursive case +33 | assert not (a or not (b or c)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 +34 | assert not (a or not (b and c)) + | + = help: Break down assertion into multiple parts + +ℹ Suggested fix +30 30 | ) +31 31 | +32 32 | # recursive case +33 |- assert not (a or not (b or c)) + 33 |+ assert not a + 34 |+ assert (b or c) +34 35 | assert not (a or not (b and c)) +35 36 | +36 37 | # detected, but no autofix for messages + +PT018.py:34:5: PT018 [*] Assertion should be broken down into multiple parts + | +32 | # recursive case +33 | assert not (a or not (b or c)) +34 | assert not (a or not (b and c)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 +35 | +36 | # detected, but no autofix for messages + | + = help: Break down assertion into multiple parts + +ℹ Suggested fix +31 31 | +32 32 | # recursive case +33 33 | assert not (a or not (b or c)) +34 |- assert not (a or not (b and c)) + 34 |+ assert not a + 35 |+ assert (b and c) +35 36 | +36 37 | # detected, but no autofix for messages +37 38 | assert something and something_else, "error message" + +PT018.py:37:5: PT018 Assertion should be broken down into multiple parts + | +36 | # detected, but no autofix for messages +37 | assert something and something_else, "error message" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 +38 | assert not (something or something_else and something_third), "with message" +39 | # detected, but no autofix for mixed conditions (e.g. `a or b and c`) + | + = help: Break down assertion into multiple parts + +PT018.py:38:5: PT018 Assertion should be broken down into multiple parts + | +36 | # detected, but no autofix for messages +37 | assert something and something_else, "error message" +38 | assert not (something or something_else and something_third), "with message" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 +39 | # detected, but no autofix for mixed conditions (e.g. `a or b and c`) +40 | assert not (something or something_else and something_third) + | + = help: Break down assertion into multiple parts + +PT018.py:40:5: PT018 Assertion should be broken down into multiple parts + | +38 | assert not (something or something_else and something_third), "with message" +39 | # detected, but no autofix for mixed conditions (e.g. `a or b and c`) +40 | assert not (something or something_else and something_third) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 + | + = help: Break down assertion into multiple parts + +PT018.py:44:1: PT018 [*] Assertion should be broken down into multiple parts + | +43 | assert something # OK +44 | assert something and something_else # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 +45 | assert something and something_else and something_third # Error + | + = help: Break down assertion into multiple parts + +ℹ Suggested fix +41 41 | +42 42 | +43 43 | assert something # OK +44 |-assert something and something_else # Error + 44 |+assert something + 45 |+assert something_else +45 46 | assert something and something_else and something_third # Error + PT018.py:45:1: PT018 [*] Assertion should be broken down into multiple parts | -44 | assert something # OK -45 | assert something and something_else # Error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 -46 | assert something and something_else and something_third # Error - | - = help: Break down assertion into multiple parts - -ℹ Suggested fix -42 42 | -43 43 | -44 44 | assert something # OK -45 |-assert something and something_else # Error - 45 |+assert something - 46 |+assert something_else -46 47 | assert something and something_else and something_third # Error - -PT018.py:46:1: PT018 [*] Assertion should be broken down into multiple parts - | -44 | assert something # OK -45 | assert something and something_else # Error -46 | assert something and something_else and something_third # Error +43 | assert something # OK +44 | assert something and something_else # Error +45 | assert something and something_else and something_third # Error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 | = help: Break down assertion into multiple parts ℹ Suggested fix -43 43 | -44 44 | assert something # OK -45 45 | assert something and something_else # Error -46 |-assert something and something_else and something_third # Error - 46 |+assert something and something_else - 47 |+assert something_third +42 42 | +43 43 | assert something # OK +44 44 | assert something and something_else # Error +45 |-assert something and something_else and something_third # Error + 45 |+assert something and something_else + 46 |+assert something_third From acb23dce3c94e0e92ab15f1843b6e77c22240b50 Mon Sep 17 00:00:00 2001 From: konstin Date: Tue, 20 Jun 2023 19:53:32 +0200 Subject: [PATCH 134/447] Fix subprocess.run on Windows Python 3.7 (#5220) ## Summary From the [subprocess docs](https://docs.python.org/3/library/subprocess.html#subprocess.Popen): > Changed in version 3.6: args parameter accepts a path-like object if shell is False and a sequence containing path-like objects on POSIX. > > Changed in version 3.8: args parameter accepts a path-like object if shell is False and a sequence containing bytes and path-like objects on Windows. We want to support python 3.7 on windows, so we need to convert the `Path` into a `str` --- python/ruff/__main__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/ruff/__main__.py b/python/ruff/__main__.py index 1631082fbe..44384bc14b 100644 --- a/python/ruff/__main__.py +++ b/python/ruff/__main__.py @@ -32,5 +32,7 @@ def find_ruff_bin() -> Path: if __name__ == "__main__": ruff = find_ruff_bin() - completed_process = subprocess.run([ruff, *sys.argv[1:]]) + # Passing a path-like to `subprocess.run()` on windows is only supported in 3.8+, + # but we also support 3.7 + completed_process = subprocess.run([os.fsdecode(ruff), *sys.argv[1:]]) sys.exit(completed_process.returncode) From b4bd5a5acbbabef94378e032f5a2d9473adb2469 Mon Sep 17 00:00:00 2001 From: konstin Date: Tue, 20 Jun 2023 20:33:09 +0200 Subject: [PATCH 135/447] Make the release workflow more resilient (#4728) ## Summary Currently, it is possible to create a tag and then have the release fail, which is a problem since we can't edit the tag (https://github.com/charliermarsh/ruff/issues/4468). This change the release process so that the tag is created inside the release workflow. This leaves as a failure mode that we have published to pypi but then creating the tag or GitHub release doesn't work, but in this case we can restart and the pypi upload is just skipped because we use the skip existing option. The release workflow is started by a workflow dispatch with the tag instead of creating the tag yourself. You can start the release workflow without a tag to do a dry run which does not publish an artifacts. You can optionally add a git sha to the workflow run and it will verify that the release runs on the mentioned commit. This also adds docs on how to release and a small style improvement for the maturin integration. ## Test Plan Testing is hard since we can't do real releases, i've tested a minimized workflow in a separate dummy repository. --- .github/workflows/ci.yaml | 3 +- .github/workflows/release.yaml | 51 +++++++++++++++++++++++++++++++--- CONTRIBUTING.md | 22 +++++++++++++++ 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6cae13b9e9..a7a5543ce0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -220,11 +220,10 @@ jobs: - name: "Build wheels" uses: PyO3/maturin-action@v1 with: - manylinux: auto args: --out dist - name: "Test wheel" run: | - pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall + pip install --force-reinstall --find-links dist ${{ env.PACKAGE_NAME }} ruff --help python -m ruff --help - name: "Remove wheels from cache" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d45dcfdffe..e30114a337 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -2,8 +2,17 @@ name: "[ruff] Release" on: workflow_dispatch: - release: - types: [ published ] + inputs: + tag: + description: "The version to tag, without the leading 'v'. If omitted, will initiate a dry run skipping uploading artifact." + type: string + sha: + description: "Optionally, the full sha of the commit to be released" + type: string + push: + paths: + # When we change pyproject.toml, we want to ensure that the maturin builds still work + - pyproject.toml concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -394,7 +403,8 @@ jobs: - linux-cross - musllinux - musllinux-cross - if: "startsWith(github.ref, 'refs/tags/')" + # If you don't set an input it's a dry run skipping uploading artifact + if: ${{ inputs.tag }} environment: name: release permissions: @@ -403,11 +413,34 @@ jobs: # For GitHub release publishing contents: write steps: + - name: Consistency check tag + run: | + version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g') + if [ "${{ inputs.tag }}" != "${version}" ]; then + echo "The input tag does not match the version from pyproject.toml:" >&2 + echo "${{ inputs.tag }}" >&2 + echo "${version}" >&2 + exit 1 + else + echo "Releasing ${version}" + fi + - name: Consistency check sha + if: ${{ inputs.sha }} + run: | + git_sha=$(git rev-parse HEAD) + if [ "${{ inputs.sha }}" != "${git_sha}" ]; then + echo "The specified sha does not match the git checkout" >&2 + echo "${{ inputs.sha }}" >&2 + echo "${git_sha}" >&2 + exit 1 + else + echo "Releasing ${git_sha}" + fi - uses: actions/download-artifact@v3 with: name: wheels path: wheels - - name: "Publish to PyPi" + - name: Publish to PyPi uses: pypa/gh-action-pypi-publish@release/v1 with: skip-existing: true @@ -417,10 +450,20 @@ jobs: with: name: binaries path: binaries + - name: git tag + run: | + git config user.email "hey@astral.sh" + git config user.name "Ruff Release CI" + git tag -m "v${{ inputs.tag }}" "v${{ inputs.tag }}" + # If there is duplicate tag, this will fail. The publish to pypi action will have been a noop (due to skip + # existing), so we make a non-destructive exit here + git push --tags - name: "Publish to GitHub" uses: softprops/action-gh-release@v1 with: + draft: true files: binaries/* + tag_name: v${{ inputs.tag }} # After the release has been published, we update downstream repositories # This is separate because if this fails the release is still fine, we just need to do some manual workflow triggers diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3feb49bd1f..3721e8815b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -271,6 +271,28 @@ them to [PyPI](https://pypi.org/project/ruff/). Ruff follows the [semver](https://semver.org/) versioning standard. However, as pre-1.0 software, even patch releases may contain [non-backwards-compatible changes](https://semver.org/#spec-item-4). +### Creating a new release + +1. Update the version with `rg 0.0.269 --files-with-matches | xargs sed -i 's/0.0.269/0.0.270/g'` +1. Update `BREAKING_CHANGES.md` +1. Create a PR with the version and `BREAKING_CHANGES.md` updated +1. Merge the PR +1. Run the release workflow with the version number (without starting `v`) as input. Make sure + main has your merged PR as last commit +1. The release workflow will do the following: + 1. Build all the assets. If this fails (even though we tested in step 4), we haven’t tagged or + uploaded anything, you can restart after pushing a fix + 1. Upload to pypi + 1. Create and push the git tag (from pyproject.toml). We create the git tag only here + because we can't change it ([#4468](https://github.com/charliermarsh/ruff/issues/4468)), so + we want to make sure everything up to and including publishing to pypi worked. + 1. Attach artifacts to draft GitHub release + 1. Trigger downstream repositories. This can fail without causing fallout, it is possible (if + inconvenient) to trigger the downstream jobs manually +1. Create release notes in GitHub UI and promote from draft to proper release() +1. If needed, [update the schemastore](https://github.com/charliermarsh/ruff/blob/main/scripts/update_schemastore.py) +1. If needed, update ruff-lsp and ruff-vscode + ## Ecosystem CI GitHub Actions will run your changes against a number of real-world projects from GitHub and From fde5dbc9aa967b086347f87fc61511c8dfe7ad0e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 20 Jun 2023 14:37:28 -0400 Subject: [PATCH 136/447] Bump version to 0.0.273 (#5218) --- 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 b2eb7daa03..5991cd08ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -733,7 +733,7 @@ dependencies = [ [[package]] name = "flake8-to-ruff" -version = "0.0.272" +version = "0.0.273" dependencies = [ "anyhow", "clap", @@ -1793,7 +1793,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.0.272" +version = "0.0.273" dependencies = [ "annotate-snippets 0.9.1", "anyhow", @@ -1889,7 +1889,7 @@ dependencies = [ [[package]] name = "ruff_cli" -version = "0.0.272" +version = "0.0.273" dependencies = [ "annotate-snippets 0.9.1", "anyhow", diff --git a/README.md b/README.md index 86755f5c23..4e8454b9a7 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.272 + rev: v0.0.273 hooks: - id: ruff ``` diff --git a/crates/flake8_to_ruff/Cargo.toml b/crates/flake8_to_ruff/Cargo.toml index 7cfcfd33a0..dd7b12cf3c 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.272" +version = "0.0.273" description = """ Convert Flake8 configuration files to Ruff configuration files. """ diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index cfea318ddc..25725e78ea 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.0.272" +version = "0.0.273" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_cli/Cargo.toml b/crates/ruff_cli/Cargo.toml index 166e4676c6..2f7085ab6d 100644 --- a/crates/ruff_cli/Cargo.toml +++ b/crates/ruff_cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_cli" -version = "0.0.272" +version = "0.0.273" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/docs/tutorial.md b/docs/tutorial.md index 0263712546..492f33bdaf 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.272 + rev: v0.0.273 hooks: - id: ruff ``` diff --git a/docs/usage.md b/docs/usage.md index 6e41618375..076ecc8532 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.272 + rev: v0.0.273 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.272 + rev: v0.0.273 hooks: - id: ruff args: [ --fix, --exit-non-zero-on-fix ] diff --git a/pyproject.toml b/pyproject.toml index e5f1a2c6f3..b359b87224 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.0.272" +version = "0.0.273" 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 e520a3a721d6cabd55c3a45b861b9d34a08266f7 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 20 Jun 2023 22:48:07 +0200 Subject: [PATCH 137/447] Fix ArgWithDefault comments handling (#5204) --- Cargo.lock | 12 ++++----- Cargo.toml | 10 +++---- crates/ruff_python_ast/src/node.rs | 2 +- .../test/fixtures/ruff/statement/function.py | 7 +++++ crates/ruff_python_formatter/src/cli.rs | 2 +- .../src/comments/placement.rs | 12 +-------- ...sitional_arguments_slash_on_same_line.snap | 6 ++--- ...on_positional_arguments_with_defaults.snap | 12 ++++----- ...sts__positional_argument_only_comment.snap | 4 +-- ...t_only_comment_without_following_node.snap | 2 +- ...l_argument_only_leading_comma_comment.snap | 4 +-- ...comments__tests__trailing_after_comma.snap | 2 +- .../src/comments/visitor.rs | 7 +++++ crates/ruff_python_formatter/src/lib.rs | 14 +++++----- .../src/other/arg_with_default.rs | 5 ++-- .../src/other/arguments.rs | 27 +++++-------------- ...ts__ruff_test__statement__function_py.snap | 14 ++++++++++ 17 files changed, 73 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5991cd08ad..0d436534b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2105,7 +2105,7 @@ dependencies = [ [[package]] name = "ruff_text_size" version = "0.0.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=ed3b4eb72b6e497bbdb4d19dec6621074d724130#ed3b4eb72b6e497bbdb4d19dec6621074d724130" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca" dependencies = [ "schemars", "serde", @@ -2183,7 +2183,7 @@ dependencies = [ [[package]] name = "rustpython-ast" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=ed3b4eb72b6e497bbdb4d19dec6621074d724130#ed3b4eb72b6e497bbdb4d19dec6621074d724130" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca" dependencies = [ "is-macro", "num-bigint", @@ -2194,7 +2194,7 @@ dependencies = [ [[package]] name = "rustpython-format" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=ed3b4eb72b6e497bbdb4d19dec6621074d724130#ed3b4eb72b6e497bbdb4d19dec6621074d724130" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca" dependencies = [ "bitflags 2.3.1", "itertools", @@ -2206,7 +2206,7 @@ dependencies = [ [[package]] name = "rustpython-literal" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=ed3b4eb72b6e497bbdb4d19dec6621074d724130#ed3b4eb72b6e497bbdb4d19dec6621074d724130" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca" dependencies = [ "hexf-parse", "is-macro", @@ -2218,7 +2218,7 @@ dependencies = [ [[package]] name = "rustpython-parser" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=ed3b4eb72b6e497bbdb4d19dec6621074d724130#ed3b4eb72b6e497bbdb4d19dec6621074d724130" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca" dependencies = [ "anyhow", "is-macro", @@ -2241,7 +2241,7 @@ dependencies = [ [[package]] name = "rustpython-parser-core" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=ed3b4eb72b6e497bbdb4d19dec6621074d724130#ed3b4eb72b6e497bbdb4d19dec6621074d724130" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca" dependencies = [ "is-macro", "memchr", diff --git a/Cargo.toml b/Cargo.toml index 7a9e8d929b..f6b7b59b22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,15 +50,15 @@ toml = { version = "0.7.2" } # v0.0.1 libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "80e4c1399f95e5beb532fdd1e209ad2dbb470438" } # v0.0.3 -ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "ed3b4eb72b6e497bbdb4d19dec6621074d724130" } +ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca" } # v0.0.3 -rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "ed3b4eb72b6e497bbdb4d19dec6621074d724130" , default-features = false, features = ["all-nodes-with-ranges", "num-bigint"]} +rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca" , default-features = false, features = ["all-nodes-with-ranges", "num-bigint"]} # v0.0.3 -rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "ed3b4eb72b6e497bbdb4d19dec6621074d724130", default-features = false, features = ["num-bigint"] } +rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca", default-features = false, features = ["num-bigint"] } # v0.0.3 -rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "ed3b4eb72b6e497bbdb4d19dec6621074d724130", default-features = false } +rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca", default-features = false } # v0.0.3 -rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "ed3b4eb72b6e497bbdb4d19dec6621074d724130" , default-features = false, features = ["full-lexer", "all-nodes-with-ranges", "num-bigint"] } +rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca" , default-features = false, features = ["full-lexer", "all-nodes-with-ranges", "num-bigint"] } [profile.release] lto = "fat" diff --git a/crates/ruff_python_ast/src/node.rs b/crates/ruff_python_ast/src/node.rs index b9a6654753..9928fa5d2a 100644 --- a/crates/ruff_python_ast/src/node.rs +++ b/crates/ruff_python_ast/src/node.rs @@ -3680,7 +3680,7 @@ impl AnyNodeRef<'_> { /// Compares two any node refs by their pointers (referential equality). pub fn ptr_eq(self, other: AnyNodeRef) -> bool { - self.as_ptr().eq(&other.as_ptr()) + self.as_ptr().eq(&other.as_ptr()) && self.kind() == other.kind() } /// Returns the node's [`kind`](NodeKind) that has no data associated and is [`Copy`]. diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py index 8b540fe646..674b9ef43e 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py @@ -90,3 +90,10 @@ else: # Regression test for https://github.com/python/cpython/blob/7199584ac8632eab57612f595a7162ab8d2ebbc0/Lib/warnings.py#L513 def f(arg1=1, *, kwonlyarg1, kwonlyarg2=2): pass + + +# Regression test for https://github.com/astral-sh/ruff/issues/5176#issuecomment-1598171989 +def foo( + b=3 + 2 # comment +): + ... diff --git a/crates/ruff_python_formatter/src/cli.rs b/crates/ruff_python_formatter/src/cli.rs index f605e66407..8a277ece24 100644 --- a/crates/ruff_python_formatter/src/cli.rs +++ b/crates/ruff_python_formatter/src/cli.rs @@ -63,7 +63,7 @@ pub fn format_and_debug_print(input: &str, cli: &Cli) -> Result { } if cli.print_comments { println!( - "{:?}", + "{:#?}", formatted.context().comments().debug(SourceCode::new(input)) ); } diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index eb845933af..aea1d1d3e0 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -627,17 +627,7 @@ fn handle_positional_only_arguments_separator_comment<'a>( }; let is_last_positional_argument = - // If the preceding node is the identifier for the last positional argument (`a`). - // ```python - // def test(a, /, b): pass - // ``` - are_same_optional(last_argument_or_default, arguments.posonlyargs.last().map(|arg| &arg.def)) - // If the preceding node is the default for the last positional argument (`10`). - // ```python - // def test(a=10, /, b): pass - // ``` - || are_same_optional(last_argument_or_default, arguments - .posonlyargs.last().and_then(|arg| arg.default.as_deref())); + are_same_optional(last_argument_or_default, arguments.posonlyargs.last()); if !is_last_positional_argument { return CommentPlacement::Default(comment); diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__non_positional_arguments_slash_on_same_line.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__non_positional_arguments_slash_on_same_line.snap index ab41a810a3..6764fffaaa 100644 --- a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__non_positional_arguments_slash_on_same_line.snap +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__non_positional_arguments_slash_on_same_line.snap @@ -19,9 +19,9 @@ expression: comments.debug(test_case.source_code) "trailing": [], }, Node { - kind: Arg, - range: 90..91, - source: `b`, + kind: ArgWithDefault, + range: 90..94, + source: `b=20`, }: { "leading": [ SourceComment { diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__non_positional_arguments_with_defaults.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__non_positional_arguments_with_defaults.snap index a4fde5b30d..6fa4a9c9d0 100644 --- a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__non_positional_arguments_with_defaults.snap +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__non_positional_arguments_with_defaults.snap @@ -24,9 +24,9 @@ expression: comments.debug(test_case.source_code) "trailing": [], }, Node { - kind: ExprConstant, - range: 17..19, - source: `10`, + kind: ArgWithDefault, + range: 15..19, + source: `a=10`, }: { "leading": [], "dangling": [], @@ -39,9 +39,9 @@ expression: comments.debug(test_case.source_code) ], }, Node { - kind: Arg, - range: 173..174, - source: `b`, + kind: ArgWithDefault, + range: 173..177, + source: `b=20`, }: { "leading": [ SourceComment { diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_comment.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_comment.snap index ff26fa7292..aa1f34eb1f 100644 --- a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_comment.snap +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_comment.snap @@ -24,7 +24,7 @@ expression: comments.debug(test_case.source_code) "trailing": [], }, Node { - kind: Arg, + kind: ArgWithDefault, range: 15..16, source: `a`, }: { @@ -39,7 +39,7 @@ expression: comments.debug(test_case.source_code) ], }, Node { - kind: Arg, + kind: ArgWithDefault, range: 166..167, source: `b`, }: { diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_comment_without_following_node.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_comment_without_following_node.snap index 2996b7bee1..2c93c46b00 100644 --- a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_comment_without_following_node.snap +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_comment_without_following_node.snap @@ -24,7 +24,7 @@ expression: comments.debug(test_case.source_code) "trailing": [], }, Node { - kind: Arg, + kind: ArgWithDefault, range: 15..16, source: `a`, }: { diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_leading_comma_comment.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_leading_comma_comment.snap index ff26fa7292..aa1f34eb1f 100644 --- a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_leading_comma_comment.snap +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_leading_comma_comment.snap @@ -24,7 +24,7 @@ expression: comments.debug(test_case.source_code) "trailing": [], }, Node { - kind: Arg, + kind: ArgWithDefault, range: 15..16, source: `a`, }: { @@ -39,7 +39,7 @@ expression: comments.debug(test_case.source_code) ], }, Node { - kind: Arg, + kind: ArgWithDefault, range: 166..167, source: `b`, }: { diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__trailing_after_comma.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__trailing_after_comma.snap index 030b63f38b..5a2c922207 100644 --- a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__trailing_after_comma.snap +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__trailing_after_comma.snap @@ -4,7 +4,7 @@ expression: comments.debug(test_case.source_code) --- { Node { - kind: Arg, + kind: ArgWithDefault, range: 15..16, source: `a`, }: { diff --git a/crates/ruff_python_formatter/src/comments/visitor.rs b/crates/ruff_python_formatter/src/comments/visitor.rs index 26850b409c..55ce3f42fc 100644 --- a/crates/ruff_python_formatter/src/comments/visitor.rs +++ b/crates/ruff_python_formatter/src/comments/visitor.rs @@ -236,6 +236,13 @@ impl<'ast> PreorderVisitor<'ast> for CommentsVisitor<'ast> { self.finish_node(arg); } + fn visit_arg_with_default(&mut self, arg_with_default: &'ast ArgWithDefault) { + if self.start_node(arg_with_default).is_traverse() { + walk_arg_with_default(self, arg_with_default); + } + self.finish_node(arg_with_default); + } + fn visit_keyword(&mut self, keyword: &'ast Keyword) { if self.start_node(keyword).is_traverse() { walk_keyword(self, keyword); diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 3e38b1d487..b915ac45eb 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -411,10 +411,12 @@ Formatted twice: #[test] fn quick_test() { let src = r#" -def test(): ... -# Comment -def with_leading_comment(): ... +def foo( + b=3 + + 2 # comment +): + ... "#; // Tokenize once let mut tokens = Vec::new(); @@ -437,10 +439,10 @@ def with_leading_comment(): ... // Use `dbg_write!(f, []) instead of `write!(f, [])` in your formatting code to print some IR // inside of a `Format` implementation // use ruff_formatter::FormatContext; - // formatted + // dbg!(formatted // .document() - // .display(formatted.context().source_code()); - + // .display(formatted.context().source_code())); + // // dbg!(formatted // .context() // .comments() diff --git a/crates/ruff_python_formatter/src/other/arg_with_default.rs b/crates/ruff_python_formatter/src/other/arg_with_default.rs index 2dc9993407..c6badb551a 100644 --- a/crates/ruff_python_formatter/src/other/arg_with_default.rs +++ b/crates/ruff_python_formatter/src/other/arg_with_default.rs @@ -1,6 +1,5 @@ -use rustpython_parser::ast::ArgWithDefault; - use ruff_formatter::write; +use rustpython_parser::ast::ArgWithDefault; use crate::prelude::*; use crate::FormatNodeRule; @@ -20,7 +19,7 @@ impl FormatNodeRule for FormatArgWithDefault { if let Some(default) = default { let space = def.annotation.is_some().then_some(space()); - write!(f, [space, text("="), space, default.format()])?; + write!(f, [space, text("="), space, group(&default.format())])?; } Ok(()) diff --git a/crates/ruff_python_formatter/src/other/arguments.rs b/crates/ruff_python_formatter/src/other/arguments.rs index 63cc7859b9..ae1a08686c 100644 --- a/crates/ruff_python_formatter/src/other/arguments.rs +++ b/crates/ruff_python_formatter/src/other/arguments.rs @@ -34,14 +34,9 @@ impl FormatNodeRule for FormatArguments { let mut last_node: Option = None; for arg_with_default in posonlyargs { - joiner.entry(&arg_with_default.into_format()); + joiner.entry(&arg_with_default.format()); - last_node = Some( - arg_with_default - .default - .as_deref() - .map_or_else(|| (&arg_with_default.def).into(), AnyNodeRef::from), - ); + last_node = Some(arg_with_default.into()); } if !posonlyargs.is_empty() { @@ -49,14 +44,9 @@ impl FormatNodeRule for FormatArguments { } for arg_with_default in args { - joiner.entry(&arg_with_default.into_format()); + joiner.entry(&arg_with_default.format()); - last_node = Some( - arg_with_default - .default - .as_deref() - .map_or_else(|| (&arg_with_default.def).into(), AnyNodeRef::from), - ); + last_node = Some(arg_with_default.into()); } // kw only args need either a `*args` ahead of them capturing all var args or a `*` @@ -74,14 +64,9 @@ impl FormatNodeRule for FormatArguments { } for arg_with_default in kwonlyargs { - joiner.entry(&arg_with_default.into_format()); + joiner.entry(&arg_with_default.format()); - last_node = Some( - arg_with_default - .default - .as_deref() - .map_or_else(|| (&arg_with_default.def).into(), AnyNodeRef::from), - ); + last_node = Some(arg_with_default.into()); } if let Some(kwarg) = kwarg { diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__function_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__function_py.snap index ad063ab680..222c32b4a4 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__function_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__function_py.snap @@ -96,6 +96,13 @@ else: # Regression test for https://github.com/python/cpython/blob/7199584ac8632eab57612f595a7162ab8d2ebbc0/Lib/warnings.py#L513 def f(arg1=1, *, kwonlyarg1, kwonlyarg2=2): pass + + +# Regression test for https://github.com/astral-sh/ruff/issues/5176#issuecomment-1598171989 +def foo( + b=3 + 2 # comment +): + ... ``` @@ -223,6 +230,13 @@ else: # Regression test for https://github.com/python/cpython/blob/7199584ac8632eab57612f595a7162ab8d2ebbc0/Lib/warnings.py#L513 def f(arg1=1, *, kwonlyarg1, kwonlyarg2=2): pass + + +# Regression test for https://github.com/astral-sh/ruff/issues/5176#issuecomment-1598171989 +def foo( + b=3 + 2, # comment +): + ... ``` From 2c0ec97782edce50cdc1116806ef743579c8093e Mon Sep 17 00:00:00 2001 From: Addison Crump Date: Tue, 20 Jun 2023 22:51:06 +0200 Subject: [PATCH 138/447] Use cpython with fuzzer corpus (#5183) Following #5055, add cpython as a member of the fuzzer corpus unconditionally. --- fuzz/init-fuzzer.sh | 1 + fuzz/reinit-fuzzer.sh | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/fuzz/init-fuzzer.sh b/fuzz/init-fuzzer.sh index eb7e026505..cc99cdee27 100644 --- a/fuzz/init-fuzzer.sh +++ b/fuzz/init-fuzzer.sh @@ -17,6 +17,7 @@ if [ ! -d corpus/ruff_fix_validity ]; then if [[ $REPLY =~ ^[Yy]$ ]]; then curl -L 'https://zenodo.org/record/3628784/files/python-corpus.tar.gz?download=1' | tar xz fi + curl -L 'https://github.com/python/cpython/archive/refs/tags/v3.12.0b2.tar.gz' | tar xz cp -r "../../../crates/ruff/resources/test" . cd - cargo fuzz cmin -s none ruff_fix_validity diff --git a/fuzz/reinit-fuzzer.sh b/fuzz/reinit-fuzzer.sh index a1acb8328f..9ff9fd1ad1 100644 --- a/fuzz/reinit-fuzzer.sh +++ b/fuzz/reinit-fuzzer.sh @@ -6,9 +6,7 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) cd "$SCRIPT_DIR" cd corpus/ruff_fix_validity -if [[ $REPLY =~ ^[Yy]$ ]]; then - curl -L 'https://zenodo.org/record/3628784/files/python-corpus.tar.gz?download=1' | tar xz -fi +curl -L 'https://github.com/python/cpython/archive/refs/tags/v3.12.0b2.tar.gz' | tar xz cp -r "../../../crates/ruff/resources/test" . cd - cargo fuzz cmin -s none ruff_fix_validity From 07409ce201cf3d8e0b323c049a731c634aa90168 Mon Sep 17 00:00:00 2001 From: Florian Stasse <58951320+Flowake@users.noreply.github.com> Date: Tue, 20 Jun 2023 22:51:51 +0200 Subject: [PATCH 139/447] Fixed typo in numpy deprecated type alias rule documentation (#5224) ## Summary It is a very simple typo fix in the "numy deprecated type alias" documentation. --- crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs b/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs index c71757dbdd..853cf8039d 100644 --- a/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs +++ b/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs @@ -12,7 +12,7 @@ use crate::registry::AsRule; /// ## Why is this bad? /// NumPy's `np.int` has long been an alias of the builtin `int`. The same /// goes for `np.float`, `np.bool`, and others. These aliases exist -/// primarily primarily for historic reasons, and have been a cause of +/// primarily for historic reasons, and have been a cause of /// frequent confusion for newcomers. /// /// These aliases were been deprecated in 1.20, and removed in 1.24. From 4717d0779f4e0de4f4e8b5320b5c4af726f54e6a Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Tue, 20 Jun 2023 22:04:32 +0100 Subject: [PATCH 140/447] Complete `flake8-debugger` documentation (#5223) ## Summary Completes the documentation for the `flake8-debugger` ruleset. Related to #2646. ## Test Plan `python scripts/check_docs_formatted.py` --- .../rules/flake8_debugger/rules/debugger.rs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/ruff/src/rules/flake8_debugger/rules/debugger.rs b/crates/ruff/src/rules/flake8_debugger/rules/debugger.rs index 94c151c95b..08a022e62a 100644 --- a/crates/ruff/src/rules/flake8_debugger/rules/debugger.rs +++ b/crates/ruff/src/rules/flake8_debugger/rules/debugger.rs @@ -7,6 +7,28 @@ use ruff_python_ast::call_path::{format_call_path, from_unqualified_name, CallPa use crate::checkers::ast::Checker; use crate::rules::flake8_debugger::types::DebuggerUsingType; +/// ## What it does +/// Checks for the presence of debugger calls and imports. +/// +/// ## Why is this bad? +/// Debugger calls and imports should be used for debugging purposes only. The +/// presence of a debugger call or import in production code is likely a +/// mistake and may cause unintended behavior, such as exposing sensitive +/// information or causing the program to hang. +/// +/// Instead, consider using a logging library to log information about the +/// program's state, and writing tests to verify that the program behaves +/// as expected. +/// +/// ## Example +/// ```python +/// def foo(): +/// breakpoint() +/// ``` +/// +/// ## References +/// - [Python documentation: `pdb` — The Python Debugger](https://docs.python.org/3/library/pdb.html) +/// - [Python documentation: `logging` — Logging facility for Python](https://docs.python.org/3/library/logging.html) #[violation] pub struct Debugger { using_type: DebuggerUsingType, From 1a2bd984f20fd71320d0d623e9b41da6687d93c6 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 20 Jun 2023 19:01:21 -0400 Subject: [PATCH 141/447] Avoid `.unwrap()` on cache access (#5229) ## Summary I haven't been able to determine why / when this is happening, but in some cases, users are reporting that this `unwrap()` is causing a panic. It's fine to just return `None` here and fallback to "No cache", certainly better than panicking (while we figure out the edge case). Closes #5225. Closes #5228. --- crates/ruff_cli/src/commands/run.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/ruff_cli/src/commands/run.rs b/crates/ruff_cli/src/commands/run.rs index 9ff134f2c9..ac27ff15c9 100644 --- a/crates/ruff_cli/src/commands/run.rs +++ b/crates/ruff_cli/src/commands/run.rs @@ -107,9 +107,7 @@ pub(crate) fn run( let settings = resolver.resolve_all(path, pyproject_config); let package_root = package.unwrap_or_else(|| path.parent().unwrap_or(path)); - let cache = caches - .as_ref() - .map(|caches| caches.get(&package_root).unwrap()); + let cache = caches.as_ref().and_then(|caches| caches.get(&package_root)); lint_path(path, package, settings, cache, noqa, autofix).map_err(|e| { (Some(path.to_owned()), { From f9f77cf617d90567ba70995a9b2d27a8f3708e0b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 20 Jun 2023 21:15:17 -0400 Subject: [PATCH 142/447] Revert change to `RUF010` to remove unnecessary `str` calls (#5232) ## Summary This PR reverts #4971 (aba073a7911bdc1bb08dc4bcb0feeb5448535db0). It turns out that `f"{str(x)}"` and `f"{x}"` are often but not exactly equivalent, and performing that conversion automatically can lead to subtle bugs, See the discussion in https://github.com/astral-sh/ruff/issues/4958. --- .../resources/test/fixtures/ruff/RUF010.py | 9 - .../explicit_f_string_type_conversion.rs | 223 ++++-------------- ..._rules__ruff__tests__RUF010_RUF010.py.snap | 122 +--------- 3 files changed, 54 insertions(+), 300 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF010.py b/crates/ruff/resources/test/fixtures/ruff/RUF010.py index 6416b79c9e..77e459c214 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF010.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF010.py @@ -34,12 +34,3 @@ f"{ascii(bla)}" # OK " intermediary content " f" that flows {repr(obj)} of type {type(obj)}.{additional_message}" # RUF010 ) - - -f"{str(bla)}" # RUF010 - -f"{str(bla):20}" # RUF010 - -f"{bla!s}" # RUF010 - -f"{bla!s:20}" # OK 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 513843feff..b87135e0bd 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 @@ -6,7 +6,6 @@ use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::prelude::ConversionFlag; use ruff_python_ast::source_code::{Locator, Stylist}; use crate::autofix::codemods::CodegenStylist; @@ -22,62 +21,36 @@ use crate::registry::AsRule; /// f-strings support dedicated conversion flags for these types, which are /// more succinct and idiomatic. /// -/// In the case of `str()`, it's also redundant, since `str()` is the default -/// conversion. +/// Note that, in many cases, calling `str()` within an f-string is +/// unnecessary and can be removed entirely, as the value will be converted +/// to a string automatically, the notable exception being for classes that +/// implement a custom `__format__` method. /// /// ## Example /// ```python /// a = "some string" -/// f"{str(a)}" /// f"{repr(a)}" /// ``` /// /// Use instead: /// ```python /// a = "some string" -/// f"{a}" /// f"{a!r}" /// ``` #[violation] -pub struct ExplicitFStringTypeConversion { - operation: Operation, -} +pub struct ExplicitFStringTypeConversion; impl AlwaysAutofixableViolation for ExplicitFStringTypeConversion { #[derive_message_formats] fn message(&self) -> String { - let ExplicitFStringTypeConversion { operation } = self; - match operation { - Operation::ConvertCallToConversionFlag => { - format!("Use explicit conversion flag") - } - Operation::RemoveCall => format!("Remove unnecessary `str` conversion"), - Operation::RemoveConversionFlag => format!("Remove unnecessary conversion flag"), - } + format!("Use explicit conversion flag") } fn autofix_title(&self) -> String { - let ExplicitFStringTypeConversion { operation } = self; - match operation { - Operation::ConvertCallToConversionFlag => { - format!("Replace with conversion flag") - } - Operation::RemoveCall => format!("Remove `str` call"), - Operation::RemoveConversionFlag => format!("Remove conversion flag"), - } + "Replace with conversion flag".to_string() } } -#[derive(Debug, PartialEq, Eq)] -enum Operation { - /// Ex) Convert `f"{repr(bla)}"` to `f"{bla!r}"` - ConvertCallToConversionFlag, - /// Ex) Convert `f"{bla!s}"` to `f"{bla}"` - RemoveConversionFlag, - /// Ex) Convert `f"{str(bla)}"` to `f"{bla}"` - RemoveCall, -} - /// RUF010 pub(crate) fn explicit_f_string_type_conversion( checker: &mut Checker, @@ -96,156 +69,50 @@ pub(crate) fn explicit_f_string_type_conversion( .enumerate() { let ast::ExprFormattedValue { - value, - conversion, - format_spec, - range: _, + value, conversion, .. } = formatted_value; - match conversion { - ConversionFlag::Ascii | ConversionFlag::Repr => { - // Nothing to do. - continue; - } - ConversionFlag::Str => { - // Skip if there's a format spec. - if format_spec.is_some() { - continue; - } - - // Remove the conversion flag entirely. - // Ex) `f"{bla!s}"` - let mut diagnostic = Diagnostic::new( - ExplicitFStringTypeConversion { - operation: Operation::RemoveConversionFlag, - }, - value.range(), - ); - if checker.patch(diagnostic.kind.rule()) { - diagnostic.try_set_fix(|| { - remove_conversion_flag(expr, index, checker.locator, checker.stylist) - }); - } - checker.diagnostics.push(diagnostic); - } - ConversionFlag::None => { - // Replace with the appropriate conversion flag. - let Expr::Call(ast::ExprCall { - func, - args, - keywords, - .. - }) = value.as_ref() else { - continue; - }; - - // Can't be a conversion otherwise. - if args.len() != 1 || !keywords.is_empty() { - continue; - } - - let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else { - continue; - }; - - if !matches!(id.as_str(), "str" | "repr" | "ascii") { - continue; - }; - - if !checker.semantic().is_builtin(id) { - continue; - } - - if id == "str" && format_spec.is_none() { - // Ex) `f"{str(bla)}"` - let mut diagnostic = Diagnostic::new( - ExplicitFStringTypeConversion { - operation: Operation::RemoveCall, - }, - value.range(), - ); - if checker.patch(diagnostic.kind.rule()) { - diagnostic.try_set_fix(|| { - remove_conversion_call(expr, index, checker.locator, checker.stylist) - }); - } - checker.diagnostics.push(diagnostic); - } else { - // Ex) `f"{repr(bla)}"` - let mut diagnostic = Diagnostic::new( - ExplicitFStringTypeConversion { - operation: Operation::ConvertCallToConversionFlag, - }, - value.range(), - ); - if checker.patch(diagnostic.kind.rule()) { - diagnostic.try_set_fix(|| { - convert_call_to_conversion_flag( - expr, - index, - checker.locator, - checker.stylist, - ) - }); - } - checker.diagnostics.push(diagnostic); - } - } + // Skip if there's already a conversion flag. + if !conversion.is_none() { + continue; } + + let Expr::Call(ast::ExprCall { + func, + args, + keywords, + .. + }) = value.as_ref() else { + continue; + }; + + // Can't be a conversion otherwise. + if args.len() != 1 || !keywords.is_empty() { + continue; + } + + let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else { + continue; + }; + + if !matches!(id.as_str(), "str" | "repr" | "ascii") { + continue; + }; + + if !checker.semantic().is_builtin(id) { + continue; + } + + let mut diagnostic = Diagnostic::new(ExplicitFStringTypeConversion, value.range()); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.try_set_fix(|| { + convert_call_to_conversion_flag(expr, index, checker.locator, checker.stylist) + }); + } + checker.diagnostics.push(diagnostic); } } -/// Generate a [`Fix`] to remove a conversion flag from a formatted expression. -fn remove_conversion_flag( - expr: &Expr, - index: usize, - locator: &Locator, - stylist: &Stylist, -) -> Result { - // Parenthesize the expression, to support implicit concatenation. - let range = expr.range(); - let content = locator.slice(range); - let parenthesized_content = format!("({content})"); - let mut expression = match_expression(&parenthesized_content)?; - - // Replace the formatted call expression at `index` with a conversion flag. - let formatted_string_expression = match_part(index, &mut expression)?; - formatted_string_expression.conversion = None; - - // Remove the parentheses (first and last characters). - let mut content = expression.codegen_stylist(stylist); - content.remove(0); - content.pop(); - - Ok(Fix::automatic(Edit::range_replacement(content, range))) -} - -/// Generate a [`Fix`] to remove a call from a formatted expression. -fn remove_conversion_call( - expr: &Expr, - index: usize, - locator: &Locator, - stylist: &Stylist, -) -> Result { - // Parenthesize the expression, to support implicit concatenation. - let range = expr.range(); - let content = locator.slice(range); - let parenthesized_content = format!("({content})"); - let mut expression = match_expression(&parenthesized_content)?; - - // Replace the formatted call expression at `index` with a conversion flag. - let formatted_string_expression = match_part(index, &mut expression)?; - let call = match_call_mut(&mut formatted_string_expression.expression)?; - formatted_string_expression.expression = call.args[0].value.clone(); - - // Remove the parentheses (first and last characters). - let mut content = expression.codegen_stylist(stylist); - content.remove(0); - content.pop(); - - Ok(Fix::automatic(Edit::range_replacement(content, range))) -} - /// Generate a [`Fix`] to replace an explicit type conversion with a conversion flag. fn convert_call_to_conversion_flag( expr: &Expr, diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap index 2f0133295c..ffbb12608b 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap @@ -1,21 +1,21 @@ --- source: crates/ruff/src/rules/ruff/mod.rs --- -RUF010.py:9:4: RUF010 [*] Remove unnecessary `str` conversion +RUF010.py:9:4: RUF010 [*] Use explicit conversion flag | 9 | f"{str(bla)}, {repr(bla)}, {ascii(bla)}" # RUF010 | ^^^^^^^^ RUF010 10 | 11 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010 | - = help: Remove `str` call + = help: Replace with conversion flag ℹ Fix 6 6 | pass 7 7 | 8 8 | 9 |-f"{str(bla)}, {repr(bla)}, {ascii(bla)}" # RUF010 - 9 |+f"{bla}, {repr(bla)}, {ascii(bla)}" # RUF010 + 9 |+f"{bla!s}, {repr(bla)}, {ascii(bla)}" # RUF010 10 10 | 11 11 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010 12 12 | @@ -58,7 +58,7 @@ RUF010.py:9:29: RUF010 [*] Use explicit conversion flag 11 11 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010 12 12 | -RUF010.py:11:4: RUF010 [*] Remove unnecessary `str` conversion +RUF010.py:11:4: RUF010 [*] Use explicit conversion flag | 9 | f"{str(bla)}, {repr(bla)}, {ascii(bla)}" # RUF010 10 | @@ -67,14 +67,14 @@ RUF010.py:11:4: RUF010 [*] Remove unnecessary `str` conversion 12 | 13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 | - = help: Remove `str` call + = help: Replace with conversion flag ℹ Fix 8 8 | 9 9 | f"{str(bla)}, {repr(bla)}, {ascii(bla)}" # RUF010 10 10 | 11 |-f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010 - 11 |+f"{d['a']}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010 + 11 |+f"{d['a']!s}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010 12 12 | 13 13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 14 14 | @@ -121,7 +121,7 @@ RUF010.py:11:35: RUF010 [*] Use explicit conversion flag 13 13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 14 14 | -RUF010.py:13:5: RUF010 [*] Remove unnecessary `str` conversion +RUF010.py:13:5: RUF010 [*] Use explicit conversion flag | 11 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010 12 | @@ -130,14 +130,14 @@ RUF010.py:13:5: RUF010 [*] Remove unnecessary `str` conversion 14 | 15 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 | - = help: Remove `str` call + = help: Replace with conversion flag ℹ Fix 10 10 | 11 11 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010 12 12 | 13 |-f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 - 13 |+f"{bla}, {(repr(bla))}, {(ascii(bla))}" # RUF010 + 13 |+f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 14 14 | 15 15 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 16 16 | @@ -184,27 +184,6 @@ RUF010.py:13:34: RUF010 [*] Use explicit conversion flag 15 15 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 16 16 | -RUF010.py:15:4: RUF010 [*] Remove unnecessary conversion flag - | -13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 -14 | -15 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 - | ^^^ RUF010 -16 | -17 | f"{foo(bla)}" # OK - | - = help: Remove conversion flag - -ℹ Fix -12 12 | -13 13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 -14 14 | -15 |-f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 - 15 |+f"{bla}, {(repr(bla))}, {(ascii(bla))}" # RUF010 -16 16 | -17 17 | f"{foo(bla)}" # OK -18 18 | - RUF010.py:15:14: RUF010 [*] Use explicit conversion flag | 13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 @@ -247,27 +226,6 @@ RUF010.py:15:29: RUF010 [*] Use explicit conversion flag 17 17 | f"{foo(bla)}" # OK 18 18 | -RUF010.py:21:4: RUF010 [*] Remove unnecessary conversion flag - | -19 | f"{str(bla, 'ascii')}, {str(bla, encoding='cp1255')}" # OK -20 | -21 | f"{bla!s} {[]!r} {'bar'!a}" # OK - | ^^^ RUF010 -22 | -23 | "Not an f-string {str(bla)}, {repr(bla)}, {ascii(bla)}" # OK - | - = help: Remove conversion flag - -ℹ Fix -18 18 | -19 19 | f"{str(bla, 'ascii')}, {str(bla, encoding='cp1255')}" # OK -20 20 | -21 |-f"{bla!s} {[]!r} {'bar'!a}" # OK - 21 |+f"{bla} {[]!r} {'bar'!a}" # OK -22 22 | -23 23 | "Not an f-string {str(bla)}, {repr(bla)}, {ascii(bla)}" # OK -24 24 | - RUF010.py:35:20: RUF010 [*] Use explicit conversion flag | 33 | f"Member of tuple mismatches type at index {i}. Expected {of_shape_i}. Got " @@ -285,67 +243,5 @@ RUF010.py:35:20: RUF010 [*] Use explicit conversion flag 35 |- f" that flows {repr(obj)} of type {type(obj)}.{additional_message}" # RUF010 35 |+ f" that flows {obj!r} of type {type(obj)}.{additional_message}" # RUF010 36 36 | ) -37 37 | -38 38 | - -RUF010.py:39:4: RUF010 [*] Remove unnecessary `str` conversion - | -39 | f"{str(bla)}" # RUF010 - | ^^^^^^^^ RUF010 -40 | -41 | f"{str(bla):20}" # RUF010 - | - = help: Remove `str` call - -ℹ Fix -36 36 | ) -37 37 | -38 38 | -39 |-f"{str(bla)}" # RUF010 - 39 |+f"{bla}" # RUF010 -40 40 | -41 41 | f"{str(bla):20}" # RUF010 -42 42 | - -RUF010.py:41:4: RUF010 [*] Use explicit conversion flag - | -39 | f"{str(bla)}" # RUF010 -40 | -41 | f"{str(bla):20}" # RUF010 - | ^^^^^^^^ RUF010 -42 | -43 | f"{bla!s}" # RUF010 - | - = help: Replace with conversion flag - -ℹ Fix -38 38 | -39 39 | f"{str(bla)}" # RUF010 -40 40 | -41 |-f"{str(bla):20}" # RUF010 - 41 |+f"{bla!s:20}" # RUF010 -42 42 | -43 43 | f"{bla!s}" # RUF010 -44 44 | - -RUF010.py:43:4: RUF010 [*] Remove unnecessary conversion flag - | -41 | f"{str(bla):20}" # RUF010 -42 | -43 | f"{bla!s}" # RUF010 - | ^^^ RUF010 -44 | -45 | f"{bla!s:20}" # OK - | - = help: Remove conversion flag - -ℹ Fix -40 40 | -41 41 | f"{str(bla):20}" # RUF010 -42 42 | -43 |-f"{bla!s}" # RUF010 - 43 |+f"{bla}" # RUF010 -44 44 | -45 45 | f"{bla!s:20}" # OK From 621e9ace887ce7ca976d000c42f677e65c1ee912 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 20 Jun 2023 21:21:45 -0400 Subject: [PATCH 143/447] Use package roots rather than package members for cache initialization (#5233) ## Summary This is a proper fix for the issue patched-over in https://github.com/astral-sh/ruff/pull/5229, thanks to an extremely helpful repro from @tlambert03 in that thread. It looks like we were using the keys of `package_roots` rather than the values to initialize the cache -- but it's a map from package to package root. ## Test Plan Reverted #5229, then ran through the plan that @tlambert03 included in https://github.com/astral-sh/ruff/pull/5229#issuecomment-1599723226. Verified the panic before but not after this change. --- crates/ruff_cli/src/commands/run.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/ruff_cli/src/commands/run.rs b/crates/ruff_cli/src/commands/run.rs index ac27ff15c9..0698fd62ce 100644 --- a/crates/ruff_cli/src/commands/run.rs +++ b/crates/ruff_cli/src/commands/run.rs @@ -7,6 +7,7 @@ use std::time::Instant; use anyhow::Result; use colored::Colorize; use ignore::Error; +use itertools::Itertools; use log::{debug, error, warn}; #[cfg(not(target_family = "wasm"))] use rayon::prelude::*; @@ -80,8 +81,10 @@ pub(crate) fn run( // Load the caches. let caches = bool::from(cache).then(|| { package_roots - .par_iter() - .map(|(package_root, _)| { + .values() + .flatten() + .dedup() + .map(|package_root| { let settings = resolver.resolve_all(package_root, pyproject_config); let cache = Cache::open( &settings.cli.cache_dir, From 1db7d9e75959ebefc0b3d06fa7b134cd910b8961 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 20 Jun 2023 21:29:12 -0400 Subject: [PATCH 144/447] Avoid erroneous RUF013 violations for quoted annotations (#5234) ## Summary Temporary fix for #5231: if we can't flag and fix these properly, just disabling them for now. \cc @dhruvmanila ## Test Plan `cargo test` --- .../resources/test/fixtures/ruff/RUF013_0.py | 15 +++++++++++++ .../src/rules/ruff/rules/implicit_optional.rs | 21 ++++++++++++------- ..._ruff__tests__PY39_RUF013_RUF013_0.py.snap | 2 ++ ...ules__ruff__tests__RUF013_RUF013_0.py.snap | 2 ++ 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py b/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py index 90252f46a1..527fe792ee 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py @@ -185,3 +185,18 @@ def f(arg: Union[Annotated[int, ...], Annotated[Optional[float], ...]] = None): def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF011 pass + + +# Quoted + + +def f(arg: "int" = None): + pass + + +def f(arg: "str" = None): + pass + + +def f(arg: "Optional[int]" = None): + pass diff --git a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs index 2cc6f1dbc0..dd0d5a7721 100644 --- a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs +++ b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs @@ -142,6 +142,7 @@ enum TypingTarget<'a> { None, Any, Object, + ForwardReference, Optional, Union(Vec<&'a Expr>), Literal(Vec<&'a Expr>), @@ -175,6 +176,10 @@ impl<'a> TypingTarget<'a> { value: Constant::None, .. }) => Some(TypingTarget::None), + Expr::Constant(ast::ExprConstant { + value: Constant::Str(_), + .. + }) => Some(TypingTarget::ForwardReference), _ => semantic.resolve_call_path(expr).and_then(|call_path| { if semantic.match_typing_call_path(&call_path, "Any") { Some(TypingTarget::Any) @@ -196,8 +201,8 @@ impl<'a> TypingTarget<'a> { | TypingTarget::Object => true, TypingTarget::Literal(elements) => elements.iter().any(|element| { let Some(new_target) = TypingTarget::try_from_expr(element, semantic) else { - return false; - }; + return false; + }; // Literal can only contain `None`, a literal value, other `Literal` // or an enum value. match new_target { @@ -208,8 +213,8 @@ impl<'a> TypingTarget<'a> { }), TypingTarget::Union(elements) => elements.iter().any(|element| { let Some(new_target) = TypingTarget::try_from_expr(element, semantic) else { - return false; - }; + return false; + }; match new_target { TypingTarget::None => true, _ => new_target.contains_none(semantic), @@ -217,13 +222,15 @@ impl<'a> TypingTarget<'a> { }), TypingTarget::Annotated(element) => { let Some(new_target) = TypingTarget::try_from_expr(element, semantic) else { - return false; - }; + return false; + }; match new_target { TypingTarget::None => true, _ => new_target.contains_none(semantic), } } + // TODO(charlie): Add support for forward references (quoted annotations). + TypingTarget::ForwardReference => true, } } } @@ -305,7 +312,7 @@ fn generate_fix(checker: &Checker, conversion_type: ConversionType, expr: &Expr) } } -/// RUF011 +/// RUF013 pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) { for ArgWithDefault { def, diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap index 956b055d44..72f5f885b6 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap @@ -312,5 +312,7 @@ RUF013_0.py:186:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 186 |-def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF011 186 |+def f(arg: Optional[Union[Annotated[int, ...], Union[str, bytes]]] = None): # RUF011 187 187 | pass +188 188 | +189 189 | diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap index 23ff31a6b6..7ec7c527d2 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap @@ -312,5 +312,7 @@ RUF013_0.py:186:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 186 |-def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF011 186 |+def f(arg: Union[Annotated[int, ...], Union[str, bytes]] | None = None): # RUF011 187 187 | pass +188 188 | +189 189 | From 07b6b7401f56ca75d1478fbe0c68f4093365cac1 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 20 Jun 2023 21:56:40 -0400 Subject: [PATCH 145/447] Move `copyright` rules to `flake8_copyright` module (#5236) ## Summary I initially wanted this category to be more general and decoupled from the plugin, but I got some feedback that the titling felt inconsistent with others. --- crates/ruff/src/checkers/physical_lines.rs | 2 +- crates/ruff/src/codes.rs | 2 +- ...ruff__rules__copyright__tests__notice.snap | 4 - ...ules__copyright__tests__notice_with_c.snap | 4 - ...s__copyright__tests__notice_with_caps.snap | 4 - ...__copyright__tests__notice_with_range.snap | 4 - ...__rules__copyright__tests__small_file.snap | 4 - ...rules__copyright__tests__valid_author.snap | 4 - .../{copyright => flake8_copyright}/mod.rs | 6 +- .../rules/missing_copyright_notice.rs | 6 +- .../rules/mod.rs | 0 .../settings.rs | 4 +- ...ke8_copyright__tests__invalid_author.snap} | 2 +- ...flake8_copyright__tests__late_notice.snap} | 2 +- ...ules__flake8_copyright__tests__notice.snap | 4 + ...lake8_copyright__tests__notice_with_c.snap | 4 + ...e8_copyright__tests__notice_with_caps.snap | 4 + ...8_copyright__tests__notice_with_range.snap | 4 + ...__flake8_copyright__tests__small_file.snap | 4 + ...flake8_copyright__tests__valid_author.snap | 4 + crates/ruff/src/rules/mod.rs | 2 +- crates/ruff/src/settings/configuration.rs | 12 +-- crates/ruff/src/settings/defaults.rs | 6 +- crates/ruff/src/settings/mod.rs | 14 ++-- crates/ruff/src/settings/options.rs | 6 +- crates/ruff_wasm/src/lib.rs | 12 +-- ruff.schema.json | 80 +++++++++---------- 27 files changed, 102 insertions(+), 102 deletions(-) delete mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice.snap delete mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_c.snap delete mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_caps.snap delete mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_range.snap delete mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__small_file.snap delete mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__valid_author.snap rename crates/ruff/src/rules/{copyright => flake8_copyright}/mod.rs (96%) rename crates/ruff/src/rules/{copyright => flake8_copyright}/rules/missing_copyright_notice.rs (88%) rename crates/ruff/src/rules/{copyright => flake8_copyright}/rules/mod.rs (100%) rename crates/ruff/src/rules/{copyright => flake8_copyright}/settings.rs (97%) rename crates/ruff/src/rules/{copyright/snapshots/ruff__rules__copyright__tests__invalid_author.snap => flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__invalid_author.snap} (73%) rename crates/ruff/src/rules/{copyright/snapshots/ruff__rules__copyright__tests__late_notice.snap => flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__late_notice.snap} (86%) create mode 100644 crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice.snap create mode 100644 crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_c.snap create mode 100644 crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_caps.snap create mode 100644 crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_range.snap create mode 100644 crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__small_file.snap create mode 100644 crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__valid_author.snap diff --git a/crates/ruff/src/checkers/physical_lines.rs b/crates/ruff/src/checkers/physical_lines.rs index 9882bc86ef..1a553f3d7d 100644 --- a/crates/ruff/src/checkers/physical_lines.rs +++ b/crates/ruff/src/checkers/physical_lines.rs @@ -8,7 +8,7 @@ use ruff_python_ast::source_code::{Indexer, Locator, Stylist}; use ruff_python_whitespace::UniversalNewlines; use crate::registry::Rule; -use crate::rules::copyright::rules::missing_copyright_notice; +use crate::rules::flake8_copyright::rules::missing_copyright_notice; use crate::rules::flake8_executable::helpers::{extract_shebang, ShebangDirective}; use crate::rules::flake8_executable::rules::{ shebang_missing, shebang_newline, shebang_not_executable, shebang_python, shebang_whitespace, diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 78356ead86..68409be315 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -375,7 +375,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Simplify, "910") => (RuleGroup::Unspecified, rules::flake8_simplify::rules::DictGetWithNoneDefault), // copyright - (Copyright, "001") => (RuleGroup::Nursery, rules::copyright::rules::MissingCopyrightNotice), + (Copyright, "001") => (RuleGroup::Nursery, rules::flake8_copyright::rules::MissingCopyrightNotice), // pyupgrade (Pyupgrade, "001") => (RuleGroup::Unspecified, rules::pyupgrade::rules::UselessMetaclassType), diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice.snap deleted file mode 100644 index 2a35fab682..0000000000 --- a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice.snap +++ /dev/null @@ -1,4 +0,0 @@ ---- -source: crates/ruff/src/rules/copyright/mod.rs ---- - diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_c.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_c.snap deleted file mode 100644 index 2a35fab682..0000000000 --- a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_c.snap +++ /dev/null @@ -1,4 +0,0 @@ ---- -source: crates/ruff/src/rules/copyright/mod.rs ---- - diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_caps.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_caps.snap deleted file mode 100644 index 2a35fab682..0000000000 --- a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_caps.snap +++ /dev/null @@ -1,4 +0,0 @@ ---- -source: crates/ruff/src/rules/copyright/mod.rs ---- - diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_range.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_range.snap deleted file mode 100644 index 2a35fab682..0000000000 --- a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_range.snap +++ /dev/null @@ -1,4 +0,0 @@ ---- -source: crates/ruff/src/rules/copyright/mod.rs ---- - diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__small_file.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__small_file.snap deleted file mode 100644 index 2a35fab682..0000000000 --- a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__small_file.snap +++ /dev/null @@ -1,4 +0,0 @@ ---- -source: crates/ruff/src/rules/copyright/mod.rs ---- - diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__valid_author.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__valid_author.snap deleted file mode 100644 index 2a35fab682..0000000000 --- a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__valid_author.snap +++ /dev/null @@ -1,4 +0,0 @@ ---- -source: crates/ruff/src/rules/copyright/mod.rs ---- - diff --git a/crates/ruff/src/rules/copyright/mod.rs b/crates/ruff/src/rules/flake8_copyright/mod.rs similarity index 96% rename from crates/ruff/src/rules/copyright/mod.rs rename to crates/ruff/src/rules/flake8_copyright/mod.rs index ceadac5efb..576d9daedf 100644 --- a/crates/ruff/src/rules/copyright/mod.rs +++ b/crates/ruff/src/rules/flake8_copyright/mod.rs @@ -75,7 +75,7 @@ import os "# .trim(), &settings::Settings { - copyright: super::settings::Settings { + flake8_copyright: super::settings::Settings { author: Some("Ruff".to_string()), ..super::settings::Settings::default() }, @@ -95,7 +95,7 @@ import os "# .trim(), &settings::Settings { - copyright: super::settings::Settings { + flake8_copyright: super::settings::Settings { author: Some("Ruff".to_string()), ..super::settings::Settings::default() }, @@ -113,7 +113,7 @@ import os "# .trim(), &settings::Settings { - copyright: super::settings::Settings { + flake8_copyright: super::settings::Settings { min_file_size: 256, ..super::settings::Settings::default() }, diff --git a/crates/ruff/src/rules/copyright/rules/missing_copyright_notice.rs b/crates/ruff/src/rules/flake8_copyright/rules/missing_copyright_notice.rs similarity index 88% rename from crates/ruff/src/rules/copyright/rules/missing_copyright_notice.rs rename to crates/ruff/src/rules/flake8_copyright/rules/missing_copyright_notice.rs index 00ceccffee..ce3cca98c7 100644 --- a/crates/ruff/src/rules/copyright/rules/missing_copyright_notice.rs +++ b/crates/ruff/src/rules/flake8_copyright/rules/missing_copyright_notice.rs @@ -28,7 +28,7 @@ pub(crate) fn missing_copyright_notice( settings: &Settings, ) -> Option { // Ignore files that are too small to contain a copyright notice. - if locator.len() < settings.copyright.min_file_size { + if locator.len() < settings.flake8_copyright.min_file_size { return None; } @@ -40,8 +40,8 @@ pub(crate) fn missing_copyright_notice( }; // Locate the copyright notice. - if let Some(match_) = settings.copyright.notice_rgx.find(contents) { - match settings.copyright.author { + if let Some(match_) = settings.flake8_copyright.notice_rgx.find(contents) { + match settings.flake8_copyright.author { Some(ref author) => { // Ensure that it's immediately followed by the author. if contents[match_.end()..].trim_start().starts_with(author) { diff --git a/crates/ruff/src/rules/copyright/rules/mod.rs b/crates/ruff/src/rules/flake8_copyright/rules/mod.rs similarity index 100% rename from crates/ruff/src/rules/copyright/rules/mod.rs rename to crates/ruff/src/rules/flake8_copyright/rules/mod.rs diff --git a/crates/ruff/src/rules/copyright/settings.rs b/crates/ruff/src/rules/flake8_copyright/settings.rs similarity index 97% rename from crates/ruff/src/rules/copyright/settings.rs rename to crates/ruff/src/rules/flake8_copyright/settings.rs index f3e400ca25..f4f8e0c17e 100644 --- a/crates/ruff/src/rules/copyright/settings.rs +++ b/crates/ruff/src/rules/flake8_copyright/settings.rs @@ -1,4 +1,4 @@ -//! Settings for the `copyright` plugin. +//! Settings for the `flake8-copyright` plugin. use once_cell::sync::Lazy; use regex::Regex; @@ -12,7 +12,7 @@ use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions}; #[serde( deny_unknown_fields, rename_all = "kebab-case", - rename = "CopyrightOptions" + rename = "Flake8CopyrightOptions" )] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct Options { diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__invalid_author.snap b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__invalid_author.snap similarity index 73% rename from crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__invalid_author.snap rename to crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__invalid_author.snap index 2b09c76351..4393791320 100644 --- a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__invalid_author.snap +++ b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__invalid_author.snap @@ -1,5 +1,5 @@ --- -source: crates/ruff/src/rules/copyright/mod.rs +source: crates/ruff/src/rules/flake8_copyright/mod.rs --- :1:1: CPY001 Missing copyright notice at top of file | diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__late_notice.snap b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__late_notice.snap similarity index 86% rename from crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__late_notice.snap rename to crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__late_notice.snap index 0bc581e98d..17efa2aa8d 100644 --- a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__late_notice.snap +++ b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__late_notice.snap @@ -1,5 +1,5 @@ --- -source: crates/ruff/src/rules/copyright/mod.rs +source: crates/ruff/src/rules/flake8_copyright/mod.rs --- :1:1: CPY001 Missing copyright notice at top of file | diff --git a/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice.snap b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice.snap new file mode 100644 index 0000000000..383a7b0dac --- /dev/null +++ b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_c.snap b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_c.snap new file mode 100644 index 0000000000..383a7b0dac --- /dev/null +++ b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_c.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_caps.snap b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_caps.snap new file mode 100644 index 0000000000..383a7b0dac --- /dev/null +++ b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_caps.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_range.snap b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_range.snap new file mode 100644 index 0000000000..383a7b0dac --- /dev/null +++ b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_range.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__small_file.snap b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__small_file.snap new file mode 100644 index 0000000000..383a7b0dac --- /dev/null +++ b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__small_file.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__valid_author.snap b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__valid_author.snap new file mode 100644 index 0000000000..383a7b0dac --- /dev/null +++ b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__valid_author.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/mod.rs b/crates/ruff/src/rules/mod.rs index 4a12fb0319..e8fd430cb0 100644 --- a/crates/ruff/src/rules/mod.rs +++ b/crates/ruff/src/rules/mod.rs @@ -1,6 +1,5 @@ #![allow(clippy::useless_format)] pub mod airflow; -pub mod copyright; pub mod eradicate; pub mod flake8_2020; pub mod flake8_annotations; @@ -12,6 +11,7 @@ pub mod flake8_bugbear; pub mod flake8_builtins; pub mod flake8_commas; pub mod flake8_comprehensions; +pub mod flake8_copyright; pub mod flake8_datetimez; pub mod flake8_debugger; pub mod flake8_django; diff --git a/crates/ruff/src/settings/configuration.rs b/crates/ruff/src/settings/configuration.rs index 213128b966..16143a83cb 100644 --- a/crates/ruff/src/settings/configuration.rs +++ b/crates/ruff/src/settings/configuration.rs @@ -16,8 +16,8 @@ use crate::fs; use crate::line_width::{LineLength, TabSize}; use crate::rule_selector::RuleSelector; use crate::rules::{ - copyright, flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, - flake8_comprehensions, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, + flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, + 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, @@ -75,14 +75,14 @@ pub struct Configuration { pub flake8_bugbear: Option, pub flake8_builtins: Option, pub flake8_comprehensions: Option, - pub copyright: Option, + pub flake8_copyright: Option, pub flake8_errmsg: Option, + pub flake8_gettext: Option, pub flake8_implicit_str_concat: Option, pub flake8_import_conventions: Option, pub flake8_pytest_style: Option, pub flake8_quotes: Option, pub flake8_self: Option, - pub flake8_gettext: Option, pub flake8_tidy_imports: Option, pub flake8_type_checking: Option, pub flake8_unused_arguments: Option, @@ -229,7 +229,7 @@ impl Configuration { flake8_bugbear: options.flake8_bugbear, flake8_builtins: options.flake8_builtins, flake8_comprehensions: options.flake8_comprehensions, - copyright: options.copyright, + flake8_copyright: options.flake8_copyright, flake8_errmsg: options.flake8_errmsg, flake8_gettext: options.flake8_gettext, flake8_implicit_str_concat: options.flake8_implicit_str_concat, @@ -308,7 +308,7 @@ impl Configuration { flake8_comprehensions: self .flake8_comprehensions .combine(config.flake8_comprehensions), - copyright: self.copyright.combine(config.copyright), + flake8_copyright: self.flake8_copyright.combine(config.flake8_copyright), flake8_errmsg: self.flake8_errmsg.combine(config.flake8_errmsg), flake8_gettext: self.flake8_gettext.combine(config.flake8_gettext), flake8_implicit_str_concat: self diff --git a/crates/ruff/src/settings/defaults.rs b/crates/ruff/src/settings/defaults.rs index 129c3a16f2..8ae6d16e73 100644 --- a/crates/ruff/src/settings/defaults.rs +++ b/crates/ruff/src/settings/defaults.rs @@ -10,8 +10,8 @@ use crate::line_width::{LineLength, TabSize}; use crate::registry::Linter; use crate::rule_selector::{prefix_to_selector, RuleSelector}; use crate::rules::{ - copyright, flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, - flake8_comprehensions, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, + flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, + 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, @@ -96,7 +96,7 @@ impl Default for Settings { flake8_bugbear: flake8_bugbear::settings::Settings::default(), flake8_builtins: flake8_builtins::settings::Settings::default(), flake8_comprehensions: flake8_comprehensions::settings::Settings::default(), - copyright: copyright::settings::Settings::default(), + flake8_copyright: flake8_copyright::settings::Settings::default(), flake8_errmsg: flake8_errmsg::settings::Settings::default(), flake8_implicit_str_concat: flake8_implicit_str_concat::settings::Settings::default(), flake8_import_conventions: flake8_import_conventions::settings::Settings::default(), diff --git a/crates/ruff/src/settings/mod.rs b/crates/ruff/src/settings/mod.rs index 9a36b59908..0f7b961734 100644 --- a/crates/ruff/src/settings/mod.rs +++ b/crates/ruff/src/settings/mod.rs @@ -16,8 +16,8 @@ use ruff_macros::CacheKey; use crate::registry::{Rule, RuleNamespace, RuleSet, INCOMPATIBLE_CODES}; use crate::rule_selector::{RuleSelector, Specificity}; use crate::rules::{ - copyright, flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, - flake8_comprehensions, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, + flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, + 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, @@ -112,11 +112,11 @@ pub struct Settings { pub flake8_bugbear: flake8_bugbear::settings::Settings, pub flake8_builtins: flake8_builtins::settings::Settings, pub flake8_comprehensions: flake8_comprehensions::settings::Settings, - pub copyright: copyright::settings::Settings, + pub flake8_copyright: flake8_copyright::settings::Settings, pub flake8_errmsg: flake8_errmsg::settings::Settings, + pub flake8_gettext: flake8_gettext::settings::Settings, pub flake8_implicit_str_concat: flake8_implicit_str_concat::settings::Settings, pub flake8_import_conventions: flake8_import_conventions::settings::Settings, - pub flake8_gettext: flake8_gettext::settings::Settings, pub flake8_pytest_style: flake8_pytest_style::settings::Settings, pub flake8_quotes: flake8_quotes::settings::Settings, pub flake8_self: flake8_self::settings::Settings, @@ -210,9 +210,9 @@ impl Settings { .flake8_comprehensions .map(flake8_comprehensions::settings::Settings::from) .unwrap_or_default(), - copyright: config - .copyright - .map(copyright::settings::Settings::try_from) + flake8_copyright: config + .flake8_copyright + .map(flake8_copyright::settings::Settings::try_from) .transpose()? .unwrap_or_default(), flake8_errmsg: config diff --git a/crates/ruff/src/settings/options.rs b/crates/ruff/src/settings/options.rs index 7015b29b60..ea18f0cb6d 100644 --- a/crates/ruff/src/settings/options.rs +++ b/crates/ruff/src/settings/options.rs @@ -8,8 +8,8 @@ use ruff_macros::ConfigurationOptions; use crate::line_width::{LineLength, TabSize}; use crate::rule_selector::RuleSelector; use crate::rules::{ - copyright, flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, - flake8_comprehensions, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, + flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, + 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, @@ -499,7 +499,7 @@ pub struct Options { pub flake8_comprehensions: Option, #[option_group] /// Options for the `copyright` plugin. - pub copyright: Option, + pub flake8_copyright: Option, #[option_group] /// Options for the `flake8-errmsg` plugin. pub flake8_errmsg: Option, diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index dce7b79797..2a449d0864 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -9,8 +9,8 @@ use ruff::line_width::{LineLength, TabSize}; use ruff::linter::{check_path, LinterResult}; use ruff::registry::AsRule; use ruff::rules::{ - copyright, flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, - flake8_comprehensions, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, + flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, + 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, @@ -138,11 +138,8 @@ pub fn defaultSettings() -> Result { flake8_bugbear: Some(flake8_bugbear::settings::Settings::default().into()), flake8_builtins: Some(flake8_builtins::settings::Settings::default().into()), flake8_comprehensions: Some(flake8_comprehensions::settings::Settings::default().into()), - copyright: Some(copyright::settings::Settings::default().into()), + flake8_copyright: Some(flake8_copyright::settings::Settings::default().into()), flake8_errmsg: Some(flake8_errmsg::settings::Settings::default().into()), - flake8_pytest_style: Some(flake8_pytest_style::settings::Settings::default().into()), - flake8_quotes: Some(flake8_quotes::settings::Settings::default().into()), - flake8_self: Some(flake8_self::settings::Settings::default().into()), flake8_gettext: Some(flake8_gettext::settings::Settings::default().into()), flake8_implicit_str_concat: Some( flake8_implicit_str_concat::settings::Settings::default().into(), @@ -150,6 +147,9 @@ pub fn defaultSettings() -> Result { flake8_import_conventions: Some( flake8_import_conventions::settings::Settings::default().into(), ), + flake8_pytest_style: Some(flake8_pytest_style::settings::Settings::default().into()), + flake8_quotes: Some(flake8_quotes::settings::Settings::default().into()), + flake8_self: Some(flake8_self::settings::Settings::default().into()), flake8_tidy_imports: Some(flake8_tidy_imports::settings::Settings::default().into()), flake8_type_checking: Some(flake8_type_checking::settings::Settings::default().into()), flake8_unused_arguments: Some( diff --git a/ruff.schema.json b/ruff.schema.json index c74c74e26b..77cd975fac 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -32,17 +32,6 @@ "null" ] }, - "copyright": { - "description": "Options for the `copyright` plugin.", - "anyOf": [ - { - "$ref": "#/definitions/CopyrightOptions" - }, - { - "type": "null" - } - ] - }, "dummy-variable-rgx": { "description": "A regular expression used to identify \"dummy\" variables, or those which should be ignored when enforcing (e.g.) unused-variable rules. The default expression matches `_`, `__`, and `_var`, but not `_var_`.", "type": [ @@ -209,6 +198,17 @@ } ] }, + "flake8-copyright": { + "description": "Options for the `copyright` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/Flake8CopyrightOptions" + }, + { + "type": "null" + } + ] + }, "flake8-errmsg": { "description": "Options for the `flake8-errmsg` plugin.", "anyOf": [ @@ -631,35 +631,6 @@ } ] }, - "CopyrightOptions": { - "type": "object", - "properties": { - "author": { - "description": "Author to enforce within the copyright notice. If provided, the author must be present immediately following the copyright notice.", - "type": [ - "string", - "null" - ] - }, - "min-file-size": { - "description": "A minimum file size (in bytes) required for a copyright notice to be enforced. By default, all files are validated.", - "type": [ - "integer", - "null" - ], - "format": "uint", - "minimum": 0.0 - }, - "notice-rgx": { - "description": "The regular expression used to match the copyright notice, compiled with the [`regex`](https://docs.rs/regex/latest/regex/) crate.\n\nDefaults to `(?i)Copyright\\s+(\\(C\\)\\s+)?\\d{4}(-\\d{4})*`, which matches the following: - `Copyright 2023` - `Copyright (C) 2023` - `Copyright 2021-2023` - `Copyright (C) 2021-2023`", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - }, "Flake8AnnotationsOptions": { "type": "object", "properties": { @@ -779,6 +750,35 @@ }, "additionalProperties": false }, + "Flake8CopyrightOptions": { + "type": "object", + "properties": { + "author": { + "description": "Author to enforce within the copyright notice. If provided, the author must be present immediately following the copyright notice.", + "type": [ + "string", + "null" + ] + }, + "min-file-size": { + "description": "A minimum file size (in bytes) required for a copyright notice to be enforced. By default, all files are validated.", + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 0.0 + }, + "notice-rgx": { + "description": "The regular expression used to match the copyright notice, compiled with the [`regex`](https://docs.rs/regex/latest/regex/) crate.\n\nDefaults to `(?i)Copyright\\s+(\\(C\\)\\s+)?\\d{4}(-\\d{4})*`, which matches the following: - `Copyright 2023` - `Copyright (C) 2023` - `Copyright 2021-2023` - `Copyright (C) 2021-2023`", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, "Flake8ErrMsgOptions": { "type": "object", "properties": { From e0339b538bd43bdbd1e58042e0b8bf55d3f077d2 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 20 Jun 2023 22:12:32 -0400 Subject: [PATCH 146/447] Bump version to 0.0.274 (#5230) --- 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 0d436534b5..e171d287d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -733,7 +733,7 @@ dependencies = [ [[package]] name = "flake8-to-ruff" -version = "0.0.273" +version = "0.0.274" dependencies = [ "anyhow", "clap", @@ -1793,7 +1793,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.0.273" +version = "0.0.274" dependencies = [ "annotate-snippets 0.9.1", "anyhow", @@ -1889,7 +1889,7 @@ dependencies = [ [[package]] name = "ruff_cli" -version = "0.0.273" +version = "0.0.274" dependencies = [ "annotate-snippets 0.9.1", "anyhow", diff --git a/README.md b/README.md index 4e8454b9a7..66a3a8ccf7 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.273 + rev: v0.0.274 hooks: - id: ruff ``` diff --git a/crates/flake8_to_ruff/Cargo.toml b/crates/flake8_to_ruff/Cargo.toml index dd7b12cf3c..c2f2e81217 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.273" +version = "0.0.274" description = """ Convert Flake8 configuration files to Ruff configuration files. """ diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 25725e78ea..4f482cff0c 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.0.273" +version = "0.0.274" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_cli/Cargo.toml b/crates/ruff_cli/Cargo.toml index 2f7085ab6d..878923e286 100644 --- a/crates/ruff_cli/Cargo.toml +++ b/crates/ruff_cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_cli" -version = "0.0.273" +version = "0.0.274" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/docs/tutorial.md b/docs/tutorial.md index 492f33bdaf..b474b378d9 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.273 + rev: v0.0.274 hooks: - id: ruff ``` diff --git a/docs/usage.md b/docs/usage.md index 076ecc8532..a3b9ad0039 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.273 + rev: v0.0.274 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.273 + rev: v0.0.274 hooks: - id: ruff args: [ --fix, --exit-non-zero-on-fix ] diff --git a/pyproject.toml b/pyproject.toml index b359b87224..a2f53c596f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.0.273" +version = "0.0.274" 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 a332f078db63c9e56e3e4d33526c376a5b619a8f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 20 Jun 2023 23:16:35 -0400 Subject: [PATCH 147/447] Checkout repo to support release tag validation (#5238) ## Summary The [release](https://github.com/astral-sh/ruff/actions/runs/5329340068/jobs/9655224008) failed due to an inability to find `pyproject.toml`. This PR moves that validation into its own step (so we can fail fast) and ensures we clone the repo. --- .github/workflows/release.yaml | 51 ++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e30114a337..699a6c295b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: tag: - description: "The version to tag, without the leading 'v'. If omitted, will initiate a dry run skipping uploading artifact." + description: "The version to tag, without the leading 'v'. If omitted, will initiate a dry run (no uploads)." type: string sha: description: "Optionally, the full sha of the commit to be released" @@ -392,28 +392,14 @@ jobs: *.tar.gz *.sha256 - release: - name: Release + validate-tag: + name: Validate tag runs-on: ubuntu-latest - needs: - - macos-universal - - macos-x86_64 - - windows - - linux - - linux-cross - - musllinux - - musllinux-cross - # If you don't set an input it's a dry run skipping uploading artifact + # If you don't set an input tag, it's a dry run (no uploads). if: ${{ inputs.tag }} - environment: - name: release - permissions: - # For pypi trusted publishing - id-token: write - # For GitHub release publishing - contents: write steps: - - name: Consistency check tag + - uses: actions/checkout@v3 + - name: Check tag consistency run: | version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g') if [ "${{ inputs.tag }}" != "${version}" ]; then @@ -424,7 +410,7 @@ jobs: else echo "Releasing ${version}" fi - - name: Consistency check sha + - name: Check SHA consistency if: ${{ inputs.sha }} run: | git_sha=$(git rev-parse HEAD) @@ -436,6 +422,29 @@ jobs: else echo "Releasing ${git_sha}" fi + + release: + name: Release + runs-on: ubuntu-latest + needs: + - macos-universal + - macos-x86_64 + - windows + - linux + - linux-cross + - musllinux + - musllinux-cross + - validate-tag + # If you don't set an input tag, it's a dry run (no uploads). + if: ${{ inputs.tag }} + environment: + name: release + permissions: + # For pypi trusted publishing + id-token: write + # For GitHub release publishing + contents: write + steps: - uses: actions/download-artifact@v3 with: name: wheels From 39738364208f9cb6b00baa475d18ab2ca9de9af8 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 21 Jun 2023 09:40:05 +0200 Subject: [PATCH 148/447] Correctly handle left/right breaking of binary expression ## Summary Black supports for layouts when it comes to breaking binary expressions: ```rust #[derive(Copy, Clone, Debug, Eq, PartialEq)] enum BinaryLayout { /// Put each operand on their own line if either side expands Default, /// Try to expand the left to make it fit. Add parentheses if the left or right don't fit. /// ///```python /// [ /// a, /// b /// ] & c ///``` ExpandLeft, /// Try to expand the right to make it fix. Add parentheses if the left or right don't fit. /// /// ```python /// a & [ /// b, /// c /// ] /// ``` ExpandRight, /// Both the left and right side can be expanded. Try in the following order: /// * expand the right side /// * expand the left side /// * expand both sides /// /// to make the expression fit /// /// ```python /// [ /// a, /// b /// ] & [ /// c, /// d /// ] /// ``` ExpandRightThenLeft, } ``` Our current implementation only handles `ExpandRight` and `Default` correctly. This PR adds support for `ExpandRightThenLeft` and `ExpandLeft`. ## Test Plan I added tests that play through all 4 binary expression layouts. --- .../test/fixtures/ruff/expression/binary.py | 135 ++++++++ .../src/expression/expr_bin_op.rs | 274 ++++++++++++----- ...sts__ruff_test__expression__binary_py.snap | 287 ++++++++++++++++++ 3 files changed, 613 insertions(+), 83 deletions(-) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py index c27141d22b..314f83c1ea 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py @@ -52,3 +52,138 @@ if ( ccccccccccc ): pass + + +# Left only breaks +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & aaaaaaaaaaaaaaaaaaaaaaaaaa: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: + ... + +# Right only can break +if aaaaaaaaaaaaaaaaaaaaaaaaaa & [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + +if aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + + +# Left or right can break +if [2222, 333] & [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & [2222, 333]: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & [fffffffffffffffff, gggggggggggggggggggg, hhhhhhhhhhhhhhhhhhhhh, iiiiiiiiiiiiiiii, jjjjjjjjjjjjj]: + ... + +if ( + # comment + [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, + ] +) & [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +]: + pass + + ... + +# Nesting +if (aaaa + b) & [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +]: + ... + +if [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +] & (a + b): + ... + + +if [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +] & ( + # comment + a + + b +): + ... + +if ( + [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, + ] + & + # comment + a + b +): + ... diff --git a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs index f9a8723728..8eb78d3cdf 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs @@ -36,68 +36,124 @@ impl FormatNodeRule for FormatExprBinOp { range: _, } = item; - let should_break_right = self.parentheses == Some(Parentheses::Custom); - - if should_break_right { - let left_group = f.group_id("BinaryLeft"); - - write!( - f, - [ - // Wrap the left in a group and gives it an id. The printer first breaks the - // right side if `right` contains any line break because the printer breaks - // sequences of groups from right to left. - // Indents the left side if the group breaks. - group(&format_args![ - if_group_breaks(&text("(")), - indent_if_group_breaks( - &format_args![ - soft_line_break(), - left.format(), - soft_line_break_or_space(), - op.format(), - space() - ], - left_group - ) - ]) - .with_group_id(Some(left_group)), - // Wrap the right in a group and indents its content but only if the left side breaks - group(&indent_if_group_breaks(&right.format(), left_group)), - // If the left side breaks, insert a hard line break to finish the indent and close the open paren. - if_group_breaks(&format_args![hard_line_break(), text(")")]) - .with_group_id(Some(left_group)) - ] - ) + let layout = if self.parentheses == Some(Parentheses::Custom) { + BinaryLayout::from(item) } else { - let comments = f.context().comments().clone(); - let operator_comments = comments.dangling_comments(item.as_any_node_ref()); - let needs_space = !is_simple_power_expression(item); + BinaryLayout::Default + }; - let before_operator_space = if needs_space { - soft_line_break_or_space() - } else { - soft_line_break() - }; + match layout { + BinaryLayout::Default => { + let comments = f.context().comments().clone(); + let operator_comments = comments.dangling_comments(item.as_any_node_ref()); + let needs_space = !is_simple_power_expression(item); - write!( - f, - [ - left.format(), - before_operator_space, - op.format(), - trailing_comments(operator_comments), - ] - )?; + let before_operator_space = if needs_space { + soft_line_break_or_space() + } else { + soft_line_break() + }; - // Format the operator on its own line if the right side has any leading comments. - if comments.has_leading_comments(right.as_ref()) { - write!(f, [hard_line_break()])?; - } else if needs_space { - write!(f, [space()])?; + write!( + f, + [ + left.format(), + before_operator_space, + op.format(), + trailing_comments(operator_comments), + ] + )?; + + // Format the operator on its own line if the right side has any leading comments. + if comments.has_leading_comments(right.as_ref()) { + write!(f, [hard_line_break()])?; + } else if needs_space { + write!(f, [space()])?; + } + + write!(f, [group(&right.format())]) } - write!(f, [group(&right.format())]) + BinaryLayout::ExpandLeft => { + let left = left.format().memoized(); + let right = right.format().memoized(); + write!( + f, + [best_fitting![ + // Everything on a single line + format_args![left, space(), op.format(), space(), right], + // Break the left over multiple lines, keep the right flat + format_args![ + group(&left).should_expand(true), + space(), + op.format(), + space(), + right + ], + // The content doesn't fit, indent the content and break before the operator. + format_args![ + text("("), + block_indent(&format_args![ + left, + hard_line_break(), + op.format(), + space(), + right + ]), + text(")") + ] + ] + .with_mode(BestFittingMode::AllLines)] + ) + } + + BinaryLayout::ExpandRight => { + let left_group = f.group_id("BinaryLeft"); + + write!( + f, + [ + // Wrap the left in a group and gives it an id. The printer first breaks the + // right side if `right` contains any line break because the printer breaks + // sequences of groups from right to left. + // Indents the left side if the group breaks. + group(&format_args![ + if_group_breaks(&text("(")), + indent_if_group_breaks( + &format_args![ + soft_line_break(), + left.format(), + soft_line_break_or_space(), + op.format(), + space() + ], + left_group + ) + ]) + .with_group_id(Some(left_group)), + // Wrap the right in a group and indents its content but only if the left side breaks + group(&indent_if_group_breaks(&right.format(), left_group)), + // If the left side breaks, insert a hard line break to finish the indent and close the open paren. + if_group_breaks(&format_args![hard_line_break(), text(")")]) + .with_group_id(Some(left_group)) + ] + ) + } + + BinaryLayout::ExpandRightThenLeft => { + // The formatter expands group-sequences from right to left, and expands both if + // there isn't enough space when expanding only one of them. + write!( + f, + [ + group(&left.format()), + space(), + op.format(), + space(), + group(&right.format()) + ] + ) + } } } @@ -179,10 +235,13 @@ impl NeedsParentheses for ExprBinOp { ) -> Parentheses { match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { Parentheses::Optional => { - if should_binary_break_right_side_first(self) { - Parentheses::Custom - } else { + if BinaryLayout::from(self) == BinaryLayout::Default + || comments.has_leading_comments(self.right.as_ref()) + || comments.has_dangling_comments(self) + { Parentheses::Optional + } else { + Parentheses::Custom } } parentheses => parentheses, @@ -190,31 +249,80 @@ impl NeedsParentheses for ExprBinOp { } } -pub(super) fn should_binary_break_right_side_first(expr: &ExprBinOp) -> bool { - use ruff_python_ast::prelude::*; +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum BinaryLayout { + /// Put each operand on their own line if either side expands + Default, - if expr.left.is_bin_op_expr() { - false - } else { - match expr.right.as_ref() { - Expr::Tuple(ExprTuple { - elts: expressions, .. - }) - | Expr::List(ExprList { - elts: expressions, .. - }) - | Expr::Set(ExprSet { - elts: expressions, .. - }) - | Expr::Dict(ExprDict { - values: expressions, - .. - }) => !expressions.is_empty(), - Expr::Call(ExprCall { args, keywords, .. }) => !args.is_empty() && !keywords.is_empty(), - Expr::ListComp(_) | Expr::SetComp(_) | Expr::DictComp(_) | Expr::GeneratorExp(_) => { - true - } - _ => false, + /// Try to expand the left to make it fit. Add parentheses if the left or right don't fit. + /// + ///```python + /// [ + /// a, + /// b + /// ] & c + ///``` + ExpandLeft, + + /// Try to expand the right to make it fix. Add parentheses if the left or right don't fit. + /// + /// ```python + /// a & [ + /// b, + /// c + /// ] + /// ``` + ExpandRight, + + /// Both the left and right side can be expanded. Try in the following order: + /// * expand the right side + /// * expand the left side + /// * expand both sides + /// + /// to make the expression fit + /// + /// ```python + /// [ + /// a, + /// b + /// ] & [ + /// c, + /// d + /// ] + /// ``` + ExpandRightThenLeft, +} + +impl BinaryLayout { + fn from(expr: &ExprBinOp) -> Self { + match (can_break(&expr.left), can_break(&expr.right)) { + (false, false) => Self::Default, + (true, false) => Self::ExpandLeft, + (false, true) => Self::ExpandRight, + (true, true) => Self::ExpandRightThenLeft, } } } + +fn can_break(expr: &Expr) -> bool { + use ruff_python_ast::prelude::*; + + match expr { + Expr::Tuple(ExprTuple { + elts: expressions, .. + }) + | Expr::List(ExprList { + elts: expressions, .. + }) + | Expr::Set(ExprSet { + elts: expressions, .. + }) + | Expr::Dict(ExprDict { + values: expressions, + .. + }) => !expressions.is_empty(), + Expr::Call(ExprCall { args, keywords, .. }) => !(args.is_empty() && keywords.is_empty()), + Expr::ListComp(_) | Expr::SetComp(_) | Expr::DictComp(_) | Expr::GeneratorExp(_) => true, + _ => false, + } +} diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap index 6f7abbf1a6..a26d01a316 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap @@ -58,6 +58,141 @@ if ( ccccccccccc ): pass + + +# Left only breaks +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & aaaaaaaaaaaaaaaaaaaaaaaaaa: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: + ... + +# Right only can break +if aaaaaaaaaaaaaaaaaaaaaaaaaa & [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + +if aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + + +# Left or right can break +if [2222, 333] & [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & [2222, 333]: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & [fffffffffffffffff, gggggggggggggggggggg, hhhhhhhhhhhhhhhhhhhhh, iiiiiiiiiiiiiiii, jjjjjjjjjjjjj]: + ... + +if ( + # comment + [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, + ] +) & [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +]: + pass + + ... + +# Nesting +if (aaaa + b) & [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +]: + ... + +if [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +] & (a + b): + ... + + +if [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +] & ( + # comment + a + + b +): + ... + +if ( + [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, + ] + & + # comment + a + b +): + ... ``` @@ -139,6 +274,158 @@ if ( ccccccccccc ): pass + + +# Left only breaks +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & aaaaaaaaaaaaaaaaaaaaaaaaaa: + ... + +if ( + [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, + ] + & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +): + ... + +# Right only can break +if aaaaaaaaaaaaaaaaaaaaaaaaaa & [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + +if ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + & [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, + ] +): + ... + + +# Left or right can break +if [2222, 333] & [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & [2222, 333]: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +]: + ... + +if ( + # comment + [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, + ] +) & [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +]: + pass + + ... + +# Nesting +if (aaaa + b) & [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +]: + ... + +if [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +] & (a + b): + ... + + +if ( + [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, + ] + & + ( + # comment + a + + b + ) +): + ... + +if ( + [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, + ] + & + # comment + a + + b +): + ... ``` From 1336ca601bfdd5a07e541c36b89c23bfc60b3b0c Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 21 Jun 2023 10:09:47 +0200 Subject: [PATCH 149/447] Format `UnaryExpr` ## Summary This PR adds basic formatting for unary expressions. ## Test Plan I added a new `unary.py` with custom test cases --- .../test/fixtures/ruff/expression/unary.py | 138 ++++++++ .../src/expression/expr_bin_op.rs | 4 + .../src/expression/expr_unary_op.rs | 94 +++++- .../src/expression/parentheses.rs | 6 +- crates/ruff_python_formatter/src/lib.rs | 9 +- ...ttribute_access_on_number_literals_py.snap | 4 +- ...er__tests__black_test__empty_lines_py.snap | 20 +- ...ter__tests__black_test__expression_py.snap | 96 +++--- ...atter__tests__black_test__fmtonoff_py.snap | 24 +- ...atter__tests__black_test__function_py.snap | 12 +- ...ests__black_test__power_op_spacing_py.snap | 27 +- ...sts__ruff_test__expression__binary_py.snap | 2 +- ...ests__ruff_test__expression__unary_py.snap | 302 ++++++++++++++++++ 13 files changed, 623 insertions(+), 115 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__unary_py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py new file mode 100644 index 0000000000..a88e20fb75 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py @@ -0,0 +1,138 @@ +if not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: + pass + +a = True +not a + +b = 10 +-b ++b + +## Leading operand comments + +if not ( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ~( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + +if -( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + + +if +( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + +if ( + not + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ( + ~ + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + +if ( + - + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + + +if ( + + + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + +## Parentheses + +if ( + # unary comment + not + # operand comment + ( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ) +): + pass + +if (not ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +) +): + pass + +if aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & (not ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +) +): + pass + +if ( + not ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ) + & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +): + pass + + +## Trailing operator comments + +if ( + not # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ( + ~ # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + +if ( + - # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ( + + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +## Varia + +if not \ + a: + pass diff --git a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs index 8eb78d3cdf..199e689fb2 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs @@ -323,6 +323,10 @@ fn can_break(expr: &Expr) -> bool { }) => !expressions.is_empty(), Expr::Call(ExprCall { args, keywords, .. }) => !(args.is_empty() && keywords.is_empty()), Expr::ListComp(_) | Expr::SetComp(_) | Expr::DictComp(_) | Expr::GeneratorExp(_) => true, + Expr::UnaryOp(ExprUnaryOp { operand, .. }) => match operand.as_ref() { + Expr::BinOp(_) => true, + _ => can_break(operand.as_ref()), + }, _ => false, } } diff --git a/crates/ruff_python_formatter/src/expression/expr_unary_op.rs b/crates/ruff_python_formatter/src/expression/expr_unary_op.rs index 9f91d13e40..db3dfc06ad 100644 --- a/crates/ruff_python_formatter/src/expression/expr_unary_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_unary_op.rs @@ -1,17 +1,68 @@ -use crate::comments::Comments; +use crate::comments::{trailing_comments, Comments}; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::ExprUnaryOp; +use crate::prelude::*; +use crate::trivia::{SimpleTokenizer, TokenKind}; +use crate::FormatNodeRule; +use ruff_formatter::FormatContext; +use ruff_python_ast::prelude::UnaryOp; +use ruff_text_size::{TextLen, TextRange}; +use rustpython_parser::ast::{ExprUnaryOp, Ranged}; #[derive(Default)] pub struct FormatExprUnaryOp; impl FormatNodeRule for FormatExprUnaryOp { fn fmt_fields(&self, item: &ExprUnaryOp, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let ExprUnaryOp { + range: _, + op, + operand, + } = item; + + let operator = match op { + UnaryOp::Invert => "~", + UnaryOp::Not => "not", + UnaryOp::UAdd => "+", + UnaryOp::USub => "-", + }; + + text(operator).fmt(f)?; + + let comments = f.context().comments().clone(); + + // Split off the comments that follow after the operator and format them as trailing comments. + // ```python + // (not # comment + // a) + // ``` + let leading_operand_comments = comments.leading_comments(operand.as_ref()); + let trailing_operator_comments_end = + leading_operand_comments.partition_point(|p| p.position().is_end_of_line()); + let (trailing_operator_comments, leading_operand_comments) = + leading_operand_comments.split_at(trailing_operator_comments_end); + + if !trailing_operator_comments.is_empty() { + trailing_comments(trailing_operator_comments).fmt(f)?; + } + + // Insert a line break if the operand has comments but itself is not parenthesized. + // ```python + // if ( + // not + // # comment + // a) + // ``` + if !leading_operand_comments.is_empty() + && !is_operand_parenthesized(item, f.context().source_code().as_str()) + { + hard_line_break().fmt(f)?; + } else if op.is_not() { + space().fmt(f)?; + } + + operand.format().fmt(f) } } @@ -22,6 +73,37 @@ impl NeedsParentheses for ExprUnaryOp { source: &str, comments: &Comments, ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + Parentheses::Optional => { + // We preserve the parentheses of the operand. It should not be necessary to break this expression. + if is_operand_parenthesized(self, source) { + Parentheses::Never + } else { + Parentheses::Optional + } + } + parentheses => parentheses, + } + } +} + +fn is_operand_parenthesized(unary: &ExprUnaryOp, source: &str) -> bool { + let operator_len = match unary.op { + UnaryOp::Invert => '~'.text_len(), + UnaryOp::Not => "not".text_len(), + UnaryOp::UAdd => '+'.text_len(), + UnaryOp::USub => '-'.text_len(), + }; + + let trivia_range = TextRange::new(unary.range.start() + operator_len, unary.operand.start()); + + if let Some(token) = SimpleTokenizer::new(source, trivia_range) + .skip_trivia() + .next() + { + debug_assert_eq!(token.kind(), TokenKind::LParen); + true + } else { + false } } diff --git a/crates/ruff_python_formatter/src/expression/parentheses.rs b/crates/ruff_python_formatter/src/expression/parentheses.rs index 58d083c8c6..7d57fe9df3 100644 --- a/crates/ruff_python_formatter/src/expression/parentheses.rs +++ b/crates/ruff_python_formatter/src/expression/parentheses.rs @@ -42,7 +42,7 @@ pub(super) fn default_expression_needs_parentheses( } /// Configures if the expression should be parenthesized. -#[derive(Copy, Clone, Debug, Default)] +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub enum Parenthesize { /// Parenthesize the expression if it has parenthesis in the source. #[default] @@ -56,11 +56,11 @@ pub enum Parenthesize { } impl Parenthesize { - const fn is_if_breaks(self) -> bool { + pub(crate) const fn is_if_breaks(self) -> bool { matches!(self, Parenthesize::IfBreaks) } - const fn is_preserve(self) -> bool { + pub(crate) const fn is_preserve(self) -> bool { matches!(self, Parenthesize::Preserve) } } diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index b915ac45eb..3f09fd51d6 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -411,11 +411,10 @@ Formatted twice: #[test] fn quick_test() { let src = r#" - -def foo( - b=3 - + 2 # comment -): +if [ + aaaaaa, + BBBB,ccccccccc,ddddddd,eeeeeeeeee,ffffff +] & bbbbbbbbbbbbbbbbbbddddddddddddddddddddddddddddbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: ... "#; // Tokenize once diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap index 81c681bc72..c736be6242 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap @@ -67,7 +67,7 @@ y = 100(no) +x = NOT_IMPLEMENTED_call() +x = 0O777 .NOT_IMPLEMENTED_attr +x = NOT_IMPLEMENTED_call() -+x = NOT_YET_IMPLEMENTED_ExprUnaryOp ++x = -100.0000J -if (10).real: +if 10 .NOT_IMPLEMENTED_attr: @@ -97,7 +97,7 @@ x = NOT_IMPLEMENTED_call() x = NOT_IMPLEMENTED_call() x = 0O777 .NOT_IMPLEMENTED_attr x = NOT_IMPLEMENTED_call() -x = NOT_YET_IMPLEMENTED_ExprUnaryOp +x = -100.0000J if 10 .NOT_IMPLEMENTED_attr: ... diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap index 5446adf8cc..03794aac66 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap @@ -105,7 +105,7 @@ def g(): ```diff --- Black +++ Ruff -@@ -1,89 +1,70 @@ +@@ -1,47 +1,35 @@ -"""Docstring.""" +"NOT_YET_IMPLEMENTED_STRING" @@ -137,11 +137,10 @@ def g(): + NOT_YET_IMPLEMENTED_StmtAssert - prev = leaf.prev_sibling -- if not prev: ++ prev = leaf.NOT_IMPLEMENTED_attr + if not prev: - prevp = preceding_leaf(p) - if not prevp or prevp.type in OPENING_BRACKETS: -+ prev = leaf.NOT_IMPLEMENTED_attr -+ if NOT_YET_IMPLEMENTED_ExprUnaryOp: + prevp = NOT_IMPLEMENTED_call() + if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: return NO @@ -171,11 +170,11 @@ def g(): return NO - ############################################################################### +@@ -49,41 +37,34 @@ # SECTION BECAUSE SECTIONS ############################################################################### -- +- def g(): - NO = "" - SPACE = " " @@ -205,10 +204,9 @@ def g(): + NOT_YET_IMPLEMENTED_StmtAssert - prev = leaf.prev_sibling -- if not prev: -- prevp = preceding_leaf(p) + prev = leaf.NOT_IMPLEMENTED_attr -+ if NOT_YET_IMPLEMENTED_ExprUnaryOp: + if not prev: +- prevp = preceding_leaf(p) + prevp = NOT_IMPLEMENTED_call() - if not prevp or prevp.type in OPENING_BRACKETS: @@ -254,7 +252,7 @@ def f(): NOT_YET_IMPLEMENTED_StmtAssert prev = leaf.NOT_IMPLEMENTED_attr - if NOT_YET_IMPLEMENTED_ExprUnaryOp: + if not prev: prevp = NOT_IMPLEMENTED_call() if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: return NO @@ -292,7 +290,7 @@ def g(): NOT_YET_IMPLEMENTED_StmtAssert prev = leaf.NOT_IMPLEMENTED_attr - if NOT_YET_IMPLEMENTED_ExprUnaryOp: + if not prev: prevp = NOT_IMPLEMENTED_call() if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap index bb84670f91..a96793d3d2 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap @@ -276,7 +276,7 @@ last_call() Name None True -@@ -7,226 +8,225 @@ +@@ -7,18 +8,18 @@ 1 1.0 1j @@ -307,12 +307,10 @@ last_call() v1 << 2 1 >> v2 1 % finished - 1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8 - ((1 + v2) - (v3 * 4)) ^ (((5**v6) / 7) // 8) --not great --~great --+value ---1 +@@ -28,205 +29,204 @@ + ~great + +value + -1 -~int and not v1 ^ 123 + v2 | True -(~int) and (not ((v1 ^ (123 + v2)) | True)) -+(really ** -(confusing ** ~(operator**-precedence))) @@ -339,13 +337,9 @@ last_call() -) -{"2.7": dead, "3.7": (long_live or die_hard)} -{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} -+NOT_YET_IMPLEMENTED_ExprUnaryOp -+NOT_YET_IMPLEMENTED_ExprUnaryOp -+NOT_YET_IMPLEMENTED_ExprUnaryOp -+NOT_YET_IMPLEMENTED_ExprUnaryOp +NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 +NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_YET_IMPLEMENTED_ExprUnaryOp +++really ** -confusing ** ~operator**-precedence +NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 +lambda x: True +lambda x: True @@ -402,11 +396,6 @@ last_call() - *a, 4, 5, --] --[ -- 4, -- *a, -- 5, + 6, + 7, + 8, @@ -415,6 +404,11 @@ last_call() + (NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2), + (NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2), ] +-[ +- 4, +- *a, +- 5, +-] +[1, 2, 3] +[NOT_YET_IMPLEMENTED_ExprStarred] +[NOT_YET_IMPLEMENTED_ExprStarred] @@ -644,17 +638,6 @@ last_call() -g = 1, *"ten" -what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set( - vars_to_remove --) --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( -- models.Customer.account_id == account_id, models.Customer.email == email_address -- ) -- .order_by(models.Customer.id.asc()) -- .all() +e = NOT_IMPLEMENTED_call() +f = 1, NOT_YET_IMPLEMENTED_ExprStarred +g = 1, NOT_YET_IMPLEMENTED_ExprStarred @@ -662,6 +645,20 @@ last_call() + (coord_names + NOT_IMPLEMENTED_call()) + + NOT_IMPLEMENTED_call() ) +-what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( +- vars_to_remove ++what_is_up_with_those_new_coord_names = ( ++ (coord_names | NOT_IMPLEMENTED_call()) ++ - NOT_IMPLEMENTED_call() + ) +-result = ( +- session.query(models.Customer.id) +- .filter( +- models.Customer.account_id == account_id, models.Customer.email == email_address +- ) +- .order_by(models.Customer.id.asc()) +- .all() +-) -result = ( - session.query(models.Customer.id) - .filter( @@ -671,10 +668,7 @@ last_call() - models.Customer.id.asc(), - ) - .all() -+what_is_up_with_those_new_coord_names = ( -+ (coord_names | NOT_IMPLEMENTED_call()) -+ - NOT_IMPLEMENTED_call() - ) +-) -Ø = set() -authors.łukasz.say_thanks() +result = NOT_IMPLEMENTED_call() @@ -714,6 +708,14 @@ last_call() -for (x,) in (1,), (2,), (3,): - ... -for y in (): +- ... +-for z in (i for i in (1, 2, 3)): +- ... +-for i in call(): +- ... +-for j in 1 + (2 + 3): +- ... +-while this and that: +NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call() @@ -727,14 +729,6 @@ last_call() +NOT_YET_IMPLEMENTED_StmtFor +while NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: ... --for z in (i for i in (1, 2, 3)): -- ... --for i in call(): -- ... --for j in 1 + (2 + 3): -- ... --while this and that: -- ... -for ( - addr_family, - addr_type, @@ -779,7 +773,7 @@ last_call() if ( - ~aaaa.a + aaaa.b - aaaa.c * aaaa.d / aaaa.e - | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l**aaaa.m // aaaa.n -+ NOT_YET_IMPLEMENTED_ExprUnaryOp ++ ~aaaa.NOT_IMPLEMENTED_attr + + aaaa.NOT_IMPLEMENTED_attr + - aaaa.NOT_IMPLEMENTED_attr * aaaa.NOT_IMPLEMENTED_attr / aaaa.NOT_IMPLEMENTED_attr + | aaaa.NOT_IMPLEMENTED_attr @@ -793,7 +787,7 @@ last_call() - ~aaaaaaaa.a + aaaaaaaa.b - aaaaaaaa.c @ aaaaaaaa.d / aaaaaaaa.e - | aaaaaaaa.f & aaaaaaaa.g % aaaaaaaa.h - ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n -+ NOT_YET_IMPLEMENTED_ExprUnaryOp ++ ~aaaaaaaa.NOT_IMPLEMENTED_attr + + aaaaaaaa.NOT_IMPLEMENTED_attr + - aaaaaaaa.NOT_IMPLEMENTED_attr + @ aaaaaaaa.NOT_IMPLEMENTED_attr @@ -815,7 +809,7 @@ last_call() - ^ aaaaaaaaaaaaaaaa.i - << aaaaaaaaaaaaaaaa.k - >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n -+ NOT_YET_IMPLEMENTED_ExprUnaryOp ++ ~aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr + + aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr + - aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr + * aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr @@ -874,13 +868,13 @@ v1 << 2 1 % finished 1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8 ((1 + v2) - (v3 * 4)) ^ (((5**v6) / 7) // 8) -NOT_YET_IMPLEMENTED_ExprUnaryOp -NOT_YET_IMPLEMENTED_ExprUnaryOp -NOT_YET_IMPLEMENTED_ExprUnaryOp -NOT_YET_IMPLEMENTED_ExprUnaryOp +not great +~great ++value +-1 NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_YET_IMPLEMENTED_ExprUnaryOp ++really ** -confusing ** ~operator**-precedence NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 lambda x: True lambda x: True @@ -1146,7 +1140,7 @@ if ( ): return True if ( - NOT_YET_IMPLEMENTED_ExprUnaryOp + ~aaaa.NOT_IMPLEMENTED_attr + aaaa.NOT_IMPLEMENTED_attr - aaaa.NOT_IMPLEMENTED_attr * aaaa.NOT_IMPLEMENTED_attr / aaaa.NOT_IMPLEMENTED_attr | aaaa.NOT_IMPLEMENTED_attr @@ -1157,7 +1151,7 @@ if ( ): return True if ( - NOT_YET_IMPLEMENTED_ExprUnaryOp + ~aaaaaaaa.NOT_IMPLEMENTED_attr + aaaaaaaa.NOT_IMPLEMENTED_attr - aaaaaaaa.NOT_IMPLEMENTED_attr @ aaaaaaaa.NOT_IMPLEMENTED_attr @@ -1172,7 +1166,7 @@ if ( ): return True if ( - NOT_YET_IMPLEMENTED_ExprUnaryOp + ~aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr + aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr * aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap index 2dcad260b6..dc725ba267 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap @@ -286,7 +286,7 @@ d={'a':1, + c=[], + d={}, + e=True, -+ f=NOT_YET_IMPLEMENTED_ExprUnaryOp, ++ f=-1, + g=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + h="NOT_YET_IMPLEMENTED_STRING", + i="NOT_YET_IMPLEMENTED_STRING", @@ -296,15 +296,13 @@ d={'a':1, def spaces_types( -@@ -50,77 +69,62 @@ - c: list = [], +@@ -51,76 +70,61 @@ d: dict = {}, e: bool = True, -- f: int = -1, + f: int = -1, - g: int = 1 if False else 2, - h: str = "", - i: str = r"", -+ f: int = NOT_YET_IMPLEMENTED_ExprUnaryOp, + g: int = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + h: str = "NOT_YET_IMPLEMENTED_STRING", + i: str = "NOT_YET_IMPLEMENTED_STRING", @@ -425,13 +423,11 @@ d={'a':1, - implicit_default=True, - ) - ) -+ NOT_IMPLEMENTED_call() - # fmt: off +- # fmt: off - a = ( - unnecessary_bracket() - ) -+ a = NOT_IMPLEMENTED_call() - # fmt: on +- # fmt: on - _type_comment_re = re.compile( - r""" - ^ @@ -452,9 +448,11 @@ d={'a':1, - ) - $ - """, -- # fmt: off ++ NOT_IMPLEMENTED_call() + # fmt: off - re.MULTILINE|re.VERBOSE -- # fmt: on ++ a = NOT_IMPLEMENTED_call() + # fmt: on - ) + _type_comment_re = NOT_IMPLEMENTED_call() @@ -563,7 +561,7 @@ def spaces( c=[], d={}, e=True, - f=NOT_YET_IMPLEMENTED_ExprUnaryOp, + f=-1, g=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, h="NOT_YET_IMPLEMENTED_STRING", i="NOT_YET_IMPLEMENTED_STRING", @@ -578,7 +576,7 @@ def spaces_types( c: list = [], d: dict = {}, e: bool = True, - f: int = NOT_YET_IMPLEMENTED_ExprUnaryOp, + f: int = -1, g: int = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, h: str = "NOT_YET_IMPLEMENTED_STRING", i: str = "NOT_YET_IMPLEMENTED_STRING", diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap index a82537bcef..a601ceda63 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap @@ -179,7 +179,7 @@ def __await__(): return (yield) + c=[], + d={}, + e=True, -+ f=NOT_YET_IMPLEMENTED_ExprUnaryOp, ++ f=-1, + g=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + h="NOT_YET_IMPLEMENTED_STRING", + i="NOT_YET_IMPLEMENTED_STRING", @@ -189,15 +189,13 @@ def __await__(): return (yield) def spaces_types( -@@ -55,71 +61,27 @@ - c: list = [], +@@ -56,70 +62,26 @@ d: dict = {}, e: bool = True, -- f: int = -1, + f: int = -1, - g: int = 1 if False else 2, - h: str = "", - i: str = r"", -+ f: int = NOT_YET_IMPLEMENTED_ExprUnaryOp, + g: int = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + h: str = "NOT_YET_IMPLEMENTED_STRING", + i: str = "NOT_YET_IMPLEMENTED_STRING", @@ -341,7 +339,7 @@ def spaces( c=[], d={}, e=True, - f=NOT_YET_IMPLEMENTED_ExprUnaryOp, + f=-1, g=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, h="NOT_YET_IMPLEMENTED_STRING", i="NOT_YET_IMPLEMENTED_STRING", @@ -356,7 +354,7 @@ def spaces_types( c: list = [], d: dict = {}, e: bool = True, - f: int = NOT_YET_IMPLEMENTED_ExprUnaryOp, + f: int = -1, g: int = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, h: str = "NOT_YET_IMPLEMENTED_STRING", i: str = "NOT_YET_IMPLEMENTED_STRING", diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap index 6358e8d7e3..e3b6cde88b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap @@ -76,13 +76,13 @@ return np.divide( ```diff --- Black +++ Ruff -@@ -11,53 +11,46 @@ - {**a, **b, **c} +@@ -12,52 +12,45 @@ --a = 5**~4 + a = 5**~4 -b = 5 ** f() --c = -(5**2) ++b = 5 ** NOT_IMPLEMENTED_call() + c = -(5**2) -d = 5 ** f["hi"] -e = lazy(lambda **kwargs: 5) -f = f() ** 5 @@ -92,9 +92,6 @@ return np.divide( -j = super().name ** 5 -k = [(2**idx, value) for idx, value in pairs] -l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) -+a = 5**NOT_YET_IMPLEMENTED_ExprUnaryOp -+b = 5 ** NOT_IMPLEMENTED_call() -+c = NOT_YET_IMPLEMENTED_ExprUnaryOp +d = 5 ** NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] +e = NOT_IMPLEMENTED_call() +f = NOT_IMPLEMENTED_call() ** 5 @@ -115,9 +112,10 @@ return np.divide( +q = [i for i in []] r = x**y --a = 5.0**~4.0 + a = 5.0**~4.0 -b = 5.0 ** f() --c = -(5.0**2.0) ++b = 5.0 ** NOT_IMPLEMENTED_call() + c = -(5.0**2.0) -d = 5.0 ** f["hi"] -e = lazy(lambda **kwargs: 5) -f = f() ** 5.0 @@ -127,9 +125,6 @@ return np.divide( -j = super().name ** 5.0 -k = [(2.0**idx, value) for idx, value in pairs] -l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) -+a = 5.0**NOT_YET_IMPLEMENTED_ExprUnaryOp -+b = 5.0 ** NOT_IMPLEMENTED_call() -+c = NOT_YET_IMPLEMENTED_ExprUnaryOp +d = 5.0 ** NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] +e = NOT_IMPLEMENTED_call() +f = NOT_IMPLEMENTED_call() ** 5.0 @@ -183,9 +178,9 @@ def function_dont_replace_spaces(): {**a, **b, **c} -a = 5**NOT_YET_IMPLEMENTED_ExprUnaryOp +a = 5**~4 b = 5 ** NOT_IMPLEMENTED_call() -c = NOT_YET_IMPLEMENTED_ExprUnaryOp +c = -(5**2) d = 5 ** NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] e = NOT_IMPLEMENTED_call() f = NOT_IMPLEMENTED_call() ** 5 @@ -202,9 +197,9 @@ p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_ q = [i for i in []] r = x**y -a = 5.0**NOT_YET_IMPLEMENTED_ExprUnaryOp +a = 5.0**~4.0 b = 5.0 ** NOT_IMPLEMENTED_call() -c = NOT_YET_IMPLEMENTED_ExprUnaryOp +c = -(5.0**2.0) d = 5.0 ** NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] e = NOT_IMPLEMENTED_call() f = NOT_IMPLEMENTED_call() ** 5.0 diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap index a26d01a316..a610419d8b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap @@ -251,7 +251,7 @@ aaaaaaaaaaaaaa + NOT_YET_IMPLEMENTED_ExprSetComp # But only for expressions that have a statement parent. -NOT_YET_IMPLEMENTED_ExprUnaryOp +not (aaaaaaaaaaaaaa + NOT_YET_IMPLEMENTED_ExprSetComp) [NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right] diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__unary_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__unary_py.snap new file mode 100644 index 0000000000..48e8852375 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__unary_py.snap @@ -0,0 +1,302 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +--- +## Input +```py +if not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: + pass + +a = True +not a + +b = 10 +-b ++b + +## Leading operand comments + +if not ( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ~( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + +if -( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + + +if +( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + +if ( + not + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ( + ~ + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + +if ( + - + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + + +if ( + + + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + +## Parentheses + +if ( + # unary comment + not + # operand comment + ( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ) +): + pass + +if (not ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +) +): + pass + +if aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & (not ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +) +): + pass + +if ( + not ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ) + & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +): + pass + + +## Trailing operator comments + +if ( + not # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ( + ~ # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + +if ( + - # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ( + + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +## Varia + +if not \ + a: + pass +``` + + + +## Output +```py +if ( + not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + +a = True +not a + +b = 10 +-b ++b + +## Leading operand comments + +if not ( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ~( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + +if -( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if +( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + +if ( + not + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ( + ~ + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + +if ( + - + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ( + + + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + +## Parentheses + +if ( + # unary comment + not ( + # operand comment + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ) +): + pass + +if not ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + +if aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & ( + not ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ) +): + pass + +if ( + not ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ) + & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +): + pass + + +## Trailing operator comments + +if ( + not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # comment + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ( + ~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # comment + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + +if ( + -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # comment + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ( + +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # comment + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +## Varia + +if not a: + pass +``` + + From db301c14bd48aa1f6b56c7090ffd0b856ef117eb Mon Sep 17 00:00:00 2001 From: konstin Date: Wed, 21 Jun 2023 11:04:56 +0200 Subject: [PATCH 150/447] Consistently name comment own line/end-of-line `line_position()` (#5215) ## Summary Previously, `DecoratedComment` used `text_position()` and `SourceComment` used `position()`. This PR unifies this to `line_position` everywhere. ## Test Plan This is a rename refactoring. --- .../src/comments/debug.rs | 10 +++---- .../src/comments/format.rs | 4 +-- .../ruff_python_formatter/src/comments/mod.rs | 22 +++++++-------- .../src/comments/placement.rs | 28 +++++++++---------- .../src/comments/visitor.rs | 26 ++++++++--------- .../src/expression/expr_list.rs | 9 +++--- .../src/statement/stmt_function_def.rs | 2 +- .../src/statement/stmt_if.rs | 2 +- .../src/statement/stmt_while.rs | 2 +- 9 files changed, 53 insertions(+), 52 deletions(-) diff --git a/crates/ruff_python_formatter/src/comments/debug.rs b/crates/ruff_python_formatter/src/comments/debug.rs index 6bb123db7d..a8adfda557 100644 --- a/crates/ruff_python_formatter/src/comments/debug.rs +++ b/crates/ruff_python_formatter/src/comments/debug.rs @@ -26,7 +26,7 @@ impl Debug for DebugComment<'_> { strut .field("text", &self.comment.slice.text(self.source_code)) - .field("position", &self.comment.position); + .field("position", &self.comment.line_position); #[cfg(debug_assertions)] strut.field("formatted", &self.comment.formatted.get()); @@ -177,7 +177,7 @@ impl Debug for DebugNodeCommentSlice<'_> { #[cfg(test)] mod tests { use crate::comments::map::MultiMap; - use crate::comments::{CommentTextPosition, Comments, CommentsMap, SourceComment}; + use crate::comments::{CommentLinePosition, Comments, CommentsMap, SourceComment}; use insta::assert_debug_snapshot; use ruff_formatter::SourceCode; use ruff_python_ast::node::AnyNode; @@ -208,7 +208,7 @@ break; continue_statement.as_ref().into(), SourceComment::new( source_code.slice(TextRange::at(TextSize::new(0), TextSize::new(17))), - CommentTextPosition::OwnLine, + CommentLinePosition::OwnLine, ), ); @@ -216,7 +216,7 @@ break; continue_statement.as_ref().into(), SourceComment::new( source_code.slice(TextRange::at(TextSize::new(28), TextSize::new(10))), - CommentTextPosition::EndOfLine, + CommentLinePosition::EndOfLine, ), ); @@ -224,7 +224,7 @@ break; break_statement.as_ref().into(), SourceComment::new( source_code.slice(TextRange::at(TextSize::new(39), TextSize::new(15))), - CommentTextPosition::OwnLine, + CommentLinePosition::OwnLine, ), ); diff --git a/crates/ruff_python_formatter/src/comments/format.rs b/crates/ruff_python_formatter/src/comments/format.rs index 70eb87f679..86cc2efbaa 100644 --- a/crates/ruff_python_formatter/src/comments/format.rs +++ b/crates/ruff_python_formatter/src/comments/format.rs @@ -136,7 +136,7 @@ impl Format> for FormatTrailingComments<'_> { { let slice = trailing.slice(); - has_trailing_own_line_comment |= trailing.position().is_own_line(); + has_trailing_own_line_comment |= trailing.line_position().is_own_line(); if has_trailing_own_line_comment { let lines_before_comment = lines_before(slice.start(), f.context().contents()); @@ -208,7 +208,7 @@ impl Format> for FormatDanglingComments<'_> { .iter() .filter(|comment| comment.is_unformatted()) { - if first && comment.position().is_end_of_line() { + if first && comment.line_position().is_end_of_line() { write!(f, [space(), space()])?; } diff --git a/crates/ruff_python_formatter/src/comments/mod.rs b/crates/ruff_python_formatter/src/comments/mod.rs index 837ce8266e..7d3aab930f 100644 --- a/crates/ruff_python_formatter/src/comments/mod.rs +++ b/crates/ruff_python_formatter/src/comments/mod.rs @@ -117,14 +117,14 @@ pub(crate) struct SourceComment { slice: SourceCodeSlice, /// Whether the comment has been formatted or not. formatted: Cell, - position: CommentTextPosition, + line_position: CommentLinePosition, } impl SourceComment { - fn new(slice: SourceCodeSlice, position: CommentTextPosition) -> Self { + fn new(slice: SourceCodeSlice, position: CommentLinePosition) -> Self { Self { slice, - position, + line_position: position, formatted: Cell::new(false), } } @@ -135,8 +135,8 @@ impl SourceComment { &self.slice } - pub(crate) const fn position(&self) -> CommentTextPosition { - self.position + pub(crate) const fn line_position(&self) -> CommentLinePosition { + self.line_position } /// Marks the comment as formatted @@ -163,7 +163,7 @@ impl SourceComment { /// The position of a comment in the source text. #[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub(crate) enum CommentTextPosition { +pub(crate) enum CommentLinePosition { /// A comment that is on the same line as the preceding token and is separated by at least one line break from the following token. /// /// # Examples @@ -176,7 +176,7 @@ pub(crate) enum CommentTextPosition { /// ``` /// /// `# comment` is an end of line comments because it is separated by at least one line break from the following token `b`. - /// Comments that not only end, but also start on a new line are [`OwnLine`](CommentTextPosition::OwnLine) comments. + /// Comments that not only end, but also start on a new line are [`OwnLine`](CommentLinePosition::OwnLine) comments. EndOfLine, /// A Comment that is separated by at least one line break from the preceding token. @@ -193,13 +193,13 @@ pub(crate) enum CommentTextPosition { OwnLine, } -impl CommentTextPosition { +impl CommentLinePosition { pub(crate) const fn is_own_line(self) -> bool { - matches!(self, CommentTextPosition::OwnLine) + matches!(self, CommentLinePosition::OwnLine) } pub(crate) const fn is_end_of_line(self) -> bool { - matches!(self, CommentTextPosition::EndOfLine) + matches!(self, CommentLinePosition::EndOfLine) } } @@ -335,7 +335,7 @@ impl<'a> Comments<'a> { { self.trailing_comments(node) .iter() - .any(|comment| comment.position().is_own_line()) + .any(|comment| comment.line_position().is_own_line()) } /// Returns an iterator over the [leading](self#leading-comments) and [trailing comments](self#trailing-comments) of `node`. diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index aea1d1d3e0..a467263e09 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -9,7 +9,7 @@ use ruff_python_ast::whitespace; use ruff_python_whitespace::{PythonWhitespace, UniversalNewlines}; use crate::comments::visitor::{CommentPlacement, DecoratedComment}; -use crate::comments::CommentTextPosition; +use crate::comments::CommentLinePosition; use crate::trivia::{SimpleTokenizer, Token, TokenKind}; /// Implements the custom comment placement logic. @@ -49,7 +49,7 @@ fn handle_match_comment<'a>( locator: &Locator, ) -> CommentPlacement<'a> { // Must be an own line comment after the last statement in a match case - if comment.text_position().is_end_of_line() || comment.following_node().is_some() { + if comment.line_position().is_end_of_line() || comment.following_node().is_some() { return CommentPlacement::Default(comment); } @@ -147,7 +147,7 @@ fn handle_in_between_except_handlers_or_except_handler_and_else_or_finally_comme comment: DecoratedComment<'a>, locator: &Locator, ) -> CommentPlacement<'a> { - if comment.text_position().is_end_of_line() || comment.following_node().is_none() { + if comment.line_position().is_end_of_line() || comment.following_node().is_none() { return CommentPlacement::Default(comment); } @@ -201,7 +201,7 @@ fn handle_in_between_bodies_own_line_comment<'a>( comment: DecoratedComment<'a>, locator: &Locator, ) -> CommentPlacement<'a> { - if !comment.text_position().is_own_line() { + if !comment.line_position().is_own_line() { return CommentPlacement::Default(comment); } @@ -310,7 +310,7 @@ fn handle_in_between_bodies_end_of_line_comment<'a>( comment: DecoratedComment<'a>, locator: &Locator, ) -> CommentPlacement<'a> { - if !comment.text_position().is_end_of_line() { + if !comment.line_position().is_end_of_line() { return CommentPlacement::Default(comment); } @@ -393,7 +393,7 @@ fn handle_trailing_body_comment<'a>( comment: DecoratedComment<'a>, locator: &Locator, ) -> CommentPlacement<'a> { - if comment.text_position().is_end_of_line() { + if comment.line_position().is_end_of_line() { return CommentPlacement::Default(comment); } @@ -485,7 +485,7 @@ fn handle_trailing_body_comment<'a>( /// ``` fn handle_trailing_end_of_line_body_comment(comment: DecoratedComment<'_>) -> CommentPlacement<'_> { // Must be an end of line comment - if comment.text_position().is_own_line() { + if comment.line_position().is_own_line() { return CommentPlacement::Default(comment); } @@ -526,7 +526,7 @@ fn handle_trailing_end_of_line_condition_comment<'a>( use ruff_python_ast::prelude::*; // Must be an end of line comment - if comment.text_position().is_own_line() { + if comment.line_position().is_own_line() { return CommentPlacement::Default(comment); } @@ -640,15 +640,15 @@ fn handle_positional_only_arguments_separator_comment<'a>( if let Some(slash_offset) = find_pos_only_slash_offset(trivia_range, locator) { let comment_start = comment.slice().range().start(); - let is_slash_comment = match comment.text_position() { - CommentTextPosition::EndOfLine => { + let is_slash_comment = match comment.line_position() { + CommentLinePosition::EndOfLine => { let preceding_end_line = locator.line_end(last_argument_or_default.end()); let slash_comments_start = preceding_end_line.min(slash_offset); comment_start >= slash_comments_start && locator.line_end(slash_offset) > comment_start } - CommentTextPosition::OwnLine => comment_start < slash_offset, + CommentLinePosition::OwnLine => comment_start < slash_offset, }; if is_slash_comment { @@ -711,7 +711,7 @@ fn handle_trailing_binary_expression_left_or_operator_comment<'a>( // ) // ``` CommentPlacement::trailing(AnyNodeRef::from(binary_expression.left.as_ref()), comment) - } else if comment.text_position().is_end_of_line() { + } else if comment.line_position().is_end_of_line() { // Is the operator on its own line. if locator.contains_line_break(TextRange::new( binary_expression.left.end(), @@ -800,7 +800,7 @@ fn handle_module_level_own_line_comment_before_class_or_function_comment<'a>( locator: &Locator, ) -> CommentPlacement<'a> { // Only applies for own line comments on the module level... - if !comment.text_position().is_own_line() || !comment.enclosing_node().is_module() { + if !comment.line_position().is_own_line() || !comment.enclosing_node().is_module() { return CommentPlacement::Default(comment); } @@ -877,7 +877,7 @@ fn handle_leading_function_with_decorators_comment(comment: DecoratedComment) -> .following_node() .map_or(false, |node| node.is_arguments()); - if comment.text_position().is_own_line() && is_preceding_decorator && is_following_arguments { + if comment.line_position().is_own_line() && is_preceding_decorator && is_following_arguments { CommentPlacement::dangling(comment.enclosing_node(), comment) } else { CommentPlacement::Default(comment) diff --git a/crates/ruff_python_formatter/src/comments/visitor.rs b/crates/ruff_python_formatter/src/comments/visitor.rs index 55ce3f42fc..6102240a40 100644 --- a/crates/ruff_python_formatter/src/comments/visitor.rs +++ b/crates/ruff_python_formatter/src/comments/visitor.rs @@ -1,6 +1,6 @@ use crate::comments::node_key::NodeRefEqualityKey; use crate::comments::placement::place_comment; -use crate::comments::{CommentTextPosition, CommentsMap, SourceComment}; +use crate::comments::{CommentLinePosition, CommentsMap, SourceComment}; use ruff_formatter::{SourceCode, SourceCodeSlice}; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::prelude::*; @@ -66,7 +66,7 @@ impl<'a> CommentsVisitor<'a> { preceding: self.preceding_node, following: Some(node), parent: self.parents.iter().rev().nth(1).copied(), - text_position: text_position(*comment_range, self.source_code), + line_position: text_position(*comment_range, self.source_code), slice: self.source_code.slice(*comment_range), }; @@ -125,7 +125,7 @@ impl<'a> CommentsVisitor<'a> { preceding: self.preceding_node, parent: self.parents.last().copied(), following: None, - text_position: text_position(*comment_range, self.source_code), + line_position: text_position(*comment_range, self.source_code), slice: self.source_code.slice(*comment_range), }; @@ -280,7 +280,7 @@ impl<'ast> PreorderVisitor<'ast> for CommentsVisitor<'ast> { } } -fn text_position(comment_range: TextRange, source_code: SourceCode) -> CommentTextPosition { +fn text_position(comment_range: TextRange, source_code: SourceCode) -> CommentLinePosition { let before = &source_code.as_str()[TextRange::up_to(comment_range.start())]; for c in before.chars().rev() { @@ -289,11 +289,11 @@ fn text_position(comment_range: TextRange, source_code: SourceCode) -> CommentTe break; } c if is_python_whitespace(c) => continue, - _ => return CommentTextPosition::EndOfLine, + _ => return CommentLinePosition::EndOfLine, } } - CommentTextPosition::OwnLine + CommentLinePosition::OwnLine } /// A comment decorated with additional information about its surrounding context in the source document. @@ -305,7 +305,7 @@ pub(super) struct DecoratedComment<'a> { preceding: Option>, following: Option>, parent: Option>, - text_position: CommentTextPosition, + line_position: CommentLinePosition, slice: SourceCodeSlice, } @@ -443,14 +443,14 @@ impl<'a> DecoratedComment<'a> { } /// The position of the comment in the text. - pub(super) fn text_position(&self) -> CommentTextPosition { - self.text_position + pub(super) fn line_position(&self) -> CommentLinePosition { + self.line_position } } impl From> for SourceComment { fn from(decorated: DecoratedComment) -> Self { - Self::new(decorated.slice, decorated.text_position) + Self::new(decorated.slice, decorated.line_position) } } @@ -659,8 +659,8 @@ impl<'a> CommentsBuilder<'a> { self.push_dangling_comment(node, comment); } CommentPlacement::Default(comment) => { - match comment.text_position() { - CommentTextPosition::EndOfLine => { + match comment.line_position() { + CommentLinePosition::EndOfLine => { match (comment.preceding_node(), comment.following_node()) { (Some(preceding), Some(_)) => { // Attach comments with both preceding and following node to the preceding @@ -682,7 +682,7 @@ impl<'a> CommentsBuilder<'a> { } } } - CommentTextPosition::OwnLine => { + CommentLinePosition::OwnLine => { match (comment.preceding_node(), comment.following_node()) { // Following always wins for a leading comment // ```python diff --git a/crates/ruff_python_formatter/src/expression/expr_list.rs b/crates/ruff_python_formatter/src/expression/expr_list.rs index 2e6c055315..8e3badce51 100644 --- a/crates/ruff_python_formatter/src/expression/expr_list.rs +++ b/crates/ruff_python_formatter/src/expression/expr_list.rs @@ -1,4 +1,4 @@ -use crate::comments::{dangling_comments, CommentTextPosition, Comments}; +use crate::comments::{dangling_comments, CommentLinePosition, Comments}; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; @@ -30,11 +30,12 @@ impl FormatNodeRule for FormatExprList { // ``` // In all other cases comments get assigned to a list element if elts.is_empty() { - let end_of_line_split = dangling - .partition_point(|comment| comment.position() == CommentTextPosition::EndOfLine); + let end_of_line_split = dangling.partition_point(|comment| { + comment.line_position() == CommentLinePosition::EndOfLine + }); debug_assert!(dangling[end_of_line_split..] .iter() - .all(|comment| comment.position() == CommentTextPosition::OwnLine)); + .all(|comment| comment.line_position() == CommentLinePosition::OwnLine)); return write!( f, [group(&format_args![ diff --git a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs index 12432c8b1d..560ab462c8 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs @@ -39,7 +39,7 @@ impl FormatRule, PyFormatContext<'_>> for FormatAnyFun let dangling_comments = comments.dangling_comments(item); let trailing_definition_comments_start = - dangling_comments.partition_point(|comment| comment.position().is_own_line()); + dangling_comments.partition_point(|comment| comment.line_position().is_own_line()); let (leading_function_definition_comments, trailing_definition_comments) = dangling_comments.split_at(trailing_definition_comments_start); diff --git a/crates/ruff_python_formatter/src/statement/stmt_if.rs b/crates/ruff_python_formatter/src/statement/stmt_if.rs index 85a9fabee1..5611a66ac3 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_if.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_if.rs @@ -73,7 +73,7 @@ impl FormatNodeRule for FormatStmtIf { if !orelse.is_empty() { // Leading comments are always own line comments let leading_else_comments_end = - else_comments.partition_point(|comment| comment.position().is_own_line()); + else_comments.partition_point(|comment| comment.line_position().is_own_line()); let (else_leading, else_trailing) = else_comments.split_at(leading_else_comments_end); write!( diff --git a/crates/ruff_python_formatter/src/statement/stmt_while.rs b/crates/ruff_python_formatter/src/statement/stmt_while.rs index 1ed71397fd..758f1d840e 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_while.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_while.rs @@ -44,7 +44,7 @@ impl FormatNodeRule for FormatStmtWhile { // Split between leading comments before the `else` keyword and end of line comments at the end of // the `else:` line. let trailing_start = - or_else_comments.partition_point(|comment| comment.position().is_own_line()); + or_else_comments.partition_point(|comment| comment.line_position().is_own_line()); let (leading, trailing) = or_else_comments.split_at(trailing_start); write!( From 653dbb6d173c1e014b2a29498d1521da9d27ab5d Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 21 Jun 2023 11:27:57 +0200 Subject: [PATCH 151/447] Format BoolOp (#4986) --- .../ruff/expression/boolean_operation.py | 64 +++++ .../src/expression/binary_like.rs | 215 ++++++++++++++ .../src/expression/expr_bin_op.rs | 271 ++++-------------- .../src/expression/expr_bool_op.rs | 138 ++++++++- .../src/expression/expr_unary_op.rs | 2 +- .../src/expression/mod.rs | 3 +- ...tter__tests__black_test__comments2_py.snap | 44 +-- ...tter__tests__black_test__comments6_py.snap | 14 +- ...er__tests__black_test__empty_lines_py.snap | 44 ++- ...ter__tests__black_test__expression_py.snap | 253 ++++++---------- ...atter__tests__black_test__fmtskip5_py.snap | 16 +- ...ck_test__skip_magic_trailing_comma_py.snap | 4 +- ...matter__tests__black_test__torture_py.snap | 13 +- ...t__trailing_comma_optional_parens1_py.snap | 4 +- ...t__trailing_comma_optional_parens2_py.snap | 12 +- ...__trailing_commas_in_leading_parts_py.snap | 4 +- ...est__expression__boolean_operation_py.snap | 143 +++++++++ ...tests__ruff_test__statement__while_py.snap | 6 +- 18 files changed, 804 insertions(+), 446 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/boolean_operation.py create mode 100644 crates/ruff_python_formatter/src/expression/binary_like.rs create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__boolean_operation_py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/boolean_operation.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/boolean_operation.py new file mode 100644 index 0000000000..f976491cd1 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/boolean_operation.py @@ -0,0 +1,64 @@ +if ( + self._proc + # has the child process finished? + and self._returncode + # the child process has finished, but the + # transport hasn't been notified yet? + and self._proc.poll() +): + pass + +if ( + self._proc + and self._returncode + and self._proc.poll() + and self._proc + and self._returncode + and self._proc.poll() +): + ... + +if ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaaaaaaaa +): + ... + + +if ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaas + and aaaaaaaaaaaaaaaaa +): + ... + + +if [2222, 333] and [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] and [2222, 333]: + pass + +# Break right only applies for boolean operations with a left and right side +if ( + aaaaaaaaaaaaaaaaaaaaaaaaaa + and bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + and ccccccccccccccccc + and [dddddddddddddd, eeeeeeeeee, fffffffffffffff] +): + pass diff --git a/crates/ruff_python_formatter/src/expression/binary_like.rs b/crates/ruff_python_formatter/src/expression/binary_like.rs new file mode 100644 index 0000000000..eff12b71d9 --- /dev/null +++ b/crates/ruff_python_formatter/src/expression/binary_like.rs @@ -0,0 +1,215 @@ +//! This module provides helper utilities to format an expression that has a left side, an operator, +//! and a right side (binary like). + +use crate::expression::parentheses::Parentheses; +use crate::prelude::*; +use ruff_formatter::{format_args, write}; +use rustpython_parser::ast::Expr; + +/// Trait to implement a binary like syntax that has a left operand, an operator, and a right operand. +pub(super) trait FormatBinaryLike<'ast> { + /// The type implementing the formatting of the operator. + type FormatOperator: Format>; + + /// Formats the binary like expression to `f`. + fn fmt_binary( + &self, + parentheses: Option, + f: &mut PyFormatter<'ast, '_>, + ) -> FormatResult<()> { + let left = self.left()?; + let operator = self.operator(); + let right = self.right()?; + + let layout = if parentheses == Some(Parentheses::Custom) { + self.binary_layout() + } else { + BinaryLayout::Default + }; + + match layout { + BinaryLayout::Default => self.fmt_default(f), + BinaryLayout::ExpandLeft => { + let left = left.format().memoized(); + let right = right.format().memoized(); + write!( + f, + [best_fitting![ + // Everything on a single line + format_args![group(&left), space(), operator, space(), right], + // Break the left over multiple lines, keep the right flat + format_args![ + group(&left).should_expand(true), + space(), + operator, + space(), + right + ], + // The content doesn't fit, indent the content and break before the operator. + format_args![ + text("("), + block_indent(&format_args![ + left, + hard_line_break(), + operator, + space(), + right + ]), + text(")") + ] + ] + .with_mode(BestFittingMode::AllLines)] + ) + } + BinaryLayout::ExpandRight => { + let left_group = f.group_id("BinaryLeft"); + + write!( + f, + [ + // Wrap the left in a group and gives it an id. The printer first breaks the + // right side if `right` contains any line break because the printer breaks + // sequences of groups from right to left. + // Indents the left side if the group breaks. + group(&format_args![ + if_group_breaks(&text("(")), + indent_if_group_breaks( + &format_args![ + soft_line_break(), + left.format(), + soft_line_break_or_space(), + operator, + space() + ], + left_group + ) + ]) + .with_group_id(Some(left_group)), + // Wrap the right in a group and indents its content but only if the left side breaks + group(&indent_if_group_breaks(&right.format(), left_group)), + // If the left side breaks, insert a hard line break to finish the indent and close the open paren. + if_group_breaks(&format_args![hard_line_break(), text(")")]) + .with_group_id(Some(left_group)) + ] + ) + } + BinaryLayout::ExpandRightThenLeft => { + // The formatter expands group-sequences from right to left, and expands both if + // there isn't enough space when expanding only one of them. + write!( + f, + [ + group(&left.format()), + space(), + operator, + space(), + group(&right.format()) + ] + ) + } + } + } + + /// Determines which binary layout to use. + fn binary_layout(&self) -> BinaryLayout { + if let (Ok(left), Ok(right)) = (self.left(), self.right()) { + BinaryLayout::from_left_right(left, right) + } else { + BinaryLayout::Default + } + } + + /// Formats the node according to the default layout. + fn fmt_default(&self, f: &mut PyFormatter<'ast, '_>) -> FormatResult<()>; + + /// Returns the left operator + fn left(&self) -> FormatResult<&Expr>; + + /// Returns the right operator. + fn right(&self) -> FormatResult<&Expr>; + + /// Returns the object that formats the operator. + fn operator(&self) -> Self::FormatOperator; +} + +fn can_break_expr(expr: &Expr) -> bool { + use ruff_python_ast::prelude::*; + + match expr { + Expr::Tuple(ExprTuple { + elts: expressions, .. + }) + | Expr::List(ExprList { + elts: expressions, .. + }) + | Expr::Set(ExprSet { + elts: expressions, .. + }) + | Expr::Dict(ExprDict { + values: expressions, + .. + }) => !expressions.is_empty(), + Expr::Call(ExprCall { args, keywords, .. }) => !(args.is_empty() && keywords.is_empty()), + Expr::ListComp(_) | Expr::SetComp(_) | Expr::DictComp(_) | Expr::GeneratorExp(_) => true, + Expr::UnaryOp(ExprUnaryOp { operand, .. }) => match operand.as_ref() { + Expr::BinOp(_) => true, + _ => can_break_expr(operand.as_ref()), + }, + _ => false, + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub(super) enum BinaryLayout { + /// Put each operand on their own line if either side expands + Default, + + /// Try to expand the left to make it fit. Add parentheses if the left or right don't fit. + /// + ///```python + /// [ + /// a, + /// b + /// ] & c + ///``` + ExpandLeft, + + /// Try to expand the right to make it fix. Add parentheses if the left or right don't fit. + /// + /// ```python + /// a & [ + /// b, + /// c + /// ] + /// ``` + ExpandRight, + + /// Both the left and right side can be expanded. Try in the following order: + /// * expand the right side + /// * expand the left side + /// * expand both sides + /// + /// to make the expression fit + /// + /// ```python + /// [ + /// a, + /// b + /// ] & [ + /// c, + /// d + /// ] + /// ``` + ExpandRightThenLeft, +} + +impl BinaryLayout { + pub(super) fn from_left_right(left: &Expr, right: &Expr) -> BinaryLayout { + match (can_break_expr(left), can_break_expr(right)) { + (false, false) => BinaryLayout::Default, + (true, false) => BinaryLayout::ExpandLeft, + (false, true) => BinaryLayout::ExpandRight, + (true, true) => BinaryLayout::ExpandRightThenLeft, + } + } +} diff --git a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs index 199e689fb2..7c50fdcc51 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs @@ -1,14 +1,12 @@ use crate::comments::{trailing_comments, Comments}; +use crate::expression::binary_like::{BinaryLayout, FormatBinaryLike}; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parenthesize, }; use crate::expression::Parentheses; use crate::prelude::*; use crate::FormatNodeRule; -use ruff_formatter::{ - format_args, write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions, -}; -use ruff_python_ast::node::AstNode; +use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions}; use rustpython_parser::ast::{ Constant, Expr, ExprAttribute, ExprBinOp, ExprConstant, ExprUnaryOp, Operator, UnaryOp, }; @@ -29,132 +27,7 @@ impl FormatRuleWithOptions> for FormatExprBinOp { impl FormatNodeRule for FormatExprBinOp { fn fmt_fields(&self, item: &ExprBinOp, f: &mut PyFormatter) -> FormatResult<()> { - let ExprBinOp { - left, - right, - op, - range: _, - } = item; - - let layout = if self.parentheses == Some(Parentheses::Custom) { - BinaryLayout::from(item) - } else { - BinaryLayout::Default - }; - - match layout { - BinaryLayout::Default => { - let comments = f.context().comments().clone(); - let operator_comments = comments.dangling_comments(item.as_any_node_ref()); - let needs_space = !is_simple_power_expression(item); - - let before_operator_space = if needs_space { - soft_line_break_or_space() - } else { - soft_line_break() - }; - - write!( - f, - [ - left.format(), - before_operator_space, - op.format(), - trailing_comments(operator_comments), - ] - )?; - - // Format the operator on its own line if the right side has any leading comments. - if comments.has_leading_comments(right.as_ref()) { - write!(f, [hard_line_break()])?; - } else if needs_space { - write!(f, [space()])?; - } - - write!(f, [group(&right.format())]) - } - - BinaryLayout::ExpandLeft => { - let left = left.format().memoized(); - let right = right.format().memoized(); - write!( - f, - [best_fitting![ - // Everything on a single line - format_args![left, space(), op.format(), space(), right], - // Break the left over multiple lines, keep the right flat - format_args![ - group(&left).should_expand(true), - space(), - op.format(), - space(), - right - ], - // The content doesn't fit, indent the content and break before the operator. - format_args![ - text("("), - block_indent(&format_args![ - left, - hard_line_break(), - op.format(), - space(), - right - ]), - text(")") - ] - ] - .with_mode(BestFittingMode::AllLines)] - ) - } - - BinaryLayout::ExpandRight => { - let left_group = f.group_id("BinaryLeft"); - - write!( - f, - [ - // Wrap the left in a group and gives it an id. The printer first breaks the - // right side if `right` contains any line break because the printer breaks - // sequences of groups from right to left. - // Indents the left side if the group breaks. - group(&format_args![ - if_group_breaks(&text("(")), - indent_if_group_breaks( - &format_args![ - soft_line_break(), - left.format(), - soft_line_break_or_space(), - op.format(), - space() - ], - left_group - ) - ]) - .with_group_id(Some(left_group)), - // Wrap the right in a group and indents its content but only if the left side breaks - group(&indent_if_group_breaks(&right.format(), left_group)), - // If the left side breaks, insert a hard line break to finish the indent and close the open paren. - if_group_breaks(&format_args![hard_line_break(), text(")")]) - .with_group_id(Some(left_group)) - ] - ) - } - - BinaryLayout::ExpandRightThenLeft => { - // The formatter expands group-sequences from right to left, and expands both if - // there isn't enough space when expanding only one of them. - write!( - f, - [ - group(&left.format()), - space(), - op.format(), - space(), - group(&right.format()) - ] - ) - } - } + item.fmt_binary(self.parentheses, f) } fn fmt_dangling_comments(&self, _node: &ExprBinOp, _f: &mut PyFormatter) -> FormatResult<()> { @@ -163,6 +36,60 @@ impl FormatNodeRule for FormatExprBinOp { } } +impl<'ast> FormatBinaryLike<'ast> for ExprBinOp { + type FormatOperator = FormatOwnedWithRule>; + + fn fmt_default(&self, f: &mut PyFormatter<'ast, '_>) -> FormatResult<()> { + let ExprBinOp { + range: _, + left, + op, + right, + } = self; + + let comments = f.context().comments().clone(); + let operator_comments = comments.dangling_comments(self); + let needs_space = !is_simple_power_expression(self); + + let before_operator_space = if needs_space { + soft_line_break_or_space() + } else { + soft_line_break() + }; + + write!( + f, + [ + left.format(), + before_operator_space, + op.format(), + trailing_comments(operator_comments), + ] + )?; + + // Format the operator on its own line if the right side has any leading comments. + if comments.has_leading_comments(right.as_ref()) { + write!(f, [hard_line_break()])?; + } else if needs_space { + write!(f, [space()])?; + } + + write!(f, [group(&right.format())]) + } + + fn left(&self) -> FormatResult<&Expr> { + Ok(&self.left) + } + + fn right(&self) -> FormatResult<&Expr> { + Ok(&self.right) + } + + fn operator(&self) -> Self::FormatOperator { + self.op.into_format() + } +} + const fn is_simple_power_expression(expr: &ExprBinOp) -> bool { expr.op.is_pow() && is_simple_power_operand(&expr.left) && is_simple_power_operand(&expr.right) } @@ -235,7 +162,7 @@ impl NeedsParentheses for ExprBinOp { ) -> Parentheses { match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { Parentheses::Optional => { - if BinaryLayout::from(self) == BinaryLayout::Default + if self.binary_layout() == BinaryLayout::Default || comments.has_leading_comments(self.right.as_ref()) || comments.has_dangling_comments(self) { @@ -248,85 +175,3 @@ impl NeedsParentheses for ExprBinOp { } } } - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -enum BinaryLayout { - /// Put each operand on their own line if either side expands - Default, - - /// Try to expand the left to make it fit. Add parentheses if the left or right don't fit. - /// - ///```python - /// [ - /// a, - /// b - /// ] & c - ///``` - ExpandLeft, - - /// Try to expand the right to make it fix. Add parentheses if the left or right don't fit. - /// - /// ```python - /// a & [ - /// b, - /// c - /// ] - /// ``` - ExpandRight, - - /// Both the left and right side can be expanded. Try in the following order: - /// * expand the right side - /// * expand the left side - /// * expand both sides - /// - /// to make the expression fit - /// - /// ```python - /// [ - /// a, - /// b - /// ] & [ - /// c, - /// d - /// ] - /// ``` - ExpandRightThenLeft, -} - -impl BinaryLayout { - fn from(expr: &ExprBinOp) -> Self { - match (can_break(&expr.left), can_break(&expr.right)) { - (false, false) => Self::Default, - (true, false) => Self::ExpandLeft, - (false, true) => Self::ExpandRight, - (true, true) => Self::ExpandRightThenLeft, - } - } -} - -fn can_break(expr: &Expr) -> bool { - use ruff_python_ast::prelude::*; - - match expr { - Expr::Tuple(ExprTuple { - elts: expressions, .. - }) - | Expr::List(ExprList { - elts: expressions, .. - }) - | Expr::Set(ExprSet { - elts: expressions, .. - }) - | Expr::Dict(ExprDict { - values: expressions, - .. - }) => !expressions.is_empty(), - Expr::Call(ExprCall { args, keywords, .. }) => !(args.is_empty() && keywords.is_empty()), - Expr::ListComp(_) | Expr::SetComp(_) | Expr::DictComp(_) | Expr::GeneratorExp(_) => true, - Expr::UnaryOp(ExprUnaryOp { operand, .. }) => match operand.as_ref() { - Expr::BinOp(_) => true, - _ => can_break(operand.as_ref()), - }, - _ => false, - } -} 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 e975949029..e3fd595bab 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bool_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bool_op.rs @@ -1,22 +1,87 @@ -use crate::comments::Comments; +use crate::comments::{leading_comments, Comments}; +use crate::expression::binary_like::{BinaryLayout, FormatBinaryLike}; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; -use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::ExprBoolOp; +use crate::prelude::*; +use ruff_formatter::{ + write, FormatError, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions, +}; +use rustpython_parser::ast::{BoolOp, Expr, ExprBoolOp}; #[derive(Default)] -pub struct FormatExprBoolOp; +pub struct FormatExprBoolOp { + parentheses: Option, +} + +impl FormatRuleWithOptions> for FormatExprBoolOp { + type Options = Option; + fn with_options(mut self, options: Self::Options) -> Self { + self.parentheses = options; + self + } +} impl FormatNodeRule for FormatExprBoolOp { - fn fmt_fields(&self, _item: &ExprBoolOp, f: &mut PyFormatter) -> FormatResult<()> { - write!( - f, - [not_yet_implemented_custom_text( - "NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2" - )] - ) + fn fmt_fields(&self, item: &ExprBoolOp, f: &mut PyFormatter) -> FormatResult<()> { + item.fmt_binary(self.parentheses, f) + } +} + +impl<'ast> FormatBinaryLike<'ast> for ExprBoolOp { + type FormatOperator = FormatOwnedWithRule>; + + fn binary_layout(&self) -> BinaryLayout { + match self.values.as_slice() { + [left, right] => BinaryLayout::from_left_right(left, right), + [..] => BinaryLayout::Default, + } + } + + fn fmt_default(&self, f: &mut PyFormatter<'ast, '_>) -> FormatResult<()> { + let ExprBoolOp { + range: _, + op, + values, + } = self; + + let mut values = values.iter(); + let comments = f.context().comments().clone(); + + let Some(first) = values.next() else { + return Ok(()) + }; + + write!(f, [group(&first.format())])?; + + for value in values { + let leading_value_comments = comments.leading_comments(value); + // Format the expressions leading comments **before** the operator + if leading_value_comments.is_empty() { + write!(f, [soft_line_break_or_space()])?; + } else { + write!( + f, + [hard_line_break(), leading_comments(leading_value_comments)] + )?; + } + + write!(f, [op.format(), space(), group(&value.format())])?; + } + + Ok(()) + } + + fn left(&self) -> FormatResult<&Expr> { + self.values.first().ok_or(FormatError::SyntaxError) + } + + fn right(&self) -> FormatResult<&Expr> { + self.values.last().ok_or(FormatError::SyntaxError) + } + + fn operator(&self) -> Self::FormatOperator { + self.op.into_format() } } @@ -27,6 +92,53 @@ impl NeedsParentheses for ExprBoolOp { source: &str, comments: &Comments, ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + Parentheses::Optional => match self.binary_layout() { + BinaryLayout::Default => Parentheses::Optional, + + BinaryLayout::ExpandRight + | BinaryLayout::ExpandLeft + | BinaryLayout::ExpandRightThenLeft + if self + .values + .last() + .map_or(false, |right| comments.has_leading_comments(right)) => + { + Parentheses::Optional + } + _ => Parentheses::Custom, + }, + parentheses => parentheses, + } + } +} + +#[derive(Copy, Clone)] +pub struct FormatBoolOp; + +impl<'ast> AsFormat> for BoolOp { + type Format<'a> = FormatRefWithRule<'a, BoolOp, FormatBoolOp, PyFormatContext<'ast>>; + + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new(self, FormatBoolOp) + } +} + +impl<'ast> IntoFormat> for BoolOp { + type Format = FormatOwnedWithRule>; + + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new(self, FormatBoolOp) + } +} + +impl FormatRule> for FormatBoolOp { + fn fmt(&self, item: &BoolOp, f: &mut Formatter>) -> FormatResult<()> { + let operator = match item { + BoolOp::And => "and", + BoolOp::Or => "or", + }; + + text(operator).fmt(f) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_unary_op.rs b/crates/ruff_python_formatter/src/expression/expr_unary_op.rs index db3dfc06ad..04151760fa 100644 --- a/crates/ruff_python_formatter/src/expression/expr_unary_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_unary_op.rs @@ -39,7 +39,7 @@ impl FormatNodeRule for FormatExprUnaryOp { // ``` let leading_operand_comments = comments.leading_comments(operand.as_ref()); let trailing_operator_comments_end = - leading_operand_comments.partition_point(|p| p.position().is_end_of_line()); + leading_operand_comments.partition_point(|p| p.line_position().is_end_of_line()); let (trailing_operator_comments, leading_operand_comments) = leading_operand_comments.split_at(trailing_operator_comments_end); diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index b6ca41efbd..a3670b1761 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -7,6 +7,7 @@ use ruff_formatter::{ }; use rustpython_parser::ast::Expr; +mod binary_like; pub(crate) mod expr_attribute; pub(crate) mod expr_await; pub(crate) mod expr_bin_op; @@ -59,7 +60,7 @@ impl FormatRule> for FormatExpr { ); let format_expr = format_with(|f| match item { - Expr::BoolOp(expr) => expr.format().fmt(f), + Expr::BoolOp(expr) => expr.format().with_options(Some(parentheses)).fmt(f), Expr::NamedExpr(expr) => expr.format().fmt(f), Expr::BinOp(expr) => expr.format().with_options(Some(parentheses)).fmt(f), Expr::UnaryOp(expr) => expr.format().fmt(f), diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap index 7869c241a8..3cd87b7335 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap @@ -227,7 +227,7 @@ instruction()#comment with bad spacing ] not_shareables = [ -@@ -37,50 +33,51 @@ +@@ -37,49 +33,57 @@ # builtin types and objects type, object, @@ -267,13 +267,13 @@ instruction()#comment with bad spacing - children[0], + parameters.NOT_IMPLEMENTED_attr = [ + NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # (1 -+ body, + body, +- children[-1], # type: ignore + NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # )1 + ] + parameters.NOT_IMPLEMENTED_attr = [ + NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], - body, -- children[-1], # type: ignore ++ body, + NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # type: ignore ] else: @@ -286,24 +286,25 @@ instruction()#comment with bad spacing + NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # )2 ] - parameters.children = [parameters.what_if_this_was_actually_long.children[0], body, parameters.children[-1]] # type: ignore -- if ( -- self._proc is not None -- # has the child process finished? -- and self._returncode is None -- # the child process has finished, but the -- # transport hasn't been notified yet? -- and self._proc.poll() is None -- ): + parameters.NOT_IMPLEMENTED_attr = [ + NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], + body, + NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], + ] # type: ignore -+ if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: + if ( +- self._proc is not None ++ NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + # has the child process finished? +- and self._returncode is None ++ and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + # the child process has finished, but the + # transport hasn't been notified yet? +- and self._proc.poll() is None ++ and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + ): pass # no newline before or after - short = [ -@@ -91,48 +88,14 @@ +@@ -91,48 +95,14 @@ ] # no newline after @@ -357,7 +358,7 @@ instruction()#comment with bad spacing while True: if False: continue -@@ -141,25 +104,13 @@ +@@ -141,25 +111,13 @@ # and round and round we go # let's return @@ -386,7 +387,7 @@ instruction()#comment with bad spacing ####################### -@@ -167,7 +118,7 @@ +@@ -167,7 +125,7 @@ ####################### @@ -479,7 +480,14 @@ def inline_comments_in_brackets_ruin_everything(): body, NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], ] # type: ignore - if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: + if ( + NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + # has the child process finished? + and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + # the child process has finished, but the + # transport hasn't been notified yet? + and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + ): pass # no newline before or after short = [ diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap index 430431fe04..2f25cc478b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap @@ -137,7 +137,7 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite def f( -@@ -49,10 +49,8 @@ +@@ -49,10 +49,11 @@ element = 0 # type: int another_element = 1 # type: float another_element_with_long_name = 2 # type: int @@ -146,11 +146,14 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite - ) # type: int - an_element_with_a_long_value = calls() or more_calls() and more() # type: bool + another_really_really_long_element_with_a_unnecessarily_long_name_to_describe_what_it_does_enterprise_style = 3 # type: int -+ an_element_with_a_long_value = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 # type: bool ++ an_element_with_a_long_value = ( ++ NOT_IMPLEMENTED_call() ++ or NOT_IMPLEMENTED_call() and NOT_IMPLEMENTED_call() ++ ) # type: bool tup = ( another_element, -@@ -84,35 +82,22 @@ +@@ -84,35 +85,22 @@ def func( @@ -255,7 +258,10 @@ def f( another_element = 1 # type: float another_element_with_long_name = 2 # type: int another_really_really_long_element_with_a_unnecessarily_long_name_to_describe_what_it_does_enterprise_style = 3 # type: int - an_element_with_a_long_value = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 # type: bool + an_element_with_a_long_value = ( + NOT_IMPLEMENTED_call() + or NOT_IMPLEMENTED_call() and NOT_IMPLEMENTED_call() + ) # type: bool tup = ( another_element, diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap index 03794aac66..099f07777c 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap @@ -105,7 +105,7 @@ def g(): ```diff --- Black +++ Ruff -@@ -1,47 +1,35 @@ +@@ -1,89 +1,79 @@ -"""Docstring.""" +"NOT_YET_IMPLEMENTED_STRING" @@ -142,7 +142,7 @@ def g(): - prevp = preceding_leaf(p) - if not prevp or prevp.type in OPENING_BRACKETS: + prevp = NOT_IMPLEMENTED_call() -+ if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: ++ if not prevp or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: return NO - if prevp.type == token.EQUAL: @@ -154,7 +154,10 @@ def g(): - syms.argument, - }: + if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: ++ if ( ++ prevp.NOT_IMPLEMENTED_attr ++ and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++ ): return NO - elif prevp.type == token.DOUBLESTAR: @@ -166,15 +169,18 @@ def g(): - syms.dictsetmaker, - }: + elif NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: ++ if ( ++ prevp.NOT_IMPLEMENTED_attr ++ and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++ ): return NO -@@ -49,41 +37,34 @@ + ############################################################################### # SECTION BECAUSE SECTIONS ############################################################################### - - + def g(): - NO = "" - SPACE = " " @@ -210,7 +216,7 @@ def g(): + prevp = NOT_IMPLEMENTED_call() - if not prevp or prevp.type in OPENING_BRACKETS: -+ if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: ++ if not prevp or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: # Start of the line or a bracketed expression. # More than one line for the comment. return NO @@ -224,7 +230,10 @@ def g(): - syms.argument, - }: + if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: ++ if ( ++ prevp.NOT_IMPLEMENTED_attr ++ and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++ ): return NO ``` @@ -254,15 +263,21 @@ def f(): prev = leaf.NOT_IMPLEMENTED_attr if not prev: prevp = NOT_IMPLEMENTED_call() - if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: + if not prevp or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: return NO if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: + if ( + prevp.NOT_IMPLEMENTED_attr + and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + ): return NO elif NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: + if ( + prevp.NOT_IMPLEMENTED_attr + and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + ): return NO @@ -293,13 +308,16 @@ def g(): if not prev: prevp = NOT_IMPLEMENTED_call() - if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: + if not prevp or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: # Start of the line or a bracketed expression. # More than one line for the comment. return NO if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: + if ( + prevp.NOT_IMPLEMENTED_attr + and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + ): return NO ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap index a96793d3d2..1bc96c10d0 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap @@ -276,43 +276,10 @@ last_call() Name None True -@@ -7,18 +8,18 @@ - 1 - 1.0 - 1j --True or False --True or False or None --True and False --True and False and None --(Name1 and Name2) or Name3 --Name1 and Name2 or Name3 --Name1 or (Name2 and Name3) --Name1 or Name2 and Name3 --(Name1 and Name2) or (Name3 and Name4) --Name1 and Name2 or Name3 and Name4 --Name1 or (Name2 and Name3) or Name4 --Name1 or Name2 and Name3 or Name4 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 - v1 << 2 - 1 >> v2 - 1 % finished -@@ -28,205 +29,204 @@ - ~great - +value +@@ -30,203 +31,178 @@ -1 --~int and not v1 ^ 123 + v2 | True --(~int) and (not ((v1 ^ (123 + v2)) | True)) + ~int and not v1 ^ 123 + v2 | True + (~int) and (not ((v1 ^ (123 + v2)) | True)) -+(really ** -(confusing ** ~(operator**-precedence))) -flags & ~select.EPOLLIN and waiters.write_task is not None -lambda arg: None @@ -337,10 +304,8 @@ last_call() -) -{"2.7": dead, "3.7": (long_live or die_hard)} -{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 ++really ** -confusing ** ~operator**-precedence -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 ++flags & ~select.NOT_IMPLEMENTED_attr and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +lambda x: True +lambda x: True +lambda x: True @@ -356,15 +321,11 @@ last_call() +(NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false) +{ + "NOT_YET_IMPLEMENTED_STRING": dead, -+ "NOT_YET_IMPLEMENTED_STRING": ( -+ NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+ ), ++ "NOT_YET_IMPLEMENTED_STRING": (long_live or die_hard), +} +{ + "NOT_YET_IMPLEMENTED_STRING": dead, -+ "NOT_YET_IMPLEMENTED_STRING": ( -+ NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+ ), ++ "NOT_YET_IMPLEMENTED_STRING": (long_live or die_hard), + **{"NOT_YET_IMPLEMENTED_STRING": verygood}, +} {**a, **b, **c} @@ -378,32 +339,31 @@ last_call() + "NOT_YET_IMPLEMENTED_STRING", + (NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false), +} -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 ++( ++ {"NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING"}, ++ (True or False), ++ (+value), ++ "NOT_YET_IMPLEMENTED_STRING", ++ b"NOT_YET_IMPLEMENTED_BYTE_STRING", ++) or None () (1,) (1, 2) (1, 2, 3) [] --[1, 2, 3, 4, 5, 6, 7, 8, 9, (10 or A), (11 or B), (12 or C)] - [ - 1, - 2, - 3, + [1, 2, 3, 4, 5, 6, 7, 8, 9, (10 or A), (11 or B), (12 or C)] +-[ +- 1, +- 2, +- 3, -] -[*a] -[*range(10)] -[ - *a, - 4, - 5, -+ 6, -+ 7, -+ 8, -+ 9, -+ (NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2), -+ (NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2), -+ (NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2), - ] +- 4, +- 5, +-] -[ - 4, - *a, @@ -503,7 +463,7 @@ last_call() - k: v - for k, v in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension + "NOT_YET_IMPLEMENTED_STRING": dead, -+ "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2, ++ "NOT_YET_IMPLEMENTED_STRING": long_live or die_hard, } -Python3 > Python2 > COBOL -Life is Life @@ -540,28 +500,7 @@ last_call() -] -very_long_variable_name_filters: t.List[ - t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], -+{ -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, -+} -+[ -+ 1, -+ 2, -+ 3, -+ 4, -+ 5, -+ 6, -+ 7, -+ 8, -+ 9, -+ NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2, -+ NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2, -+ NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2, - ] +-] -xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore - sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) -) @@ -600,7 +539,15 @@ last_call() -(str or None) if (sys.version_info[0] > (3,)) else (str or bytes or None) -{"2.7": dead, "3.7": long_live or die_hard} -{"2.7", "3.6", "3.7", "3.8", "3.9", "4.0" if gilectomy else "3.10"} --[1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C] ++{ ++ "NOT_YET_IMPLEMENTED_STRING", ++ "NOT_YET_IMPLEMENTED_STRING", ++ "NOT_YET_IMPLEMENTED_STRING", ++ "NOT_YET_IMPLEMENTED_STRING", ++ "NOT_YET_IMPLEMENTED_STRING", ++ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, ++} + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C] (SomeName) SomeName (Good, Bad, Ugly) @@ -638,19 +585,10 @@ last_call() -g = 1, *"ten" -what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set( - vars_to_remove -+e = NOT_IMPLEMENTED_call() -+f = 1, NOT_YET_IMPLEMENTED_ExprStarred -+g = 1, NOT_YET_IMPLEMENTED_ExprStarred -+what_is_up_with_those_new_coord_names = ( -+ (coord_names + NOT_IMPLEMENTED_call()) -+ + NOT_IMPLEMENTED_call() - ) +-) -what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( - vars_to_remove -+what_is_up_with_those_new_coord_names = ( -+ (coord_names | NOT_IMPLEMENTED_call()) -+ - NOT_IMPLEMENTED_call() - ) +-) -result = ( - session.query(models.Customer.id) - .filter( @@ -658,7 +596,13 @@ last_call() - ) - .order_by(models.Customer.id.asc()) - .all() --) ++e = NOT_IMPLEMENTED_call() ++f = 1, NOT_YET_IMPLEMENTED_ExprStarred ++g = 1, NOT_YET_IMPLEMENTED_ExprStarred ++what_is_up_with_those_new_coord_names = ( ++ (coord_names + NOT_IMPLEMENTED_call()) ++ + NOT_IMPLEMENTED_call() + ) -result = ( - session.query(models.Customer.id) - .filter( @@ -668,7 +612,10 @@ last_call() - models.Customer.id.asc(), - ) - .all() --) ++what_is_up_with_those_new_coord_names = ( ++ (coord_names | NOT_IMPLEMENTED_call()) ++ - NOT_IMPLEMENTED_call() + ) -Ø = set() -authors.łukasz.say_thanks() +result = NOT_IMPLEMENTED_call() @@ -678,7 +625,7 @@ last_call() mapping = { A: 0.25 * (10.0 / 12), B: 0.1 * (10.0 / 12), -@@ -236,65 +236,35 @@ +@@ -236,64 +212,38 @@ def gen(): @@ -715,7 +662,6 @@ last_call() - ... -for j in 1 + (2 + 3): - ... --while this and that: +NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call() @@ -727,7 +673,7 @@ last_call() +NOT_YET_IMPLEMENTED_StmtFor +NOT_YET_IMPLEMENTED_StmtFor +NOT_YET_IMPLEMENTED_StmtFor -+while NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: + while this and that: ... -for ( - addr_family, @@ -753,21 +699,22 @@ last_call() - aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp - is not qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz -) --if ( -- threading.current_thread() != threading.main_thread() -- and threading.current_thread() != threading.main_thread() -- or signal.getsignal(signal.SIGINT) != signal.default_int_handler --): +NOT_YET_IMPLEMENTED_StmtFor +a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: + if ( +- threading.current_thread() != threading.main_thread() +- and threading.current_thread() != threading.main_thread() +- or signal.getsignal(signal.SIGINT) != signal.default_int_handler ++ NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++ and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++ or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + ): return True if ( - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -@@ -327,24 +297,44 @@ +@@ -327,24 +277,44 @@ ): return True if ( @@ -824,7 +771,7 @@ last_call() ): return True ( -@@ -363,8 +353,9 @@ +@@ -363,8 +333,9 @@ bbbb >> bbbb * bbbb ( aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa @@ -851,18 +798,18 @@ False 1 1.0 1j -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 +True or False +True or False or None +True and False +True and False and None +(Name1 and Name2) or Name3 +Name1 and Name2 or Name3 +Name1 or (Name2 and Name3) +Name1 or Name2 and Name3 +(Name1 and Name2) or (Name3 and Name4) +Name1 and Name2 or Name3 and Name4 +Name1 or (Name2 and Name3) or Name4 +Name1 or Name2 and Name3 or Name4 v1 << 2 1 >> v2 1 % finished @@ -872,10 +819,10 @@ not great ~great +value -1 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 +~int and not v1 ^ 123 + v2 | True +(~int) and (not ((v1 ^ (123 + v2)) | True)) +really ** -confusing ** ~operator**-precedence -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 +flags & ~select.NOT_IMPLEMENTED_attr and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right lambda x: True lambda x: True lambda x: True @@ -891,15 +838,11 @@ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false (NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false) { "NOT_YET_IMPLEMENTED_STRING": dead, - "NOT_YET_IMPLEMENTED_STRING": ( - NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 - ), + "NOT_YET_IMPLEMENTED_STRING": (long_live or die_hard), } { "NOT_YET_IMPLEMENTED_STRING": dead, - "NOT_YET_IMPLEMENTED_STRING": ( - NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 - ), + "NOT_YET_IMPLEMENTED_STRING": (long_live or die_hard), **{"NOT_YET_IMPLEMENTED_STRING": verygood}, } {**a, **b, **c} @@ -911,26 +854,19 @@ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false "NOT_YET_IMPLEMENTED_STRING", (NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false), } -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 +( + {"NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING"}, + (True or False), + (+value), + "NOT_YET_IMPLEMENTED_STRING", + b"NOT_YET_IMPLEMENTED_BYTE_STRING", +) or None () (1,) (1, 2) (1, 2, 3) [] -[ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - (NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2), - (NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2), - (NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2), -] +[1, 2, 3, 4, 5, 6, 7, 8, 9, (10 or A), (11 or B), (12 or C)] [1, 2, 3] [NOT_YET_IMPLEMENTED_ExprStarred] [NOT_YET_IMPLEMENTED_ExprStarred] @@ -1010,7 +946,7 @@ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false { "NOT_YET_IMPLEMENTED_STRING": dead, - "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2, + "NOT_YET_IMPLEMENTED_STRING": long_live or die_hard, } { "NOT_YET_IMPLEMENTED_STRING", @@ -1020,20 +956,7 @@ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false "NOT_YET_IMPLEMENTED_STRING", NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, } -[ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2, - NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2, - NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2, -] +[1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C] (SomeName) SomeName (Good, Bad, Ugly) @@ -1100,14 +1023,18 @@ NOT_YET_IMPLEMENTED_StmtFor NOT_YET_IMPLEMENTED_StmtFor NOT_YET_IMPLEMENTED_StmtFor NOT_YET_IMPLEMENTED_StmtFor -while NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: +while this and that: ... NOT_YET_IMPLEMENTED_StmtFor a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: +if ( + NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +): return True if ( aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap index 5c4a69a412..7ad743ff86 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap @@ -22,15 +22,17 @@ else: ```diff --- Black +++ Ruff -@@ -1,9 +1,5 @@ +@@ -1,9 +1,9 @@ a, b, c = 3, 4, 5 --if ( + if ( - a == 3 - and b != 9 # fmt: skip - and c is not None --): ++ NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++ and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right # fmt: skip ++ and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + ): - print("I'm good!") -+if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: + NOT_IMPLEMENTED_call() else: - print("I'm bad") @@ -41,7 +43,11 @@ else: ```py a, b, c = 3, 4, 5 -if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: +if ( + NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right # fmt: skip + and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +): NOT_IMPLEMENTED_call() else: NOT_IMPLEMENTED_call() diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap index 2d9a375f59..0ca6ef0a1f 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap @@ -97,7 +97,7 @@ func( + b, + c, + d, -+) = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 ++) = NOT_IMPLEMENTED_call() and NOT_IMPLEMENTED_call() -func(argument1, (one, two), argument4, argument5, argument6) +NOT_IMPLEMENTED_call() @@ -133,7 +133,7 @@ NOT_IMPLEMENTED_call() b, c, d, -) = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 +) = NOT_IMPLEMENTED_call() and NOT_IMPLEMENTED_call() NOT_IMPLEMENTED_call() ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap index 2fe7481d4e..e9d41df04d 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap @@ -42,7 +42,7 @@ assert ( ```diff --- Black +++ Ruff -@@ -2,57 +2,21 @@ +@@ -2,57 +2,24 @@ ( () << 0 @@ -96,8 +96,10 @@ assert ( - othr.parameters, - othr.meta_data, - othr.schedule, -- ) -+ return NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 ++ return ( ++ NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++ and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + ) -assert a_function( @@ -128,7 +130,10 @@ NOT_YET_IMPLEMENTED_StmtClassDef def test(self, othr): - return NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 + return ( + NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + ) NOT_YET_IMPLEMENTED_StmtAssert diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap index 1528c308d1..9b6fe74f54 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap @@ -43,7 +43,7 @@ class A: - _winapi.ERROR_SEM_TIMEOUT, - _winapi.ERROR_PIPE_BUSY, -) or _check_timeout(t): -+if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: ++if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right or NOT_IMPLEMENTED_call(): pass if x: @@ -82,7 +82,7 @@ class A: ## Ruff Output ```py -if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: +if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right or NOT_IMPLEMENTED_call(): pass if x: diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens2_py.snap index 2f591416f5..2c3f46e706 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens2_py.snap @@ -16,20 +16,26 @@ if (e123456.get_tk_patchlevel() >= (8, 6, 0, 'final') or ```diff --- Black +++ Ruff -@@ -1,6 +1,2 @@ +@@ -1,6 +1,5 @@ -if e123456.get_tk_patchlevel() >= (8, 6, 0, "final") or ( - 8, - 5, - 8, -) <= get_tk_patchlevel() < (8, 6): -+if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: ++if ( ++ NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++ or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++): pass ``` ## Ruff Output ```py -if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: +if ( + NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +): pass ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap index 9999f85e05..fbbed48d6e 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap @@ -75,7 +75,7 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( -) = func1( - arg1 -) and func2(arg2) -+) = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 ++) = NOT_IMPLEMENTED_call() and NOT_IMPLEMENTED_call() # Example from https://github.com/psf/black/issues/3229 @@ -121,7 +121,7 @@ NOT_IMPLEMENTED_call() b, c, d, -) = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 +) = NOT_IMPLEMENTED_call() and NOT_IMPLEMENTED_call() # Example from https://github.com/psf/black/issues/3229 diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__boolean_operation_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__boolean_operation_py.snap new file mode 100644 index 0000000000..4bc16ef83a --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__boolean_operation_py.snap @@ -0,0 +1,143 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +--- +## Input +```py +if ( + self._proc + # has the child process finished? + and self._returncode + # the child process has finished, but the + # transport hasn't been notified yet? + and self._proc.poll() +): + pass + +if ( + self._proc + and self._returncode + and self._proc.poll() + and self._proc + and self._returncode + and self._proc.poll() +): + ... + +if ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaaaaaaaa +): + ... + + +if ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaas + and aaaaaaaaaaaaaaaaa +): + ... + + +if [2222, 333] and [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] and [2222, 333]: + pass + +# Break right only applies for boolean operations with a left and right side +if ( + aaaaaaaaaaaaaaaaaaaaaaaaaa + and bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + and ccccccccccccccccc + and [dddddddddddddd, eeeeeeeeee, fffffffffffffff] +): + pass +``` + + + +## Output +```py +if ( + self.NOT_IMPLEMENTED_attr + # has the child process finished? + and self.NOT_IMPLEMENTED_attr + # the child process has finished, but the + # transport hasn't been notified yet? + and NOT_IMPLEMENTED_call() +): + pass + +if ( + self.NOT_IMPLEMENTED_attr + and self.NOT_IMPLEMENTED_attr + and NOT_IMPLEMENTED_call() + and self.NOT_IMPLEMENTED_attr + and self.NOT_IMPLEMENTED_attr + and NOT_IMPLEMENTED_call() +): + ... + +if ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaaaaaaaa +): + ... + + +if ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaas + and aaaaaaaaaaaaaaaaa +): + ... + + +if [2222, 333] and [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] and [2222, 333]: + pass + +# Break right only applies for boolean operations with a left and right side +if ( + aaaaaaaaaaaaaaaaaaaaaaaaaa + and bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + and ccccccccccccccccc + and [dddddddddddddd, eeeeeeeeee, fffffffffffffff] +): + pass +``` + + diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__while_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__while_py.snap index 804cc9303c..32b93d696d 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__while_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__while_py.snap @@ -61,12 +61,14 @@ while ( else: ... -while NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: # comment +while NOT_IMPLEMENTED_call() and anotherCondition or aThirdCondition: # comment NOT_IMPLEMENTED_call() while ( - NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 # trailing third condition + NOT_IMPLEMENTED_call() # trailing some condition + and anotherCondition + or aThirdCondition # trailing third condition ): # comment NOT_IMPLEMENTED_call() ``` From f551c9aad2ebe3b41ef993b219dcf6d30f8c7686 Mon Sep 17 00:00:00 2001 From: konstin Date: Wed, 21 Jun 2023 11:39:56 +0200 Subject: [PATCH 152/447] Unify benchmarking and profiling docs (#5145) This moves all docs about benchmarking and profiling into CONTRIBUTING.md by moving the readme of `ruff_benchmark` and adding more information on profiling. We need to somehow consolidate that documentation, but i'm not convinced that this is the best way (i tried subpages in mkdocs, but that didn't seem good either), so i'm happy to take suggestions. --- CONTRIBUTING.md | 131 ++++++++++++++++++++++++++++++-- crates/ruff_benchmark/README.md | 92 +--------------------- 2 files changed, 129 insertions(+), 94 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3721e8815b..43c143cf5f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ Welcome! We're happy to have you here. Thank you in advance for your contributio - [Example: Adding a new configuration option](#example-adding-a-new-configuration-option) - [MkDocs](#mkdocs) - [Release Process](#release-process) -- [Benchmarks](#benchmarks) +- [Benchmarks](#benchmarking-and-profiling) ## The Basics @@ -307,7 +307,15 @@ downloading the [`known-github-tomls.json`](https://github.com/akx/ruff-usage-ag as `github_search.jsonl` and following the instructions in [scripts/Dockerfile.ecosystem](https://github.com/astral-sh/ruff/blob/main/scripts/Dockerfile.ecosystem). Note that this check will take a while to run. -## Benchmarks +## Benchmarking and Profiling + +We have several ways of benchmarking and profiling Ruff: + +- Our main performance benchmark comparing Ruff with other tools on the CPython codebase +- Microbenchmarks which the linter or the formatter on individual files. There run on pull requests. +- Profiling the linter on either the microbenchmarks or entire projects + +### CPython Benchmark First, clone [CPython](https://github.com/python/cpython). It's a large and diverse Python codebase, which makes it a good target for benchmarking. @@ -386,9 +394,9 @@ Summary 159.43 ± 2.48 times faster than 'pycodestyle crates/ruff/resources/test/cpython' ``` -You can run `poetry install` from `./scripts` to create a working environment for the above. All -reported benchmarks were computed using the versions specified by `./scripts/pyproject.toml` -on Python 3.11. +You can run `poetry install` from `./scripts/benchmarks` to create a working environment for the +above. All reported benchmarks were computed using the versions specified by +`./scripts/benchmarks/pyproject.toml` on Python 3.11. To benchmark Pylint, remove the following files from the CPython repository: @@ -429,3 +437,116 @@ Benchmark 1: find . -type f -name "*.py" | xargs -P 0 pyupgrade --py311-plus Time (mean ± σ): 30.119 s ± 0.195 s [User: 28.638 s, System: 0.390 s] Range (min … max): 29.813 s … 30.356 s 10 runs ``` + +## Microbenchmarks + +The `ruff_benchmark` crate benchmarks the linter and the formatter on individual files. + +You can run the benchmarks with + +```shell +cargo benchmark +``` + +### Benchmark driven Development + +Ruff uses [Criterion.rs](https://bheisler.github.io/criterion.rs/book/) for benchmarks. You can use +`--save-baseline=` to store an initial baseline benchmark (e.g. on `main`) and then use +`--benchmark=` to compare against that benchmark. Criterion will print a message telling you +if the benchmark improved/regressed compared to that baseline. + +```shell +# Run once on your "baseline" code +cargo benchmark --save-baseline=main + +# Then iterate with +cargo benchmark --baseline=main +``` + +### PR Summary + +You can use `--save-baseline` and `critcmp` to get a pretty comparison between two recordings. +This is useful to illustrate the improvements of a PR. + +```shell +# On main +cargo benchmark --save-baseline=main + +# After applying your changes +cargo benchmark --save-baseline=pr + +critcmp main pr +``` + +You must install [`critcmp`](https://github.com/BurntSushi/critcmp) for the comparison. + +```bash +cargo install critcmp +``` + +### Tips + +- Use `cargo benchmark ` to only run specific benchmarks. For example: `cargo benchmark linter/pydantic` + to only run the pydantic tests. +- Use `cargo benchmark --quiet` for a more cleaned up output (without statistical relevance) +- Use `cargo benchmark --quick` to get faster results (more prone to noise) + +## Profiling Projects + +You can either use the microbenchmarks from above or a project directory for benchmarking. There +are a lot of profiling tools out there, +[The Rust Performance Book](https://nnethercote.github.io/perf-book/profiling.html) lists some +examples. + +### Linux + +Install `perf` and build `ruff_benchmark` with the `release-debug` profile and then run it with perf + +```shell +cargo bench -p ruff_benchmark --no-run --profile=release-debug && perf record -g -F 9999 cargo bench -p ruff_benchmark --profile=release-debug -- --profile-time=1 +``` + +You can also use the `ruff_dev` launcher to run `ruff check` multiple times on a repository to +gather enough samples for a good flamegraph (change the 999, the sample rate, and the 30, the number +of checks, to your liking) + +```shell +cargo build --bin ruff_dev --profile=release-debug +perf record -g -F 999 target/release-debug/ruff_dev repeat --repeat 30 --exit-zero --no-cache path/to/cpython > /dev/null +``` + +Then convert the recorded profile + +```shell +perf script -F +pid > /tmp/test.perf +``` + +You can now view the converted file with [firefox profiler](https://profiler.firefox.com/), with a +more in-depth guide [here](https://profiler.firefox.com/docs/#/./guide-perf-profiling) + +An alternative is to convert the perf data to `flamegraph.svg` using +[flamegraph](https://github.com/flamegraph-rs/flamegraph) (`cargo install flamegraph`): + +```shell +flamegraph --perfdata perf.data +``` + +### Mac + +Install [`cargo-instruments`](https://crates.io/crates/cargo-instruments): + +```shell +cargo install cargo-instruments +``` + +Then run the profiler with + +```shell +cargo instruments -t time --bench linter --profile release-debug -p ruff_benchmark -- --profile-time=1 +``` + +- `-t`: Specifies what to profile. Useful options are `time` to profile the wall time and `alloc` + for profiling the allocations. +- You may want to pass an additional filter to run a single test file + +Otherwise, follow the instructions from the linux section. diff --git a/crates/ruff_benchmark/README.md b/crates/ruff_benchmark/README.md index 3151b03b95..02a683b19e 100644 --- a/crates/ruff_benchmark/README.md +++ b/crates/ruff_benchmark/README.md @@ -1,91 +1,5 @@ -# Ruff Micro-benchmarks +# Ruff Benchmarks -Benchmarks for the different Ruff-tools. +The `ruff_benchmark` crate benchmarks the linter and the formatter on individual files. -## Run Benchmark - -You can run the benchmarks with - -```shell -cargo benchmark -``` - -## Benchmark driven Development - -You can use `--save-baseline=` to store an initial baseline benchmark (e.g. on `main`) and -then use `--benchmark=` to compare against that benchmark. Criterion will print a message -telling you if the benchmark improved/regressed compared to that baseline. - -```shell -# Run once on your "baseline" code -cargo benchmark --save-baseline=main - -# Then iterate with -cargo benchmark --baseline=main -``` - -## PR Summary - -You can use `--save-baseline` and `critcmp` to get a pretty comparison between two recordings. -This is useful to illustrate the improvements of a PR. - -```shell -# On main -cargo benchmark --save-baseline=main - -# After applying your changes -cargo benchmark --save-baseline=pr - -critcmp main pr -``` - -You must install [`critcmp`](https://github.com/BurntSushi/critcmp) for the comparison. - -```bash -cargo install critcmp -``` - -## Tips - -- Use `cargo benchmark ` to only run specific benchmarks. For example: `cargo benchmark linter/pydantic` - to only run the pydantic tests. -- Use `cargo benchmark --quiet` for a more cleaned up output (without statistical relevance) -- Use `cargo benchmark --quick` to get faster results (more prone to noise) - -## Profiling - -### Linux - -Install `perf` and build `ruff_benchmark` with the `release-debug` profile and then run it with perf - -```shell -cargo bench -p ruff_benchmark --no-run --profile=release-debug && perf record -g -F 9999 cargo bench -p ruff_benchmark --profile=release-debug -- --profile-time=1 -``` - -Then convert the recorded profile - -```shell -perf script -F +pid > /tmp/test.perf -``` - -You can now view the converted file with [firefox profiler](https://profiler.firefox.com/) - -You can find a more in-depth guide [here](https://profiler.firefox.com/docs/#/./guide-perf-profiling) - -### Mac - -Install [`cargo-instruments`](https://crates.io/crates/cargo-instruments): - -```shell -cargo install cargo-instruments -``` - -Then run the profiler with - -```shell -cargo instruments -t time --bench linter --profile release-debug -p ruff_benchmark -- --profile-time=1 -``` - -- `-t`: Specifies what to profile. Useful options are `time` to profile the wall time and `alloc` - for profiling the allocations. -- You may want to pass an additional filter to run a single test file +See [CONTRIBUTING.md](../../CONTRIBUTING.md) on how to use these benchmarks. From 44156f696247449375892864fe36ea7cb26763fa Mon Sep 17 00:00:00 2001 From: konstin Date: Wed, 21 Jun 2023 11:52:13 +0200 Subject: [PATCH 153/447] Improve debuggability of `place_comment` (#5209) ## Summary I found it hard to figure out which function decides placement for a specific comment. An explicit loop makes this easier to debug ## Test Plan There should be no functional changes, no changes to the formatting of the fixtures. --- .../src/comments/placement.rs | 49 ++++++++++++------- .../src/comments/visitor.rs | 12 ----- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index a467263e09..a1f9fa2cae 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -14,25 +14,30 @@ use crate::trivia::{SimpleTokenizer, Token, TokenKind}; /// Implements the custom comment placement logic. pub(super) fn place_comment<'a>( - comment: DecoratedComment<'a>, + mut comment: DecoratedComment<'a>, locator: &Locator, ) -> CommentPlacement<'a> { - handle_in_between_except_handlers_or_except_handler_and_else_or_finally_comment( - comment, locator, - ) - .or_else(|comment| handle_match_comment(comment, locator)) - .or_else(|comment| handle_in_between_bodies_own_line_comment(comment, locator)) - .or_else(|comment| handle_in_between_bodies_end_of_line_comment(comment, locator)) - .or_else(|comment| handle_trailing_body_comment(comment, locator)) - .or_else(handle_trailing_end_of_line_body_comment) - .or_else(|comment| handle_trailing_end_of_line_condition_comment(comment, locator)) - .or_else(|comment| { - handle_module_level_own_line_comment_before_class_or_function_comment(comment, locator) - }) - .or_else(|comment| handle_positional_only_arguments_separator_comment(comment, locator)) - .or_else(|comment| handle_trailing_binary_expression_left_or_operator_comment(comment, locator)) - .or_else(handle_leading_function_with_decorators_comment) - .or_else(|comment| handle_dict_unpacking_comment(comment, locator)) + static HANDLERS: &[for<'a> fn(DecoratedComment<'a>, &Locator) -> CommentPlacement<'a>] = &[ + handle_in_between_except_handlers_or_except_handler_and_else_or_finally_comment, + handle_match_comment, + handle_in_between_bodies_own_line_comment, + handle_in_between_bodies_end_of_line_comment, + handle_trailing_body_comment, + handle_trailing_end_of_line_body_comment, + handle_trailing_end_of_line_condition_comment, + handle_module_level_own_line_comment_before_class_or_function_comment, + handle_positional_only_arguments_separator_comment, + handle_trailing_binary_expression_left_or_operator_comment, + handle_leading_function_with_decorators_comment, + handle_dict_unpacking_comment, + ]; + for handler in HANDLERS { + comment = match handler(comment, locator) { + CommentPlacement::Default(comment) => comment, + placement => return placement, + }; + } + CommentPlacement::Default(comment) } /// Handles leading comments in front of a match case or a trailing comment of the `match` statement. @@ -483,7 +488,10 @@ fn handle_trailing_body_comment<'a>( /// if something.changed: /// do.stuff() # trailing comment /// ``` -fn handle_trailing_end_of_line_body_comment(comment: DecoratedComment<'_>) -> CommentPlacement<'_> { +fn handle_trailing_end_of_line_body_comment<'a>( + comment: DecoratedComment<'a>, + _locator: &Locator, +) -> CommentPlacement<'a> { // Must be an end of line comment if comment.line_position().is_own_line() { return CommentPlacement::Default(comment); @@ -868,7 +876,10 @@ fn find_pos_only_slash_offset( /// def test(): /// ... /// ``` -fn handle_leading_function_with_decorators_comment(comment: DecoratedComment) -> CommentPlacement { +fn handle_leading_function_with_decorators_comment<'a>( + comment: DecoratedComment<'a>, + _locator: &Locator, +) -> CommentPlacement<'a> { let is_preceding_decorator = comment .preceding_node() .map_or(false, |node| node.is_decorator()); diff --git a/crates/ruff_python_formatter/src/comments/visitor.rs b/crates/ruff_python_formatter/src/comments/visitor.rs index 6102240a40..828cf6e10e 100644 --- a/crates/ruff_python_formatter/src/comments/visitor.rs +++ b/crates/ruff_python_formatter/src/comments/visitor.rs @@ -615,18 +615,6 @@ impl<'a> CommentPlacement<'a> { comment: comment.into(), } } - - /// Returns the placement if it isn't [`CommentPlacement::Default`], otherwise calls `f` and returns the result. - #[inline] - pub(super) fn or_else(self, f: F) -> Self - where - F: FnOnce(DecoratedComment<'a>) -> CommentPlacement<'a>, - { - match self { - CommentPlacement::Default(comment) => f(comment), - placement => placement, - } - } } #[derive(Copy, Clone, Eq, PartialEq, Debug)] From 10885d09a1de8e1606b3b9d684da51c688787ade Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 21 Jun 2023 10:23:37 -0400 Subject: [PATCH 154/447] Add support for top-level quoted annotations in RUF013 (#5235) ## Summary This PR adds support for autofixing annotations like: ```python def f(x: "int" = None): ... ``` However, we don't yet support nested quotes, like: ```python def f(x: Union["int", "str"] = None): ... ``` Closes #5231. --- .../resources/test/fixtures/ruff/RUF013_0.py | 50 ++++-- .../src/rules/ruff/rules/implicit_optional.rs | 56 ++++-- ..._ruff__tests__PY39_RUF013_RUF013_0.py.snap | 170 +++++++++++------- ...ules__ruff__tests__RUF013_RUF013_0.py.snap | 170 +++++++++++------- 4 files changed, 288 insertions(+), 158 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py b/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py index 527fe792ee..acb0e82cd3 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py @@ -18,19 +18,19 @@ def f(arg: object = None): pass -def f(arg: int = None): # RUF011 +def f(arg: int = None): # RUF013 pass -def f(arg: str = None): # RUF011 +def f(arg: str = None): # RUF013 pass -def f(arg: typing.List[str] = None): # RUF011 +def f(arg: typing.List[str] = None): # RUF013 pass -def f(arg: Tuple[str] = None): # RUF011 +def f(arg: Tuple[str] = None): # RUF013 pass @@ -64,15 +64,15 @@ def f(arg: Union[int, str, Any] = None): pass -def f(arg: Union = None): # RUF011 +def f(arg: Union = None): # RUF013 pass -def f(arg: Union[int, str] = None): # RUF011 +def f(arg: Union[int, str] = None): # RUF013 pass -def f(arg: typing.Union[int, str] = None): # RUF011 +def f(arg: typing.Union[int, str] = None): # RUF013 pass @@ -91,11 +91,11 @@ def f(arg: int | float | str | None = None): pass -def f(arg: int | float = None): # RUF011 +def f(arg: int | float = None): # RUF013 pass -def f(arg: int | float | str | bytes = None): # RUF011 +def f(arg: int | float | str | bytes = None): # RUF013 pass @@ -110,11 +110,11 @@ def f(arg: Literal[1, 2, None, 3] = None): pass -def f(arg: Literal[1, "foo"] = None): # RUF011 +def f(arg: Literal[1, "foo"] = None): # RUF013 pass -def f(arg: typing.Literal[1, "foo", True] = None): # RUF011 +def f(arg: typing.Literal[1, "foo", True] = None): # RUF013 pass @@ -133,11 +133,11 @@ def f(arg: Annotated[Any, ...] = None): pass -def f(arg: Annotated[int, ...] = None): # RUF011 +def f(arg: Annotated[int, ...] = None): # RUF013 pass -def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF011 +def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF013 pass @@ -153,9 +153,9 @@ def f( def f( - arg1: int = None, # RUF011 - arg2: Union[int, float] = None, # RUF011 - arg3: Literal[1, 2, 3] = None, # RUF011 + arg1: int = None, # RUF013 + arg2: Union[int, float] = None, # RUF013 + arg3: Literal[1, 2, 3] = None, # RUF013 ): pass @@ -183,20 +183,32 @@ def f(arg: Union[Annotated[int, ...], Annotated[Optional[float], ...]] = None): pass -def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF011 +def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF013 pass # Quoted -def f(arg: "int" = None): +def f(arg: "int" = None): # RUF013 pass -def f(arg: "str" = None): +def f(arg: "str" = None): # RUF013 + pass + + +def f(arg: "st" "r" = None): # RUF013 pass def f(arg: "Optional[int]" = None): pass + + +def f(arg: Union["int", "str"] = None): # False negative + pass + + +def f(arg: Union["int", "None"] = None): + pass diff --git a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs index dd0d5a7721..e8702f9620 100644 --- a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs +++ b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs @@ -4,9 +4,10 @@ use anyhow::Result; use ruff_text_size::TextRange; use rustpython_parser::ast::{self, ArgWithDefault, Arguments, Constant, Expr, Operator, Ranged}; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::is_const_none; +use ruff_python_ast::typing::parse_type_annotation; use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; @@ -65,14 +66,16 @@ pub struct ImplicitOptional { conversion_type: ConversionType, } -impl AlwaysAutofixableViolation for ImplicitOptional { +impl Violation for ImplicitOptional { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { format!("PEP 484 prohibits implicit `Optional`") } - fn autofix_title(&self) -> String { - format!("Convert to `{}`", self.conversion_type) + fn autofix_title(&self) -> Option { + Some(format!("Convert to `{}`", self.conversion_type)) } } @@ -229,7 +232,7 @@ impl<'a> TypingTarget<'a> { _ => new_target.contains_none(semantic), } } - // TODO(charlie): Add support for forward references (quoted annotations). + // TODO(charlie): Add support for nested forward references (e.g., `Union["A", "B"]`). TypingTarget::ForwardReference => true, } } @@ -333,15 +336,42 @@ pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) { let Some(annotation) = &def.annotation else { continue }; - let Some(expr) = type_hint_explicitly_allows_none(annotation, checker.semantic()) else { - continue; - }; - let conversion_type = checker.settings.target_version.into(); - let mut diagnostic = Diagnostic::new(ImplicitOptional { conversion_type }, expr.range()); - if checker.patch(diagnostic.kind.rule()) { - diagnostic.try_set_fix(|| generate_fix(checker, conversion_type, expr)); + if let Expr::Constant(ast::ExprConstant { + range, + value: Constant::Str(value), + .. + }) = annotation.as_ref() + { + // Quoted annotation. + if let Ok((annotation, kind)) = parse_type_annotation(value, *range, checker.locator) { + let Some(expr) = type_hint_explicitly_allows_none(&annotation, checker.semantic()) else { + continue; + }; + let conversion_type = checker.settings.target_version.into(); + + let mut diagnostic = + Diagnostic::new(ImplicitOptional { conversion_type }, expr.range()); + if checker.patch(diagnostic.kind.rule()) { + if kind.is_simple() { + diagnostic.try_set_fix(|| generate_fix(checker, conversion_type, expr)); + } + } + checker.diagnostics.push(diagnostic); + } + } else { + // Unquoted annotation. + let Some(expr) = type_hint_explicitly_allows_none(annotation, checker.semantic()) else { + continue; + }; + let conversion_type = checker.settings.target_version.into(); + + let mut diagnostic = + Diagnostic::new(ImplicitOptional { conversion_type }, expr.range()); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.try_set_fix(|| generate_fix(checker, conversion_type, expr)); + } + checker.diagnostics.push(diagnostic); } - checker.diagnostics.push(diagnostic); } } diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap index 72f5f885b6..323513456c 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap @@ -3,7 +3,7 @@ source: crates/ruff/src/rules/ruff/mod.rs --- RUF013_0.py:21:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -21 | def f(arg: int = None): # RUF011 +21 | def f(arg: int = None): # RUF013 | ^^^ RUF013 22 | pass | @@ -13,15 +13,15 @@ RUF013_0.py:21:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 18 18 | pass 19 19 | 20 20 | -21 |-def f(arg: int = None): # RUF011 - 21 |+def f(arg: Optional[int] = None): # RUF011 +21 |-def f(arg: int = None): # RUF013 + 21 |+def f(arg: Optional[int] = None): # RUF013 22 22 | pass 23 23 | 24 24 | RUF013_0.py:25:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -25 | def f(arg: str = None): # RUF011 +25 | def f(arg: str = None): # RUF013 | ^^^ RUF013 26 | pass | @@ -31,15 +31,15 @@ RUF013_0.py:25:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 22 22 | pass 23 23 | 24 24 | -25 |-def f(arg: str = None): # RUF011 - 25 |+def f(arg: Optional[str] = None): # RUF011 +25 |-def f(arg: str = None): # RUF013 + 25 |+def f(arg: Optional[str] = None): # RUF013 26 26 | pass 27 27 | 28 28 | RUF013_0.py:29:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -29 | def f(arg: typing.List[str] = None): # RUF011 +29 | def f(arg: typing.List[str] = None): # RUF013 | ^^^^^^^^^^^^^^^^ RUF013 30 | pass | @@ -49,15 +49,15 @@ RUF013_0.py:29:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 26 26 | pass 27 27 | 28 28 | -29 |-def f(arg: typing.List[str] = None): # RUF011 - 29 |+def f(arg: Optional[typing.List[str]] = None): # RUF011 +29 |-def f(arg: typing.List[str] = None): # RUF013 + 29 |+def f(arg: Optional[typing.List[str]] = None): # RUF013 30 30 | pass 31 31 | 32 32 | RUF013_0.py:33:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -33 | def f(arg: Tuple[str] = None): # RUF011 +33 | def f(arg: Tuple[str] = None): # RUF013 | ^^^^^^^^^^ RUF013 34 | pass | @@ -67,15 +67,15 @@ RUF013_0.py:33:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 30 30 | pass 31 31 | 32 32 | -33 |-def f(arg: Tuple[str] = None): # RUF011 - 33 |+def f(arg: Optional[Tuple[str]] = None): # RUF011 +33 |-def f(arg: Tuple[str] = None): # RUF013 + 33 |+def f(arg: Optional[Tuple[str]] = None): # RUF013 34 34 | pass 35 35 | 36 36 | RUF013_0.py:67:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -67 | def f(arg: Union = None): # RUF011 +67 | def f(arg: Union = None): # RUF013 | ^^^^^ RUF013 68 | pass | @@ -85,15 +85,15 @@ RUF013_0.py:67:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 64 64 | pass 65 65 | 66 66 | -67 |-def f(arg: Union = None): # RUF011 - 67 |+def f(arg: Optional[Union] = None): # RUF011 +67 |-def f(arg: Union = None): # RUF013 + 67 |+def f(arg: Optional[Union] = None): # RUF013 68 68 | pass 69 69 | 70 70 | RUF013_0.py:71:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -71 | def f(arg: Union[int, str] = None): # RUF011 +71 | def f(arg: Union[int, str] = None): # RUF013 | ^^^^^^^^^^^^^^^ RUF013 72 | pass | @@ -103,15 +103,15 @@ RUF013_0.py:71:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 68 68 | pass 69 69 | 70 70 | -71 |-def f(arg: Union[int, str] = None): # RUF011 - 71 |+def f(arg: Optional[Union[int, str]] = None): # RUF011 +71 |-def f(arg: Union[int, str] = None): # RUF013 + 71 |+def f(arg: Optional[Union[int, str]] = None): # RUF013 72 72 | pass 73 73 | 74 74 | RUF013_0.py:75:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -75 | def f(arg: typing.Union[int, str] = None): # RUF011 +75 | def f(arg: typing.Union[int, str] = None): # RUF013 | ^^^^^^^^^^^^^^^^^^^^^^ RUF013 76 | pass | @@ -121,15 +121,15 @@ RUF013_0.py:75:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 72 72 | pass 73 73 | 74 74 | -75 |-def f(arg: typing.Union[int, str] = None): # RUF011 - 75 |+def f(arg: Optional[typing.Union[int, str]] = None): # RUF011 +75 |-def f(arg: typing.Union[int, str] = None): # RUF013 + 75 |+def f(arg: Optional[typing.Union[int, str]] = None): # RUF013 76 76 | pass 77 77 | 78 78 | RUF013_0.py:94:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -94 | def f(arg: int | float = None): # RUF011 +94 | def f(arg: int | float = None): # RUF013 | ^^^^^^^^^^^ RUF013 95 | pass | @@ -139,15 +139,15 @@ RUF013_0.py:94:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 91 91 | pass 92 92 | 93 93 | -94 |-def f(arg: int | float = None): # RUF011 - 94 |+def f(arg: Optional[int | float] = None): # RUF011 +94 |-def f(arg: int | float = None): # RUF013 + 94 |+def f(arg: Optional[int | float] = None): # RUF013 95 95 | pass 96 96 | 97 97 | RUF013_0.py:98:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -98 | def f(arg: int | float | str | bytes = None): # RUF011 +98 | def f(arg: int | float | str | bytes = None): # RUF013 | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013 99 | pass | @@ -157,15 +157,15 @@ RUF013_0.py:98:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 95 95 | pass 96 96 | 97 97 | -98 |-def f(arg: int | float | str | bytes = None): # RUF011 - 98 |+def f(arg: Optional[int | float | str | bytes] = None): # RUF011 +98 |-def f(arg: int | float | str | bytes = None): # RUF013 + 98 |+def f(arg: Optional[int | float | str | bytes] = None): # RUF013 99 99 | pass 100 100 | 101 101 | RUF013_0.py:113:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -113 | def f(arg: Literal[1, "foo"] = None): # RUF011 +113 | def f(arg: Literal[1, "foo"] = None): # RUF013 | ^^^^^^^^^^^^^^^^^ RUF013 114 | pass | @@ -175,15 +175,15 @@ RUF013_0.py:113:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 110 110 | pass 111 111 | 112 112 | -113 |-def f(arg: Literal[1, "foo"] = None): # RUF011 - 113 |+def f(arg: Optional[Literal[1, "foo"]] = None): # RUF011 +113 |-def f(arg: Literal[1, "foo"] = None): # RUF013 + 113 |+def f(arg: Optional[Literal[1, "foo"]] = None): # RUF013 114 114 | pass 115 115 | 116 116 | RUF013_0.py:117:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -117 | def f(arg: typing.Literal[1, "foo", True] = None): # RUF011 +117 | def f(arg: typing.Literal[1, "foo", True] = None): # RUF013 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013 118 | pass | @@ -193,15 +193,15 @@ RUF013_0.py:117:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 114 114 | pass 115 115 | 116 116 | -117 |-def f(arg: typing.Literal[1, "foo", True] = None): # RUF011 - 117 |+def f(arg: Optional[typing.Literal[1, "foo", True]] = None): # RUF011 +117 |-def f(arg: typing.Literal[1, "foo", True] = None): # RUF013 + 117 |+def f(arg: Optional[typing.Literal[1, "foo", True]] = None): # RUF013 118 118 | pass 119 119 | 120 120 | RUF013_0.py:136:22: RUF013 [*] PEP 484 prohibits implicit `Optional` | -136 | def f(arg: Annotated[int, ...] = None): # RUF011 +136 | def f(arg: Annotated[int, ...] = None): # RUF013 | ^^^ RUF013 137 | pass | @@ -211,15 +211,15 @@ RUF013_0.py:136:22: RUF013 [*] PEP 484 prohibits implicit `Optional` 133 133 | pass 134 134 | 135 135 | -136 |-def f(arg: Annotated[int, ...] = None): # RUF011 - 136 |+def f(arg: Annotated[Optional[int], ...] = None): # RUF011 +136 |-def f(arg: Annotated[int, ...] = None): # RUF013 + 136 |+def f(arg: Annotated[Optional[int], ...] = None): # RUF013 137 137 | pass 138 138 | 139 139 | RUF013_0.py:140:32: RUF013 [*] PEP 484 prohibits implicit `Optional` | -140 | def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF011 +140 | def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF013 | ^^^^^^^^^ RUF013 141 | pass | @@ -229,8 +229,8 @@ RUF013_0.py:140:32: RUF013 [*] PEP 484 prohibits implicit `Optional` 137 137 | pass 138 138 | 139 139 | -140 |-def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF011 - 140 |+def f(arg: Annotated[Annotated[Optional[int | str], ...], ...] = None): # RUF011 +140 |-def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF013 + 140 |+def f(arg: Annotated[Annotated[Optional[int | str], ...], ...] = None): # RUF013 141 141 | pass 142 142 | 143 143 | @@ -238,10 +238,10 @@ RUF013_0.py:140:32: RUF013 [*] PEP 484 prohibits implicit `Optional` RUF013_0.py:156:11: RUF013 [*] PEP 484 prohibits implicit `Optional` | 155 | def f( -156 | arg1: int = None, # RUF011 +156 | arg1: int = None, # RUF013 | ^^^ RUF013 -157 | arg2: Union[int, float] = None, # RUF011 -158 | arg3: Literal[1, 2, 3] = None, # RUF011 +157 | arg2: Union[int, float] = None, # RUF013 +158 | arg3: Literal[1, 2, 3] = None, # RUF013 | = help: Convert to `Optional[T]` @@ -249,19 +249,19 @@ RUF013_0.py:156:11: RUF013 [*] PEP 484 prohibits implicit `Optional` 153 153 | 154 154 | 155 155 | def f( -156 |- arg1: int = None, # RUF011 - 156 |+ arg1: Optional[int] = None, # RUF011 -157 157 | arg2: Union[int, float] = None, # RUF011 -158 158 | arg3: Literal[1, 2, 3] = None, # RUF011 +156 |- arg1: int = None, # RUF013 + 156 |+ arg1: Optional[int] = None, # RUF013 +157 157 | arg2: Union[int, float] = None, # RUF013 +158 158 | arg3: Literal[1, 2, 3] = None, # RUF013 159 159 | ): RUF013_0.py:157:11: RUF013 [*] PEP 484 prohibits implicit `Optional` | 155 | def f( -156 | arg1: int = None, # RUF011 -157 | arg2: Union[int, float] = None, # RUF011 +156 | arg1: int = None, # RUF013 +157 | arg2: Union[int, float] = None, # RUF013 | ^^^^^^^^^^^^^^^^^ RUF013 -158 | arg3: Literal[1, 2, 3] = None, # RUF011 +158 | arg3: Literal[1, 2, 3] = None, # RUF013 159 | ): | = help: Convert to `Optional[T]` @@ -269,18 +269,18 @@ RUF013_0.py:157:11: RUF013 [*] PEP 484 prohibits implicit `Optional` ℹ Suggested fix 154 154 | 155 155 | def f( -156 156 | arg1: int = None, # RUF011 -157 |- arg2: Union[int, float] = None, # RUF011 - 157 |+ arg2: Optional[Union[int, float]] = None, # RUF011 -158 158 | arg3: Literal[1, 2, 3] = None, # RUF011 +156 156 | arg1: int = None, # RUF013 +157 |- arg2: Union[int, float] = None, # RUF013 + 157 |+ arg2: Optional[Union[int, float]] = None, # RUF013 +158 158 | arg3: Literal[1, 2, 3] = None, # RUF013 159 159 | ): 160 160 | pass RUF013_0.py:158:11: RUF013 [*] PEP 484 prohibits implicit `Optional` | -156 | arg1: int = None, # RUF011 -157 | arg2: Union[int, float] = None, # RUF011 -158 | arg3: Literal[1, 2, 3] = None, # RUF011 +156 | arg1: int = None, # RUF013 +157 | arg2: Union[int, float] = None, # RUF013 +158 | arg3: Literal[1, 2, 3] = None, # RUF013 | ^^^^^^^^^^^^^^^^ RUF013 159 | ): 160 | pass @@ -289,17 +289,17 @@ RUF013_0.py:158:11: RUF013 [*] PEP 484 prohibits implicit `Optional` ℹ Suggested fix 155 155 | def f( -156 156 | arg1: int = None, # RUF011 -157 157 | arg2: Union[int, float] = None, # RUF011 -158 |- arg3: Literal[1, 2, 3] = None, # RUF011 - 158 |+ arg3: Optional[Literal[1, 2, 3]] = None, # RUF011 +156 156 | arg1: int = None, # RUF013 +157 157 | arg2: Union[int, float] = None, # RUF013 +158 |- arg3: Literal[1, 2, 3] = None, # RUF013 + 158 |+ arg3: Optional[Literal[1, 2, 3]] = None, # RUF013 159 159 | ): 160 160 | pass 161 161 | RUF013_0.py:186:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -186 | def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF011 +186 | def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF013 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013 187 | pass | @@ -309,10 +309,54 @@ RUF013_0.py:186:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 183 183 | pass 184 184 | 185 185 | -186 |-def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF011 - 186 |+def f(arg: Optional[Union[Annotated[int, ...], Union[str, bytes]]] = None): # RUF011 +186 |-def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF013 + 186 |+def f(arg: Optional[Union[Annotated[int, ...], Union[str, bytes]]] = None): # RUF013 187 187 | pass 188 188 | 189 189 | +RUF013_0.py:193:13: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +193 | def f(arg: "int" = None): # RUF013 + | ^^^ RUF013 +194 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +190 190 | # Quoted +191 191 | +192 192 | +193 |-def f(arg: "int" = None): # RUF013 + 193 |+def f(arg: "Optional[int]" = None): # RUF013 +194 194 | pass +195 195 | +196 196 | + +RUF013_0.py:197:13: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +197 | def f(arg: "str" = None): # RUF013 + | ^^^ RUF013 +198 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +194 194 | pass +195 195 | +196 196 | +197 |-def f(arg: "str" = None): # RUF013 + 197 |+def f(arg: "Optional[str]" = None): # RUF013 +198 198 | pass +199 199 | +200 200 | + +RUF013_0.py:201:12: RUF013 PEP 484 prohibits implicit `Optional` + | +201 | def f(arg: "st" "r" = None): # RUF013 + | ^^^^^^^^ RUF013 +202 | pass + | + = help: Convert to `Optional[T]` + diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap index 7ec7c527d2..25ffc55a4d 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap @@ -3,7 +3,7 @@ source: crates/ruff/src/rules/ruff/mod.rs --- RUF013_0.py:21:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -21 | def f(arg: int = None): # RUF011 +21 | def f(arg: int = None): # RUF013 | ^^^ RUF013 22 | pass | @@ -13,15 +13,15 @@ RUF013_0.py:21:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 18 18 | pass 19 19 | 20 20 | -21 |-def f(arg: int = None): # RUF011 - 21 |+def f(arg: int | None = None): # RUF011 +21 |-def f(arg: int = None): # RUF013 + 21 |+def f(arg: int | None = None): # RUF013 22 22 | pass 23 23 | 24 24 | RUF013_0.py:25:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -25 | def f(arg: str = None): # RUF011 +25 | def f(arg: str = None): # RUF013 | ^^^ RUF013 26 | pass | @@ -31,15 +31,15 @@ RUF013_0.py:25:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 22 22 | pass 23 23 | 24 24 | -25 |-def f(arg: str = None): # RUF011 - 25 |+def f(arg: str | None = None): # RUF011 +25 |-def f(arg: str = None): # RUF013 + 25 |+def f(arg: str | None = None): # RUF013 26 26 | pass 27 27 | 28 28 | RUF013_0.py:29:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -29 | def f(arg: typing.List[str] = None): # RUF011 +29 | def f(arg: typing.List[str] = None): # RUF013 | ^^^^^^^^^^^^^^^^ RUF013 30 | pass | @@ -49,15 +49,15 @@ RUF013_0.py:29:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 26 26 | pass 27 27 | 28 28 | -29 |-def f(arg: typing.List[str] = None): # RUF011 - 29 |+def f(arg: typing.List[str] | None = None): # RUF011 +29 |-def f(arg: typing.List[str] = None): # RUF013 + 29 |+def f(arg: typing.List[str] | None = None): # RUF013 30 30 | pass 31 31 | 32 32 | RUF013_0.py:33:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -33 | def f(arg: Tuple[str] = None): # RUF011 +33 | def f(arg: Tuple[str] = None): # RUF013 | ^^^^^^^^^^ RUF013 34 | pass | @@ -67,15 +67,15 @@ RUF013_0.py:33:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 30 30 | pass 31 31 | 32 32 | -33 |-def f(arg: Tuple[str] = None): # RUF011 - 33 |+def f(arg: Tuple[str] | None = None): # RUF011 +33 |-def f(arg: Tuple[str] = None): # RUF013 + 33 |+def f(arg: Tuple[str] | None = None): # RUF013 34 34 | pass 35 35 | 36 36 | RUF013_0.py:67:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -67 | def f(arg: Union = None): # RUF011 +67 | def f(arg: Union = None): # RUF013 | ^^^^^ RUF013 68 | pass | @@ -85,15 +85,15 @@ RUF013_0.py:67:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 64 64 | pass 65 65 | 66 66 | -67 |-def f(arg: Union = None): # RUF011 - 67 |+def f(arg: Union | None = None): # RUF011 +67 |-def f(arg: Union = None): # RUF013 + 67 |+def f(arg: Union | None = None): # RUF013 68 68 | pass 69 69 | 70 70 | RUF013_0.py:71:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -71 | def f(arg: Union[int, str] = None): # RUF011 +71 | def f(arg: Union[int, str] = None): # RUF013 | ^^^^^^^^^^^^^^^ RUF013 72 | pass | @@ -103,15 +103,15 @@ RUF013_0.py:71:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 68 68 | pass 69 69 | 70 70 | -71 |-def f(arg: Union[int, str] = None): # RUF011 - 71 |+def f(arg: Union[int, str] | None = None): # RUF011 +71 |-def f(arg: Union[int, str] = None): # RUF013 + 71 |+def f(arg: Union[int, str] | None = None): # RUF013 72 72 | pass 73 73 | 74 74 | RUF013_0.py:75:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -75 | def f(arg: typing.Union[int, str] = None): # RUF011 +75 | def f(arg: typing.Union[int, str] = None): # RUF013 | ^^^^^^^^^^^^^^^^^^^^^^ RUF013 76 | pass | @@ -121,15 +121,15 @@ RUF013_0.py:75:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 72 72 | pass 73 73 | 74 74 | -75 |-def f(arg: typing.Union[int, str] = None): # RUF011 - 75 |+def f(arg: typing.Union[int, str] | None = None): # RUF011 +75 |-def f(arg: typing.Union[int, str] = None): # RUF013 + 75 |+def f(arg: typing.Union[int, str] | None = None): # RUF013 76 76 | pass 77 77 | 78 78 | RUF013_0.py:94:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -94 | def f(arg: int | float = None): # RUF011 +94 | def f(arg: int | float = None): # RUF013 | ^^^^^^^^^^^ RUF013 95 | pass | @@ -139,15 +139,15 @@ RUF013_0.py:94:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 91 91 | pass 92 92 | 93 93 | -94 |-def f(arg: int | float = None): # RUF011 - 94 |+def f(arg: int | float | None = None): # RUF011 +94 |-def f(arg: int | float = None): # RUF013 + 94 |+def f(arg: int | float | None = None): # RUF013 95 95 | pass 96 96 | 97 97 | RUF013_0.py:98:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -98 | def f(arg: int | float | str | bytes = None): # RUF011 +98 | def f(arg: int | float | str | bytes = None): # RUF013 | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013 99 | pass | @@ -157,15 +157,15 @@ RUF013_0.py:98:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 95 95 | pass 96 96 | 97 97 | -98 |-def f(arg: int | float | str | bytes = None): # RUF011 - 98 |+def f(arg: int | float | str | bytes | None = None): # RUF011 +98 |-def f(arg: int | float | str | bytes = None): # RUF013 + 98 |+def f(arg: int | float | str | bytes | None = None): # RUF013 99 99 | pass 100 100 | 101 101 | RUF013_0.py:113:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -113 | def f(arg: Literal[1, "foo"] = None): # RUF011 +113 | def f(arg: Literal[1, "foo"] = None): # RUF013 | ^^^^^^^^^^^^^^^^^ RUF013 114 | pass | @@ -175,15 +175,15 @@ RUF013_0.py:113:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 110 110 | pass 111 111 | 112 112 | -113 |-def f(arg: Literal[1, "foo"] = None): # RUF011 - 113 |+def f(arg: Literal[1, "foo"] | None = None): # RUF011 +113 |-def f(arg: Literal[1, "foo"] = None): # RUF013 + 113 |+def f(arg: Literal[1, "foo"] | None = None): # RUF013 114 114 | pass 115 115 | 116 116 | RUF013_0.py:117:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -117 | def f(arg: typing.Literal[1, "foo", True] = None): # RUF011 +117 | def f(arg: typing.Literal[1, "foo", True] = None): # RUF013 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013 118 | pass | @@ -193,15 +193,15 @@ RUF013_0.py:117:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 114 114 | pass 115 115 | 116 116 | -117 |-def f(arg: typing.Literal[1, "foo", True] = None): # RUF011 - 117 |+def f(arg: typing.Literal[1, "foo", True] | None = None): # RUF011 +117 |-def f(arg: typing.Literal[1, "foo", True] = None): # RUF013 + 117 |+def f(arg: typing.Literal[1, "foo", True] | None = None): # RUF013 118 118 | pass 119 119 | 120 120 | RUF013_0.py:136:22: RUF013 [*] PEP 484 prohibits implicit `Optional` | -136 | def f(arg: Annotated[int, ...] = None): # RUF011 +136 | def f(arg: Annotated[int, ...] = None): # RUF013 | ^^^ RUF013 137 | pass | @@ -211,15 +211,15 @@ RUF013_0.py:136:22: RUF013 [*] PEP 484 prohibits implicit `Optional` 133 133 | pass 134 134 | 135 135 | -136 |-def f(arg: Annotated[int, ...] = None): # RUF011 - 136 |+def f(arg: Annotated[int | None, ...] = None): # RUF011 +136 |-def f(arg: Annotated[int, ...] = None): # RUF013 + 136 |+def f(arg: Annotated[int | None, ...] = None): # RUF013 137 137 | pass 138 138 | 139 139 | RUF013_0.py:140:32: RUF013 [*] PEP 484 prohibits implicit `Optional` | -140 | def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF011 +140 | def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF013 | ^^^^^^^^^ RUF013 141 | pass | @@ -229,8 +229,8 @@ RUF013_0.py:140:32: RUF013 [*] PEP 484 prohibits implicit `Optional` 137 137 | pass 138 138 | 139 139 | -140 |-def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF011 - 140 |+def f(arg: Annotated[Annotated[int | str | None, ...], ...] = None): # RUF011 +140 |-def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF013 + 140 |+def f(arg: Annotated[Annotated[int | str | None, ...], ...] = None): # RUF013 141 141 | pass 142 142 | 143 143 | @@ -238,10 +238,10 @@ RUF013_0.py:140:32: RUF013 [*] PEP 484 prohibits implicit `Optional` RUF013_0.py:156:11: RUF013 [*] PEP 484 prohibits implicit `Optional` | 155 | def f( -156 | arg1: int = None, # RUF011 +156 | arg1: int = None, # RUF013 | ^^^ RUF013 -157 | arg2: Union[int, float] = None, # RUF011 -158 | arg3: Literal[1, 2, 3] = None, # RUF011 +157 | arg2: Union[int, float] = None, # RUF013 +158 | arg3: Literal[1, 2, 3] = None, # RUF013 | = help: Convert to `T | None` @@ -249,19 +249,19 @@ RUF013_0.py:156:11: RUF013 [*] PEP 484 prohibits implicit `Optional` 153 153 | 154 154 | 155 155 | def f( -156 |- arg1: int = None, # RUF011 - 156 |+ arg1: int | None = None, # RUF011 -157 157 | arg2: Union[int, float] = None, # RUF011 -158 158 | arg3: Literal[1, 2, 3] = None, # RUF011 +156 |- arg1: int = None, # RUF013 + 156 |+ arg1: int | None = None, # RUF013 +157 157 | arg2: Union[int, float] = None, # RUF013 +158 158 | arg3: Literal[1, 2, 3] = None, # RUF013 159 159 | ): RUF013_0.py:157:11: RUF013 [*] PEP 484 prohibits implicit `Optional` | 155 | def f( -156 | arg1: int = None, # RUF011 -157 | arg2: Union[int, float] = None, # RUF011 +156 | arg1: int = None, # RUF013 +157 | arg2: Union[int, float] = None, # RUF013 | ^^^^^^^^^^^^^^^^^ RUF013 -158 | arg3: Literal[1, 2, 3] = None, # RUF011 +158 | arg3: Literal[1, 2, 3] = None, # RUF013 159 | ): | = help: Convert to `T | None` @@ -269,18 +269,18 @@ RUF013_0.py:157:11: RUF013 [*] PEP 484 prohibits implicit `Optional` ℹ Suggested fix 154 154 | 155 155 | def f( -156 156 | arg1: int = None, # RUF011 -157 |- arg2: Union[int, float] = None, # RUF011 - 157 |+ arg2: Union[int, float] | None = None, # RUF011 -158 158 | arg3: Literal[1, 2, 3] = None, # RUF011 +156 156 | arg1: int = None, # RUF013 +157 |- arg2: Union[int, float] = None, # RUF013 + 157 |+ arg2: Union[int, float] | None = None, # RUF013 +158 158 | arg3: Literal[1, 2, 3] = None, # RUF013 159 159 | ): 160 160 | pass RUF013_0.py:158:11: RUF013 [*] PEP 484 prohibits implicit `Optional` | -156 | arg1: int = None, # RUF011 -157 | arg2: Union[int, float] = None, # RUF011 -158 | arg3: Literal[1, 2, 3] = None, # RUF011 +156 | arg1: int = None, # RUF013 +157 | arg2: Union[int, float] = None, # RUF013 +158 | arg3: Literal[1, 2, 3] = None, # RUF013 | ^^^^^^^^^^^^^^^^ RUF013 159 | ): 160 | pass @@ -289,17 +289,17 @@ RUF013_0.py:158:11: RUF013 [*] PEP 484 prohibits implicit `Optional` ℹ Suggested fix 155 155 | def f( -156 156 | arg1: int = None, # RUF011 -157 157 | arg2: Union[int, float] = None, # RUF011 -158 |- arg3: Literal[1, 2, 3] = None, # RUF011 - 158 |+ arg3: Literal[1, 2, 3] | None = None, # RUF011 +156 156 | arg1: int = None, # RUF013 +157 157 | arg2: Union[int, float] = None, # RUF013 +158 |- arg3: Literal[1, 2, 3] = None, # RUF013 + 158 |+ arg3: Literal[1, 2, 3] | None = None, # RUF013 159 159 | ): 160 160 | pass 161 161 | RUF013_0.py:186:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | -186 | def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF011 +186 | def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF013 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013 187 | pass | @@ -309,10 +309,54 @@ RUF013_0.py:186:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 183 183 | pass 184 184 | 185 185 | -186 |-def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF011 - 186 |+def f(arg: Union[Annotated[int, ...], Union[str, bytes]] | None = None): # RUF011 +186 |-def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF013 + 186 |+def f(arg: Union[Annotated[int, ...], Union[str, bytes]] | None = None): # RUF013 187 187 | pass 188 188 | 189 189 | +RUF013_0.py:193:13: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +193 | def f(arg: "int" = None): # RUF013 + | ^^^ RUF013 +194 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +190 190 | # Quoted +191 191 | +192 192 | +193 |-def f(arg: "int" = None): # RUF013 + 193 |+def f(arg: "int | None" = None): # RUF013 +194 194 | pass +195 195 | +196 196 | + +RUF013_0.py:197:13: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +197 | def f(arg: "str" = None): # RUF013 + | ^^^ RUF013 +198 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +194 194 | pass +195 195 | +196 196 | +197 |-def f(arg: "str" = None): # RUF013 + 197 |+def f(arg: "str | None" = None): # RUF013 +198 198 | pass +199 199 | +200 200 | + +RUF013_0.py:201:12: RUF013 PEP 484 prohibits implicit `Optional` + | +201 | def f(arg: "st" "r" = None): # RUF013 + | ^^^^^^^^ RUF013 +202 | pass + | + = help: Convert to `T | None` + From 4634560c8040b8ca68b8cdcdfe0d16dc2bea42c5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 21 Jun 2023 10:24:33 -0400 Subject: [PATCH 155/447] Ensure release tagging has access to repo clone (#5240) ## Summary The [release failed](https://github.com/astral-sh/ruff/actions/runs/5329733171/jobs/9656004063), but late enough that I was able to do the remaining steps manually. The issue here is that the tagging step requires that we clone the repo. I split the upload (to PyPI), tagging (in Git), and publishing (to GitHub Releases) phases into their own steps, since they need different resources + permissions anyway. --- .github/workflows/release.yaml | 39 ++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 699a6c295b..434ab1a9ff 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -423,8 +423,8 @@ jobs: echo "Releasing ${git_sha}" fi - release: - name: Release + upload-release: + name: Upload to PyPI runs-on: ubuntu-latest needs: - macos-universal @@ -442,8 +442,6 @@ jobs: permissions: # For pypi trusted publishing id-token: write - # For GitHub release publishing - contents: write steps: - uses: actions/download-artifact@v3 with: @@ -455,10 +453,18 @@ jobs: skip-existing: true packages-dir: wheels verbose: true - - uses: actions/download-artifact@v3 - with: - name: binaries - path: binaries + + tag-release: + name: Tag release + runs-on: ubuntu-latest + needs: upload-release + # If you don't set an input tag, it's a dry run (no uploads). + if: ${{ inputs.tag }} + permissions: + # For git tag + contents: write + steps: + - uses: actions/checkout@v3 - name: git tag run: | git config user.email "hey@astral.sh" @@ -467,10 +473,25 @@ jobs: # If there is duplicate tag, this will fail. The publish to pypi action will have been a noop (due to skip # existing), so we make a non-destructive exit here git push --tags + + publish-release: + name: Publish to GitHub + runs-on: ubuntu-latest + needs: tag-release + # If you don't set an input tag, it's a dry run (no uploads). + if: ${{ inputs.tag }} + permissions: + # For GitHub release publishing + contents: write + steps: + - uses: actions/download-artifact@v3 + with: + name: binaries + path: binaries - name: "Publish to GitHub" uses: softprops/action-gh-release@v1 with: - draft: true + draft: false files: binaries/* tag_name: v${{ inputs.tag }} From 6155fd647d2270171f9e4e4fd641c1fcd2145e90 Mon Sep 17 00:00:00 2001 From: konstin Date: Wed, 21 Jun 2023 17:09:39 +0200 Subject: [PATCH 156/447] Format Slice Expressions (#5047) This formats slice expressions and subscript expressions. Spaces around the colons follows the same rules as black (https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#slices): ```python e00 = "e"[:] e01 = "e"[:1] e02 = "e"[: a()] e10 = "e"[1:] e11 = "e"[1:1] e12 = "e"[1 : a()] e20 = "e"[a() :] e21 = "e"[a() : 1] e22 = "e"[a() : a()] e200 = "e"[a() : :] e201 = "e"[a() :: 1] e202 = "e"[a() :: a()] e210 = "e"[a() : 1 :] ``` Comment placement is different due to our very different infrastructure. If we have explicit bounds (e.g. `x[1:2]`) all comments get assigned as leading or trailing to the bound expression. If a bound is missing `[:]`, comments get marked as dangling and placed in the same section as they were originally in: ```python x = "x"[ # a # b : # c # d ] ``` to ```python x = "x"[ # a # b : # c # d ] ``` Except for the potential trailing end-of-line comments, all comments get formatted on their own line. This can be improved by keeping end-of-line comments after the opening bracket or after a colon as such but the changes were already complex enough. I added tests for comment placement and spaces. --- .../test/fixtures/ruff/expression/slice.py | 82 +++++ .../src/comments/placement.rs | 100 +++++- .../src/expression/expr_slice.rs | 260 +++++++++++++++- .../src/expression/expr_subscript.rs | 49 ++- .../src/expression/expr_unary_op.rs | 6 +- crates/ruff_python_formatter/src/lib.rs | 67 ++-- ...ttribute_access_on_number_literals_py.snap | 5 +- ...tter__tests__black_test__comments2_py.snap | 38 ++- ...tter__tests__black_test__comments6_py.snap | 9 +- ...ter__tests__black_test__expression_py.snap | 289 +++++++++--------- ...atter__tests__black_test__fmtonoff_py.snap | 37 ++- ...atter__tests__black_test__function_py.snap | 4 +- ...lack_test__function_trailing_comma_py.snap | 27 +- ..._black_test__one_element_subscript_py.snap | 30 +- ...ests__black_test__power_op_spacing_py.snap | 8 +- ...test__prefer_rhs_split_reformatted_py.snap | 4 +- ...k_test__return_annotation_brackets_py.snap | 47 +-- ...ck_test__skip_magic_trailing_comma_py.snap | 28 +- ...rmatter__tests__black_test__slices_py.snap | 211 ++++++------- ...__trailing_commas_in_leading_parts_py.snap | 14 +- ...ests__ruff_test__expression__slice_py.snap | 177 +++++++++++ crates/ruff_python_formatter/src/trivia.rs | 3 +- 22 files changed, 1065 insertions(+), 430 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/slice.py create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__slice_py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/slice.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/slice.py new file mode 100644 index 0000000000..7406108226 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/slice.py @@ -0,0 +1,82 @@ +# Handle comments both when lower and upper exist and when they don't +a1 = "a"[ + # a + 1 # b + : # c + 2 # d +] +a2 = "a"[ + # a + # b + : # c + # d +] + +# Check all places where comments can exist +b1 = "b"[ # a + # b + 1 # c + # d + : # e + # f + 2 # g + # h + : # i + # j + 3 # k + # l +] + +# Handle the spacing from the colon correctly with upper leading comments +c1 = "c"[ + 1 + : # e + # f + 2 +] +c2 = "c"[ + 1 + : # e + 2 +] +c3 = "c"[ + 1 + : + # f + 2 +] +c4 = "c"[ + 1 + : # f + 2 +] + +# End of line comments +d1 = "d"[ # comment + : +] +d2 = "d"[ # comment + 1: +] +d3 = "d"[ + 1 # comment + : +] + +# Spacing around the colon(s) +def a(): + ... + +e00 = "e"[:] +e01 = "e"[:1] +e02 = "e"[: a()] +e10 = "e"[1:] +e11 = "e"[1:1] +e12 = "e"[1 : a()] +e20 = "e"[a() :] +e21 = "e"[a() : 1] +e22 = "e"[a() : a()] +e200 = "e"[a() :: ] +e201 = "e"[a() :: 1] +e202 = "e"[a() :: a()] +e210 = "e"[a() : 1 :] diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index a1f9fa2cae..fa6fa5baaa 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -1,16 +1,14 @@ -use std::cmp::Ordering; - -use ruff_text_size::{TextRange, TextSize}; -use rustpython_parser::ast::Ranged; - -use ruff_python_ast::node::AnyNodeRef; +use crate::comments::visitor::{CommentPlacement, DecoratedComment}; +use crate::comments::CommentLinePosition; +use crate::expression::expr_slice::{assign_comment_in_slice, ExprSliceCommentSection}; +use crate::trivia::{first_non_trivia_token_rev, SimpleTokenizer, Token, TokenKind}; +use ruff_python_ast::node::{AnyNodeRef, AstNode}; use ruff_python_ast::source_code::Locator; use ruff_python_ast::whitespace; use ruff_python_whitespace::{PythonWhitespace, UniversalNewlines}; - -use crate::comments::visitor::{CommentPlacement, DecoratedComment}; -use crate::comments::CommentLinePosition; -use crate::trivia::{SimpleTokenizer, Token, TokenKind}; +use ruff_text_size::{TextRange, TextSize}; +use rustpython_parser::ast::{Expr, ExprSlice, Ranged}; +use std::cmp::Ordering; /// Implements the custom comment placement logic. pub(super) fn place_comment<'a>( @@ -30,6 +28,7 @@ pub(super) fn place_comment<'a>( handle_trailing_binary_expression_left_or_operator_comment, handle_leading_function_with_decorators_comment, handle_dict_unpacking_comment, + handle_slice_comments, ]; for handler in HANDLERS { comment = match handler(comment, locator) { @@ -837,6 +836,87 @@ fn handle_module_level_own_line_comment_before_class_or_function_comment<'a>( } } +/// Handles the attaching comments left or right of the colon in a slice as trailing comment of the +/// preceding node or leading comment of the following node respectively. +/// ```python +/// a = "input"[ +/// 1 # c +/// # d +/// :2 +/// ] +/// ``` +fn handle_slice_comments<'a>( + comment: DecoratedComment<'a>, + locator: &Locator, +) -> CommentPlacement<'a> { + let expr_slice = match comment.enclosing_node() { + AnyNodeRef::ExprSlice(expr_slice) => expr_slice, + AnyNodeRef::ExprSubscript(expr_subscript) => { + if expr_subscript.value.end() < expr_subscript.slice.start() { + if let Expr::Slice(expr_slice) = expr_subscript.slice.as_ref() { + expr_slice + } else { + return CommentPlacement::Default(comment); + } + } else { + return CommentPlacement::Default(comment); + } + } + _ => return CommentPlacement::Default(comment), + }; + + let ExprSlice { + range: _, + lower, + upper, + step, + } = expr_slice; + + // Check for `foo[ # comment`, but only if they are on the same line + let after_lbracket = matches!( + first_non_trivia_token_rev(comment.slice().start(), locator.contents()), + Some(Token { + kind: TokenKind::LBracket, + .. + }) + ); + if comment.line_position().is_end_of_line() && after_lbracket { + // Keep comments after the opening bracket there by formatting them outside the + // soft block indent + // ```python + // "a"[ # comment + // 1: + // ] + // ``` + debug_assert!( + matches!(comment.enclosing_node(), AnyNodeRef::ExprSubscript(_)), + "{:?}", + comment.enclosing_node() + ); + return CommentPlacement::dangling(comment.enclosing_node(), comment); + } + + let assignment = + assign_comment_in_slice(comment.slice().range(), locator.contents(), expr_slice); + let node = match assignment { + ExprSliceCommentSection::Lower => lower, + ExprSliceCommentSection::Upper => upper, + ExprSliceCommentSection::Step => step, + }; + + if let Some(node) = node { + if comment.slice().start() < node.start() { + CommentPlacement::leading(node.as_ref().into(), comment) + } else { + // If a trailing comment is an end of line comment that's fine because we have a node + // ahead of it + CommentPlacement::trailing(node.as_ref().into(), comment) + } + } else { + CommentPlacement::dangling(expr_slice.as_any_node_ref(), comment) + } +} + /// Finds the offset of the `/` that separates the positional only and arguments from the other arguments. /// Returns `None` if the positional only separator `/` isn't present in the specified range. fn find_pos_only_slash_offset( diff --git a/crates/ruff_python_formatter/src/expression/expr_slice.rs b/crates/ruff_python_formatter/src/expression/expr_slice.rs index 529ef4679b..2edbcc3146 100644 --- a/crates/ruff_python_formatter/src/expression/expr_slice.rs +++ b/crates/ruff_python_formatter/src/expression/expr_slice.rs @@ -1,26 +1,264 @@ +use crate::comments::{dangling_comments, Comments, SourceComment}; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; -use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; - -use crate::comments::Comments; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::trivia::Token; +use crate::trivia::{first_non_trivia_token, TokenKind}; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{hard_line_break, line_suffix_boundary, space, text}; +use ruff_formatter::{write, Buffer, Format, FormatError, FormatResult}; +use ruff_python_ast::node::AstNode; +use ruff_python_ast::prelude::{Expr, Ranged}; +use ruff_text_size::TextRange; use rustpython_parser::ast::ExprSlice; #[derive(Default)] pub struct FormatExprSlice; impl FormatNodeRule for FormatExprSlice { - fn fmt_fields(&self, _item: &ExprSlice, f: &mut PyFormatter) -> FormatResult<()> { - write!( - f, - [not_yet_implemented_custom_text( - "NOT_IMPLEMENTED_start:NOT_IMPLEMENTED_end" - )] - ) + /// This implementation deviates from black in that comments are attached to the section of the + /// slice they originate in + fn fmt_fields(&self, item: &ExprSlice, f: &mut PyFormatter) -> FormatResult<()> { + // `[lower:upper:step]` + let ExprSlice { + range, + lower, + upper, + step, + } = item; + + let (first_colon, second_colon) = + find_colons(f.context().contents(), *range, lower, upper)?; + + // Handle comment placement + // In placements.rs, we marked comment for None nodes a dangling and associated all others + // as leading or dangling wrt to a node. That means we either format a node and only have + // to handle newlines and spacing, or the node is None and we insert the corresponding + // slice of dangling comments + let comments = f.context().comments().clone(); + let slice_dangling_comments = comments.dangling_comments(item.as_any_node_ref()); + // Put the dangling comments (where the nodes are missing) into buckets + let first_colon_partition_index = slice_dangling_comments + .partition_point(|x| x.slice().start() < first_colon.range.start()); + let (dangling_lower_comments, dangling_upper_step_comments) = + slice_dangling_comments.split_at(first_colon_partition_index); + let (dangling_upper_comments, dangling_step_comments) = + if let Some(second_colon) = &second_colon { + let second_colon_partition_index = dangling_upper_step_comments + .partition_point(|x| x.slice().start() < second_colon.range.start()); + dangling_upper_step_comments.split_at(second_colon_partition_index) + } else { + // Without a second colon they remaining dangling comments belong between the first + // colon and the closing parentheses + (dangling_upper_step_comments, [].as_slice()) + }; + + // Ensure there a no dangling comments for a node if the node is present + debug_assert!(lower.is_none() || dangling_lower_comments.is_empty()); + debug_assert!(upper.is_none() || dangling_upper_comments.is_empty()); + debug_assert!(step.is_none() || dangling_step_comments.is_empty()); + + // Handle spacing around the colon(s) + // https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#slices + let lower_simple = lower.as_ref().map_or(true, |expr| is_simple_expr(expr)); + let upper_simple = upper.as_ref().map_or(true, |expr| is_simple_expr(expr)); + let step_simple = step.as_ref().map_or(true, |expr| is_simple_expr(expr)); + let all_simple = lower_simple && upper_simple && step_simple; + + // lower + if let Some(lower) = lower { + write!(f, [lower.format(), line_suffix_boundary()])?; + } else { + dangling_comments(dangling_lower_comments).fmt(f)?; + } + + // First colon + // The spacing after the colon depends on both the lhs and the rhs: + // ``` + // e00 = x[:] + // e01 = x[:1] + // e02 = x[: a()] + // e10 = x[1:] + // e11 = x[1:1] + // e12 = x[1 : a()] + // e20 = x[a() :] + // e21 = x[a() : 1] + // e22 = x[a() : a()] + // e200 = "e"[a() : :] + // e201 = "e"[a() :: 1] + // e202 = "e"[a() :: a()] + // ``` + if !all_simple { + space().fmt(f)?; + } + text(":").fmt(f)?; + // No upper node, no need for a space, e.g. `x[a() :]` + if !all_simple && upper.is_some() { + space().fmt(f)?; + } + + // Upper + if let Some(upper) = upper { + let upper_leading_comments = comments.leading_comments(upper.as_ref()); + leading_comments_spacing(f, upper_leading_comments)?; + write!(f, [upper.format(), line_suffix_boundary()])?; + } else { + if let Some(first) = dangling_upper_comments.first() { + // Here the spacing for end-of-line comments works but own line comments need + // explicit spacing + if first.line_position().is_own_line() { + hard_line_break().fmt(f)?; + } + } + dangling_comments(dangling_upper_comments).fmt(f)?; + } + + // (optionally) step + if second_colon.is_some() { + // Same spacing rules as for the first colon, except for the strange case when the + // second colon exists, but neither upper nor step + // ``` + // e200 = "e"[a() : :] + // e201 = "e"[a() :: 1] + // e202 = "e"[a() :: a()] + // ``` + if !all_simple && (upper.is_some() || step.is_none()) { + space().fmt(f)?; + } + text(":").fmt(f)?; + // No step node, no need for a space + if !all_simple && step.is_some() { + space().fmt(f)?; + } + if let Some(step) = step { + let step_leading_comments = comments.leading_comments(step.as_ref()); + leading_comments_spacing(f, step_leading_comments)?; + step.format().fmt(f)?; + } else { + if !dangling_step_comments.is_empty() { + // Put the colon and comments on their own lines + write!( + f, + [hard_line_break(), dangling_comments(dangling_step_comments)] + )?; + } + } + } else { + debug_assert!(step.is_none(), "step can't exist without a second colon"); + } + Ok(()) } } +/// We're in a slice, so we know there's a first colon, but with have to look into the source +/// to find out whether there is a second one, too, e.g. `[1:2]` and `[1:10:2]`. +/// +/// Returns the first and optionally the second colon. +pub(crate) fn find_colons( + contents: &str, + range: TextRange, + lower: &Option>, + upper: &Option>, +) -> FormatResult<(Token, Option)> { + let after_lower = lower + .as_ref() + .map_or(range.start(), |lower| lower.range().end()); + let first_colon = + first_non_trivia_token(after_lower, contents).ok_or(FormatError::SyntaxError)?; + if first_colon.kind != TokenKind::Colon { + return Err(FormatError::SyntaxError); + } + + let after_upper = upper + .as_ref() + .map_or(first_colon.end(), |upper| upper.range().end()); + // At least the closing bracket must exist, so there must be a token there + let next_token = + first_non_trivia_token(after_upper, contents).ok_or(FormatError::SyntaxError)?; + let second_colon = if next_token.kind == TokenKind::Colon { + debug_assert!( + next_token.range.start() < range.end(), + "The next token in a slice must either be a colon or the closing bracket" + ); + Some(next_token) + } else { + None + }; + Ok((first_colon, second_colon)) +} + +/// Determines whether this expression needs a space around the colon +/// +fn is_simple_expr(expr: &Expr) -> bool { + matches!(expr, Expr::Constant(_) | Expr::Name(_)) +} + +pub(crate) enum ExprSliceCommentSection { + Lower, + Upper, + Step, +} + +/// Assigns a comment to lower/upper/step in `[lower:upper:step]`. +/// +/// ```python +/// "sliceable"[ +/// # lower comment +/// : +/// # upper comment +/// : +/// # step comment +/// ] +/// ``` +pub(crate) fn assign_comment_in_slice( + comment: TextRange, + contents: &str, + expr_slice: &ExprSlice, +) -> ExprSliceCommentSection { + let ExprSlice { + range, + lower, + upper, + step: _, + } = expr_slice; + + let (first_colon, second_colon) = find_colons(contents, *range, lower, upper) + .expect("SyntaxError when trying to parse slice"); + + if comment.start() < first_colon.range.start() { + ExprSliceCommentSection::Lower + } else { + // We are to the right of the first colon + if let Some(second_colon) = second_colon { + if comment.start() < second_colon.range.start() { + ExprSliceCommentSection::Upper + } else { + ExprSliceCommentSection::Step + } + } else { + // No second colon means there is no step + ExprSliceCommentSection::Upper + } + } +} + +/// Manual spacing for the leading comments of upper and step +fn leading_comments_spacing( + f: &mut PyFormatter, + leading_comments: &[SourceComment], +) -> FormatResult<()> { + if let Some(first) = leading_comments.first() { + if first.line_position().is_own_line() { + // Insert a newline after the colon so the comment ends up on its own line + hard_line_break().fmt(f)?; + } else { + // Insert the two spaces between the colon and the end-of-line comment after the colon + write!(f, [space(), space()])?; + } + } + Ok(()) +} + impl NeedsParentheses for ExprSlice { fn needs_parentheses( &self, diff --git a/crates/ruff_python_formatter/src/expression/expr_subscript.rs b/crates/ruff_python_formatter/src/expression/expr_subscript.rs index 6874b1415a..55f8d22490 100644 --- a/crates/ruff_python_formatter/src/expression/expr_subscript.rs +++ b/crates/ruff_python_formatter/src/expression/expr_subscript.rs @@ -1,24 +1,52 @@ +use crate::comments::{trailing_comments, Comments}; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; -use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; - -use crate::comments::Comments; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{group, soft_block_indent, text}; +use ruff_formatter::{format_args, write, Buffer, FormatResult}; +use ruff_python_ast::node::AstNode; use rustpython_parser::ast::ExprSubscript; #[derive(Default)] pub struct FormatExprSubscript; impl FormatNodeRule for FormatExprSubscript { - fn fmt_fields(&self, _item: &ExprSubscript, f: &mut PyFormatter) -> FormatResult<()> { + fn fmt_fields(&self, item: &ExprSubscript, f: &mut PyFormatter) -> FormatResult<()> { + let ExprSubscript { + range: _, + value, + slice, + ctx: _, + } = item; + + let comments = f.context().comments().clone(); + let dangling_comments = comments.dangling_comments(item.as_any_node_ref()); + debug_assert!( + dangling_comments.len() <= 1, + "The subscript expression must have at most a single comment, the one after the bracket" + ); + write!( f, - [not_yet_implemented_custom_text( - "NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]" - )] + [group(&format_args![ + value.format(), + text("["), + trailing_comments(dangling_comments), + soft_block_indent(&slice.format()), + text("]") + ])] ) } + + fn fmt_dangling_comments( + &self, + _node: &ExprSubscript, + _f: &mut PyFormatter, + ) -> FormatResult<()> { + // Handled inside of `fmt_fields` + Ok(()) + } } impl NeedsParentheses for ExprSubscript { @@ -28,6 +56,9 @@ impl NeedsParentheses for ExprSubscript { source: &str, comments: &Comments, ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + Parentheses::Optional => Parentheses::Never, + parentheses => parentheses, + } } } diff --git a/crates/ruff_python_formatter/src/expression/expr_unary_op.rs b/crates/ruff_python_formatter/src/expression/expr_unary_op.rs index 04151760fa..e55a2db81a 100644 --- a/crates/ruff_python_formatter/src/expression/expr_unary_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_unary_op.rs @@ -2,10 +2,10 @@ use crate::comments::{trailing_comments, Comments}; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; -use crate::prelude::*; use crate::trivia::{SimpleTokenizer, TokenKind}; -use crate::FormatNodeRule; -use ruff_formatter::FormatContext; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{hard_line_break, space, text}; +use ruff_formatter::{Format, FormatContext, FormatResult}; use ruff_python_ast::prelude::UnaryOp; use ruff_text_size::{TextLen, TextRange}; use rustpython_parser::ast::{ExprUnaryOp, Ranged}; diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 3f09fd51d6..ec332985a3 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -273,36 +273,7 @@ if True: let formatted_code = printed.as_code(); - let reformatted = match format_module(formatted_code) { - Ok(reformatted) => reformatted, - Err(err) => { - panic!( - "Expected formatted code to be valid syntax: {err}:\ - \n---\n{formatted_code}---\n", - ); - } - }; - - if reformatted.as_code() != formatted_code { - let diff = TextDiff::from_lines(formatted_code, reformatted.as_code()) - .unified_diff() - .header("Formatted once", "Formatted twice") - .to_string(); - panic!( - r#"Reformatting the formatted code a second time resulted in formatting changes. ---- -{diff}--- - -Formatted once: ---- -{formatted_code}--- - -Formatted twice: ---- -{}---"#, - reformatted.as_code() - ); - } + ensure_stability_when_formatting_twice(formatted_code); if formatted_code == expected_output { // Black and Ruff formatting matches. Delete any existing snapshot files because the Black output @@ -374,6 +345,8 @@ Formatted twice: let reformatted = format_module(formatted_code).unwrap_or_else(|err| panic!("Expected formatted code to be valid syntax but it contains syntax errors: {err}\n{formatted_code}")); + ensure_stability_when_formatting_twice(formatted_code); + if reformatted.as_code() != formatted_code { let diff = TextDiff::from_lines(formatted_code, reformatted.as_code()) .unified_diff() @@ -406,6 +379,40 @@ Formatted twice: Ok(()) } + /// Format another time and make sure that there are no changes anymore + fn ensure_stability_when_formatting_twice(formatted_code: &str) { + let reformatted = match format_module(formatted_code) { + Ok(reformatted) => reformatted, + Err(err) => { + panic!( + "Expected formatted code to be valid syntax: {err}:\ + \n---\n{formatted_code}---\n", + ); + } + }; + + if reformatted.as_code() != formatted_code { + let diff = TextDiff::from_lines(formatted_code, reformatted.as_code()) + .unified_diff() + .header("Formatted once", "Formatted twice") + .to_string(); + panic!( + r#"Reformatting the formatted code a second time resulted in formatting changes. +--- +{diff}--- + +Formatted once: +--- +{formatted_code}--- + +Formatted twice: +--- +{}---"#, + reformatted.as_code() + ); + } + } + /// Use this test to debug the formatting of some snipped #[ignore] #[test] diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap index c736be6242..da19ad12f5 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap @@ -73,9 +73,8 @@ y = 100(no) +if 10 .NOT_IMPLEMENTED_attr: ... --y = 100[no] + y = 100[no] -y = 100(no) -+y = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] +y = NOT_IMPLEMENTED_call() ``` @@ -102,7 +101,7 @@ x = -100.0000J if 10 .NOT_IMPLEMENTED_attr: ... -y = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] +y = 100[no] y = NOT_IMPLEMENTED_call() ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap index 3cd87b7335..db67f3dc39 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap @@ -264,32 +264,30 @@ instruction()#comment with bad spacing if typedargslist: - parameters.children = [children[0], body, children[-1]] # (1 # )1 - parameters.children = [ -- children[0], + parameters.NOT_IMPLEMENTED_attr = [ -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # (1 - body, -- children[-1], # type: ignore -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # )1 ++ children[0], # (1 ++ body, ++ children[-1], # )1 + ] + parameters.NOT_IMPLEMENTED_attr = [ -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], -+ body, -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # type: ignore + children[0], + body, + children[-1], # type: ignore ] else: - parameters.children = [ - parameters.children[0], # (2 what if this was actually long + parameters.NOT_IMPLEMENTED_attr = [ -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # (2 what if this was actually long ++ parameters.NOT_IMPLEMENTED_attr[0], # (2 what if this was actually long body, - parameters.children[-1], # )2 -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # )2 ++ parameters.NOT_IMPLEMENTED_attr[-1], # )2 ] - parameters.children = [parameters.what_if_this_was_actually_long.children[0], body, parameters.children[-1]] # type: ignore + parameters.NOT_IMPLEMENTED_attr = [ -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], ++ parameters.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr[0], + body, -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], ++ parameters.NOT_IMPLEMENTED_attr[-1], + ] # type: ignore if ( - self._proc is not None @@ -460,25 +458,25 @@ else: def inline_comments_in_brackets_ruin_everything(): if typedargslist: parameters.NOT_IMPLEMENTED_attr = [ - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # (1 + children[0], # (1 body, - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # )1 + children[-1], # )1 ] parameters.NOT_IMPLEMENTED_attr = [ - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], + children[0], body, - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # type: ignore + children[-1], # type: ignore ] else: parameters.NOT_IMPLEMENTED_attr = [ - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # (2 what if this was actually long + parameters.NOT_IMPLEMENTED_attr[0], # (2 what if this was actually long body, - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # )2 + parameters.NOT_IMPLEMENTED_attr[-1], # )2 ] parameters.NOT_IMPLEMENTED_attr = [ - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], + parameters.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr[0], body, - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], + parameters.NOT_IMPLEMENTED_attr[-1], ] # type: ignore if ( NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap index 2f25cc478b..10302ecac7 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap @@ -153,12 +153,9 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite tup = ( another_element, -@@ -84,35 +85,22 @@ - - +@@ -86,33 +87,20 @@ def func( -- a=some_list[0], # type: int -+ a=NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # type: int + a=some_list[0], # type: int ): # type: () -> int - c = call( - 0.0123, @@ -293,7 +290,7 @@ def f( def func( - a=NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # type: int + a=some_list[0], # type: int ): # type: () -> int c = NOT_IMPLEMENTED_call() diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap index 1bc96c10d0..772e6104cb 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap @@ -276,7 +276,7 @@ last_call() Name None True -@@ -30,203 +31,178 @@ +@@ -30,134 +31,120 @@ -1 ~int and not v1 ^ 123 + v2 | True (~int) and (not ((v1 ^ (123 + v2)) | True)) @@ -297,13 +297,6 @@ last_call() -(str or None) if True else (str or bytes or None) -str or None if (1 if True else 2) else str or bytes or None -(str or None) if (1 if True else 2) else (str or bytes or None) --( -- (super_long_variable_name or None) -- if (1 if super_long_test_name else 2) -- else (str or bytes or None) --) --{"2.7": dead, "3.7": (long_live or die_hard)} --{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} ++really ** -confusing ** ~operator**-precedence +flags & ~select.NOT_IMPLEMENTED_attr and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +lambda x: True @@ -328,9 +321,7 @@ last_call() + "NOT_YET_IMPLEMENTED_STRING": (long_live or die_hard), + **{"NOT_YET_IMPLEMENTED_STRING": verygood}, +} - {**a, **b, **c} --{"2.7", "3.6", "3.7", "3.8", "3.9", ("4.0" if gilectomy else "3.10")} --({"a": "b"}, (True or False), (+value), "string", b"bytes") or None ++{**a, **b, **c} +{ + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", @@ -339,7 +330,16 @@ last_call() + "NOT_YET_IMPLEMENTED_STRING", + (NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false), +} -+( + ( +- (super_long_variable_name or None) +- if (1 if super_long_test_name else 2) +- else (str or bytes or None) +-) +-{"2.7": dead, "3.7": (long_live or die_hard)} +-{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} +-{**a, **b, **c} +-{"2.7", "3.6", "3.7", "3.8", "3.9", ("4.0" if gilectomy else "3.10")} +-({"a": "b"}, (True or False), (+value), "string", b"bytes") or None + {"NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING"}, + (True or False), + (+value), @@ -352,7 +352,12 @@ last_call() (1, 2, 3) [] [1, 2, 3, 4, 5, 6, 7, 8, 9, (10 or A), (11 or B), (12 or C)] --[ ++[1, 2, 3] ++[NOT_YET_IMPLEMENTED_ExprStarred] ++[NOT_YET_IMPLEMENTED_ExprStarred] ++[NOT_YET_IMPLEMENTED_ExprStarred, 4, 5] ++[4, NOT_YET_IMPLEMENTED_ExprStarred, 5] + [ - 1, - 2, - 3, @@ -369,12 +374,7 @@ last_call() - *a, - 5, -] -+[1, 2, 3] -+[NOT_YET_IMPLEMENTED_ExprStarred] -+[NOT_YET_IMPLEMENTED_ExprStarred] -+[NOT_YET_IMPLEMENTED_ExprStarred, 4, 5] -+[4, NOT_YET_IMPLEMENTED_ExprStarred, 5] - [ +-[ this_is_a_very_long_variable_which_will_force_a_delimiter_split, element, another, @@ -393,6 +393,33 @@ last_call() -{i: j for i, j in ((1, "a"), (2, "b"), (3, "c"))} -{a: b * 2 for a, b in dictionary.items()} -{a: b * -2 for a, b in dictionary.items()} +-{ +- k: v +- for k, v in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension +-} +-Python3 > Python2 > COBOL +-Life is Life +-call() +-call(arg) +-call(kwarg="hey") +-call(arg, kwarg="hey") +-call(arg, another, kwarg="hey", **kwargs) +-call( +- this_is_a_very_long_variable_which_will_force_a_delimiter_split, +- arg, +- another, +- kwarg="hey", +- **kwargs, +-) # note: no trailing comma pre-3.6 +-call(*gidgets[:2]) +-call(a, *gidgets[:2]) +-call(**self.screen_kwargs) +-call(b, **self.screen_kwargs) +-lukasz.langa.pl +-call.me(maybe) +-(1).real +-(1.0).real +-....__class__ +NOT_YET_IMPLEMENTED_ExprSetComp +NOT_YET_IMPLEMENTED_ExprSetComp +NOT_YET_IMPLEMENTED_ExprSetComp @@ -423,81 +450,22 @@ last_call() +1 .NOT_IMPLEMENTED_attr +1.0 .NOT_IMPLEMENTED_attr +....NOT_IMPLEMENTED_attr -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_YET_IMPLEMENTED_StmtAnnAssign -+NOT_YET_IMPLEMENTED_StmtAnnAssign -+NOT_YET_IMPLEMENTED_StmtAnnAssign -+NOT_YET_IMPLEMENTED_StmtAnnAssign # type: ignore -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false - { -- k: v -- for k, v in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension -+ "NOT_YET_IMPLEMENTED_STRING": dead, -+ "NOT_YET_IMPLEMENTED_STRING": long_live or die_hard, - } --Python3 > Python2 > COBOL --Life is Life --call() --call(arg) --call(kwarg="hey") --call(arg, kwarg="hey") --call(arg, another, kwarg="hey", **kwargs) --call( -- this_is_a_very_long_variable_which_will_force_a_delimiter_split, -- arg, -- another, -- kwarg="hey", -- **kwargs, --) # note: no trailing comma pre-3.6 --call(*gidgets[:2]) --call(a, *gidgets[:2]) --call(**self.screen_kwargs) --call(b, **self.screen_kwargs) --lukasz.langa.pl --call.me(maybe) --(1).real --(1.0).real --....__class__ --list[str] --dict[str, int] --tuple[str, ...] --tuple[str, int, float, dict[str, int]] --tuple[ + list[str] + dict[str, int] + tuple[str, ...] + tuple[str, int, float, dict[str, int]] + tuple[ - str, - int, - float, - dict[str, int], --] ++ ( ++ str, ++ int, ++ float, ++ dict[str, int], ++ ) + ] -very_long_variable_name_filters: t.List[ - t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], -] @@ -510,35 +478,43 @@ last_call() -xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( - sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) -) # type: ignore --slice[0] --slice[0:1] --slice[0:1:2] --slice[:] ++NOT_YET_IMPLEMENTED_StmtAnnAssign ++NOT_YET_IMPLEMENTED_StmtAnnAssign ++NOT_YET_IMPLEMENTED_StmtAnnAssign ++NOT_YET_IMPLEMENTED_StmtAnnAssign # type: ignore + slice[0] + slice[0:1] + slice[0:1:2] + slice[:] -slice[:-1] --slice[1:] ++slice[ : -1] + slice[1:] -slice[::-1] --slice[d :: d + 1] --slice[:c, c - 1] --numpy[:, 0:1] ++slice[ :: -1] + slice[d :: d + 1] + slice[:c, c - 1] + numpy[:, 0:1] -numpy[:, :-1] --numpy[0, :] --numpy[:, i] --numpy[0, :2] --numpy[:N, 0] --numpy[:2, :4] --numpy[2:4, 1:5] --numpy[4:, 2:] --numpy[:, (0, 1, 2, 5)] --numpy[0, [0]] --numpy[:, [i]] --numpy[1 : c + 1, c] --numpy[-(c + 1) :, d] --numpy[:, l[-2]] ++numpy[:, : -1] + numpy[0, :] + numpy[:, i] + numpy[0, :2] +@@ -171,62 +158,58 @@ + numpy[1 : c + 1, c] + numpy[-(c + 1) :, d] + numpy[:, l[-2]] -numpy[:, ::-1] -numpy[np.newaxis, :] -(str or None) if (sys.version_info[0] > (3,)) else (str or bytes or None) -{"2.7": dead, "3.7": long_live or die_hard} -{"2.7", "3.6", "3.7", "3.8", "3.9", "4.0" if gilectomy else "3.10"} ++numpy[:, :: -1] ++numpy[np.NOT_IMPLEMENTED_attr, :] ++NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false ++{ ++ "NOT_YET_IMPLEMENTED_STRING": dead, ++ "NOT_YET_IMPLEMENTED_STRING": long_live or die_hard, ++} +{ + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", @@ -585,7 +561,13 @@ last_call() -g = 1, *"ten" -what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set( - vars_to_remove --) ++e = NOT_IMPLEMENTED_call() ++f = 1, NOT_YET_IMPLEMENTED_ExprStarred ++g = 1, NOT_YET_IMPLEMENTED_ExprStarred ++what_is_up_with_those_new_coord_names = ( ++ (coord_names + NOT_IMPLEMENTED_call()) ++ + NOT_IMPLEMENTED_call() + ) -what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( - vars_to_remove -) @@ -596,13 +578,7 @@ last_call() - ) - .order_by(models.Customer.id.asc()) - .all() -+e = NOT_IMPLEMENTED_call() -+f = 1, NOT_YET_IMPLEMENTED_ExprStarred -+g = 1, NOT_YET_IMPLEMENTED_ExprStarred -+what_is_up_with_those_new_coord_names = ( -+ (coord_names + NOT_IMPLEMENTED_call()) -+ + NOT_IMPLEMENTED_call() - ) +-) -result = ( - session.query(models.Customer.id) - .filter( @@ -625,7 +601,7 @@ last_call() mapping = { A: 0.25 * (10.0 / 12), B: 0.1 * (10.0 / 12), -@@ -236,64 +212,38 @@ +@@ -236,64 +219,38 @@ def gen(): @@ -714,7 +690,7 @@ last_call() ): return True if ( -@@ -327,24 +277,44 @@ +@@ -327,24 +284,44 @@ ): return True if ( @@ -771,7 +747,7 @@ last_call() ): return True ( -@@ -363,8 +333,9 @@ +@@ -363,8 +340,9 @@ bbbb >> bbbb * bbbb ( aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa @@ -908,41 +884,48 @@ NOT_IMPLEMENTED_call() 1 .NOT_IMPLEMENTED_attr 1.0 .NOT_IMPLEMENTED_attr ....NOT_IMPLEMENTED_attr -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] +list[str] +dict[str, int] +tuple[str, ...] +tuple[str, int, float, dict[str, int]] +tuple[ + ( + str, + int, + float, + dict[str, int], + ) +] NOT_YET_IMPLEMENTED_StmtAnnAssign NOT_YET_IMPLEMENTED_StmtAnnAssign NOT_YET_IMPLEMENTED_StmtAnnAssign NOT_YET_IMPLEMENTED_StmtAnnAssign # type: ignore -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] +slice[0] +slice[0:1] +slice[0:1:2] +slice[:] +slice[ : -1] +slice[1:] +slice[ :: -1] +slice[d :: d + 1] +slice[:c, c - 1] +numpy[:, 0:1] +numpy[:, : -1] +numpy[0, :] +numpy[:, i] +numpy[0, :2] +numpy[:N, 0] +numpy[:2, :4] +numpy[2:4, 1:5] +numpy[4:, 2:] +numpy[:, (0, 1, 2, 5)] +numpy[0, [0]] +numpy[:, [i]] +numpy[1 : c + 1, c] +numpy[-(c + 1) :, d] +numpy[:, l[-2]] +numpy[:, :: -1] +numpy[np.NOT_IMPLEMENTED_attr, :] NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false { "NOT_YET_IMPLEMENTED_STRING": dead, diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap index dc725ba267..b1786e90a8 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap @@ -273,7 +273,7 @@ d={'a':1, + debug: bool = False, + **kwargs, +) -> str: -+ return NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] ++ return text[number : -1] + + # fmt: on @@ -296,7 +296,7 @@ d={'a':1, def spaces_types( -@@ -51,76 +70,61 @@ +@@ -51,76 +70,71 @@ d: dict = {}, e: bool = True, f: int = -1, @@ -323,15 +323,22 @@ d={'a':1, def subscriptlist(): -- atom[ -- # fmt: off + atom[ + # fmt: off - 'some big and', - 'complex subscript', - # fmt: on - goes + here, - andhere, -- ] -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] ++ ( ++ "NOT_YET_IMPLEMENTED_STRING", ++ "NOT_YET_IMPLEMENTED_STRING", ++ # fmt: on ++ goes ++ + here, ++ andhere, ++ ) + ] def import_as_names(): @@ -390,7 +397,7 @@ d={'a':1, # fmt: off # hey, that won't work -@@ -130,13 +134,15 @@ +@@ -130,13 +144,15 @@ def on_and_off_broken(): @@ -411,7 +418,7 @@ d={'a':1, # fmt: on # fmt: off # ...but comments still get reformatted even though they should not be -@@ -145,80 +151,21 @@ +@@ -145,80 +161,21 @@ def long_lines(): if True: @@ -551,7 +558,7 @@ def function_signature_stress_test( debug: bool = False, **kwargs, ) -> str: - return NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] + return text[number : -1] # fmt: on @@ -595,7 +602,17 @@ something = { def subscriptlist(): - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] + atom[ + # fmt: off + ( + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + # fmt: on + goes + + here, + andhere, + ) + ] def import_as_names(): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap index a601ceda63..25404fa93b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap @@ -167,7 +167,7 @@ def __await__(): return (yield) **kwargs, ) -> str: - return text[number:-1] -+ return NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] ++ return text[number : -1] -def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): @@ -330,7 +330,7 @@ def function_signature_stress_test( debug: bool = False, **kwargs, ) -> str: - return NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] + return text[number : -1] def spaces( diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap index 89e72cdcc0..f0fb15c471 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap @@ -94,7 +94,7 @@ some_module.some_function( } tup = ( 1, -@@ -24,45 +24,18 @@ +@@ -24,45 +24,23 @@ def f( a: int = 1, ): @@ -106,7 +106,9 @@ some_module.some_function( - call2( - arg=[1, 2, 3], - ) -- x = { ++ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call() + x = { - "a": 1, - "b": 2, - }["a"] @@ -123,9 +125,11 @@ some_module.some_function( - "h": 8, - }["a"] - ): -+ NOT_IMPLEMENTED_call() -+ NOT_IMPLEMENTED_call() -+ x = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] ++ "NOT_YET_IMPLEMENTED_STRING": 1, ++ "NOT_YET_IMPLEMENTED_STRING": 2, ++ }[ ++ "NOT_YET_IMPLEMENTED_STRING" ++ ] + if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: pass @@ -133,7 +137,7 @@ some_module.some_function( -def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> ( - Set["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"] -): -+def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]: ++def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set["NOT_YET_IMPLEMENTED_STRING"]: json = { - "k": { - "k2": { @@ -148,7 +152,7 @@ some_module.some_function( } -@@ -80,35 +53,16 @@ +@@ -80,35 +58,16 @@ pass @@ -221,12 +225,17 @@ def f( ): NOT_IMPLEMENTED_call() NOT_IMPLEMENTED_call() - x = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] + x = { + "NOT_YET_IMPLEMENTED_STRING": 1, + "NOT_YET_IMPLEMENTED_STRING": 2, + }[ + "NOT_YET_IMPLEMENTED_STRING" + ] if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: pass -def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]: +def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set["NOT_YET_IMPLEMENTED_STRING"]: json = { "NOT_YET_IMPLEMENTED_STRING": { "NOT_YET_IMPLEMENTED_STRING": {"NOT_YET_IMPLEMENTED_STRING": [1]}, diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__one_element_subscript_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__one_element_subscript_py.snap index b52d1de815..66e37c3bad 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__one_element_subscript_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__one_element_subscript_py.snap @@ -25,25 +25,28 @@ list_of_types = [tuple[int,],] ```diff --- Black +++ Ruff -@@ -1,22 +1,12 @@ +@@ -1,22 +1,17 @@ # We should not treat the trailing comma # in a single-element subscript. -a: tuple[int,] -b = tuple[int,] +NOT_YET_IMPLEMENTED_StmtAnnAssign -+b = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] ++b = tuple[(int,)] # The magic comma still applies to multi-element subscripts. -c: tuple[ - int, - int, -] --d = tuple[ -- int, -- int, --] +NOT_YET_IMPLEMENTED_StmtAnnAssign -+d = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] + d = tuple[ +- int, +- int, ++ ( ++ int, ++ int, ++ ) + ] # Magic commas still work as expected for non-subscripts. -small_list = [ @@ -53,7 +56,7 @@ list_of_types = [tuple[int,],] - tuple[int,], -] +small_list = [1] -+list_of_types = [NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]] ++list_of_types = [tuple[(int,)]] ``` ## Ruff Output @@ -62,15 +65,20 @@ list_of_types = [tuple[int,],] # We should not treat the trailing comma # in a single-element subscript. NOT_YET_IMPLEMENTED_StmtAnnAssign -b = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] +b = tuple[(int,)] # The magic comma still applies to multi-element subscripts. NOT_YET_IMPLEMENTED_StmtAnnAssign -d = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] +d = tuple[ + ( + int, + int, + ) +] # Magic commas still work as expected for non-subscripts. small_list = [1] -list_of_types = [NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]] +list_of_types = [tuple[(int,)]] ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap index e3b6cde88b..3ccecffed8 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap @@ -92,7 +92,7 @@ return np.divide( -j = super().name ** 5 -k = [(2**idx, value) for idx, value in pairs] -l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) -+d = 5 ** NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] ++d = 5 ** f["NOT_YET_IMPLEMENTED_STRING"] +e = NOT_IMPLEMENTED_call() +f = NOT_IMPLEMENTED_call() ** 5 +g = a.NOT_IMPLEMENTED_attr**c.NOT_IMPLEMENTED_attr @@ -125,7 +125,7 @@ return np.divide( -j = super().name ** 5.0 -k = [(2.0**idx, value) for idx, value in pairs] -l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) -+d = 5.0 ** NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] ++d = 5.0 ** f["NOT_YET_IMPLEMENTED_STRING"] +e = NOT_IMPLEMENTED_call() +f = NOT_IMPLEMENTED_call() ** 5.0 +g = a.NOT_IMPLEMENTED_attr**c.NOT_IMPLEMENTED_attr @@ -181,7 +181,7 @@ def function_dont_replace_spaces(): a = 5**~4 b = 5 ** NOT_IMPLEMENTED_call() c = -(5**2) -d = 5 ** NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] +d = 5 ** f["NOT_YET_IMPLEMENTED_STRING"] e = NOT_IMPLEMENTED_call() f = NOT_IMPLEMENTED_call() ** 5 g = a.NOT_IMPLEMENTED_attr**c.NOT_IMPLEMENTED_attr @@ -200,7 +200,7 @@ r = x**y a = 5.0**~4.0 b = 5.0 ** NOT_IMPLEMENTED_call() c = -(5.0**2.0) -d = 5.0 ** NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] +d = 5.0 ** f["NOT_YET_IMPLEMENTED_STRING"] e = NOT_IMPLEMENTED_call() f = NOT_IMPLEMENTED_call() ** 5.0 g = a.NOT_IMPLEMENTED_attr**c.NOT_IMPLEMENTED_attr diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__prefer_rhs_split_reformatted_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__prefer_rhs_split_reformatted_py.snap index bdc722edb5..89478d9b1f 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__prefer_rhs_split_reformatted_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__prefer_rhs_split_reformatted_py.snap @@ -40,7 +40,7 @@ xxxxxxxxx_yyy_zzzzzzzz[xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxx -xxxxxxxxx_yyy_zzzzzzzz[ - xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1) -] = 1 -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] = 1 ++xxxxxxxxx_yyy_zzzzzzzz[NOT_IMPLEMENTED_call(), NOT_IMPLEMENTED_call()] = 1 ``` ## Ruff Output @@ -61,7 +61,7 @@ xxxxxxxxx_yyy_zzzzzzzz[xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxx # Make when when the left side of assignment plus the opening paren "... = (" is # exactly line length limit + 1, it won't be split like that. -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] = 1 +xxxxxxxxx_yyy_zzzzzzzz[NOT_IMPLEMENTED_call(), NOT_IMPLEMENTED_call()] = 1 ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__return_annotation_brackets_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__return_annotation_brackets_py.snap index 404098c431..db50309690 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__return_annotation_brackets_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__return_annotation_brackets_py.snap @@ -123,35 +123,36 @@ def foo() -> tuple[int, int, int,]: return 2 -@@ -95,26 +99,14 @@ - - - # Return type with commas --def foo() -> tuple[int, int, int]: -+def foo() -> NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]: +@@ -99,22 +103,22 @@ return 2 -def foo() -> ( - tuple[ -- loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, -- loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, -- loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, ++def foo() -> tuple[ ++ ( + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, - ] -): -+def foo() -> NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]: ++ ) ++]: return 2 # Magic trailing comma example -def foo() -> ( - tuple[ -- int, -- int, -- int, ++def foo() -> tuple[ ++ ( + int, + int, + int, - ] -): -+def foo() -> NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]: ++ ) ++]: return 2 ``` @@ -259,16 +260,28 @@ def foo() -> ( # Return type with commas -def foo() -> NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]: +def foo() -> tuple[int, int, int]: return 2 -def foo() -> NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]: +def foo() -> tuple[ + ( + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, + ) +]: return 2 # Magic trailing comma example -def foo() -> NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]: +def foo() -> tuple[ + ( + int, + int, + int, + ) +]: return 2 ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap index 0ca6ef0a1f..5a86ffbc95 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap @@ -60,26 +60,31 @@ func( ```diff --- Black +++ Ruff -@@ -1,25 +1,30 @@ +@@ -1,25 +1,35 @@ # We should not remove the trailing comma in a single-element subscript. -a: tuple[int,] -b = tuple[int,] +NOT_YET_IMPLEMENTED_StmtAnnAssign -+b = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] ++b = tuple[(int,)] # But commas in multiple element subscripts should be removed. -c: tuple[int, int] -d = tuple[int, int] +NOT_YET_IMPLEMENTED_StmtAnnAssign -+d = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] ++d = tuple[ ++ ( ++ int, ++ int, ++ ) ++] # Remove commas for non-subscripts. small_list = [1] -list_of_types = [tuple[int,]] -+list_of_types = [NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]] ++list_of_types = [tuple[(int,)]] small_set = {1} -set_of_types = {tuple[int,]} -+set_of_types = {NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]} ++set_of_types = {tuple[(int,)]} # Except single element tuples small_tuple = (1,) @@ -108,17 +113,22 @@ func( ```py # We should not remove the trailing comma in a single-element subscript. NOT_YET_IMPLEMENTED_StmtAnnAssign -b = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] +b = tuple[(int,)] # But commas in multiple element subscripts should be removed. NOT_YET_IMPLEMENTED_StmtAnnAssign -d = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] +d = tuple[ + ( + int, + int, + ) +] # Remove commas for non-subscripts. small_list = [1] -list_of_types = [NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]] +list_of_types = [tuple[(int,)]] small_set = {1} -set_of_types = {NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]} +set_of_types = {tuple[(int,)]} # Except single element tuples small_tuple = (1,) diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap index 1d2e046b96..808ecffe6b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap @@ -76,158 +76,137 @@ x[ ```diff --- Black +++ Ruff -@@ -1,59 +1,48 @@ +@@ -1,33 +1,33 @@ -slice[a.b : c.d] --slice[d :: d + 1] --slice[d + 1 :: d] --slice[d::d] --slice[0] --slice[-1] ++slice[a.NOT_IMPLEMENTED_attr : c.NOT_IMPLEMENTED_attr] + slice[d :: d + 1] + slice[d + 1 :: d] + slice[d::d] + slice[0] + slice[-1] -slice[:-1] -slice[::-1] --slice[:c, c - 1] --slice[c, c + 1, d::] --slice[ham[c::d] :: 1] --slice[ham[cheese**2 : -1] : 1 : 1, ham[1:2]] ++slice[ : -1] ++slice[ :: -1] + slice[:c, c - 1] + slice[c, c + 1, d::] + slice[ham[c::d] :: 1] + slice[ham[cheese**2 : -1] : 1 : 1, ham[1:2]] -slice[:-1:] -slice[lambda: None : lambda: None] -slice[lambda x, y, *args, really=2, **kwargs: None :, None::] --slice[1 or 2 : True and False] ++slice[ : -1 :] ++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[(1 for i in range(42)) : x] -slice[:: [i for i in range(42)]] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] ++slice[not so_simple : NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right] ++slice[(i for i in []) : x] ++slice[ :: [i for i in []]] async def f(): - slice[await x : [i async for i in arange(42)] : 42] -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] ++ slice[await x : [i for i in []] : 42] # These are from PEP-8: --ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:] --ham[lower:upper], ham[lower:upper:], ham[lower::step] -+( -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], -+) -+( -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], -+) + ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:] + ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] -ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] --ham[lower + offset : upper + offset] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] ++ham[ : NOT_IMPLEMENTED_call() : NOT_IMPLEMENTED_call()], ham[ :: NOT_IMPLEMENTED_call()] + ham[lower + offset : upper + offset] --slice[::, ::] --slice[ -- # A -- : -- # B -- : -- # C --] --slice[ -- # A -- 1: -- # B -- 2: -- # C -- 3 --] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] - --slice[ -- # A -- 1 + slice[::, ::] +@@ -50,10 +50,14 @@ + slice[ + # A + 1 - + 2 : -- # B ++ + 2 : + # B - 3 : -- # C -- 4 --] ++ 3 : + # C + 4 + ] -x[1:2:3] # A # B # C -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] ++x[ ++ 1: # A ++ 2: # B ++ 3 # C ++] ``` ## Ruff Output ```py -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] +slice[a.NOT_IMPLEMENTED_attr : c.NOT_IMPLEMENTED_attr] +slice[d :: d + 1] +slice[d + 1 :: d] +slice[d::d] +slice[0] +slice[-1] +slice[ : -1] +slice[ :: -1] +slice[:c, c - 1] +slice[c, c + 1, d::] +slice[ham[c::d] :: 1] +slice[ham[cheese**2 : -1] : 1 : 1, ham[1:2]] +slice[ : -1 :] +slice[lambda x: True : lambda x: True] +slice[lambda x: True :, None::] +slice[1 or 2 : True and False] +slice[not so_simple : NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right] +slice[(i for i in []) : x] +slice[ :: [i for i in []]] async def f(): - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] + slice[await x : [i for i in []] : 42] # These are from PEP-8: -( - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], -) -( - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], -) +ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:] +ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] +ham[ : NOT_IMPLEMENTED_call() : NOT_IMPLEMENTED_call()], ham[ :: NOT_IMPLEMENTED_call()] +ham[lower + offset : upper + offset] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] +slice[::, ::] +slice[ + # A + : + # B + : + # C +] +slice[ + # A + 1: + # B + 2: + # C + 3 +] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] +slice[ + # A + 1 + + 2 : + # B + 3 : + # C + 4 +] +x[ + 1: # A + 2: # B + 3 # C +] ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap index fbbed48d6e..e2b670003f 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap @@ -46,7 +46,7 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( ```diff --- Black +++ Ruff -@@ -1,50 +1,26 @@ +@@ -1,50 +1,30 @@ -zero( - one, -).two( @@ -86,7 +86,11 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( - }, - api_key=api_key, - )["extensions"]["sdk"]["token"] -+ return NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] ++ return NOT_IMPLEMENTED_call()["NOT_YET_IMPLEMENTED_STRING"][ ++ "NOT_YET_IMPLEMENTED_STRING" ++ ][ ++ "NOT_YET_IMPLEMENTED_STRING" ++ ] # Edge case where a bug in a working-in-progress version of @@ -126,7 +130,11 @@ NOT_IMPLEMENTED_call() # Example from https://github.com/psf/black/issues/3229 def refresh_token(self, device_family, refresh_token, api_key): - return NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] + return NOT_IMPLEMENTED_call()["NOT_YET_IMPLEMENTED_STRING"][ + "NOT_YET_IMPLEMENTED_STRING" + ][ + "NOT_YET_IMPLEMENTED_STRING" + ] # Edge case where a bug in a working-in-progress version of diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__slice_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__slice_py.snap new file mode 100644 index 0000000000..4fa305f243 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__slice_py.snap @@ -0,0 +1,177 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +--- +## Input +```py +# Handle comments both when lower and upper exist and when they don't +a1 = "a"[ + # a + 1 # b + : # c + 2 # d +] +a2 = "a"[ + # a + # b + : # c + # d +] + +# Check all places where comments can exist +b1 = "b"[ # a + # b + 1 # c + # d + : # e + # f + 2 # g + # h + : # i + # j + 3 # k + # l +] + +# Handle the spacing from the colon correctly with upper leading comments +c1 = "c"[ + 1 + : # e + # f + 2 +] +c2 = "c"[ + 1 + : # e + 2 +] +c3 = "c"[ + 1 + : + # f + 2 +] +c4 = "c"[ + 1 + : # f + 2 +] + +# End of line comments +d1 = "d"[ # comment + : +] +d2 = "d"[ # comment + 1: +] +d3 = "d"[ + 1 # comment + : +] + +# Spacing around the colon(s) +def a(): + ... + +e00 = "e"[:] +e01 = "e"[:1] +e02 = "e"[: a()] +e10 = "e"[1:] +e11 = "e"[1:1] +e12 = "e"[1 : a()] +e20 = "e"[a() :] +e21 = "e"[a() : 1] +e22 = "e"[a() : a()] +e200 = "e"[a() :: ] +e201 = "e"[a() :: 1] +e202 = "e"[a() :: a()] +e210 = "e"[a() : 1 :] +``` + + + +## Output +```py +# Handle comments both when lower and upper exist and when they don't +a1 = "NOT_YET_IMPLEMENTED_STRING"[ + # a + 1 # b + : # c + 2 # d +] +a2 = "NOT_YET_IMPLEMENTED_STRING"[ + # a + # b + : # c + # d +] + +# Check all places where comments can exist +b1 = "NOT_YET_IMPLEMENTED_STRING"[ # a + # b + 1 # c + # d + : # e + # f + 2 # g + # h + : # i + # j + 3 # k + # l +] + +# Handle the spacing from the colon correctly with upper leading comments +c1 = "NOT_YET_IMPLEMENTED_STRING"[ + 1: # e + # f + 2 +] +c2 = "NOT_YET_IMPLEMENTED_STRING"[ + 1: # e + 2 +] +c3 = "NOT_YET_IMPLEMENTED_STRING"[ + 1: + # f + 2 +] +c4 = "NOT_YET_IMPLEMENTED_STRING"[ + 1: # f + 2 +] + +# End of line comments +d1 = "NOT_YET_IMPLEMENTED_STRING"[ # comment + : +] +d2 = "NOT_YET_IMPLEMENTED_STRING"[ # comment + 1: +] +d3 = "NOT_YET_IMPLEMENTED_STRING"[ + 1 # comment + : +] + + +# Spacing around the colon(s) +def a(): + ... + + +e00 = "NOT_YET_IMPLEMENTED_STRING"[:] +e01 = "NOT_YET_IMPLEMENTED_STRING"[:1] +e02 = "NOT_YET_IMPLEMENTED_STRING"[ : NOT_IMPLEMENTED_call()] +e10 = "NOT_YET_IMPLEMENTED_STRING"[1:] +e11 = "NOT_YET_IMPLEMENTED_STRING"[1:1] +e12 = "NOT_YET_IMPLEMENTED_STRING"[1 : NOT_IMPLEMENTED_call()] +e20 = "NOT_YET_IMPLEMENTED_STRING"[NOT_IMPLEMENTED_call() :] +e21 = "NOT_YET_IMPLEMENTED_STRING"[NOT_IMPLEMENTED_call() : 1] +e22 = "NOT_YET_IMPLEMENTED_STRING"[NOT_IMPLEMENTED_call() : NOT_IMPLEMENTED_call()] +e200 = "NOT_YET_IMPLEMENTED_STRING"[NOT_IMPLEMENTED_call() : :] +e201 = "NOT_YET_IMPLEMENTED_STRING"[NOT_IMPLEMENTED_call() :: 1] +e202 = "NOT_YET_IMPLEMENTED_STRING"[NOT_IMPLEMENTED_call() :: NOT_IMPLEMENTED_call()] +e210 = "NOT_YET_IMPLEMENTED_STRING"[NOT_IMPLEMENTED_call() : 1 :] +``` + + diff --git a/crates/ruff_python_formatter/src/trivia.rs b/crates/ruff_python_formatter/src/trivia.rs index 04a06dd404..955f77c93f 100644 --- a/crates/ruff_python_formatter/src/trivia.rs +++ b/crates/ruff_python_formatter/src/trivia.rs @@ -112,9 +112,8 @@ impl Token { self.range.start() } - #[allow(unused)] pub(crate) const fn end(&self) -> TextSize { - self.range.start() + self.range.end() } } From e47aa468d51067ed802f5c31e1734aa4730ed4c8 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 21 Jun 2023 17:35:37 +0200 Subject: [PATCH 157/447] Format Identifier (#5255) --- crates/ruff_python_formatter/src/other/arg.rs | 15 ++-------- .../src/other/identifier.rs | 28 +++++++++++++++++++ crates/ruff_python_formatter/src/other/mod.rs | 1 + .../src/statement/stmt_function_def.rs | 2 +- 4 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 crates/ruff_python_formatter/src/other/identifier.rs diff --git a/crates/ruff_python_formatter/src/other/arg.rs b/crates/ruff_python_formatter/src/other/arg.rs index a0eebd3a96..3974223faa 100644 --- a/crates/ruff_python_formatter/src/other/arg.rs +++ b/crates/ruff_python_formatter/src/other/arg.rs @@ -1,7 +1,6 @@ use crate::prelude::*; use crate::FormatNodeRule; use ruff_formatter::write; -use ruff_text_size::{TextLen, TextRange}; use rustpython_parser::ast::Arg; #[derive(Default)] @@ -10,21 +9,13 @@ pub struct FormatArg; impl FormatNodeRule for FormatArg { fn fmt_fields(&self, item: &Arg, f: &mut PyFormatter) -> FormatResult<()> { let Arg { - range, + range: _, arg, annotation, type_comment: _, } = item; - write!( - f, - [ - // The name of the argument - source_text_slice( - TextRange::at(range.start(), arg.text_len()), - ContainsNewlines::No - ) - ] - )?; + + arg.format().fmt(f)?; if let Some(annotation) = annotation { write!(f, [text(":"), space(), annotation.format()])?; diff --git a/crates/ruff_python_formatter/src/other/identifier.rs b/crates/ruff_python_formatter/src/other/identifier.rs new file mode 100644 index 0000000000..40e588b491 --- /dev/null +++ b/crates/ruff_python_formatter/src/other/identifier.rs @@ -0,0 +1,28 @@ +use crate::prelude::*; +use crate::AsFormat; +use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule}; +use rustpython_parser::ast::{Identifier, Ranged}; + +pub struct FormatIdentifier; + +impl FormatRule> for FormatIdentifier { + fn fmt(&self, item: &Identifier, f: &mut PyFormatter) -> FormatResult<()> { + source_text_slice(item.range(), ContainsNewlines::No).fmt(f) + } +} + +impl<'ast> AsFormat> for Identifier { + type Format<'a> = FormatRefWithRule<'a, Identifier, FormatIdentifier, PyFormatContext<'ast>>; + + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new(self, FormatIdentifier) + } +} + +impl<'ast> IntoFormat> for Identifier { + type Format = FormatOwnedWithRule>; + + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new(self, FormatIdentifier) + } +} diff --git a/crates/ruff_python_formatter/src/other/mod.rs b/crates/ruff_python_formatter/src/other/mod.rs index fc2530e7c2..76c0b2857d 100644 --- a/crates/ruff_python_formatter/src/other/mod.rs +++ b/crates/ruff_python_formatter/src/other/mod.rs @@ -5,6 +5,7 @@ pub(crate) mod arguments; pub(crate) mod comprehension; pub(crate) mod decorator; pub(crate) mod except_handler_except_handler; +pub(crate) mod identifier; pub(crate) mod keyword; pub(crate) mod match_case; pub(crate) mod type_ignore_type_ignore; diff --git a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs index 560ab462c8..491429b2af 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs @@ -85,7 +85,7 @@ impl FormatRule, PyFormatContext<'_>> for FormatAnyFun [ text("def"), space(), - dynamic_text(name.as_str(), None), + name.format(), item.arguments().format(), ] )?; From d99b3bf661e249ec59f7fd198b658aefc5c42330 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 21 Jun 2023 12:42:58 -0400 Subject: [PATCH 158/447] Add some projects to the ecosystem CI check (#5258) --- scripts/check_ecosystem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/check_ecosystem.py b/scripts/check_ecosystem.py index d7afcb764b..d60ad3fc6d 100755 --- a/scripts/check_ecosystem.py +++ b/scripts/check_ecosystem.py @@ -85,6 +85,8 @@ REPOSITORIES: list[Repository] = [ Repository("bokeh", "bokeh", "branch-3.2", select="ALL"), Repository("pypa", "build", "main"), Repository("pypa", "cibuildwheel", "main"), + Repository("pypa", "setuptools", "main"), + Repository("python", "mypy", "master"), Repository("DisnakeDev", "disnake", "master"), Repository("scikit-build", "scikit-build", "main"), Repository("scikit-build", "scikit-build-core", "main"), From ecf61d49fa3fdfacb542c300d740c059b2cd8c00 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 21 Jun 2023 12:53:58 -0400 Subject: [PATCH 159/447] Restore existing bindings when unbinding caught exceptions (#5256) ## Summary In the latest release, we made some improvements to the semantic model, but our modifications to exception-unbinding are causing some false-positives. For example: ```py try: v = 3 except ImportError as v: print(v) else: print(v) ``` In the latest release, we started unbinding `v` after the `except` handler. (We used to restore the existing binding, the `v = 3`, but this was quite complicated.) Because we don't have full branch analysis, we can't then know that `v` is still bound in the `else` branch. The solution here modifies `resolve_read` to skip-lookup when hitting unbound exceptions. So when store the "unbind" for `except ImportError as v`, we save the binding that it shadowed `v = 3`, and skip to that. Closes #5249. Closes #5250. --- crates/ruff/src/checkers/ast/mod.rs | 20 +-- crates/ruff/src/renamer.rs | 2 +- crates/ruff/src/rules/pyflakes/mod.rs | 125 +++++++++++++++++- ...er_multiple_unbinds_from_module_scope.snap | 44 ++++++ ...s__load_after_unbind_from_class_scope.snap | 32 +++++ ...__load_after_unbind_from_module_scope.snap | 24 ++++ ...after_unbind_from_nested_module_scope.snap | 44 ++++++ ...in_body_after_double_shadowing_except.snap | 45 +++++++ ...print_in_body_after_shadowing_except.snap} | 10 +- ...int_in_if_else_after_shadowing_except.snap | 4 + ...nt_in_try_else_after_shadowing_except.snap | 4 + crates/ruff_python_semantic/src/binding.rs | 8 +- crates/ruff_python_semantic/src/model.rs | 92 ++++++++++++- 13 files changed, 429 insertions(+), 25 deletions(-) create mode 100644 crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_multiple_unbinds_from_module_scope.snap create mode 100644 crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_class_scope.snap create mode 100644 crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_module_scope.snap create mode 100644 crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_nested_module_scope.snap create mode 100644 crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_body_after_double_shadowing_except.snap rename crates/ruff/src/rules/pyflakes/snapshots/{ruff__rules__pyflakes__tests__print_after_shadowing_except.snap => ruff__rules__pyflakes__tests__print_in_body_after_shadowing_except.snap} (76%) create mode 100644 crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_if_else_after_shadowing_except.snap create mode 100644 crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_try_else_after_shadowing_except.snap diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 33e59c3841..ecd0b18a7a 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -3852,6 +3852,9 @@ where ); } + // Store the existing binding, if any. + let existing_id = self.semantic.lookup(name); + // Add the bound exception name to the scope. let binding_id = self.add_binding( name, @@ -3862,14 +3865,6 @@ where walk_except_handler(self, except_handler); - // Remove it from the scope immediately after. - self.add_binding( - name, - range, - BindingKind::UnboundException, - BindingFlags::empty(), - ); - // If the exception name wasn't used in the scope, emit a diagnostic. if !self.semantic.is_used(binding_id) { if self.enabled(Rule::UnusedVariable) { @@ -3889,6 +3884,13 @@ where self.diagnostics.push(diagnostic); } } + + self.add_binding( + name, + range, + BindingKind::UnboundException(existing_id), + BindingFlags::empty(), + ); } None => walk_except_handler(self, except_handler), } @@ -4236,7 +4238,7 @@ impl<'a> Checker<'a> { let shadowed = &self.semantic.bindings[shadowed_id]; if !matches!( shadowed.kind, - BindingKind::Builtin | BindingKind::Deletion | BindingKind::UnboundException, + BindingKind::Builtin | BindingKind::Deletion | BindingKind::UnboundException(_), ) { let references = shadowed.references.clone(); let is_global = shadowed.is_global(); diff --git a/crates/ruff/src/renamer.rs b/crates/ruff/src/renamer.rs index fe31104198..a7beaafc5e 100644 --- a/crates/ruff/src/renamer.rs +++ b/crates/ruff/src/renamer.rs @@ -251,7 +251,7 @@ impl Renamer { | BindingKind::ClassDefinition | BindingKind::FunctionDefinition | BindingKind::Deletion - | BindingKind::UnboundException => { + | BindingKind::UnboundException(_) => { Some(Edit::range_replacement(target.to_string(), binding.range)) } } diff --git a/crates/ruff/src/rules/pyflakes/mod.rs b/crates/ruff/src/rules/pyflakes/mod.rs index 87876e96d6..243be429ce 100644 --- a/crates/ruff/src/rules/pyflakes/mod.rs +++ b/crates/ruff/src/rules/pyflakes/mod.rs @@ -353,9 +353,59 @@ mod tests { except Exception as x: pass + # No error here, though it should arguably be an F821 error. `x` will + # be unbound after the `except` block (assuming an exception is raised + # and caught). print(x) "#, - "print_after_shadowing_except" + "print_in_body_after_shadowing_except" + )] + #[test_case( + r#" + def f(): + x = 1 + + try: + 1 / 0 + except ValueError as x: + pass + except ImportError as x: + pass + + # No error here, though it should arguably be an F821 error. `x` will + # be unbound after the `except` block (assuming an exception is raised + # and caught). + print(x) + "#, + "print_in_body_after_double_shadowing_except" + )] + #[test_case( + r#" + def f(): + try: + x = 3 + except ImportError as x: + print(x) + else: + print(x) + "#, + "print_in_try_else_after_shadowing_except" + )] + #[test_case( + r#" + def f(): + list = [1, 2, 3] + + for e in list: + if e % 2 == 0: + try: + pass + except Exception as e: + print(e) + else: + print(e) + "#, + "print_in_if_else_after_shadowing_except" )] #[test_case( r#" @@ -366,6 +416,79 @@ mod tests { "#, "double_del" )] + #[test_case( + r#" + x = 1 + + def f(): + try: + pass + except ValueError as x: + pass + + # This should resolve to the `x` in `x = 1`. + print(x) + "#, + "load_after_unbind_from_module_scope" + )] + #[test_case( + r#" + x = 1 + + def f(): + try: + pass + except ValueError as x: + pass + + try: + pass + except ValueError as x: + pass + + # This should resolve to the `x` in `x = 1`. + print(x) + "#, + "load_after_multiple_unbinds_from_module_scope" + )] + #[test_case( + r#" + x = 1 + + def f(): + try: + pass + except ValueError as x: + pass + + def g(): + try: + pass + except ValueError as x: + pass + + # This should resolve to the `x` in `x = 1`. + print(x) + "#, + "load_after_unbind_from_nested_module_scope" + )] + #[test_case( + r#" + class C: + x = 1 + + def f(): + try: + pass + except ValueError as x: + pass + + # This should raise an F821 error, rather than resolving to the + # `x` in `x = 1`. + print(x) + "#, + "load_after_unbind_from_class_scope" + )] fn contents(contents: &str, snapshot: &str) { let diagnostics = test_snippet(contents, &Settings::for_rules(&Linter::Pyflakes)); assert_messages!(snapshot, diagnostics); diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_multiple_unbinds_from_module_scope.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_multiple_unbinds_from_module_scope.snap new file mode 100644 index 0000000000..32d31a0137 --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_multiple_unbinds_from_module_scope.snap @@ -0,0 +1,44 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +:7:26: F841 [*] Local variable `x` is assigned to but never used + | +5 | try: +6 | pass +7 | except ValueError as x: + | ^ F841 +8 | pass + | + = help: Remove assignment to unused variable `x` + +ℹ Fix +4 4 | def f(): +5 5 | try: +6 6 | pass +7 |- except ValueError as x: + 7 |+ except ValueError: +8 8 | pass +9 9 | +10 10 | try: + +:12:26: F841 [*] Local variable `x` is assigned to but never used + | +10 | try: +11 | pass +12 | except ValueError as x: + | ^ F841 +13 | pass + | + = help: Remove assignment to unused variable `x` + +ℹ Fix +9 9 | +10 10 | try: +11 11 | pass +12 |- except ValueError as x: + 12 |+ except ValueError: +13 13 | pass +14 14 | +15 15 | # This should resolve to the `x` in `x = 1`. + + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_class_scope.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_class_scope.snap new file mode 100644 index 0000000000..058a92ae6f --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_class_scope.snap @@ -0,0 +1,32 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +:8:30: F841 [*] Local variable `x` is assigned to but never used + | +6 | try: +7 | pass +8 | except ValueError as x: + | ^ F841 +9 | pass + | + = help: Remove assignment to unused variable `x` + +ℹ Fix +5 5 | def f(): +6 6 | try: +7 7 | pass +8 |- except ValueError as x: + 8 |+ except ValueError: +9 9 | pass +10 10 | +11 11 | # This should raise an F821 error, rather than resolving to the + +:13:15: F821 Undefined name `x` + | +11 | # This should raise an F821 error, rather than resolving to the +12 | # `x` in `x = 1`. +13 | print(x) + | ^ F821 + | + + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_module_scope.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_module_scope.snap new file mode 100644 index 0000000000..2f25f19aeb --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_module_scope.snap @@ -0,0 +1,24 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +:7:26: F841 [*] Local variable `x` is assigned to but never used + | +5 | try: +6 | pass +7 | except ValueError as x: + | ^ F841 +8 | pass + | + = help: Remove assignment to unused variable `x` + +ℹ Fix +4 4 | def f(): +5 5 | try: +6 6 | pass +7 |- except ValueError as x: + 7 |+ except ValueError: +8 8 | pass +9 9 | +10 10 | # This should resolve to the `x` in `x = 1`. + + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_nested_module_scope.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_nested_module_scope.snap new file mode 100644 index 0000000000..7ab30bf910 --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_nested_module_scope.snap @@ -0,0 +1,44 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +:7:26: F841 [*] Local variable `x` is assigned to but never used + | +5 | try: +6 | pass +7 | except ValueError as x: + | ^ F841 +8 | pass + | + = help: Remove assignment to unused variable `x` + +ℹ Fix +4 4 | def f(): +5 5 | try: +6 6 | pass +7 |- except ValueError as x: + 7 |+ except ValueError: +8 8 | pass +9 9 | +10 10 | def g(): + +:13:30: F841 [*] Local variable `x` is assigned to but never used + | +11 | try: +12 | pass +13 | except ValueError as x: + | ^ F841 +14 | pass + | + = help: Remove assignment to unused variable `x` + +ℹ Fix +10 10 | def g(): +11 11 | try: +12 12 | pass +13 |- except ValueError as x: + 13 |+ except ValueError: +14 14 | pass +15 15 | +16 16 | # This should resolve to the `x` in `x = 1`. + + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_body_after_double_shadowing_except.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_body_after_double_shadowing_except.snap new file mode 100644 index 0000000000..616577852c --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_body_after_double_shadowing_except.snap @@ -0,0 +1,45 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +:7:26: F841 [*] Local variable `x` is assigned to but never used + | +5 | try: +6 | 1 / 0 +7 | except ValueError as x: + | ^ F841 +8 | pass +9 | except ImportError as x: + | + = help: Remove assignment to unused variable `x` + +ℹ Fix +4 4 | +5 5 | try: +6 6 | 1 / 0 +7 |- except ValueError as x: + 7 |+ except ValueError: +8 8 | pass +9 9 | except ImportError as x: +10 10 | pass + +:9:27: F841 [*] Local variable `x` is assigned to but never used + | + 7 | except ValueError as x: + 8 | pass + 9 | except ImportError as x: + | ^ F841 +10 | pass + | + = help: Remove assignment to unused variable `x` + +ℹ Fix +6 6 | 1 / 0 +7 7 | except ValueError as x: +8 8 | pass +9 |- except ImportError as x: + 9 |+ except ImportError: +10 10 | pass +11 11 | +12 12 | # No error here, though it should arguably be an F821 error. `x` will + + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_after_shadowing_except.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_body_after_shadowing_except.snap similarity index 76% rename from crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_after_shadowing_except.snap rename to crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_body_after_shadowing_except.snap index 1bc5062f45..085cb7c453 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_after_shadowing_except.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_body_after_shadowing_except.snap @@ -19,14 +19,6 @@ source: crates/ruff/src/rules/pyflakes/mod.rs 7 |+ except Exception: 8 8 | pass 9 9 | -10 10 | print(x) - -:10:11: F821 Undefined name `x` - | - 8 | pass - 9 | -10 | print(x) - | ^ F821 - | +10 10 | # No error here, though it should arguably be an F821 error. `x` will diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_if_else_after_shadowing_except.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_if_else_after_shadowing_except.snap new file mode 100644 index 0000000000..1976c4331d --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_if_else_after_shadowing_except.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_try_else_after_shadowing_except.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_try_else_after_shadowing_except.snap new file mode 100644 index 0000000000..1976c4331d --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_try_else_after_shadowing_except.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- + diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index 833ad4effe..00d138494f 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -75,7 +75,7 @@ impl<'a> Binding<'a> { pub const fn is_unbound(&self) -> bool { matches!( self.kind, - BindingKind::Annotation | BindingKind::Deletion | BindingKind::UnboundException + BindingKind::Annotation | BindingKind::Deletion | BindingKind::UnboundException(_) ) } @@ -427,7 +427,11 @@ pub enum BindingKind<'a> { /// /// After the `except` block, `x` is unbound, despite the lack /// of an explicit `del` statement. - UnboundException, + /// + /// + /// Stores the ID of the binding that was shadowed in the enclosing + /// scope, if any. + UnboundException(Option), } bitflags! { diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 3ea95b2dee..d39d36def7 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -327,14 +327,56 @@ impl<'a> SemanticModel<'a> { // ``` // // The `x` in `print(x)` should be treated as unresolved. - BindingKind::Deletion | BindingKind::UnboundException => { + // + // Similarly, given: + // + // ```python + // try: + // pass + // except ValueError as x: + // pass + // + // print(x) + // + // The `x` in `print(x)` should be treated as unresolved. + BindingKind::Deletion | BindingKind::UnboundException(None) => { return ResolvedRead::UnboundLocal(binding_id) } - // Otherwise, treat it as resolved. - _ => { + // If we hit an unbound exception that shadowed a bound name, resole to the + // bound name. For example, given: + // + // ```python + // x = 1 + // + // try: + // pass + // except ValueError as x: + // pass + // + // print(x) + // ``` + // + // The `x` in `print(x)` should resolve to the `x` in `x = 1`. + BindingKind::UnboundException(Some(binding_id)) => { + // Mark the binding as used. + let context = self.execution_context(); + let reference_id = self.references.push(self.scope_id, range, context); + self.bindings[binding_id].references.push(reference_id); + + // Mark any submodule aliases as used. + if let Some(binding_id) = + self.resolve_submodule(symbol, scope_id, binding_id) + { + let reference_id = self.references.push(self.scope_id, range, context); + self.bindings[binding_id].references.push(reference_id); + } + return ResolvedRead::Resolved(binding_id); } + + // Otherwise, treat it as resolved. + _ => return ResolvedRead::Resolved(binding_id), } } @@ -370,6 +412,50 @@ impl<'a> SemanticModel<'a> { } } + /// Lookup a symbol in the current scope. This is a carbon copy of [`Self::resolve_read`], but + /// doesn't add any read references to the resolved symbol. + pub fn lookup(&mut self, symbol: &str) -> Option { + if self.in_forward_reference() { + if let Some(binding_id) = self.scopes.global().get(symbol) { + if !self.bindings[binding_id].is_unbound() { + return Some(binding_id); + } + } + } + + let mut seen_function = false; + for (index, scope_id) in self.scopes.ancestor_ids(self.scope_id).enumerate() { + let scope = &self.scopes[scope_id]; + if scope.kind.is_class() { + if seen_function && matches!(symbol, "__class__") { + return None; + } + if index > 0 { + continue; + } + } + + if let Some(binding_id) = scope.get(symbol) { + match self.bindings[binding_id].kind { + BindingKind::Annotation => continue, + BindingKind::Deletion | BindingKind::UnboundException(None) => return None, + BindingKind::UnboundException(Some(binding_id)) => return Some(binding_id), + _ => return Some(binding_id), + } + } + + if index == 0 && scope.kind.is_class() { + if matches!(symbol, "__module__" | "__qualname__") { + return None; + } + } + + seen_function |= scope.kind.is_any_function(); + } + + None + } + /// Given a `BindingId`, return the `BindingId` of the submodule import that it aliases. fn resolve_submodule( &self, From 0aa21277c64d60dcf0819cf5dfa2232687ec764d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 21 Jun 2023 13:02:20 -0400 Subject: [PATCH 160/447] Improve documentation for overlong-line rules (#5260) Closes https://github.com/astral-sh/ruff/issues/5248. --- .../pycodestyle/rules/doc_line_too_long.rs | 23 ++++++++++++++++++- .../rules/pycodestyle/rules/line_too_long.rs | 16 ++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/crates/ruff/src/rules/pycodestyle/rules/doc_line_too_long.rs b/crates/ruff/src/rules/pycodestyle/rules/doc_line_too_long.rs index 68890d0235..7c047ab120 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/doc_line_too_long.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/doc_line_too_long.rs @@ -10,7 +10,21 @@ use crate::settings::Settings; /// /// ## Why is this bad? /// For flowing long blocks of text (docstrings or comments), overlong lines -/// can hurt readability. +/// can hurt readability. [PEP 8], for example, recommends that such lines be +/// limited to 72 characters. +/// +/// In the context of this rule, a "doc line" is defined as a line consisting +/// of either a standalone comment or a standalone string, like a docstring. +/// +/// In the interest of pragmatism, this rule makes a few exceptions when +/// determining whether a line is overlong. Namely, it ignores lines that +/// consist of a single "word" (i.e., without any whitespace between its +/// characters), and lines that end with a URL (as long as the URL starts +/// before the line-length threshold). +/// +/// If `pycodestyle.ignore_overlong_task_comments` is `true`, this rule will +/// also ignore comments that start with any of the specified `task-tags` +/// (e.g., `# TODO:`). /// /// ## Example /// ```python @@ -26,6 +40,13 @@ use crate::settings::Settings; /// Duis auctor purus ut ex fermentum, at maximus est hendrerit. /// """ /// ``` +/// +/// +/// ## Options +/// - `task-tags` +/// - `pycodestyle.ignore-overlong-task-comments` +/// +/// [PEP 8]: https://peps.python.org/pep-0008/#maximum-line-length #[violation] pub struct DocLineTooLong(pub usize, pub usize); diff --git a/crates/ruff/src/rules/pycodestyle/rules/line_too_long.rs b/crates/ruff/src/rules/pycodestyle/rules/line_too_long.rs index 6cb228239b..780906c598 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/line_too_long.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/line_too_long.rs @@ -9,7 +9,18 @@ use crate::settings::Settings; /// Checks for lines that exceed the specified maximum character length. /// /// ## Why is this bad? -/// Overlong lines can hurt readability. +/// Overlong lines can hurt readability. [PEP 8], for example, recommends +/// limiting lines to 79 characters. +/// +/// In the interest of pragmatism, this rule makes a few exceptions when +/// determining whether a line is overlong. Namely, it ignores lines that +/// consist of a single "word" (i.e., without any whitespace between its +/// characters), and lines that end with a URL (as long as the URL starts +/// before the line-length threshold). +/// +/// If `pycodestyle.ignore_overlong_task_comments` is `true`, this rule will +/// also ignore comments that start with any of the specified `task-tags` +/// (e.g., `# TODO:`). /// /// ## Example /// ```python @@ -26,6 +37,9 @@ use crate::settings::Settings; /// /// ## Options /// - `task-tags` +/// - `pycodestyle.ignore-overlong-task-comments` +/// +/// [PEP 8]: https://peps.python.org/pep-0008/#maximum-line-length #[violation] pub struct LineTooLong(pub usize, pub usize); From 41ef17b007c0ab1f7efa6e29dac974109f6d2aca Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Wed, 21 Jun 2023 12:04:55 -0500 Subject: [PATCH 161/447] Add Applicability to pyflakes (#5253) --- .../rules/f_string_missing_placeholders.rs | 3 +- ..._rules__pyflakes__tests__F541_F541.py.snap | 30 +++++++++---------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/crates/ruff/src/rules/pyflakes/rules/f_string_missing_placeholders.rs b/crates/ruff/src/rules/pyflakes/rules/f_string_missing_placeholders.rs index b8d04af426..25171ea0eb 100644 --- a/crates/ruff/src/rules/pyflakes/rules/f_string_missing_placeholders.rs +++ b/crates/ruff/src/rules/pyflakes/rules/f_string_missing_placeholders.rs @@ -89,8 +89,7 @@ fn fix_f_string_missing_placeholders( checker: &mut Checker, ) -> Fix { let content = &checker.locator.contents()[TextRange::new(prefix_range.end(), tok_range.end())]; - #[allow(deprecated)] - Fix::unspecified(Edit::replacement( + Fix::automatic(Edit::replacement( unescape_f_string(content), prefix_range.start(), tok_range.end(), diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F541_F541.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F541_F541.py.snap index 34c7b466fa..1681859307 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F541_F541.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F541_F541.py.snap @@ -11,7 +11,7 @@ F541.py:6:5: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 3 3 | b = f"ghi{'jkl'}" 4 4 | 5 5 | # Errors @@ -32,7 +32,7 @@ F541.py:7:5: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 4 4 | 5 5 | # Errors 6 6 | c = f"def" @@ -53,7 +53,7 @@ F541.py:9:5: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 6 6 | c = f"def" 7 7 | d = f"def" + "ghi" 8 8 | e = ( @@ -74,7 +74,7 @@ F541.py:13:5: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 10 10 | "ghi" 11 11 | ) 12 12 | f = ( @@ -95,7 +95,7 @@ F541.py:14:5: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 11 11 | ) 12 12 | f = ( 13 13 | f"a" @@ -116,7 +116,7 @@ F541.py:16:5: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 13 13 | f"a" 14 14 | F"b" 15 15 | "c" @@ -137,7 +137,7 @@ F541.py:17:5: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 14 14 | F"b" 15 15 | "c" 16 16 | rf"d" @@ -158,7 +158,7 @@ F541.py:19:5: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 16 16 | rf"d" 17 17 | fr"e" 18 18 | ) @@ -178,7 +178,7 @@ F541.py:25:13: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 22 22 | g = f"ghi{123:{45}}" 23 23 | 24 24 | # Error @@ -198,7 +198,7 @@ F541.py:34:7: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 31 31 | f"{f'{v:0.2f}'}" 32 32 | 33 33 | # Errors @@ -219,7 +219,7 @@ F541.py:35:4: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 32 32 | 33 33 | # Errors 34 34 | f"{v:{f'0.2f'}}" @@ -240,7 +240,7 @@ F541.py:36:1: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 33 33 | # Errors 34 34 | f"{v:{f'0.2f'}}" 35 35 | f"{f''}" @@ -261,7 +261,7 @@ F541.py:37:1: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 34 34 | f"{v:{f'0.2f'}}" 35 35 | f"{f''}" 36 36 | f"{{test}}" @@ -281,7 +281,7 @@ F541.py:38:1: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 35 35 | f"{f''}" 36 36 | f"{{test}}" 37 37 | f'{{ 40 }}' @@ -302,7 +302,7 @@ F541.py:39:1: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 36 36 | f"{{test}}" 37 37 | f'{{ 40 }}' 38 38 | f"{{a {{x}}" From 2b76d88bd386f585540332a7327186676fe5e04d Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Wed, 21 Jun 2023 12:12:47 -0500 Subject: [PATCH 162/447] Add Applicability to pandas_vet (#5252) --- crates/ruff/src/rules/pandas_vet/fixes.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/ruff/src/rules/pandas_vet/fixes.rs b/crates/ruff/src/rules/pandas_vet/fixes.rs index 23ac1a5c3e..8a3d368f07 100644 --- a/crates/ruff/src/rules/pandas_vet/fixes.rs +++ b/crates/ruff/src/rules/pandas_vet/fixes.rs @@ -40,6 +40,5 @@ pub(super) fn convert_inplace_argument_to_assignment( false, ) .ok()?; - #[allow(deprecated)] - Some(Fix::unspecified_edits(insert_assignment, [remove_argument])) + Some(Fix::suggested_edits(insert_assignment, [remove_argument])) } From f9ffb3d50db14fca2c0fef84c77fcaf7aae92cbd Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Wed, 21 Jun 2023 12:22:01 -0500 Subject: [PATCH 163/447] Add Applicability to pylint (#5251) --- .../pylint/rules/invalid_string_characters.rs | 3 +-- .../src/rules/pylint/rules/nested_min_max.rs | 3 +-- .../src/rules/pylint/rules/sys_exit_alias.rs | 3 +-- .../rules/pylint/rules/useless_import_alias.rs | 3 +-- ...t__tests__PLE2510_invalid_characters.py.snap | 2 +- ...t__tests__PLE2512_invalid_characters.py.snap | 2 +- ...t__tests__PLE2513_invalid_characters.py.snap | 2 +- ...t__tests__PLE2514_invalid_characters.py.snap | Bin 411 -> 401 bytes ...t__tests__PLE2515_invalid_characters.py.snap | 8 ++++---- 9 files changed, 11 insertions(+), 15 deletions(-) diff --git a/crates/ruff/src/rules/pylint/rules/invalid_string_characters.rs b/crates/ruff/src/rules/pylint/rules/invalid_string_characters.rs index 7a792d3bcb..3b5b28fcf3 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_string_characters.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_string_characters.rs @@ -191,8 +191,7 @@ pub(crate) fn invalid_string_characters(locator: &Locator, range: TextRange) -> let location = range.start() + TextSize::try_from(column).unwrap(); let range = TextRange::at(location, c.text_len()); - #[allow(deprecated)] - diagnostics.push(Diagnostic::new(rule, range).with_fix(Fix::unspecified( + diagnostics.push(Diagnostic::new(rule, range).with_fix(Fix::automatic( Edit::range_replacement(replacement.to_string(), range), ))); } 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 4551376ef6..1ca5ad354a 100644 --- a/crates/ruff/src/rules/pylint/rules/nested_min_max.rs +++ b/crates/ruff/src/rules/pylint/rules/nested_min_max.rs @@ -157,8 +157,7 @@ pub(crate) fn nested_min_max( keywords: keywords.to_owned(), range: TextRange::default(), }); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&flattened_expr), expr.range(), ))); diff --git a/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs b/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs index 72005b96a6..c23bc22e6a 100644 --- a/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs +++ b/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs @@ -82,8 +82,7 @@ pub(crate) fn sys_exit_alias(checker: &mut Checker, func: &Expr) { checker.semantic(), )?; let reference_edit = Edit::range_replacement(binding, func.range()); - #[allow(deprecated)] - Ok(Fix::unspecified_edits(import_edit, [reference_edit])) + Ok(Fix::suggested_edits(import_edit, [reference_edit])) }); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pylint/rules/useless_import_alias.rs b/crates/ruff/src/rules/pylint/rules/useless_import_alias.rs index 708de5926b..481be6265a 100644 --- a/crates/ruff/src/rules/pylint/rules/useless_import_alias.rs +++ b/crates/ruff/src/rules/pylint/rules/useless_import_alias.rs @@ -49,8 +49,7 @@ pub(crate) fn useless_import_alias(checker: &mut Checker, alias: &Alias) { let mut diagnostic = Diagnostic::new(UselessImportAlias, alias.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( asname.to_string(), alias.range(), ))); diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2510_invalid_characters.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2510_invalid_characters.py.snap index 5760228fe2..7cba4adf6d 100644 --- a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2510_invalid_characters.py.snap +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2510_invalid_characters.py.snap @@ -12,7 +12,7 @@ invalid_characters.py:15:6: PLE2510 [*] Invalid unescaped character backspace, u | = help: Replace with escape sequence -ℹ Suggested fix +ℹ Fix 12 12 | # (Pylint, "C0414") => Rule::UselessImportAlias, 13 13 | # (Pylint, "C3002") => Rule::UnnecessaryDirectLambdaCall, 14 14 | #foo = 'hi' diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2512_invalid_characters.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2512_invalid_characters.py.snap index 19aec0795a..dbde22f71b 100644 --- a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2512_invalid_characters.py.snap +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2512_invalid_characters.py.snap @@ -12,7 +12,7 @@ invalid_characters.py:21:12: PLE2512 [*] Invalid unescaped character SUB, use "\ | = help: Replace with escape sequence -ℹ Suggested fix +ℹ Fix 18 18 | 19 19 | cr_ok = '\\r' 20 20 | diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2513_invalid_characters.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2513_invalid_characters.py.snap index 2a080f617d..d8b61f4e13 100644 --- a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2513_invalid_characters.py.snap +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2513_invalid_characters.py.snap @@ -12,7 +12,7 @@ invalid_characters.py:25:16: PLE2513 [*] Invalid unescaped character ESC, use "\ | = help: Replace with escape sequence -ℹ Suggested fix +ℹ Fix 22 22 | 23 23 | sub_ok = '\x1a' 24 24 | diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2514_invalid_characters.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2514_invalid_characters.py.snap index 9b1f9ba80aeb94978167822276aecea720ce6198..6adc278155badea216947efabd040490c3287c30 100644 GIT binary patch delta 13 UcmbQuJdt?=FC(MdWC_M*02ze?X8-^I delta 23 ecmbQpJezp~FC%wwX?l8UaY<^5LfT|L#%2Ik<_A&$ diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2515_invalid_characters.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2515_invalid_characters.py.snap index f6bdbe5ec5..b12204f098 100644 --- a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2515_invalid_characters.py.snap +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2515_invalid_characters.py.snap @@ -12,7 +12,7 @@ invalid_characters.py:34:13: PLE2515 [*] Invalid unescaped character zero-width- | = help: Replace with escape sequence -ℹ Suggested fix +ℹ Fix 31 31 | 32 32 | nul_ok = '\0' 33 33 | @@ -32,7 +32,7 @@ invalid_characters.py:38:36: PLE2515 [*] Invalid unescaped character zero-width- | = help: Replace with escape sequence -ℹ Suggested fix +ℹ Fix 35 35 | 36 36 | zwsp_ok = '\u200b' 37 37 | @@ -48,7 +48,7 @@ invalid_characters.py:39:60: PLE2515 [*] Invalid unescaped character zero-width- | = help: Replace with escape sequence -ℹ Suggested fix +ℹ Fix 36 36 | zwsp_ok = '\u200b' 37 37 | 38 38 | zwsp_after_multibyte_character = "ಫ​" @@ -63,7 +63,7 @@ invalid_characters.py:39:61: PLE2515 [*] Invalid unescaped character zero-width- | = help: Replace with escape sequence -ℹ Suggested fix +ℹ Fix 36 36 | zwsp_ok = '\u200b' 37 37 | 38 38 | zwsp_after_multibyte_character = "ಫ​" From c792c10eaa3494aa279df0856a74dec39dfe17f0 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Wed, 21 Jun 2023 22:55:27 +0530 Subject: [PATCH 164/447] Add support for nested quoted annotations in RUF013 (#5254) ## Summary This is a follow up on #5235 to add support for nested quoted annotations for RUF013. ## Test Plan `cargo test` --- .../resources/test/fixtures/ruff/RUF013_0.py | 11 +++- .../src/rules/ruff/rules/implicit_optional.rs | 57 ++++++++++--------- ..._ruff__tests__PY39_RUF013_RUF013_0.py.snap | 18 ++++++ ...ules__ruff__tests__RUF013_RUF013_0.py.snap | 18 ++++++ 4 files changed, 77 insertions(+), 27 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py b/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py index acb0e82cd3..371c57da11 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py @@ -206,9 +206,18 @@ def f(arg: "Optional[int]" = None): pass -def f(arg: Union["int", "str"] = None): # False negative +def f(arg: Union["int", "str"] = None): # RUF013 pass def f(arg: Union["int", "None"] = None): pass + + +def f(arg: Union["No" "ne", "int"] = None): + pass + + +# Avoid flagging when there's a parse error in the forward reference +def f(arg: Union["<>", "int"] = None): + pass diff --git a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs index e8702f9620..459d7eb451 100644 --- a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs +++ b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs @@ -7,6 +7,7 @@ use rustpython_parser::ast::{self, ArgWithDefault, Arguments, Constant, Expr, Op use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::is_const_none; +use ruff_python_ast::source_code::Locator; use ruff_python_ast::typing::parse_type_annotation; use ruff_python_semantic::SemanticModel; @@ -145,15 +146,15 @@ enum TypingTarget<'a> { None, Any, Object, - ForwardReference, Optional, + ForwardReference(Expr), Union(Vec<&'a Expr>), Literal(Vec<&'a Expr>), Annotated(&'a Expr), } impl<'a> TypingTarget<'a> { - fn try_from_expr(expr: &'a Expr, semantic: &SemanticModel) -> Option { + fn try_from_expr(expr: &'a Expr, semantic: &SemanticModel, locator: &Locator) -> Option { match expr { Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { if semantic.match_typing_expr(value, "Optional") { @@ -180,9 +181,14 @@ impl<'a> TypingTarget<'a> { .. }) => Some(TypingTarget::None), Expr::Constant(ast::ExprConstant { - value: Constant::Str(_), + value: Constant::Str(string), + range, .. - }) => Some(TypingTarget::ForwardReference), + }) => parse_type_annotation(string, *range, locator) + // In case of a parse error, we return `Any` to avoid false positives. + .map_or(Some(TypingTarget::Any), |(expr, _)| { + Some(TypingTarget::ForwardReference(expr)) + }), _ => semantic.resolve_call_path(expr).and_then(|call_path| { if semantic.match_typing_call_path(&call_path, "Any") { Some(TypingTarget::Any) @@ -196,44 +202,42 @@ impl<'a> TypingTarget<'a> { } /// Check if the [`TypingTarget`] explicitly allows `None`. - fn contains_none(&self, semantic: &SemanticModel) -> bool { + fn contains_none(&self, semantic: &SemanticModel, locator: &Locator) -> bool { match self { TypingTarget::None | TypingTarget::Optional | TypingTarget::Any | TypingTarget::Object => true, TypingTarget::Literal(elements) => elements.iter().any(|element| { - let Some(new_target) = TypingTarget::try_from_expr(element, semantic) else { + let Some(new_target) = TypingTarget::try_from_expr(element, semantic, locator) 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), + TypingTarget::Literal(_) => new_target.contains_none(semantic, locator), _ => false, } }), TypingTarget::Union(elements) => elements.iter().any(|element| { - let Some(new_target) = TypingTarget::try_from_expr(element, semantic) else { + let Some(new_target) = TypingTarget::try_from_expr(element, semantic, locator) else { return false; }; - match new_target { - TypingTarget::None => true, - _ => new_target.contains_none(semantic), - } + new_target.contains_none(semantic, locator) }), TypingTarget::Annotated(element) => { - let Some(new_target) = TypingTarget::try_from_expr(element, semantic) else { + let Some(new_target) = TypingTarget::try_from_expr(element, semantic, locator) else { return false; }; - match new_target { - TypingTarget::None => true, - _ => new_target.contains_none(semantic), - } + new_target.contains_none(semantic, locator) + } + TypingTarget::ForwardReference(expr) => { + let Some(new_target) = TypingTarget::try_from_expr(expr, semantic, locator) else { + return false; + }; + new_target.contains_none(semantic, locator) } - // TODO(charlie): Add support for nested forward references (e.g., `Union["A", "B"]`). - TypingTarget::ForwardReference => true, } } } @@ -248,8 +252,9 @@ impl<'a> TypingTarget<'a> { fn type_hint_explicitly_allows_none<'a>( annotation: &'a Expr, semantic: &SemanticModel, + locator: &Locator, ) -> Option<&'a Expr> { - let Some(target) = TypingTarget::try_from_expr(annotation, semantic) else { + let Some(target) = TypingTarget::try_from_expr(annotation, semantic, locator) else { return Some(annotation); }; match target { @@ -259,9 +264,9 @@ fn type_hint_explicitly_allows_none<'a>( // return the inner type if it doesn't allow `None`. If `Annotated` // is found nested inside another type, then the outer type should // be returned. - TypingTarget::Annotated(expr) => type_hint_explicitly_allows_none(expr, semantic), + TypingTarget::Annotated(expr) => type_hint_explicitly_allows_none(expr, semantic, locator), _ => { - if target.contains_none(semantic) { + if target.contains_none(semantic, locator) { None } else { Some(annotation) @@ -339,13 +344,13 @@ pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) { if let Expr::Constant(ast::ExprConstant { range, - value: Constant::Str(value), + value: Constant::Str(string), .. }) = annotation.as_ref() { // Quoted annotation. - if let Ok((annotation, kind)) = parse_type_annotation(value, *range, checker.locator) { - let Some(expr) = type_hint_explicitly_allows_none(&annotation, checker.semantic()) else { + 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) else { continue; }; let conversion_type = checker.settings.target_version.into(); @@ -361,7 +366,7 @@ pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) { } } else { // Unquoted annotation. - let Some(expr) = type_hint_explicitly_allows_none(annotation, checker.semantic()) else { + let Some(expr) = type_hint_explicitly_allows_none(annotation, checker.semantic(), checker.locator) else { continue; }; let conversion_type = checker.settings.target_version.into(); diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap index 323513456c..9ee735a80b 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap @@ -359,4 +359,22 @@ RUF013_0.py:201:12: RUF013 PEP 484 prohibits implicit `Optional` | = help: Convert to `Optional[T]` +RUF013_0.py:209:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +209 | def f(arg: Union["int", "str"] = None): # RUF013 + | ^^^^^^^^^^^^^^^^^^^ RUF013 +210 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +206 206 | pass +207 207 | +208 208 | +209 |-def f(arg: Union["int", "str"] = None): # RUF013 + 209 |+def f(arg: Optional[Union["int", "str"]] = None): # RUF013 +210 210 | pass +211 211 | +212 212 | + diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap index 25ffc55a4d..2f21fcd56b 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap @@ -359,4 +359,22 @@ RUF013_0.py:201:12: RUF013 PEP 484 prohibits implicit `Optional` | = help: Convert to `T | None` +RUF013_0.py:209:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +209 | def f(arg: Union["int", "str"] = None): # RUF013 + | ^^^^^^^^^^^^^^^^^^^ RUF013 +210 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +206 206 | pass +207 207 | +208 208 | +209 |-def f(arg: Union["int", "str"] = None): # RUF013 + 209 |+def f(arg: Union["int", "str"] | None = None): # RUF013 +210 210 | pass +211 211 | +212 212 | + From bf1a94ee545e7fd1b11ee8f706b08e5349198cf9 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 21 Jun 2023 13:29:09 -0400 Subject: [PATCH 165/447] Initialize caches for packages and standalone files (#5237) ## Summary While fixing https://github.com/astral-sh/ruff/pull/5233, I noticed that in FastAPI, 343 out of 823 files weren't hitting the cache. It turns out these are standalone files in the documentation that lack a "package root". Later, when looking up the cache entries, we fallback to the package directory. This PR ensures that we initialize the cache for both kinds of files: those that are in a package, and those that aren't. The total size of the FastAPI cache for me is now 388K. I also suspect that this approach is much faster than as initially written, since before, we were probably initializing one cache per _directory_. ## Test Plan Ran `cargo run -p ruff_cli -- check ../fastapi --verbose`; verified that, on second execution, there were no "Checking" entries in the logs. --- crates/ruff/src/packaging.rs | 2 +- crates/ruff_cli/src/commands/run.rs | 27 ++++++++++++++++++--------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/crates/ruff/src/packaging.rs b/crates/ruff/src/packaging.rs index c80eedecd0..c746bb4d6f 100644 --- a/crates/ruff/src/packaging.rs +++ b/crates/ruff/src/packaging.rs @@ -78,7 +78,7 @@ fn detect_package_root_with_cache<'a>( current } -/// Return a mapping from Python file to its package root. +/// Return a mapping from Python package to its package root. pub fn detect_package_roots<'a>( files: &[&'a Path], resolver: &'a Resolver, diff --git a/crates/ruff_cli/src/commands/run.rs b/crates/ruff_cli/src/commands/run.rs index 0698fd62ce..61cdf6bf7a 100644 --- a/crates/ruff_cli/src/commands/run.rs +++ b/crates/ruff_cli/src/commands/run.rs @@ -81,17 +81,18 @@ pub(crate) fn run( // Load the caches. let caches = bool::from(cache).then(|| { package_roots - .values() - .flatten() - .dedup() - .map(|package_root| { - let settings = resolver.resolve_all(package_root, pyproject_config); + .iter() + .map(|(package, package_root)| package_root.unwrap_or(package)) + .unique() + .par_bridge() + .map(|cache_root| { + let settings = resolver.resolve_all(cache_root, pyproject_config); let cache = Cache::open( &settings.cli.cache_dir, - package_root.to_path_buf(), + cache_root.to_path_buf(), &settings.lib, ); - (&**package_root, cache) + (cache_root, cache) }) .collect::>() }); @@ -109,8 +110,16 @@ pub(crate) fn run( .and_then(|package| *package); let settings = resolver.resolve_all(path, pyproject_config); - let package_root = package.unwrap_or_else(|| path.parent().unwrap_or(path)); - let cache = caches.as_ref().and_then(|caches| caches.get(&package_root)); + + let cache_root = package.unwrap_or_else(|| path.parent().unwrap_or(path)); + let cache = caches.as_ref().and_then(|caches| { + if let Some(cache) = caches.get(&cache_root) { + Some(cache) + } else { + debug!("No cache found for {}", cache_root.display()); + None + } + }); lint_path(path, package, settings, cache, noqa, autofix).map_err(|e| { (Some(path.to_owned()), { From bc63cc9b3c055b5091da1a8bf433cfb95f77f75c Mon Sep 17 00:00:00 2001 From: konstin Date: Wed, 21 Jun 2023 19:45:53 +0200 Subject: [PATCH 166/447] Fix remaining CPython formatter errors except for function argument separator comments (#5210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This fixes two problems discovered when trying to format the cpython repo with `cargo run --bin ruff_dev -- check-formatter-stability projects/cpython`: The first is to ignore try/except trailing comments for now since they lead to unstable formatting on the dummy. The second is to avoid dropping trailing if comments through placement: This changes the placement to keep a comment trailing an if-elif or if-elif-else to keep the comment a trailing comment on the entire if. Previously the last comment would have been lost. ```python if "first if":     pass elif "first elif":     pass ``` The last remaining problem in cpython so far is function signature argument separator comment placement which is its own PR on top of this. ## Test Plan I added test fixtures of minimized examples with links back to the original cpython location --- .../test/fixtures/ruff/statement/if.py | 27 ++++++++++ .../src/comments/placement.rs | 24 ++++++++- ...atter__tests__black_test__comments_py.snap | 40 +++++++------- ...__black_test__remove_except_parens_py.snap | 32 ++++++----- ...r__tests__ruff_test__statement__if_py.snap | 54 +++++++++++++++++++ .../src/statement/stmt_try.rs | 5 ++ 6 files changed, 148 insertions(+), 34 deletions(-) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py index 65b21e30e9..91303ef592 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py @@ -35,3 +35,30 @@ elif aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + else: ... + +# Regression test: Don't drop the trailing comment by associating it with the elif +# instead of the else. +# Originally found in https://github.com/python/cpython/blob/ab3823a97bdeefb0266b3c8d493f7f6223ce3686/Lib/dataclasses.py#L539 + +if "if 1": + pass +elif "elif 1": + pass +# Don't drop this comment 1 +x = 1 + +if "if 2": + pass +elif "elif 2": + pass +else: + pass +# Don't drop this comment 2 +x = 2 + +if "if 3": + pass +else: + pass +# Don't drop this comment 3 +x = 3 diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index fa6fa5baaa..32c795fee2 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -402,7 +402,11 @@ fn handle_trailing_body_comment<'a>( } // Only do something if the preceding node has a body (has indented statements). - let Some(last_child) = comment.preceding_node().and_then(last_child_in_body) else { + let Some(preceding_node) = comment.preceding_node() else { + return CommentPlacement::Default(comment); + }; + + let Some(last_child) = last_child_in_body(preceding_node) else { return CommentPlacement::Default(comment); }; @@ -415,8 +419,24 @@ fn handle_trailing_body_comment<'a>( // the indent-level doesn't depend on the tab width (the indent level must be the same if the tab width is 1 or 8). let comment_indentation_len = comment_indentation.len(); + // Keep the comment on the entire statement in case it's a trailing comment + // ```python + // if "first if": + // pass + // elif "first elif": + // pass + // # 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 { + return CommentPlacement::Default(comment); + }; + if comment_indentation_len == preceding_node_indentation.len() { + return CommentPlacement::Default(comment); + } + let mut current_child = last_child; - let mut parent_body = comment.preceding_node(); + let mut parent_body = Some(preceding_node); let mut grand_parent_body = None; loop { diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap index 5a2dcb790f..e9ed496189 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap @@ -109,38 +109,35 @@ async def wat(): ```diff --- Black +++ Ruff -@@ -4,24 +4,15 @@ +@@ -4,21 +4,15 @@ # # Has many lines. Many, many lines. # Many, many, many lines. -"""Module docstring. -+"NOT_YET_IMPLEMENTED_STRING" - +- -Possibly also many, many lines. -""" -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImport ++"NOT_YET_IMPLEMENTED_STRING" -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 -except ImportError: - import slow as fast -- -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImportFrom # some noqa comment - --# Some comment before a function. +NOT_YET_IMPLEMENTED_StmtTry - y = 1 - ( - # some strings -@@ -30,67 +21,50 @@ + + + # Some comment before a function. +@@ -30,67 +24,50 @@ def function(default=None): @@ -177,17 +174,17 @@ async def wat(): # 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.""" - @@ -245,6 +242,9 @@ NOT_YET_IMPLEMENTED_StmtImport NOT_YET_IMPLEMENTED_StmtImportFrom # some noqa comment NOT_YET_IMPLEMENTED_StmtTry + + +# Some comment before a function. y = 1 ( # some strings diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_except_parens_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_except_parens_py.snap index 21fdd1942d..680584a6d7 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_except_parens_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_except_parens_py.snap @@ -48,28 +48,31 @@ except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.ov ```diff --- Black +++ Ruff -@@ -1,42 +1,9 @@ +@@ -1,42 +1,17 @@ # These brackets are redundant, therefore remove. -try: - a.something -except AttributeError as err: - raise err -- --# This is tuple of exceptions. --# Although this could be replaced with just the exception, --# we do not remove brackets to preserve AST. ++NOT_YET_IMPLEMENTED_StmtTry + + # This is tuple of exceptions. + # Although this could be replaced with just the exception, + # we do not remove brackets to preserve AST. -try: - a.something -except (AttributeError,) as err: - raise err -- --# This is a tuple of exceptions. Do not remove brackets. ++NOT_YET_IMPLEMENTED_StmtTry + + # This is a tuple of exceptions. Do not remove brackets. -try: - a.something -except (AttributeError, ValueError) as err: - raise err -- --# Test long variants. ++NOT_YET_IMPLEMENTED_StmtTry + + # Test long variants. -try: - a.something -except ( @@ -77,9 +80,6 @@ except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.ov -) as err: - raise err +NOT_YET_IMPLEMENTED_StmtTry -+NOT_YET_IMPLEMENTED_StmtTry -+NOT_YET_IMPLEMENTED_StmtTry -+NOT_YET_IMPLEMENTED_StmtTry -try: - a.something @@ -104,8 +104,16 @@ except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.ov ```py # These brackets are redundant, therefore remove. NOT_YET_IMPLEMENTED_StmtTry + +# This is tuple of exceptions. +# Although this could be replaced with just the exception, +# we do not remove brackets to preserve AST. NOT_YET_IMPLEMENTED_StmtTry + +# This is a tuple of exceptions. Do not remove brackets. NOT_YET_IMPLEMENTED_StmtTry + +# Test long variants. NOT_YET_IMPLEMENTED_StmtTry NOT_YET_IMPLEMENTED_StmtTry diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap index a58a992e81..ef06f0e165 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap @@ -41,6 +41,33 @@ elif aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + else: ... + +# Regression test: Don't drop the trailing comment by associating it with the elif +# instead of the else. +# Originally found in https://github.com/python/cpython/blob/ab3823a97bdeefb0266b3c8d493f7f6223ce3686/Lib/dataclasses.py#L539 + +if "if 1": + pass +elif "elif 1": + pass +# Don't drop this comment 1 +x = 1 + +if "if 2": + pass +elif "elif 2": + pass +else: + pass +# Don't drop this comment 2 +x = 2 + +if "if 3": + pass +else: + pass +# Don't drop this comment 3 +x = 3 ``` @@ -83,6 +110,33 @@ elif aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + else: ... + +# Regression test: Don't drop the trailing comment by associating it with the elif +# instead of the else. +# Originally found in https://github.com/python/cpython/blob/ab3823a97bdeefb0266b3c8d493f7f6223ce3686/Lib/dataclasses.py#L539 + +if "NOT_YET_IMPLEMENTED_STRING": + pass +elif "NOT_YET_IMPLEMENTED_STRING": + pass +# Don't drop this comment 1 +x = 1 + +if "NOT_YET_IMPLEMENTED_STRING": + pass +elif "NOT_YET_IMPLEMENTED_STRING": + pass +else: + pass +# Don't drop this comment 2 +x = 2 + +if "NOT_YET_IMPLEMENTED_STRING": + pass +else: + pass +# Don't drop this comment 3 +x = 3 ``` diff --git a/crates/ruff_python_formatter/src/statement/stmt_try.rs b/crates/ruff_python_formatter/src/statement/stmt_try.rs index 35afc06669..faab829230 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_try.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_try.rs @@ -9,4 +9,9 @@ impl FormatNodeRule for FormatStmtTry { fn fmt_fields(&self, item: &StmtTry, f: &mut PyFormatter) -> FormatResult<()> { write!(f, [not_yet_implemented(item)]) } + + fn fmt_dangling_comments(&self, _node: &StmtTry, _f: &mut PyFormatter) -> FormatResult<()> { + // TODO(konstin): Needs node formatting or this leads to unstable formatting + Ok(()) + } } From d7c7484618405cb5d020e34d046fae379b2047d2 Mon Sep 17 00:00:00 2001 From: konstin Date: Wed, 21 Jun 2023 19:56:47 +0200 Subject: [PATCH 167/447] Format function argument separator comments (#5211) ## Summary This is a complete rewrite of the handling of `/` and `*` comment handling in function signatures. The key problem is that slash and star don't have a note. We now parse out the positions of slash and star and their respective preceding and following note. I've left code comments for each possible case of function signature structure and comment placement ## Test Plan I extended the function statement fixtures with cases that i found. If you have more weird edge cases your input would be appreciated. --- .../test/fixtures/ruff/statement/function.py | 134 ++++++ .../src/comments/placement.rs | 100 +---- .../src/other/arguments.rs | 423 +++++++++++++++++- ...ts__ruff_test__statement__function_py.snap | 276 ++++++++++++ 4 files changed, 847 insertions(+), 86 deletions(-) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py index 674b9ef43e..2c9450a926 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py @@ -97,3 +97,137 @@ def foo( b=3 + 2 # comment ): ... + + +# Comments on the slash or the star, both of which don't have a node +def f11( + a, + # positional only comment, leading + /, # positional only comment, trailing + b, +): + pass + +def f12( + a=1, + # positional only comment, leading + /, # positional only comment, trailing + b=2, +): + pass + +def f13( + a, + # positional only comment, leading + /, # positional only comment, trailing +): + pass + +def f21( + a=1, + # keyword only comment, leading + *, # keyword only comment, trailing + b=2, +): + pass + +def f22( + a, + # keyword only comment, leading + *, # keyword only comment, trailing + b, +): + pass + +def f23( + a, + # keyword only comment, leading + *args, # keyword only comment, trailing + b, +): + pass + +def f24( + # keyword only comment, leading + *, # keyword only comment, trailing + a +): + pass + + +def f31( + a=1, + # positional only comment, leading + /, # positional only comment, trailing + b=2, + # keyword only comment, leading + *, # keyword only comment, trailing + c=3, +): + pass + +def f32( + a, + # positional only comment, leading + /, # positional only comment, trailing + b, + # keyword only comment, leading + *, # keyword only comment, trailing + c, +): + pass + +def f33( + a, + # positional only comment, leading + /, # positional only comment, trailing + # keyword only comment, leading + *args, # keyword only comment, trailing + c, +): + pass + + +def f34( + a, + # positional only comment, leading + /, # positional only comment, trailing + # keyword only comment, leading + *, # keyword only comment, trailing + c, +): + pass + +def f35( + # keyword only comment, leading + *, # keyword only comment, trailing + c, +): + pass + +# Multiple trailing comments +def f41( + a, + / # 1 + , # 2 + # 3 + * # 4 + , # 5 + c, +): + pass + +# Multiple trailing comments strangely places. The goal here is only stable formatting, +# the comments are placed to strangely to keep their relative position intact +def f42( + a, + / # 1 + # 2 + , # 3 + # 4 + * # 5 + # 6 + , # 7 + c, +): + pass diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 32c795fee2..97502a7e67 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -1,12 +1,14 @@ use crate::comments::visitor::{CommentPlacement, DecoratedComment}; -use crate::comments::CommentLinePosition; use crate::expression::expr_slice::{assign_comment_in_slice, ExprSliceCommentSection}; +use crate::other::arguments::{ + assign_argument_separator_comment_placement, find_argument_separators, +}; use crate::trivia::{first_non_trivia_token_rev, SimpleTokenizer, Token, TokenKind}; use ruff_python_ast::node::{AnyNodeRef, AstNode}; use ruff_python_ast::source_code::Locator; use ruff_python_ast::whitespace; use ruff_python_whitespace::{PythonWhitespace, UniversalNewlines}; -use ruff_text_size::{TextRange, TextSize}; +use ruff_text_size::TextRange; use rustpython_parser::ast::{Expr, ExprSlice, Ranged}; use std::cmp::Ordering; @@ -24,7 +26,7 @@ pub(super) fn place_comment<'a>( handle_trailing_end_of_line_body_comment, handle_trailing_end_of_line_condition_comment, handle_module_level_own_line_comment_before_class_or_function_comment, - handle_positional_only_arguments_separator_comment, + handle_arguments_separator_comment, handle_trailing_binary_expression_left_or_operator_comment, handle_leading_function_with_decorators_comment, handle_dict_unpacking_comment, @@ -629,18 +631,11 @@ fn handle_trailing_end_of_line_condition_comment<'a>( CommentPlacement::Default(comment) } -/// Attaches comments for the positional-only arguments separator `/` as trailing comments to the -/// enclosing [`Arguments`] node. +/// Attaches comments for the positional only arguments separator `/` or the keywords only arguments +/// separator `*` as dangling comments to the enclosing [`Arguments`] node. /// -/// ```python -/// def test( -/// a, -/// # Positional arguments only after here -/// /, # trailing positional argument comment. -/// b, -/// ): pass -/// ``` -fn handle_positional_only_arguments_separator_comment<'a>( +/// See [`assign_argument_separator_comment_placement`] +fn handle_arguments_separator_comment<'a>( comment: DecoratedComment<'a>, locator: &Locator, ) -> CommentPlacement<'a> { @@ -648,45 +643,19 @@ fn handle_positional_only_arguments_separator_comment<'a>( return CommentPlacement::Default(comment); }; - // Using the `/` without any leading arguments is a syntax error. - let Some(last_argument_or_default) = comment.preceding_node() else { - return CommentPlacement::Default(comment); - }; - - let is_last_positional_argument = - are_same_optional(last_argument_or_default, arguments.posonlyargs.last()); - - if !is_last_positional_argument { - return CommentPlacement::Default(comment); + let (slash, star) = find_argument_separators(locator.contents(), arguments); + let comment_range = comment.slice().range(); + let placement = assign_argument_separator_comment_placement( + slash.as_ref(), + star.as_ref(), + comment_range, + comment.line_position(), + ); + if placement.is_some() { + return CommentPlacement::dangling(comment.enclosing_node(), comment); } - let trivia_end = comment - .following_node() - .map_or(arguments.end(), |following| following.start()); - let trivia_range = TextRange::new(last_argument_or_default.end(), trivia_end); - - if let Some(slash_offset) = find_pos_only_slash_offset(trivia_range, locator) { - let comment_start = comment.slice().range().start(); - let is_slash_comment = match comment.line_position() { - CommentLinePosition::EndOfLine => { - let preceding_end_line = locator.line_end(last_argument_or_default.end()); - let slash_comments_start = preceding_end_line.min(slash_offset); - - comment_start >= slash_comments_start - && locator.line_end(slash_offset) > comment_start - } - CommentLinePosition::OwnLine => comment_start < slash_offset, - }; - - if is_slash_comment { - CommentPlacement::dangling(comment.enclosing_node(), comment) - } else { - CommentPlacement::Default(comment) - } - } else { - // Should not happen, but let's go with it - CommentPlacement::Default(comment) - } + CommentPlacement::Default(comment) } /// Handles comments between the left side and the operator of a binary expression (trailing comments of the left), @@ -937,35 +906,6 @@ fn handle_slice_comments<'a>( } } -/// Finds the offset of the `/` that separates the positional only and arguments from the other arguments. -/// Returns `None` if the positional only separator `/` isn't present in the specified range. -fn find_pos_only_slash_offset( - between_arguments_range: TextRange, - locator: &Locator, -) -> Option { - let mut tokens = - SimpleTokenizer::new(locator.contents(), between_arguments_range).skip_trivia(); - - if let Some(comma) = tokens.next() { - debug_assert_eq!(comma.kind(), TokenKind::Comma); - - if let Some(maybe_slash) = tokens.next() { - if maybe_slash.kind() == TokenKind::Slash { - return Some(maybe_slash.start()); - } - - debug_assert_eq!( - maybe_slash.kind(), - TokenKind::RParen, - "{:?}", - maybe_slash.kind() - ); - } - } - - None -} - /// Handles own line comments between the last function decorator and the *header* of the function. /// It attaches these comments as dangling comments to the function instead of making them /// leading argument comments. diff --git a/crates/ruff_python_formatter/src/other/arguments.rs b/crates/ruff_python_formatter/src/other/arguments.rs index ae1a08686c..0864249b85 100644 --- a/crates/ruff_python_formatter/src/other/arguments.rs +++ b/crates/ruff_python_formatter/src/other/arguments.rs @@ -5,11 +5,15 @@ use rustpython_parser::ast::{Arguments, Ranged}; use ruff_formatter::{format_args, write}; use ruff_python_ast::node::{AnyNodeRef, AstNode}; -use crate::comments::{dangling_node_comments, leading_node_comments}; +use crate::comments::{ + dangling_comments, leading_comments, leading_node_comments, trailing_comments, + CommentLinePosition, SourceComment, +}; use crate::context::NodeLevel; use crate::prelude::*; use crate::trivia::{first_non_trivia_token, SimpleTokenizer, Token, TokenKind}; use crate::FormatNodeRule; +use ruff_text_size::{TextRange, TextSize}; #[derive(Default)] pub struct FormatArguments; @@ -28,6 +32,10 @@ impl FormatNodeRule for FormatArguments { let saved_level = f.context().node_level(); f.context_mut().set_node_level(NodeLevel::Expression); + let comments = f.context().comments().clone(); + let dangling = comments.dangling_comments(item); + let (slash, star) = find_argument_separators(f.context().contents(), item); + let format_inner = format_with(|f: &mut PyFormatter| { let separator = format_with(|f| write!(f, [text(","), soft_line_break_or_space()])); let mut joiner = f.join_with(separator); @@ -39,9 +47,29 @@ impl FormatNodeRule for FormatArguments { last_node = Some(arg_with_default.into()); } - if !posonlyargs.is_empty() { - joiner.entry(&text("/")); - } + let slash_comments_end = if posonlyargs.is_empty() { + 0 + } else { + let slash_comments_end = dangling.partition_point(|comment| { + let assignment = assign_argument_separator_comment_placement( + slash.as_ref(), + star.as_ref(), + comment.slice().range(), + comment.line_position(), + ) + .expect("Unexpected dangling comment type in function arguments"); + matches!( + assignment, + ArgumentSeparatorCommentLocation::SlashLeading + | ArgumentSeparatorCommentLocation::SlashTrailing + ) + }); + joiner.entry(&CommentsAroundText { + text: "/", + comments: &dangling[..slash_comments_end], + }); + slash_comments_end + }; for arg_with_default in args { joiner.entry(&arg_with_default.format()); @@ -60,7 +88,26 @@ impl FormatNodeRule for FormatArguments { ]); last_node = Some(vararg.as_any_node_ref()); } else if !kwonlyargs.is_empty() { - joiner.entry(&text("*")); + // Given very strange comment placement, comments here may not actually have been + // marked as `StarLeading`/`StarTrailing`, but that's fine since we still produce + // a stable formatting in this case + // ```python + // def f42( + // a, + // / # 1 + // # 2 + // , # 3 + // # 4 + // * # 5 + // , # 6 + // c, + // ): + // pass + // ``` + joiner.entry(&CommentsAroundText { + text: "*", + comments: &dangling[slash_comments_end..], + }); } for arg_with_default in kwonlyargs { @@ -127,7 +174,7 @@ impl FormatNodeRule for FormatArguments { f, [ text("("), - block_indent(&dangling_node_comments(item)), + block_indent(&dangling_comments(dangling)), text(")") ] )?; @@ -152,3 +199,367 @@ impl FormatNodeRule for FormatArguments { Ok(()) } } + +struct CommentsAroundText<'a> { + text: &'static str, + comments: &'a [SourceComment], +} + +impl Format> for CommentsAroundText<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + if self.comments.is_empty() { + text(self.text).fmt(f) + } else { + // There might be own line comments in trailing, but those are weird and we can kinda + // ignore them + // ```python + // def f42( + // a, + // # leading comment (own line) + // / # first trailing comment (end-of-line) + // # trailing own line comment + // , + // c, + // ): + // ``` + let (leading, trailing) = self.comments.split_at( + self.comments + .partition_point(|comment| comment.line_position().is_own_line()), + ); + write!( + f, + [ + leading_comments(leading), + text(self.text), + trailing_comments(trailing) + ] + ) + } + } +} + +/// `/` and `*` in a function signature +/// +/// ```text +/// def f(arg_a, /, arg_b, *, arg_c): pass +/// ^ ^ ^ ^ ^ ^ slash preceding end +/// ^ ^ ^ ^ ^ slash (a separator) +/// ^ ^ ^ ^ slash following start +/// ^ ^ ^ star preceding end +/// ^ ^ star (a separator) +/// ^ star following start +/// ``` +#[derive(Debug)] +pub(crate) struct ArgumentSeparator { + /// The end of the last node or separator before this separator + pub(crate) preceding_end: TextSize, + /// The range of the separator itself + pub(crate) separator: TextRange, + /// The start of the first node or separator following this separator + pub(crate) following_start: TextSize, +} + +/// Finds slash and star in `f(a, /, b, *, c)` +/// +/// Returns slash and star +pub(crate) fn find_argument_separators( + contents: &str, + arguments: &Arguments, +) -> (Option, Option) { + // We only compute preceding_end and token location here since following_start depends on the + // star location, but the star location depends on slash's position + let slash = if let Some(preceding_end) = arguments.posonlyargs.last().map(Ranged::end) { + // ```text + // def f(a1=1, a2=2, /, a3, a4): pass + // ^^^^^^^^^^^ the range (defaults) + // def f(a1, a2, /, a3, a4): pass + // ^^^^^^^^^^^^ the range (no default) + // ``` + let range = TextRange::new(preceding_end, arguments.end()); + let mut tokens = SimpleTokenizer::new(contents, range).skip_trivia(); + + let comma = tokens + .next() + .expect("The function definition can't end here"); + debug_assert!(comma.kind() == TokenKind::Comma, "{comma:?}"); + let slash = tokens + .next() + .expect("The function definition can't end here"); + debug_assert!(slash.kind() == TokenKind::Slash, "{slash:?}"); + + Some((preceding_end, slash.range)) + } else { + None + }; + + // If we have a vararg we have a node that the comments attach to + let star = if arguments.vararg.is_some() { + // When the vararg is present the comments attach there and we don't need to do manual + // formatting + None + } else if let Some(first_keyword_argument) = arguments.kwonlyargs.first() { + // Check in that order: + // * `f(a, /, b, *, c)` and `f(a=1, /, b=2, *, c)` + // * `f(a, /, *, b)` + // * `f(*, b)` (else branch) + let after_arguments = arguments + .args + .last() + .map(|arg| arg.range.end()) + .or(slash.map(|(_, slash)| slash.end())); + if let Some(preceding_end) = after_arguments { + let range = TextRange::new(preceding_end, arguments.end()); + let mut tokens = SimpleTokenizer::new(contents, range).skip_trivia(); + + let comma = tokens + .next() + .expect("The function definition can't end here"); + debug_assert!(comma.kind() == TokenKind::Comma, "{comma:?}"); + let star = tokens + .next() + .expect("The function definition can't end here"); + debug_assert!(star.kind() == TokenKind::Star, "{star:?}"); + + Some(ArgumentSeparator { + preceding_end, + separator: star.range, + following_start: first_keyword_argument.start(), + }) + } else { + let mut tokens = SimpleTokenizer::new(contents, arguments.range).skip_trivia(); + + let lparen = tokens + .next() + .expect("The function definition can't end here"); + debug_assert!(lparen.kind() == TokenKind::LParen, "{lparen:?}"); + let star = tokens + .next() + .expect("The function definition can't end here"); + debug_assert!(star.kind() == TokenKind::Star, "{star:?}"); + Some(ArgumentSeparator { + preceding_end: arguments.range.start(), + separator: star.range, + following_start: first_keyword_argument.start(), + }) + } + } else { + None + }; + + // Now that we have star, compute how long slash trailing comments can go + // Check in that order: + // * `f(a, /, b)` + // * `f(a, /, *b)` + // * `f(a, /, *, b)` + // * `f(a, /)` + let slash_following_start = arguments + .args + .first() + .map(Ranged::start) + .or(arguments.vararg.as_ref().map(|first| first.start())) + .or(star.as_ref().map(|star| star.separator.start())) + .unwrap_or(arguments.end()); + let slash = slash.map(|(preceding_end, slash)| ArgumentSeparator { + preceding_end, + separator: slash, + following_start: slash_following_start, + }); + + (slash, star) +} + +/// Locates positional only arguments separator `/` or the keywords only arguments +/// separator `*` comments. +/// +/// ```python +/// def test( +/// a, +/// # Positional only arguments after here +/// /, # trailing positional argument comment. +/// b, +/// ): +/// pass +/// ``` +/// or +/// ```python +/// def f( +/// a="", +/// # Keyword only arguments only after here +/// *, # trailing keyword argument comment. +/// b="", +/// ): +/// pass +/// ``` +/// or +/// ```python +/// def f( +/// a, +/// # positional only comment, leading +/// /, # positional only comment, trailing +/// b, +/// # keyword only comment, leading +/// *, # keyword only comment, trailing +/// c, +/// ): +/// pass +/// ``` +/// Notably, the following is possible: +/// ```python +/// def f32( +/// a, +/// # positional only comment, leading +/// /, # positional only comment, trailing +/// # keyword only comment, leading +/// *, # keyword only comment, trailing +/// c, +/// ): +/// pass +/// ``` +/// +/// ## Background +/// +/// ```text +/// def f(a1, a2): pass +/// ^^^^^^ arguments (args) +/// ``` +/// Use a star to separate keyword only arguments: +/// ```text +/// def f(a1, a2, *, a3, a4): pass +/// ^^^^^^ arguments (args) +/// ^^^^^^ keyword only arguments (kwargs) +/// ``` +/// Use a slash to separate positional only arguments. Note that this changes the arguments left +/// of the slash while the star change the arguments right of it: +/// ```text +/// def f(a1, a2, /, a3, a4): pass +/// ^^^^^^ positional only arguments (posonlyargs) +/// ^^^^^^ arguments (args) +/// ``` +/// You can combine both: +/// ```text +/// def f(a1, a2, /, a3, a4, *, a5, a6): pass +/// ^^^^^^ positional only arguments (posonlyargs) +/// ^^^^^^ arguments (args) +/// ^^^^^^ keyword only arguments (kwargs) +/// ``` +/// They can all have defaults, meaning that the preceding node ends at the default instead of the +/// argument itself: +/// ```text +/// def f(a1=1, a2=2, /, a3=3, a4=4, *, a5=5, a6=6): pass +/// ^ ^ ^ ^ ^ ^ defaults +/// ^^^^^^^^^^ positional only arguments (posonlyargs) +/// ^^^^^^^^^^ arguments (args) +/// ^^^^^^^^^^ keyword only arguments (kwargs) +/// ``` +/// An especially difficult case is having no regular arguments, so comments from both slash and +/// star will attach to either a2 or a3 and the next token is incorrect. +/// ```text +/// def f(a1, a2, /, *, a3, a4): pass +/// ^^^^^^ positional only arguments (posonlyargs) +/// ^^^^^^ keyword only arguments (kwargs) +/// ``` +pub(crate) fn assign_argument_separator_comment_placement( + slash: Option<&ArgumentSeparator>, + star: Option<&ArgumentSeparator>, + comment_range: TextRange, + text_position: CommentLinePosition, +) -> Option { + if let Some(ArgumentSeparator { + preceding_end, + separator: slash, + following_start, + }) = slash + { + // ```python + // def f( + // # start too early + // a, # not own line + // # this is the one + // /, # too late (handled later) + // b, + // ) + // ``` + if comment_range.start() > *preceding_end + && comment_range.start() < slash.start() + && text_position.is_own_line() + { + return Some(ArgumentSeparatorCommentLocation::SlashLeading); + } + + // ```python + // def f( + // a, + // # too early (handled above) + // /, # this is the one + // # not end-of-line + // b, + // ) + // ``` + if comment_range.start() > slash.end() + && comment_range.start() < *following_start + && text_position.is_end_of_line() + { + return Some(ArgumentSeparatorCommentLocation::SlashTrailing); + } + } + + if let Some(ArgumentSeparator { + preceding_end, + separator: star, + following_start, + }) = star + { + // ```python + // def f( + // # start too early + // a, # not own line + // # this is the one + // *, # too late (handled later) + // b, + // ) + // ``` + if comment_range.start() > *preceding_end + && comment_range.start() < star.start() + && text_position.is_own_line() + { + return Some(ArgumentSeparatorCommentLocation::StarLeading); + } + + // ```python + // def f( + // a, + // # too early (handled above) + // *, # this is the one + // # not end-of-line + // b, + // ) + // ``` + if comment_range.start() > star.end() + && comment_range.start() < *following_start + && text_position.is_end_of_line() + { + return Some(ArgumentSeparatorCommentLocation::StarTrailing); + } + } + None +} + +/// ```python +/// def f( +/// a, +/// # before slash +/// /, # after slash +/// b, +/// # before star +/// *, # after star +/// c, +/// ): +/// pass +/// ``` +#[derive(Debug)] +pub(crate) enum ArgumentSeparatorCommentLocation { + SlashLeading, + SlashTrailing, + StarLeading, + StarTrailing, +} diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__function_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__function_py.snap index 222c32b4a4..c2f44bcdba 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__function_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__function_py.snap @@ -103,6 +103,140 @@ def foo( b=3 + 2 # comment ): ... + + +# Comments on the slash or the star, both of which don't have a node +def f11( + a, + # positional only comment, leading + /, # positional only comment, trailing + b, +): + pass + +def f12( + a=1, + # positional only comment, leading + /, # positional only comment, trailing + b=2, +): + pass + +def f13( + a, + # positional only comment, leading + /, # positional only comment, trailing +): + pass + +def f21( + a=1, + # keyword only comment, leading + *, # keyword only comment, trailing + b=2, +): + pass + +def f22( + a, + # keyword only comment, leading + *, # keyword only comment, trailing + b, +): + pass + +def f23( + a, + # keyword only comment, leading + *args, # keyword only comment, trailing + b, +): + pass + +def f24( + # keyword only comment, leading + *, # keyword only comment, trailing + a +): + pass + + +def f31( + a=1, + # positional only comment, leading + /, # positional only comment, trailing + b=2, + # keyword only comment, leading + *, # keyword only comment, trailing + c=3, +): + pass + +def f32( + a, + # positional only comment, leading + /, # positional only comment, trailing + b, + # keyword only comment, leading + *, # keyword only comment, trailing + c, +): + pass + +def f33( + a, + # positional only comment, leading + /, # positional only comment, trailing + # keyword only comment, leading + *args, # keyword only comment, trailing + c, +): + pass + + +def f34( + a, + # positional only comment, leading + /, # positional only comment, trailing + # keyword only comment, leading + *, # keyword only comment, trailing + c, +): + pass + +def f35( + # keyword only comment, leading + *, # keyword only comment, trailing + c, +): + pass + +# Multiple trailing comments +def f41( + a, + / # 1 + , # 2 + # 3 + * # 4 + , # 5 + c, +): + pass + +# Multiple trailing comments strangely places. The goal here is only stable formatting, +# the comments are placed to strangely to keep their relative position intact +def f42( + a, + / # 1 + # 2 + , # 3 + # 4 + * # 5 + # 6 + , # 7 + c, +): + pass ``` @@ -237,6 +371,148 @@ def foo( b=3 + 2, # comment ): ... + + +# Comments on the slash or the star, both of which don't have a node +def f11( + a, + # positional only comment, leading + /, # positional only comment, trailing + b, +): + pass + + +def f12( + a=1, + # positional only comment, leading + /, # positional only comment, trailing + b=2, +): + pass + + +def f13( + a, + # positional only comment, leading + /, # positional only comment, trailing +): + pass + + +def f21( + a=1, + # keyword only comment, leading + *, # keyword only comment, trailing + b=2, +): + pass + + +def f22( + a, + # keyword only comment, leading + *, # keyword only comment, trailing + b, +): + pass + + +def f23( + a, + # keyword only comment, leading + *args, # keyword only comment, trailing + b, +): + pass + + +def f24( + # keyword only comment, leading + *, # keyword only comment, trailing + a, +): + pass + + +def f31( + a=1, + # positional only comment, leading + /, # positional only comment, trailing + b=2, + # keyword only comment, leading + *, # keyword only comment, trailing + c=3, +): + pass + + +def f32( + a, + # positional only comment, leading + /, # positional only comment, trailing + b, + # keyword only comment, leading + *, # keyword only comment, trailing + c, +): + pass + + +def f33( + a, + # positional only comment, leading + /, # positional only comment, trailing + # keyword only comment, leading + *args, # keyword only comment, trailing + c, +): + pass + + +def f34( + a, + # positional only comment, leading + /, # positional only comment, trailing + # keyword only comment, leading + *, # keyword only comment, trailing + c, +): + pass + + +def f35( + # keyword only comment, leading + *, # keyword only comment, trailing + c, +): + pass + + +# Multiple trailing comments +def f41( + a, + /, # 1 # 2 + # 3 + *, # 4 # 5 + c, +): + pass + + +# Multiple trailing comments strangely places. The goal here is only stable formatting, +# the comments are placed to strangely to keep their relative position intact +def f42( + a, + /, # 1 + # 2 + # 3 + # 4 + *, # 5 # 7 + # 6 + c, +): + pass ``` From 9b5fb8f38f837eeb4e92f03eef741bddd5861cb6 Mon Sep 17 00:00:00 2001 From: James Berry Date: Wed, 21 Jun 2023 14:40:58 -0400 Subject: [PATCH 168/447] Fix AST visitor traversal order (#5221) ## Summary According to the AST visitor documentation, the AST visitor "visits all nodes in the AST recursively in evaluation-order". However, the current traversal fails to meet this specification in a few places. ### Function traversal ```python order = [] @(order.append("decorator") or (lambda x: x)) def f( posonly: order.append("posonly annotation") = order.append("posonly default"), /, arg: order.append("arg annotation") = order.append("arg default"), *args: order.append("vararg annotation"), kwarg: order.append("kwarg annotation") = order.append("kwarg default"), **kwargs: order.append("kwarg annotation") ) -> order.append("return annotation"): pass print(order) ``` Executing the above snippet using CPython 3.10.6 prints the following result (formatted for readability): ```python [ 'decorator', 'posonly default', 'arg default', 'kwarg default', 'arg annotation', 'posonly annotation', 'vararg annotation', 'kwarg annotation', 'kwarg annotation', 'return annotation', ] ``` Here we can see that decorators are evaluated first, followed by argument defaults, and annotations are last. The current traversal of a function's AST does not align with this order. ### Annotated assignment traversal ```python order = [] x: order.append("annotation") = order.append("expression") print(order) ``` Executing the above snippet using CPython 3.10.6 prints the following result: ```python ['expression', 'annotation'] ``` Here we can see that an annotated assignments annotation gets evaluated after the assignment's expression. The current traversal of an annotated assignment's AST does not align with this order. ## Why? I'm slowly working on #3946 and porting over some of the logic and tests from ssort. ssort is very sensitive to AST traversal order, so ensuring the utmost correctness here is important. ## Test Plan There doesn't seem to be existing tests for the AST visitor, so I didn't bother adding tests for these very subtle changes. However, this behavior will be captured in the tests for the PR which addresses #3946. --- crates/ruff_python_ast/src/visitor.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/ruff_python_ast/src/visitor.rs b/crates/ruff_python_ast/src/visitor.rs index e9f506fc8a..5830b8e09d 100644 --- a/crates/ruff_python_ast/src/visitor.rs +++ b/crates/ruff_python_ast/src/visitor.rs @@ -99,10 +99,10 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { returns, .. }) => { - visitor.visit_arguments(args); for decorator in decorator_list { visitor.visit_decorator(decorator); } + visitor.visit_arguments(args); for expr in returns { visitor.visit_annotation(expr); } @@ -115,10 +115,10 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { returns, .. }) => { - visitor.visit_arguments(args); for decorator in decorator_list { visitor.visit_decorator(decorator); } + visitor.visit_arguments(args); for expr in returns { visitor.visit_annotation(expr); } @@ -131,15 +131,15 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { decorator_list, .. }) => { + for decorator in decorator_list { + visitor.visit_decorator(decorator); + } for expr in bases { visitor.visit_expr(expr); } for keyword in keywords { visitor.visit_keyword(keyword); } - for decorator in decorator_list { - visitor.visit_decorator(decorator); - } visitor.visit_body(body); } Stmt::Return(ast::StmtReturn { @@ -180,10 +180,10 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { value, .. }) => { - visitor.visit_annotation(annotation); if let Some(expr) = value { visitor.visit_expr(expr); } + visitor.visit_annotation(annotation); visitor.visit_expr(target); } Stmt::For(ast::StmtFor { @@ -633,10 +633,10 @@ pub fn walk_arg_with_default<'a, V: Visitor<'a> + ?Sized>( visitor: &mut V, arg_with_default: &'a ArgWithDefault, ) { - visitor.visit_arg(&arg_with_default.def); if let Some(expr) = &arg_with_default.default { visitor.visit_expr(expr); } + visitor.visit_arg(&arg_with_default.def); } pub fn walk_keyword<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, keyword: &'a Keyword) { From 9419d3f9c89c297d8202552be78c024cd45a9976 Mon Sep 17 00:00:00 2001 From: konstin Date: Wed, 21 Jun 2023 21:17:47 +0200 Subject: [PATCH 169/447] Special `ExprTuple` formatting option for `for`-loops (#5175) ## Motivation While black keeps parentheses nearly everywhere, the notable exception is in the body of for loops: ```python for (a, b) in x: pass ``` becomes ```python for a, b in x: pass ``` This currently blocks #5163, which this PR should unblock. ## Solution This changes the `ExprTuple` formatting option to include one additional option that removes the parentheses when not using magic trailing comma and not breaking. It is supposed to be used through ```rust #[derive(Debug)] struct ExprTupleWithoutParentheses<'a>(&'a Expr); impl Format> for ExprTupleWithoutParentheses<'_> { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { match self.0 { Expr::Tuple(expr_tuple) => expr_tuple .format() .with_options(TupleParentheses::StripInsideForLoop) .fmt(f), other => other.format().with_options(Parenthesize::IfBreaks).fmt(f), } } } ``` ## Testing The for loop formatting isn't merged due to missing this (and i didn't want to create more git weirdness across two people), but I've confirmed that when applying this to while loops instead of for loops, then ```rust write!( f, [ text("while"), space(), ExprTupleWithoutParentheses(test.as_ref()), text(":"), trailing_comments(trailing_condition_comments), block_indent(&body.format()) ] )?; ``` makes ```python while (a, b): pass while ( ajssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssa, b, ): pass while (a,b,): pass ``` formatted as ```python while a, b: pass while ( ajssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssa, b, ): pass while ( a, b, ): pass ``` --- .../src/expression/expr_tuple.rs | 50 +++++++++++++++++-- .../src/expression/mod.rs | 6 ++- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/crates/ruff_python_formatter/src/expression/expr_tuple.rs b/crates/ruff_python_formatter/src/expression/expr_tuple.rs index e7d1ce0865..cc60fd57ea 100644 --- a/crates/ruff_python_formatter/src/expression/expr_tuple.rs +++ b/crates/ruff_python_formatter/src/expression/expr_tuple.rs @@ -10,13 +10,48 @@ use ruff_formatter::formatter::Formatter; use ruff_formatter::prelude::{ block_indent, group, if_group_breaks, soft_block_indent, soft_line_break_or_space, text, }; -use ruff_formatter::{format_args, write, Buffer, Format, FormatResult}; +use ruff_formatter::{format_args, write, Buffer, Format, FormatResult, FormatRuleWithOptions}; use ruff_python_ast::prelude::{Expr, Ranged}; use ruff_text_size::TextRange; use rustpython_parser::ast::ExprTuple; +#[derive(Eq, PartialEq, Debug, Default)] +pub enum TupleParentheses { + /// Effectively `None` in `Option` + #[default] + Default, + /// Effectively `Some(Parentheses)` in `Option` + Expr(Parentheses), + /// Handle the special case where we remove parentheses even if they were initially present + /// + /// Normally, black keeps parentheses, but in the case of loops it formats + /// ```python + /// for (a, b) in x: + /// pass + /// ``` + /// to + /// ```python + /// for a, b in x: + /// pass + /// ``` + /// Black still does use parentheses in this position if the group breaks or magic trailing + /// comma is used. + StripInsideForLoop, +} + #[derive(Default)] -pub struct FormatExprTuple; +pub struct FormatExprTuple { + parentheses: TupleParentheses, +} + +impl FormatRuleWithOptions> for FormatExprTuple { + type Options = TupleParentheses; + + fn with_options(mut self, options: Self::Options) -> Self { + self.parentheses = options; + self + } +} impl FormatNodeRule for FormatExprTuple { fn fmt_fields(&self, item: &ExprTuple, f: &mut PyFormatter) -> FormatResult<()> { @@ -74,9 +109,14 @@ impl FormatNodeRule for FormatExprTuple { &text(")"), ] )?; - } else if is_parenthesized(*range, elts, f) { - // If the tuple has parentheses, keep them. Note that unlike other expr parentheses, - // those are actually part of the range + } else if is_parenthesized(*range, elts, f) + && self.parentheses != TupleParentheses::StripInsideForLoop + { + // If the tuple has parentheses, we generally want to keep them. The exception are for + // loops, see `TupleParentheses::StripInsideForLoop` doc comment. + // + // Unlike other expression parentheses, tuple parentheses are part of the range of the + // tuple itself. write!( f, [group(&format_args![ diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index a3670b1761..6637c091c6 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -1,5 +1,6 @@ use crate::comments::Comments; use crate::context::NodeLevel; +use crate::expression::expr_tuple::TupleParentheses; use crate::expression::parentheses::{NeedsParentheses, Parentheses, Parenthesize}; use crate::prelude::*; use ruff_formatter::{ @@ -85,7 +86,10 @@ impl FormatRule> for FormatExpr { Expr::Starred(expr) => expr.format().fmt(f), Expr::Name(expr) => expr.format().fmt(f), Expr::List(expr) => expr.format().fmt(f), - Expr::Tuple(expr) => expr.format().fmt(f), + Expr::Tuple(expr) => expr + .format() + .with_options(TupleParentheses::Expr(parentheses)) + .fmt(f), Expr::Slice(expr) => expr.format().fmt(f), }); From 62e2c46f980a0a4f363a89566da3e740190c33b0 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 21 Jun 2023 15:47:02 -0400 Subject: [PATCH 170/447] Move `compare-to-empty-string` to nursery (#5264) ## Summary This rule has too many false positives. It has parity with the Pylint version, but the Pylint version is part of an [extension](https://pylint.readthedocs.io/en/stable/user_guide/messages/convention/compare-to-empty-string.html), and so requires explicit opt-in. I'm moving this rule to the nursery to require explicit opt-in, as with Pylint. Closes #4282. --- crates/ruff/src/codes.rs | 2 +- .../pylint/rules/compare_to_empty_string.rs | 106 +++++++++--------- ruff.schema.json | 3 - 3 files changed, 56 insertions(+), 55 deletions(-) diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 68409be315..f4f6211e43 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -157,7 +157,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { // pylint (Pylint, "C0414") => (RuleGroup::Unspecified, rules::pylint::rules::UselessImportAlias), - (Pylint, "C1901") => (RuleGroup::Unspecified, rules::pylint::rules::CompareToEmptyString), + (Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString), (Pylint, "C3002") => (RuleGroup::Unspecified, rules::pylint::rules::UnnecessaryDirectLambdaCall), (Pylint, "C0208") => (RuleGroup::Unspecified, rules::pylint::rules::IterationOverSet), (Pylint, "E0100") => (RuleGroup::Unspecified, rules::pylint::rules::YieldInInit), diff --git a/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs b/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs index da44e79fe3..078feea363 100644 --- a/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs +++ b/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs @@ -7,49 +7,6 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub(crate) enum EmptyStringCmpOp { - Is, - IsNot, - Eq, - NotEq, -} - -impl TryFrom<&CmpOp> for EmptyStringCmpOp { - type Error = anyhow::Error; - - fn try_from(value: &CmpOp) -> Result { - match value { - CmpOp::Is => Ok(Self::Is), - CmpOp::IsNot => Ok(Self::IsNot), - CmpOp::Eq => Ok(Self::Eq), - CmpOp::NotEq => Ok(Self::NotEq), - _ => bail!("{value:?} cannot be converted to EmptyStringCmpOp"), - } - } -} - -impl EmptyStringCmpOp { - pub(crate) fn into_unary(self) -> &'static str { - match self { - Self::Is | Self::Eq => "not ", - Self::IsNot | Self::NotEq => "", - } - } -} - -impl std::fmt::Display for EmptyStringCmpOp { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let repr = match self { - Self::Is => "is", - Self::IsNot => "is not", - Self::Eq => "==", - Self::NotEq => "!=", - }; - write!(f, "{repr}") - } -} - /// ## What it does /// Checks for comparisons to empty strings. /// @@ -83,13 +40,15 @@ pub struct CompareToEmptyString { impl Violation for CompareToEmptyString { #[derive_message_formats] fn message(&self) -> String { - format!( - "`{}` can be simplified to `{}` as an empty string is falsey", - self.existing, self.replacement, - ) + let CompareToEmptyString { + existing, + replacement, + } = self; + format!("`{existing}` can be simplified to `{replacement}` as an empty string is falsey",) } } +/// PLC1901 pub(crate) fn compare_to_empty_string( checker: &mut Checker, left: &Expr, @@ -98,10 +57,12 @@ pub(crate) fn compare_to_empty_string( ) { // Omit string comparison rules within subscripts. This is mostly commonly used within // DataFrame and np.ndarray indexing. - for parent in checker.semantic().expr_ancestors() { - if matches!(parent, Expr::Subscript(_)) { - return; - } + if checker + .semantic() + .expr_ancestors() + .any(|parent| parent.is_subscript_expr()) + { + return; } let mut first = true; @@ -153,3 +114,46 @@ pub(crate) fn compare_to_empty_string( } } } + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum EmptyStringCmpOp { + Is, + IsNot, + Eq, + NotEq, +} + +impl TryFrom<&CmpOp> for EmptyStringCmpOp { + type Error = anyhow::Error; + + fn try_from(value: &CmpOp) -> Result { + match value { + CmpOp::Is => Ok(Self::Is), + CmpOp::IsNot => Ok(Self::IsNot), + CmpOp::Eq => Ok(Self::Eq), + CmpOp::NotEq => Ok(Self::NotEq), + _ => bail!("{value:?} cannot be converted to EmptyStringCmpOp"), + } + } +} + +impl EmptyStringCmpOp { + fn into_unary(self) -> &'static str { + match self { + Self::Is | Self::Eq => "not ", + Self::IsNot | Self::NotEq => "", + } + } +} + +impl std::fmt::Display for EmptyStringCmpOp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let repr = match self { + Self::Is => "is", + Self::IsNot => "is not", + Self::Eq => "==", + Self::NotEq => "!=", + }; + write!(f, "{repr}") + } +} diff --git a/ruff.schema.json b/ruff.schema.json index 77cd975fac..b6c0437890 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2093,9 +2093,6 @@ "PLC04", "PLC041", "PLC0414", - "PLC1", - "PLC19", - "PLC190", "PLC1901", "PLC3", "PLC30", From f194572be83843f8162d128b203976e6197d36db Mon Sep 17 00:00:00 2001 From: James Berry Date: Wed, 21 Jun 2023 16:00:24 -0400 Subject: [PATCH 171/447] Remove visit_arg_with_default (#5265) ## Summary This is a follow up to #5221. Turns out it was easy to restructure the visitor to get the right order, I'm just dumb :man_shrugging: I've removed `visit_arg_with_default` entirely from the `Visitor`, although it still exists as part of `preorder::Visitor`. --- crates/ruff_python_ast/src/visitor.rs | 38 +++++++++++++++------------ 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/crates/ruff_python_ast/src/visitor.rs b/crates/ruff_python_ast/src/visitor.rs index 5830b8e09d..766ed29441 100644 --- a/crates/ruff_python_ast/src/visitor.rs +++ b/crates/ruff_python_ast/src/visitor.rs @@ -2,7 +2,7 @@ pub mod preorder; -use rustpython_ast::{ArgWithDefault, Decorator}; +use rustpython_ast::Decorator; use rustpython_parser::ast::{ self, Alias, Arg, Arguments, BoolOp, CmpOp, Comprehension, Constant, ExceptHandler, Expr, ExprContext, Keyword, MatchCase, Operator, Pattern, Stmt, UnaryOp, WithItem, @@ -61,9 +61,6 @@ pub trait Visitor<'a> { fn visit_arg(&mut self, arg: &'a Arg) { walk_arg(self, arg); } - fn visit_arg_with_default(&mut self, arg_with_default: &'a ArgWithDefault) { - walk_arg_with_default(self, arg_with_default); - } fn visit_keyword(&mut self, keyword: &'a Keyword) { walk_keyword(self, keyword); } @@ -606,17 +603,34 @@ pub fn walk_except_handler<'a, V: Visitor<'a> + ?Sized>( } pub fn walk_arguments<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arguments: &'a Arguments) { + // Defaults are evaluated before annotations. for arg in &arguments.posonlyargs { - visitor.visit_arg_with_default(arg); + if let Some(default) = &arg.default { + visitor.visit_expr(default); + } } for arg in &arguments.args { - visitor.visit_arg_with_default(arg); + if let Some(default) = &arg.default { + visitor.visit_expr(default); + } + } + for arg in &arguments.kwonlyargs { + if let Some(default) = &arg.default { + visitor.visit_expr(default); + } + } + + for arg in &arguments.posonlyargs { + visitor.visit_arg(&arg.def); + } + for arg in &arguments.args { + visitor.visit_arg(&arg.def); } if let Some(arg) = &arguments.vararg { visitor.visit_arg(arg); } for arg in &arguments.kwonlyargs { - visitor.visit_arg_with_default(arg); + visitor.visit_arg(&arg.def); } if let Some(arg) = &arguments.kwarg { visitor.visit_arg(arg); @@ -629,16 +643,6 @@ pub fn walk_arg<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arg: &'a Arg) { } } -pub fn walk_arg_with_default<'a, V: Visitor<'a> + ?Sized>( - visitor: &mut V, - arg_with_default: &'a ArgWithDefault, -) { - if let Some(expr) = &arg_with_default.default { - visitor.visit_expr(expr); - } - visitor.visit_arg(&arg_with_default.def); -} - pub fn walk_keyword<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, keyword: &'a Keyword) { visitor.visit_expr(&keyword.value); } From e71f044f0d6b6869365530e45ddd86c3c9097f7d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 21 Jun 2023 16:11:40 -0400 Subject: [PATCH 172/447] Avoid including nursery rules in linter-level selectors (#5268) ## Summary Ensures that `--select PL` and `--select PLC` don't include `PLC1901`. Previously, `--select PL` _did_, because it's a "linter-level selector" (`--select PLC` is viewed as selecting the `C` prefix from `PL`), and we were missing this filtering path. --- crates/ruff_macros/src/map_codes.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/ruff_macros/src/map_codes.rs b/crates/ruff_macros/src/map_codes.rs index 950c66f7c8..94fbcbdac5 100644 --- a/crates/ruff_macros/src/map_codes.rs +++ b/crates/ruff_macros/src/map_codes.rs @@ -192,7 +192,8 @@ fn rules_by_prefix( for (code, rule) in rules { // Nursery rules have to be explicitly selected, so we ignore them when looking at - // prefixes. + // prefix-level selectors (e.g., `--select SIM10`), but add the rule itself under + // its fully-qualified code (e.g., `--select SIM101`). if is_nursery(&rule.group) { rules_by_prefix.insert(code.clone(), vec![(rule.path.clone(), rule.attrs.clone())]); continue; @@ -329,10 +330,17 @@ fn generate_iter_impl( ) -> TokenStream { let mut linter_into_iter_match_arms = quote!(); for (linter, map) in linter_to_rules { - let rule_paths = map.values().map(|Rule { attrs, path, .. }| { - let rule_name = path.segments.last().unwrap(); - quote!(#(#attrs)* Rule::#rule_name) - }); + 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_name = path.segments.last().unwrap(); + quote!(#(#attrs)* Rule::#rule_name) + }); linter_into_iter_match_arms.extend(quote! { Linter::#linter => vec![#(#rule_paths,)*].into_iter(), }); From 1eccbbb60e7f5a38238c280940b161d03079b163 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Wed, 21 Jun 2023 22:00:31 +0100 Subject: [PATCH 173/447] Format StmtFor (#5163) ## Summary format StmtFor still trying to learn how to help out with the formatter. trying something slightly more advanced than [break](#5158) mostly copied form StmtWhile ## Test Plan snapshots --- .../test/fixtures/ruff/statement/for.py | 34 ++++++ ...r__tests__black_test__bracketmatch_py.snap | 13 ++- ...er__tests__black_test__collections_py.snap | 18 ++-- ...tter__tests__black_test__comments3_py.snap | 10 +- ...tter__tests__black_test__comments5_py.snap | 45 ++++---- ...ter__tests__black_test__expression_py.snap | 101 +++++++++--------- ...tter__tests__black_test__fmtonoff5_py.snap | 10 +- ...atter__tests__black_test__fmtonoff_py.snap | 28 ++--- ...atter__tests__black_test__fmtskip8_py.snap | 14 +-- ...atter__tests__black_test__function_py.snap | 15 +-- ...s__black_test__remove_for_brackets_py.snap | 44 +++++--- ...move_newline_after_code_block_open_py.snap | 22 ++-- ...__tests__ruff_test__statement__for_py.snap | 87 +++++++++++++++ .../src/statement/stmt_for.rs | 85 ++++++++++++++- 14 files changed, 392 insertions(+), 134 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/for.py create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__for_py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/for.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/for.py new file mode 100644 index 0000000000..3a96b5390f --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/for.py @@ -0,0 +1,34 @@ +for x in y: # trailing test comment + pass # trailing last statement comment + + # trailing for body comment + +# leading else comment + +else: # trailing else comment + pass + + # trailing else body comment + + +for aVeryLongNameThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn in anotherVeryLongNameThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn: # trailing comment + pass + +else: + ... + +for ( + x, + y, + ) in z: # comment + ... + + +# remove brackets around x,y but keep them around z,w +for (x, y) in (z, w): + ... + + +# type comment +for x in (): # type: int + ... diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__bracketmatch_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__bracketmatch_py.snap index fd018bb9fa..2a11007f5d 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__bracketmatch_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__bracketmatch_py.snap @@ -19,12 +19,14 @@ lambda x=lambda y={1: 3}: y['x':lambda y: {1: 2}]: x ```diff --- Black +++ Ruff -@@ -1,4 +1,3 @@ +@@ -1,4 +1,6 @@ -for ((x in {}) or {})["a"] in x: -- pass ++for ((NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right) or {})[ ++ "NOT_YET_IMPLEMENTED_STRING" ++] in x: + pass -pem_spam = lambda l, spam={"x": 3}: not spam.get(l.strip()) -lambda x=lambda y={1: 3}: y["x" : lambda y: {1: 2}]: x -+NOT_YET_IMPLEMENTED_StmtFor +pem_spam = lambda x: True +lambda x: True ``` @@ -32,7 +34,10 @@ lambda x=lambda y={1: 3}: y['x':lambda y: {1: 2}]: x ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtFor +for ((NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right) or {})[ + "NOT_YET_IMPLEMENTED_STRING" +] in x: + pass pem_spam = lambda x: True lambda x: True ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap index 0b9b054e8a..0c369db6d6 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap @@ -84,7 +84,7 @@ if True: ```diff --- Black +++ Ruff -@@ -1,99 +1,56 @@ +@@ -1,61 +1,40 @@ -import core, time, a +NOT_YET_IMPLEMENTED_StmtImport @@ -164,12 +164,10 @@ if True: +NOT_YET_IMPLEMENTED_StmtAssert # looping over a 1-tuple should also not get wrapped --for x in (1,): -- pass --for (x,) in (1,), (2,), (3,): -- pass -+NOT_YET_IMPLEMENTED_StmtFor -+NOT_YET_IMPLEMENTED_StmtFor + for x in (1,): +@@ -63,37 +42,17 @@ + for (x,) in (1,), (2,), (3,): + pass -[ - 1, @@ -257,8 +255,10 @@ y = { NOT_YET_IMPLEMENTED_StmtAssert # looping over a 1-tuple should also not get wrapped -NOT_YET_IMPLEMENTED_StmtFor -NOT_YET_IMPLEMENTED_StmtFor +for x in (1,): + pass +for (x,) in (1,), (2,), (3,): + pass [1, 2, 3] diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap index 1d64b8c286..0c05857e26 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap @@ -61,7 +61,7 @@ def func(): ```diff --- Black +++ Ruff -@@ -3,46 +3,15 @@ +@@ -3,46 +3,17 @@ # %% def func(): @@ -97,7 +97,9 @@ def func(): - ) - # This should be left alone (after) - ) -+ NOT_YET_IMPLEMENTED_StmtFor ++ for exc in exc_value.NOT_IMPLEMENTED_attr: ++ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: ++ NOT_IMPLEMENTED_call() # everything is fine if the expression isn't nested - traceback.TracebackException.from_exception( @@ -128,7 +130,9 @@ def func(): # Capture each of the exceptions in the MultiError along with each of their causes and contexts if NOT_IMPLEMENTED_call(): embedded = [] - NOT_YET_IMPLEMENTED_StmtFor + for exc in exc_value.NOT_IMPLEMENTED_attr: + if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + NOT_IMPLEMENTED_call() # everything is fine if the expression isn't nested NOT_IMPLEMENTED_call() diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap index 4cdfd0bf99..debbe95378 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap @@ -86,7 +86,7 @@ if __name__ == "__main__": ```diff --- Black +++ Ruff -@@ -1,33 +1,20 @@ +@@ -1,6 +1,6 @@ while True: - if something.changed: - do.stuff() # trailing comment @@ -95,38 +95,40 @@ if __name__ == "__main__": # Comment belongs to the `if` block. # This one belongs to the `while` block. - # Should this one, too? I guess so. +@@ -8,26 +8,20 @@ # This one is properly standalone now. -- --for i in range(100): -- # first we do this -- if i % 33 == 0: -- break -- # then we do this +-for i in range(100): ++for i in NOT_IMPLEMENTED_call(): + # first we do this +- if i % 33 == 0: ++ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + break + + # then we do this - print(i) -- # and finally we loop around -+NOT_YET_IMPLEMENTED_StmtFor ++ NOT_IMPLEMENTED_call() + # and finally we loop around -with open(some_temp_file) as f: - data = f.read() -- ++NOT_YET_IMPLEMENTED_StmtWith + -try: - with open(some_other_file) as w: - w.write(data) -+NOT_YET_IMPLEMENTED_StmtWith ++NOT_YET_IMPLEMENTED_StmtTry -except OSError: - print("problems") -+NOT_YET_IMPLEMENTED_StmtTry - +- -import sys +NOT_YET_IMPLEMENTED_StmtImport # leading function comment -@@ -42,7 +29,7 @@ +@@ -42,7 +36,7 @@ # leading 1 @deco1 # leading 2 @@ -135,7 +137,7 @@ if __name__ == "__main__": # leading 3 @deco3 def decorated1(): -@@ -52,7 +39,7 @@ +@@ -52,7 +46,7 @@ # leading 1 @deco1 # leading 2 @@ -144,7 +146,7 @@ if __name__ == "__main__": # leading function comment def decorated1(): ... -@@ -69,5 +56,5 @@ +@@ -69,5 +63,5 @@ ... @@ -167,7 +169,14 @@ while True: # This one is properly standalone now. -NOT_YET_IMPLEMENTED_StmtFor +for i in NOT_IMPLEMENTED_call(): + # first we do this + if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + break + + # then we do this + NOT_IMPLEMENTED_call() + # and finally we loop around NOT_YET_IMPLEMENTED_StmtWith diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap index 772e6104cb..78ff32e2eb 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap @@ -352,12 +352,7 @@ last_call() (1, 2, 3) [] [1, 2, 3, 4, 5, 6, 7, 8, 9, (10 or A), (11 or B), (12 or C)] -+[1, 2, 3] -+[NOT_YET_IMPLEMENTED_ExprStarred] -+[NOT_YET_IMPLEMENTED_ExprStarred] -+[NOT_YET_IMPLEMENTED_ExprStarred, 4, 5] -+[4, NOT_YET_IMPLEMENTED_ExprStarred, 5] - [ +-[ - 1, - 2, - 3, @@ -374,7 +369,12 @@ last_call() - *a, - 5, -] --[ ++[1, 2, 3] ++[NOT_YET_IMPLEMENTED_ExprStarred] ++[NOT_YET_IMPLEMENTED_ExprStarred] ++[NOT_YET_IMPLEMENTED_ExprStarred, 4, 5] ++[4, NOT_YET_IMPLEMENTED_ExprStarred, 5] + [ this_is_a_very_long_variable_which_will_force_a_delimiter_split, element, another, @@ -459,6 +459,9 @@ last_call() - int, - float, - dict[str, int], +-] +-very_long_variable_name_filters: t.List[ +- t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], + ( + str, + int, @@ -466,9 +469,6 @@ last_call() + dict[str, int], + ) ] --very_long_variable_name_filters: t.List[ -- t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], --] -xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore - sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) -) @@ -570,7 +570,10 @@ last_call() ) -what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( - vars_to_remove --) ++what_is_up_with_those_new_coord_names = ( ++ (coord_names | NOT_IMPLEMENTED_call()) ++ - NOT_IMPLEMENTED_call() + ) -result = ( - session.query(models.Customer.id) - .filter( @@ -588,10 +591,7 @@ last_call() - models.Customer.id.asc(), - ) - .all() -+what_is_up_with_those_new_coord_names = ( -+ (coord_names | NOT_IMPLEMENTED_call()) -+ - NOT_IMPLEMENTED_call() - ) +-) -Ø = set() -authors.łukasz.say_thanks() +result = NOT_IMPLEMENTED_call() @@ -601,7 +601,7 @@ last_call() mapping = { A: 0.25 * (10.0 / 12), B: 0.1 * (10.0 / 12), -@@ -236,64 +219,38 @@ +@@ -236,31 +219,29 @@ def gen(): @@ -628,37 +628,31 @@ last_call() - force=False -), "Short message" -assert parens is TooMany --for (x,) in (1,), (2,), (3,): -- ... --for y in (): -- ... --for z in (i for i in (1, 2, 3)): -- ... --for i in call(): -- ... --for j in 1 + (2 + 3): -- ... +NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call() +NOT_YET_IMPLEMENTED_StmtAssert +NOT_YET_IMPLEMENTED_StmtAssert +NOT_YET_IMPLEMENTED_StmtAssert -+NOT_YET_IMPLEMENTED_StmtFor -+NOT_YET_IMPLEMENTED_StmtFor -+NOT_YET_IMPLEMENTED_StmtFor -+NOT_YET_IMPLEMENTED_StmtFor -+NOT_YET_IMPLEMENTED_StmtFor - while this and that: + for (x,) in (1,), (2,), (3,): ... --for ( -- addr_family, -- addr_type, -- addr_proto, -- addr_canonname, -- addr_sockaddr, + for y in (): + ... +-for z in (i for i in (1, 2, 3)): ++for z in (i for i in []): + ... +-for i in call(): ++for i in NOT_IMPLEMENTED_call(): + ... + for j in 1 + (2 + 3): + ... +@@ -272,28 +253,16 @@ + addr_proto, + addr_canonname, + addr_sockaddr, -) in socket.getaddrinfo("google.com", "http"): -- pass ++) in NOT_IMPLEMENTED_call(): + pass -a = ( - aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp - in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz @@ -675,7 +669,6 @@ last_call() - aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp - is not qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz -) -+NOT_YET_IMPLEMENTED_StmtFor +a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right @@ -690,7 +683,7 @@ last_call() ): return True if ( -@@ -327,24 +284,44 @@ +@@ -327,24 +296,44 @@ ): return True if ( @@ -747,7 +740,7 @@ last_call() ): return True ( -@@ -363,8 +340,9 @@ +@@ -363,8 +352,9 @@ bbbb >> bbbb * bbbb ( aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa @@ -1001,14 +994,26 @@ NOT_IMPLEMENTED_call() NOT_YET_IMPLEMENTED_StmtAssert NOT_YET_IMPLEMENTED_StmtAssert NOT_YET_IMPLEMENTED_StmtAssert -NOT_YET_IMPLEMENTED_StmtFor -NOT_YET_IMPLEMENTED_StmtFor -NOT_YET_IMPLEMENTED_StmtFor -NOT_YET_IMPLEMENTED_StmtFor -NOT_YET_IMPLEMENTED_StmtFor +for (x,) in (1,), (2,), (3,): + ... +for y in (): + ... +for z in (i for i in []): + ... +for i in NOT_IMPLEMENTED_call(): + ... +for j in 1 + (2 + 3): + ... while this and that: ... -NOT_YET_IMPLEMENTED_StmtFor +for ( + addr_family, + addr_type, + addr_proto, + addr_canonname, + addr_sockaddr, +) in NOT_IMPLEMENTED_call(): + pass a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap index 7582c7cdee..aada9db6c5 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap @@ -134,7 +134,7 @@ elif unformatted: return True # yapf: enable elif b: -@@ -39,49 +21,27 @@ +@@ -39,49 +21,29 @@ # Regression test for https://github.com/psf/black/issues/2567. if True: # fmt: off @@ -142,7 +142,9 @@ elif unformatted: - # fmt: on - print ( "This won't be formatted" ) - print ( "This won't be formatted either" ) -+ NOT_YET_IMPLEMENTED_StmtFor ++ for _ in NOT_IMPLEMENTED_call(): ++ # fmt: on ++ NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call() else: - print("This will be formatted") @@ -220,7 +222,9 @@ def test_func(): # Regression test for https://github.com/psf/black/issues/2567. if True: # fmt: off - NOT_YET_IMPLEMENTED_StmtFor + for _ in NOT_IMPLEMENTED_call(): + # fmt: on + NOT_IMPLEMENTED_call() NOT_IMPLEMENTED_call() else: NOT_IMPLEMENTED_call() diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap index b1786e90a8..b99773a340 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap @@ -222,7 +222,7 @@ d={'a':1, # Comment 1 # Comment 2 -@@ -18,30 +16,51 @@ +@@ -18,30 +16,53 @@ # fmt: off def func_no_args(): @@ -241,7 +241,9 @@ d={'a':1, + NOT_YET_IMPLEMENTED_StmtRaise + if False: + ... -+ NOT_YET_IMPLEMENTED_StmtFor ++ for i in NOT_IMPLEMENTED_call(): ++ NOT_IMPLEMENTED_call() ++ continue + NOT_IMPLEMENTED_call() + return None + @@ -296,7 +298,7 @@ d={'a':1, def spaces_types( -@@ -51,76 +70,71 @@ +@@ -51,76 +72,71 @@ d: dict = {}, e: bool = True, f: int = -1, @@ -397,7 +399,7 @@ d={'a':1, # fmt: off # hey, that won't work -@@ -130,13 +144,15 @@ +@@ -130,13 +146,15 @@ def on_and_off_broken(): @@ -418,7 +420,7 @@ d={'a':1, # fmt: on # fmt: off # ...but comments still get reformatted even though they should not be -@@ -145,80 +161,21 @@ +@@ -145,80 +163,21 @@ def long_lines(): if True: @@ -430,11 +432,13 @@ d={'a':1, - implicit_default=True, - ) - ) -- # fmt: off ++ NOT_IMPLEMENTED_call() + # fmt: off - a = ( - unnecessary_bracket() - ) -- # fmt: on ++ a = NOT_IMPLEMENTED_call() + # fmt: on - _type_comment_re = re.compile( - r""" - ^ @@ -455,11 +459,9 @@ d={'a':1, - ) - $ - """, -+ NOT_IMPLEMENTED_call() - # fmt: off +- # fmt: off - re.MULTILINE|re.VERBOSE -+ a = NOT_IMPLEMENTED_call() - # fmt: on +- # fmt: on - ) + _type_comment_re = NOT_IMPLEMENTED_call() @@ -537,7 +539,9 @@ def func_no_args(): NOT_YET_IMPLEMENTED_StmtRaise if False: ... - NOT_YET_IMPLEMENTED_StmtFor + for i in NOT_IMPLEMENTED_call(): + NOT_IMPLEMENTED_call() + continue NOT_IMPLEMENTED_call() return None diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap index f8d1d415c3..eb9f51311f 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap @@ -75,7 +75,7 @@ async def test_async_with(): ```diff --- Black +++ Ruff -@@ -1,62 +1,46 @@ +@@ -1,62 +1,47 @@ # Make sure a leading comment is not removed. -def some_func( unformatted, args ): # fmt: skip - print("I am some_func") @@ -99,13 +99,13 @@ async def test_async_with(): - def some_method( self, unformatted, args ): # fmt: skip - print("I am some_method") - return 0 -- ++NOT_YET_IMPLEMENTED_StmtClassDef + - async def some_async_method( self, unformatted, args ): # fmt: skip - print("I am some_async_method") - await asyncio.sleep(1) -+NOT_YET_IMPLEMENTED_StmtClassDef - +- # Make sure a leading comment is not removed. -if unformatted_call( args ): # fmt: skip - print("First branch") @@ -130,7 +130,8 @@ async def test_async_with(): -for i in some_iter( unformatted, args ): # fmt: skip - print("Do something") -+NOT_YET_IMPLEMENTED_StmtFor # fmt: skip ++for i in NOT_IMPLEMENTED_call(): # fmt: skip ++ NOT_IMPLEMENTED_call() async def test_async_for(): @@ -193,7 +194,8 @@ while NOT_IMPLEMENTED_call(): # fmt: skip NOT_IMPLEMENTED_call() -NOT_YET_IMPLEMENTED_StmtFor # fmt: skip +for i in NOT_IMPLEMENTED_call(): # fmt: skip + NOT_IMPLEMENTED_call() async def test_async_for(): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap index 25404fa93b..f983c55b44 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap @@ -126,7 +126,7 @@ def __await__(): return (yield) def func_no_args(): -@@ -14,39 +13,46 @@ +@@ -14,39 +13,48 @@ b c if True: @@ -136,9 +136,10 @@ def __await__(): return (yield) ... - for i in range(10): - print(i) -- continue ++ for i in NOT_IMPLEMENTED_call(): ++ NOT_IMPLEMENTED_call() + continue - exec("new-style exec", {}, {}) -+ NOT_YET_IMPLEMENTED_StmtFor + NOT_IMPLEMENTED_call() return None @@ -189,7 +190,7 @@ def __await__(): return (yield) def spaces_types( -@@ -56,70 +62,26 @@ +@@ -56,70 +64,26 @@ d: dict = {}, e: bool = True, f: int = -1, @@ -269,7 +270,7 @@ def __await__(): return (yield) def trailing_comma(): -@@ -135,14 +97,8 @@ +@@ -135,14 +99,8 @@ a, **kwargs, ) -> A: @@ -309,7 +310,9 @@ def func_no_args(): NOT_YET_IMPLEMENTED_StmtRaise if False: ... - NOT_YET_IMPLEMENTED_StmtFor + for i in NOT_IMPLEMENTED_call(): + NOT_IMPLEMENTED_call() + continue NOT_IMPLEMENTED_call() return None diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap index dbdfdc6508..d43018c8dc 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap @@ -32,25 +32,28 @@ for (((((k, v))))) in d.items(): ```diff --- Black +++ Ruff -@@ -1,27 +1,13 @@ +@@ -1,27 +1,22 @@ # Only remove tuple brackets after `for` -for k, v in d.items(): - print(k, v) -+NOT_YET_IMPLEMENTED_StmtFor ++for k, v in NOT_IMPLEMENTED_call(): ++ NOT_IMPLEMENTED_call() # Don't touch tuple brackets after `in` --for module in (core, _unicodefun): + for module in (core, _unicodefun): - if hasattr(module, "_verify_python3_env"): - module._verify_python3_env = lambda: None -+NOT_YET_IMPLEMENTED_StmtFor ++ if NOT_IMPLEMENTED_call(): ++ module.NOT_IMPLEMENTED_attr = lambda x: True # Brackets remain for long for loop lines --for ( -- why_would_anyone_choose_to_name_a_loop_variable_with_a_name_this_long, -- i_dont_know_but_we_should_still_check_the_behaviour_if_they_do, + for ( + why_would_anyone_choose_to_name_a_loop_variable_with_a_name_this_long, + i_dont_know_but_we_should_still_check_the_behaviour_if_they_do, -) in d.items(): - print(k, v) -+NOT_YET_IMPLEMENTED_StmtFor ++) in NOT_IMPLEMENTED_call(): ++ NOT_IMPLEMENTED_call() -for ( - k, @@ -59,30 +62,41 @@ for (((((k, v))))) in d.items(): - dfkasdjfldsjflkdsjflkdsjfdslkfjldsjfgkjdshgkljjdsfldgkhsdofudsfudsofajdslkfjdslkfjldisfjdffjsdlkfjdlkjjkdflskadjldkfjsalkfjdasj.items() -): - print(k, v) -+NOT_YET_IMPLEMENTED_StmtFor ++for k, v in NOT_IMPLEMENTED_call(): ++ NOT_IMPLEMENTED_call() # Test deeply nested brackets -for k, v in d.items(): - print(k, v) -+NOT_YET_IMPLEMENTED_StmtFor ++for k, v in NOT_IMPLEMENTED_call(): ++ NOT_IMPLEMENTED_call() ``` ## Ruff Output ```py # Only remove tuple brackets after `for` -NOT_YET_IMPLEMENTED_StmtFor +for k, v in NOT_IMPLEMENTED_call(): + NOT_IMPLEMENTED_call() # Don't touch tuple brackets after `in` -NOT_YET_IMPLEMENTED_StmtFor +for module in (core, _unicodefun): + if NOT_IMPLEMENTED_call(): + module.NOT_IMPLEMENTED_attr = lambda x: True # Brackets remain for long for loop lines -NOT_YET_IMPLEMENTED_StmtFor +for ( + why_would_anyone_choose_to_name_a_loop_variable_with_a_name_this_long, + i_dont_know_but_we_should_still_check_the_behaviour_if_they_do, +) in NOT_IMPLEMENTED_call(): + NOT_IMPLEMENTED_call() -NOT_YET_IMPLEMENTED_StmtFor +for k, v in NOT_IMPLEMENTED_call(): + NOT_IMPLEMENTED_call() # Test deeply nested brackets -NOT_YET_IMPLEMENTED_StmtFor +for k, v in NOT_IMPLEMENTED_call(): + NOT_IMPLEMENTED_call() ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap index 79f86d3478..642e62ce92 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap @@ -121,7 +121,7 @@ with open("/path/to/file.txt", mode="r") as read_file: ```diff --- Black +++ Ruff -@@ -1,78 +1,68 @@ +@@ -1,78 +1,72 @@ -import random +NOT_YET_IMPLEMENTED_StmtImport @@ -159,18 +159,22 @@ with open("/path/to/file.txt", mode="r") as read_file: -for i in range(5): - print(f"{i}) The line above me should be removed!") -+NOT_YET_IMPLEMENTED_StmtFor ++for i in NOT_IMPLEMENTED_call(): ++ NOT_IMPLEMENTED_call() -for i in range(5): - print(f"{i}) The lines above me should be removed!") -+NOT_YET_IMPLEMENTED_StmtFor ++for i in NOT_IMPLEMENTED_call(): ++ NOT_IMPLEMENTED_call() -for i in range(5): - for j in range(7): - print(f"{i}) The lines above me should be removed!") -+NOT_YET_IMPLEMENTED_StmtFor ++for i in NOT_IMPLEMENTED_call(): ++ for j in NOT_IMPLEMENTED_call(): ++ NOT_IMPLEMENTED_call() -if random.randint(0, 3) == 0: @@ -254,13 +258,17 @@ def foo4(): NOT_YET_IMPLEMENTED_StmtClassDef -NOT_YET_IMPLEMENTED_StmtFor +for i in NOT_IMPLEMENTED_call(): + NOT_IMPLEMENTED_call() -NOT_YET_IMPLEMENTED_StmtFor +for i in NOT_IMPLEMENTED_call(): + NOT_IMPLEMENTED_call() -NOT_YET_IMPLEMENTED_StmtFor +for i in NOT_IMPLEMENTED_call(): + for j in NOT_IMPLEMENTED_call(): + NOT_IMPLEMENTED_call() if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__for_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__for_py.snap new file mode 100644 index 0000000000..b176abbfb0 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__for_py.snap @@ -0,0 +1,87 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +--- +## Input +```py +for x in y: # trailing test comment + pass # trailing last statement comment + + # trailing for body comment + +# leading else comment + +else: # trailing else comment + pass + + # trailing else body comment + + +for aVeryLongNameThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn in anotherVeryLongNameThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn: # trailing comment + pass + +else: + ... + +for ( + x, + y, + ) in z: # comment + ... + + +# remove brackets around x,y but keep them around z,w +for (x, y) in (z, w): + ... + + +# type comment +for x in (): # type: int + ... +``` + + + +## Output +```py +for x in y: # trailing test comment + pass # trailing last statement comment + + # trailing for body comment + +# leading else comment + +else: # trailing else comment + pass + + # trailing else body comment + + +for ( + aVeryLongNameThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn +) in ( + anotherVeryLongNameThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn +): # trailing comment + pass + +else: + ... + +for ( + x, + y, +) in z: # comment + ... + + +# remove brackets around x,y but keep them around z,w +for x, y in (z, w): + ... + + +# type comment +for x in (): # type: int + ... +``` + + diff --git a/crates/ruff_python_formatter/src/statement/stmt_for.rs b/crates/ruff_python_formatter/src/statement/stmt_for.rs index b943de7cc1..528e06f152 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_for.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_for.rs @@ -1,12 +1,91 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; +use crate::comments::{leading_alternate_branch_comments, trailing_comments}; +use crate::expression::expr_tuple::TupleParentheses; +use crate::expression::parentheses::Parenthesize; +use crate::prelude::*; +use crate::{FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::StmtFor; +use ruff_python_ast::node::AstNode; +use rustpython_parser::ast::{Expr, Ranged, Stmt, StmtFor}; + +#[derive(Debug)] +struct ExprTupleWithoutParentheses<'a>(&'a Expr); + +impl Format> for ExprTupleWithoutParentheses<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + match self.0 { + Expr::Tuple(expr_tuple) => expr_tuple + .format() + .with_options(TupleParentheses::StripInsideForLoop) + .fmt(f), + other => other.format().with_options(Parenthesize::IfBreaks).fmt(f), + } + } +} #[derive(Default)] pub struct FormatStmtFor; impl FormatNodeRule for FormatStmtFor { fn fmt_fields(&self, item: &StmtFor, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let StmtFor { + range: _, + target, + iter, + body, + orelse, + type_comment: _, + } = item; + + let comments = f.context().comments().clone(); + let dangling_comments = comments.dangling_comments(item.as_any_node_ref()); + let body_start = body.first().map_or(iter.end(), Stmt::start); + let or_else_comments_start = + dangling_comments.partition_point(|comment| comment.slice().end() < body_start); + + let (trailing_condition_comments, or_else_comments) = + dangling_comments.split_at(or_else_comments_start); + + write!( + f, + [ + text("for"), + space(), + ExprTupleWithoutParentheses(target.as_ref()), + space(), + text("in"), + space(), + iter.format().with_options(Parenthesize::IfBreaks), + text(":"), + trailing_comments(trailing_condition_comments), + block_indent(&body.format()) + ] + )?; + + if orelse.is_empty() { + debug_assert!(or_else_comments.is_empty()); + } else { + // Split between leading comments before the `else` keyword and end of line comments at the end of + // the `else:` line. + let trailing_start = + or_else_comments.partition_point(|comment| comment.line_position().is_own_line()); + let (leading, trailing) = or_else_comments.split_at(trailing_start); + + write!( + f, + [ + leading_alternate_branch_comments(leading, body.last()), + text("else:"), + trailing_comments(trailing), + block_indent(&orelse.format()) + ] + )?; + } + + Ok(()) + } + + fn fmt_dangling_comments(&self, _node: &StmtFor, _f: &mut PyFormatter) -> FormatResult<()> { + // Handled in `fmt_fields` + Ok(()) } } From 3d7411bfaf2428de0975979bb94b13b400c70a6e Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 21 Jun 2023 23:26:09 +0200 Subject: [PATCH 174/447] Use trait for labels instead of `TypeId` (#5270) --- crates/ruff_formatter/src/builders.rs | 27 ++++++++--- .../ruff_formatter/src/format_element/tag.rs | 46 +++++++++++-------- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/crates/ruff_formatter/src/builders.rs b/crates/ruff_formatter/src/builders.rs index 1f3ef8fff1..441fb00a95 100644 --- a/crates/ruff_formatter/src/builders.rs +++ b/crates/ruff_formatter/src/builders.rs @@ -528,7 +528,22 @@ impl Format for LineSuffixBoundary { /// use ruff_formatter::prelude::*; /// use ruff_formatter::{format, write, LineWidth}; /// -/// enum SomeLabelId {} +/// #[derive(Debug, Copy, Clone)] +/// enum MyLabels { +/// Main +/// } +/// +/// impl tag::LabelDefinition for MyLabels { +/// fn value(&self) -> u64 { +/// *self as u64 +/// } +/// +/// fn name(&self) -> &'static str { +/// match self { +/// Self::Main => "Main" +/// } +/// } +/// } /// /// # fn main() -> FormatResult<()> { /// let formatted = format!( @@ -537,24 +552,24 @@ impl Format for LineSuffixBoundary { /// let mut recording = f.start_recording(); /// write!(recording, [ /// labelled( -/// LabelId::of::(), +/// LabelId::of(MyLabels::Main), /// &text("'I have a label'") /// ) /// ])?; /// /// let recorded = recording.stop(); /// -/// let is_labelled = recorded.first().map_or(false, |element| element.has_label(LabelId::of::())); +/// let is_labelled = recorded.first().map_or(false, |element| element.has_label(LabelId::of(MyLabels::Main))); /// /// if is_labelled { -/// write!(f, [text(" has label SomeLabelId")]) +/// write!(f, [text(" has label `Main`")]) /// } else { -/// write!(f, [text(" doesn't have label SomeLabelId")]) +/// write!(f, [text(" doesn't have label `Main`")]) /// } /// })] /// )?; /// -/// assert_eq!("'I have a label' has label SomeLabelId", formatted.print()?.as_code()); +/// assert_eq!("'I have a label' has label `Main`", formatted.print()?.as_code()); /// # Ok(()) /// # } /// ``` diff --git a/crates/ruff_formatter/src/format_element/tag.rs b/crates/ruff_formatter/src/format_element/tag.rs index 6c8d03c20b..a443f909f0 100644 --- a/crates/ruff_formatter/src/format_element/tag.rs +++ b/crates/ruff_formatter/src/format_element/tag.rs @@ -1,8 +1,5 @@ use crate::format_element::PrintMode; use crate::{GroupId, TextSize}; -#[cfg(debug_assertions)] -use std::any::type_name; -use std::any::TypeId; use std::cell::Cell; use std::num::NonZeroU8; @@ -235,37 +232,48 @@ impl Align { } } -#[derive(Eq, PartialEq, Copy, Clone)] +#[derive(Debug, Eq, Copy, Clone)] pub struct LabelId { - id: TypeId, + value: u64, #[cfg(debug_assertions)] - label: &'static str, + name: &'static str, } -#[cfg(debug_assertions)] -impl std::fmt::Debug for LabelId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.label) - } -} +impl PartialEq for LabelId { + fn eq(&self, other: &Self) -> bool { + let is_equal = self.value == other.value; -#[cfg(not(debug_assertions))] -impl std::fmt::Debug for LabelId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::write!(f, "#{:?}", self.id) + #[cfg(debug_assertions)] + { + if is_equal { + assert_eq!(self.name, other.name, "Two `LabelId`s with different names have the same `value`. Are you mixing labels of two different `LabelDefinition` or are the values returned by the `LabelDefinition` not unique?"); + } + } + + is_equal } } impl LabelId { - pub fn of() -> Self { + pub fn of(label: T) -> Self { Self { - id: TypeId::of::(), + value: label.value(), #[cfg(debug_assertions)] - label: type_name::(), + name: label.name(), } } } +/// Defines the valid labels of a language. You want to have at most one implementation per formatter +/// project. +pub trait LabelDefinition { + /// Returns the `u64` uniquely identifying this specific label. + fn value(&self) -> u64; + + /// Returns the name of the label that is shown in debug builds. + fn name(&self) -> &'static str; +} + #[derive(Clone, Copy, Eq, PartialEq, Debug)] pub enum VerbatimKind { Bogus, From 341b12d918e58b15d328dbf27e188d2cb7a3680d Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Wed, 21 Jun 2023 22:30:44 +0100 Subject: [PATCH 175/447] Complete documentation for Ruff-specific rules (#5262) ## Summary Completes the documentation for the Ruff-specific ruleset. Related to #2646. ## Test Plan `python scripts/check_docs_formatted.py` --- .../ruff/rules/ambiguous_unicode_character.rs | 48 +++++++++++++++++++ .../rules/collection_literal_concatenation.rs | 30 ++++++++++++ .../rules/ruff/rules/pairwise_over_zipped.rs | 26 ++++++++++ 3 files changed, 104 insertions(+) diff --git a/crates/ruff/src/rules/ruff/rules/ambiguous_unicode_character.rs b/crates/ruff/src/rules/ruff/rules/ambiguous_unicode_character.rs index 3963e44aae..c89f132dd4 100644 --- a/crates/ruff/src/rules/ruff/rules/ambiguous_unicode_character.rs +++ b/crates/ruff/src/rules/ruff/rules/ambiguous_unicode_character.rs @@ -11,6 +11,22 @@ use crate::rules::ruff::rules::confusables::CONFUSABLES; use crate::rules::ruff::rules::Context; use crate::settings::Settings; +/// ## What it does +/// Checks for ambiguous unicode characters in strings. +/// +/// ## Why is this bad? +/// The use of ambiguous unicode characters can confuse readers and cause +/// subtle bugs. +/// +/// ## Example +/// ```python +/// print("Ηello, world!") # "Η" is the Greek eta (`U+0397`). +/// ``` +/// +/// Use instead: +/// ```python +/// print("Hello, world!") # "H" is the Latin capital H (`U+0048`). +/// ``` #[violation] pub struct AmbiguousUnicodeCharacterString { confusable: char, @@ -44,6 +60,22 @@ impl AlwaysAutofixableViolation for AmbiguousUnicodeCharacterString { } } +/// ## What it does +/// Checks for ambiguous unicode characters in docstrings. +/// +/// ## Why is this bad? +/// The use of ambiguous unicode characters can confuse readers and cause +/// subtle bugs. +/// +/// ## Example +/// ```python +/// """A lovely docstring (with a `U+FF09` parenthesis).""" +/// ``` +/// +/// Use instead: +/// ```python +/// """A lovely docstring (with no strange parentheses).""" +/// ``` #[violation] pub struct AmbiguousUnicodeCharacterDocstring { confusable: char, @@ -77,6 +109,22 @@ impl AlwaysAutofixableViolation for AmbiguousUnicodeCharacterDocstring { } } +/// ## What it does +/// Checks for ambiguous unicode characters in comments. +/// +/// ## Why is this bad? +/// The use of ambiguous unicode characters can confuse readers and cause +/// subtle bugs. +/// +/// ## Example +/// ```python +/// foo() # nоqa # "о" is Cyrillic (`U+043E`) +/// ``` +/// +/// Use instead: +/// ```python +/// foo() # noqa # "o" is Latin (`U+006F`) +/// ``` #[violation] pub struct AmbiguousUnicodeCharacterComment { confusable: char, 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 ef2da7967b..e544ab0750 100644 --- a/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs +++ b/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs @@ -13,6 +13,36 @@ pub struct CollectionLiteralConcatenation { expr: String, } +/// ## What it does +/// Checks for uses of the `+` operator to concatenate collections. +/// +/// ## Why is this bad? +/// In Python, the `+` operator can be used to concatenate collections (e.g., +/// `x + y` to concatenate the lists `x` and `y`). +/// +/// However, collections can be concatenated more efficiently using the +/// unpacking operator (e.g., `[*x, *y]` to concatenate `x` and `y`). +/// +/// Prefer the unpacking operator to concatenate collections, as it is more +/// readable and flexible. The `*` operator can unpack any iterable, whereas +/// `+` operates only on particular sequences which, in many cases, must be of +/// the same type. +/// +/// ## Example +/// ```python +/// foo = [2, 3, 4] +/// bar = [1] + foo + [5, 6] +/// ``` +/// +/// Use instead: +/// ```python +/// foo = [2, 3, 4] +/// bar = [1, *foo, 5, 6] +/// ``` +/// +/// ## References +/// - [PEP 448 – Additional Unpacking Generalizations](https://peps.python.org/pep-0448/) +/// - [Python docs: Sequence Types — `list`, `tuple`, `range`](https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range) impl Violation for CollectionLiteralConcatenation { const AUTOFIX: AutofixKind = AutofixKind::Sometimes; 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 5aec8dc085..657a1638aa 100644 --- a/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs +++ b/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs @@ -6,6 +6,32 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for use of `zip()` to iterate over successive pairs of elements. +/// +/// ## Why is this bad? +/// When iterating over successive pairs of elements, prefer +/// `itertools.pairwise()` over `zip()`. +/// +/// `itertools.pairwise()` is more readable and conveys the intent of the code +/// more clearly. +/// +/// ## Example +/// ```python +/// letters = "ABCD" +/// zip(letters, letters[1:]) # ("A", "B"), ("B", "C"), ("C", "D") +/// ``` +/// +/// Use instead: +/// ```python +/// from itertools import pairwise +/// +/// letters = "ABCD" +/// pairwise(letters) # ("A", "B"), ("B", "C"), ("C", "D") +/// ``` +/// +/// ## References +/// - [Python documentation: `itertools.pairwise`](https://docs.python.org/3/library/itertools.html#itertools.pairwise) #[violation] pub struct PairwiseOverZipped; From ccf34aae8c4d086d1f0c0f9e486aca847c3023c2 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 21 Jun 2023 23:33:53 +0200 Subject: [PATCH 176/447] Format Attribute Expression (#5259) --- .../fixtures/ruff/expression/attribute.py | 29 +++ .../src/comments/placement.rs | 38 ++++ .../src/expression/expr_attribute.rs | 48 +++- .../src/expression/parentheses.rs | 15 +- ...ttribute_access_on_number_literals_py.snap | 29 ++- ...er__tests__black_test__collections_py.snap | 19 +- ...tter__tests__black_test__comments2_py.snap | 39 ++-- ...tter__tests__black_test__comments3_py.snap | 5 +- ...tter__tests__black_test__comments5_py.snap | 11 +- ...er__tests__black_test__empty_lines_py.snap | 72 ++---- ...ter__tests__black_test__expression_py.snap | 207 +++++++----------- ...atter__tests__black_test__fmtonoff_py.snap | 27 +-- ...atter__tests__black_test__function_py.snap | 9 +- ..._tests__black_test__import_spacing_py.snap | 44 +--- ...ests__black_test__power_op_spacing_py.snap | 30 ++- ...s__black_test__remove_for_brackets_py.snap | 4 +- ...rmatter__tests__black_test__slices_py.snap | 8 +- ...__ruff_test__expression__attribute_py.snap | 73 ++++++ ...est__expression__boolean_operation_py.snap | 12 +- crates/ruff_python_formatter/src/trivia.rs | 4 + 20 files changed, 394 insertions(+), 329 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/attribute.py create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__attribute_py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/attribute.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/attribute.py new file mode 100644 index 0000000000..bdc18f4696 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/attribute.py @@ -0,0 +1,29 @@ +( + a + # comment + .b # trailing comment +) + +( + a + # comment + .b # trailing dot comment # trailing identifier comment +) + +( + a + # comment + .b # trailing identifier comment +) + + +( + a + # comment + . # trailing dot comment + # in between + b # trailing identifier comment +) + + +aaaaaaaaaaaaaaaaaaaaa.lllllllllllllllllllllllllllloooooooooong.chaaaaaaaaaaaaaaaaaaaaaaiiiiiiiiiiiiiiiiiiiiiiinnnnnnnn.ooooooooooooooooooooooooofffffffff.aaaaaaaaaattr diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 97502a7e67..5d09f24592 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -31,6 +31,7 @@ pub(super) fn place_comment<'a>( handle_leading_function_with_decorators_comment, handle_dict_unpacking_comment, handle_slice_comments, + handle_attribute_comment, ]; for handler in HANDLERS { comment = match handler(comment, locator) { @@ -1005,6 +1006,43 @@ fn handle_dict_unpacking_comment<'a>( CommentPlacement::Default(comment) } +// Own line comments coming after the node are always dangling comments +// ```python +// ( +// a +// # trailing a comment +// . # dangling comment +// # or this +// b +// ) +// ``` +fn handle_attribute_comment<'a>( + comment: DecoratedComment<'a>, + locator: &Locator, +) -> CommentPlacement<'a> { + let Some(attribute) = comment.enclosing_node().expr_attribute() else { + return CommentPlacement::Default(comment); + }; + + // It must be a comment AFTER the name + if comment.preceding_node().is_none() { + return CommentPlacement::Default(comment); + } + + let between_value_and_attr = TextRange::new(attribute.value.end(), attribute.attr.start()); + + let dot = SimpleTokenizer::new(locator.contents(), between_value_and_attr) + .skip_trivia() + .next() + .expect("Expected the `.` character after the value"); + + if TextRange::new(dot.end(), attribute.attr.start()).contains(comment.slice().start()) { + CommentPlacement::dangling(attribute.into(), comment) + } else { + CommentPlacement::Default(comment) + } +} + /// Returns `true` if `right` is `Some` and `left` and `right` are referentially equal. fn are_same_optional<'a, T>(left: AnyNodeRef, right: Option) -> bool where diff --git a/crates/ruff_python_formatter/src/expression/expr_attribute.rs b/crates/ruff_python_formatter/src/expression/expr_attribute.rs index 9732025e35..fd43e9f20a 100644 --- a/crates/ruff_python_formatter/src/expression/expr_attribute.rs +++ b/crates/ruff_python_formatter/src/expression/expr_attribute.rs @@ -1,9 +1,9 @@ -use crate::comments::Comments; +use crate::comments::{leading_comments, trailing_comments, Comments}; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; use crate::prelude::*; -use crate::{not_yet_implemented_custom_text, FormatNodeRule}; +use crate::FormatNodeRule; use ruff_formatter::write; use rustpython_parser::ast::{Constant, Expr, ExprAttribute, ExprConstant}; @@ -15,11 +15,11 @@ impl FormatNodeRule for FormatExprAttribute { let ExprAttribute { value, range: _, - attr: _, + attr, ctx: _, } = item; - let requires_space = matches!( + let needs_parentheses = matches!( value.as_ref(), Expr::Constant(ExprConstant { value: Constant::Int(_) | Constant::Float(_), @@ -27,16 +27,45 @@ impl FormatNodeRule for FormatExprAttribute { }) ); + if needs_parentheses { + value.format().with_options(Parenthesize::Always).fmt(f)?; + } else { + value.format().fmt(f)?; + } + + let comments = f.context().comments().clone(); + + if comments.has_trailing_own_line_comments(value.as_ref()) { + hard_line_break().fmt(f)?; + } + + let dangling_comments = comments.dangling_comments(item); + + let leading_attribute_comments_start = + dangling_comments.partition_point(|comment| comment.line_position().is_end_of_line()); + let (trailing_dot_comments, leading_attribute_comments) = + dangling_comments.split_at(leading_attribute_comments_start); + write!( f, [ - item.value.format(), - requires_space.then_some(space()), text("."), - not_yet_implemented_custom_text("NOT_IMPLEMENTED_attr") + trailing_comments(trailing_dot_comments), + (!leading_attribute_comments.is_empty()).then_some(hard_line_break()), + leading_comments(leading_attribute_comments), + attr.format() ] ) } + + fn fmt_dangling_comments( + &self, + _node: &ExprAttribute, + _f: &mut PyFormatter, + ) -> FormatResult<()> { + // handle in `fmt_fields` + Ok(()) + } } impl NeedsParentheses for ExprAttribute { @@ -46,6 +75,9 @@ impl NeedsParentheses for ExprAttribute { source: &str, comments: &Comments, ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + Parentheses::Optional => Parentheses::Never, + parentheses => parentheses, + } } } diff --git a/crates/ruff_python_formatter/src/expression/parentheses.rs b/crates/ruff_python_formatter/src/expression/parentheses.rs index 7d57fe9df3..d1506bd518 100644 --- a/crates/ruff_python_formatter/src/expression/parentheses.rs +++ b/crates/ruff_python_formatter/src/expression/parentheses.rs @@ -23,8 +23,12 @@ pub(super) fn default_expression_needs_parentheses( "Should only be called for expressions" ); + #[allow(clippy::if_same_then_else)] + if parenthesize.is_always() { + Parentheses::Always + } // `Optional` or `Preserve` and expression has parentheses in source code. - if !parenthesize.is_if_breaks() && is_expression_parenthesized(node, source) { + else if !parenthesize.is_if_breaks() && is_expression_parenthesized(node, source) { Parentheses::Always } // `Optional` or `IfBreaks`: Add parentheses if the expression doesn't fit on a line but enforce @@ -53,9 +57,15 @@ pub enum Parenthesize { /// Parenthesizes the expression only if it doesn't fit on a line. IfBreaks, + + Always, } impl Parenthesize { + pub(crate) const fn is_always(self) -> bool { + matches!(self, Parenthesize::Always) + } + pub(crate) const fn is_if_breaks(self) -> bool { matches!(self, Parenthesize::IfBreaks) } @@ -70,7 +80,8 @@ impl Parenthesize { /// whether there are parentheses in the source code or not. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum Parentheses { - /// Always create parentheses + /// Always set parentheses regardless if the expression breaks or if they were + /// present in the source. Always, /// Only add parentheses when necessary because the expression breaks over multiple lines. diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap index da19ad12f5..72a7fd2ecd 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap @@ -55,22 +55,21 @@ y = 100(no) +x = NOT_IMPLEMENTED_call() +x = NOT_IMPLEMENTED_call() +x = NOT_IMPLEMENTED_call() -+x = 1. .NOT_IMPLEMENTED_attr -+x = 1E+1 .NOT_IMPLEMENTED_attr -+x = 1E-1 .NOT_IMPLEMENTED_attr ++x = (1.).imag ++x = (1E+1).imag ++x = (1E-1).real +x = NOT_IMPLEMENTED_call() -+x = 123456789.123456789E123456789 .NOT_IMPLEMENTED_attr ++x = (123456789.123456789E123456789).real +x = NOT_IMPLEMENTED_call() -+x = 123456789J.NOT_IMPLEMENTED_attr ++x = 123456789J.real +x = NOT_IMPLEMENTED_call() +x = NOT_IMPLEMENTED_call() +x = NOT_IMPLEMENTED_call() -+x = 0O777 .NOT_IMPLEMENTED_attr ++x = (0O777).real +x = NOT_IMPLEMENTED_call() +x = -100.0000J --if (10).real: -+if 10 .NOT_IMPLEMENTED_attr: + if (10).real: ... y = 100[no] @@ -84,21 +83,21 @@ y = 100(no) x = NOT_IMPLEMENTED_call() x = NOT_IMPLEMENTED_call() x = NOT_IMPLEMENTED_call() -x = 1. .NOT_IMPLEMENTED_attr -x = 1E+1 .NOT_IMPLEMENTED_attr -x = 1E-1 .NOT_IMPLEMENTED_attr +x = (1.).imag +x = (1E+1).imag +x = (1E-1).real x = NOT_IMPLEMENTED_call() -x = 123456789.123456789E123456789 .NOT_IMPLEMENTED_attr +x = (123456789.123456789E123456789).real x = NOT_IMPLEMENTED_call() -x = 123456789J.NOT_IMPLEMENTED_attr +x = 123456789J.real x = NOT_IMPLEMENTED_call() x = NOT_IMPLEMENTED_call() x = NOT_IMPLEMENTED_call() -x = 0O777 .NOT_IMPLEMENTED_attr +x = (0O777).real x = NOT_IMPLEMENTED_call() x = -100.0000J -if 10 .NOT_IMPLEMENTED_attr: +if (10).real: ... y = 100[no] diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap index 0c369db6d6..b567509488 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap @@ -165,7 +165,7 @@ if True: # looping over a 1-tuple should also not get wrapped for x in (1,): -@@ -63,37 +42,17 @@ +@@ -63,14 +42,10 @@ for (x,) in (1,), (2,), (3,): pass @@ -181,13 +181,9 @@ if True: +NOT_IMPLEMENTED_call() if True: -- IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = ( -- Config.IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING -- | {pylons.controllers.WSGIController} -- ) -+ IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = Config.NOT_IMPLEMENTED_attr | { -+ pylons.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr, -+ } + IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = ( +@@ -79,21 +54,6 @@ + ) if True: - ec2client.get_waiter("instance_stopped").wait( @@ -266,9 +262,10 @@ division_result_tuple = (6 / 2,) NOT_IMPLEMENTED_call() if True: - IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = Config.NOT_IMPLEMENTED_attr | { - pylons.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr, - } + IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = ( + Config.IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING + | {pylons.controllers.WSGIController} + ) if True: NOT_IMPLEMENTED_call() diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap index db67f3dc39..42acde710a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap @@ -227,7 +227,7 @@ instruction()#comment with bad spacing ] not_shareables = [ -@@ -37,49 +33,57 @@ +@@ -37,31 +33,35 @@ # builtin types and objects type, object, @@ -263,31 +263,24 @@ instruction()#comment with bad spacing def inline_comments_in_brackets_ruin_everything(): if typedargslist: - parameters.children = [children[0], body, children[-1]] # (1 # )1 -- parameters.children = [ -+ parameters.NOT_IMPLEMENTED_attr = [ + parameters.children = [ + children[0], # (1 + body, + children[-1], # )1 + ] -+ parameters.NOT_IMPLEMENTED_attr = [ ++ parameters.children = [ children[0], body, children[-1], # type: ignore - ] - else: -- parameters.children = [ -- parameters.children[0], # (2 what if this was actually long -+ parameters.NOT_IMPLEMENTED_attr = [ -+ parameters.NOT_IMPLEMENTED_attr[0], # (2 what if this was actually long +@@ -72,14 +72,18 @@ body, -- parameters.children[-1], # )2 -+ parameters.NOT_IMPLEMENTED_attr[-1], # )2 + parameters.children[-1], # )2 ] - parameters.children = [parameters.what_if_this_was_actually_long.children[0], body, parameters.children[-1]] # type: ignore -+ parameters.NOT_IMPLEMENTED_attr = [ -+ parameters.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr[0], ++ parameters.children = [ ++ parameters.what_if_this_was_actually_long.children[0], + body, -+ parameters.NOT_IMPLEMENTED_attr[-1], ++ parameters.children[-1], + ] # type: ignore if ( - self._proc is not None @@ -457,26 +450,26 @@ else: # Comment before function. def inline_comments_in_brackets_ruin_everything(): if typedargslist: - parameters.NOT_IMPLEMENTED_attr = [ + parameters.children = [ children[0], # (1 body, children[-1], # )1 ] - parameters.NOT_IMPLEMENTED_attr = [ + parameters.children = [ children[0], body, children[-1], # type: ignore ] else: - parameters.NOT_IMPLEMENTED_attr = [ - parameters.NOT_IMPLEMENTED_attr[0], # (2 what if this was actually long + parameters.children = [ + parameters.children[0], # (2 what if this was actually long body, - parameters.NOT_IMPLEMENTED_attr[-1], # )2 + parameters.children[-1], # )2 ] - parameters.NOT_IMPLEMENTED_attr = [ - parameters.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr[0], + parameters.children = [ + parameters.what_if_this_was_actually_long.children[0], body, - parameters.NOT_IMPLEMENTED_attr[-1], + parameters.children[-1], ] # type: ignore if ( NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap index 0c05857e26..00e41d0c6a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap @@ -82,7 +82,7 @@ def func(): - if isinstance(exc_value, MultiError): + if NOT_IMPLEMENTED_call(): embedded = [] -- for exc in exc_value.exceptions: + for exc in exc_value.exceptions: - if exc not in _seen: - embedded.append( - # This should be left alone (before) @@ -97,7 +97,6 @@ def func(): - ) - # This should be left alone (after) - ) -+ for exc in exc_value.NOT_IMPLEMENTED_attr: + if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + NOT_IMPLEMENTED_call() @@ -130,7 +129,7 @@ def func(): # Capture each of the exceptions in the MultiError along with each of their causes and contexts if NOT_IMPLEMENTED_call(): embedded = [] - for exc in exc_value.NOT_IMPLEMENTED_attr: + for exc in exc_value.exceptions: if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: NOT_IMPLEMENTED_call() diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap index debbe95378..ef6cfc7bec 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap @@ -88,9 +88,8 @@ if __name__ == "__main__": +++ Ruff @@ -1,6 +1,6 @@ while True: -- if something.changed: + if something.changed: - do.stuff() # trailing comment -+ if something.NOT_IMPLEMENTED_attr: + NOT_IMPLEMENTED_call() # trailing comment # Comment belongs to the `if` block. # This one belongs to the `while` block. @@ -118,11 +117,11 @@ if __name__ == "__main__": -try: - with open(some_other_file) as w: - w.write(data) -+NOT_YET_IMPLEMENTED_StmtTry - +- -except OSError: - print("problems") -- ++NOT_YET_IMPLEMENTED_StmtTry + -import sys +NOT_YET_IMPLEMENTED_StmtImport @@ -160,7 +159,7 @@ if __name__ == "__main__": ```py while True: - if something.NOT_IMPLEMENTED_attr: + if something.changed: NOT_IMPLEMENTED_call() # trailing comment # Comment belongs to the `if` block. # This one belongs to the `while` block. diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap index 099f07777c..e23d22d0e3 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap @@ -105,7 +105,7 @@ def g(): ```diff --- Black +++ Ruff -@@ -1,89 +1,79 @@ +@@ -1,59 +1,46 @@ -"""Docstring.""" +"NOT_YET_IMPLEMENTED_STRING" @@ -119,12 +119,9 @@ def g(): + SPACE = "NOT_YET_IMPLEMENTED_STRING" + DOUBLESPACE = "NOT_YET_IMPLEMENTED_STRING" -- t = leaf.type -- p = leaf.parent # trailing comment -- v = leaf.value -+ t = leaf.NOT_IMPLEMENTED_attr -+ p = leaf.NOT_IMPLEMENTED_attr # trailing comment -+ v = leaf.NOT_IMPLEMENTED_attr + t = leaf.type + p = leaf.parent # trailing comment + v = leaf.value - if t in ALWAYS_NO_SPACE: + if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: @@ -136,8 +133,7 @@ def g(): - assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}" + NOT_YET_IMPLEMENTED_StmtAssert -- prev = leaf.prev_sibling -+ prev = leaf.NOT_IMPLEMENTED_attr + prev = leaf.prev_sibling if not prev: - prevp = preceding_leaf(p) - if not prevp or prevp.type in OPENING_BRACKETS: @@ -154,10 +150,7 @@ def g(): - syms.argument, - }: + if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ if ( -+ prevp.NOT_IMPLEMENTED_attr -+ and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+ ): ++ if prevp.parent and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: return NO - elif prevp.type == token.DOUBLESTAR: @@ -169,10 +162,7 @@ def g(): - syms.dictsetmaker, - }: + elif NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ if ( -+ prevp.NOT_IMPLEMENTED_attr -+ and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+ ): ++ if prevp.parent and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: return NO @@ -189,12 +179,9 @@ def g(): + SPACE = "NOT_YET_IMPLEMENTED_STRING" + DOUBLESPACE = "NOT_YET_IMPLEMENTED_STRING" -- t = leaf.type -- p = leaf.parent -- v = leaf.value -+ t = leaf.NOT_IMPLEMENTED_attr -+ p = leaf.NOT_IMPLEMENTED_attr -+ v = leaf.NOT_IMPLEMENTED_attr + t = leaf.type + p = leaf.parent +@@ -61,29 +48,23 @@ # Comment because comments @@ -209,8 +196,7 @@ def g(): - assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}" + NOT_YET_IMPLEMENTED_StmtAssert -- prev = leaf.prev_sibling -+ prev = leaf.NOT_IMPLEMENTED_attr + prev = leaf.prev_sibling if not prev: - prevp = preceding_leaf(p) + prevp = NOT_IMPLEMENTED_call() @@ -230,10 +216,7 @@ def g(): - syms.argument, - }: + if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ if ( -+ prevp.NOT_IMPLEMENTED_attr -+ and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+ ): ++ if prevp.parent and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: return NO ``` @@ -249,9 +232,9 @@ def f(): SPACE = "NOT_YET_IMPLEMENTED_STRING" DOUBLESPACE = "NOT_YET_IMPLEMENTED_STRING" - t = leaf.NOT_IMPLEMENTED_attr - p = leaf.NOT_IMPLEMENTED_attr # trailing comment - v = leaf.NOT_IMPLEMENTED_attr + t = leaf.type + p = leaf.parent # trailing comment + v = leaf.value if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: pass @@ -260,24 +243,18 @@ def f(): NOT_YET_IMPLEMENTED_StmtAssert - prev = leaf.NOT_IMPLEMENTED_attr + prev = leaf.prev_sibling if not prev: prevp = NOT_IMPLEMENTED_call() if not prevp or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: return NO if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - if ( - prevp.NOT_IMPLEMENTED_attr - and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right - ): + if prevp.parent and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: return NO elif NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - if ( - prevp.NOT_IMPLEMENTED_attr - and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right - ): + if prevp.parent and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: return NO @@ -290,9 +267,9 @@ def g(): SPACE = "NOT_YET_IMPLEMENTED_STRING" DOUBLESPACE = "NOT_YET_IMPLEMENTED_STRING" - t = leaf.NOT_IMPLEMENTED_attr - p = leaf.NOT_IMPLEMENTED_attr - v = leaf.NOT_IMPLEMENTED_attr + t = leaf.type + p = leaf.parent + v = leaf.value # Comment because comments @@ -304,7 +281,7 @@ def g(): # Another comment because more comments NOT_YET_IMPLEMENTED_StmtAssert - prev = leaf.NOT_IMPLEMENTED_attr + prev = leaf.prev_sibling if not prev: prevp = NOT_IMPLEMENTED_call() @@ -314,10 +291,7 @@ def g(): return NO if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - if ( - prevp.NOT_IMPLEMENTED_attr - and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right - ): + if prevp.parent and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: return NO ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap index 78ff32e2eb..b2ff9dc82e 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap @@ -276,7 +276,7 @@ last_call() Name None True -@@ -30,134 +31,120 @@ +@@ -30,98 +31,90 @@ -1 ~int and not v1 ^ 123 + v2 | True (~int) and (not ((v1 ^ (123 + v2)) | True)) @@ -297,8 +297,15 @@ last_call() -(str or None) if True else (str or bytes or None) -str or None if (1 if True else 2) else str or bytes or None -(str or None) if (1 if True else 2) else (str or bytes or None) +-( +- (super_long_variable_name or None) +- if (1 if super_long_test_name else 2) +- else (str or bytes or None) +-) +-{"2.7": dead, "3.7": (long_live or die_hard)} +-{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} ++really ** -confusing ** ~operator**-precedence -+flags & ~select.NOT_IMPLEMENTED_attr and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++flags & ~select.EPOLLIN and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +lambda x: True +lambda x: True +lambda x: True @@ -321,7 +328,9 @@ last_call() + "NOT_YET_IMPLEMENTED_STRING": (long_live or die_hard), + **{"NOT_YET_IMPLEMENTED_STRING": verygood}, +} -+{**a, **b, **c} + {**a, **b, **c} +-{"2.7", "3.6", "3.7", "3.8", "3.9", ("4.0" if gilectomy else "3.10")} +-({"a": "b"}, (True or False), (+value), "string", b"bytes") or None +{ + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", @@ -330,16 +339,7 @@ last_call() + "NOT_YET_IMPLEMENTED_STRING", + (NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false), +} - ( -- (super_long_variable_name or None) -- if (1 if super_long_test_name else 2) -- else (str or bytes or None) --) --{"2.7": dead, "3.7": (long_live or die_hard)} --{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} --{**a, **b, **c} --{"2.7", "3.6", "3.7", "3.8", "3.9", ("4.0" if gilectomy else "3.10")} --({"a": "b"}, (True or False), (+value), "string", b"bytes") or None ++( + {"NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING"}, + (True or False), + (+value), @@ -364,17 +364,17 @@ last_call() - 4, - 5, -] --[ -- 4, -- *a, -- 5, --] +[1, 2, 3] +[NOT_YET_IMPLEMENTED_ExprStarred] +[NOT_YET_IMPLEMENTED_ExprStarred] +[NOT_YET_IMPLEMENTED_ExprStarred, 4, 5] +[4, NOT_YET_IMPLEMENTED_ExprStarred, 5] [ +- 4, +- *a, +- 5, +-] +-[ this_is_a_very_long_variable_which_will_force_a_delimiter_split, element, another, @@ -415,11 +415,6 @@ last_call() -call(a, *gidgets[:2]) -call(**self.screen_kwargs) -call(b, **self.screen_kwargs) --lukasz.langa.pl --call.me(maybe) --(1).real --(1.0).real --....__class__ +NOT_YET_IMPLEMENTED_ExprSetComp +NOT_YET_IMPLEMENTED_ExprSetComp +NOT_YET_IMPLEMENTED_ExprSetComp @@ -445,13 +440,13 @@ last_call() +NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call() -+lukasz.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr + lukasz.langa.pl +-call.me(maybe) +NOT_IMPLEMENTED_call() -+1 .NOT_IMPLEMENTED_attr -+1.0 .NOT_IMPLEMENTED_attr -+....NOT_IMPLEMENTED_attr - list[str] - dict[str, int] + (1).real + (1.0).real + ....__class__ +@@ -130,34 +123,28 @@ tuple[str, ...] tuple[str, int, float, dict[str, int]] tuple[ @@ -504,12 +499,11 @@ last_call() numpy[-(c + 1) :, d] numpy[:, l[-2]] -numpy[:, ::-1] --numpy[np.newaxis, :] ++numpy[:, :: -1] + numpy[np.newaxis, :] -(str or None) if (sys.version_info[0] > (3,)) else (str or bytes or None) -{"2.7": dead, "3.7": long_live or die_hard} -{"2.7", "3.6", "3.7", "3.8", "3.9", "4.0" if gilectomy else "3.10"} -+numpy[:, :: -1] -+numpy[np.NOT_IMPLEMENTED_attr, :] +NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false +{ + "NOT_YET_IMPLEMENTED_STRING": dead, @@ -570,10 +564,7 @@ last_call() ) -what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( - vars_to_remove -+what_is_up_with_those_new_coord_names = ( -+ (coord_names | NOT_IMPLEMENTED_call()) -+ - NOT_IMPLEMENTED_call() - ) +-) -result = ( - session.query(models.Customer.id) - .filter( @@ -591,7 +582,10 @@ last_call() - models.Customer.id.asc(), - ) - .all() --) ++what_is_up_with_those_new_coord_names = ( ++ (coord_names | NOT_IMPLEMENTED_call()) ++ - NOT_IMPLEMENTED_call() + ) -Ø = set() -authors.łukasz.say_thanks() +result = NOT_IMPLEMENTED_call() @@ -683,70 +677,40 @@ last_call() ): return True if ( -@@ -327,24 +296,44 @@ +@@ -327,13 +296,18 @@ ): return True if ( - ~aaaa.a + aaaa.b - aaaa.c * aaaa.d / aaaa.e -- | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l**aaaa.m // aaaa.n -+ ~aaaa.NOT_IMPLEMENTED_attr -+ + aaaa.NOT_IMPLEMENTED_attr -+ - aaaa.NOT_IMPLEMENTED_attr * aaaa.NOT_IMPLEMENTED_attr / aaaa.NOT_IMPLEMENTED_attr -+ | aaaa.NOT_IMPLEMENTED_attr -+ & aaaa.NOT_IMPLEMENTED_attr % aaaa.NOT_IMPLEMENTED_attr -+ ^ aaaa.NOT_IMPLEMENTED_attr -+ << aaaa.NOT_IMPLEMENTED_attr -+ >> aaaa.NOT_IMPLEMENTED_attr**aaaa.NOT_IMPLEMENTED_attr // aaaa.NOT_IMPLEMENTED_attr ++ ~aaaa.a ++ + aaaa.b ++ - aaaa.c * aaaa.d / aaaa.e + | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l**aaaa.m // aaaa.n ): return True if ( - ~aaaaaaaa.a + aaaaaaaa.b - aaaaaaaa.c @ aaaaaaaa.d / aaaaaaaa.e - | aaaaaaaa.f & aaaaaaaa.g % aaaaaaaa.h -- ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n -+ ~aaaaaaaa.NOT_IMPLEMENTED_attr -+ + aaaaaaaa.NOT_IMPLEMENTED_attr -+ - aaaaaaaa.NOT_IMPLEMENTED_attr -+ @ aaaaaaaa.NOT_IMPLEMENTED_attr -+ / aaaaaaaa.NOT_IMPLEMENTED_attr -+ | aaaaaaaa.NOT_IMPLEMENTED_attr -+ & aaaaaaaa.NOT_IMPLEMENTED_attr % aaaaaaaa.NOT_IMPLEMENTED_attr -+ ^ aaaaaaaa.NOT_IMPLEMENTED_attr -+ << aaaaaaaa.NOT_IMPLEMENTED_attr -+ >> aaaaaaaa.NOT_IMPLEMENTED_attr -+ **aaaaaaaa.NOT_IMPLEMENTED_attr -+ // aaaaaaaa.NOT_IMPLEMENTED_attr ++ ~aaaaaaaa.a ++ + aaaaaaaa.b ++ - aaaaaaaa.c @ aaaaaaaa.d / aaaaaaaa.e ++ | aaaaaaaa.f ++ & aaaaaaaa.g % aaaaaaaa.h + ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n ): return True - if ( -- ~aaaaaaaaaaaaaaaa.a -- + aaaaaaaaaaaaaaaa.b -- - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e +@@ -341,7 +315,8 @@ + ~aaaaaaaaaaaaaaaa.a + + aaaaaaaaaaaaaaaa.b + - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e - | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h -- ^ aaaaaaaaaaaaaaaa.i -- << aaaaaaaaaaaaaaaa.k -- >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n -+ ~aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ + aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ - aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ * aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ @ aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ | aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ & aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr % aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ ^ aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ << aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ >> aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ **aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ // aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - ): - return True - ( -@@ -363,8 +352,9 @@ - bbbb >> bbbb * bbbb - ( - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -- ^ bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -+ ^ bbbb.NOT_IMPLEMENTED_attr -+ & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++ | aaaaaaaaaaaaaaaa.f ++ & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h + ^ aaaaaaaaaaaaaaaa.i + << aaaaaaaaaaaaaaaa.k + >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n +@@ -366,5 +341,5 @@ + ^ bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ^ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ) -last_call() @@ -791,7 +755,7 @@ not great ~int and not v1 ^ 123 + v2 | True (~int) and (not ((v1 ^ (123 + v2)) | True)) +really ** -confusing ** ~operator**-precedence -flags & ~select.NOT_IMPLEMENTED_attr and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +flags & ~select.EPOLLIN and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right lambda x: True lambda x: True lambda x: True @@ -872,11 +836,11 @@ NOT_IMPLEMENTED_call() NOT_IMPLEMENTED_call() NOT_IMPLEMENTED_call() NOT_IMPLEMENTED_call() -lukasz.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr +lukasz.langa.pl NOT_IMPLEMENTED_call() -1 .NOT_IMPLEMENTED_attr -1.0 .NOT_IMPLEMENTED_attr -....NOT_IMPLEMENTED_attr +(1).real +(1.0).real +....__class__ list[str] dict[str, int] tuple[str, ...] @@ -918,7 +882,7 @@ numpy[1 : c + 1, c] numpy[-(c + 1) :, d] numpy[:, l[-2]] numpy[:, :: -1] -numpy[np.NOT_IMPLEMENTED_attr, :] +numpy[np.newaxis, :] NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false { "NOT_YET_IMPLEMENTED_STRING": dead, @@ -1055,44 +1019,30 @@ if ( ): return True if ( - ~aaaa.NOT_IMPLEMENTED_attr - + aaaa.NOT_IMPLEMENTED_attr - - aaaa.NOT_IMPLEMENTED_attr * aaaa.NOT_IMPLEMENTED_attr / aaaa.NOT_IMPLEMENTED_attr - | aaaa.NOT_IMPLEMENTED_attr - & aaaa.NOT_IMPLEMENTED_attr % aaaa.NOT_IMPLEMENTED_attr - ^ aaaa.NOT_IMPLEMENTED_attr - << aaaa.NOT_IMPLEMENTED_attr - >> aaaa.NOT_IMPLEMENTED_attr**aaaa.NOT_IMPLEMENTED_attr // aaaa.NOT_IMPLEMENTED_attr + ~aaaa.a + + aaaa.b + - aaaa.c * aaaa.d / aaaa.e + | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l**aaaa.m // aaaa.n ): return True if ( - ~aaaaaaaa.NOT_IMPLEMENTED_attr - + aaaaaaaa.NOT_IMPLEMENTED_attr - - aaaaaaaa.NOT_IMPLEMENTED_attr - @ aaaaaaaa.NOT_IMPLEMENTED_attr - / aaaaaaaa.NOT_IMPLEMENTED_attr - | aaaaaaaa.NOT_IMPLEMENTED_attr - & aaaaaaaa.NOT_IMPLEMENTED_attr % aaaaaaaa.NOT_IMPLEMENTED_attr - ^ aaaaaaaa.NOT_IMPLEMENTED_attr - << aaaaaaaa.NOT_IMPLEMENTED_attr - >> aaaaaaaa.NOT_IMPLEMENTED_attr - **aaaaaaaa.NOT_IMPLEMENTED_attr - // aaaaaaaa.NOT_IMPLEMENTED_attr + ~aaaaaaaa.a + + aaaaaaaa.b + - aaaaaaaa.c @ aaaaaaaa.d / aaaaaaaa.e + | aaaaaaaa.f + & aaaaaaaa.g % aaaaaaaa.h + ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n ): return True if ( - ~aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - + aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - - aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - * aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - @ aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - | aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - & aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr % aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - ^ aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - << aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - >> aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - **aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - // aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr + ~aaaaaaaaaaaaaaaa.a + + aaaaaaaaaaaaaaaa.b + - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e + | aaaaaaaaaaaaaaaa.f + & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h + ^ aaaaaaaaaaaaaaaa.i + << aaaaaaaaaaaaaaaa.k + >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n ): return True ( @@ -1111,8 +1061,7 @@ aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa bbbb >> bbbb * bbbb ( aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - ^ bbbb.NOT_IMPLEMENTED_attr - & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + ^ bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ^ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ) NOT_IMPLEMENTED_call() diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap index b99773a340..de6064324a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap @@ -253,19 +253,18 @@ d={'a':1, - async with some_connection() as conn: - await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2) - await asyncio.sleep(1) --@asyncio.coroutine ++ "NOT_YET_IMPLEMENTED_STRING" ++ NOT_YET_IMPLEMENTED_StmtAsyncWith ++ await NOT_IMPLEMENTED_call() ++ ++ + @asyncio.coroutine -@some_decorator( -with_args=True, -many_args=[1,2,3] -) -def function_signature_stress_test(number:int,no_annotation=None,text:str='default',* ,debug:bool=False,**kwargs) -> str: - return text[number:-1] -+ "NOT_YET_IMPLEMENTED_STRING" -+ NOT_YET_IMPLEMENTED_StmtAsyncWith -+ await NOT_IMPLEMENTED_call() -+ -+ -+@asyncio.NOT_IMPLEMENTED_attr +@NOT_IMPLEMENTED_call() +def function_signature_stress_test( + number: int, @@ -399,7 +398,7 @@ d={'a':1, # fmt: off # hey, that won't work -@@ -130,13 +146,15 @@ +@@ -130,13 +146,13 @@ def on_and_off_broken(): @@ -414,13 +413,11 @@ d={'a':1, + this = NOT_IMPLEMENTED_call() + and_ = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + NOT_IMPLEMENTED_call() -+ ( -+ now.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr -+ ) ++ now.considers.multiple.fmt.directives.within.one.prefix # fmt: on # fmt: off # ...but comments still get reformatted even though they should not be -@@ -145,80 +163,21 @@ +@@ -145,80 +161,21 @@ def long_lines(): if True: @@ -552,7 +549,7 @@ async def coroutine(arg, exec=False): await NOT_IMPLEMENTED_call() -@asyncio.NOT_IMPLEMENTED_attr +@asyncio.coroutine @NOT_IMPLEMENTED_call() def function_signature_stress_test( number: int, @@ -668,9 +665,7 @@ def on_and_off_broken(): this = NOT_IMPLEMENTED_call() and_ = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right NOT_IMPLEMENTED_call() - ( - now.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr - ) + now.considers.multiple.fmt.directives.within.one.prefix # fmt: on # fmt: off # ...but comments still get reformatted even though they should not be diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap index f983c55b44..9379157af1 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap @@ -116,10 +116,10 @@ def __await__(): return (yield) +NOT_YET_IMPLEMENTED_StmtImport -from third_party import X, Y, Z +- +-from library import some_connection, some_decorator +NOT_YET_IMPLEMENTED_StmtImportFrom --from library import some_connection, some_decorator -- -f"trigger 3.6 mode" +NOT_YET_IMPLEMENTED_StmtImportFrom +NOT_YET_IMPLEMENTED_ExprJoinedStr @@ -154,9 +154,8 @@ def __await__(): return (yield) + await NOT_IMPLEMENTED_call() --@asyncio.coroutine + @asyncio.coroutine -@some_decorator(with_args=True, many_args=[1, 2, 3]) -+@asyncio.NOT_IMPLEMENTED_attr +@NOT_IMPLEMENTED_call() def function_signature_stress_test( number: int, @@ -323,7 +322,7 @@ async def coroutine(arg, exec=False): await NOT_IMPLEMENTED_call() -@asyncio.NOT_IMPLEMENTED_attr +@asyncio.coroutine @NOT_IMPLEMENTED_call() def function_signature_stress_test( number: int, diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap index 4160f7edaf..df1dfc81f6 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap @@ -62,7 +62,7 @@ __all__ = ( ```diff --- Black +++ Ruff -@@ -1,64 +1,42 @@ +@@ -1,54 +1,32 @@ -"""The asyncio package, tracking PEP 3156.""" +"NOT_YET_IMPLEMENTED_STRING" @@ -136,27 +136,7 @@ __all__ = ( +NOT_YET_IMPLEMENTED_StmtImportFrom __all__ = ( -- base_events.__all__ -- + coroutines.__all__ -- + events.__all__ -- + futures.__all__ -- + locks.__all__ -- + protocols.__all__ -- + runners.__all__ -- + queues.__all__ -- + streams.__all__ -- + tasks.__all__ -+ base_events.NOT_IMPLEMENTED_attr -+ + coroutines.NOT_IMPLEMENTED_attr -+ + events.NOT_IMPLEMENTED_attr -+ + futures.NOT_IMPLEMENTED_attr -+ + locks.NOT_IMPLEMENTED_attr -+ + protocols.NOT_IMPLEMENTED_attr -+ + runners.NOT_IMPLEMENTED_attr -+ + queues.NOT_IMPLEMENTED_attr -+ + streams.NOT_IMPLEMENTED_attr -+ + tasks.NOT_IMPLEMENTED_attr - ) + base_events.__all__ ``` ## Ruff Output @@ -193,16 +173,16 @@ NOT_YET_IMPLEMENTED_StmtImportFrom NOT_YET_IMPLEMENTED_StmtImportFrom __all__ = ( - base_events.NOT_IMPLEMENTED_attr - + coroutines.NOT_IMPLEMENTED_attr - + events.NOT_IMPLEMENTED_attr - + futures.NOT_IMPLEMENTED_attr - + locks.NOT_IMPLEMENTED_attr - + protocols.NOT_IMPLEMENTED_attr - + runners.NOT_IMPLEMENTED_attr - + queues.NOT_IMPLEMENTED_attr - + streams.NOT_IMPLEMENTED_attr - + tasks.NOT_IMPLEMENTED_attr + base_events.__all__ + + coroutines.__all__ + + events.__all__ + + futures.__all__ + + locks.__all__ + + protocols.__all__ + + runners.__all__ + + queues.__all__ + + streams.__all__ + + tasks.__all__ ) ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap index 3ccecffed8..bf18bb0df8 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap @@ -86,19 +86,18 @@ return np.divide( -d = 5 ** f["hi"] -e = lazy(lambda **kwargs: 5) -f = f() ** 5 --g = a.b**c.d ++d = 5 ** f["NOT_YET_IMPLEMENTED_STRING"] ++e = NOT_IMPLEMENTED_call() ++f = NOT_IMPLEMENTED_call() ** 5 + g = a.b**c.d -h = 5 ** funcs.f() -i = funcs.f() ** 5 -j = super().name ** 5 -k = [(2**idx, value) for idx, value in pairs] -l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) -+d = 5 ** f["NOT_YET_IMPLEMENTED_STRING"] -+e = NOT_IMPLEMENTED_call() -+f = NOT_IMPLEMENTED_call() ** 5 -+g = a.NOT_IMPLEMENTED_attr**c.NOT_IMPLEMENTED_attr +h = 5 ** NOT_IMPLEMENTED_call() +i = NOT_IMPLEMENTED_call() ** 5 -+j = NOT_IMPLEMENTED_call().NOT_IMPLEMENTED_attr ** 5 ++j = NOT_IMPLEMENTED_call().name ** 5 +k = [i for i in []] +l = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right m = [([2**63], [1, 2**63])] @@ -119,19 +118,18 @@ return np.divide( -d = 5.0 ** f["hi"] -e = lazy(lambda **kwargs: 5) -f = f() ** 5.0 --g = a.b**c.d ++d = 5.0 ** f["NOT_YET_IMPLEMENTED_STRING"] ++e = NOT_IMPLEMENTED_call() ++f = NOT_IMPLEMENTED_call() ** 5.0 + g = a.b**c.d -h = 5.0 ** funcs.f() -i = funcs.f() ** 5.0 -j = super().name ** 5.0 -k = [(2.0**idx, value) for idx, value in pairs] -l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) -+d = 5.0 ** f["NOT_YET_IMPLEMENTED_STRING"] -+e = NOT_IMPLEMENTED_call() -+f = NOT_IMPLEMENTED_call() ** 5.0 -+g = a.NOT_IMPLEMENTED_attr**c.NOT_IMPLEMENTED_attr +h = 5.0 ** NOT_IMPLEMENTED_call() +i = NOT_IMPLEMENTED_call() ** 5.0 -+j = NOT_IMPLEMENTED_call().NOT_IMPLEMENTED_attr ** 5.0 ++j = NOT_IMPLEMENTED_call().name ** 5.0 +k = [i for i in []] +l = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right m = [([2.0**63.0], [1.0, 2**63.0])] @@ -184,10 +182,10 @@ c = -(5**2) d = 5 ** f["NOT_YET_IMPLEMENTED_STRING"] e = NOT_IMPLEMENTED_call() f = NOT_IMPLEMENTED_call() ** 5 -g = a.NOT_IMPLEMENTED_attr**c.NOT_IMPLEMENTED_attr +g = a.b**c.d h = 5 ** NOT_IMPLEMENTED_call() i = NOT_IMPLEMENTED_call() ** 5 -j = NOT_IMPLEMENTED_call().NOT_IMPLEMENTED_attr ** 5 +j = NOT_IMPLEMENTED_call().name ** 5 k = [i for i in []] l = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right m = [([2**63], [1, 2**63])] @@ -203,10 +201,10 @@ c = -(5.0**2.0) d = 5.0 ** f["NOT_YET_IMPLEMENTED_STRING"] e = NOT_IMPLEMENTED_call() f = NOT_IMPLEMENTED_call() ** 5.0 -g = a.NOT_IMPLEMENTED_attr**c.NOT_IMPLEMENTED_attr +g = a.b**c.d h = 5.0 ** NOT_IMPLEMENTED_call() i = NOT_IMPLEMENTED_call() ** 5.0 -j = NOT_IMPLEMENTED_call().NOT_IMPLEMENTED_attr ** 5.0 +j = NOT_IMPLEMENTED_call().name ** 5.0 k = [i for i in []] l = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right m = [([2.0**63.0], [1.0, 2**63.0])] diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap index d43018c8dc..b745fd054e 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap @@ -44,7 +44,7 @@ for (((((k, v))))) in d.items(): - if hasattr(module, "_verify_python3_env"): - module._verify_python3_env = lambda: None + if NOT_IMPLEMENTED_call(): -+ module.NOT_IMPLEMENTED_attr = lambda x: True ++ module._verify_python3_env = lambda x: True # Brackets remain for long for loop lines for ( @@ -82,7 +82,7 @@ for k, v in NOT_IMPLEMENTED_call(): # Don't touch tuple brackets after `in` for module in (core, _unicodefun): if NOT_IMPLEMENTED_call(): - module.NOT_IMPLEMENTED_attr = lambda x: True + module._verify_python3_env = lambda x: True # Brackets remain for long for loop lines for ( diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap index 808ecffe6b..f13a98ef72 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap @@ -76,11 +76,7 @@ x[ ```diff --- Black +++ Ruff -@@ -1,33 +1,33 @@ --slice[a.b : c.d] -+slice[a.NOT_IMPLEMENTED_attr : c.NOT_IMPLEMENTED_attr] - slice[d :: d + 1] - slice[d + 1 :: d] +@@ -4,30 +4,30 @@ slice[d::d] slice[0] slice[-1] @@ -144,7 +140,7 @@ x[ ## Ruff Output ```py -slice[a.NOT_IMPLEMENTED_attr : c.NOT_IMPLEMENTED_attr] +slice[a.b : c.d] slice[d :: d + 1] slice[d + 1 :: d] slice[d::d] diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__attribute_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__attribute_py.snap new file mode 100644 index 0000000000..991210a185 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__attribute_py.snap @@ -0,0 +1,73 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +--- +## Input +```py +( + a + # comment + .b # trailing comment +) + +( + a + # comment + .b # trailing dot comment # trailing identifier comment +) + +( + a + # comment + .b # trailing identifier comment +) + + +( + a + # comment + . # trailing dot comment + # in between + b # trailing identifier comment +) + + +aaaaaaaaaaaaaaaaaaaaa.lllllllllllllllllllllllllllloooooooooong.chaaaaaaaaaaaaaaaaaaaaaaiiiiiiiiiiiiiiiiiiiiiiinnnnnnnn.ooooooooooooooooooooooooofffffffff.aaaaaaaaaattr +``` + + + +## Output +```py +( + a + # comment + .b # trailing comment +) + +( + a + # comment + .b # trailing dot comment # trailing identifier comment +) + +( + a + # comment + .b # trailing identifier comment +) + + +( + a + # comment + . # trailing dot comment + # in between + b # trailing identifier comment +) + + +aaaaaaaaaaaaaaaaaaaaa.lllllllllllllllllllllllllllloooooooooong.chaaaaaaaaaaaaaaaaaaaaaaiiiiiiiiiiiiiiiiiiiiiiinnnnnnnn.ooooooooooooooooooooooooofffffffff.aaaaaaaaaattr +``` + + diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__boolean_operation_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__boolean_operation_py.snap index 4bc16ef83a..cfc2bd43fe 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__boolean_operation_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__boolean_operation_py.snap @@ -75,9 +75,9 @@ if ( ## Output ```py if ( - self.NOT_IMPLEMENTED_attr + self._proc # has the child process finished? - and self.NOT_IMPLEMENTED_attr + and self._returncode # the child process has finished, but the # transport hasn't been notified yet? and NOT_IMPLEMENTED_call() @@ -85,11 +85,11 @@ if ( pass if ( - self.NOT_IMPLEMENTED_attr - and self.NOT_IMPLEMENTED_attr + self._proc + and self._returncode and NOT_IMPLEMENTED_call() - and self.NOT_IMPLEMENTED_attr - and self.NOT_IMPLEMENTED_attr + and self._proc + and self._returncode and NOT_IMPLEMENTED_call() ): ... diff --git a/crates/ruff_python_formatter/src/trivia.rs b/crates/ruff_python_formatter/src/trivia.rs index 955f77c93f..942e75d341 100644 --- a/crates/ruff_python_formatter/src/trivia.rs +++ b/crates/ruff_python_formatter/src/trivia.rs @@ -164,6 +164,9 @@ pub(crate) enum TokenKind { /// '*' Star, + /// `.`. + Dot, + /// Any other non trivia token. Always has a length of 1 Other, @@ -184,6 +187,7 @@ impl TokenKind { ':' => TokenKind::Colon, '/' => TokenKind::Slash, '*' => TokenKind::Star, + '.' => TokenKind::Dot, _ => TokenKind::Other, } } From 1229600e1d66ea736dec42596c3495045ba0287f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 21 Jun 2023 19:59:44 -0400 Subject: [PATCH 177/447] Ignore Pydantic classes when evaluating `mutable-class-default` (`RUF012`) (#5273) Closes https://github.com/astral-sh/ruff/issues/5272. --- crates/ruff/resources/test/fixtures/ruff/RUF012.py | 12 ++++++++++++ crates/ruff/src/rules/ruff/rules/helpers.rs | 9 +++++++++ .../src/rules/ruff/rules/mutable_class_default.rs | 14 +++++++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF012.py b/crates/ruff/resources/test/fixtures/ruff/RUF012.py index 1a51f179cd..83cb33045b 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF012.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF012.py @@ -31,3 +31,15 @@ class C: correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT perfectly_fine: list[int] = field(default_factory=list) class_variable: ClassVar[list[int]] = [] + + +from pydantic import BaseModel + + +class D(BaseModel): + mutable_default: list[int] = [] + immutable_annotation: Sequence[int] = [] + without_annotation = [] + correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT + perfectly_fine: list[int] = field(default_factory=list) + class_variable: ClassVar[list[int]] = [] diff --git a/crates/ruff/src/rules/ruff/rules/helpers.rs b/crates/ruff/src/rules/ruff/rules/helpers.rs index b5d09012e5..83cf0db919 100644 --- a/crates/ruff/src/rules/ruff/rules/helpers.rs +++ b/crates/ruff/src/rules/ruff/rules/helpers.rs @@ -28,3 +28,12 @@ pub(super) fn is_dataclass(class_def: &ast::StmtClassDef, semantic: &SemanticMod }) }) } + +/// Returns `true` if the given class is a Pydantic `BaseModel`. +pub(super) fn is_pydantic_model(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool { + class_def.bases.iter().any(|expr| { + semantic.resolve_call_path(expr).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["pydantic", "BaseModel"]) + }) + }) +} diff --git a/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs b/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs index d2c263ec21..83b02fe3c2 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs @@ -5,7 +5,9 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr}; use crate::checkers::ast::Checker; -use crate::rules::ruff::rules::helpers::{is_class_var_annotation, is_dataclass}; +use crate::rules::ruff::rules::helpers::{ + is_class_var_annotation, is_dataclass, is_pydantic_model, +}; /// ## What it does /// Checks for mutable default values in class attributes. @@ -57,6 +59,11 @@ pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::Stmt && !is_immutable_annotation(annotation, checker.semantic()) && !is_dataclass(class_def, checker.semantic()) { + // Avoid Pydantic models, which end up copying defaults on instance creation. + if is_pydantic_model(class_def, checker.semantic()) { + return; + } + checker .diagnostics .push(Diagnostic::new(MutableClassDefault, value.range())); @@ -64,6 +71,11 @@ pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::Stmt } Stmt::Assign(ast::StmtAssign { value, .. }) => { if is_mutable_expr(value, checker.semantic()) { + // Avoid Pydantic models, which end up copying defaults on instance creation. + if is_pydantic_model(class_def, checker.semantic()) { + return; + } + checker .diagnostics .push(Diagnostic::new(MutableClassDefault, value.range())); From ac146e11f01e4e6756f94999a8d3ab377ddcd8d3 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 21 Jun 2023 20:24:53 -0400 Subject: [PATCH 178/447] Allow `typing.Final` for `mutable-class-default annotations` (`RUF012`) (#5274) ## Summary See: https://github.com/astral-sh/ruff/issues/5243. --- .../resources/test/fixtures/ruff/RUF012.py | 6 +++- .../function_call_in_dataclass_default.rs | 2 +- crates/ruff/src/rules/ruff/rules/helpers.rs | 8 +++++ .../rules/ruff/rules/mutable_class_default.rs | 3 +- ..._rules__ruff__tests__RUF012_RUF012.py.snap | 34 +++++++++---------- 5 files changed, 33 insertions(+), 20 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF012.py b/crates/ruff/resources/test/fixtures/ruff/RUF012.py index 83cb33045b..081c13bac3 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF012.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF012.py @@ -1,5 +1,5 @@ import typing -from typing import ClassVar, Sequence +from typing import ClassVar, Sequence, Final KNOWINGLY_MUTABLE_DEFAULT = [] @@ -10,6 +10,7 @@ class A: without_annotation = [] correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT class_variable: typing.ClassVar[list[int]] = [] + final_variable: typing.Final[list[int]] = [] class B: @@ -18,6 +19,7 @@ class B: without_annotation = [] correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT class_variable: ClassVar[list[int]] = [] + final_variable: Final[list[int]] = [] from dataclasses import dataclass, field @@ -31,6 +33,7 @@ class C: correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT perfectly_fine: list[int] = field(default_factory=list) class_variable: ClassVar[list[int]] = [] + final_variable: Final[list[int]] = [] from pydantic import BaseModel @@ -43,3 +46,4 @@ class D(BaseModel): correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT perfectly_fine: list[int] = field(default_factory=list) class_variable: ClassVar[list[int]] = [] + final_variable: Final[list[int]] = [] diff --git a/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs b/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs index f2bea235a7..93961572ae 100644 --- a/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs +++ b/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs @@ -55,7 +55,7 @@ use crate::rules::ruff::rules::helpers::{ /// - `flake8-bugbear.extend-immutable-calls` #[violation] pub struct FunctionCallInDataclassDefaultArgument { - pub name: Option, + name: Option, } impl Violation for FunctionCallInDataclassDefaultArgument { diff --git a/crates/ruff/src/rules/ruff/rules/helpers.rs b/crates/ruff/src/rules/ruff/rules/helpers.rs index 83cf0db919..9643508cb7 100644 --- a/crates/ruff/src/rules/ruff/rules/helpers.rs +++ b/crates/ruff/src/rules/ruff/rules/helpers.rs @@ -18,6 +18,14 @@ pub(super) fn is_class_var_annotation(annotation: &Expr, semantic: &SemanticMode semantic.match_typing_expr(value, "ClassVar") } +/// Returns `true` if the given [`Expr`] is a `typing.Final` annotation. +pub(super) fn is_final_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool { + let Expr::Subscript(ast::ExprSubscript { value, .. }) = &annotation else { + return false; + }; + semantic.match_typing_expr(value, "Final") +} + /// Returns `true` if the given class is a dataclass. pub(super) fn is_dataclass(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool { class_def.decorator_list.iter().any(|decorator| { diff --git a/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs b/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs index 83b02fe3c2..2c5955e95b 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs @@ -6,7 +6,7 @@ use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_ use crate::checkers::ast::Checker; use crate::rules::ruff::rules::helpers::{ - is_class_var_annotation, is_dataclass, is_pydantic_model, + is_class_var_annotation, is_dataclass, is_final_annotation, is_pydantic_model, }; /// ## What it does @@ -56,6 +56,7 @@ pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::Stmt }) => { if is_mutable_expr(value, checker.semantic()) && !is_class_var_annotation(annotation, checker.semantic()) + && !is_final_annotation(annotation, checker.semantic()) && !is_immutable_annotation(annotation, checker.semantic()) && !is_dataclass(class_def, checker.semantic()) { diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap index 55536d8dc2..285c0c6acc 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap @@ -20,33 +20,33 @@ RUF012.py:10:26: RUF012 Mutable class attributes should be annotated with `typin 12 | class_variable: typing.ClassVar[list[int]] = [] | -RUF012.py:16:34: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` +RUF012.py:17:34: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` | -15 | class B: -16 | mutable_default: list[int] = [] +16 | class B: +17 | mutable_default: list[int] = [] | ^^ RUF012 -17 | immutable_annotation: Sequence[int] = [] -18 | without_annotation = [] +18 | immutable_annotation: Sequence[int] = [] +19 | without_annotation = [] | -RUF012.py:18:26: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` +RUF012.py:19:26: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` | -16 | mutable_default: list[int] = [] -17 | immutable_annotation: Sequence[int] = [] -18 | without_annotation = [] +17 | mutable_default: list[int] = [] +18 | immutable_annotation: Sequence[int] = [] +19 | without_annotation = [] | ^^ RUF012 -19 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT -20 | class_variable: ClassVar[list[int]] = [] +20 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT +21 | class_variable: ClassVar[list[int]] = [] | -RUF012.py:30:26: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` +RUF012.py:32:26: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` | -28 | mutable_default: list[int] = [] -29 | immutable_annotation: Sequence[int] = [] -30 | without_annotation = [] +30 | mutable_default: list[int] = [] +31 | immutable_annotation: Sequence[int] = [] +32 | without_annotation = [] | ^^ RUF012 -31 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT -32 | perfectly_fine: list[int] = field(default_factory=list) +33 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT +34 | perfectly_fine: list[int] = field(default_factory=list) | From c0c59b82ec661b59995ef79545602f4ab5352f8a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 21 Jun 2023 21:44:52 -0400 Subject: [PATCH 179/447] Use 'Checks for uses' consistently (#5279) --- .../src/rules/flake8_pyi/rules/bad_version_info_comparison.rs | 2 +- .../flake8_simplify/rules/open_file_with_context_handler.rs | 2 +- crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs | 2 +- crates/ruff/src/rules/pylint/rules/await_outside_async.rs | 2 +- .../src/rules/pylint/rules/load_before_global_declaration.rs | 2 +- .../ruff/src/rules/pylint/rules/named_expr_without_context.rs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) 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 305e1ba2c7..8d43070991 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 @@ -6,7 +6,7 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; /// ## What it does -/// Checks for usages of comparators other than `<` and `>=` for +/// Checks for uses of comparators other than `<` and `>=` for /// `sys.version_info` checks in `.pyi` files. All other comparators, such /// as `>`, `<=`, and `==`, are banned. /// 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 b59d76e040..98c35c4d15 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 @@ -7,7 +7,7 @@ use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; /// ## What it does -/// Checks for usages of the builtin `open()` function without an associated context +/// Checks for uses of the builtin `open()` function without an associated context /// manager. /// /// ## Why is this bad? diff --git a/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs b/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs index 921d634e51..a5d8572f95 100644 --- a/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs +++ b/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs @@ -6,7 +6,7 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; /// ## What it does -/// Checks for usages of the builtin `eval()` function. +/// Checks for uses of the builtin `eval()` function. /// /// ## Why is this bad? /// The `eval()` function is insecure as it enables arbitrary code execution. diff --git a/crates/ruff/src/rules/pylint/rules/await_outside_async.rs b/crates/ruff/src/rules/pylint/rules/await_outside_async.rs index dd17f4eb7b..850cf7a9f7 100644 --- a/crates/ruff/src/rules/pylint/rules/await_outside_async.rs +++ b/crates/ruff/src/rules/pylint/rules/await_outside_async.rs @@ -6,7 +6,7 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; /// ## What it does -/// Checks for usages of `await` outside of `async` functions. +/// Checks for uses of `await` outside of `async` functions. /// /// ## Why is this bad? /// Using `await` outside of an `async` function is a syntax error. diff --git a/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs b/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs index 687692ffbd..e97da007cb 100644 --- a/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs +++ b/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs @@ -7,7 +7,7 @@ use ruff_python_ast::source_code::OneIndexed; use crate::checkers::ast::Checker; /// ## What it does -/// Checks for usages of names that are declared as `global` prior to the +/// Checks for uses of names that are declared as `global` prior to the /// relevant `global` declaration. /// /// ## Why is this bad? diff --git a/crates/ruff/src/rules/pylint/rules/named_expr_without_context.rs b/crates/ruff/src/rules/pylint/rules/named_expr_without_context.rs index 3576d36ea3..27627585c8 100644 --- a/crates/ruff/src/rules/pylint/rules/named_expr_without_context.rs +++ b/crates/ruff/src/rules/pylint/rules/named_expr_without_context.rs @@ -6,7 +6,7 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; /// ## What it does -/// Checks for usages of named expressions (e.g., `a := 42`) that can be +/// Checks for uses of named expressions (e.g., `a := 42`) that can be /// replaced by regular assignment statements (e.g., `a = 42`). /// /// ## Why is this bad? From 6b8b318d6b6d8f70d4c715606fda6100c686fe31 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 21 Jun 2023 21:50:28 -0400 Subject: [PATCH 180/447] Use `mod tests` consistently (#5278) As per the Rust documentation. --- crates/ruff/src/jupyter/notebook.rs | 2 +- ...snap => ruff__jupyter__notebook__tests__import_sorting.snap} | 0 crates/ruff/src/rules/pyupgrade/fixes.rs | 2 +- .../ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs | 2 +- crates/ruff_cli/src/cache.rs | 2 +- crates/ruff_cli/src/diagnostics.rs | 2 +- crates/ruff_dev/src/generate_cli_help.rs | 2 +- crates/ruff_dev/src/generate_json_schema.rs | 2 +- 8 files changed, 7 insertions(+), 7 deletions(-) rename crates/ruff/src/jupyter/snapshots/{ruff__jupyter__notebook__test__import_sorting.snap => ruff__jupyter__notebook__tests__import_sorting.snap} (100%) diff --git a/crates/ruff/src/jupyter/notebook.rs b/crates/ruff/src/jupyter/notebook.rs index 0c206ace46..7e8a74bd0d 100644 --- a/crates/ruff/src/jupyter/notebook.rs +++ b/crates/ruff/src/jupyter/notebook.rs @@ -445,7 +445,7 @@ impl Notebook { } #[cfg(test)] -mod test { +mod tests { use std::path::Path; use anyhow::Result; diff --git a/crates/ruff/src/jupyter/snapshots/ruff__jupyter__notebook__test__import_sorting.snap b/crates/ruff/src/jupyter/snapshots/ruff__jupyter__notebook__tests__import_sorting.snap similarity index 100% rename from crates/ruff/src/jupyter/snapshots/ruff__jupyter__notebook__test__import_sorting.snap rename to crates/ruff/src/jupyter/snapshots/ruff__jupyter__notebook__tests__import_sorting.snap diff --git a/crates/ruff/src/rules/pyupgrade/fixes.rs b/crates/ruff/src/rules/pyupgrade/fixes.rs index 5f37034a2d..b20e351635 100644 --- a/crates/ruff/src/rules/pyupgrade/fixes.rs +++ b/crates/ruff/src/rules/pyupgrade/fixes.rs @@ -126,7 +126,7 @@ pub(crate) fn remove_import_members(contents: &str, members: &[&str]) -> String } #[cfg(test)] -mod test { +mod tests { use crate::rules::pyupgrade::fixes::remove_import_members; #[test] diff --git a/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs b/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs index b0ea539d23..48ede85924 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs @@ -477,7 +477,7 @@ pub(crate) fn printf_string_formatting( } #[cfg(test)] -mod test { +mod tests { use test_case::test_case; use super::*; diff --git a/crates/ruff_cli/src/cache.rs b/crates/ruff_cli/src/cache.rs index d543a8bf94..9c4aa3db73 100644 --- a/crates/ruff_cli/src/cache.rs +++ b/crates/ruff_cli/src/cache.rs @@ -299,7 +299,7 @@ pub(crate) fn init(path: &Path) -> Result<()> { } #[cfg(test)] -mod test { +mod tests { use std::env::temp_dir; use std::fs; use std::io::{self, Write}; diff --git a/crates/ruff_cli/src/diagnostics.rs b/crates/ruff_cli/src/diagnostics.rs index 7fabf26141..1da81ed4cd 100644 --- a/crates/ruff_cli/src/diagnostics.rs +++ b/crates/ruff_cli/src/diagnostics.rs @@ -346,7 +346,7 @@ pub(crate) fn lint_stdin( } #[cfg(test)] -mod test { +mod tests { use std::path::Path; use crate::diagnostics::{load_jupyter_notebook, Diagnostics}; diff --git a/crates/ruff_dev/src/generate_cli_help.rs b/crates/ruff_dev/src/generate_cli_help.rs index 25e2417467..420927495f 100644 --- a/crates/ruff_dev/src/generate_cli_help.rs +++ b/crates/ruff_dev/src/generate_cli_help.rs @@ -119,7 +119,7 @@ fn check_help_text() -> String { } #[cfg(test)] -mod test { +mod tests { use anyhow::Result; use crate::generate_all::Mode; diff --git a/crates/ruff_dev/src/generate_json_schema.rs b/crates/ruff_dev/src/generate_json_schema.rs index 046b917294..bde0f4abdc 100644 --- a/crates/ruff_dev/src/generate_json_schema.rs +++ b/crates/ruff_dev/src/generate_json_schema.rs @@ -53,7 +53,7 @@ pub(crate) fn main(args: &Args) -> Result<()> { } #[cfg(test)] -mod test { +mod tests { use anyhow::Result; use std::env; From 1c0a3a467f402676553c63d501aae7455c6c67b0 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 21 Jun 2023 21:53:37 -0400 Subject: [PATCH 181/447] Bump version to 0.0.275 (#5276) --- 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 e171d287d2..52a39d829f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -733,7 +733,7 @@ dependencies = [ [[package]] name = "flake8-to-ruff" -version = "0.0.274" +version = "0.0.275" dependencies = [ "anyhow", "clap", @@ -1793,7 +1793,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.0.274" +version = "0.0.275" dependencies = [ "annotate-snippets 0.9.1", "anyhow", @@ -1889,7 +1889,7 @@ dependencies = [ [[package]] name = "ruff_cli" -version = "0.0.274" +version = "0.0.275" dependencies = [ "annotate-snippets 0.9.1", "anyhow", diff --git a/README.md b/README.md index 66a3a8ccf7..fdf828ab1c 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.274 + rev: v0.0.275 hooks: - id: ruff ``` diff --git a/crates/flake8_to_ruff/Cargo.toml b/crates/flake8_to_ruff/Cargo.toml index c2f2e81217..f8191279e2 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.274" +version = "0.0.275" description = """ Convert Flake8 configuration files to Ruff configuration files. """ diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 4f482cff0c..8dee42ef80 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.0.274" +version = "0.0.275" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_cli/Cargo.toml b/crates/ruff_cli/Cargo.toml index 878923e286..d662bb91ce 100644 --- a/crates/ruff_cli/Cargo.toml +++ b/crates/ruff_cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_cli" -version = "0.0.274" +version = "0.0.275" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/docs/tutorial.md b/docs/tutorial.md index b474b378d9..3491519feb 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.274 + rev: v0.0.275 hooks: - id: ruff ``` diff --git a/docs/usage.md b/docs/usage.md index a3b9ad0039..8528ec9142 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.274 + rev: v0.0.275 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.274 + rev: v0.0.275 hooks: - id: ruff args: [ --fix, --exit-non-zero-on-fix ] diff --git a/pyproject.toml b/pyproject.toml index a2f53c596f..e5e55e84ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.0.274" +version = "0.0.275" 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 2c63f8cdeabf196d72225d3eb727abb121dcee6c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 21 Jun 2023 22:04:43 -0400 Subject: [PATCH 182/447] Update reference to release step (#5280) --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 434ab1a9ff..9b29bbedc2 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -500,7 +500,7 @@ jobs: update-dependents: name: Release runs-on: ubuntu-latest - needs: release + needs: publish-release steps: - name: "Update pre-commit mirror" uses: actions/github-script@v6 From 7d4f8e59da196d4d29aac94b66bbd83d966e8946 Mon Sep 17 00:00:00 2001 From: konstin Date: Thu, 22 Jun 2023 10:59:30 +0200 Subject: [PATCH 183/447] Improve FormatExprCall dummy (#5290) This solves an instability when formatting cpython. It also introduces another one, but i think it's still a worthwhile change for now. There's no proper testing since this is just a dummy. --- .../src/expression/expr_call.rs | 19 ++- crates/ruff_python_formatter/src/lib.rs | 4 +- ...ttribute_access_on_number_literals_py.snap | 8 +- ...s__black_test__beginning_backslash_py.snap | 4 +- ...er__tests__black_test__collections_py.snap | 16 +-- ...tter__tests__black_test__comments2_py.snap | 28 ++--- ...tter__tests__black_test__comments3_py.snap | 12 +- ...tter__tests__black_test__comments4_py.snap | 8 +- ...tter__tests__black_test__comments5_py.snap | 16 +-- ...tter__tests__black_test__comments6_py.snap | 16 +-- ...tter__tests__black_test__comments9_py.snap | 12 +- ..._test__comments_non_breaking_space_py.snap | 4 +- ...atter__tests__black_test__comments_py.snap | 20 ++-- ...er__tests__black_test__empty_lines_py.snap | 8 +- ...ter__tests__black_test__expression_py.snap | 110 +++++++++--------- ...tter__tests__black_test__fmtonoff2_py.snap | 4 +- ...tter__tests__black_test__fmtonoff4_py.snap | 8 +- ...tter__tests__black_test__fmtonoff5_py.snap | 28 ++--- ...atter__tests__black_test__fmtonoff_py.snap | 40 +++---- ...atter__tests__black_test__fmtskip5_py.snap | 8 +- ...atter__tests__black_test__fmtskip8_py.snap | 48 ++++---- ...tter__tests__black_test__function2_py.snap | 16 +-- ...atter__tests__black_test__function_py.snap | 40 +++---- ...lack_test__function_trailing_comma_py.snap | 24 ++-- ...ests__black_test__power_op_spacing_py.snap | 28 ++--- ...test__prefer_rhs_split_reformatted_py.snap | 16 +-- ...s__black_test__remove_await_parens_py.snap | 52 ++++----- ...s__black_test__remove_for_brackets_py.snap | 20 ++-- ...move_newline_after_code_block_open_py.snap | 72 ++++++------ ...ck_test__skip_magic_trailing_comma_py.snap | 16 +-- ...rmatter__tests__black_test__slices_py.snap | 18 ++- ...t__trailing_comma_optional_parens1_py.snap | 16 ++- ...t__trailing_comma_optional_parens3_py.snap | 4 +- ...__trailing_commas_in_leading_parts_py.snap | 20 ++-- ...er__tests__black_test__tupleassign_py.snap | 4 +- ...sts__ruff_test__expression__binary_py.snap | 2 +- ...tests__ruff_test__statement__while_py.snap | 8 +- 37 files changed, 403 insertions(+), 374 deletions(-) diff --git a/crates/ruff_python_formatter/src/expression/expr_call.rs b/crates/ruff_python_formatter/src/expression/expr_call.rs index 3f73b6cbbd..2e48d6aa56 100644 --- a/crates/ruff_python_formatter/src/expression/expr_call.rs +++ b/crates/ruff_python_formatter/src/expression/expr_call.rs @@ -10,11 +10,20 @@ use rustpython_parser::ast::ExprCall; pub struct FormatExprCall; impl FormatNodeRule for FormatExprCall { - fn fmt_fields(&self, _item: &ExprCall, f: &mut PyFormatter) -> FormatResult<()> { - write!( - f, - [not_yet_implemented_custom_text("NOT_IMPLEMENTED_call()")] - ) + fn fmt_fields(&self, item: &ExprCall, f: &mut PyFormatter) -> FormatResult<()> { + if item.args.is_empty() && item.keywords.is_empty() { + write!( + f, + [not_yet_implemented_custom_text("NOT_IMPLEMENTED_call()")] + ) + } else { + write!( + f, + [not_yet_implemented_custom_text( + "NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)" + )] + ) + } } } diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index ec332985a3..0faf824f09 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -247,12 +247,12 @@ mod tests { let input = r#" # preceding if True: - print( "hi" ) + pass # trailing "#; let expected = r#"# preceding if True: - NOT_IMPLEMENTED_call() + pass # trailing "#; let actual = format_module(input)?.as_code().to_string(); diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap index 72a7fd2ecd..62da55712c 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap @@ -62,7 +62,7 @@ y = 100(no) +x = (123456789.123456789E123456789).real +x = NOT_IMPLEMENTED_call() +x = 123456789J.real -+x = NOT_IMPLEMENTED_call() ++x = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +x = NOT_IMPLEMENTED_call() +x = NOT_IMPLEMENTED_call() +x = (0O777).real @@ -74,7 +74,7 @@ y = 100(no) y = 100[no] -y = 100(no) -+y = NOT_IMPLEMENTED_call() ++y = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Ruff Output @@ -90,7 +90,7 @@ x = NOT_IMPLEMENTED_call() x = (123456789.123456789E123456789).real x = NOT_IMPLEMENTED_call() x = 123456789J.real -x = NOT_IMPLEMENTED_call() +x = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) x = NOT_IMPLEMENTED_call() x = NOT_IMPLEMENTED_call() x = (0O777).real @@ -101,7 +101,7 @@ if (10).real: ... y = 100[no] -y = NOT_IMPLEMENTED_call() +y = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__beginning_backslash_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__beginning_backslash_py.snap index e6b8b1a44b..6b56a193c0 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__beginning_backslash_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__beginning_backslash_py.snap @@ -22,13 +22,13 @@ print("hello, world") +++ Ruff @@ -1 +1 @@ -print("hello, world") -+NOT_IMPLEMENTED_call() ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Ruff Output ```py -NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap index b567509488..4dd883ec2f 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap @@ -178,7 +178,7 @@ if True: division_result_tuple = (6 / 2,) -print("foo %r", (foo.bar,)) -+NOT_IMPLEMENTED_call() ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) if True: IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = ( @@ -204,9 +204,9 @@ if True: - "Delay": 5, - }, - ) -+ NOT_IMPLEMENTED_call() -+ NOT_IMPLEMENTED_call() -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Ruff Output @@ -259,7 +259,7 @@ for (x,) in (1,), (2,), (3,): [1, 2, 3] division_result_tuple = (6 / 2,) -NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) if True: IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = ( @@ -268,9 +268,9 @@ if True: ) if True: - NOT_IMPLEMENTED_call() - NOT_IMPLEMENTED_call() - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap index 42acde710a..1a5581f602 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap @@ -243,19 +243,19 @@ instruction()#comment with bad spacing Cheese, - Cheese("Wensleydale"), - SubBytes(b"spam"), -+ NOT_IMPLEMENTED_call(), -+ NOT_IMPLEMENTED_call(), ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), ] -if "PYTHON" in os.environ: - add_compiler(compiler_from_env()) +if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) else: # for compiler in compilers.values(): # add_compiler(compiler) - add_compiler(compilers[(7.0, 32)]) -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # add_compiler(compilers[(7.1, 64)]) @@ -307,7 +307,7 @@ instruction()#comment with bad spacing -""", - arg3=True, - ) -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ############################################################################ @@ -342,7 +342,7 @@ instruction()#comment with bad spacing - # right - if element is not None - ] -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + lcomp = [i for i in []] + lcomp2 = [i for i in []] + lcomp3 = [i for i in []] @@ -357,7 +357,7 @@ instruction()#comment with bad spacing - syms.simple_stmt, - [Node(statement, result), Leaf(token.NEWLINE, "\n")], # FIXME: \r\n? - ) -+ return NOT_IMPLEMENTED_call() ++ return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -CONFIG_FILES = ( @@ -434,16 +434,16 @@ not_shareables = [ "NOT_YET_IMPLEMENTED_STRING", # user-defined types and objects Cheese, - NOT_IMPLEMENTED_call(), - NOT_IMPLEMENTED_call(), + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), ] if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) else: # for compiler in compilers.values(): # add_compiler(compiler) - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # add_compiler(compilers[(7.1, 64)]) @@ -489,11 +489,11 @@ def inline_comments_in_brackets_ruin_everything(): ] # no newline after - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ############################################################################ - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) lcomp = [i for i in []] lcomp2 = [i for i in []] lcomp3 = [i for i in []] @@ -505,7 +505,7 @@ def inline_comments_in_brackets_ruin_everything(): # and round and round we go # let's return - return NOT_IMPLEMENTED_call() + return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) CONFIG_FILES = [CONFIG_FILE] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap index 00e41d0c6a..aa92645b7a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap @@ -80,7 +80,7 @@ def func(): + lcomp3 = [i for i in []] # Capture each of the exceptions in the MultiError along with each of their causes and contexts - if isinstance(exc_value, MultiError): -+ if NOT_IMPLEMENTED_call(): ++ if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): embedded = [] for exc in exc_value.exceptions: - if exc not in _seen: @@ -98,7 +98,7 @@ def func(): - # This should be left alone (after) - ) + if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # everything is fine if the expression isn't nested - traceback.TracebackException.from_exception( @@ -110,7 +110,7 @@ def func(): - # shared between sub-exceptions are not omitted - _seen=set(_seen), - ) -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # %% @@ -127,14 +127,14 @@ def func(): x = "NOT_YET_IMPLEMENTED_STRING" lcomp3 = [i for i in []] # Capture each of the exceptions in the MultiError along with each of their causes and contexts - if NOT_IMPLEMENTED_call(): + if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): embedded = [] for exc in exc_value.exceptions: if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # everything is fine if the expression isn't nested - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # %% diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap index 9d741d8b92..9df63bd077 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap @@ -195,7 +195,7 @@ def foo3(list_a, list_b): - ) - .filter(User.xyz.is_(None)) - ) -+ return NOT_IMPLEMENTED_call() ++ return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def foo3(list_a, list_b): @@ -206,7 +206,7 @@ def foo3(list_a, list_b): - db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ) ``` @@ -227,13 +227,13 @@ def foo(list_a, list_b): def foo2(list_a, list_b): # Standalone comment reasonably placed. - return NOT_IMPLEMENTED_call() + return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def foo3(list_a, list_b): return ( # Standalone comment but weirdly placed. - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ) ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap index ef6cfc7bec..6273ad1e97 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap @@ -99,7 +99,7 @@ if __name__ == "__main__": # This one is properly standalone now. -for i in range(100): -+for i in NOT_IMPLEMENTED_call(): ++for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # first we do this - if i % 33 == 0: + if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: @@ -107,7 +107,7 @@ if __name__ == "__main__": # then we do this - print(i) -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # and finally we loop around -with open(some_temp_file) as f: @@ -132,7 +132,7 @@ if __name__ == "__main__": @deco1 # leading 2 -@deco2(with_args=True) -+@NOT_IMPLEMENTED_call() ++@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # leading 3 @deco3 def decorated1(): @@ -141,7 +141,7 @@ if __name__ == "__main__": @deco1 # leading 2 -@deco2(with_args=True) -+@NOT_IMPLEMENTED_call() ++@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # leading function comment def decorated1(): ... @@ -168,13 +168,13 @@ while True: # This one is properly standalone now. -for i in NOT_IMPLEMENTED_call(): +for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # first we do this if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: break # then we do this - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # and finally we loop around NOT_YET_IMPLEMENTED_StmtWith @@ -196,7 +196,7 @@ def wat(): # leading 1 @deco1 # leading 2 -@NOT_IMPLEMENTED_call() +@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # leading 3 @deco3 def decorated1(): @@ -206,7 +206,7 @@ def decorated1(): # leading 1 @deco1 # leading 2 -@NOT_IMPLEMENTED_call() +@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # leading function comment def decorated1(): ... diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap index 10302ecac7..abc2ecaafd 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap @@ -169,12 +169,12 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite - 0.0789, - a[-1], # type: ignore - ) -+ c = NOT_IMPLEMENTED_call() ++ c = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - c = call( - "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa" # type: ignore - ) -+ c = NOT_IMPLEMENTED_call() ++ c = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -result = ( # aaa @@ -194,10 +194,10 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite - foo, - [AAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAA, BBBBBBBBBBBB], # type: ignore -) -+NOT_IMPLEMENTED_call() ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type] -+aaaaaaaaaaaaa, bbbbbbbbb = NOT_IMPLEMENTED_call() # type: ignore[arg-type] ++aaaaaaaaaaaaa, bbbbbbbbb = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # type: ignore[arg-type] ``` ## Ruff Output @@ -292,9 +292,9 @@ def f( def func( a=some_list[0], # type: int ): # type: () -> int - c = NOT_IMPLEMENTED_call() + c = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - c = NOT_IMPLEMENTED_call() + c = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) result = "NOT_YET_IMPLEMENTED_STRING" # aaa @@ -306,9 +306,9 @@ AAAAAAAAAAAAA = ( + AAAAAAAAAAAAA ) # type: ignore -NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -aaaaaaaaaaaaa, bbbbbbbbb = NOT_IMPLEMENTED_call() # type: ignore[arg-type] +aaaaaaaaaaaaa, bbbbbbbbb = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # type: ignore[arg-type] ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap index cddd66b042..8168093768 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap @@ -188,7 +188,7 @@ def bar(): # leading 2 # leading 2 extra -@deco2(with_args=True) -+@NOT_IMPLEMENTED_call() ++@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # leading 3 @deco3 # leading 4 @@ -197,7 +197,7 @@ def bar(): @deco1 # leading 2 -@deco2(with_args=True) -+@NOT_IMPLEMENTED_call() ++@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # leading 3 that already has an empty line @deco3 @@ -206,7 +206,7 @@ def bar(): @deco1 # leading 2 -@deco2(with_args=True) -+@NOT_IMPLEMENTED_call() ++@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # leading 3 @deco3 @@ -300,7 +300,7 @@ some = statement @deco1 # leading 2 # leading 2 extra -@NOT_IMPLEMENTED_call() +@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # leading 3 @deco3 # leading 4 @@ -314,7 +314,7 @@ some = statement # leading 1 @deco1 # leading 2 -@NOT_IMPLEMENTED_call() +@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # leading 3 that already has an empty line @deco3 @@ -329,7 +329,7 @@ some = statement # leading 1 @deco1 # leading 2 -@NOT_IMPLEMENTED_call() +@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # leading 3 @deco3 diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap index daf0c71385..976fcc5d8f 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap @@ -47,7 +47,7 @@ def function(a:int=42): result = 1 #  type: ignore result = 1 # This comment is talking about type: ignore -square = Square(4) #  type: Optional[Square] -+square = NOT_IMPLEMENTED_call() #  type: Optional[Square] ++square = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) #  type: Optional[Square] def function(a: int = 42): @@ -71,7 +71,7 @@ result = (1,) # Another one result = 1 #  type: ignore result = 1 # This comment is talking about type: ignore -square = NOT_IMPLEMENTED_call() #  type: Optional[Square] +square = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) #  type: Optional[Square] def function(a: int = 42): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap index e9ed496189..8935f71b8a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap @@ -166,9 +166,9 @@ async def wat(): # Explains why we use global state. -GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} +GLOBAL_STATE = { -+ "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(), -+ "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(), -+ "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(), ++ "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), ++ "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), ++ "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), +} @@ -201,7 +201,7 @@ async def wat(): -@fast(really=True) -+@NOT_IMPLEMENTED_call() ++@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) async def wat(): # This comment, for some reason \ # contains a trailing backslash. @@ -211,7 +211,7 @@ async def wat(): # Comment after ending a block. if result: - print("A OK", file=sys.stdout) -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Comment between things. - print() + NOT_IMPLEMENTED_call() @@ -269,9 +269,9 @@ def function(default=None): # Explains why we use global state. GLOBAL_STATE = { - "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(), - "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(), - "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(), + "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), + "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), + "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), } @@ -285,14 +285,14 @@ NOT_YET_IMPLEMENTED_StmtClassDef #'

This is pweave!

-@NOT_IMPLEMENTED_call() +@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) async def wat(): # This comment, for some reason \ # contains a trailing backslash. NOT_YET_IMPLEMENTED_StmtAsyncWith # Some more comments # Comment after ending a block. if result: - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Comment between things. NOT_IMPLEMENTED_call() diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap index e23d22d0e3..a9b2674d3a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap @@ -137,7 +137,7 @@ def g(): if not prev: - prevp = preceding_leaf(p) - if not prevp or prevp.type in OPENING_BRACKETS: -+ prevp = NOT_IMPLEMENTED_call() ++ prevp = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + if not prevp or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: return NO @@ -199,7 +199,7 @@ def g(): prev = leaf.prev_sibling if not prev: - prevp = preceding_leaf(p) -+ prevp = NOT_IMPLEMENTED_call() ++ prevp = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - if not prevp or prevp.type in OPENING_BRACKETS: + if not prevp or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: @@ -245,7 +245,7 @@ def f(): prev = leaf.prev_sibling if not prev: - prevp = NOT_IMPLEMENTED_call() + prevp = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) if not prevp or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: return NO @@ -283,7 +283,7 @@ def g(): prev = leaf.prev_sibling if not prev: - prevp = NOT_IMPLEMENTED_call() + prevp = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) if not prevp or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: # Start of the line or a bracketed expression. diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap index b2ff9dc82e..b049cca61c 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap @@ -431,18 +431,18 @@ last_call() +NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() # note: no trailing comma pre-3.6 -+NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # note: no trailing comma pre-3.6 ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) lukasz.langa.pl -call.me(maybe) -+NOT_IMPLEMENTED_call() ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) (1).real (1.0).real ....__class__ @@ -454,9 +454,6 @@ last_call() - int, - float, - dict[str, int], --] --very_long_variable_name_filters: t.List[ -- t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], + ( + str, + int, @@ -464,6 +461,9 @@ last_call() + dict[str, int], + ) ] +-very_long_variable_name_filters: t.List[ +- t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], +-] -xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore - sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) -) @@ -494,7 +494,7 @@ last_call() numpy[0, :] numpy[:, i] numpy[0, :2] -@@ -171,62 +158,58 @@ +@@ -171,62 +158,59 @@ numpy[1 : c + 1, c] numpy[-(c + 1) :, d] numpy[:, l[-2]] @@ -541,7 +541,8 @@ last_call() + "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(), -+ "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(), ++ "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call() ++ + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), + "NOT_YET_IMPLEMENTED_STRING": 1, + "NOT_YET_IMPLEMENTED_STRING": 1, **kwargs, @@ -555,16 +556,19 @@ last_call() -g = 1, *"ten" -what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set( - vars_to_remove -+e = NOT_IMPLEMENTED_call() ++e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +f = 1, NOT_YET_IMPLEMENTED_ExprStarred +g = 1, NOT_YET_IMPLEMENTED_ExprStarred +what_is_up_with_those_new_coord_names = ( -+ (coord_names + NOT_IMPLEMENTED_call()) -+ + NOT_IMPLEMENTED_call() ++ (coord_names + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) ++ + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ) -what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( - vars_to_remove --) ++what_is_up_with_those_new_coord_names = ( ++ (coord_names | NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) ++ - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + ) -result = ( - session.query(models.Customer.id) - .filter( @@ -582,10 +586,7 @@ last_call() - models.Customer.id.asc(), - ) - .all() -+what_is_up_with_those_new_coord_names = ( -+ (coord_names | NOT_IMPLEMENTED_call()) -+ - NOT_IMPLEMENTED_call() - ) +-) -Ø = set() -authors.łukasz.say_thanks() +result = NOT_IMPLEMENTED_call() @@ -595,7 +596,7 @@ last_call() mapping = { A: 0.25 * (10.0 / 12), B: 0.1 * (10.0 / 12), -@@ -236,31 +219,29 @@ +@@ -236,31 +220,29 @@ def gen(): @@ -611,7 +612,7 @@ last_call() async def f(): - await some.complicated[0].call(with_args=(True or (1 is not 1))) -+ await NOT_IMPLEMENTED_call() ++ await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -print(*[] or [1]) @@ -622,9 +623,9 @@ last_call() - force=False -), "Short message" -assert parens is TooMany -+NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +NOT_YET_IMPLEMENTED_StmtAssert +NOT_YET_IMPLEMENTED_StmtAssert +NOT_YET_IMPLEMENTED_StmtAssert @@ -640,12 +641,12 @@ last_call() ... for j in 1 + (2 + 3): ... -@@ -272,28 +253,16 @@ +@@ -272,28 +254,16 @@ addr_proto, addr_canonname, addr_sockaddr, -) in socket.getaddrinfo("google.com", "http"): -+) in NOT_IMPLEMENTED_call(): ++) in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): pass -a = ( - aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp @@ -677,7 +678,7 @@ last_call() ): return True if ( -@@ -327,13 +296,18 @@ +@@ -327,13 +297,18 @@ ): return True if ( @@ -699,7 +700,7 @@ last_call() ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n ): return True -@@ -341,7 +315,8 @@ +@@ -341,7 +316,8 @@ ~aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e @@ -709,7 +710,7 @@ last_call() ^ aaaaaaaaaaaaaaaa.i << aaaaaaaaaaaaaaaa.k >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n -@@ -366,5 +341,5 @@ +@@ -366,5 +342,5 @@ ^ bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ^ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ) @@ -827,17 +828,17 @@ NOT_YET_IMPLEMENTED_ExprSetComp NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() # note: no trailing comma pre-3.6 -NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # note: no trailing comma pre-3.6 +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) lukasz.langa.pl -NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) (1).real (1.0).real ....__class__ @@ -909,7 +910,8 @@ SomeName "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(), - "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(), + "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call() + + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), "NOT_YET_IMPLEMENTED_STRING": 1, "NOT_YET_IMPLEMENTED_STRING": 1, **kwargs, @@ -918,16 +920,16 @@ a = (1,) b = (1,) c = 1 d = (1,) + a + (2,) -e = NOT_IMPLEMENTED_call() +e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) f = 1, NOT_YET_IMPLEMENTED_ExprStarred g = 1, NOT_YET_IMPLEMENTED_ExprStarred what_is_up_with_those_new_coord_names = ( - (coord_names + NOT_IMPLEMENTED_call()) - + NOT_IMPLEMENTED_call() + (coord_names + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) + + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ) what_is_up_with_those_new_coord_names = ( - (coord_names | NOT_IMPLEMENTED_call()) - - NOT_IMPLEMENTED_call() + (coord_names | NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) + - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ) result = NOT_IMPLEMENTED_call() result = NOT_IMPLEMENTED_call() @@ -949,12 +951,12 @@ def gen(): async def f(): - await NOT_IMPLEMENTED_call() + await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) NOT_YET_IMPLEMENTED_StmtAssert NOT_YET_IMPLEMENTED_StmtAssert NOT_YET_IMPLEMENTED_StmtAssert @@ -976,7 +978,7 @@ for ( addr_proto, addr_canonname, addr_sockaddr, -) in NOT_IMPLEMENTED_call(): +) in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): pass a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap index 7dc0b255ac..8790bc64bf 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap @@ -73,7 +73,7 @@ def test_calculate_fades(): - ('stuff', 'in') - ], -]) -+@NOT_IMPLEMENTED_call() ++@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def test_fader(test): pass @@ -122,7 +122,7 @@ TmEx = 2 # Test data: # Position, Volume, State, TmSt/TmEx/None, [call, [arg1...]] -@NOT_IMPLEMENTED_call() +@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def test_fader(test): pass diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff4_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff4_py.snap index 9fd0b0b4b4..9b5d673289 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff4_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff4_py.snap @@ -32,7 +32,7 @@ def f(): pass - 1, 2, - 3, 4, -]) -+@NOT_IMPLEMENTED_call() ++@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # fmt: on def f(): pass @@ -46,7 +46,7 @@ def f(): pass - 4, - ] -) -+@NOT_IMPLEMENTED_call() ++@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def f(): pass ``` @@ -55,13 +55,13 @@ def f(): pass ```py # fmt: off -@NOT_IMPLEMENTED_call() +@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # fmt: on def f(): pass -@NOT_IMPLEMENTED_call() +@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def f(): pass ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap index aada9db6c5..8c6e03ade9 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap @@ -109,7 +109,7 @@ elif unformatted: - ] # Includes an formatted indentation. - }, -) -+NOT_IMPLEMENTED_call() ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Regression test for https://github.com/psf/black/issues/2015. @@ -123,14 +123,14 @@ elif unformatted: - + path, - check=True, -) -+NOT_IMPLEMENTED_call() ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Regression test for https://github.com/psf/black/issues/3026. def test_func(): # yapf: disable - if unformatted( args ): -+ if NOT_IMPLEMENTED_call(): ++ if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): return True # yapf: enable elif b: @@ -142,13 +142,13 @@ elif unformatted: - # fmt: on - print ( "This won't be formatted" ) - print ( "This won't be formatted either" ) -+ for _ in NOT_IMPLEMENTED_call(): ++ for _ in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): + # fmt: on -+ NOT_IMPLEMENTED_call() -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) else: - print("This will be formatted") -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Regression test for https://github.com/psf/black/issues/3184. @@ -200,17 +200,17 @@ elif unformatted: ```py # Regression test for https://github.com/psf/black/issues/3129. -NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Regression test for https://github.com/psf/black/issues/2015. -NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Regression test for https://github.com/psf/black/issues/3026. def test_func(): # yapf: disable - if NOT_IMPLEMENTED_call(): + if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): return True # yapf: enable elif b: @@ -222,12 +222,12 @@ def test_func(): # Regression test for https://github.com/psf/black/issues/2567. if True: # fmt: off - for _ in NOT_IMPLEMENTED_call(): + for _ in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: on - NOT_IMPLEMENTED_call() - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) else: - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Regression test for https://github.com/psf/black/issues/3184. diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap index de6064324a..d5245d0799 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap @@ -241,10 +241,10 @@ d={'a':1, + NOT_YET_IMPLEMENTED_StmtRaise + if False: + ... -+ for i in NOT_IMPLEMENTED_call(): -+ NOT_IMPLEMENTED_call() ++ for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + continue -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + return None + + @@ -255,7 +255,7 @@ d={'a':1, - await asyncio.sleep(1) + "NOT_YET_IMPLEMENTED_STRING" + NOT_YET_IMPLEMENTED_StmtAsyncWith -+ await NOT_IMPLEMENTED_call() ++ await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + + @asyncio.coroutine @@ -265,7 +265,7 @@ d={'a':1, -) -def function_signature_stress_test(number:int,no_annotation=None,text:str='default',* ,debug:bool=False,**kwargs) -> str: - return text[number:-1] -+@NOT_IMPLEMENTED_call() ++@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +def function_signature_stress_test( + number: int, + no_annotation=None, @@ -292,7 +292,7 @@ d={'a':1, + h="NOT_YET_IMPLEMENTED_STRING", + i="NOT_YET_IMPLEMENTED_STRING", +): -+ offset = NOT_IMPLEMENTED_call() ++ offset = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + NOT_YET_IMPLEMENTED_StmtAssert @@ -312,7 +312,7 @@ d={'a':1, -def spaces2(result=_core.Value(None)): -+def spaces2(result=NOT_IMPLEMENTED_call()): ++def spaces2(result=NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)): ... @@ -429,7 +429,7 @@ d={'a':1, - implicit_default=True, - ) - ) -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # fmt: off - a = ( - unnecessary_bracket() @@ -460,7 +460,7 @@ d={'a':1, - re.MULTILINE|re.VERBOSE - # fmt: on - ) -+ _type_comment_re = NOT_IMPLEMENTED_call() ++ _type_comment_re = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def single_literal_yapf_disable(): @@ -496,7 +496,7 @@ d={'a':1, - # fmt: on - xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5, -) -+NOT_IMPLEMENTED_call() ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # fmt: off -yield 'hello' +NOT_YET_IMPLEMENTED_ExprYield @@ -536,21 +536,21 @@ def func_no_args(): NOT_YET_IMPLEMENTED_StmtRaise if False: ... - for i in NOT_IMPLEMENTED_call(): - NOT_IMPLEMENTED_call() + for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) continue - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) return None async def coroutine(arg, exec=False): "NOT_YET_IMPLEMENTED_STRING" NOT_YET_IMPLEMENTED_StmtAsyncWith - await NOT_IMPLEMENTED_call() + await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) @asyncio.coroutine -@NOT_IMPLEMENTED_call() +@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def function_signature_stress_test( number: int, no_annotation=None, @@ -574,7 +574,7 @@ def spaces( h="NOT_YET_IMPLEMENTED_STRING", i="NOT_YET_IMPLEMENTED_STRING", ): - offset = NOT_IMPLEMENTED_call() + offset = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) NOT_YET_IMPLEMENTED_StmtAssert @@ -592,7 +592,7 @@ def spaces_types( ... -def spaces2(result=NOT_IMPLEMENTED_call()): +def spaces2(result=NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)): ... @@ -674,11 +674,11 @@ def on_and_off_broken(): def long_lines(): if True: - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # fmt: off a = NOT_IMPLEMENTED_call() # fmt: on - _type_comment_re = NOT_IMPLEMENTED_call() + _type_comment_re = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def single_literal_yapf_disable(): @@ -686,7 +686,7 @@ def single_literal_yapf_disable(): BAZ = {(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)} # yapf: disable -NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # fmt: off NOT_YET_IMPLEMENTED_ExprYield # No formatting to the end of the file diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap index 7ad743ff86..2247055281 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap @@ -33,10 +33,10 @@ else: + and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ): - print("I'm good!") -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) else: - print("I'm bad") -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Ruff Output @@ -48,9 +48,9 @@ if ( and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right # fmt: skip and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ): - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) else: - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap index eb9f51311f..3c3e9d8cf2 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap @@ -80,7 +80,7 @@ async def test_async_with(): -def some_func( unformatted, args ): # fmt: skip - print("I am some_func") +def some_func(unformatted, args): # fmt: skip -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) return 0 # Make sure this comment is not removed. @@ -90,8 +90,8 @@ async def test_async_with(): - print("I am some_async_func") - await asyncio.sleep(1) +async def some_async_func(unformatted, args): # fmt: skip -+ NOT_IMPLEMENTED_call() -+ await NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Make sure a leading comment is not removed. @@ -109,29 +109,29 @@ async def test_async_with(): # Make sure a leading comment is not removed. -if unformatted_call( args ): # fmt: skip - print("First branch") -+if NOT_IMPLEMENTED_call(): # fmt: skip -+ NOT_IMPLEMENTED_call() ++if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: skip ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Make sure this is not removed. -elif another_unformatted_call( args ): # fmt: skip - print("Second branch") -else : # fmt: skip - print("Last branch") -+elif NOT_IMPLEMENTED_call(): # fmt: skip -+ NOT_IMPLEMENTED_call() ++elif NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: skip ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +else: # fmt: skip -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -while some_condition( unformatted, args ): # fmt: skip - print("Do something") -+while NOT_IMPLEMENTED_call(): # fmt: skip -+ NOT_IMPLEMENTED_call() ++while NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: skip ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -for i in some_iter( unformatted, args ): # fmt: skip - print("Do something") -+for i in NOT_IMPLEMENTED_call(): # fmt: skip -+ NOT_IMPLEMENTED_call() ++for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: skip ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) async def test_async_for(): @@ -165,15 +165,15 @@ async def test_async_with(): ```py # Make sure a leading comment is not removed. def some_func(unformatted, args): # fmt: skip - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) return 0 # Make sure this comment is not removed. # Make sure a leading comment is not removed. async def some_async_func(unformatted, args): # fmt: skip - NOT_IMPLEMENTED_call() - await NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Make sure a leading comment is not removed. @@ -181,21 +181,21 @@ NOT_YET_IMPLEMENTED_StmtClassDef # Make sure a leading comment is not removed. -if NOT_IMPLEMENTED_call(): # fmt: skip - NOT_IMPLEMENTED_call() +if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: skip + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Make sure this is not removed. -elif NOT_IMPLEMENTED_call(): # fmt: skip - NOT_IMPLEMENTED_call() +elif NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: skip + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) else: # fmt: skip - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -while NOT_IMPLEMENTED_call(): # fmt: skip - NOT_IMPLEMENTED_call() +while NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: skip + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -for i in NOT_IMPLEMENTED_call(): # fmt: skip - NOT_IMPLEMENTED_call() +for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: skip + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) async def test_async_for(): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap index 44b9556da9..a3f8df4f44 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap @@ -82,8 +82,8 @@ with hmm_but_this_should_get_two_preceding_newlines(): - **kwargs, - ) + NOT_YET_IMPLEMENTED_StmtWith -+ NOT_IMPLEMENTED_call() # negate top -+ return NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # negate top ++ return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def g(): @@ -94,7 +94,7 @@ with hmm_but_this_should_get_two_preceding_newlines(): pass - print("Inner defs should breathe a little.") -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def h(): @@ -102,7 +102,7 @@ with hmm_but_this_should_get_two_preceding_newlines(): pass - print("Inner defs should breathe a little.") -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -if os.name == "posix": @@ -153,8 +153,8 @@ def f( **kwargs, ) -> A: NOT_YET_IMPLEMENTED_StmtWith - NOT_IMPLEMENTED_call() # negate top - return NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # negate top + return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def g(): @@ -163,14 +163,14 @@ def g(): def inner(): pass - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def h(): def inner(): pass - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap index 9379157af1..f51aeec357 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap @@ -136,11 +136,11 @@ def __await__(): return (yield) ... - for i in range(10): - print(i) -+ for i in NOT_IMPLEMENTED_call(): -+ NOT_IMPLEMENTED_call() ++ for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) continue - exec("new-style exec", {}, {}) -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) return None @@ -151,12 +151,12 @@ def __await__(): return (yield) - await asyncio.sleep(1) + "NOT_YET_IMPLEMENTED_STRING" + NOT_YET_IMPLEMENTED_StmtAsyncWith -+ await NOT_IMPLEMENTED_call() ++ await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) @asyncio.coroutine -@some_decorator(with_args=True, many_args=[1, 2, 3]) -+@NOT_IMPLEMENTED_call() ++@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def function_signature_stress_test( number: int, no_annotation=None, @@ -184,7 +184,7 @@ def __await__(): return (yield) + h="NOT_YET_IMPLEMENTED_STRING", + i="NOT_YET_IMPLEMENTED_STRING", +): -+ offset = NOT_IMPLEMENTED_call() ++ offset = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + NOT_YET_IMPLEMENTED_StmtAssert @@ -205,7 +205,7 @@ def __await__(): return (yield) -def spaces2(result=_core.Value(None)): - assert fut is self._read_fut, (fut, self._read_fut) -+def spaces2(result=NOT_IMPLEMENTED_call()): ++def spaces2(result=NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)): + NOT_YET_IMPLEMENTED_StmtAssert @@ -263,9 +263,9 @@ def __await__(): return (yield) - """, - re.MULTILINE | re.VERBOSE, - ) -+ NOT_IMPLEMENTED_call() -+ NOT_IMPLEMENTED_call() -+ _type_comment_re = NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ _type_comment_re = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def trailing_comma(): @@ -309,21 +309,21 @@ def func_no_args(): NOT_YET_IMPLEMENTED_StmtRaise if False: ... - for i in NOT_IMPLEMENTED_call(): - NOT_IMPLEMENTED_call() + for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) continue - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) return None async def coroutine(arg, exec=False): "NOT_YET_IMPLEMENTED_STRING" NOT_YET_IMPLEMENTED_StmtAsyncWith - await NOT_IMPLEMENTED_call() + await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) @asyncio.coroutine -@NOT_IMPLEMENTED_call() +@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def function_signature_stress_test( number: int, no_annotation=None, @@ -346,7 +346,7 @@ def spaces( h="NOT_YET_IMPLEMENTED_STRING", i="NOT_YET_IMPLEMENTED_STRING", ): - offset = NOT_IMPLEMENTED_call() + offset = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) NOT_YET_IMPLEMENTED_StmtAssert @@ -364,7 +364,7 @@ def spaces_types( ... -def spaces2(result=NOT_IMPLEMENTED_call()): +def spaces2(result=NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)): NOT_YET_IMPLEMENTED_StmtAssert @@ -374,9 +374,9 @@ def example(session): def long_lines(): if True: - NOT_IMPLEMENTED_call() - NOT_IMPLEMENTED_call() - _type_comment_re = NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + _type_comment_re = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def trailing_comma(): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap index f0fb15c471..965322236b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap @@ -106,8 +106,8 @@ some_module.some_function( - call2( - arg=[1, 2, 3], - ) -+ NOT_IMPLEMENTED_call() -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) x = { - "a": 1, - "b": 2, @@ -161,7 +161,7 @@ some_module.some_function( - this_shouldn_t_get_a_trailing_comma_too - ) -): -+def func() -> NOT_IMPLEMENTED_call(): ++def func() -> NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): pass @@ -170,7 +170,7 @@ some_module.some_function( - this_shouldn_t_get_a_trailing_comma_too - ) -): -+def func() -> NOT_IMPLEMENTED_call(): ++def func() -> NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): pass @@ -178,7 +178,7 @@ some_module.some_function( -some_module.some_function( - argument1, (one_element_tuple,), argument4, argument5, argument6 -) -+NOT_IMPLEMENTED_call() ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Inner trailing comma causes outer to explode -some_module.some_function( @@ -191,7 +191,7 @@ some_module.some_function( - argument5, - argument6, -) -+NOT_IMPLEMENTED_call() ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Ruff Output @@ -223,8 +223,8 @@ def f2( def f( a: int = 1, ): - NOT_IMPLEMENTED_call() - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) x = { "NOT_YET_IMPLEMENTED_STRING": 1, "NOT_YET_IMPLEMENTED_STRING": 2, @@ -257,19 +257,19 @@ def some_method_with_a_really_long_name( pass -def func() -> NOT_IMPLEMENTED_call(): +def func() -> NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): pass -def func() -> NOT_IMPLEMENTED_call(): +def func() -> NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): pass # Make sure inner one-element tuple won't explode -NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Inner trailing comma causes outer to explode -NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap index bf18bb0df8..70995c951c 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap @@ -87,7 +87,7 @@ return np.divide( -e = lazy(lambda **kwargs: 5) -f = f() ** 5 +d = 5 ** f["NOT_YET_IMPLEMENTED_STRING"] -+e = NOT_IMPLEMENTED_call() ++e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +f = NOT_IMPLEMENTED_call() ** 5 g = a.b**c.d -h = 5 ** funcs.f() @@ -106,7 +106,7 @@ return np.divide( -p = {(k, k**2): v**2 for k, v in pairs} -q = [10**i for i in range(6)] +n = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+o = NOT_IMPLEMENTED_call() ++o = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} +q = [i for i in []] r = x**y @@ -119,7 +119,7 @@ return np.divide( -e = lazy(lambda **kwargs: 5) -f = f() ** 5.0 +d = 5.0 ** f["NOT_YET_IMPLEMENTED_STRING"] -+e = NOT_IMPLEMENTED_call() ++e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +f = NOT_IMPLEMENTED_call() ** 5.0 g = a.b**c.d -h = 5.0 ** funcs.f() @@ -138,7 +138,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)] +n = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+o = NOT_IMPLEMENTED_call() ++o = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} +q = [i for i in []] @@ -151,13 +151,13 @@ return np.divide( - out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr] - where=view.sum_of_weights**2 > view.sum_of_weights_squared, # type: ignore[union-attr] - ) -+if NOT_IMPLEMENTED_call(): -+ return NOT_IMPLEMENTED_call() ++if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): ++ return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -return np.divide( - where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared, # type: ignore -) -+return NOT_IMPLEMENTED_call() ++return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Ruff Output @@ -180,7 +180,7 @@ a = 5**~4 b = 5 ** NOT_IMPLEMENTED_call() c = -(5**2) d = 5 ** f["NOT_YET_IMPLEMENTED_STRING"] -e = NOT_IMPLEMENTED_call() +e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) f = NOT_IMPLEMENTED_call() ** 5 g = a.b**c.d h = 5 ** NOT_IMPLEMENTED_call() @@ -190,7 +190,7 @@ k = [i for i in []] l = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right m = [([2**63], [1, 2**63])] n = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -o = NOT_IMPLEMENTED_call() +o = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} q = [i for i in []] r = x**y @@ -199,7 +199,7 @@ a = 5.0**~4.0 b = 5.0 ** NOT_IMPLEMENTED_call() c = -(5.0**2.0) d = 5.0 ** f["NOT_YET_IMPLEMENTED_STRING"] -e = NOT_IMPLEMENTED_call() +e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) f = NOT_IMPLEMENTED_call() ** 5.0 g = a.b**c.d h = 5.0 ** NOT_IMPLEMENTED_call() @@ -209,16 +209,16 @@ k = [i for i in []] l = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right m = [([2.0**63.0], [1.0, 2**63.0])] n = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -o = NOT_IMPLEMENTED_call() +o = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} q = [i for i in []] # WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873) -if NOT_IMPLEMENTED_call(): - return NOT_IMPLEMENTED_call() +if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): + return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -return NOT_IMPLEMENTED_call() +return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__prefer_rhs_split_reformatted_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__prefer_rhs_split_reformatted_py.snap index 89478d9b1f..db5e55f09b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__prefer_rhs_split_reformatted_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__prefer_rhs_split_reformatted_py.snap @@ -25,7 +25,7 @@ xxxxxxxxx_yyy_zzzzzzzz[xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxx ```diff --- Black +++ Ruff -@@ -9,13 +9,8 @@ +@@ -9,13 +9,10 @@ m2, ), third_value, @@ -33,14 +33,14 @@ xxxxxxxxx_yyy_zzzzzzzz[xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxx - arg1, - arg2, -) -+) = NOT_IMPLEMENTED_call() ++) = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Make when when the left side of assignment plus the opening paren "... = (" is # exactly line length limit + 1, it won't be split like that. --xxxxxxxxx_yyy_zzzzzzzz[ + xxxxxxxxx_yyy_zzzzzzzz[ - xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1) --] = 1 -+xxxxxxxxx_yyy_zzzzzzzz[NOT_IMPLEMENTED_call(), NOT_IMPLEMENTED_call()] = 1 ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + ] = 1 ``` ## Ruff Output @@ -57,11 +57,13 @@ xxxxxxxxx_yyy_zzzzzzzz[xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxx m2, ), third_value, -) = NOT_IMPLEMENTED_call() +) = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Make when when the left side of assignment plus the opening paren "... = (" is # exactly line length limit + 1, it won't be split like that. -xxxxxxxxx_yyy_zzzzzzzz[NOT_IMPLEMENTED_call(), NOT_IMPLEMENTED_call()] = 1 +xxxxxxxxx_yyy_zzzzzzzz[ + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +] = 1 ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap index 7923c03bb5..b13ce8545b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap @@ -102,23 +102,23 @@ async def main(): # Control example async def main(): - await asyncio.sleep(1) -+ await NOT_IMPLEMENTED_call() ++ await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Remove brackets for short coroutine/task async def main(): - await asyncio.sleep(1) -+ await (NOT_IMPLEMENTED_call()) ++ await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) async def main(): - await asyncio.sleep(1) -+ await (NOT_IMPLEMENTED_call()) ++ await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) async def main(): - await asyncio.sleep(1) -+ await (NOT_IMPLEMENTED_call()) ++ await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) # Check comments @@ -126,7 +126,7 @@ async def main(): - await asyncio.sleep(1) # Hello + ( + await # Hello -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + ) @@ -134,14 +134,14 @@ async def main(): - await asyncio.sleep(1) # Hello + ( + await ( -+ NOT_IMPLEMENTED_call() # Hello ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Hello + ) + ) async def main(): - await asyncio.sleep(1) # Hello -+ await (NOT_IMPLEMENTED_call()) # Hello ++ await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) # Hello # Long lines @@ -155,7 +155,7 @@ async def main(): - asyncio.sleep(1), - asyncio.sleep(1), - ) -+ await NOT_IMPLEMENTED_call() ++ await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Same as above but with magic trailing comma in function @@ -169,13 +169,13 @@ async def main(): - asyncio.sleep(1), - asyncio.sleep(1), - ) -+ await NOT_IMPLEMENTED_call() ++ await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Cr@zY Br@ck3Tz async def main(): - await black(1) -+ await (NOT_IMPLEMENTED_call()) ++ await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) # Keep brackets around non power operations and nested awaits @@ -184,7 +184,7 @@ async def main(): async def main(): - await (await asyncio.sleep(1)) -+ await (await NOT_IMPLEMENTED_call()) ++ await (await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) # It's awaits all the way down... @@ -198,12 +198,12 @@ async def main(): async def main(): - await (await asyncio.sleep(1)) -+ await (await (NOT_IMPLEMENTED_call())) ++ await (await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg))) async def main(): - await (await (await (await (await asyncio.sleep(1))))) -+ await (await (await (await (await (NOT_IMPLEMENTED_call()))))) ++ await (await (await (await (await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)))))) async def main(): @@ -219,55 +219,55 @@ NOT_YET_IMPLEMENTED_StmtImport # Control example async def main(): - await NOT_IMPLEMENTED_call() + await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Remove brackets for short coroutine/task async def main(): - await (NOT_IMPLEMENTED_call()) + await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) async def main(): - await (NOT_IMPLEMENTED_call()) + await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) async def main(): - await (NOT_IMPLEMENTED_call()) + await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) # Check comments async def main(): ( await # Hello - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ) async def main(): ( await ( - NOT_IMPLEMENTED_call() # Hello + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Hello ) ) async def main(): - await (NOT_IMPLEMENTED_call()) # Hello + await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) # Hello # Long lines async def main(): - await NOT_IMPLEMENTED_call() + await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Same as above but with magic trailing comma in function async def main(): - await NOT_IMPLEMENTED_call() + await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Cr@zY Br@ck3Tz async def main(): - await (NOT_IMPLEMENTED_call()) + await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) # Keep brackets around non power operations and nested awaits @@ -276,7 +276,7 @@ async def main(): async def main(): - await (await NOT_IMPLEMENTED_call()) + await (await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) # It's awaits all the way down... @@ -289,11 +289,11 @@ async def main(): async def main(): - await (await (NOT_IMPLEMENTED_call())) + await (await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg))) async def main(): - await (await (await (await (await (NOT_IMPLEMENTED_call()))))) + await (await (await (await (await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)))))) async def main(): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap index b745fd054e..ab7e8ada6d 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap @@ -37,13 +37,13 @@ for (((((k, v))))) in d.items(): -for k, v in d.items(): - print(k, v) +for k, v in NOT_IMPLEMENTED_call(): -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Don't touch tuple brackets after `in` for module in (core, _unicodefun): - if hasattr(module, "_verify_python3_env"): - module._verify_python3_env = lambda: None -+ if NOT_IMPLEMENTED_call(): ++ if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): + module._verify_python3_env = lambda x: True # Brackets remain for long for loop lines @@ -53,7 +53,7 @@ for (((((k, v))))) in d.items(): -) in d.items(): - print(k, v) +) in NOT_IMPLEMENTED_call(): -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -for ( - k, @@ -63,13 +63,13 @@ for (((((k, v))))) in d.items(): -): - print(k, v) +for k, v in NOT_IMPLEMENTED_call(): -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Test deeply nested brackets -for k, v in d.items(): - print(k, v) +for k, v in NOT_IMPLEMENTED_call(): -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Ruff Output @@ -77,11 +77,11 @@ for (((((k, v))))) in d.items(): ```py # Only remove tuple brackets after `for` for k, v in NOT_IMPLEMENTED_call(): - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Don't touch tuple brackets after `in` for module in (core, _unicodefun): - if NOT_IMPLEMENTED_call(): + if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): module._verify_python3_env = lambda x: True # Brackets remain for long for loop lines @@ -89,14 +89,14 @@ for ( why_would_anyone_choose_to_name_a_loop_variable_with_a_name_this_long, i_dont_know_but_we_should_still_check_the_behaviour_if_they_do, ) in NOT_IMPLEMENTED_call(): - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) for k, v in NOT_IMPLEMENTED_call(): - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Test deeply nested brackets for k, v in NOT_IMPLEMENTED_call(): - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap index 642e62ce92..a3a57b200a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap @@ -128,27 +128,27 @@ with open("/path/to/file.txt", mode="r") as read_file: def foo1(): - print("The newline above me should be deleted!") -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def foo2(): - print("All the newlines above me should be deleted!") -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def foo3(): - print("No newline above me!") -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - print("There is a newline above me, and that's OK!") -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def foo4(): # There is a comment here - print("The newline above me should not be deleted!") -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -class Foo: @@ -159,34 +159,34 @@ with open("/path/to/file.txt", mode="r") as read_file: -for i in range(5): - print(f"{i}) The line above me should be removed!") -+for i in NOT_IMPLEMENTED_call(): -+ NOT_IMPLEMENTED_call() ++for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -for i in range(5): - print(f"{i}) The lines above me should be removed!") -+for i in NOT_IMPLEMENTED_call(): -+ NOT_IMPLEMENTED_call() ++for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -for i in range(5): - for j in range(7): - print(f"{i}) The lines above me should be removed!") -+for i in NOT_IMPLEMENTED_call(): -+ for j in NOT_IMPLEMENTED_call(): -+ NOT_IMPLEMENTED_call() ++for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): ++ for j in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -if random.randint(0, 3) == 0: - print("The new line above me is about to be removed!") +if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -if random.randint(0, 3) == 0: - print("The new lines above me is about to be removed!") +if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -if random.randint(0, 3) == 0: @@ -194,23 +194,23 @@ with open("/path/to/file.txt", mode="r") as read_file: - print("Two lines above me are about to be removed!") +if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) while True: - print("The newline above me should be deleted!") -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) while True: - print("The newlines above me should be deleted!") -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) while True: while False: - print("The newlines above me should be deleted!") -+ NOT_IMPLEMENTED_call() ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -with open("/path/to/file.txt", mode="w") as file: @@ -236,65 +236,65 @@ NOT_YET_IMPLEMENTED_StmtImport def foo1(): - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def foo2(): - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def foo3(): - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def foo4(): # There is a comment here - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) NOT_YET_IMPLEMENTED_StmtClassDef -for i in NOT_IMPLEMENTED_call(): - NOT_IMPLEMENTED_call() +for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -for i in NOT_IMPLEMENTED_call(): - NOT_IMPLEMENTED_call() +for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -for i in NOT_IMPLEMENTED_call(): - for j in NOT_IMPLEMENTED_call(): - NOT_IMPLEMENTED_call() +for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): + for j in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) while True: - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) while True: - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) while True: while False: - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) NOT_YET_IMPLEMENTED_StmtWith diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap index 5a86ffbc95..227fddc38c 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap @@ -91,10 +91,10 @@ func( # Trailing commas in multiple chained non-nested parens. -zero(one).two(three).four(five) -+NOT_IMPLEMENTED_call() ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -func1(arg1).func2(arg2).func3(arg3).func4(arg4).func5(arg5) -+NOT_IMPLEMENTED_call() ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -(a, b, c, d) = func1(arg1) and func2(arg2) +( @@ -102,10 +102,10 @@ func( + b, + c, + d, -+) = NOT_IMPLEMENTED_call() and NOT_IMPLEMENTED_call() ++) = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) and NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -func(argument1, (one, two), argument4, argument5, argument6) -+NOT_IMPLEMENTED_call() ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Ruff Output @@ -134,18 +134,18 @@ set_of_types = {tuple[(int,)]} small_tuple = (1,) # Trailing commas in multiple chained non-nested parens. -NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ( a, b, c, d, -) = NOT_IMPLEMENTED_call() and NOT_IMPLEMENTED_call() +) = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) and NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap index f13a98ef72..ae15220407 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap @@ -76,7 +76,7 @@ x[ ```diff --- Black +++ Ruff -@@ -4,30 +4,30 @@ +@@ -4,30 +4,35 @@ slice[d::d] slice[0] slice[-1] @@ -113,11 +113,16 @@ x[ ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] -ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] -+ham[ : NOT_IMPLEMENTED_call() : NOT_IMPLEMENTED_call()], ham[ :: NOT_IMPLEMENTED_call()] ++( ++ ham[ ++ : NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) : NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ ], ++ ham[ :: NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)], ++) ham[lower + offset : upper + offset] slice[::, ::] -@@ -50,10 +50,14 @@ +@@ -50,10 +55,14 @@ slice[ # A 1 @@ -169,7 +174,12 @@ async def f(): ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:] ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] -ham[ : NOT_IMPLEMENTED_call() : NOT_IMPLEMENTED_call()], ham[ :: NOT_IMPLEMENTED_call()] +( + ham[ + : NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) : NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + ], + ham[ :: NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)], +) ham[lower + offset : upper + offset] slice[::, ::] diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap index 9b6fe74f54..6fb26dec18 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap @@ -38,12 +38,15 @@ class A: ```diff --- Black +++ Ruff -@@ -1,34 +1,12 @@ +@@ -1,34 +1,15 @@ -if e1234123412341234.winerror not in ( - _winapi.ERROR_SEM_TIMEOUT, - _winapi.ERROR_PIPE_BUSY, -) or _check_timeout(t): -+if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right or NOT_IMPLEMENTED_call(): ++if ( ++ NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++ or NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++): pass if x: @@ -55,7 +58,7 @@ class A: - ) - + 1 - ) -+ new_id = NOT_IMPLEMENTED_call() + 1 ++ new_id = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + 1 -class X: @@ -82,12 +85,15 @@ class A: ## Ruff Output ```py -if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right or NOT_IMPLEMENTED_call(): +if ( + NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + or NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +): pass if x: if y: - new_id = NOT_IMPLEMENTED_call() + 1 + new_id = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + 1 NOT_YET_IMPLEMENTED_StmtClassDef diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens3_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens3_py.snap index ea49d311ae..4108c2434f 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens3_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens3_py.snap @@ -30,7 +30,7 @@ if True: - + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", - "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", - ) % {"reported_username": reported_username, "report_reason": report_reason} -+ return NOT_IMPLEMENTED_call() % { ++ return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) % { + "NOT_YET_IMPLEMENTED_STRING": reported_username, + "NOT_YET_IMPLEMENTED_STRING": report_reason, + } @@ -42,7 +42,7 @@ if True: if True: if True: if True: - return NOT_IMPLEMENTED_call() % { + return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) % { "NOT_YET_IMPLEMENTED_STRING": reported_username, "NOT_YET_IMPLEMENTED_STRING": report_reason, } diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap index e2b670003f..3ef4708db1 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap @@ -54,18 +54,18 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( -).four( - five, -) -+NOT_IMPLEMENTED_call() ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -func1(arg1).func2( - arg2, -).func3(arg3).func4( - arg4, -).func5(arg5) -+NOT_IMPLEMENTED_call() ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Inner one-element tuple shouldn't explode -func1(arg1).func2(arg1, (one_tuple,)).func3(arg3) -+NOT_IMPLEMENTED_call() ++NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ( a, @@ -75,7 +75,7 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( -) = func1( - arg1 -) and func2(arg2) -+) = NOT_IMPLEMENTED_call() and NOT_IMPLEMENTED_call() ++) = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) and NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Example from https://github.com/psf/black/issues/3229 @@ -86,7 +86,7 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( - }, - api_key=api_key, - )["extensions"]["sdk"]["token"] -+ return NOT_IMPLEMENTED_call()["NOT_YET_IMPLEMENTED_STRING"][ ++ return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)["NOT_YET_IMPLEMENTED_STRING"][ + "NOT_YET_IMPLEMENTED_STRING" + ][ + "NOT_YET_IMPLEMENTED_STRING" @@ -113,24 +113,24 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( ## Ruff Output ```py -NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Inner one-element tuple shouldn't explode -NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ( a, b, c, d, -) = NOT_IMPLEMENTED_call() and NOT_IMPLEMENTED_call() +) = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) and NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Example from https://github.com/psf/black/issues/3229 def refresh_token(self, device_family, refresh_token, api_key): - return NOT_IMPLEMENTED_call()["NOT_YET_IMPLEMENTED_STRING"][ + return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)["NOT_YET_IMPLEMENTED_STRING"][ "NOT_YET_IMPLEMENTED_STRING" ][ "NOT_YET_IMPLEMENTED_STRING" diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap index e335debb8b..d67608accf 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap @@ -29,7 +29,7 @@ this_will_be_wrapped_in_parens, = struct.unpack(b"12345678901234567890") # This is as well. -(this_will_be_wrapped_in_parens,) = struct.unpack(b"12345678901234567890") -+(this_will_be_wrapped_in_parens,) = NOT_IMPLEMENTED_call() ++(this_will_be_wrapped_in_parens,) = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -(a,) = call() +(a,) = NOT_IMPLEMENTED_call() @@ -47,7 +47,7 @@ this_will_be_wrapped_in_parens, = struct.unpack(b"12345678901234567890") ) = 1, 2, 3 # This is as well. -(this_will_be_wrapped_in_parens,) = NOT_IMPLEMENTED_call() +(this_will_be_wrapped_in_parens,) = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) (a,) = NOT_IMPLEMENTED_call() ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap index a610419d8b..82106ab6d3 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap @@ -214,7 +214,7 @@ if ( # Black breaks the right side first for the following expressions: -aaaaaaaaaaaaaa + NOT_IMPLEMENTED_call() +aaaaaaaaaaaaaa + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) aaaaaaaaaaaaaa + [ bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__while_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__while_py.snap index 32b93d696d..785c69cb8e 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__while_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__while_py.snap @@ -61,16 +61,16 @@ while ( else: ... -while NOT_IMPLEMENTED_call() and anotherCondition or aThirdCondition: # comment - NOT_IMPLEMENTED_call() +while NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) and anotherCondition or aThirdCondition: # comment + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) while ( - NOT_IMPLEMENTED_call() # trailing some condition + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # trailing some condition and anotherCondition or aThirdCondition # trailing third condition ): # comment - NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` From f7e1cf4b51168f23d9366cf796d86fa582a314e3 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 22 Jun 2023 11:09:43 +0200 Subject: [PATCH 184/447] Format `class` definitions (#5289) --- Cargo.lock | 12 +- Cargo.toml | 10 +- .../ruff/statement/class_definition.py | 36 ++ .../src/comments/placement.rs | 15 +- .../src/expression/parentheses.rs | 10 + .../src/other/keyword.rs | 16 +- ...lack_test__class_blank_parentheses_py.snap | 135 ------- ...black_test__class_methods_new_line_py.snap | 348 +++++++++--------- ...tter__tests__black_test__comments2_py.snap | 24 +- ...tter__tests__black_test__comments4_py.snap | 22 +- ...tter__tests__black_test__comments9_py.snap | 68 ++-- ...atter__tests__black_test__comments_py.snap | 66 ++-- ...est__composition_no_trailing_comma_py.snap | 116 ++++-- ...er__tests__black_test__composition_py.snap | 116 ++++-- ...ing_no_extra_empty_line_before_eof_py.snap | 9 +- ...tter__tests__black_test__docstring_py.snap | 26 +- ...tter__tests__black_test__fmtonoff5_py.snap | 66 ++-- ...atter__tests__black_test__fmtskip6_py.snap | 18 +- ...atter__tests__black_test__fmtskip8_py.snap | 22 +- ...tter__tests__black_test__function2_py.snap | 14 +- ...move_newline_after_code_block_open_py.snap | 12 +- ...matter__tests__black_test__torture_py.snap | 17 +- ...t__trailing_comma_optional_parens1_py.snap | 35 +- ..._test__statement__class_definition_py.snap | 97 +++++ ...ormatter__tests__ruff_test__trivia_py.snap | 5 +- .../src/statement/stmt_class_def.rs | 134 ++++++- .../src/statement/suite.rs | 6 +- 27 files changed, 914 insertions(+), 541 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py delete mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_blank_parentheses_py.snap create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__class_definition_py.snap diff --git a/Cargo.lock b/Cargo.lock index 52a39d829f..43e96c3a5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2105,7 +2105,7 @@ dependencies = [ [[package]] name = "ruff_text_size" version = "0.0.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=f60e204b73b95bdb6ce87ccd0de34081b4a17c11#f60e204b73b95bdb6ce87ccd0de34081b4a17c11" dependencies = [ "schemars", "serde", @@ -2183,7 +2183,7 @@ dependencies = [ [[package]] name = "rustpython-ast" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=f60e204b73b95bdb6ce87ccd0de34081b4a17c11#f60e204b73b95bdb6ce87ccd0de34081b4a17c11" dependencies = [ "is-macro", "num-bigint", @@ -2194,7 +2194,7 @@ dependencies = [ [[package]] name = "rustpython-format" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=f60e204b73b95bdb6ce87ccd0de34081b4a17c11#f60e204b73b95bdb6ce87ccd0de34081b4a17c11" dependencies = [ "bitflags 2.3.1", "itertools", @@ -2206,7 +2206,7 @@ dependencies = [ [[package]] name = "rustpython-literal" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=f60e204b73b95bdb6ce87ccd0de34081b4a17c11#f60e204b73b95bdb6ce87ccd0de34081b4a17c11" dependencies = [ "hexf-parse", "is-macro", @@ -2218,7 +2218,7 @@ dependencies = [ [[package]] name = "rustpython-parser" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=f60e204b73b95bdb6ce87ccd0de34081b4a17c11#f60e204b73b95bdb6ce87ccd0de34081b4a17c11" dependencies = [ "anyhow", "is-macro", @@ -2241,7 +2241,7 @@ dependencies = [ [[package]] name = "rustpython-parser-core" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=f60e204b73b95bdb6ce87ccd0de34081b4a17c11#f60e204b73b95bdb6ce87ccd0de34081b4a17c11" dependencies = [ "is-macro", "memchr", diff --git a/Cargo.toml b/Cargo.toml index f6b7b59b22..c4d9205169 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,15 +50,15 @@ toml = { version = "0.7.2" } # v0.0.1 libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "80e4c1399f95e5beb532fdd1e209ad2dbb470438" } # v0.0.3 -ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca" } +ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "f60e204b73b95bdb6ce87ccd0de34081b4a17c11" } # v0.0.3 -rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca" , default-features = false, features = ["all-nodes-with-ranges", "num-bigint"]} +rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "f60e204b73b95bdb6ce87ccd0de34081b4a17c11" , default-features = false, features = ["all-nodes-with-ranges", "num-bigint"]} # v0.0.3 -rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca", default-features = false, features = ["num-bigint"] } +rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "f60e204b73b95bdb6ce87ccd0de34081b4a17c11", default-features = false, features = ["num-bigint"] } # v0.0.3 -rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca", default-features = false } +rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "f60e204b73b95bdb6ce87ccd0de34081b4a17c11", default-features = false } # v0.0.3 -rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca" , default-features = false, features = ["full-lexer", "all-nodes-with-ranges", "num-bigint"] } +rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "f60e204b73b95bdb6ce87ccd0de34081b4a17c11" , default-features = false, features = ["full-lexer", "all-nodes-with-ranges", "num-bigint"] } [profile.release] lto = "fat" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py new file mode 100644 index 0000000000..ec58f89c51 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py @@ -0,0 +1,36 @@ +class Test( + Aaaaaaaaaaaaaaaaa, + Bbbbbbbbbbbbbbbb, + DDDDDDDDDDDDDDDD, + EEEEEEEEEEEEEE, + metaclass=meta, +): + pass + + +class Test((Aaaaaaaaaaaaaaaaa), Bbbbbbbbbbbbbbbb, metaclass=meta): + pass + +class Test( # trailing class comment + Aaaaaaaaaaaaaaaaa, # trailing comment + + # in between comment + + Bbbbbbbbbbbbbbbb, + # another leading comment + DDDDDDDDDDDDDDDD, + EEEEEEEEEEEEEE, + # meta comment + metaclass=meta, # trailing meta comment +): + pass + +class Test((Aaaa)): + ... + + +class Test(aaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccccc + dddddddddddddddddddddd + eeeeeeeee, ffffffffffffffffff, gggggggggggggggggg): + pass + +class Test(Aaaa): # trailing comment + pass diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 5d09f24592..b118a80ef8 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -582,6 +582,12 @@ fn handle_trailing_end_of_line_condition_comment<'a>( .as_deref() .map(AnyNodeRef::from) .or_else(|| Some(AnyNodeRef::from(args.as_ref()))), + AnyNodeRef::StmtClassDef(StmtClassDef { + bases, keywords, .. + }) => keywords + .last() + .map(AnyNodeRef::from) + .or_else(|| bases.last().map(AnyNodeRef::from)), _ => None, }; @@ -622,8 +628,13 @@ fn handle_trailing_end_of_line_condition_comment<'a>( TokenKind::RParen => { // Skip over any closing parentheses } - _ => { - unreachable!("Only ')' or ':' should follow the condition") + TokenKind::Comma => { + // Skip over any trailing comma + } + kind => { + unreachable!( + "Only ')' or ':' should follow the condition but encountered {kind:?}" + ) } } } diff --git a/crates/ruff_python_formatter/src/expression/parentheses.rs b/crates/ruff_python_formatter/src/expression/parentheses.rs index d1506bd518..c535d65745 100644 --- a/crates/ruff_python_formatter/src/expression/parentheses.rs +++ b/crates/ruff_python_formatter/src/expression/parentheses.rs @@ -26,6 +26,8 @@ pub(super) fn default_expression_needs_parentheses( #[allow(clippy::if_same_then_else)] if parenthesize.is_always() { Parentheses::Always + } else if parenthesize.is_never() { + Parentheses::Never } // `Optional` or `Preserve` and expression has parentheses in source code. else if !parenthesize.is_if_breaks() && is_expression_parenthesized(node, source) { @@ -58,7 +60,11 @@ pub enum Parenthesize { /// Parenthesizes the expression only if it doesn't fit on a line. IfBreaks, + /// Always adds parentheses Always, + + /// Never adds parentheses. Parentheses are handled by the caller. + Never, } impl Parenthesize { @@ -66,6 +72,10 @@ impl Parenthesize { matches!(self, Parenthesize::Always) } + pub(crate) const fn is_never(self) -> bool { + matches!(self, Parenthesize::Never) + } + pub(crate) const fn is_if_breaks(self) -> bool { matches!(self, Parenthesize::IfBreaks) } diff --git a/crates/ruff_python_formatter/src/other/keyword.rs b/crates/ruff_python_formatter/src/other/keyword.rs index 7998efc47c..d93a56d66a 100644 --- a/crates/ruff_python_formatter/src/other/keyword.rs +++ b/crates/ruff_python_formatter/src/other/keyword.rs @@ -1,5 +1,6 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::prelude::*; +use crate::FormatNodeRule; +use ruff_formatter::write; use rustpython_parser::ast::Keyword; #[derive(Default)] @@ -7,6 +8,15 @@ pub struct FormatKeyword; impl FormatNodeRule for FormatKeyword { fn fmt_fields(&self, item: &Keyword, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let Keyword { + range: _, + arg, + value, + } = item; + if let Some(argument) = arg { + write!(f, [argument.format(), text("=")])?; + } + + value.format().fmt(f) } } diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_blank_parentheses_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_blank_parentheses_py.snap deleted file mode 100644 index 301e65440e..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_blank_parentheses_py.snap +++ /dev/null @@ -1,135 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/class_blank_parentheses.py ---- -## Input - -```py -class SimpleClassWithBlankParentheses(): - pass -class ClassWithSpaceParentheses ( ): - first_test_data = 90 - second_test_data = 100 - def test_func(self): - return None -class ClassWithEmptyFunc(object): - - def func_with_blank_parentheses(): - return 5 - - -def public_func_with_blank_parentheses(): - return None -def class_under_the_func_with_blank_parentheses(): - class InsideFunc(): - pass -class NormalClass ( -): - def func_for_testing(self, first, second): - sum = first + second - return sum -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,18 +1,10 @@ --class SimpleClassWithBlankParentheses: -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef - - --class ClassWithSpaceParentheses: -- first_test_data = 90 -- second_test_data = 100 -- -- def test_func(self): -- return None -+NOT_YET_IMPLEMENTED_StmtClassDef - - --class ClassWithEmptyFunc(object): -- def func_with_blank_parentheses(): -- return 5 -+NOT_YET_IMPLEMENTED_StmtClassDef - - - def public_func_with_blank_parentheses(): -@@ -20,11 +12,7 @@ - - - def class_under_the_func_with_blank_parentheses(): -- class InsideFunc: -- pass -+ NOT_YET_IMPLEMENTED_StmtClassDef - - --class NormalClass: -- def func_for_testing(self, first, second): -- sum = first + second -- return sum -+NOT_YET_IMPLEMENTED_StmtClassDef -``` - -## Ruff Output - -```py -NOT_YET_IMPLEMENTED_StmtClassDef - - -NOT_YET_IMPLEMENTED_StmtClassDef - - -NOT_YET_IMPLEMENTED_StmtClassDef - - -def public_func_with_blank_parentheses(): - return None - - -def class_under_the_func_with_blank_parentheses(): - NOT_YET_IMPLEMENTED_StmtClassDef - - -NOT_YET_IMPLEMENTED_StmtClassDef -``` - -## Black Output - -```py -class SimpleClassWithBlankParentheses: - pass - - -class ClassWithSpaceParentheses: - first_test_data = 90 - second_test_data = 100 - - def test_func(self): - return None - - -class ClassWithEmptyFunc(object): - def func_with_blank_parentheses(): - return 5 - - -def public_func_with_blank_parentheses(): - return None - - -def class_under_the_func_with_blank_parentheses(): - class InsideFunc: - pass - - -class NormalClass: - def func_for_testing(self, first, second): - sum = first + second - return sum -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap index ecb8824ed1..3ea7548946 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap @@ -113,259 +113,257 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: ```diff --- Black +++ Ruff -@@ -1,165 +1,61 @@ --class ClassSimplest: -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef +@@ -7,7 +7,7 @@ --class ClassWithSingleField: -- a = 1 -+NOT_YET_IMPLEMENTED_StmtClassDef - - --class ClassWithJustTheDocstring: + class ClassWithJustTheDocstring: - """Just a docstring.""" -+NOT_YET_IMPLEMENTED_StmtClassDef ++ "NOT_YET_IMPLEMENTED_STRING" --class ClassWithInit: -- def __init__(self): -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef + class ClassWithInit: +@@ -16,7 +16,7 @@ --class ClassWithTheDocstringAndInit: + class ClassWithTheDocstringAndInit: - """Just a docstring.""" -+NOT_YET_IMPLEMENTED_StmtClassDef ++ "NOT_YET_IMPLEMENTED_STRING" -- def __init__(self): -- pass - -+NOT_YET_IMPLEMENTED_StmtClassDef - --class ClassWithInitAndVars: -- cls_var = 100 - -- def __init__(self): -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef + def __init__(self): + pass +@@ -30,8 +30,7 @@ --class ClassWithInitAndVarsAndDocstring: + class ClassWithInitAndVarsAndDocstring: - """Test class""" -+NOT_YET_IMPLEMENTED_StmtClassDef +- ++ "NOT_YET_IMPLEMENTED_STRING" + cls_var = 100 -- cls_var = 100 - -- def __init__(self): -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef + def __init__(self): +@@ -53,8 +52,7 @@ --class ClassWithDecoInit: -- @deco -- def __init__(self): -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef - - --class ClassWithDecoInitAndVars: -- cls_var = 100 -+NOT_YET_IMPLEMENTED_StmtClassDef - -- @deco -- def __init__(self): -- pass - -+NOT_YET_IMPLEMENTED_StmtClassDef - --class ClassWithDecoInitAndVarsAndDocstring: + class ClassWithDecoInitAndVarsAndDocstring: - """Test class""" +- ++ "NOT_YET_IMPLEMENTED_STRING" + cls_var = 100 -- cls_var = 100 -+NOT_YET_IMPLEMENTED_StmtClassDef + @deco +@@ -69,7 +67,7 @@ -- @deco -- def __init__(self): -- pass - -+NOT_YET_IMPLEMENTED_StmtClassDef - --class ClassSimplestWithInner: -- class Inner: -- pass - -+NOT_YET_IMPLEMENTED_StmtClassDef - --class ClassSimplestWithInnerWithDocstring: -- class Inner: + class ClassSimplestWithInnerWithDocstring: + class Inner: - """Just a docstring.""" ++ "NOT_YET_IMPLEMENTED_STRING" -- def __init__(self): -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef + def __init__(self): + pass +@@ -83,7 +81,7 @@ --class ClassWithSingleFieldWithInner: -- a = 1 -+NOT_YET_IMPLEMENTED_StmtClassDef - -- class Inner: -- pass - -+NOT_YET_IMPLEMENTED_StmtClassDef - --class ClassWithJustTheDocstringWithInner: + class ClassWithJustTheDocstringWithInner: - """Just a docstring.""" ++ "NOT_YET_IMPLEMENTED_STRING" -- class Inner: -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef + class Inner: + pass +@@ -108,8 +106,7 @@ --class ClassWithInitWithInner: -- class Inner: -- pass -- -- def __init__(self): -- pass -- -- --class ClassWithInitAndVarsWithInner: -- cls_var = 100 -- -- class Inner: -- pass -- -- def __init__(self): -- pass -- -- --class ClassWithInitAndVarsAndDocstringWithInner: + class ClassWithInitAndVarsAndDocstringWithInner: - """Test class""" - -- cls_var = 100 -- -- class Inner: -- pass -- -- def __init__(self): -- pass -- -- --class ClassWithDecoInitWithInner: -- class Inner: -- pass -- -- @deco -- def __init__(self): -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef ++ "NOT_YET_IMPLEMENTED_STRING" + cls_var = 100 + + class Inner: +@@ -140,8 +137,7 @@ --class ClassWithDecoInitAndVarsWithInner: -- cls_var = 100 -- -- class Inner: -- pass -- -- @deco -- def __init__(self): -- pass -- -- --class ClassWithDecoInitAndVarsAndDocstringWithInner: + class ClassWithDecoInitAndVarsAndDocstringWithInner: - """Test class""" - -- cls_var = 100 -- -- class Inner: -- pass -- -- @deco -- def __init__(self): -- pass -- -- --class ClassWithDecoInitAndVarsAndDocstringWithInner2: ++ "NOT_YET_IMPLEMENTED_STRING" + cls_var = 100 + + class Inner: +@@ -153,7 +149,7 @@ + + + class ClassWithDecoInitAndVarsAndDocstringWithInner2: - """Test class""" -- -- class Inner: -- pass -- -- cls_var = 100 -- -- @deco -- def __init__(self): -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef ++ "NOT_YET_IMPLEMENTED_STRING" + + class Inner: + pass ``` ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassSimplest: + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithSingleField: + a = 1 -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithJustTheDocstring: + "NOT_YET_IMPLEMENTED_STRING" -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithInit: + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithTheDocstringAndInit: + "NOT_YET_IMPLEMENTED_STRING" + + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithInitAndVars: + cls_var = 100 + + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithInitAndVarsAndDocstring: + "NOT_YET_IMPLEMENTED_STRING" + cls_var = 100 + + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithDecoInit: + @deco + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithDecoInitAndVars: + cls_var = 100 + + @deco + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithDecoInitAndVarsAndDocstring: + "NOT_YET_IMPLEMENTED_STRING" + cls_var = 100 + + @deco + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassSimplestWithInner: + class Inner: + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassSimplestWithInnerWithDocstring: + class Inner: + "NOT_YET_IMPLEMENTED_STRING" + + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithSingleFieldWithInner: + a = 1 + + class Inner: + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithJustTheDocstringWithInner: + "NOT_YET_IMPLEMENTED_STRING" + + class Inner: + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithInitWithInner: + class Inner: + pass + + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithInitAndVarsWithInner: + cls_var = 100 + + class Inner: + pass + + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithInitAndVarsAndDocstringWithInner: + "NOT_YET_IMPLEMENTED_STRING" + cls_var = 100 + + class Inner: + pass + + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithDecoInitWithInner: + class Inner: + pass + + @deco + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithDecoInitAndVarsWithInner: + cls_var = 100 + + class Inner: + pass + + @deco + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithDecoInitAndVarsAndDocstringWithInner: + "NOT_YET_IMPLEMENTED_STRING" + cls_var = 100 + + class Inner: + pass + + @deco + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithDecoInitAndVarsAndDocstringWithInner2: + "NOT_YET_IMPLEMENTED_STRING" + + class Inner: + pass + + cls_var = 100 + + @deco + def __init__(self): + pass ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap index 1a5581f602..5454f791b8 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap @@ -349,7 +349,7 @@ instruction()#comment with bad spacing while True: if False: continue -@@ -141,25 +111,13 @@ +@@ -141,24 +111,18 @@ # and round and round we go # let's return @@ -370,15 +370,17 @@ instruction()#comment with bad spacing +CONFIG_FILES = [CONFIG_FILE] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final --class Test: -- def _init_host(self, parsed) -> None: + class Test: + def _init_host(self, parsed) -> None: - if parsed.hostname is None or not parsed.hostname.strip(): # type: ignore -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef ++ if ( ++ NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right # type: ignore ++ or not NOT_IMPLEMENTED_call() ++ ): + pass - ####################### -@@ -167,7 +125,7 @@ +@@ -167,7 +131,7 @@ ####################### @@ -511,7 +513,13 @@ def inline_comments_in_brackets_ruin_everything(): CONFIG_FILES = [CONFIG_FILE] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final -NOT_YET_IMPLEMENTED_StmtClassDef +class Test: + def _init_host(self, parsed) -> None: + if ( + NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right # type: ignore + or not NOT_IMPLEMENTED_call() + ): + pass ####################### diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap index 9df63bd077..620587b2e6 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap @@ -107,7 +107,7 @@ def foo3(list_a, list_b): ```diff --- Black +++ Ruff -@@ -1,94 +1,22 @@ +@@ -1,94 +1,28 @@ -from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent, # NOT DRY -) @@ -118,7 +118,7 @@ def foo3(list_a, list_b): +NOT_YET_IMPLEMENTED_StmtImportFrom --class C: + class C: - @pytest.mark.parametrize( - ("post_data", "message"), - [ @@ -161,12 +161,14 @@ def foo3(list_a, list_b): - ), - ], - ) -- def test_fails_invalid_post_data( -- self, pyramid_config, db_request, post_data, message -- ): ++ @NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + def test_fails_invalid_post_data( + self, pyramid_config, db_request, post_data, message + ): - pyramid_config.testing_securitypolicy(userid=1) - db_request.POST = MultiDict(post_data) -+NOT_YET_IMPLEMENTED_StmtClassDef ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ db_request.POST = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def foo(list_a, list_b): @@ -217,7 +219,13 @@ NOT_YET_IMPLEMENTED_StmtImportFrom NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtClassDef +class C: + @NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + def test_fails_invalid_post_data( + self, pyramid_config, db_request, post_data, message + ): + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + db_request.POST = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def foo(list_a, list_b): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap index 8168093768..857ff11509 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap @@ -152,38 +152,16 @@ def bar(): ```diff --- Black +++ Ruff -@@ -30,8 +30,7 @@ +@@ -44,7 +44,7 @@ - # This comment should be split from the statement above by two lines. --class MyClass: -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef - - - some = statement -@@ -39,17 +38,14 @@ - - - # This should be split from the above by two lines --class MyClassWithComplexLeadingComments: -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef - - --class ClassWithDocstring: + class ClassWithDocstring: - """A docstring.""" -+NOT_YET_IMPLEMENTED_StmtClassDef ++ "NOT_YET_IMPLEMENTED_STRING" # Leading comment after a class with just a docstring --class MyClassAfterAnotherClassWithDocstring: -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef - - - some = statement -@@ -59,7 +55,7 @@ +@@ -59,7 +59,7 @@ @deco1 # leading 2 # leading 2 extra @@ -192,7 +170,7 @@ def bar(): # leading 3 @deco3 # leading 4 -@@ -73,7 +69,7 @@ +@@ -73,7 +73,7 @@ # leading 1 @deco1 # leading 2 @@ -201,7 +179,7 @@ def bar(): # leading 3 that already has an empty line @deco3 -@@ -88,7 +84,7 @@ +@@ -88,7 +88,7 @@ # leading 1 @deco1 # leading 2 @@ -210,7 +188,7 @@ def bar(): # leading 3 @deco3 -@@ -106,7 +102,6 @@ +@@ -106,7 +106,6 @@ # Another leading comment def another_inline(): pass @@ -218,7 +196,7 @@ def bar(): else: # More leading comments def inline_after_else(): -@@ -121,18 +116,13 @@ +@@ -121,7 +120,6 @@ # Another leading comment def another_top_level_quote_inline_inline(): pass @@ -226,18 +204,6 @@ def bar(): else: # More leading comments def top_level_quote_inline_after_else(): - pass - - --class MyClass: -- # First method has no empty lines between bare class def. -- # More comments. -- def first_method(self): -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef - - - # Regression test for https://github.com/psf/black/issues/3454. ``` ## Ruff Output @@ -275,7 +241,8 @@ some = statement # This comment should be split from the statement above by two lines. -NOT_YET_IMPLEMENTED_StmtClassDef +class MyClass: + pass some = statement @@ -283,14 +250,17 @@ some = statement # This should be split from the above by two lines -NOT_YET_IMPLEMENTED_StmtClassDef +class MyClassWithComplexLeadingComments: + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithDocstring: + "NOT_YET_IMPLEMENTED_STRING" # Leading comment after a class with just a docstring -NOT_YET_IMPLEMENTED_StmtClassDef +class MyClassAfterAnotherClassWithDocstring: + pass some = statement @@ -367,7 +337,11 @@ else: pass -NOT_YET_IMPLEMENTED_StmtClassDef +class MyClass: + # First method has no empty lines between bare class def. + # More comments. + def first_method(self): + pass # Regression test for https://github.com/psf/black/issues/3454. diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap index 8935f71b8a..ae7d0c0315 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap @@ -114,16 +114,16 @@ async def wat(): # Has many lines. Many, many lines. # Many, many, many lines. -"""Module docstring. -- ++"NOT_YET_IMPLEMENTED_STRING" + -Possibly also many, many lines. -""" -+"NOT_YET_IMPLEMENTED_STRING" ++NOT_YET_IMPLEMENTED_StmtImport ++NOT_YET_IMPLEMENTED_StmtImport -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 @@ -137,7 +137,7 @@ async def wat(): # Some comment before a function. -@@ -30,67 +24,50 @@ +@@ -30,25 +24,26 @@ def function(default=None): @@ -173,28 +173,29 @@ async def wat(): # Another comment! - # This time two lines. +@@ -56,7 +51,7 @@ --class Foo: + 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 ++ "NOT_YET_IMPLEMENTED_STRING" + + #: Doc comment for class attribute Foo.bar. + #: It can have multiple lines. +@@ -65,32 +60,31 @@ + 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 ++ "NOT_YET_IMPLEMENTED_STRING" + + def __init__(self): + #: Doc comment for instance attribute qux. + self.qux = 3 + + self.spam = 4 - """Docstring for instance attribute spam.""" -+NOT_YET_IMPLEMENTED_StmtClassDef ++ "NOT_YET_IMPLEMENTED_STRING" #'

This is pweave!

@@ -279,7 +280,24 @@ GLOBAL_STATE = { # This time two lines. -NOT_YET_IMPLEMENTED_StmtClassDef +class Foo: + "NOT_YET_IMPLEMENTED_STRING" + + #: 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 + "NOT_YET_IMPLEMENTED_STRING" + + def __init__(self): + #: Doc comment for instance attribute qux. + self.qux = 3 + + self.spam = 4 + "NOT_YET_IMPLEMENTED_STRING" #'

This is pweave!

diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_no_trailing_comma_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_no_trailing_comma_py.snap index b45f26eec3..56529790aa 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_no_trailing_comma_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_no_trailing_comma_py.snap @@ -194,9 +194,9 @@ class C: ```diff --- Black +++ Ruff -@@ -1,181 +1 @@ --class C: -- def test(self) -> None: +@@ -1,181 +1,46 @@ + class C: + def test(self) -> None: - with patch("black.out", print): - self.assertEqual( - unstyle(str(report)), "1 file reformatted, 1 file failed to reformat." @@ -239,23 +239,38 @@ class C: - return ( - 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' - % (test.name, test.filename, lineno, lname, err) -- ) -- -- def omitting_trailers(self) -> None: ++ NOT_YET_IMPLEMENTED_StmtWith ++ xxxxxxxxxxxxxxxx = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ return "NOT_YET_IMPLEMENTED_STRING" % ( ++ test.name, ++ test.filename, ++ lineno, ++ lname, ++ err, + ) + + def omitting_trailers(self) -> None: - get_collection( - hey_this_is_a_very_long_call, it_has_funny_attributes, really=True - )[OneLevelIndex] - get_collection( - hey_this_is_a_very_long_call, it_has_funny_attributes, really=True - )[OneLevelIndex][TwoLevelIndex][ThreeLevelIndex][FourLevelIndex] -- d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ -- 22 -- ] ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)[OneLevelIndex] ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)[OneLevelIndex][TwoLevelIndex][ ++ ThreeLevelIndex ++ ][ ++ FourLevelIndex ++ ] + d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ + 22 + ] - assignment = ( - some.rather.elaborate.rule() and another.rule.ending_with.index[123] - ) -- -- def easy_asserts(self) -> None: ++ assignment = NOT_IMPLEMENTED_call() and another.rule.ending_with.index[123] + + def easy_asserts(self) -> None: - assert { - key1: value1, - key2: value2, @@ -267,7 +282,8 @@ class C: - key8: value8, - key9: value9, - } == expected, "Not what we expected" -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - assert expected == { - key1: value1, - key2: value2, @@ -279,7 +295,8 @@ class C: - key8: value8, - key9: value9, - }, "Not what we expected" -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - assert expected == { - key1: value1, - key2: value2, @@ -291,8 +308,9 @@ class C: - key8: value8, - key9: value9, - } -- -- def tricky_asserts(self) -> None: ++ NOT_YET_IMPLEMENTED_StmtAssert + + def tricky_asserts(self) -> None: - assert { - key1: value1, - key2: value2, @@ -306,7 +324,8 @@ class C: - } == expected( - value, is_going_to_be="too long to fit in a single line", srsly=True - ), "Not what we expected" -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - assert { - key1: value1, - key2: value2, @@ -320,7 +339,8 @@ class C: - } == expected, ( - "Not what we expected and the message is too long to fit in one line" - ) -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - assert expected( - value, is_going_to_be="too long to fit in a single line", srsly=True - ) == { @@ -334,7 +354,8 @@ class C: - key8: value8, - key9: value9, - }, "Not what we expected" -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - assert expected == { - key1: value1, - key2: value2, @@ -349,7 +370,8 @@ class C: - "Not what we expected and the message is too long to fit in one line" - " because it's too long" - ) -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - dis_c_instance_method = """\ - %3d 0 LOAD_FAST 1 (x) - 2 LOAD_CONST 1 (1) @@ -360,8 +382,11 @@ class C: - 12 RETURN_VALUE - """ % ( - _C.__init__.__code__.co_firstlineno + 1, -- ) -- ++ dis_c_instance_method = "NOT_YET_IMPLEMENTED_STRING" % ( ++ _C.__init__.__code__.co_firstlineno ++ + 1, + ) + - assert ( - expectedexpectedexpectedexpectedexpectedexpectedexpectedexpectedexpect - == { @@ -376,13 +401,58 @@ class C: - key9: value9, - } - ) -+NOT_YET_IMPLEMENTED_StmtClassDef ++ NOT_YET_IMPLEMENTED_StmtAssert ``` ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtClassDef +class C: + def test(self) -> None: + NOT_YET_IMPLEMENTED_StmtWith + xxxxxxxxxxxxxxxx = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + return "NOT_YET_IMPLEMENTED_STRING" % ( + test.name, + test.filename, + lineno, + lname, + err, + ) + + def omitting_trailers(self) -> None: + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)[OneLevelIndex] + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)[OneLevelIndex][TwoLevelIndex][ + ThreeLevelIndex + ][ + FourLevelIndex + ] + d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ + 22 + ] + assignment = NOT_IMPLEMENTED_call() and another.rule.ending_with.index[123] + + def easy_asserts(self) -> None: + NOT_YET_IMPLEMENTED_StmtAssert + + NOT_YET_IMPLEMENTED_StmtAssert + + NOT_YET_IMPLEMENTED_StmtAssert + + def tricky_asserts(self) -> None: + NOT_YET_IMPLEMENTED_StmtAssert + + NOT_YET_IMPLEMENTED_StmtAssert + + NOT_YET_IMPLEMENTED_StmtAssert + + NOT_YET_IMPLEMENTED_StmtAssert + + dis_c_instance_method = "NOT_YET_IMPLEMENTED_STRING" % ( + _C.__init__.__code__.co_firstlineno + + 1, + ) + + NOT_YET_IMPLEMENTED_StmtAssert ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_py.snap index 86812da5ea..09dc5f7269 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_py.snap @@ -194,9 +194,9 @@ class C: ```diff --- Black +++ Ruff -@@ -1,181 +1 @@ --class C: -- def test(self) -> None: +@@ -1,181 +1,46 @@ + class C: + def test(self) -> None: - with patch("black.out", print): - self.assertEqual( - unstyle(str(report)), "1 file reformatted, 1 file failed to reformat." @@ -239,23 +239,38 @@ class C: - return ( - 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' - % (test.name, test.filename, lineno, lname, err) -- ) -- -- def omitting_trailers(self) -> None: ++ NOT_YET_IMPLEMENTED_StmtWith ++ xxxxxxxxxxxxxxxx = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ return "NOT_YET_IMPLEMENTED_STRING" % ( ++ test.name, ++ test.filename, ++ lineno, ++ lname, ++ err, + ) + + def omitting_trailers(self) -> None: - get_collection( - hey_this_is_a_very_long_call, it_has_funny_attributes, really=True - )[OneLevelIndex] - get_collection( - hey_this_is_a_very_long_call, it_has_funny_attributes, really=True - )[OneLevelIndex][TwoLevelIndex][ThreeLevelIndex][FourLevelIndex] -- d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ -- 22 -- ] ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)[OneLevelIndex] ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)[OneLevelIndex][TwoLevelIndex][ ++ ThreeLevelIndex ++ ][ ++ FourLevelIndex ++ ] + d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ + 22 + ] - assignment = ( - some.rather.elaborate.rule() and another.rule.ending_with.index[123] - ) -- -- def easy_asserts(self) -> None: ++ assignment = NOT_IMPLEMENTED_call() and another.rule.ending_with.index[123] + + def easy_asserts(self) -> None: - assert { - key1: value1, - key2: value2, @@ -267,7 +282,8 @@ class C: - key8: value8, - key9: value9, - } == expected, "Not what we expected" -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - assert expected == { - key1: value1, - key2: value2, @@ -279,7 +295,8 @@ class C: - key8: value8, - key9: value9, - }, "Not what we expected" -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - assert expected == { - key1: value1, - key2: value2, @@ -291,8 +308,9 @@ class C: - key8: value8, - key9: value9, - } -- -- def tricky_asserts(self) -> None: ++ NOT_YET_IMPLEMENTED_StmtAssert + + def tricky_asserts(self) -> None: - assert { - key1: value1, - key2: value2, @@ -306,7 +324,8 @@ class C: - } == expected( - value, is_going_to_be="too long to fit in a single line", srsly=True - ), "Not what we expected" -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - assert { - key1: value1, - key2: value2, @@ -320,7 +339,8 @@ class C: - } == expected, ( - "Not what we expected and the message is too long to fit in one line" - ) -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - assert expected( - value, is_going_to_be="too long to fit in a single line", srsly=True - ) == { @@ -334,7 +354,8 @@ class C: - key8: value8, - key9: value9, - }, "Not what we expected" -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - assert expected == { - key1: value1, - key2: value2, @@ -349,7 +370,8 @@ class C: - "Not what we expected and the message is too long to fit in one line" - " because it's too long" - ) -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - dis_c_instance_method = """\ - %3d 0 LOAD_FAST 1 (x) - 2 LOAD_CONST 1 (1) @@ -360,8 +382,11 @@ class C: - 12 RETURN_VALUE - """ % ( - _C.__init__.__code__.co_firstlineno + 1, -- ) -- ++ dis_c_instance_method = "NOT_YET_IMPLEMENTED_STRING" % ( ++ _C.__init__.__code__.co_firstlineno ++ + 1, + ) + - assert ( - expectedexpectedexpectedexpectedexpectedexpectedexpectedexpectedexpect - == { @@ -376,13 +401,58 @@ class C: - key9: value9, - } - ) -+NOT_YET_IMPLEMENTED_StmtClassDef ++ NOT_YET_IMPLEMENTED_StmtAssert ``` ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtClassDef +class C: + def test(self) -> None: + NOT_YET_IMPLEMENTED_StmtWith + xxxxxxxxxxxxxxxx = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + return "NOT_YET_IMPLEMENTED_STRING" % ( + test.name, + test.filename, + lineno, + lname, + err, + ) + + def omitting_trailers(self) -> None: + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)[OneLevelIndex] + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)[OneLevelIndex][TwoLevelIndex][ + ThreeLevelIndex + ][ + FourLevelIndex + ] + d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ + 22 + ] + assignment = NOT_IMPLEMENTED_call() and another.rule.ending_with.index[123] + + def easy_asserts(self) -> None: + NOT_YET_IMPLEMENTED_StmtAssert + + NOT_YET_IMPLEMENTED_StmtAssert + + NOT_YET_IMPLEMENTED_StmtAssert + + def tricky_asserts(self) -> None: + NOT_YET_IMPLEMENTED_StmtAssert + + NOT_YET_IMPLEMENTED_StmtAssert + + NOT_YET_IMPLEMENTED_StmtAssert + + NOT_YET_IMPLEMENTED_StmtAssert + + dis_c_instance_method = "NOT_YET_IMPLEMENTED_STRING" % ( + _C.__init__.__code__.co_firstlineno + + 1, + ) + + NOT_YET_IMPLEMENTED_StmtAssert ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_no_extra_empty_line_before_eof_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_no_extra_empty_line_before_eof_py.snap index 69d1e86ad3..d56f37ee25 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_no_extra_empty_line_before_eof_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_no_extra_empty_line_before_eof_py.snap @@ -17,12 +17,12 @@ class ClassWithDocstring: ```diff --- Black +++ Ruff -@@ -1,4 +1,3 @@ +@@ -1,4 +1,4 @@ # Make sure when the file ends with class's docstring, # It doesn't add extra blank lines. --class ClassWithDocstring: + class ClassWithDocstring: - """A docstring.""" -+NOT_YET_IMPLEMENTED_StmtClassDef ++ "NOT_YET_IMPLEMENTED_STRING" ``` ## Ruff Output @@ -30,7 +30,8 @@ class ClassWithDocstring: ```py # Make sure when the file ends with class's docstring, # It doesn't add extra blank lines. -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithDocstring: + "NOT_YET_IMPLEMENTED_STRING" ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap index ca085ed088..784587197d 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap @@ -234,18 +234,19 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): ```diff --- Black +++ Ruff -@@ -1,219 +1,149 @@ --class MyClass: +@@ -1,219 +1,154 @@ + class MyClass: - """Multiline - class docstring - """ -- -- def method(self): ++ "NOT_YET_IMPLEMENTED_STRING" + + def method(self): - """Multiline - method docstring - """ -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef ++ "NOT_YET_IMPLEMENTED_STRING" + pass def foo(): @@ -396,12 +397,12 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): - tab at start of line and then a tab separated value - multiple tabs at the beginning and inline - mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. -- -- line ends with some tabs -- """ + "NOT_YET_IMPLEMENTED_STRING" +- line ends with some tabs +- """ +- def docstring_with_inline_tabs_and_tab_indentation(): - """hey - @@ -494,7 +495,12 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtClassDef +class MyClass: + "NOT_YET_IMPLEMENTED_STRING" + + def method(self): + "NOT_YET_IMPLEMENTED_STRING" + pass def foo(): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap index 8c6e03ade9..cc7ee04dd8 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap @@ -134,7 +134,7 @@ elif unformatted: return True # yapf: enable elif b: -@@ -39,49 +21,29 @@ +@@ -39,12 +21,12 @@ # Regression test for https://github.com/psf/black/issues/2567. if True: # fmt: off @@ -152,40 +152,44 @@ elif unformatted: # Regression test for https://github.com/psf/black/issues/3184. --class A: -- async def call(param): -- if param: -- # fmt: off +@@ -52,29 +34,27 @@ + async def call(param): + if param: + # fmt: off - if param[0:4] in ( - "ABCD", "EFGH" - ) : -- # fmt: on ++ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + # fmt: on - print ( "This won't be formatted" ) -- ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + - elif param[0:4] in ("ZZZZ",): - print ( "This won't be formatted either" ) -- ++ elif NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + - print("This will be formatted") -+NOT_YET_IMPLEMENTED_StmtClassDef ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Regression test for https://github.com/psf/black/issues/2985. --class Named(t.Protocol): -- # fmt: off -- @property + class Named(t.Protocol): + # fmt: off + @property - def this_wont_be_formatted ( self ) -> str: ... -+NOT_YET_IMPLEMENTED_StmtClassDef ++ def this_wont_be_formatted(self) -> str: ++ ... --class Factory(t.Protocol): -- def this_will_be_formatted(self, **kwargs) -> Named: -- ... + class Factory(t.Protocol): + def this_will_be_formatted(self, **kwargs) -> Named: + ... - -- # fmt: on -+NOT_YET_IMPLEMENTED_StmtClassDef + # fmt: on - # Regression test for https://github.com/psf/black/issues/3436. +@@ -82,6 +62,6 @@ if x: return x # fmt: off @@ -231,14 +235,32 @@ else: # Regression test for https://github.com/psf/black/issues/3184. -NOT_YET_IMPLEMENTED_StmtClassDef +class A: + async def call(param): + if param: + # fmt: off + if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + # fmt: on + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + + elif NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Regression test for https://github.com/psf/black/issues/2985. -NOT_YET_IMPLEMENTED_StmtClassDef +class Named(t.Protocol): + # fmt: off + @property + def this_wont_be_formatted(self) -> str: + ... -NOT_YET_IMPLEMENTED_StmtClassDef +class Factory(t.Protocol): + def this_will_be_formatted(self, **kwargs) -> Named: + ... + # fmt: on # Regression test for https://github.com/psf/black/issues/3436. diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip6_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip6_py.snap index ea2bbeca77..5a73724ef4 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip6_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip6_py.snap @@ -18,19 +18,23 @@ class A: ```diff --- Black +++ Ruff -@@ -1,5 +1 @@ --class A: -- def f(self): +@@ -1,5 +1,5 @@ + class A: + def f(self): - for line in range(10): -- if True: -- pass # fmt: skip -+NOT_YET_IMPLEMENTED_StmtClassDef ++ for line in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): + if True: + pass # fmt: skip ``` ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtClassDef +class A: + def f(self): + for line in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): + if True: + pass # fmt: skip ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap index 3c3e9d8cf2..408c2b92e9 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap @@ -75,7 +75,7 @@ async def test_async_with(): ```diff --- Black +++ Ruff -@@ -1,62 +1,47 @@ +@@ -1,62 +1,54 @@ # Make sure a leading comment is not removed. -def some_func( unformatted, args ): # fmt: skip - print("I am some_func") @@ -98,14 +98,19 @@ async def test_async_with(): -class SomeClass( Unformatted, SuperClasses ): # fmt: skip - def some_method( self, unformatted, args ): # fmt: skip - print("I am some_method") -- return 0 -+NOT_YET_IMPLEMENTED_StmtClassDef ++class SomeClass(Unformatted, SuperClasses): # fmt: skip ++ def some_method(self, unformatted, args): # fmt: skip ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + return 0 - async def some_async_method( self, unformatted, args ): # fmt: skip - print("I am some_async_method") - await asyncio.sleep(1) ++ async def some_async_method(self, unformatted, args): # fmt: skip ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + -- # Make sure a leading comment is not removed. -if unformatted_call( args ): # fmt: skip - print("First branch") @@ -177,7 +182,14 @@ async def some_async_func(unformatted, args): # fmt: skip # Make sure a leading comment is not removed. -NOT_YET_IMPLEMENTED_StmtClassDef +class SomeClass(Unformatted, SuperClasses): # fmt: skip + def some_method(self, unformatted, args): # fmt: skip + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + return 0 + + async def some_async_method(self, unformatted, args): # fmt: skip + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Make sure a leading comment is not removed. diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap index a3f8df4f44..93b9b22acd 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap @@ -66,7 +66,7 @@ with hmm_but_this_should_get_two_preceding_newlines(): ```diff --- Black +++ Ruff -@@ -2,64 +2,39 @@ +@@ -2,64 +2,41 @@ a, **kwargs, ) -> A: @@ -129,17 +129,17 @@ with hmm_but_this_should_get_two_preceding_newlines(): elif False: - -- class IHopeYouAreHavingALovelyDay: -- def __call__(self): + class IHopeYouAreHavingALovelyDay: + def __call__(self): - print("i_should_be_followed_by_only_one_newline") - -+ NOT_YET_IMPLEMENTED_StmtClassDef ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) else: - def foo(): pass - - + -with hmm_but_this_should_get_two_preceding_newlines(): - pass +NOT_YET_IMPLEMENTED_StmtWith @@ -182,7 +182,9 @@ elif NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: NOT_YET_IMPLEMENTED_StmtTry elif False: - NOT_YET_IMPLEMENTED_StmtClassDef + class IHopeYouAreHavingALovelyDay: + def __call__(self): + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) else: def foo(): pass diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap index a3a57b200a..27eeb9155b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap @@ -121,7 +121,7 @@ with open("/path/to/file.txt", mode="r") as read_file: ```diff --- Black +++ Ruff -@@ -1,78 +1,72 @@ +@@ -1,78 +1,74 @@ -import random +NOT_YET_IMPLEMENTED_StmtImport @@ -151,10 +151,10 @@ with open("/path/to/file.txt", mode="r") as read_file: + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) --class Foo: -- def bar(self): + class Foo: + def bar(self): - print("The newline above me should be deleted!") -+NOT_YET_IMPLEMENTED_StmtClassDef ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -for i in range(5): @@ -255,7 +255,9 @@ def foo4(): NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -NOT_YET_IMPLEMENTED_StmtClassDef +class Foo: + def bar(self): + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap index e9d41df04d..5aa9c0c161 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap @@ -42,7 +42,7 @@ assert ( ```diff --- Black +++ Ruff -@@ -2,57 +2,24 @@ +@@ -2,20 +2,10 @@ ( () << 0 @@ -65,16 +65,16 @@ assert ( importA 0 - 0 ^ 0 # +@@ -24,35 +14,15 @@ - --class A: -- def foo(self): + class A: + def foo(self): - for _ in range(10): - aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc( - xxxxxxxxxxxx - ) # pylint: disable=no-member -+NOT_YET_IMPLEMENTED_StmtClassDef ++ for _ in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): ++ aaaaaaaaaaaaaaaaaaa = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def test(self, othr): @@ -126,7 +126,10 @@ importA 0 ^ 0 # -NOT_YET_IMPLEMENTED_StmtClassDef +class A: + def foo(self): + for _ in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): + aaaaaaaaaaaaaaaaaaa = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def test(self, othr): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap index 6fb26dec18..c3a440c26d 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap @@ -38,7 +38,7 @@ class A: ```diff --- Black +++ Ruff -@@ -1,34 +1,15 @@ +@@ -1,34 +1,25 @@ -if e1234123412341234.winerror not in ( - _winapi.ERROR_SEM_TIMEOUT, - _winapi.ERROR_PIPE_BUSY, @@ -61,25 +61,30 @@ class A: + new_id = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + 1 --class X: -- def get_help_text(self): + class X: + def get_help_text(self): - return ngettext( - "Your password must contain at least %(min_length)d character.", - "Your password must contain at least %(min_length)d characters.", - self.min_length, - ) % {"min_length": self.min_length} -+NOT_YET_IMPLEMENTED_StmtClassDef ++ return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) % { ++ "NOT_YET_IMPLEMENTED_STRING": self.min_length, ++ } --class A: -- def b(self): + class A: + def b(self): - if self.connection.mysql_is_mariadb and ( - 10, - 4, - 3, - ) < self.connection.mysql_version < (10, 5, 2): -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef ++ if ( ++ self.connection.mysql_is_mariadb ++ and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++ ): + pass ``` ## Ruff Output @@ -96,10 +101,20 @@ if x: new_id = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + 1 -NOT_YET_IMPLEMENTED_StmtClassDef +class X: + def get_help_text(self): + return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) % { + "NOT_YET_IMPLEMENTED_STRING": self.min_length, + } -NOT_YET_IMPLEMENTED_StmtClassDef +class A: + def b(self): + if ( + self.connection.mysql_is_mariadb + and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + ): + pass ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__class_definition_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__class_definition_py.snap new file mode 100644 index 0000000000..24adda2166 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__class_definition_py.snap @@ -0,0 +1,97 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +--- +## Input +```py +class Test( + Aaaaaaaaaaaaaaaaa, + Bbbbbbbbbbbbbbbb, + DDDDDDDDDDDDDDDD, + EEEEEEEEEEEEEE, + metaclass=meta, +): + pass + + +class Test((Aaaaaaaaaaaaaaaaa), Bbbbbbbbbbbbbbbb, metaclass=meta): + pass + +class Test( # trailing class comment + Aaaaaaaaaaaaaaaaa, # trailing comment + + # in between comment + + Bbbbbbbbbbbbbbbb, + # another leading comment + DDDDDDDDDDDDDDDD, + EEEEEEEEEEEEEE, + # meta comment + metaclass=meta, # trailing meta comment +): + pass + +class Test((Aaaa)): + ... + + +class Test(aaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccccc + dddddddddddddddddddddd + eeeeeeeee, ffffffffffffffffff, gggggggggggggggggg): + pass + +class Test(Aaaa): # trailing comment + pass +``` + + + +## Output +```py +class Test( + Aaaaaaaaaaaaaaaaa, + Bbbbbbbbbbbbbbbb, + DDDDDDDDDDDDDDDD, + EEEEEEEEEEEEEE, + metaclass=meta, +): + pass + + +class Test((Aaaaaaaaaaaaaaaaa), Bbbbbbbbbbbbbbbb, metaclass=meta): + pass + + +class Test( + # trailing class comment + Aaaaaaaaaaaaaaaaa, # trailing comment + # in between comment + Bbbbbbbbbbbbbbbb, + # another leading comment + DDDDDDDDDDDDDDDD, + EEEEEEEEEEEEEE, + # meta comment + metaclass=meta, # trailing meta comment +): + pass + + +class Test((Aaaa)): + ... + + +class Test( + aaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbb + + cccccccccccccccccccccccc + + dddddddddddddddddddddd + + eeeeeeeee, + ffffffffffffffffff, + gggggggggggggggggg, +): + pass + + +class Test(Aaaa): # trailing comment + pass +``` + + diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__trivia_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__trivia_py.snap index a103d8ccae..df8702bcb0 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__trivia_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__trivia_py.snap @@ -52,7 +52,10 @@ b = 20 # Adds two lines after `b` -NOT_YET_IMPLEMENTED_StmtClassDef +class Test: + def a(self): + pass + # trailing comment # two lines before, one line after diff --git a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs index 5c2f8db3ab..e8784719c8 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs @@ -1,12 +1,138 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::StmtClassDef; +use crate::comments::trailing_comments; +use crate::expression::parentheses::Parenthesize; +use crate::prelude::*; +use crate::trivia::{first_non_trivia_token, SimpleTokenizer, Token, TokenKind}; +use crate::USE_MAGIC_TRAILING_COMMA; +use ruff_formatter::{format_args, write}; +use ruff_text_size::TextRange; +use rustpython_parser::ast::{Expr, Keyword, Ranged, StmtClassDef}; #[derive(Default)] pub struct FormatStmtClassDef; impl FormatNodeRule for FormatStmtClassDef { fn fmt_fields(&self, item: &StmtClassDef, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let StmtClassDef { + range: _, + name, + bases, + keywords, + body, + decorator_list, + } = item; + + f.join_with(hard_line_break()) + .entries(decorator_list.iter().formatted()) + .finish()?; + + if !decorator_list.is_empty() { + hard_line_break().fmt(f)?; + } + + write!(f, [text("class"), space(), name.format()])?; + + if !(bases.is_empty() && keywords.is_empty()) { + write!( + f, + [group(&format_args![ + text("("), + soft_block_indent(&FormatInheritanceClause { + class_definition: item + }), + text(")") + ])] + )?; + } + + let comments = f.context().comments().clone(); + let trailing_head_comments = comments.dangling_comments(item); + + write!( + f, + [ + text(":"), + trailing_comments(trailing_head_comments), + block_indent(&body.format()) + ] + ) + } + + fn fmt_dangling_comments( + &self, + _node: &StmtClassDef, + _f: &mut PyFormatter, + ) -> FormatResult<()> { + // handled in fmt_fields + Ok(()) + } +} + +struct FormatInheritanceClause<'a> { + class_definition: &'a StmtClassDef, +} + +impl Format> for FormatInheritanceClause<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let StmtClassDef { + bases, + keywords, + name, + .. + } = self.class_definition; + + let separator = format_with(|f| write!(f, [text(","), soft_line_break_or_space()])); + let source = f.context().contents(); + + let mut joiner = f.join_with(&separator); + + if let Some((first, rest)) = bases.split_first() { + // Manually handle parentheses for the first expression because the logic in `FormatExpr` + // doesn't know that it should disregard the parentheses of the inheritance clause. + // ```python + // class Test(A) # A is not parenthesized, the parentheses belong to the inheritance clause + // class Test((A)) # A is parenthesized + // ``` + // parentheses from the inheritance clause belong to the expression. + let tokenizer = SimpleTokenizer::new(source, TextRange::new(name.end(), first.start())) + .skip_trivia(); + + let left_paren_count = tokenizer + .take_while(|token| token.kind() == TokenKind::LParen) + .count(); + + // Ignore the first parentheses count + let parenthesize = if left_paren_count > 1 { + Parenthesize::Always + } else { + Parenthesize::Never + }; + + joiner.entry(&first.format().with_options(parenthesize)); + joiner.entries(rest.iter().formatted()); + } + + joiner.entries(keywords.iter().formatted()).finish()?; + + if_group_breaks(&text(",")).fmt(f)?; + + if USE_MAGIC_TRAILING_COMMA { + let last_end = keywords + .last() + .map(Keyword::end) + .or_else(|| bases.last().map(Expr::end)) + .unwrap(); + + if matches!( + first_non_trivia_token(last_end, f.context().contents()), + Some(Token { + kind: TokenKind::Comma, + .. + }) + ) { + hard_line_break().fmt(f)?; + } + } + + Ok(()) } } diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index 0a264a6214..85329fba8c 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -252,7 +252,8 @@ one_leading_newline = 10 no_leading_newline = 30 -NOT_YET_IMPLEMENTED_StmtClassDef +class InTheMiddle: + pass trailing_statement = 1 @@ -283,7 +284,8 @@ two_leading_newlines = 20 one_leading_newline = 10 no_leading_newline = 30 -NOT_YET_IMPLEMENTED_StmtClassDef +class InTheMiddle: + pass trailing_statement = 1 From d407165aa766252ee9b7663ef457611f5154accc Mon Sep 17 00:00:00 2001 From: konstin Date: Thu, 22 Jun 2023 16:52:48 +0200 Subject: [PATCH 185/447] Fix formatter panic with comment after parenthesized dict value (#5293) ## Summary This snippet used to panic because it expected to see a comma or something similar after the `2` but met the closing parentheses that is not part of the range and panicked ```python a = { 1: (2), # comment 3: True, } ``` Originally found in https://github.com/bolucat/Firefox/blob/636a717ef025c16434997dc89e42351ef740ee6b/testing/marionette/client/marionette_driver/geckoinstance.py#L109 This snippet is also the test plan. --- .../test/fixtures/ruff/expression/dict.py | 8 +++++++ .../src/comments/placement.rs | 24 ++++++++++++------- ...tests__ruff_test__expression__dict_py.snap | 16 +++++++++++++ 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/dict.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/dict.py index f8cf5a9351..a0631959db 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/dict.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/dict.py @@ -48,3 +48,11 @@ mapping = { C: 0.1 * (10.0 / 12), D: 0.1 * (10.0 / 12), } + +# Regression test for formatter panic with comment after parenthesized dict value +# Originally found in https://github.com/bolucat/Firefox/blob/636a717ef025c16434997dc89e42351ef740ee6b/testing/marionette/client/marionette_driver/geckoinstance.py#L109 +a = { + 1: (2), + # comment + 3: True, +} diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index b118a80ef8..e1cded005f 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -991,14 +991,22 @@ fn handle_dict_unpacking_comment<'a>( .skip_trivia(); // we start from the preceding node but we skip its token - if let Some(first) = tokens.next() { - debug_assert!(matches!( - first, - Token { - kind: TokenKind::LBrace | TokenKind::Comma | TokenKind::Colon, - .. - } - )); + for token in tokens.by_ref() { + // Skip closing parentheses that are not part of the node range + if token.kind == TokenKind::RParen { + continue; + } + debug_assert!( + matches!( + token, + Token { + kind: TokenKind::LBrace | TokenKind::Comma | TokenKind::Colon, + .. + } + ), + "{token:?}", + ); + break; } // if the remaining tokens from the previous node is exactly `**`, diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__dict_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__dict_py.snap index 2c0cba1259..d59022c544 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__dict_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__dict_py.snap @@ -54,6 +54,14 @@ mapping = { C: 0.1 * (10.0 / 12), D: 0.1 * (10.0 / 12), } + +# Regression test for formatter panic with comment after parenthesized dict value +# Originally found in https://github.com/bolucat/Firefox/blob/636a717ef025c16434997dc89e42351ef740ee6b/testing/marionette/client/marionette_driver/geckoinstance.py#L109 +a = { + 1: (2), + # comment + 3: True, +} ``` @@ -112,6 +120,14 @@ mapping = { C: 0.1 * (10.0 / 12), D: 0.1 * (10.0 / 12), } + +# Regression test for formatter panic with comment after parenthesized dict value +# Originally found in https://github.com/bolucat/Firefox/blob/636a717ef025c16434997dc89e42351ef740ee6b/testing/marionette/client/marionette_driver/geckoinstance.py#L109 +a = { + 1: (2), + # comment + 3: True, +} ``` From e8ebe0a4259885132de1db8935a813424c3c61bd Mon Sep 17 00:00:00 2001 From: trag1c Date: Thu, 22 Jun 2023 17:19:34 +0200 Subject: [PATCH 186/447] Update docs to match updated logo and color palette (#5283) ![8511](https://github.com/astral-sh/ruff/assets/77130613/862d151f-ff1d-4da8-9230-8dd32f41f197) ## Summary Supersedes #5277, includes redesigned dark mode. ## Test Plan * `python scripts/generate_mkdocs.py` * `mkdocs serve` --- README.md | 6 +-- docs/assets/bolt.svg | 3 ++ docs/assets/ruff-favicon.png | Bin 28017 -> 751 bytes docs/assets/ruff.svg | 6 --- docs/stylesheets/extra.css | 69 +++++++++++++++++++++++++++++++++++ mkdocs.template.yml | 10 ++--- scripts/transform_readme.py | 4 +- 7 files changed, 82 insertions(+), 16 deletions(-) create mode 100644 docs/assets/bolt.svg delete mode 100644 docs/assets/ruff.svg create mode 100644 docs/stylesheets/extra.css diff --git a/README.md b/README.md index fdf828ab1c..5c77173aad 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@ An extremely fast Python linter, written in Rust.

- - - Shows a bar chart with benchmark results. + + + Shows a bar chart with benchmark results.

diff --git a/docs/assets/bolt.svg b/docs/assets/bolt.svg new file mode 100644 index 0000000000..1d2a503d51 --- /dev/null +++ b/docs/assets/bolt.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/assets/ruff-favicon.png b/docs/assets/ruff-favicon.png index 7395c3fbce1d30afc21d1d7f0375f23f80c75a78..0a6d0871c7bfe46ed709aab4eac94710a18d0f49 100644 GIT binary patch literal 751 zcmeAS@N?(olHy`uVBq!ia0vp^DIm``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&di3`{MaE{-7;jBju4&lXN(IQH@R-YrUNujpLT zIhxaCG3S9w%d**vGG!Lha6yZ&!l z+5R#1KC3^5>p8wf(%Z>^?b- z?$amZ*RIY!y5l>Suu5;?`)?CB-Kg8QaJ6}O2#Zf7#Jyab+m_^W=H1)5LMQCHmsXb( z>&&CGoAPFCT2}BQZN(nJ6t3!iJFR?{lP_*I^S`qByJU}O8t=^M?Pvel*B5X2{xs!z zf*6NUqFDLgNQ2awiXDx!n3t z;+-y&j2j=C)j!W)-j96d(8s!mNX8SJXhtLUo0$ zd%8#aGMw~fu;C=Gf?|zq0$f4!9_(QJ^<}^HbxWS9oIcsKVd3aL!Sl@iqVH*c!cE}o z#QFZ?Ocx)#z{|R~@5jp&H<@g#3LnDWrVU^3%@q(fFHKilY-KgpUJS$alY8XNOwh0W zj}{^xon27}$3j#-dhhhHu(K`hmn)xr;I6y~yZB!3$BO|qc=v!fx ze!>I$x7)~DS({w#bddQR#|tL?)cVG_KI9xc<(IDJ!yR|zIES^^UL?|0yY+3jh>v@2 zI$wCh?E9~MEK2(cvF6}sW4g8ml024YnTr05WGO%KCtcXAJUg7*3}m`)Rx;Vuc8mMm zS3k}nua>L5`>}G{V5GxjS&I`)U7Xz>6(0E5P_j#R(bOgIs;G;ONvmbjAYo>>zmHZI zxRFBKA@pfo!|mzX!2IzTf)=%j9w9hu$iFN|MF_F8e^JQbV-y5pFrjjE0$iZdFVk42 zVwKV;(Y3Ug`ipg3>79k>4PjH2yn-~`hC>U5iVXNWi>2<#oI?ky!X&kdYi7%`mLx5! za+3ZjrO~QyYT8!w9(YN#+O?KrKU<>+Jp)*!t2>;JJ7RwD!KxE_d0YfDnPyn*X|7h( z$)Cq*mP}s z&UKDFgmiT&G+d}Q+))>DG@h$&H&uGKiJ)X+%tZ!>CPQM{QiJE5Wm}`D>pi#TzK-R2Q;hOxZnH(E)4ox3 z;jXt**ZhdY!lg5frY*~?qE`FZJdW7heHY4#U!$;qj-RTfp}(P};3@ODAD3%Mf@p4F zVaOSb31W1Ce{L+~Dt?lTJZ0HMZ|e40PVK99jopnDPsl+-NzP+>X^e&!vvA zb~;tI9Yq*7_9$C=oU_;-CIh9(@Iu;cy&L`ema8E37&leW{q4J@gML-l#soMG#6d3$ zo2hp7oU#1}^7!Vf&fOxROb$$QBz9~Ru&hB5oKFe@KG9%R%;?RPn_^1mAs@m7ahX!B zo3h|C!rK+<8qiM0AG*6V5fXY1a%w43Tc?}F36fEBg^H0@>Z&4o+*V3ezD|&G`W>1# zb3xiwaU>amvK4OfYDH}-`Xx_;JCobk&4wS>q41>ofARC5zISIaESeg%dQLbPQq^cr zuy9LbQkHb16NaXOL3qq$^etBkai{2O8sPovoJEOK9l%GKa;V-7zaIBMtEWjcP_!zu zW6usg6yy7Nw-RL9vZUPH>t>|-d-_l?&vb7|u1dm5i7j0>smW&f{ib*GE)>=jN5tcq z73=l%s)Kb6JAUz@nRgVDl8&+^VpzPfaNcLGxQ=Q~{+4-~iupSnCkc+BS|r?E z4?ht-%!?cBkVou43Z&RbZS99ASYpSH(X=S4-fL;9cm(xya%hjHMD_;F%m*xMV#fK8 z94(>l+tCo%eLUkv6U!B8`lNEzI-7Wl0;85|9!JS(&t;kePmx-~k=5UjL>pg%2f=|7 zEwZ`^b}u3uhgP~@gY>F+#%wVL`D(jiUMdC-rxqE=sTPAtHH@?24<8Iq<7j8rbLEB? zUO=H+&d)d(6fWWRrJ`j5yit|{5}jF#p|RW4h`yfST5gNLSzt}N?d(}@l{N(>VAcu z_gUMZ2!aeK*Dx6%aB^vKyxq4b87h(fz|?yQTp?;#Y3HGGY|Vvd1&mYwWX_Yt$FXsp ztmuG|E+D)-~%@?Y0fg& zLfNA93p)$X9Lzj4#DMvPIpUUEq59iVbpqMCw_SlIG5C-lPq?-1dp`dLb}JP=7FqC( zLPvB29jin?68vHxr%KjHB$LE zXvq!phQ}0&CV2z&yP_};{Tf}xtjs<<=!F3Ez@m5TPA3MY6;qQw(u+T{)ICIgkp{C) zTd7;TnL*az|2RRd|wz5PUdO?RM2Y8 zO6oFGd+vLb9WcM4P5U7cx1PblJBU-4e7=&l46fcshrQp_jKd5gc7@hx{K@PI zHy}JIeDIzwL73?F#o*odX=4L|44;YOVJ3@;EhiC>klz`?5}Gnqd?!1g#FhAMG(cv% zL}l!mp=f-TbtS5j_ggZpTedw7+iN$gtZc`C%wX){@g7Wvxp^!C%L3yn{e40j5lax-}OO5 z@$$=^DkPOw7E(A3|3qwJvZO+Y92vF{+b^>iHS@hv%WS0gJsdU|dj9twslu)BsMdR4 zLSnGERmQ6mo5(OlV~NLPsz!EE86V>rE@gG7l@YUEG4 zuUvcPSkZ{MGPEr1EtBkMD>}Xy{orbA;SK*}#mB0{$J4H&NMwL_r1-D?%`&FP>W^(gD`?cWKipNKeoj4X98#UV?QE z?e82PMqA}I8st9e!@{M!zpLXxSr1D~W)DPefUQqYYNPw2VUSm2E1mrQ3}3A|EOdk! z1$-lVntZ1uab^x>7zqo}5K&~XIg}~E0*-dYOENfo3Y>L>ZkhRrrDsjlb9d=@@7YFN znZ3b(JZn7A4wDuUS_{+`wy!@w?VbzW_mMm+U%4*r>}EhglODQD`_&n|BZ%XV#GTM4L)@q*+L|m zoX`yk|Fc7XfrUhVZ}KxiB+j}aA`&U1LCkqVb=ftnCSiUVfzm9VRgFYz4pmeLYyg7J z;R}5!lF}Ux1M{T(-D92l3mJ@i(ViZ$dAW@7t{qWtwIwWjLQ9<54q;^}L(%?V)oH3$ zo~*Le_oav-ICU7IOHN~N4xU(tg@)kMhgb#j%1;Cd(pZfg{gCJ*{^&NVZuaO?pHRs*e!HQj+9|liwE@KmtJhfo_WVG{nms7na zhS%`QMq&w#1lBq0^_y6ajo`GW3mo1!;8%s$%@b0yUr~NMRQUhc?5ugf%2y<2mjFef zs94QHFO`IXJtXExN;*W?NvrhNpCfy23nbmlU+{tlBv8;{GY>&mK+(y5<+5HoLVnqo zxi@(ius(?OeKkE%RM^OfVuwF1r?Ap&{>1px74hDw*>Jb8PsEcJnsfj2q<+F^E+X`m z;$%}@EA{-YkHm(XVu?w`fz5SYSBKFm`^kX9M^LaZ%YxMoZ9$0UdX^A5w7vH<*~1bP zL`HS8>EJI3s6rf6XmFh6ISXNSHg{gnp-(ZkJnGqUcZe5O)`iQAU`pkqOBAe41C+H5 zaMzggHT$2!2iI72IxW6ZS{}!2=ANzBgY`t|ZjwyT@!@l&QB$`hL{!OTR5ec3%SY-< zrx2fM*P}-sO4t{-w>XzXrbt?Bd}k5ZF(0i0WfE^Bvl2p;LMKz)2#+PFOlMCm8mCkB zB7Ajq*RUSkw5ScB;W;f*`|yqpkChq}4pkD%N>4)ReyzIn$?#cTh4B39Rcpf+~2`Z&>_@9bLHPhz1$3#VEA4KjvwKT=Z0 zcDgw7pzxe{JdyT>_-cNxkQVCv&6G{22!+nhpA!BpTI8TmoMB_V@NP2BHSfJL1jS^p zC)SVVfJI~IQ47l-K_%9g;i2cJjd>=jvdQZ0=ugs!fhVvm@m$G|onUDkWioh2c4qBM z)cZMJ()$`Xp|mg6^AV!0V>~B9voG|Z<`)C31*~I5jfn<>QMGSe=aEo_F#dibQXcf? z5zCREGrb)Zf3n#okaCLiC2z>%#|o(_hL*A7uj|tOcxGn&f+uwYPW92+ebJG-oig(B z-M72<&g#FQ4)>tvDmNor0z&MqQj+`j(JQ4^;;usR2~0#^1Gtp@RC31PHXU?fJjJDvPw%w77DqsqqAG3l}<18HX;68e)eX=bI_7 z4CU?=@bQS^#RhNCYD|3fE~S+DQ~yqOboXPZ{6@l>z1LkiUjMl>QIN40FJo&3oKayn z)PSq*78JtALMf-$yJfaqRA-c1c7-yzWF+=hD{N8O`pOJ$r}co4jpIV^c!=++Ow_|B z+{gmx5gIfMHDAzlh*_xUQ-WerA$nnj#r5roE36pqe>hsaLe9)I^5D$6gpR9grV0g5>9)^Q2%FW5>e6Z|M`~$fNovN%_ z|2)b9@igX^H+KUypFMXAL8a8$1Lg93GPAKNtT~fcCZZc$>wATH0{Gd+va5Yp{pYWD z3a_p{UcE=$oJk`G5&S7r%;nL^AAR!!VF@8OUbOmONp4Ax3sP^03787QLWw5giPF%y zru~ED4hT-1`-J=C{9%NUzdF-AU{oEMhn)QoeJldI0!bpch?vhjdLXj3&sp@Q+*)?Y z*p+i+pEl6JKMY})H8->;BkdHpB9cgR+x7giMIR@7Zp063B^jUPQ+9M8gO?SH9o0ah zOl{>=3pW1P`-fgWH|HH~J^Lr{-vdNbZc$T)mxGnd`xdGk?POT z%Wl|2RkadpN}^6>W*}S$?rc7?9ta9mLY)kKd%nsZj}lM16<5YsUQcE)he6Ac>&MZz zPj#?!-+hd!9nF1B$TYsy_hZSE!S3c-K3>Ca;ukBj_5|JO(q9~D*m9maAcFfl!*EDN z5OxIMZP*04-2I9k8lI-2{1s;Q#g9}V1_j0%p^>)Lc&h8UA#Q}HM&beD7pj0>rljJT zHV7828?Lwb>N9N*^Sj}?ci6+h%3sb@PQw=h$;WmjQ|PZ7RI%1IDMzBx);{D3olG9` z`@ARXjIzv>vAIlxwhm9gwX4|d*+h|mgjsVCl@(`(l8bcbB^#B}(Rb)#gb-Gd1LHv8 zs1sA{3w@1W3Ut6ZrFdmD3I=E1dITk2f;w~X4`{u6q`@k%{7oA|Ak`?>j}-MGW|`E^z7-_`o1_$xhC?wH*jMYa zozAl;7Wqiiw)665!IGkMXfOhwz&pYq+E8yx(@ckZt*V0KTGpidr-4PN?Isip&wIyk zbl3^D2E2E>RW!rF9@m%%)`TQ%qVQShQy2D~xK;^Qd}^8U1b5A+Mh~WltFc&;k!UrG z^)~ZZ2!R%UQKH=zithvvk3q>OUeICztZQYYn;23Xx_c64w#pmGUl%id&B9c`&+o$J z>t_d^`=oRVCYtXPaq>8Sky=xFM^u{A`=HJ+-Aq+wkKY&&?>%zO_bIXCv-1sZ-*zdl<5xHbz*rw0^^F!lx9G>dQ{x!JJV+mKl1TLfWP( z9&4`E=hn{jIAnbJ)gz;Jw%{)jWTE6L=b5g-2CaQ2Z zf+*ClG)JK^H`0dXf{y1UjfZ%6%iYsk;*a&Qa?yqlxPfUV> z@qwOopoOrof~2tUf3>NB=5&U49G^t5Ab!8TvglO!R|1i6;}W?KBBJ;WaH1tBmO*L+ zGW5>@6o``H5&4^1b{C-9qs?Cxi!_gV>a-I#gdB(K4`YAB5 zj2&UavaYc!hc%}RSB2|VqfAB+lVB&Mn*5YhLhIZzhe${NbL>`7`{xl$RW@=`?g0QA?HE(?HAS_imP6Q*JhSe2Ne7))nv}e z*m)p#pxUAKj{|{Yn#bx2+z~yU&5y{-xk=`7PTv3GBq9}Dpr0(^8o#aGW|i~XWU(u3 zdiuuAcqQpkStuIg_hP;ItF@5IvwIhb@y>Xt=tSysnh-y7d-fEEiNC*gj@j68WlPKq zEU~OExvk$EcmK~^SC#6~7&7B+k&=WSgi#1_VUB_9IqQUAi&zh$&lF1`iqStw;LbHU%A}C-*1l@NQnOqakAng(U4Uj z7PfUTA!em#rDvoQb+d3`CgF!8=5;VO6?gu$Ph7c1UXpPIa%1+5WmSZG_rMe;v*pe z-V^`V{;cg}W&c~ejpIM90N8`U&Crg4iJpHiq(-}d(Q=C3;cx)EUdf6M*1(f_6U-@(8rSy^roTafeH_9R94 zNZ!`xHns&>7<2#qlbMx+!`KkSOvhwu$VSJ?!OlfzXvEG&2Qp+eGG#I1;9_S1{fj6` z8%HNY8<5GHC_p&91t5ow%aEBBWWq(qY0PFq#|mO$p)+JPW}-7OG&W^oHRdoj`8N^r z4i*3^4Zr;BuHHl$1EM&LxR{v?SxxC!SlHR=SecEO=!}e*jOYwaI9QntIl0)mSlIvW z#u&scX6s;W2)NV2+R)5|!Oq6)?~6BsbAMKlZANxBX0HDkq-Nsa2te^oC=(++%RdCa`Gp(M2C%H* z8&ClOe`f&7a0@$_7&_THsMy+m;Ujt56Y<;3zxXER{fAd1EF6Im?r)I)C+L+;?EmrK zKVsmE#ot%N#D8(iZ3z0uB94YGCdPjk0_OeW70BGs#>@l=@P8uezn)wCU%bU>%w%L@ z%E3j)#>&J($7;lCNXKbt!b)ev$!^TW%F4mS$j0&S(j9G0om>qaOg@_dngZGY2>PoH zG37sON%il;UCmA248_RILdOWeib;i$m7AH9n~j}@k%^m;k%Zx&8Z*4b^?#|%%kcj( z3h&%u=K5cA{YMt~ zj|TrQy8hQ(|B(g$qrv}+uK(Z61@}Kk9upgY2Dt(U8Wl5oRNycKVa8S^@A>W&>Hkw!>@p-2I7(&2zHS#B^aO6h{K^p zJ}WMSLt|RNy$kRo?jTEzPnF^75|F!TI;)~IaY!M%J#KAwVF*m+n&A4GS?o=Sg8HBT z#O04DpofOywJA60i|kzJEb#Fi`}=S;=}od*Lt$rv z-TFSqU1!LIfY$adgRGiVy7WS&WBfvgaxHWdi@X|b;S4_|Fx;c|x39260elT=K{M2jts8#eP&9F85(8bj{eX7GZCen)Oz#;Yr zxWeULaqg!_zVEMvN6YMhs&SA9^XtlH(*#S9m+fbrf~UfF<*2=pV_BRNw<^vyM9`t3 zLkt*hi21T{kH`ry`!40mo4DY!JblTbiQoG5u~{3hZ<@MjZ>fNdS%!vvb(lC>XZk~d zK6o!UpBo%vm{yILuEXurgZEkA*_H;7HtS}SyoEi<(oyr;pXx2oIX^cM$m;{LIP-@Z z{pA=ePfu`nozZ=hHfggQMo+?~HMm#0PH(Nm(E2!1q3!np{|3eiw1gOQCgA%E&z}$W z9*eizqI*~nMFZ-ogfKKz0`p?Nyb^DC#v$ZqR73MW zL<#JH;etQ=OZ#&_-3-P2_+hJ};ujjK@&SOZ&ib5kweRgu0d|`d=1Ad<8T;-hje1_U zbM(6BE8p#j9Y`#QCdgvKo#U#?yFvg3TxnS?Y`2lp6Ve?UefHe2igy2k7t=9*|4;$lsqm*On$DEOEH}F1ws$@Yz7j2xw?M5~?6L2_XTCgcJiu1z;5#Wpk9e zRI#vR;d?sCTPUDV&b$t~ie9WJCt4Qp0%m%5-JJ?;%6E?mKKHsUQ8E%@N3#X0?wTWs zh?EtanYFGxGmM`s|B1Yg!)E<8bnEm)J;TcXWyN-3qf=A6^AxLV4ap74yx$*qleN?h zRyJAC;z3NJRLDl>)UuM5;qcJM)9xBz|1m6c|1O5GXOEzk%eTdBqY`s#t4~YUnP>9q zT>zv35@QK>tc?#IP_q_I;K zt=^WP1@}5tAL!uMZuSlN zi7vqG$iq@I^aC0Mj76PWIBC=r;cPZPPM&0vc__W9rrB>_9 zC%;4Zv_Z1VLjTTg-kkQ_WzVrk1$)CFT*C!Mufo>h8W1CZpE!O%|bzPF4U zp8|u54aH@lE0}Oi(#b&Cwzo2nh!tm^pwy)XKa^T$w=a1j&)39NZb*{9aDw#SGrB}) zLxqG8S?$h8%(3xe5R*8}@^Vn-=bTdEG_l)kb|RG1hMs0+Vj29T@d^0IGR44$ni8x^ zS-U)q9~!!N)Wf3mdU1P}y6=2`fD`Skm3=yrPchL&u+uu#ALesN%!q>>&i1NUSqeC^ z@zB>^EZx3oZ`A@Qrs#fdnx~S$L2>EcYhPo5bk{zK&2Ci;H@x!Iu_oId5D2nA<)6 zo_9WutW$h3g&Jl#zWV6r4Z!~6QDotD&tCYL$6r>U4qInB%Nzi^L=huOxi-)}LB3;+)6=>wrRu9kW z000dCS{?`kWZ7HJ@xfq6ZloXv^UFJF{ypEUi>OV01G0|qW zCorXiXO=OrU947k4xvA3JXBZc^;@?v zaIg{%`OOCEPL^j<$7|$QCjk0j4D3FuK0X1F((LH<-jSc4G-(kn7_XM%r@*Z8o5ZJN zz04V0S=>ZtZDE^9JF$%9{_8|215AnzPQxEX90wKAgUN(CLmo`1K;y-NmI-080 z`Ltgye_DYTbz(|5xk34R`cnx?$VURcsJU^v=I406iKbI}wG{eV=0#Vx!2klXjn|LB z`KE9%;G;9<$t}RZ9A`zT`2}=GkHM%!a1B^quCk*BC}9fa4^S;;z(rzjKE4}*)w=N8 zeoL4BNh`+~EVJEpSMBuVFqdcSiL1Rd28M7tMM7Y^y*8;TKGJeQXbsRP1r>TCn00uW zoz>^Is5e;eb-s@TNnZAZbba~|f_Y4u;X6tgj;@W*1VIaC1I~VYsE905FmU8kw6fM< zJ<$K`X?)$gNn5sgRQCFTV=Ll(q~-AUGmt-O!by?T+)7|cl=7XAV_lvKek?WZdg}T@ zSgI!Df2H9Kd~y1)Svc~R@pnW~sBYCs zx6tZUOWtUTVM|BX!k>alf_%mxghPqLwfbdne!sTj2?WR>w2FUtOUSQ5YekqC8pE}u zK=t{xK(;RpD)O-qA_Zbp6{mK8xM?ciXy`(=UBxjqxsuI862f3Y^ zZ((c;Q^@5ak~;DUz^s8ca`SlPt(m;HTciienjc;b&~BdFejug>pC(4A^Y`tgMcw+Y zS%DI08QCN@XQ@2PxO>yCvmYer;@mCpwSW$=xl&XQXNR^n=b?k&O;tEeph+}|(y9eq zYL3(w7e5?$^iEXXcah)}2VqKnp(>t#E;G*L=#;f1JLvXrGGqo@1S^lcI%+xFFKl8T ziytnM$?HWV-(e&221q3WI4xs&u}s<-kyr{up7cQX>Y>LQA$d`5r4EeFeN zIsJ!08*A&PmfWkZk#~yWnQdO|M}x;mG$Ep4m`Z%QhiTUI)k%Gqvt^b8-JF;6T(#z- za^l-YI5s}<^3Fr}f~y@-DFdk1I($R*>#v_xx)Tz<*k9302Rylvj7O`mg((u%#N56> zoZciv_0=MAcwB_bs5ezp+SdA z{9)W9!WncnkF&SF5<469hhb|V2>6&Fu6Mz*za1DdIMI#rmX`~(?AjRNtlGYd>7>uq z$|u*yQ4C1V<`lNPTI*Y60Hc}i$H1ogk{DNES7F-=isisbly;ph&m5dB5?q$o)Q#5+ z0G5Y7ZROx8l1G6obH(&K3M}_BFqskuQU;pD;}Wo+s@8E{Vugip`vRf*!rVk3XB;l~ z!f)xB4W!G}^!LayjEvB3Qh#&EG~6*~ojy+7O0KH%M1G1XdPC^kmD|fHbFdsOIkud+ zTc2N?1~i)Lh7_<3XT%PYHqGQ2C0olmvRyQpx<;+O@0$^f$YYe1k!a)$e|@K_l}%sj zET)t2XK?$dXno6NO5xR%x^oLuvw4LAnN4qc6&*O9GU(UjZ}?VDB)&Nvy+5m$qf?r2 zyWGoo^mBP=y3AgGU8*R&yu`a z40dAc+OGAMMNI*O04+zLMy0X>NHO0Wws1;)4_xgBMAue|5!trf8istCWdS0S2riGI zF6QP1w(1v73XpH5Z$Le9_;Zvoe^ivE`Bk>}0M58f^<`n;l*z{HivW=qy4*qC>>P2UV1lFjZLssF)~Gi<6EY_9=U_@+zvGVo_cP7l}*v$&1HB- zw`=PW25e^nkT#EE#NT?ewLxZ#cHvq(-7e=bj)Qkf{2FPK7hsD>tM7j*)6f7R7`UZw z#?hXmd(>-Q&=6puE6cHmL$bm2~lLb&Jk+wv{_?-fG2} zH0|DaD^z>mz+yTMq_3sVq0b=n6yBmbUMoS!)#H^AXJ60f^TU{Z??J)-%k8%y*?}=c z#yw15llCX(yUgxQc=p-F^HNS-kH?(eyNL z$v2tmM|HqS;w$I{1S)g12kX23@Y0-$Z9+8+{RkNhm>6xgwFD|Q*{085u~ZFI4%|S8 zgu4S2QUBwRaPWTi0w8E}W;DGB?v07hg}(vk9b<0QYFFxbm+&ym2^m@!mrFt}NWM|D zZbQA?P0s7AL16ebZflH@Zx79JC!$Uin>+oHe<)cDL^Y}R^5kkccca89yeUn?P%e|~ z-LE#rrbsGDCpR!JNQ6P|BsCy1hI6NwNA4}C2Z`W9_qiz$)Ctb`PT?V>XQhN|` zKx~wM!z3n=%+fYF$bBdt@8ar_0JK*H_ z&j5x}St;vGnxa#i(y{sGNW@KTB{OKwHYUVJ(Fh57{L^cZf`Pm;H07*&dtFg=It`-_ zP&0LO+!E1fUf#3xz4FM5Ma0#Rh9fCJF>(1e)3XX7xsI1A$8uAyZJR?Dl&MBX3YatR zVG1Z?L7_cQgKr`MeA%#}9ztZNgtFq)$@zTKf@>%Iq#WLXKs|3x^*t&zhr5fO-zWVW zbOdrI#aW+CRD&`tO>hzim5wLDN^LEyLLF#Uv4oxXiiAw5jl@Hw40iwnMm3;dRI982 zL*=68W^*mPJ&Ru|1e#O1*crJWBrA&b{{ZYM#(;BciwV-I|66b+u-}_wfYDDCeAeVJ zHfv!J_T)x8%QrD3^jg=)+4_xZ7xNZ0Ml}EE>MhwuQ#U^$w+u8arA8O|Y11We)^d7X z$fo#DBr4!an1{u4GQwjAIMY_nDo__uHjWNE70BRbIJ;DjAF8KwdX$S&SMM`zhM~=W zk-BS<1u7Dl=j;dooW;G?vqQ~}30b1mTG1m_3jF*JrT7~*p^R=i0Ct%TYI=lcAyHTA z>7|ArJdA)ai*w*nDELsyJ323V_gL~w`Ur zn#-UI_k@{Jt@h!MV1d$K8rJs@8s_cC+aR$EFD^rvwgtfD9Yjptuh2ZOxDlm)@~;V` z@wV@J{vfb*+r%C__gF>=*+b*3;9;_Pk}D6@Nc3yrnUX zSD-s^0Wxp|9XJ`F8o(`^(8Hlf)&s{X_3FB5-0LpRt5)=AXI)SL)>!Ehg^CUH<1@3E z3{^jtjJaDPq&)bxf2@`k264OsL3*)=eh0(xbo=UTx?hncs^>RxS<_DRoh-@s$jE+& z-5@{tw>?Of0IZ8Uzr=846JT8}f`v4tECIU24 zT`aRadP1%0s%&%<9pE<=%NH3wg!Gq>wAftxG@eQxwBK}5A0w%_g+hp+6{&w}YSRvh z?0TxS8W_W_pJj2prqxy-C)k!+Y^B+X$ol~93b`^n;y}rINYzyaM0tbq=&bVKdXl53 zT+y~yg`@))oDBNBb=b7kf!_Qx26{}AeLQ`~{web1xYYYP2e8;ZV4YK(Kpy)5X$9))didf@xYi2h=x% z?gnYu)eDKAo(lc(YR7LF=cWNS_P$ZDlV$ej>#YT2WJUy!v}J@848R^OCNfM6z6wc0 z$!z{ae#ibXvm5AR9}~IF-8j^IHVg1CKcNN*hv31Cm{IC{P9uzz5H<^X zVEK)G7aTF*06Pv~8(T3FgKVP0E5k;z!-{R+DF26eD1{%?Af4Jqod;F_{j zxiQ@;dh2O!gcg!Vir!5jV%^I4_M}&oEqUSF??A()mlUo&reOh;ucbK)8MiUlTD!ty zUDC8+@2vEK#HtiQ*3=*%io;t)Hnjy_1J#VUSnw98JATuA!S-s7u4kcE-&Yze;~!Wj z6)NMOk}4zY*7EzkuDpAiKwlu1!Fd4R5r#e$p{ko@dueCa|0vozfx{>fjo$!TCujKj zN;q4!;Kwq^K_QfaJKYaXk~}UzV^2D!e7kj=l~P37u{(=wR4%hn^y=rGsINdS-lDNQ64IHFr%zX#auH1CraCxsf zfjCm)JwTP}%4V_o0I#4^0+{9;Ju4*;BzM@xlh7nN!a~y zW+E6P5_S;K{mKJF#h#o=*r;1mx{+W-Ur*@w4vM5x3tq-P4 zt(U$w%Ya(S1+pcK-bNCC> z@`IwEiZWMf^z84ZBBF&_lf9bO`*8sXu2w9EK$&jjy9L{*GqCMC>oyS44PJ*Y&L(nF zY9Vk*=9E}TDOmQsD>GZK+EaYu8{dRf8xp4+9m%%}Y>gHV(DhZAEz~@im3l=?g>F5G zPDK$av;^i9kc9B3ILd`X_6~223vf;+xd@E9h-<8YK7AuwI_XBL{dQ*96-0K|z55a& zTNI^Wyu$ymyM086Z3Z|bNoGTX28+vytpw?=CsS)<$eQg0%&oPwL@ekbMW+c&iJc<~`%0G(Da`6S`_o(N7}CXN~2O;mdwYgSO|yhTnsweB3Jv zxDTT&`%f>9=GH!<*MVg!{qJ(J`Cz5B%WRHj@b)$}ONa7{ zI6D7EBhQWE>r{XF-EBepI2!;F{h)@-sxty!x)8n8If0Gic8t?Q6s~E7dmN2U50KMv zAIFnbw?>_jw{acCE++BZh_|{YukOUp)>3E15miADt0aB=0V5T(&w4v)qx;z2P*ciJ z-74)$IXvGcE?veWy@->(86w6`7Yb;gG+pqncZQ8%`hfzP3gAsNBWEWJ@9y>+k)6#-e)a#Zt8HlzbI|?jMUkVKqzzzrCuHOq;@R(Y9 zsJl|6JKrTfz=oYswTTk9fx>3+)go=QYXd`*c`NFBL&{#-H#`z;U_x{l8}5I77N8fR zKpR$ms^G`E|Gl0W5wUcWo(K}TjGuSP-h{wusI|_s@-;l^2kpBPJpTq;FT&DVw)Qe- zzqgxa=eWm|u&-BDW$K&Q6~Ky6E0A8MefdNP>AzlWhL;!RQ8s=LhfCy8$c(5TgSmSf zS9Uq^u>8RVdfbQw2bx%MQYsK$wkfrLSk~WH(%z(RV4FJikIBQ{04XaJV4kiMb<^oL zU7Xv?VuP*pu-~T~Oi*jpv4QX3`1Z~(qRp@hY}9^w8g9r3Qow1LYC3uE+*yS4DZIOS zw!(%ZmR(+!A*v+FN64$WFh@wzYTrC#XXj>OapzVZAeCfj6%#v`^azW@#}4)#%$&zZ z!j_FftCoXVEO&d`2M=<}O_YX`Q6-1j4+n%;&8hp55wPH?j$_7=VGYKWgGshNZCX`) zLm?rN9QKQP7C+Yqo-s!xg>2B?^qYY%lT(un-;&ZlT@U0MEOHv9Ho-8PaorcVQ!7E(>+$@Ux3Kv~@G>hUg@ z)+$QP&}o#UmxTt zP@o{Xs)g*Ry`X3(W;_iW35(mQ)=LV#e5^=m*~Tx6eSIO|ju!>fOr9YK~U59Yr%%N3QSVZCWnR*&3wmcvow9bz@{^S zC9Rq36#;p_QIfpBG6OSi>W6dpLO?OH>u$Cc>3&)tr&+L z`UP05PmaKjBvW%uWym<4GFL}wdh5O1(H@sQ_cgMZh(Yg!;=lwV&eISF<>xSMcJEmRl6C1Qs~e5O(SCsGQ*F}DBcNNDZ!KYkN<=}2>rJav>vQ= zdg0{R4u)1%O3(18<`Uh}FyWa`By$62zc}kQkS|y9z&lsWNAS6Qt#^Lk55Gu*@3 zTuDfV$j6=BYa%*nPYZ&#C!!JB_xaCkK=h z&Y^Ke&Pqu*-=mBHfO`Uo*0_*omIswf>V-}6%zEjc^<~m4%d*3^0~;) zLf@|%ck#scj*v3mH0HNt_sN(Y(J_xFC7H8Brj-!uG1qA7%yL<&@M z>py-1AVgOJeN;-#Z@$bjE;!C9fsQW;>=*M7L z`ocw9Z95Ta2#|`XpNceG>`q*8!~bdHtmB&c{yz>(M7pIxagx#vBb0JT2uO*9A|i~C z?ov{b=}>9#BPBgrLZlIphJkbpP)3Xx9lwL$e}6vs^X_`jx#zs&^?KgpSf;1BnJw}{ z@*}#S=doJjShHYfy`#So1($x+Yc(hBh6i#$IbJSf2@Yy#NV{L_;yHTLT-~JKmfEVl z5OFM|hT6&S3v(`-FWCvF6bj(8E|T0OU-YF?ZoeCejN6`RK(`)R(2!fH)XC{_%=eQT6?u=Xa50E`-2V}1|8(_xlZsB# zWC+I%kAmi*N2GUq7ry>UPkSpfHOf?o)$y@!urRH=z49slB8*@(U00dq^Ak&@7MA|} zu9{WE{+(x2@tI)Zl(1#P3{9~h9=-tC+-)=a%JV20`cU?PMkAvh^s2XvsMjEP2vAWD zfsnRcb;H`3;cfF!)e`KW0f|hE+)Df{3%8Dwc|@Q>#|xSM0B!}Dr;V|2!1YdBy%w98 zs&i=SCATLhW{yV<&SZHq@cA?hMqvwVB=F~&$Snou9FABIv;%NSQ(WZj>b3IqW2Fd_ zXQ#J|%5mW-A!55?^~*0zcJ=ikea!{XsVfM3vzCj9VEj~sR%Pb{lYYVGkVXTigv~$- zG!q{XMw>XH=u}CM3(IpiCsT`X`x* zKhHuYy|$j~!h4-;2VUfqDzs%aP*(@i7TWMYbbJ!^UwzQqhsqROLA^mo)vX%C8Mg#o zUwKYu4-vd*^W?mZ0OjbVxh`F&X?$+FCfOk@oZogY08I+XSx)MjTJzgtt#hAi79=&z_%t6 zD>Y9b0wHC*nqO-wW3={hk%>tSkqsoI0B^wsG(D|iG~a?jXThjRgf|A>!tBRY>6msW zwZpX!rtmSr;DurRC&uWE2`6Dr3ICNIaf9y*D_Qo{`Y1O@E`%{G(+dpAsCKA~Zvpps zJ?h*}GwDJ1f${esegZ<4;A#&Tg*uhU-DJVP;QUE z2??3HT7`32G$#~tcNU}-&fO2Yi+W+UtHo}|9Jcmg>s)v#?%<*l2zr_NjQWw6yv#>g zy{VD>rM7|L^pPj-=GE0{f|M_R1@Ye%cb@c8XEQx~4>vdQ)FkGG3sEEDv8+W8t*nGv za;}oA+&HsQTJWskgasJWOe^_~9Ki)#@05x4fy~*__)HJgxt0o@4Xa8x(UXRk5WN@5 zzk*0A)NC*9fPQVzdEGPjWsjSWYTHQIlH(pq59(Bo?-2*c%7Lw8(%q5j%}r(V9*;bd z)Qgp2M98r-p;6)f6ILbMC}_%6+Uns`_@IM&cj#;5Jz8JXZ%c-izhNAr+#&Pn`Ao&C$DL?nV}C;PL!kuJ)4h)!{+l?u zeZ8$RpEoWESP5H346%VRGbd1%EiNLCFs4UxU9)1Km0O6%1Df*$N0WwwlBp0lj^9pivwB#h z554S@h>?kC$LQh&EJs=VC$8AOzxQc1u(lvzqf}jO;L$elYki$P-52b{7{WQ-b)2Xntg5s%Pw7AIn4i~ivo{vkzTH@Ijf_66UXPV{ zq9eGYLS2?OUJc(Cz|&){8@w5W0u%;&zt$?sQ>`>kwDtIzYP<>UJ?HXL={^vVV29K3XD)RY|hY6g(Rv=>THl{6zd+Qc>B%u8+~2IiNkB=hO~;5`6{Pw=JS5aO>ODXHojk0 z2cUJ(^!n0YPBMVwSkX=RZNxPG zre(jpQlKdWj#7|3ub=}auH9&EjFl&sVG(tBR#>7?0GCh=ShNa~kbq|+T)}pIc0f2J zd0u1cD7PTn4#KkQ6g@kpPZ><@?$QXI+FW0*a$#(Ij!OwE!2fu)K>x?pkA~+ zJ+i!=k+fb@5yIRgYs)H5HV)no@RVbKm0F0X|D`lMoHix^6Vmo?!+tMqjq`8s@9b&u zDJhvC*KJF|-Je$n-u)B3hSorrn!OC%!N-cerJtvuVj3a6oe|R8Kf_?@iwcc5k?|G9 zWY+=@n&Afj@I0HGc%DJcp5u+MTUB=#lEPGo=H?N)&}K+3oOc6-$Gb?Mo%fYrGFnA!?Xopt?IS+D0SkB3$x%{wvVgi-d` zt(^_lelaRZ`;d2UCtaooP9-ppI!Uf&Urb{@(~NE_t1m)-P?KvoQ*ItwX^4*@Rp+xQ z7F?;$(=o5P=`=Q2?ry7ur@#!B2MUK6fYE+j+}=^v3&>2A?TYAddV8bB*(&0%8(9*; zn(bJ}<@k&SZ01m^19m;s8xjp6%kM#XHM8v&`ziAJW(dyY?U=49gFA5n&7B22H+)>L zOhtJYt-WcYnI;E1Vh&=c$ppW#cxW)%>$_));A}>oE%6n~X|Y|{0%Ek2CI|au7 zr*h`#NMpv?xy7goy?ufG-|qCbwFd0FHaQaA5UHk)_3C)`U4%12OjEI>6)?V^xz8?ed!LLO{wTg#)=Nm>)ZOM!!5)An#&Z0XnAr0D`R|VgmKET zB5JHIAN(Mos$>4p-SPwfv*Tni(wiZZe2ORLN{OAm**!gKcy6S?mV{f+}!2 z7bEYrM4zC*lPD458U_r#^^z1N2C&kvPLnH`v$vokoMd)NzCRW!Di+qRWBMM9Qx=h$>)x2$nV&dLK%8q!^uy}QD`SKz1D}h!YDd+*LST$D^r!9q&ACX z=$~g{4qOIVFZ>Xou!Hnmxl??v%+&PlCCp#_n1E{@BY`+?DpwPlnuOGV(K*xrsjk!yK$6XzF}$g{vg>qEKY#xzJW; z^1fpXSVtD$2dmXHbZte%uO@~2T^1 zTrM1a@#>(0Z~vrv`~2W$LlO~2*w3r|OcY*qenfk@w3Qs0*oyR~hszi?lJ2;H!;AgJsEAOQiZ)U`ssX^yPv2q%saBwd`(t)c8iLEYElt?Ci8*? zADN*Ag40LCIq#=?FeSvbsKthqiOc_aB#f!tvYVK^t@6kThj}g1#9HKmo7|OtO`=g? zM^@!6iZqq-8x|;Qzyi-wI>&9qa`G6opPD;B^+5_h(g0E#(|Lc`FygyE(wccJAYq z%17tOPgU;x{%jcR!F}7Z47Ge$=?D|`3yB{vc`F$h=u&Ko&w6PH>(i4dqBmwA8LQmF z*R1E=`a8bI)L+3TT|_?i2*6;t?~)x23l684uU2jxWR0ytH7eBW zP!`gd2i|(@-%@>^C+|Ji?9ZC8tQu6rI?g9v*IBnrp%mv%dO*)R0T-2d2@LQu*|*b# z#x!&B*|z5v>7`PxEuYdQ0HOhq*Ua=4uZXbHu+A{K-L_cp7$Ts2HIfI@{c2Ph2DX@Q zBh}7GD?io6)f)M(XMSqS2TgEe8NJK7$^ z$MtuO*YM={D|m&Je-E6Mp?%h~zw)-L-~ExPRjIpyds?JTl;7G-)mc|`F~eO>bBHc+ zhmgDmnCoJI?7%Yv%r*#z3i0;}a5An33f%*-ISuT5PN|Lv8NymJ+(*jUclp8o0yQHm~n(v%0HboEPh;K8nYIb|#)tF#5`flG_) zBDJM2+Y|{ojAt`kEq#5TyQ+AsjI1zZG@%I@(5=CAZB)PhnP|W8zWuqf*qAca-BQsp z<~OYdN>5-rDVe`{F8wYWtG3o=!?R(Mu|0!PYVRRK&;aI|y~u~b`d>4_3CQ`PV_3?R zTL%qshEk_N$63W$T7_XOXrF}5;o+NKxbIet0s|-6n-aEfnui~+oKAs;0`czq&1!-o zFX&G-aAY4-Age`H5lZD<iF~;@(0$TM|3j2 zCx!$UrW77YK?8_HjB%`$msKwtss7ck%Bjr>}_GUyA%(NqZjLx<<6 zM_{QyT3e<5(rms3B!O#@(>#4_N<*VSo3YsOp_5*%RWmUf;zxmdmzPOkiWN|E%65l% z9__+}UFhbDbut4%vkD&EZtvLM$L&fGtsjXWPdZ%m31x3MG?}FI-;GCEsthI4{-E=dNY(YRDyL-`$hPpYFl_^PnRKXYej8A3pv?xgUoP4O%)?f0;PUaoQI zTY3VvFkCy_Uro&b3Br_&B8-oEJ7W-F*AmZ|uORQ&cD1x$2cK2q=gZOQfVl^;lAX+X zIN=6Zt8e9S+-{3AEA}wAJHn`-thtg?chG>?b+kzOtW|(Y%WjT-PQ56=lowpwD+^fa zw}F|7X9eWFD1uZ<(+o;}Q;b|{_qUPkI=Z-rzO~?k5*KT6o+ikD%qh|@Msef3i?Jfr#fND-AhEp!q7-XUTWE1RqRaa=Q0-% zuf#5I5I2(O5VAp`fn`Zys)KDRCx?cn!&cV2_I~S-xX&C5{hDUHXL0U1(pO_m{i|(4 zuSA)P*CZwi?L1UDu$1BNwqftRopy>^uP8ni>{gcwx^*Ji#(hlVPw|-A?CqsbS^wVs z&+~%Bl@}p0Wpmq#q$SZ|bzR%FfDQ8}w1Uz0_S0H^@kxhs6JsE>#BPv4Ld3;ze|$M4 zFZp1xzr6R=crM^IG~bO`NbV4P*n{lf+v!WZ4!Y<~66+TBl8MiGVA0Siyy$P;+E|>S zWS7$oWS3X5Xt4tyLP*V$m)kr1DzPSO(wgHds_2q|i(W8Hp8)1PDd(EBM>T+d9{Oo0 ztgA d`Bwc@yEH%pRm{x?ZqCl$k*RDvpzQ>vUzk4w~I~{V2To51H;Hvy)!|rEv^D zfMCKyge1-+GE`>f1y~YSaGxGR0aUnUX;j*&Sp+s+MjeRzMSj{oxrUI{j2W-!6Qnqa z@a8+TYzisTd&z6m`aT<41)$=ta=!#Vf$B9Ol^i(~_))Y`s_$RkUth8UTrg!GKd zAa-J}c_TLSr2v5THH(eA@pISs*a=Vye&+sbg(#>vDax|%BnjMwUc7m_-Bm1{M^4-U1VWBf+6YO#alGdT zz?vkT`Md)mTo0v& zid-)^(GBql}Sw3J#ezG4DKL)NpS@wy&NIu=p3=hYCw*9Eqk2dY`u z0-O{O{qI}=0I(eoa(JFilxlahj(g>5i$4XRRMLgH5CDQ)9weoS##RV_u`}pNf70*6 z&c?x&FFsduZSI5W6qK4tfFpH1?k;}ioxSE)lvDe7+MnJqj5DOaRpyJ^UacIV+mBPA zXpQGIm^i_}Eb2lVS6z>zj~|z`W+s!h6X{8hl7VxNO|!f|&xP-*i|@(09pgm%lAhlg z?hYw+!+hG=6((K|GSUKGrG~Zyo<^LVYa0sUBD5pe%pu@-|EJ$jM90TN74TY%_Fw8I zao7_6NiIaHw&^tTJmbYmq9AMC_^=@n*ICl%*A&d`1p!r}mPW!H-C4nP9_&JSSKEVGp5?)4yv^Vk-b9MK_= z(J?V3P<+#$D6}}$k|k#<0bC6FTPw4^ zR;iPQ@A6MqZmzcosczGujeX5Qar5zW6ArJ-k#f9`(G;wK9^?cA*RgeYh63yM+a3o! zWGI?1@I^JP-7zZ)womjk1_^=haS zw@Y(s8N*h}bV?EYwICW9u~8tDtc;_v)&m$9Nkik%ZiSDqC&I;vy?h_~~%xP|kv znbT-fi*|R;&{?Ze4%fdW#=k^3BIUJT>QNi(p5GD_iTe^m5zF+t{EN*WHN(p4$~I_? za~(6gQ;(C5`3F~*gDgX**Q%(r!CRBd)l*2N%s}6xMOegZRUh9_-TF+HDOI1$6_vE~ zz;8B<{R;j2wmg@o;?r>hpuRFBinUsP|Dn>pEOh%|>H`JjFTHi&7HdV#-q(ttg{df) zCZ;u!Y)iWnw-f3IM3GJI)QcM{8F`lJB#K zclW}y+67v19iPm_+FeB~qyO!2s7mh4Z`HsZRC7->)wVv4fc!RpS5wcnHC{Qr@J-{Y z{L1AwuzhW1Q|7>WbR_MVZQ}Cl7=f_1o`}K5Uqhs4IqH}FdHxVI;|`x#dQX-+%sMKK zj}NVX+oZXC^|6~x`yEESs88VUAJSf$*`+r}phpLTjl02!Xcy@Y-Oct+b!w66dIVA) zv3ylFG1Ha8>RUVRK7d93W0gMUM3bA-a1Os?rnkT5?$bYxtsEl#Te>UhSLqk`TR!iz zzow?%T+O|BYSQiS<1bTBA)FmE48rP57!bIiXMEH1_fDP5Lan1_Ib35=`kjBz{Js4H zL$(sRj`c^#CdCv~hy;kZ$&9|R%BO^Tj|X?16TM_F;Qd0x;9%UjXYjjx{QObWtkUdh z^Jad6$#H+_w8f-p19q{mB2gjZas$<`_$QuRYYAA*rm>CLi&Gl6WUqJPL{e4pT~whz zmOkmdV?v!+SPdwnj&%FA9>^DrL#CG^WbBfU<*Z2&6WWhba;;o$-683DGs~swz zu7mtg#1-z>B>oEg@x75Ov8Pqv)j?w*``Z1bXX3LEV(Q!U?{W9fjOE^X3RQ%Vo7yK+ zTHnqSLX52)lj-*KZj24*)F_OP8uGm~lk(}>yc;aV%PSXFD`o8IQ^Ib9eeleLOgK8U z=DYY}S#m-`t3_Wb*$M0Ix##U0gYKz~)b#H;etoWlZtheJMKy{G-<0`A)rX@^~7sN`^-?d3ilRyh+1orbqePl^xo$*; g_y7Ml)OkkAuNYrE9d6_UPVXUtYwBy1+_8Q6e-rXtZvX%Q diff --git a/docs/assets/ruff.svg b/docs/assets/ruff.svg deleted file mode 100644 index 4494fe78ff..0000000000 --- a/docs/assets/ruff.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - \ No newline at end of file diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000000..11835649c6 --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,69 @@ +:root { + --black: #261230; + --white: #ffffff; + --astral-purple: #1f092a; + --astral-purple-alt: #30173d; + --light-gray: #826894; + --radiate: #d7ff64; + --flare: #6340ac; + --rock: #78876e; + --galaxy: #261230; + --space: #30173d; + --comet: #6f5d6f; + --cosmic: #de5fe9; + --sun: #ffac2f; + --electron: #46ebe1; + --aurora: #46eb74; + --constellation: #5f6de9; + --neutron: #cff3cf; + --proton: #f6afbc; + --nebula: #cdcbfb; + --supernova: #f1aff6; + --starlight: #f4f4f1; + --lunar: #fbf2fc; + --asteroid: #e3cee3; + --crater: #f0dfdf; +} + +[data-md-color-scheme="astral-light"] { + --md-default-bg-color--dark: var(--black); + --md-primary-fg-color: var(--galaxy); + --md-typeset-a-color: var(--flare); + --md-accent-fg-color: var(--cosmic); +} + +[data-md-color-scheme="astral-dark"] { + --md-default-bg-color: var(--astral-purple); + --md-default-fg-color: var(--white); + --md-default-fg-color--light: var(--white); + --md-default-fg-color--lighter: var(--white); + --md-primary-fg-color: var(--astral-purple-alt); + --md-primary-bg-color: var(--white); + --md-accent-fg-color: var(--radiate); + + --md-typeset-color: var(--white); + --md-typeset-a-color: var(--radiate); + --md-typeset-mark-color: var(--sun); + + --md-code-fg-color: var(--white); + --md-code-bg-color: var(--astral-purple-alt); + + --md-code-hl-comment-color: var(--light-gray); + --md-code-hl-punctuation-color: var(--light-gray); + --md-code-hl-generic-color: var(--light-gray); + --md-code-hl-variable-color: var(--light-gray); + --md-code-hl-string-color: var(--aurora); + --md-code-hl-keyword-color: var(--sun); + --md-code-hl-operator-color: var(--sun); + --md-code-hl-number-color: hsla(0, 67%, 50%, 1); + --md-code-hl-special-color: hsla(340, 83%, 47%, 1); + --md-code-hl-function-color: var(--cosmic); + --md-code-hl-constant-color: var(--radiate); + --md-code-hl-name-color: var(--md-code-fg-color); + + --md-typeset-del-color: hsla(6, 90%, 60%, 0.15); + --md-typeset-ins-color: hsla(150, 90%, 44%, 0.15); + + --md-typeset-table-color: hsla(0, 0%, 100%, 0.12); + --md-typeset-table-color--light: hsla(0, 0%, 100%, 0.035); +} diff --git a/mkdocs.template.yml b/mkdocs.template.yml index c789f2482f..ec7dab6ced 100644 --- a/mkdocs.template.yml +++ b/mkdocs.template.yml @@ -1,7 +1,7 @@ site_name: Ruff theme: name: material - logo: assets/ruff.svg + logo: assets/bolt.svg favicon: assets/ruff-favicon.png features: - navigation.instant @@ -14,14 +14,12 @@ theme: - content.code.copy palette: - media: "(prefers-color-scheme: light)" - scheme: default - primary: red + scheme: astral-light toggle: icon: material/weather-sunny name: Switch to dark mode - media: "(prefers-color-scheme: dark)" - scheme: slate - primary: red + scheme: astral-dark toggle: icon: material/weather-night name: Switch to light mode @@ -50,3 +48,5 @@ markdown_extensions: anchor_linenums: true plugins: - search +extra_css: + - stylesheets/extra.css diff --git a/scripts/transform_readme.py b/scripts/transform_readme.py index d2afcdf1bf..963f55ca3f 100644 --- a/scripts/transform_readme.py +++ b/scripts/transform_readme.py @@ -8,8 +8,8 @@ import argparse from pathlib import Path URL = "https://user-images.githubusercontent.com/1309177/{}.svg" -URL_LIGHT = URL.format("212613257-5f4bca12-6d6b-4c79-9bac-51a4c6d08928") -URL_DARK = URL.format("212613422-7faaf278-706b-4294-ad92-236ffcab3430") +URL_LIGHT = URL.format("232603516-4fb4892d-585c-4b20-b810-3db9161831e4") +URL_DARK = URL.format("232603514-c95e9b0f-6b31-43de-9a80-9e844173fd6a") # https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#specifying-the-theme-an-image-is-shown-to GITHUB = f""" From 84259f5440d3f267af0fa5348632d1ed83f8ca42 Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Thu, 22 Jun 2023 10:25:20 -0500 Subject: [PATCH 187/447] Add Applicability to pycodestyle (#5282) --- .../rules/pycodestyle/rules/invalid_escape_sequence.rs | 3 +-- .../rules/logical_lines/missing_whitespace.rs | 3 +-- .../logical_lines/whitespace_before_parameters.rs | 3 +-- .../rules/missing_newline_at_end_of_file.rs | 3 +-- .../ruff__rules__pycodestyle__tests__E211_E21.py.snap | 8 ++++---- .../ruff__rules__pycodestyle__tests__E231_E23.py.snap | 10 +++++----- ...uff__rules__pycodestyle__tests__W292_W292_0.py.snap | 2 +- ...uff__rules__pycodestyle__tests__W605_W605_0.py.snap | 8 ++++---- ...uff__rules__pycodestyle__tests__W605_W605_1.py.snap | 8 ++++---- .../ruff__rules__pycodestyle__tests__w292_4.snap | 2 +- 10 files changed, 23 insertions(+), 27 deletions(-) diff --git a/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs b/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs index 4bd8d52ed9..792ec845ec 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs @@ -108,8 +108,7 @@ pub(crate) fn invalid_escape_sequence( let range = TextRange::at(location, next_char.text_len() + TextSize::from(1)); let mut diagnostic = Diagnostic::new(InvalidEscapeSequence(*next_char), range); if autofix { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::insertion( + diagnostic.set_fix(Fix::automatic(Edit::insertion( r"\".to_string(), range.start() + TextSize::from(1), ))); diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs index 385ea9efe0..1c50f85e35 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs @@ -92,8 +92,7 @@ pub(crate) fn missing_whitespace( let mut diagnostic = Diagnostic::new(kind, token.range()); if autofix { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::insertion( + diagnostic.set_fix(Fix::automatic(Edit::insertion( " ".to_string(), token.end(), ))); diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_parameters.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_parameters.rs index a65220fa74..3cb6c0694d 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_parameters.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_parameters.rs @@ -65,8 +65,7 @@ pub(crate) fn whitespace_before_parameters( let mut diagnostic = Diagnostic::new(kind, TextRange::new(start, end)); if autofix { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::deletion(start, end))); + diagnostic.set_fix(Fix::automatic(Edit::deletion(start, end))); } context.push_diagnostic(diagnostic); } diff --git a/crates/ruff/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs b/crates/ruff/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs index 58bb6d12c7..ea876524e4 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs @@ -55,8 +55,7 @@ pub(crate) fn no_newline_at_end_of_file( let mut diagnostic = Diagnostic::new(MissingNewlineAtEndOfFile, range); if autofix { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::insertion( + diagnostic.set_fix(Fix::automatic(Edit::insertion( stylist.line_ending().to_string(), range.start(), ))); diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E211_E21.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E211_E21.py.snap index bd71617569..594efa073b 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E211_E21.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E211_E21.py.snap @@ -11,7 +11,7 @@ E21.py:2:5: E211 [*] Whitespace before '(' | = help: Removed whitespace before '(' -ℹ Suggested fix +ℹ Fix 1 1 | #: E211 2 |-spam (1) 2 |+spam(1) @@ -30,7 +30,7 @@ E21.py:4:5: E211 [*] Whitespace before '[' | = help: Removed whitespace before '[' -ℹ Suggested fix +ℹ Fix 1 1 | #: E211 2 2 | spam (1) 3 3 | #: E211 E211 @@ -51,7 +51,7 @@ E21.py:4:20: E211 [*] Whitespace before '[' | = help: Removed whitespace before '[' -ℹ Suggested fix +ℹ Fix 1 1 | #: E211 2 2 | spam (1) 3 3 | #: E211 E211 @@ -72,7 +72,7 @@ E21.py:6:12: E211 [*] Whitespace before '[' | = help: Removed whitespace before '[' -ℹ Suggested fix +ℹ Fix 3 3 | #: E211 E211 4 4 | dict ['key'] = list [index] 5 5 | #: E211 diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E231_E23.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E231_E23.py.snap index a985571437..34bd1a6274 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E231_E23.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E231_E23.py.snap @@ -11,7 +11,7 @@ E23.py:2:7: E231 [*] Missing whitespace after ',' | = help: Added missing whitespace after ',' -ℹ Suggested fix +ℹ Fix 1 1 | #: E231 2 |-a = (1,2) 2 |+a = (1, 2) @@ -30,7 +30,7 @@ E23.py:4:5: E231 [*] Missing whitespace after ',' | = help: Added missing whitespace after ',' -ℹ Suggested fix +ℹ Fix 1 1 | #: E231 2 2 | a = (1,2) 3 3 | #: E231 @@ -51,7 +51,7 @@ E23.py:6:10: E231 [*] Missing whitespace after ':' | = help: Added missing whitespace after ':' -ℹ Suggested fix +ℹ Fix 3 3 | #: E231 4 4 | a[b1,:] 5 5 | #: E231 @@ -71,7 +71,7 @@ E23.py:19:10: E231 [*] Missing whitespace after ',' | = help: Added missing whitespace after ',' -ℹ Suggested fix +ℹ Fix 16 16 | 17 17 | def foo() -> None: 18 18 | #: E231 @@ -91,7 +91,7 @@ E23.py:29:20: E231 [*] Missing whitespace after ':' | = help: Added missing whitespace after ':' -ℹ Suggested fix +ℹ Fix 26 26 | #: E231:2:20 27 27 | mdtypes_template = { 28 28 | 'tag_full': [('mdtype', 'u4'), ('byte_count', 'u4')], diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W292_W292_0.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W292_W292_0.py.snap index 90f1c0015d..7b67a4f6a1 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W292_W292_0.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W292_W292_0.py.snap @@ -9,7 +9,7 @@ W292_0.py:2:9: W292 [*] No newline at end of file | = help: Add trailing newline -ℹ Suggested fix +ℹ Fix 1 1 | def fn() -> None: 2 |- pass 2 |+ pass diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_0.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_0.py.snap index 60999dc01a..fb5b4e8950 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_0.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_0.py.snap @@ -11,7 +11,7 @@ W605_0.py:2:10: W605 [*] Invalid escape sequence: `\.` | = help: Add backslash to escape sequence -ℹ Suggested fix +ℹ Fix 1 1 | #: W605:1:10 2 |-regex = '\.png$' 2 |+regex = '\\.png$' @@ -29,7 +29,7 @@ W605_0.py:6:1: W605 [*] Invalid escape sequence: `\.` | = help: Add backslash to escape sequence -ℹ Suggested fix +ℹ Fix 3 3 | 4 4 | #: W605:2:1 5 5 | regex = ''' @@ -49,7 +49,7 @@ W605_0.py:11:6: W605 [*] Invalid escape sequence: `\_` | = help: Add backslash to escape sequence -ℹ Suggested fix +ℹ Fix 8 8 | 9 9 | #: W605:2:6 10 10 | f( @@ -70,7 +70,7 @@ W605_0.py:18:6: W605 [*] Invalid escape sequence: `\_` | = help: Add backslash to escape sequence -ℹ Suggested fix +ℹ Fix 15 15 | """ 16 16 | multi-line 17 17 | literal diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_1.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_1.py.snap index e0e9a9371c..b6e26e233a 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_1.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_1.py.snap @@ -11,7 +11,7 @@ W605_1.py:2:10: W605 [*] Invalid escape sequence: `\.` | = help: Add backslash to escape sequence -ℹ Suggested fix +ℹ Fix 1 1 | #: W605:1:10 2 |-regex = '\.png$' 2 |+regex = '\\.png$' @@ -29,7 +29,7 @@ W605_1.py:6:1: W605 [*] Invalid escape sequence: `\.` | = help: Add backslash to escape sequence -ℹ Suggested fix +ℹ Fix 3 3 | 4 4 | #: W605:2:1 5 5 | regex = ''' @@ -49,7 +49,7 @@ W605_1.py:11:6: W605 [*] Invalid escape sequence: `\_` | = help: Add backslash to escape sequence -ℹ Suggested fix +ℹ Fix 8 8 | 9 9 | #: W605:2:6 10 10 | f( @@ -70,7 +70,7 @@ W605_1.py:18:6: W605 [*] Invalid escape sequence: `\_` | = help: Add backslash to escape sequence -ℹ Suggested fix +ℹ Fix 15 15 | """ 16 16 | multi-line 17 17 | literal diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__w292_4.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__w292_4.snap index 2b15945ea1..68d23e984d 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__w292_4.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__w292_4.snap @@ -8,7 +8,7 @@ W292_4.py:1:2: W292 [*] No newline at end of file | = help: Add trailing newline -ℹ Suggested fix +ℹ Fix 1 |- 1 |+ From eaa10ad2d9ee9d38f9b1cc690c3a5e0ab7d62ff0 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Thu, 22 Jun 2023 16:34:44 +0100 Subject: [PATCH 188/447] Fix `deprecated-import` false positives (#5291) ## Summary Remove recommendations to replace `typing_extensions.dataclass_transform` and `typing_extensions.SupportsIndex` with their `typing` library counterparts. Closes #5112. ## Test Plan Added extra checks to the test fixture. `cargo test` --- .../test/fixtures/pyupgrade/UP035.py | 9 +++++++ .../pyupgrade/rules/deprecated_import.rs | 26 +++++++++++++------ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pyupgrade/UP035.py b/crates/ruff/resources/test/fixtures/pyupgrade/UP035.py index aefd7064d1..a185c4867f 100644 --- a/crates/ruff/resources/test/fixtures/pyupgrade/UP035.py +++ b/crates/ruff/resources/test/fixtures/pyupgrade/UP035.py @@ -48,3 +48,12 @@ if True: from collections import ( # OK from a import b + +# Ok: `typing_extensions` contains backported improvements. +from typing_extensions import SupportsIndex + +# Ok: `typing_extensions` contains backported improvements. +from typing_extensions import NamedTuple + +# Ok: `typing_extensions` supports `frozen_default` (backported from 3.12). +from typing_extensions import dataclass_transform diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs index 335354234c..c2813769b6 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs @@ -43,6 +43,12 @@ enum Deprecation { /// Deprecated imports may be removed in future versions of Python, and /// should be replaced with their new equivalents. /// +/// Note that, in some cases, it may be preferable to continue importing +/// members from `typing_extensions` even after they're added to the Python +/// standard library, as `typing_extensions` can backport bugfixes and +/// optimizations from later Python versions. This rule thus avoids flagging +/// imports from `typing_extensions` in such cases. +/// /// ## Example /// ```python /// from collections import Sequence @@ -139,10 +145,12 @@ const TYPING_EXTENSIONS_TO_TYPING: &[&str] = &[ "ContextManager", "Coroutine", "DefaultDict", - "NewType", "TYPE_CHECKING", "Text", "Type", + // Introduced in Python 3.5.2, but `typing_extensions` contains backported bugfixes and + // optimizations, + // "NewType", ]; // Python 3.7+ @@ -168,11 +176,13 @@ const MYPY_EXTENSIONS_TO_TYPING_38: &[&str] = &["TypedDict"]; // Members of `typing_extensions` that were moved to `typing`. const TYPING_EXTENSIONS_TO_TYPING_38: &[&str] = &[ "Final", - "Literal", "OrderedDict", - "Protocol", - "SupportsIndex", "runtime_checkable", + // Introduced in Python 3.8, but `typing_extensions` contains backported bugfixes and + // optimizations. + // "Literal", + // "Protocol", + // "SupportsIndex", ]; // Python 3.9+ @@ -243,6 +253,8 @@ const TYPING_TO_COLLECTIONS_ABC_310: &[&str] = &["Callable"]; // Members of `typing_extensions` that were moved to `typing`. const TYPING_EXTENSIONS_TO_TYPING_310: &[&str] = &[ "Concatenate", + "Literal", + "NewType", "ParamSpecArgs", "ParamSpecKwargs", "TypeAlias", @@ -258,21 +270,19 @@ const TYPING_EXTENSIONS_TO_TYPING_310: &[&str] = &[ const TYPING_EXTENSIONS_TO_TYPING_311: &[&str] = &[ "Any", "LiteralString", - "NamedTuple", "Never", "NotRequired", "Required", "Self", - "TypedDict", - "Unpack", "assert_never", "assert_type", "clear_overloads", - "dataclass_transform", "final", "get_overloads", "overload", "reveal_type", + // Introduced in Python 3.11, but `typing_extensions` backports the `frozen_default` argument. + // "dataclass_transform", ]; struct ImportReplacer<'a> { From f9f0cf7524707cd0099e4088b8122540b20b34bd Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 22 Jun 2023 11:40:16 -0400 Subject: [PATCH 189/447] Use `__future__` imports in scripts (#5301) --- scripts/_utils.py | 2 ++ scripts/add_plugin.py | 1 + scripts/add_rule.py | 1 + scripts/check_docs_formatted.py | 7 ++++++- scripts/ecosystem_all_check.py | 5 +++-- scripts/generate_known_standard_library.py | 1 + scripts/generate_mkdocs.py | 2 ++ scripts/pyproject.toml | 5 ++++- scripts/transform_readme.py | 2 ++ scripts/update_ambiguous_characters.py | 2 ++ scripts/update_schemastore.py | 1 + 11 files changed, 25 insertions(+), 4 deletions(-) diff --git a/scripts/_utils.py b/scripts/_utils.py index 23ac16d57d..7871be6cd9 100644 --- a/scripts/_utils.py +++ b/scripts/_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import re from pathlib import Path diff --git a/scripts/add_plugin.py b/scripts/add_plugin.py index 7cc83974b7..9e171faf5c 100755 --- a/scripts/add_plugin.py +++ b/scripts/add_plugin.py @@ -8,6 +8,7 @@ Example usage: --url https://pypi.org/project/flake8-pie/ --prefix PIE """ +from __future__ import annotations import argparse diff --git a/scripts/add_rule.py b/scripts/add_rule.py index 43bfddfade..1fb271aa85 100755 --- a/scripts/add_rule.py +++ b/scripts/add_rule.py @@ -9,6 +9,7 @@ Example usage: --code 807 \ --linter flake8-pie """ +from __future__ import annotations import argparse import subprocess diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index 336ce040eb..7256f28c7c 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -1,17 +1,22 @@ #!/usr/bin/env python3 """Check code snippets in docs are formatted by black.""" +from __future__ import annotations + import argparse import os import re import textwrap -from collections.abc import Sequence from pathlib import Path from re import Match +from typing import TYPE_CHECKING import black from black.mode import Mode, TargetVersion from black.parsing import InvalidInput +if TYPE_CHECKING: + from collections.abc import Sequence + TARGET_VERSIONS = ["py37", "py38", "py39", "py310", "py311"] SNIPPED_RE = re.compile( r"(?P^(?P *)```\s*python\n)" diff --git a/scripts/ecosystem_all_check.py b/scripts/ecosystem_all_check.py index 9f509c57ac..96107c7365 100644 --- a/scripts/ecosystem_all_check.py +++ b/scripts/ecosystem_all_check.py @@ -3,13 +3,14 @@ panics, autofix errors and similar problems. It's a less elaborate, more hacky version of check_ecosystem.py """ +from __future__ import annotations import json import subprocess import sys from pathlib import Path from subprocess import CalledProcessError -from typing import NamedTuple, Optional +from typing import NamedTuple from tqdm import tqdm @@ -19,7 +20,7 @@ class Repository(NamedTuple): org: str repo: str - ref: Optional[str] + ref: str | None def main() -> None: diff --git a/scripts/generate_known_standard_library.py b/scripts/generate_known_standard_library.py index 7fd7ae7222..5d9c66c58b 100644 --- a/scripts/generate_known_standard_library.py +++ b/scripts/generate_known_standard_library.py @@ -5,6 +5,7 @@ Source: Only the generation of the file has been modified for use in this project. """ +from __future__ import annotations from pathlib import Path diff --git a/scripts/generate_mkdocs.py b/scripts/generate_mkdocs.py index 5793858e91..8d0b92b882 100644 --- a/scripts/generate_mkdocs.py +++ b/scripts/generate_mkdocs.py @@ -1,4 +1,6 @@ """Generate an MkDocs-compatible `docs` and `mkdocs.yml` from the README.md.""" +from __future__ import annotations + import argparse import re import shutil diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml index ca7d45e508..7f4282c4c3 100644 --- a/scripts/pyproject.toml +++ b/scripts/pyproject.toml @@ -2,7 +2,7 @@ name = "scripts" version = "0.0.1" dependencies = ["sphinx"] -requires-python = ">=3.9" +requires-python = ">=3.8" [tool.black] line-length = 88 @@ -21,5 +21,8 @@ ignore = [ "FBT", # flake8-boolean-trap ] +[tool.ruff.isort] +required-imports = ["from __future__ import annotations"] + [tool.ruff.pydocstyle] convention = "pep257" diff --git a/scripts/transform_readme.py b/scripts/transform_readme.py index 963f55ca3f..7170eef20f 100644 --- a/scripts/transform_readme.py +++ b/scripts/transform_readme.py @@ -4,6 +4,8 @@ By default, we assume that our README.md will be rendered on GitHub. However, di targets have different strategies for rendering light- and dark-mode images. This script adjusts the images in the README.md to support the given target. """ +from __future__ import annotations + import argparse from pathlib import Path diff --git a/scripts/update_ambiguous_characters.py b/scripts/update_ambiguous_characters.py index 4243313e6e..27a06fd039 100644 --- a/scripts/update_ambiguous_characters.py +++ b/scripts/update_ambiguous_characters.py @@ -1,4 +1,6 @@ """Generate the confusables.rs file from the VS Code ambiguous.json file.""" +from __future__ import annotations + import json import subprocess from pathlib import Path diff --git a/scripts/update_schemastore.py b/scripts/update_schemastore.py index 9c79d7dc42..685fa7bbff 100644 --- a/scripts/update_schemastore.py +++ b/scripts/update_schemastore.py @@ -4,6 +4,7 @@ This script will clone astral-sh/schemastore, update the schema and push the cha to a new branch tagged with the ruff git hash. You should see a URL to create the PR to schemastore in the CLI. """ +from __future__ import annotations import json from pathlib import Path From 03694ef6497b7f6d7af873e72845d9218e48a94b Mon Sep 17 00:00:00 2001 From: konstin Date: Thu, 22 Jun 2023 17:48:11 +0200 Subject: [PATCH 190/447] More stability checker options (#5299) ## Summary This contains three changes: * repos in `check_ecosystem.py` are stored as `org:name` instead of `org/name` to create a flat directory layout * `check_ecosystem.py` performs a maximum of 50 parallel jobs at the same time to avoid consuming to much RAM * `check-formatter-stability` gets a new option `--multi-project` so it's possible to do `cargo run --bin ruff_dev -- check-formatter-stability --multi-project target/checkouts` With these three changes it becomes easy to check the formatter stability over a larger number of repositories. This is part of the integration of integrating formatter regressions checks into the ecosystem checks. ## Test Plan ```shell 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 ``` --- .../ruff_dev/src/check_formatter_stability.rs | 53 ++++++++++++++++--- scripts/check_ecosystem.py | 32 +++++++---- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/crates/ruff_dev/src/check_formatter_stability.rs b/crates/ruff_dev/src/check_formatter_stability.rs index 26b1d68902..39f90224b2 100644 --- a/crates/ruff_dev/src/check_formatter_stability.rs +++ b/crates/ruff_dev/src/check_formatter_stability.rs @@ -42,6 +42,9 @@ pub(crate) struct Args { /// Print only the first error and exit, `-x` is same as pytest #[arg(long, short = 'x')] pub(crate) exit_first_error: bool, + /// Checks each project inside a directory + #[arg(long)] + pub(crate) multi_project: bool, } /// Generate ourself a `try_parse_from` impl for `CheckArgs`. This is a strange way to use clap but @@ -54,6 +57,35 @@ struct WrapperArgs { } pub(crate) fn main(args: &Args) -> anyhow::Result { + let all_success = if args.multi_project { + let mut all_success = true; + for base_dir in &args.files { + for dir in base_dir.read_dir()? { + let dir = dir?; + println!("Starting {}", dir.path().display()); + let success = check_repo(&Args { + files: vec![dir.path().clone()], + ..*args + }); + println!("Finished {}: {:?}", dir.path().display(), success); + if !matches!(success, Ok(true)) { + all_success = false; + } + } + } + all_success + } else { + check_repo(args)? + }; + if all_success { + Ok(ExitCode::SUCCESS) + } else { + Ok(ExitCode::FAILURE) + } +} + +/// Returns whether the check was successful +pub(crate) fn check_repo(args: &Args) -> anyhow::Result { let start = Instant::now(); // Find files to check (or in this case, format twice). Adapted from ruff_cli @@ -77,13 +109,20 @@ pub(crate) fn main(args: &Args) -> anyhow::Result { let (paths, _resolver) = python_files_in_path(&cli.files, &pyproject_config, &overrides)?; assert!(!paths.is_empty(), "no python files in {:?}", cli.files); + let mut formatted_counter = 0; let errors = paths .into_iter() .map(|dir_entry| { // Doesn't make sense to recover here in this test script - let file = dir_entry - .expect("Iterating the files in the repository failed") - .into_path(); + dir_entry.expect("Iterating the files in the repository failed") + }) + .filter(|dir_entry| { + // For some reason it does not filter in the beginning + dir_entry.file_name() != "pyproject.toml" + }) + .map(|dir_entry| { + let file = dir_entry.path().to_path_buf(); + formatted_counter += 1; // Handle panics (mostly in `debug_assert!`) let result = match catch_unwind(|| check_file(&file)) { Ok(result) => result, @@ -166,20 +205,20 @@ Formatted twice: } if args.exit_first_error { - return Ok(ExitCode::FAILURE); + return Ok(false); } } let duration = start.elapsed(); println!( "Formatting {} files twice took {:.2}s", - cli.files.len(), + formatted_counter, duration.as_secs_f32() ); if any_errors { - Ok(ExitCode::FAILURE) + Ok(false) } else { - Ok(ExitCode::SUCCESS) + Ok(true) } } diff --git a/scripts/check_ecosystem.py b/scripts/check_ecosystem.py index d60ad3fc6d..c8c1916299 100755 --- a/scripts/check_ecosystem.py +++ b/scripts/check_ecosystem.py @@ -44,11 +44,11 @@ class Repository(NamedTuple): async def clone(self: Self, checkout_dir: Path) -> AsyncIterator[Path]: """Shallow clone this repository to a temporary directory.""" if checkout_dir.exists(): - logger.debug(f"Reusing {self.org}/{self.repo}") + logger.debug(f"Reusing {self.org}:{self.repo}") yield Path(checkout_dir) return - logger.debug(f"Cloning {self.org}/{self.repo}") + logger.debug(f"Cloning {self.org}:{self.repo}") git_command = [ "git", "clone", @@ -177,18 +177,17 @@ async def compare( """Check a specific repository against two versions of ruff.""" removed, added = set(), set() - # Allows to keep the checkouts locations + # By the default, the git clone are transient, but if the user provides a + # directory for permanent storage we keep it there if checkouts: - checkout_parent = checkouts.joinpath(repo.org) - # Don't create the repodir itself, we need that for checking for existing - # clones - checkout_parent.mkdir(exist_ok=True, parents=True) - location_context = nullcontext(checkout_parent) + location_context = nullcontext(checkouts) else: location_context = tempfile.TemporaryDirectory() with location_context as checkout_parent: - checkout_dir = Path(checkout_parent).joinpath(repo.repo) + assert ":" not in repo.org + assert ":" not in repo.repo + checkout_dir = Path(checkout_parent).joinpath(f"{repo.org}:{repo.repo}") async with repo.clone(checkout_dir) as path: try: async with asyncio.TaskGroup() as tg: @@ -284,8 +283,19 @@ async def main( logger.debug(f"Checking {len(repositories)} projects") + # https://stackoverflow.com/a/61478547/3549270 + # Otherwise doing 3k repositories can take >8GB RAM + semaphore = asyncio.Semaphore(50) + + async def limited_parallelism(coroutine): # noqa: ANN + async with semaphore: + return await coroutine + results = await asyncio.gather( - *[compare(ruff1, ruff2, repo, checkouts) for repo in repositories.values()], + *[ + limited_parallelism(compare(ruff1, ruff2, repo, checkouts)) + for repo in repositories.values() + ], return_exceptions=True, ) @@ -433,6 +443,8 @@ if __name__ == "__main__": logging.basicConfig(level=logging.INFO) loop = asyncio.get_event_loop() + if args.checkouts: + args.checkouts.mkdir(exist_ok=True, parents=True) main_task = asyncio.ensure_future( main( ruff1=args.ruff1, From 96ecfae1c5f9ebcedc90e51252aa3cb5274c8e45 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 22 Jun 2023 11:52:03 -0400 Subject: [PATCH 191/447] Remove off-palette colors (#5302) --- docs/stylesheets/extra.css | 8 +++----- scripts/pyproject.toml | 1 - 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 11835649c6..254fae832d 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -1,8 +1,6 @@ :root { --black: #261230; --white: #ffffff; - --astral-purple: #1f092a; - --astral-purple-alt: #30173d; --light-gray: #826894; --radiate: #d7ff64; --flare: #6340ac; @@ -33,11 +31,11 @@ } [data-md-color-scheme="astral-dark"] { - --md-default-bg-color: var(--astral-purple); + --md-default-bg-color: var(--galaxy); --md-default-fg-color: var(--white); --md-default-fg-color--light: var(--white); --md-default-fg-color--lighter: var(--white); - --md-primary-fg-color: var(--astral-purple-alt); + --md-primary-fg-color: var(--space); --md-primary-bg-color: var(--white); --md-accent-fg-color: var(--radiate); @@ -46,7 +44,7 @@ --md-typeset-mark-color: var(--sun); --md-code-fg-color: var(--white); - --md-code-bg-color: var(--astral-purple-alt); + --md-code-bg-color: var(--space); --md-code-hl-comment-color: var(--light-gray); --md-code-hl-punctuation-color: var(--light-gray); diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml index 7f4282c4c3..387f364f26 100644 --- a/scripts/pyproject.toml +++ b/scripts/pyproject.toml @@ -15,7 +15,6 @@ ignore = [ "D", # pydocstyle "PL", # pylint "S", # bandit - "CPY", # copyright "G", # flake8-logging "T", # flake8-print "FBT", # flake8-boolean-trap From 3238a6ef1fcc78be9172dfc866051bcb1471cede Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 22 Jun 2023 11:58:24 -0400 Subject: [PATCH 192/447] Fix 'our' to 'your' typo (#5303) --- docs/tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index 3491519feb..e9f6592061 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -26,7 +26,7 @@ def sum_even_numbers(numbers: List[int]) -> int: return sum(num for num in numbers if num % 2 == 0) ``` -To start, we'll install Ruff through PyPI (or with our [preferred package manager](installation.md)): +To start, we'll install Ruff through PyPI (or with your [preferred package manager](installation.md)): ```shell > pip install ruff From c0f93fcf3ee34fcbef4ce463249ca14c2055bdd5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 22 Jun 2023 12:11:43 -0400 Subject: [PATCH 193/447] Publish GitHub release as draft (#5304) I accidentally changed `draft: false` to `draft: true` in #5240. I actually think Copilot did this without me realizing. --- .github/workflows/release.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9b29bbedc2..dfd6fbbd47 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -491,14 +491,14 @@ jobs: - name: "Publish to GitHub" uses: softprops/action-gh-release@v1 with: - draft: false + draft: true files: binaries/* tag_name: v${{ inputs.tag }} # After the release has been published, we update downstream repositories # This is separate because if this fails the release is still fine, we just need to do some manual workflow triggers update-dependents: - name: Release + name: Update dependents runs-on: ubuntu-latest needs: publish-release steps: From 5dd00b19e6cbce8dc0b117a4cf41432ec61b9e46 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 22 Jun 2023 12:31:22 -0400 Subject: [PATCH 194/447] Remove off-palette colors from code (#5305) --- docs/stylesheets/extra.css | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 254fae832d..f6c92cfb86 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -1,7 +1,6 @@ :root { --black: #261230; --white: #ffffff; - --light-gray: #826894; --radiate: #d7ff64; --flare: #6340ac; --rock: #78876e; @@ -46,16 +45,16 @@ --md-code-fg-color: var(--white); --md-code-bg-color: var(--space); - --md-code-hl-comment-color: var(--light-gray); - --md-code-hl-punctuation-color: var(--light-gray); - --md-code-hl-generic-color: var(--light-gray); - --md-code-hl-variable-color: var(--light-gray); - --md-code-hl-string-color: var(--aurora); - --md-code-hl-keyword-color: var(--sun); - --md-code-hl-operator-color: var(--sun); - --md-code-hl-number-color: hsla(0, 67%, 50%, 1); - --md-code-hl-special-color: hsla(340, 83%, 47%, 1); - --md-code-hl-function-color: var(--cosmic); + --md-code-hl-comment-color: var(--asteroid); + --md-code-hl-punctuation-color: var(--asteroid); + --md-code-hl-generic-color: var(--supernova); + --md-code-hl-variable-color: var(--starlight); + --md-code-hl-string-color: var(--radiate); + --md-code-hl-keyword-color: var(--supernova); + --md-code-hl-operator-color: var(--supernova); + --md-code-hl-number-color: var(--electron); + --md-code-hl-special-color: var(--electron); + --md-code-hl-function-color: var(--neutron); --md-code-hl-constant-color: var(--radiate); --md-code-hl-name-color: var(--md-code-fg-color); From 1c2be54b4a4849cfcfdca5f529cfd2beee58d3be Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 22 Jun 2023 15:27:05 -0400 Subject: [PATCH 195/447] Support `pydantic.BaseSettings` in `mutable-class-default` (#5312) Closes #5308. --- crates/ruff/src/rules/ruff/rules/helpers.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/ruff/src/rules/ruff/rules/helpers.rs b/crates/ruff/src/rules/ruff/rules/helpers.rs index 9643508cb7..944b1b52a2 100644 --- a/crates/ruff/src/rules/ruff/rules/helpers.rs +++ b/crates/ruff/src/rules/ruff/rules/helpers.rs @@ -37,11 +37,14 @@ pub(super) fn is_dataclass(class_def: &ast::StmtClassDef, semantic: &SemanticMod }) } -/// Returns `true` if the given class is a Pydantic `BaseModel`. +/// Returns `true` if the given class is a Pydantic `BaseModel` or `BaseSettings` subclass. pub(super) fn is_pydantic_model(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool { class_def.bases.iter().any(|expr| { semantic.resolve_call_path(expr).map_or(false, |call_path| { - matches!(call_path.as_slice(), ["pydantic", "BaseModel"]) + matches!( + call_path.as_slice(), + ["pydantic", "BaseModel" | "BaseSettings"] + ) }) }) } From 5f88ff8a96b7d9f53cd72c628b775c9b70a44802 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 22 Jun 2023 15:40:54 -0400 Subject: [PATCH 196/447] Allow `__slots__` assignments in `mutable-class-default` (#5314) Closes #5309. --- .../resources/test/fixtures/ruff/RUF012.py | 18 ++----- crates/ruff/src/rules/ruff/rules/helpers.rs | 16 ++++++ .../rules/ruff/rules/mutable_class_default.rs | 11 ++-- ..._rules__ruff__tests__RUF012_RUF012.py.snap | 54 +++++++------------ 4 files changed, 45 insertions(+), 54 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF012.py b/crates/ruff/resources/test/fixtures/ruff/RUF012.py index 081c13bac3..9be4b88c76 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF012.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF012.py @@ -1,23 +1,14 @@ -import typing from typing import ClassVar, Sequence, Final -KNOWINGLY_MUTABLE_DEFAULT = [] - class A: - mutable_default: list[int] = [] - immutable_annotation: typing.Sequence[int] = [] - without_annotation = [] - correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT - class_variable: typing.ClassVar[list[int]] = [] - final_variable: typing.Final[list[int]] = [] + __slots__ = { + "mutable_default": "A mutable default value", + } - -class B: mutable_default: list[int] = [] immutable_annotation: Sequence[int] = [] without_annotation = [] - correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT class_variable: ClassVar[list[int]] = [] final_variable: Final[list[int]] = [] @@ -30,7 +21,6 @@ class C: mutable_default: list[int] = [] immutable_annotation: Sequence[int] = [] without_annotation = [] - correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT perfectly_fine: list[int] = field(default_factory=list) class_variable: ClassVar[list[int]] = [] final_variable: Final[list[int]] = [] @@ -43,7 +33,5 @@ class D(BaseModel): mutable_default: list[int] = [] immutable_annotation: Sequence[int] = [] without_annotation = [] - correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT - perfectly_fine: list[int] = field(default_factory=list) class_variable: ClassVar[list[int]] = [] final_variable: Final[list[int]] = [] diff --git a/crates/ruff/src/rules/ruff/rules/helpers.rs b/crates/ruff/src/rules/ruff/rules/helpers.rs index 944b1b52a2..b70c6918e1 100644 --- a/crates/ruff/src/rules/ruff/rules/helpers.rs +++ b/crates/ruff/src/rules/ruff/rules/helpers.rs @@ -3,6 +3,22 @@ use rustpython_parser::ast::{self, Expr}; use ruff_python_ast::helpers::map_callable; use ruff_python_semantic::SemanticModel; +/// Return `true` if the given [`Expr`] is a special class attribute, like `__slots__`. +/// +/// While `__slots__` is typically defined via a tuple, Python accepts any iterable and, in +/// particular, allows the use of a dictionary to define the attribute names (as keys) and +/// docstrings (as values). +pub(super) fn is_special_attribute(value: &Expr) -> bool { + if let Expr::Name(ast::ExprName { id, .. }) = value { + matches!( + id.as_str(), + "__slots__" | "__dict__" | "__weakref__" | "__annotations__" + ) + } else { + false + } +} + /// Returns `true` if the given [`Expr`] is a `dataclasses.field` call. pub(super) fn is_dataclass_field(func: &Expr, semantic: &SemanticModel) -> bool { semantic.resolve_call_path(func).map_or(false, |call_path| { diff --git a/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs b/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs index 2c5955e95b..c1d17c30d5 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs @@ -7,6 +7,7 @@ use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_ use crate::checkers::ast::Checker; use crate::rules::ruff::rules::helpers::{ is_class_var_annotation, is_dataclass, is_final_annotation, is_pydantic_model, + is_special_attribute, }; /// ## What it does @@ -51,10 +52,12 @@ pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::Stmt match statement { Stmt::AnnAssign(ast::StmtAnnAssign { annotation, + target, value: Some(value), .. }) => { - if is_mutable_expr(value, checker.semantic()) + if !is_special_attribute(target) + && is_mutable_expr(value, checker.semantic()) && !is_class_var_annotation(annotation, checker.semantic()) && !is_final_annotation(annotation, checker.semantic()) && !is_immutable_annotation(annotation, checker.semantic()) @@ -70,8 +73,10 @@ pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::Stmt .push(Diagnostic::new(MutableClassDefault, value.range())); } } - Stmt::Assign(ast::StmtAssign { value, .. }) => { - if is_mutable_expr(value, checker.semantic()) { + Stmt::Assign(ast::StmtAssign { value, targets, .. }) => { + if !targets.iter().all(is_special_attribute) + && is_mutable_expr(value, checker.semantic()) + { // Avoid Pydantic models, which end up copying defaults on instance creation. if is_pydantic_model(class_def, checker.semantic()) { return; diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap index 285c0c6acc..676e2a1a03 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap @@ -1,52 +1,34 @@ --- source: crates/ruff/src/rules/ruff/mod.rs --- -RUF012.py:8:34: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` +RUF012.py:9:34: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` | - 7 | class A: - 8 | mutable_default: list[int] = [] + 7 | } + 8 | + 9 | mutable_default: list[int] = [] | ^^ RUF012 - 9 | immutable_annotation: typing.Sequence[int] = [] -10 | without_annotation = [] +10 | immutable_annotation: Sequence[int] = [] +11 | without_annotation = [] | -RUF012.py:10:26: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` +RUF012.py:11:26: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` | - 8 | mutable_default: list[int] = [] - 9 | immutable_annotation: typing.Sequence[int] = [] -10 | without_annotation = [] + 9 | mutable_default: list[int] = [] +10 | immutable_annotation: Sequence[int] = [] +11 | without_annotation = [] | ^^ RUF012 -11 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT -12 | class_variable: typing.ClassVar[list[int]] = [] +12 | class_variable: ClassVar[list[int]] = [] +13 | final_variable: Final[list[int]] = [] | -RUF012.py:17:34: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` +RUF012.py:23:26: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` | -16 | class B: -17 | mutable_default: list[int] = [] - | ^^ RUF012 -18 | immutable_annotation: Sequence[int] = [] -19 | without_annotation = [] - | - -RUF012.py:19:26: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` - | -17 | mutable_default: list[int] = [] -18 | immutable_annotation: Sequence[int] = [] -19 | without_annotation = [] +21 | mutable_default: list[int] = [] +22 | immutable_annotation: Sequence[int] = [] +23 | without_annotation = [] | ^^ RUF012 -20 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT -21 | class_variable: ClassVar[list[int]] = [] - | - -RUF012.py:32:26: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` - | -30 | mutable_default: list[int] = [] -31 | immutable_annotation: Sequence[int] = [] -32 | without_annotation = [] - | ^^ RUF012 -33 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT -34 | perfectly_fine: list[int] = field(default_factory=list) +24 | perfectly_fine: list[int] = field(default_factory=list) +25 | class_variable: ClassVar[list[int]] = [] | From cdbd0bd5cd5cdcde04ad04a094977c4e7a4b22a8 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 22 Jun 2023 15:52:36 -0400 Subject: [PATCH 197/447] Respect `abc` decorators when classifying function types (#5315) Closes #5307. --- .../test/fixtures/pep8_naming/N805.py | 8 ++++-- .../fixtures/pep8_naming/ignore_names/N805.py | 19 ++++++++++++- ...les__pep8_naming__tests__N805_N805.py.snap | 28 +++++++++---------- ...naming__tests__classmethod_decorators.snap | 10 +++---- ...ing__tests__ignore_names_N805_N805.py.snap | 10 +++---- .../src/analyze/function_type.rs | 12 ++++---- 6 files changed, 55 insertions(+), 32 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/N805.py b/crates/ruff/resources/test/fixtures/pep8_naming/N805.py index d45a9c3118..99fcf2ed0c 100644 --- a/crates/ruff/resources/test/fixtures/pep8_naming/N805.py +++ b/crates/ruff/resources/test/fixtures/pep8_naming/N805.py @@ -1,4 +1,4 @@ -from abc import ABCMeta +import abc import pydantic @@ -19,6 +19,10 @@ class Class: def class_method(cls): pass + @abc.abstractclassmethod + def abstract_class_method(cls): + pass + @staticmethod def static_method(x): return x @@ -41,7 +45,7 @@ class Class: ... -class MetaClass(ABCMeta): +class MetaClass(abc.ABCMeta): def bad_method(self): pass diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N805.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N805.py index ae5206483e..590df0bd6d 100644 --- a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N805.py +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N805.py @@ -1,4 +1,4 @@ -from abc import ABCMeta +import abc import pydantic @@ -34,6 +34,23 @@ class Class: def stillBad(cls, my_field: str) -> str: pass + @classmethod + def badAllowed(cls): + pass + + @classmethod + def stillBad(cls): + pass + + @abc.abstractclassmethod + def badAllowed(cls): + pass + + @abc.abstractclassmethod + def stillBad(cls): + pass + + class PosOnlyClass: def badAllowed(this, blah, /, self, something: str): pass diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__N805_N805.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__N805_N805.py.snap index 9c4369878a..714a639ab7 100644 --- a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__N805_N805.py.snap +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__N805_N805.py.snap @@ -18,29 +18,29 @@ N805.py:12:30: N805 First argument of a method should be named `self` 13 | pass | -N805.py:27:15: N805 First argument of a method should be named `self` - | -26 | @pydantic.validator -27 | def lower(cls, my_field: str) -> str: - | ^^^ N805 -28 | pass - | - N805.py:31:15: N805 First argument of a method should be named `self` | -30 | @pydantic.validator("my_field") +30 | @pydantic.validator 31 | def lower(cls, my_field: str) -> str: | ^^^ N805 32 | pass | -N805.py:60:29: N805 First argument of a method should be named `self` +N805.py:35:15: N805 First argument of a method should be named `self` | -58 | pass -59 | -60 | def bad_method_pos_only(this, blah, /, self, something: str): +34 | @pydantic.validator("my_field") +35 | def lower(cls, my_field: str) -> str: + | ^^^ N805 +36 | pass + | + +N805.py:64:29: N805 First argument of a method should be named `self` + | +62 | pass +63 | +64 | def bad_method_pos_only(this, blah, /, self, something: str): | ^^^^ N805 -61 | pass +65 | pass | diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__classmethod_decorators.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__classmethod_decorators.snap index a6d3bc8592..06c9407f28 100644 --- a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__classmethod_decorators.snap +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__classmethod_decorators.snap @@ -18,13 +18,13 @@ N805.py:12:30: N805 First argument of a method should be named `self` 13 | pass | -N805.py:60:29: N805 First argument of a method should be named `self` +N805.py:64:29: N805 First argument of a method should be named `self` | -58 | pass -59 | -60 | def bad_method_pos_only(this, blah, /, self, something: str): +62 | pass +63 | +64 | def bad_method_pos_only(this, blah, /, self, something: str): | ^^^^ N805 -61 | pass +65 | pass | diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N805_N805.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N805_N805.py.snap index 46bfb51c01..fee3fee254 100644 --- a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N805_N805.py.snap +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N805_N805.py.snap @@ -35,13 +35,13 @@ N805.py:34:18: N805 First argument of a method should be named `self` 35 | pass | -N805.py:41:18: N805 First argument of a method should be named `self` +N805.py:58:18: N805 First argument of a method should be named `self` | -39 | pass -40 | -41 | def stillBad(this, blah, /, self, something: str): +56 | pass +57 | +58 | def stillBad(this, blah, /, self, something: str): | ^^^^ N805 -42 | pass +59 | pass | diff --git a/crates/ruff_python_semantic/src/analyze/function_type.rs b/crates/ruff_python_semantic/src/analyze/function_type.rs index 46d984b276..63f9b8ce71 100644 --- a/crates/ruff_python_semantic/src/analyze/function_type.rs +++ b/crates/ruff_python_semantic/src/analyze/function_type.rs @@ -35,10 +35,12 @@ pub fn classify( semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { - matches!(call_path.as_slice(), ["", "staticmethod"]) - || staticmethod_decorators - .iter() - .any(|decorator| call_path == from_qualified_name(decorator)) + matches!( + call_path.as_slice(), + ["", "staticmethod"] | ["abc", "abstractstaticmethod"] + ) || staticmethod_decorators + .iter() + .any(|decorator| call_path == from_qualified_name(decorator)) }) }) { FunctionType::StaticMethod @@ -55,7 +57,7 @@ pub fn classify( || 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"]) || + matches!(call_path.as_slice(), ["", "classmethod"] | ["abc", "abstractclassmethod"]) || classmethod_decorators .iter() .any(|decorator| call_path == from_qualified_name(decorator)) From 8bc7378002d080094d572e83f3a3a69df1023030 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 22 Jun 2023 16:01:07 -0400 Subject: [PATCH 198/447] Add `PythonVersion::Py312` (#5316) Closes #5310. --- .../src/rules/pyupgrade/rules/deprecated_import.rs | 14 ++++++++++++-- crates/ruff/src/settings/options.rs | 2 +- crates/ruff/src/settings/types.rs | 2 ++ docs/configuration.md | 2 +- ruff.schema.json | 3 ++- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs index c2813769b6..f178e3e8cb 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs @@ -281,8 +281,15 @@ const TYPING_EXTENSIONS_TO_TYPING_311: &[&str] = &[ "get_overloads", "overload", "reveal_type", - // Introduced in Python 3.11, but `typing_extensions` backports the `frozen_default` argument. - // "dataclass_transform", +]; + +// Python 3.12+ + +// Members of `typing_extensions` that were moved to `typing`. +const TYPING_EXTENSIONS_TO_TYPING_312: &[&str] = &[ + // Introduced in Python 3.11, but `typing_extensions` backports the `frozen_default` argument, + // which was introduced in Python 3.12. + "dataclass_transform", ]; struct ImportReplacer<'a> { @@ -369,6 +376,9 @@ impl<'a> ImportReplacer<'a> { if self.version >= PythonVersion::Py311 { typing_extensions_to_typing.extend(TYPING_EXTENSIONS_TO_TYPING_311); } + if self.version >= PythonVersion::Py312 { + typing_extensions_to_typing.extend(TYPING_EXTENSIONS_TO_TYPING_312); + } if let Some(operation) = self.try_replace(&typing_extensions_to_typing, "typing") { operations.push(operation); } diff --git a/crates/ruff/src/settings/options.rs b/crates/ruff/src/settings/options.rs index ea18f0cb6d..ef5a6a3597 100644 --- a/crates/ruff/src/settings/options.rs +++ b/crates/ruff/src/settings/options.rs @@ -433,7 +433,7 @@ pub struct Options { pub namespace_packages: Option>, #[option( default = r#""py310""#, - value_type = r#""py37" | "py38" | "py39" | "py310" | "py311""#, + value_type = r#""py37" | "py38" | "py39" | "py310" | "py311" | "py312""#, example = r#" # Always generate Python 3.7-compatible code. target-version = "py37" diff --git a/crates/ruff/src/settings/types.rs b/crates/ruff/src/settings/types.rs index 63e6376e93..ca813bcb0a 100644 --- a/crates/ruff/src/settings/types.rs +++ b/crates/ruff/src/settings/types.rs @@ -30,6 +30,7 @@ pub enum PythonVersion { Py39, Py310, Py311, + Py312, } impl From for Pep440Version { @@ -47,6 +48,7 @@ impl PythonVersion { Self::Py39 => (3, 9), Self::Py310 => (3, 10), Self::Py311 => (3, 11), + Self::Py312 => (3, 12), } } diff --git a/docs/configuration.md b/docs/configuration.md index 538e19ce8f..20ee6ba11a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -215,7 +215,7 @@ Options: -o, --output-file Specify file to write the linter output to (default: stdout) --target-version - The minimum Python version that should be supported [possible values: py37, py38, py39, py310, py311] + The minimum Python version that should be supported [possible values: py37, py38, py39, py310, py311, py312] --config Path to the `pyproject.toml` or `ruff.toml` file to use for configuration --statistics diff --git a/ruff.schema.json b/ruff.schema.json index b6c0437890..a30f989acf 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1555,7 +1555,8 @@ "py38", "py39", "py310", - "py311" + "py311", + "py312" ] }, "Quote": { From e0e1d13d9fa73b0f310b7cec5d251c208fbd5ec3 Mon Sep 17 00:00:00 2001 From: "Edgar R. M" Date: Thu, 22 Jun 2023 14:06:47 -0600 Subject: [PATCH 199/447] Fix `diagnostics` variable name in `add_plugin.py` script (#5317) ## Summary Fix a variable name in the `add_plugin.py` script. ## Test Plan I don't think there are any tests for the scripts, other than manual confirmation --- scripts/add_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/add_plugin.py b/scripts/add_plugin.py index 9e171faf5c..ce9e143a21 100755 --- a/scripts/add_plugin.py +++ b/scripts/add_plugin.py @@ -45,7 +45,7 @@ mod tests { fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); - let messages = test_path( + let diagnostics = test_path( Path::new("%s").join(path).as_path(), &settings::Settings::for_rule(rule_code), )?; From 50f0edd2cbfb14d423f0c155639ff459b9138eba Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 22 Jun 2023 16:11:38 -0400 Subject: [PATCH 200/447] Add dark- and light-mode image modifiers for custom MkDocs themes (#5318) ## Summary Roughly following the docs [here](https://squidfunk.github.io/mkdocs-material/reference/images/#custom-light-scheme). Closes #5311. --- docs/stylesheets/extra.css | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index f6c92cfb86..cc60cb1295 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -64,3 +64,23 @@ --md-typeset-table-color: hsla(0, 0%, 100%, 0.12); --md-typeset-table-color--light: hsla(0, 0%, 100%, 0.035); } + +[data-md-color-scheme="astral-light"] img[src$="#only-dark"], +[data-md-color-scheme="astral-light"] img[src$="#gh-dark-mode-only"] { + display: none; /* Hide dark images in light mode */ +} + +[data-md-color-scheme="astral-light"] img[src$="#only-light"], +[data-md-color-scheme="astral-light"] img[src$="#gh-light-mode-only"] { + display: inline; /* Show light images in light mode */ +} + +[data-md-color-scheme="astral-dark"] img[src$="#only-light"], +[data-md-color-scheme="astral-dark"] img[src$="#gh-light-mode-only"] { + display: none; /* Hide light images in dark mode */ +} + +[data-md-color-scheme="astral-dark"] img[src$="#only-dark"], +[data-md-color-scheme="astral-dark"] img[src$="#gh-dark-mode-only"] { + display: inline; /* Show dark images in dark mode */ +} From 38e618cd18278530147a23997ccb7c87a51143bb Mon Sep 17 00:00:00 2001 From: qdegraaf <34540841+qdegraaf@users.noreply.github.com> Date: Thu, 22 Jun 2023 22:44:26 +0200 Subject: [PATCH 201/447] [`perflint`] Add `PERF101` with autofix (#5121) ## Summary Adds PERF101 which checks for unnecessary casts to `list` in for loops. NOTE: Is not fully equal to its upstream implementation as this implementation does not flag based on type annotations (i.e.): ```python def foo(x: List[str]): for y in list(x): ... ``` With the current set-up it's quite hard to get the annotation from a function arg from its binding. Problem is best considered broader than this implementation. ## Test Plan Added fixture. ## Issue links Refers: https://github.com/astral-sh/ruff/issues/4789 --------- Co-authored-by: Charlie Marsh --- .../test/fixtures/perflint/PERF101.py | 52 +++++ crates/ruff/src/checkers/ast/mod.rs | 3 + crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/perflint/mod.rs | 1 + crates/ruff/src/rules/perflint/rules/mod.rs | 4 +- .../perflint/rules/unnecessary_list_cast.rs | 129 ++++++++++++ ...__perflint__tests__PERF101_PERF101.py.snap | 183 ++++++++++++++++++ ruff.schema.json | 1 + 8 files changed, 373 insertions(+), 1 deletion(-) create mode 100644 crates/ruff/resources/test/fixtures/perflint/PERF101.py create mode 100644 crates/ruff/src/rules/perflint/rules/unnecessary_list_cast.rs create mode 100644 crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF101_PERF101.py.snap diff --git a/crates/ruff/resources/test/fixtures/perflint/PERF101.py b/crates/ruff/resources/test/fixtures/perflint/PERF101.py new file mode 100644 index 0000000000..6123e6d652 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/perflint/PERF101.py @@ -0,0 +1,52 @@ +foo_tuple = (1, 2, 3) +foo_list = [1, 2, 3] +foo_set = {1, 2, 3} +foo_dict = {1: 2, 3: 4} +foo_int = 123 + +for i in list(foo_tuple): # PERF101 + pass + +for i in list(foo_list): # PERF101 + pass + +for i in list(foo_set): # PERF101 + pass + +for i in list((1, 2, 3)): # PERF101 + pass + +for i in list([1, 2, 3]): # PERF101 + pass + +for i in list({1, 2, 3}): # PERF101 + pass + +for i in list( + { + 1, + 2, + 3, + } +): + pass + +for i in list( # Comment + {1, 2, 3} +): # PERF101 + pass + +for i in list(foo_dict): # Ok + pass + +for i in list(1): # Ok + pass + +for i in list(foo_int): # Ok + pass + + +import itertools + +for i in itertools.product(foo_int): # Ok + pass diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index ecd0b18a7a..11f73fa09f 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1481,6 +1481,9 @@ where if self.enabled(Rule::IncorrectDictIterator) { perflint::rules::incorrect_dict_iterator(self, target, iter); } + if self.enabled(Rule::UnnecessaryListCast) { + perflint::rules::unnecessary_list_cast(self, iter); + } } Stmt::Try(ast::StmtTry { body, diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index f4f6211e43..1ea96ca00b 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -784,6 +784,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Airflow, "001") => (RuleGroup::Unspecified, rules::airflow::rules::AirflowVariableNameTaskIdMismatch), // perflint + (Perflint, "101") => (RuleGroup::Unspecified, rules::perflint::rules::UnnecessaryListCast), (Perflint, "102") => (RuleGroup::Unspecified, rules::perflint::rules::IncorrectDictIterator), // flake8-fixme diff --git a/crates/ruff/src/rules/perflint/mod.rs b/crates/ruff/src/rules/perflint/mod.rs index c0b0dd894d..b39aa84fc9 100644 --- a/crates/ruff/src/rules/perflint/mod.rs +++ b/crates/ruff/src/rules/perflint/mod.rs @@ -13,6 +13,7 @@ mod tests { use crate::settings::Settings; use crate::test::test_path; + #[test_case(Rule::UnnecessaryListCast, Path::new("PERF101.py"))] #[test_case(Rule::IncorrectDictIterator, Path::new("PERF102.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); diff --git a/crates/ruff/src/rules/perflint/rules/mod.rs b/crates/ruff/src/rules/perflint/rules/mod.rs index a092bb73f8..cc35c428b0 100644 --- a/crates/ruff/src/rules/perflint/rules/mod.rs +++ b/crates/ruff/src/rules/perflint/rules/mod.rs @@ -1,3 +1,5 @@ -pub(crate) use incorrect_dict_iterator::{incorrect_dict_iterator, IncorrectDictIterator}; +pub(crate) use incorrect_dict_iterator::*; +pub(crate) use unnecessary_list_cast::*; mod incorrect_dict_iterator; +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 new file mode 100644 index 0000000000..57f2f49caa --- /dev/null +++ b/crates/ruff/src/rules/perflint/rules/unnecessary_list_cast.rs @@ -0,0 +1,129 @@ +use ruff_text_size::TextRange; +use rustpython_parser::ast::{self, Expr}; + +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::prelude::Stmt; + +use crate::checkers::ast::Checker; +use crate::registry::AsRule; + +/// ## What it does +/// Checks for explicit casts to `list` on for-loop iterables. +/// +/// ## Why is this bad? +/// Using a `list()` call to eagerly iterate over an already-iterable type +/// (like a tuple, list, or set) is inefficient, as it forces Python to create +/// a new list unnecessarily. +/// +/// Removing the `list()` call will not change the behavior of the code, but +/// may improve performance. +/// +/// ## Example +/// ```python +/// items = (1, 2, 3) +/// for i in list(items): +/// print(i) +/// ``` +/// +/// Use instead: +/// ```python +/// items = (1, 2, 3) +/// for i in items: +/// print(i) +/// ``` +#[violation] +pub struct UnnecessaryListCast; + +impl AlwaysAutofixableViolation for UnnecessaryListCast { + #[derive_message_formats] + fn message(&self) -> String { + format!("Do not cast an iterable to `list` before iterating over it") + } + + fn autofix_title(&self) -> String { + format!("Remove `list()` cast") + } +} + +/// PERF101 +pub(crate) fn unnecessary_list_cast(checker: &mut Checker, iter: &Expr) { + let Expr::Call(ast::ExprCall{ func, args, range: list_range, ..}) = iter else { + return; + }; + + if args.len() != 1 { + return; + } + + let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else{ + return; + }; + + if !(id == "list" && checker.semantic().is_builtin("list")) { + return; + } + + match &args[0] { + Expr::Tuple(ast::ExprTuple { + range: iterable_range, + .. + }) + | Expr::List(ast::ExprList { + range: iterable_range, + .. + }) + | Expr::Set(ast::ExprSet { + range: iterable_range, + .. + }) => { + let mut diagnostic = Diagnostic::new(UnnecessaryListCast, *list_range); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.set_fix(remove_cast(*list_range, *iterable_range)); + } + checker.diagnostics.push(diagnostic); + } + Expr::Name(ast::ExprName { + id, + range: iterable_range, + .. + }) => { + let scope = checker.semantic().scope(); + if let Some(binding_id) = scope.get(id) { + let binding = checker.semantic().binding(binding_id); + if binding.kind.is_assignment() || binding.kind.is_named_expr_assignment() { + if let Some(parent_id) = binding.source { + let parent = checker.semantic().stmts[parent_id]; + if let Stmt::Assign(ast::StmtAssign { value, .. }) + | Stmt::AnnAssign(ast::StmtAnnAssign { + value: Some(value), .. + }) + | Stmt::AugAssign(ast::StmtAugAssign { value, .. }) = parent + { + if matches!( + value.as_ref(), + Expr::Tuple(_) | Expr::List(_) | Expr::Set(_) + ) { + let mut diagnostic = + Diagnostic::new(UnnecessaryListCast, *list_range); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.set_fix(remove_cast(*list_range, *iterable_range)); + } + checker.diagnostics.push(diagnostic); + } + } + } + } + } + } + _ => {} + } +} + +/// Generate a [`Fix`] to remove a `list` cast from an expression. +fn remove_cast(list_range: TextRange, iterable_range: TextRange) -> Fix { + Fix::automatic_edits( + Edit::deletion(list_range.start(), iterable_range.start()), + [Edit::deletion(iterable_range.end(), list_range.end())], + ) +} diff --git a/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF101_PERF101.py.snap b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF101_PERF101.py.snap new file mode 100644 index 0000000000..54a17b4c4f --- /dev/null +++ b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF101_PERF101.py.snap @@ -0,0 +1,183 @@ +--- +source: crates/ruff/src/rules/perflint/mod.rs +--- +PERF101.py:7:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it + | +5 | foo_int = 123 +6 | +7 | for i in list(foo_tuple): # PERF101 + | ^^^^^^^^^^^^^^^ PERF101 +8 | pass + | + = help: Remove `list()` cast + +ℹ Fix +4 4 | foo_dict = {1: 2, 3: 4} +5 5 | foo_int = 123 +6 6 | +7 |-for i in list(foo_tuple): # PERF101 + 7 |+for i in foo_tuple: # PERF101 +8 8 | pass +9 9 | +10 10 | for i in list(foo_list): # PERF101 + +PERF101.py:10:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it + | + 8 | pass + 9 | +10 | for i in list(foo_list): # PERF101 + | ^^^^^^^^^^^^^^ PERF101 +11 | pass + | + = help: Remove `list()` cast + +ℹ Fix +7 7 | for i in list(foo_tuple): # PERF101 +8 8 | pass +9 9 | +10 |-for i in list(foo_list): # PERF101 + 10 |+for i in foo_list: # PERF101 +11 11 | pass +12 12 | +13 13 | for i in list(foo_set): # PERF101 + +PERF101.py:13:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it + | +11 | pass +12 | +13 | for i in list(foo_set): # PERF101 + | ^^^^^^^^^^^^^ PERF101 +14 | pass + | + = help: Remove `list()` cast + +ℹ Fix +10 10 | for i in list(foo_list): # PERF101 +11 11 | pass +12 12 | +13 |-for i in list(foo_set): # PERF101 + 13 |+for i in foo_set: # PERF101 +14 14 | pass +15 15 | +16 16 | for i in list((1, 2, 3)): # PERF101 + +PERF101.py:16:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it + | +14 | pass +15 | +16 | for i in list((1, 2, 3)): # PERF101 + | ^^^^^^^^^^^^^^^ PERF101 +17 | pass + | + = help: Remove `list()` cast + +ℹ Fix +13 13 | for i in list(foo_set): # PERF101 +14 14 | pass +15 15 | +16 |-for i in list((1, 2, 3)): # PERF101 + 16 |+for i in (1, 2, 3): # PERF101 +17 17 | pass +18 18 | +19 19 | for i in list([1, 2, 3]): # PERF101 + +PERF101.py:19:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it + | +17 | pass +18 | +19 | for i in list([1, 2, 3]): # PERF101 + | ^^^^^^^^^^^^^^^ PERF101 +20 | pass + | + = help: Remove `list()` cast + +ℹ Fix +16 16 | for i in list((1, 2, 3)): # PERF101 +17 17 | pass +18 18 | +19 |-for i in list([1, 2, 3]): # PERF101 + 19 |+for i in [1, 2, 3]: # PERF101 +20 20 | pass +21 21 | +22 22 | for i in list({1, 2, 3}): # PERF101 + +PERF101.py:22:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it + | +20 | pass +21 | +22 | for i in list({1, 2, 3}): # PERF101 + | ^^^^^^^^^^^^^^^ PERF101 +23 | pass + | + = help: Remove `list()` cast + +ℹ Fix +19 19 | for i in list([1, 2, 3]): # PERF101 +20 20 | pass +21 21 | +22 |-for i in list({1, 2, 3}): # PERF101 + 22 |+for i in {1, 2, 3}: # PERF101 +23 23 | pass +24 24 | +25 25 | for i in list( + +PERF101.py:25:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it + | +23 | pass +24 | +25 | for i in list( + | __________^ +26 | | { +27 | | 1, +28 | | 2, +29 | | 3, +30 | | } +31 | | ): + | |_^ PERF101 +32 | pass + | + = help: Remove `list()` cast + +ℹ Fix +22 22 | for i in list({1, 2, 3}): # PERF101 +23 23 | pass +24 24 | +25 |-for i in list( +26 |- { + 25 |+for i in { +27 26 | 1, +28 27 | 2, +29 28 | 3, +30 |- } +31 |-): + 29 |+ }: +32 30 | pass +33 31 | +34 32 | for i in list( # Comment + +PERF101.py:34:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it + | +32 | pass +33 | +34 | for i in list( # Comment + | __________^ +35 | | {1, 2, 3} +36 | | ): # PERF101 + | |_^ PERF101 +37 | pass + | + = help: Remove `list()` cast + +ℹ Fix +31 31 | ): +32 32 | pass +33 33 | +34 |-for i in list( # Comment +35 |- {1, 2, 3} +36 |-): # PERF101 + 34 |+for i in {1, 2, 3}: # PERF101 +37 35 | pass +38 36 | +39 37 | for i in list(foo_dict): # Ok + + diff --git a/ruff.schema.json b/ruff.schema.json index a30f989acf..52ba8fea34 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2063,6 +2063,7 @@ "PERF", "PERF1", "PERF10", + "PERF101", "PERF102", "PGH", "PGH0", From 4a81cfc51afd28cccc403148ee3c98bdbd109c20 Mon Sep 17 00:00:00 2001 From: Lukas Mayrhofer <34736939+mayrholu@users.noreply.github.com> Date: Thu, 22 Jun 2023 22:53:58 +0200 Subject: [PATCH 202/447] Allow `@Author` format for "Missing Author" rule in `flake8-todos` (#4903) ## Summary The TD-002 rule "Missing Author" was updated to allow another format using "@". This reflects the current 0.3.0 version of flake8-todos. --- .../test/fixtures/flake8_todos/TD002.py | 8 ++- .../src/rules/flake8_todos/rules/todos.rs | 16 +++-- ...__tests__missing-todo-author_TD002.py.snap | 66 +++++++++---------- 3 files changed, 52 insertions(+), 38 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_todos/TD002.py b/crates/ruff/resources/test/fixtures/flake8_todos/TD002.py index 3c4867516f..438d6a1e6a 100644 --- a/crates/ruff/resources/test/fixtures/flake8_todos/TD002.py +++ b/crates/ruff/resources/test/fixtures/flake8_todos/TD002.py @@ -1,6 +1,12 @@ # T002 - accepted # TODO (evanrittenhouse): this has an author -# TODO(evanrittenhouse): this also has an author +# TODO(evanrittenhouse): this has an author +# TODO (evanrittenhouse) and more: this has an author +# TODO(evanrittenhouse) and more: this has an author +# TODO@mayrholu: this has an author +# TODO @mayrholu: this has an author +# TODO@mayrholu and more: this has an author +# TODO @mayrholu and more: this has an author # T002 - errors # TODO: this has no author # FIXME: neither does this diff --git a/crates/ruff/src/rules/flake8_todos/rules/todos.rs b/crates/ruff/src/rules/flake8_todos/rules/todos.rs index ce6c1ee9fa..20072b0ded 100644 --- a/crates/ruff/src/rules/flake8_todos/rules/todos.rs +++ b/crates/ruff/src/rules/flake8_todos/rules/todos.rs @@ -67,7 +67,7 @@ pub struct MissingTodoAuthor; impl Violation for MissingTodoAuthor { #[derive_message_formats] fn message(&self) -> String { - format!("Missing author in TODO; try: `# TODO(): ...`") + format!("Missing author in TODO; try: `# TODO(): ...` or `# TODO @: ...`") } } @@ -229,7 +229,7 @@ static ISSUE_LINK_REGEX_SET: Lazy = Lazy::new(|| { RegexSet::new([ r#"^#\s*(http|https)://.*"#, // issue link r#"^#\s*\d+$"#, // issue code - like "003" - r#"^#\s*[A-Z]{1,6}\-?\d+$"#, // issue code - like "TD003" or "TD-003" + r#"^#\s*[A-Z]{1,6}\-?\d+$"#, // issue code - like "TD003" ]) .unwrap() }); @@ -339,8 +339,7 @@ fn directive_errors( } } -/// Checks for "static" errors in the comment: missing colon, missing author, etc. This function -/// modifies `diagnostics` in-place. +/// Checks for "static" errors in the comment: missing colon, missing author, etc. fn static_errors( diagnostics: &mut Vec, comment: &str, @@ -358,6 +357,15 @@ fn static_errors( } else { trimmed.text_len() } + } else if trimmed.starts_with('@') { + if let Some(end_index) = trimmed.find(|c: char| c.is_whitespace() || c == ':') { + TextSize::try_from(end_index).unwrap() + } else { + // TD002 + diagnostics.push(Diagnostic::new(MissingTodoAuthor, directive.range)); + + TextSize::new(0) + } } else { // TD002 diagnostics.push(Diagnostic::new(MissingTodoAuthor, directive.range)); diff --git a/crates/ruff/src/rules/flake8_todos/snapshots/ruff__rules__flake8_todos__tests__missing-todo-author_TD002.py.snap b/crates/ruff/src/rules/flake8_todos/snapshots/ruff__rules__flake8_todos__tests__missing-todo-author_TD002.py.snap index 0647b705d4..f14a00cd5f 100644 --- a/crates/ruff/src/rules/flake8_todos/snapshots/ruff__rules__flake8_todos__tests__missing-todo-author_TD002.py.snap +++ b/crates/ruff/src/rules/flake8_todos/snapshots/ruff__rules__flake8_todos__tests__missing-todo-author_TD002.py.snap @@ -1,41 +1,41 @@ --- source: crates/ruff/src/rules/flake8_todos/mod.rs --- -TD002.py:5:3: TD002 Missing author in TODO; try: `# TODO(): ...` - | -3 | # TODO(evanrittenhouse): this also has an author -4 | # T002 - errors -5 | # TODO: this has no author - | ^^^^ TD002 -6 | # FIXME: neither does this -7 | # TODO : and neither does this - | +TD002.py:11:3: TD002 Missing author in TODO; try: `# TODO(): ...` or `# TODO @: ...` + | + 9 | # TODO @mayrholu and more: this has an author +10 | # T002 - errors +11 | # TODO: this has no author + | ^^^^ TD002 +12 | # FIXME: neither does this +13 | # TODO : and neither does this + | -TD002.py:6:3: TD002 Missing author in TODO; try: `# TODO(): ...` - | -4 | # T002 - errors -5 | # TODO: this has no author -6 | # FIXME: neither does this - | ^^^^^ TD002 -7 | # TODO : and neither does this -8 | # foo # TODO: this doesn't either - | +TD002.py:12:3: TD002 Missing author in TODO; try: `# TODO(): ...` or `# TODO @: ...` + | +10 | # T002 - errors +11 | # TODO: this has no author +12 | # FIXME: neither does this + | ^^^^^ TD002 +13 | # TODO : and neither does this +14 | # foo # TODO: this doesn't either + | -TD002.py:7:3: TD002 Missing author in TODO; try: `# TODO(): ...` - | -5 | # TODO: this has no author -6 | # FIXME: neither does this -7 | # TODO : and neither does this - | ^^^^ TD002 -8 | # foo # TODO: this doesn't either - | +TD002.py:13:3: TD002 Missing author in TODO; try: `# TODO(): ...` or `# TODO @: ...` + | +11 | # TODO: this has no author +12 | # FIXME: neither does this +13 | # TODO : and neither does this + | ^^^^ TD002 +14 | # foo # TODO: this doesn't either + | -TD002.py:8:9: TD002 Missing author in TODO; try: `# TODO(): ...` - | -6 | # FIXME: neither does this -7 | # TODO : and neither does this -8 | # foo # TODO: this doesn't either - | ^^^^ TD002 - | +TD002.py:14:9: TD002 Missing author in TODO; try: `# TODO(): ...` or `# TODO @: ...` + | +12 | # FIXME: neither does this +13 | # TODO : and neither does this +14 | # foo # TODO: this doesn't either + | ^^^^ TD002 + | From 7819b95d7f26fc40ce30fd19c9b3349df0f8320f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 22 Jun 2023 17:21:09 -0400 Subject: [PATCH 203/447] Avoid syntax errors when removing f-string prefixes (#5319) Closes https://github.com/astral-sh/ruff/issues/5281. Closes https://github.com/astral-sh/ruff/issues/4827. --- .../resources/test/fixtures/pyflakes/F541.py | 5 +- .../rules/f_string_docstring.rs | 6 +- .../rules/f_string_missing_placeholders.rs | 59 +++++++++----- ..._rules__pyflakes__tests__F541_F541.py.snap | 79 +++++++++++++++++-- 4 files changed, 118 insertions(+), 31 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F541.py b/crates/ruff/resources/test/fixtures/pyflakes/F541.py index 8a30463c47..09c10216eb 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F541.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F541.py @@ -37,7 +37,10 @@ f"{{test}}" f'{{ 40 }}' f"{{a {{x}}" f"{{{{x}}}}" +""f"" +''f"" +(""f""r"") # To be fixed # Error: f-string: single '}' is not allowed at line 41 column 8 -# f"\{{x}}" +# f"\{{x}}" diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs b/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs index 02bfbe804c..1a5bd2e011 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Expr, Stmt}; +use rustpython_parser::ast::{self, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -50,9 +50,9 @@ pub(crate) fn f_string_docstring(checker: &mut Checker, body: &[Stmt]) { let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt else { return; }; - let Expr::JoinedStr ( _) = value.as_ref() else { + if !value.is_joined_str_expr() { return; - }; + } checker .diagnostics .push(Diagnostic::new(FStringDocstring, stmt.identifier())); diff --git a/crates/ruff/src/rules/pyflakes/rules/f_string_missing_placeholders.rs b/crates/ruff/src/rules/pyflakes/rules/f_string_missing_placeholders.rs index 25171ea0eb..bef3508e15 100644 --- a/crates/ruff/src/rules/pyflakes/rules/f_string_missing_placeholders.rs +++ b/crates/ruff/src/rules/pyflakes/rules/f_string_missing_placeholders.rs @@ -79,23 +79,6 @@ fn find_useless_f_strings<'a>( }) } -fn unescape_f_string(content: &str) -> String { - content.replace("{{", "{").replace("}}", "}") -} - -fn fix_f_string_missing_placeholders( - prefix_range: TextRange, - tok_range: TextRange, - checker: &mut Checker, -) -> Fix { - let content = &checker.locator.contents()[TextRange::new(prefix_range.end(), tok_range.end())]; - Fix::automatic(Edit::replacement( - unescape_f_string(content), - prefix_range.start(), - tok_range.end(), - )) -} - /// F541 pub(crate) fn f_string_missing_placeholders(expr: &Expr, values: &[Expr], checker: &mut Checker) { if !values @@ -105,13 +88,51 @@ pub(crate) fn f_string_missing_placeholders(expr: &Expr, values: &[Expr], checke for (prefix_range, tok_range) in find_useless_f_strings(expr, checker.locator) { let mut diagnostic = Diagnostic::new(FStringMissingPlaceholders, tok_range); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(fix_f_string_missing_placeholders( + diagnostic.set_fix(convert_f_string_to_regular_string( prefix_range, tok_range, - checker, + checker.locator, )); } checker.diagnostics.push(diagnostic); } } } + +/// Unescape an f-string body by replacing `{{` with `{` and `}}` with `}`. +/// +/// In Python, curly-brace literals within f-strings must be escaped by doubling the braces. +/// When rewriting an f-string to a regular string, we need to unescape any curly-brace literals. +/// For example, given `{{Hello, world!}}`, return `{Hello, world!}`. +fn unescape_f_string(content: &str) -> String { + content.replace("{{", "{").replace("}}", "}") +} + +/// Generate a [`Fix`] to rewrite an f-string as a regular string. +fn convert_f_string_to_regular_string( + prefix_range: TextRange, + tok_range: TextRange, + locator: &Locator, +) -> Fix { + // Extract the f-string body. + let mut content = + unescape_f_string(locator.slice(TextRange::new(prefix_range.end(), tok_range.end()))); + + // If the preceding character is equivalent to the quote character, insert a space to avoid a + // syntax error. For example, when removing the `f` prefix in `""f""`, rewrite to `"" ""` + // instead of `""""`. + if locator + .slice(TextRange::up_to(prefix_range.start())) + .chars() + .last() + .map_or(false, |char| content.starts_with(char)) + { + content.insert(0, ' '); + } + + Fix::automatic(Edit::replacement( + content, + prefix_range.start(), + tok_range.end(), + )) +} diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F541_F541.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F541_F541.py.snap index 1681859307..7ca008c27e 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F541_F541.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F541_F541.py.snap @@ -269,7 +269,7 @@ F541.py:37:1: F541 [*] f-string without any placeholders 37 |+'{ 40 }' 38 38 | f"{{a {{x}}" 39 39 | f"{{{{x}}}}" -40 40 | +40 40 | ""f"" F541.py:38:1: F541 [*] f-string without any placeholders | @@ -278,6 +278,7 @@ F541.py:38:1: F541 [*] f-string without any placeholders 38 | f"{{a {{x}}" | ^^^^^^^^^^^^ F541 39 | f"{{{{x}}}}" +40 | ""f"" | = help: Remove extraneous `f` prefix @@ -288,8 +289,8 @@ F541.py:38:1: F541 [*] f-string without any placeholders 38 |-f"{{a {{x}}" 38 |+"{a {x}" 39 39 | f"{{{{x}}}}" -40 40 | -41 41 | # To be fixed +40 40 | ""f"" +41 41 | ''f"" F541.py:39:1: F541 [*] f-string without any placeholders | @@ -297,8 +298,8 @@ F541.py:39:1: F541 [*] f-string without any placeholders 38 | f"{{a {{x}}" 39 | f"{{{{x}}}}" | ^^^^^^^^^^^^ F541 -40 | -41 | # To be fixed +40 | ""f"" +41 | ''f"" | = help: Remove extraneous `f` prefix @@ -308,8 +309,70 @@ F541.py:39:1: F541 [*] f-string without any placeholders 38 38 | f"{{a {{x}}" 39 |-f"{{{{x}}}}" 39 |+"{{x}}" -40 40 | -41 41 | # To be fixed -42 42 | # Error: f-string: single '}' is not allowed at line 41 column 8 +40 40 | ""f"" +41 41 | ''f"" +42 42 | (""f""r"") + +F541.py:40:3: F541 [*] f-string without any placeholders + | +38 | f"{{a {{x}}" +39 | f"{{{{x}}}}" +40 | ""f"" + | ^^^ F541 +41 | ''f"" +42 | (""f""r"") + | + = help: Remove extraneous `f` prefix + +ℹ Fix +37 37 | f'{{ 40 }}' +38 38 | f"{{a {{x}}" +39 39 | f"{{{{x}}}}" +40 |-""f"" + 40 |+"" "" +41 41 | ''f"" +42 42 | (""f""r"") +43 43 | + +F541.py:41:3: F541 [*] f-string without any placeholders + | +39 | f"{{{{x}}}}" +40 | ""f"" +41 | ''f"" + | ^^^ F541 +42 | (""f""r"") + | + = help: Remove extraneous `f` prefix + +ℹ Fix +38 38 | f"{{a {{x}}" +39 39 | f"{{{{x}}}}" +40 40 | ""f"" +41 |-''f"" + 41 |+''"" +42 42 | (""f""r"") +43 43 | +44 44 | # To be fixed + +F541.py:42:4: F541 [*] f-string without any placeholders + | +40 | ""f"" +41 | ''f"" +42 | (""f""r"") + | ^^^ F541 +43 | +44 | # To be fixed + | + = help: Remove extraneous `f` prefix + +ℹ Fix +39 39 | f"{{{{x}}}}" +40 40 | ""f"" +41 41 | ''f"" +42 |-(""f""r"") + 42 |+("" ""r"") +43 43 | +44 44 | # To be fixed +45 45 | # Error: f-string: single '}' is not allowed at line 41 column 8 From 1cf307c34cc11117fe92481f7150884fa2ef24d4 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Thu, 22 Jun 2023 23:37:54 +0100 Subject: [PATCH 204/447] Fix `collection-literal-concatenation` documentation (#5320) ## Summary Move `collection-literal-concatenation` markdown documentation to the correct place. Fixes error in #5262. ## Test Plan `python scripts/check_docs_formatted.py` --- .../ruff/rules/collection_literal_concatenation.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 e544ab0750..86b51129d3 100644 --- a/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs +++ b/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs @@ -8,11 +8,6 @@ use ruff_python_ast::helpers::has_comments; use crate::checkers::ast::Checker; use crate::registry::AsRule; -#[violation] -pub struct CollectionLiteralConcatenation { - expr: String, -} - /// ## What it does /// Checks for uses of the `+` operator to concatenate collections. /// @@ -43,6 +38,11 @@ pub struct CollectionLiteralConcatenation { /// ## References /// - [PEP 448 – Additional Unpacking Generalizations](https://peps.python.org/pep-0448/) /// - [Python docs: Sequence Types — `list`, `tuple`, `range`](https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range) +#[violation] +pub struct CollectionLiteralConcatenation { + expr: String, +} + impl Violation for CollectionLiteralConcatenation { const AUTOFIX: AutofixKind = AutofixKind::Sometimes; From 2142bf6141db67adb3845837f89af9373f3f8482 Mon Sep 17 00:00:00 2001 From: James Berry Date: Thu, 22 Jun 2023 23:55:42 -0400 Subject: [PATCH 205/447] Fix annotation and format spec visitors (#5324) ## Summary The `Visitor` and `preorder::Visitor` traits provide some convenience functions, `visit_annotation` and `visit_format_spec`, for handling annotation and format spec expressions respectively. Both of these functions accept an `&Expr` and have a default implementation which delegates to `walk_expr`. The problem with this approach is that any custom handling done in `visit_expr` will be skipped for annotations and format specs. Instead, to capture any custom logic implemented in `visit_expr`, both of these function's default implementations should delegate to `visit_expr` instead of `walk_expr`. ## Example Consider the below `Visitor` implementation: ```rust impl<'a> Visitor<'a> for Example<'a> { fn visit_expr(&mut self, expr: &'a Expr) { match expr { Expr::Name(ExprName { id, .. }) => println!("Visiting {:?}", id), _ => walk_expr(self, expr), } } } ``` Run on the following Python snippet: ```python a: b ``` I would expect such a visitor to print the following: ``` Visiting b Visiting a ``` But it instead prints the following: ``` Visiting a ``` Our custom `visit_expr` handler is not invoked for the annotation. ## Test Plan Tests added in #5271 caught this behavior. --- crates/ruff_python_ast/src/visitor.rs | 12 ++++++++++-- crates/ruff_python_ast/src/visitor/preorder.rs | 15 +++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/crates/ruff_python_ast/src/visitor.rs b/crates/ruff_python_ast/src/visitor.rs index 766ed29441..9cf9d7786b 100644 --- a/crates/ruff_python_ast/src/visitor.rs +++ b/crates/ruff_python_ast/src/visitor.rs @@ -20,7 +20,7 @@ pub trait Visitor<'a> { walk_stmt(self, stmt); } fn visit_annotation(&mut self, expr: &'a Expr) { - walk_expr(self, expr); + walk_annotation(self, expr); } fn visit_decorator(&mut self, decorator: &'a Decorator) { walk_decorator(self, decorator); @@ -53,7 +53,7 @@ pub trait Visitor<'a> { walk_except_handler(self, except_handler); } fn visit_format_spec(&mut self, format_spec: &'a Expr) { - walk_expr(self, format_spec); + walk_format_spec(self, format_spec); } fn visit_arguments(&mut self, arguments: &'a Arguments) { walk_arguments(self, arguments); @@ -322,6 +322,10 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { } } +pub fn walk_annotation<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { + visitor.visit_expr(expr); +} + pub fn walk_decorator<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, decorator: &'a Decorator) { visitor.visit_expr(&decorator.expression); } @@ -602,6 +606,10 @@ pub fn walk_except_handler<'a, V: Visitor<'a> + ?Sized>( } } +pub fn walk_format_spec<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, format_spec: &'a Expr) { + visitor.visit_expr(format_spec); +} + pub fn walk_arguments<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arguments: &'a Arguments) { // Defaults are evaluated before annotations. for arg in &arguments.posonlyargs { diff --git a/crates/ruff_python_ast/src/visitor/preorder.rs b/crates/ruff_python_ast/src/visitor/preorder.rs index 544a0c8402..8bf9deabfe 100644 --- a/crates/ruff_python_ast/src/visitor/preorder.rs +++ b/crates/ruff_python_ast/src/visitor/preorder.rs @@ -11,7 +11,7 @@ pub trait PreorderVisitor<'a> { } fn visit_annotation(&mut self, expr: &'a Expr) { - walk_expr(self, expr); + walk_annotation(self, expr); } fn visit_expr(&mut self, expr: &'a Expr) { @@ -51,7 +51,7 @@ pub trait PreorderVisitor<'a> { } fn visit_format_spec(&mut self, format_spec: &'a Expr) { - walk_expr(self, format_spec); + walk_format_spec(self, format_spec); } fn visit_arguments(&mut self, arguments: &'a Arguments) { @@ -395,6 +395,10 @@ where } } +pub fn walk_annotation<'a, V: PreorderVisitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { + visitor.visit_expr(expr); +} + pub fn walk_decorator<'a, V>(visitor: &mut V, decorator: &'a Decorator) where V: PreorderVisitor<'a> + ?Sized, @@ -726,6 +730,13 @@ where } } +pub fn walk_format_spec<'a, V: PreorderVisitor<'a> + ?Sized>( + visitor: &mut V, + format_spec: &'a Expr, +) { + visitor.visit_expr(format_spec); +} + pub fn walk_arguments<'a, V>(visitor: &mut V, arguments: &'a Arguments) where V: PreorderVisitor<'a> + ?Sized, From 3e12bdff45d268b4bc19c373603476273d58da69 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 23 Jun 2023 09:35:29 +0200 Subject: [PATCH 206/447] Format Compare Op ## Summary This PR adds basic formatting for compare operations. The implementation currently breaks diffeently when nesting binary like expressions. I haven't yet figured out what Black's logic is in that case but I think that this by itself is already an improvement worth merging. ## Test Plan I added a few new tests --- .../test/fixtures/ruff/expression/compare.py | 61 ++++++ .../src/expression/expr_compare.rs | 156 +++++++++++++-- .../src/expression/mod.rs | 2 +- ...r__tests__black_test__bracketmatch_py.snap | 10 +- ...tter__tests__black_test__comments2_py.snap | 28 +-- ...tter__tests__black_test__comments3_py.snap | 5 +- ...tter__tests__black_test__comments5_py.snap | 15 +- ...er__tests__black_test__empty_lines_py.snap | 128 ++++++++---- ...ter__tests__black_test__expression_py.snap | 144 +++++++------ ...tter__tests__black_test__fmtonoff5_py.snap | 20 +- ...atter__tests__black_test__fmtonoff_py.snap | 4 +- ...atter__tests__black_test__fmtskip5_py.snap | 18 +- ...tter__tests__black_test__function2_py.snap | 8 +- ...lack_test__function_trailing_comma_py.snap | 50 +++-- ...ests__black_test__power_op_spacing_py.snap | 18 +- ...move_newline_after_code_block_open_py.snap | 16 +- ...rmatter__tests__black_test__slices_py.snap | 5 +- ...matter__tests__black_test__torture_py.snap | 48 ++++- ...t__trailing_comma_optional_parens1_py.snap | 24 ++- ...t__trailing_comma_optional_parens2_py.snap | 8 +- ...sts__ruff_test__expression__binary_py.snap | 8 +- ...ts__ruff_test__expression__compare_py.snap | 189 ++++++++++++++++++ ...r__tests__ruff_test__statement__if_py.snap | 10 +- ...ormatter__tests__ruff_test__trivia_py.snap | 4 +- 24 files changed, 735 insertions(+), 244 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/compare.py create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__compare_py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/compare.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/compare.py new file mode 100644 index 0000000000..906d5710aa --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/compare.py @@ -0,0 +1,61 @@ +a == b +a != b +a < b +a <= b +a > b +a >= b +a is b +a is not b +a in b +a not in b + +(a == + # comment + b +) + +(a == # comment + b + ) + +a < b > c == d + +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa < bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb > ccccccccccccccccccccccccccccc == ddddddddddddddddddddd + +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa < [ + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ff, +] < [ccccccccccccccccccccccccccccc, dddd] < ddddddddddddddddddddddddddddddddddddddddddd + +return 1 == 2 and ( + name, + description, + self_default, + self_selected, + self_auto_generated, + self_parameters, + self_meta_data, + self_schedule, +) == ( + name, + description, + othr_default, + othr_selected, + othr_auto_generated, + othr_parameters, + othr_meta_data, + othr_schedule, +) + +(name, description, self_default, self_selected, self_auto_generated, self_parameters, self_meta_data, self_schedule) == (name, description, other_default, othr_selected, othr_auto_generated, othr_parameters, othr_meta_data, othr_schedule) +((name, description, self_default, self_selected, self_auto_generated, self_parameters, self_meta_data, self_schedule) == (name, description, other_default, othr_selected, othr_auto_generated, othr_parameters, othr_meta_data, othr_schedule)) + +[ + ( + a + + [ + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ] + >= c + ) +] diff --git a/crates/ruff_python_formatter/src/expression/expr_compare.rs b/crates/ruff_python_formatter/src/expression/expr_compare.rs index 54c5301e85..d9031357a0 100644 --- a/crates/ruff_python_formatter/src/expression/expr_compare.rs +++ b/crates/ruff_python_formatter/src/expression/expr_compare.rs @@ -1,23 +1,96 @@ +use crate::comments::{leading_comments, Comments}; +use crate::expression::binary_like::{BinaryLayout, FormatBinaryLike}; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; -use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; - -use crate::comments::Comments; -use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::ExprCompare; +use crate::prelude::*; +use crate::FormatNodeRule; +use ruff_formatter::{ + write, FormatError, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions, +}; +use ruff_python_ast::prelude::Expr; +use rustpython_parser::ast::{CmpOp, ExprCompare}; #[derive(Default)] -pub struct FormatExprCompare; +pub struct FormatExprCompare { + parentheses: Option, +} + +impl FormatRuleWithOptions> for FormatExprCompare { + type Options = Option; + + fn with_options(mut self, options: Self::Options) -> Self { + self.parentheses = options; + self + } +} impl FormatNodeRule for FormatExprCompare { - fn fmt_fields(&self, _item: &ExprCompare, f: &mut PyFormatter) -> FormatResult<()> { - write!( - f, - [not_yet_implemented_custom_text( - "NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right" - )] - ) + fn fmt_fields(&self, item: &ExprCompare, f: &mut PyFormatter) -> FormatResult<()> { + item.fmt_binary(self.parentheses, f) + } +} + +impl<'ast> FormatBinaryLike<'ast> for ExprCompare { + type FormatOperator = FormatOwnedWithRule>; + + fn binary_layout(&self) -> BinaryLayout { + if self.ops.len() == 1 { + match self.comparators.as_slice() { + [right] => BinaryLayout::from_left_right(&self.left, right), + [..] => BinaryLayout::Default, + } + } else { + BinaryLayout::Default + } + } + + fn fmt_default(&self, f: &mut PyFormatter<'ast, '_>) -> FormatResult<()> { + let ExprCompare { + range: _, + left, + ops, + comparators, + } = self; + + let comments = f.context().comments().clone(); + + write!(f, [group(&left.format())])?; + + assert_eq!(comparators.len(), ops.len()); + + for (operator, comparator) in ops.iter().zip(comparators) { + let leading_comparator_comments = comments.leading_comments(comparator); + if leading_comparator_comments.is_empty() { + write!(f, [soft_line_break_or_space()])?; + } else { + // Format the expressions leading comments **before** the operator + write!( + f, + [ + hard_line_break(), + leading_comments(leading_comparator_comments) + ] + )?; + } + + write!(f, [operator.format(), space(), group(&comparator.format())])?; + } + + Ok(()) + } + + fn left(&self) -> FormatResult<&Expr> { + Ok(self.left.as_ref()) + } + + fn right(&self) -> FormatResult<&Expr> { + self.comparators.last().ok_or(FormatError::SyntaxError) + } + + fn operator(&self) -> Self::FormatOperator { + let op = *self.ops.first().unwrap(); + op.into_format() } } @@ -28,6 +101,61 @@ impl NeedsParentheses for ExprCompare { source: &str, comments: &Comments, ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + parentheses @ Parentheses::Optional => match self.binary_layout() { + BinaryLayout::Default => parentheses, + + BinaryLayout::ExpandRight + | BinaryLayout::ExpandLeft + | BinaryLayout::ExpandRightThenLeft + if self + .comparators + .last() + .map_or(false, |right| comments.has_leading_comments(right)) => + { + parentheses + } + _ => Parentheses::Custom, + }, + parentheses => parentheses, + } + } +} + +#[derive(Copy, Clone)] +pub struct FormatCmpOp; + +impl<'ast> AsFormat> for CmpOp { + type Format<'a> = FormatRefWithRule<'a, CmpOp, FormatCmpOp, PyFormatContext<'ast>>; + + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new(self, FormatCmpOp) + } +} + +impl<'ast> IntoFormat> for CmpOp { + type Format = FormatOwnedWithRule>; + + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new(self, FormatCmpOp) + } +} + +impl FormatRule> for FormatCmpOp { + fn fmt(&self, item: &CmpOp, f: &mut Formatter>) -> FormatResult<()> { + let operator = match item { + CmpOp::Eq => "==", + CmpOp::NotEq => "!=", + CmpOp::Lt => "<", + CmpOp::LtE => "<=", + CmpOp::Gt => ">", + CmpOp::GtE => ">=", + CmpOp::Is => "is", + CmpOp::IsNot => "is not", + CmpOp::In => "in", + CmpOp::NotIn => "not in", + }; + + text(operator).fmt(f) } } diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 6637c091c6..46214396fc 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -76,7 +76,7 @@ impl FormatRule> for FormatExpr { Expr::Await(expr) => expr.format().fmt(f), Expr::Yield(expr) => expr.format().fmt(f), Expr::YieldFrom(expr) => expr.format().fmt(f), - Expr::Compare(expr) => expr.format().fmt(f), + Expr::Compare(expr) => expr.format().with_options(Some(parentheses)).fmt(f), Expr::Call(expr) => expr.format().fmt(f), Expr::FormattedValue(expr) => expr.format().fmt(f), Expr::JoinedStr(expr) => expr.format().fmt(f), diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__bracketmatch_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__bracketmatch_py.snap index 2a11007f5d..a04acefcee 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__bracketmatch_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__bracketmatch_py.snap @@ -19,11 +19,9 @@ lambda x=lambda y={1: 3}: y['x':lambda y: {1: 2}]: x ```diff --- Black +++ Ruff -@@ -1,4 +1,6 @@ +@@ -1,4 +1,4 @@ -for ((x in {}) or {})["a"] in x: -+for ((NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right) or {})[ -+ "NOT_YET_IMPLEMENTED_STRING" -+] in x: ++for ((x in {}) or {})["NOT_YET_IMPLEMENTED_STRING"] in x: pass -pem_spam = lambda l, spam={"x": 3}: not spam.get(l.strip()) -lambda x=lambda y={1: 3}: y["x" : lambda y: {1: 2}]: x @@ -34,9 +32,7 @@ lambda x=lambda y={1: 3}: y['x':lambda y: {1: 2}]: x ## Ruff Output ```py -for ((NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right) or {})[ - "NOT_YET_IMPLEMENTED_STRING" -] in x: +for ((x in {}) or {})["NOT_YET_IMPLEMENTED_STRING"] in x: pass pem_spam = lambda x: True lambda x: True diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap index 5454f791b8..6e43e9b850 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap @@ -249,7 +249,7 @@ instruction()#comment with bad spacing -if "PYTHON" in os.environ: - add_compiler(compiler_from_env()) -+if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: ++if "NOT_YET_IMPLEMENTED_STRING" in os.environ: + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) else: # for compiler in compilers.values(): @@ -283,15 +283,13 @@ instruction()#comment with bad spacing + parameters.children[-1], + ] # type: ignore if ( -- self._proc is not None -+ NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + self._proc is not None # has the child process finished? -- and self._returncode is None -+ and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + and self._returncode is None # the child process has finished, but the # transport hasn't been notified yet? - and self._proc.poll() is None -+ and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++ and NOT_IMPLEMENTED_call() is None ): pass # no newline before or after @@ -349,7 +347,7 @@ instruction()#comment with bad spacing while True: if False: continue -@@ -141,24 +111,18 @@ +@@ -141,24 +111,19 @@ # and round and round we go # let's return @@ -374,13 +372,14 @@ instruction()#comment with bad spacing def _init_host(self, parsed) -> None: - if parsed.hostname is None or not parsed.hostname.strip(): # type: ignore + if ( -+ NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right # type: ignore ++ parsed.hostname ++ is None # type: ignore + or not NOT_IMPLEMENTED_call() + ): pass -@@ -167,7 +131,7 @@ +@@ -167,7 +132,7 @@ ####################### @@ -440,7 +439,7 @@ not_shareables = [ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), ] -if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: +if "NOT_YET_IMPLEMENTED_STRING" in os.environ: NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) else: # for compiler in compilers.values(): @@ -474,12 +473,12 @@ def inline_comments_in_brackets_ruin_everything(): parameters.children[-1], ] # type: ignore if ( - NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + self._proc is not None # has the child process finished? - and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + and self._returncode is None # the child process has finished, but the # transport hasn't been notified yet? - and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + and NOT_IMPLEMENTED_call() is None ): pass # no newline before or after @@ -516,7 +515,8 @@ CONFIG_FILES = [CONFIG_FILE] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: class Test: def _init_host(self, parsed) -> None: if ( - NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right # type: ignore + parsed.hostname + is None # type: ignore or not NOT_IMPLEMENTED_call() ): pass diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap index aa92645b7a..ef02e64a10 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap @@ -83,7 +83,7 @@ def func(): + if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): embedded = [] for exc in exc_value.exceptions: -- if exc not in _seen: + if exc not in _seen: - embedded.append( - # This should be left alone (before) - traceback.TracebackException.from_exception( @@ -97,7 +97,6 @@ def func(): - ) - # This should be left alone (after) - ) -+ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # everything is fine if the expression isn't nested @@ -130,7 +129,7 @@ def func(): if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): embedded = [] for exc in exc_value.exceptions: - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + if exc not in _seen: NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # everything is fine if the expression isn't nested diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap index 6273ad1e97..762929aa96 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap @@ -101,8 +101,7 @@ if __name__ == "__main__": -for i in range(100): +for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # first we do this -- if i % 33 == 0: -+ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + if i % 33 == 0: break # then we do this @@ -117,11 +116,11 @@ if __name__ == "__main__": -try: - with open(some_other_file) as w: - w.write(data) -- --except OSError: -- print("problems") +NOT_YET_IMPLEMENTED_StmtTry +-except OSError: +- print("problems") +- -import sys +NOT_YET_IMPLEMENTED_StmtImport @@ -151,7 +150,7 @@ if __name__ == "__main__": -if __name__ == "__main__": - main() -+if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: ++if __name__ == "NOT_YET_IMPLEMENTED_STRING": + NOT_IMPLEMENTED_call() ``` @@ -170,7 +169,7 @@ while True: for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # first we do this - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + if i % 33 == 0: break # then we do this @@ -223,7 +222,7 @@ def g(): ... -if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: +if __name__ == "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call() ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap index a9b2674d3a..4bb4e71127 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap @@ -105,7 +105,7 @@ def g(): ```diff --- Black +++ Ruff -@@ -1,59 +1,46 @@ +@@ -1,11 +1,11 @@ -"""Docstring.""" +"NOT_YET_IMPLEMENTED_STRING" @@ -121,13 +121,8 @@ def g(): t = leaf.type p = leaf.parent # trailing comment - v = leaf.value - -- if t in ALWAYS_NO_SPACE: -+ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - pass -- if t == token.COMMENT: # another trailing comment -+ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: # another trailing comment +@@ -16,44 +16,51 @@ + if t == token.COMMENT: # another trailing comment return DOUBLESPACE - assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}" @@ -136,12 +131,11 @@ def g(): prev = leaf.prev_sibling if not prev: - prevp = preceding_leaf(p) -- if not prevp or prevp.type in OPENING_BRACKETS: + prevp = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ if not prevp or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + if not prevp or prevp.type in OPENING_BRACKETS: return NO -- if prevp.type == token.EQUAL: + if prevp.type == token.EQUAL: - if prevp.parent and prevp.parent.type in { - syms.typedargslist, - syms.varargslist, @@ -149,11 +143,20 @@ def g(): - syms.arglist, - syms.argument, - }: -+ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ if prevp.parent and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: ++ if ( ++ prevp.parent ++ and prevp.parent.type ++ in { ++ syms.typedargslist, ++ syms.varargslist, ++ syms.parameters, ++ syms.arglist, ++ syms.argument, ++ } ++ ): return NO -- elif prevp.type == token.DOUBLESTAR: + elif prevp.type == token.DOUBLESTAR: - if prevp.parent and prevp.parent.type in { - syms.typedargslist, - syms.varargslist, @@ -161,8 +164,17 @@ def g(): - syms.arglist, - syms.dictsetmaker, - }: -+ elif NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ if prevp.parent and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: ++ if ( ++ prevp.parent ++ and prevp.parent.type ++ in { ++ syms.typedargslist, ++ syms.varargslist, ++ syms.parameters, ++ syms.arglist, ++ syms.dictsetmaker, ++ } ++ ): return NO @@ -181,15 +193,7 @@ def g(): t = leaf.type p = leaf.parent -@@ -61,29 +48,23 @@ - - # Comment because comments - -- if t in ALWAYS_NO_SPACE: -+ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - pass -- if t == token.COMMENT: -+ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: +@@ -67,11 +74,11 @@ return DOUBLESPACE # Another comment because more comments @@ -201,13 +205,12 @@ def g(): - prevp = preceding_leaf(p) + prevp = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -- if not prevp or prevp.type in OPENING_BRACKETS: -+ if not prevp or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + if not prevp or prevp.type in OPENING_BRACKETS: # Start of the line or a bracketed expression. - # More than one line for the comment. +@@ -79,11 +86,15 @@ return NO -- if prevp.type == token.EQUAL: + if prevp.type == token.EQUAL: - if prevp.parent and prevp.parent.type in { - syms.typedargslist, - syms.varargslist, @@ -215,8 +218,17 @@ def g(): - syms.arglist, - syms.argument, - }: -+ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ if prevp.parent and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: ++ if ( ++ prevp.parent ++ and prevp.parent.type ++ in { ++ syms.typedargslist, ++ syms.varargslist, ++ syms.parameters, ++ syms.arglist, ++ syms.argument, ++ } ++ ): return NO ``` @@ -236,9 +248,9 @@ def f(): p = leaf.parent # trailing comment v = leaf.value - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + if t in ALWAYS_NO_SPACE: pass - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: # another trailing comment + if t == token.COMMENT: # another trailing comment return DOUBLESPACE NOT_YET_IMPLEMENTED_StmtAssert @@ -246,15 +258,35 @@ def f(): prev = leaf.prev_sibling if not prev: prevp = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - if not prevp or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + if not prevp or prevp.type in OPENING_BRACKETS: return NO - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - if prevp.parent and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + if prevp.type == token.EQUAL: + if ( + prevp.parent + and prevp.parent.type + in { + syms.typedargslist, + syms.varargslist, + syms.parameters, + syms.arglist, + syms.argument, + } + ): return NO - elif NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - if prevp.parent and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + elif prevp.type == token.DOUBLESTAR: + if ( + prevp.parent + and prevp.parent.type + in { + syms.typedargslist, + syms.varargslist, + syms.parameters, + syms.arglist, + syms.dictsetmaker, + } + ): return NO @@ -273,9 +305,9 @@ def g(): # Comment because comments - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + if t in ALWAYS_NO_SPACE: pass - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + if t == token.COMMENT: return DOUBLESPACE # Another comment because more comments @@ -285,13 +317,23 @@ def g(): if not prev: prevp = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - if not prevp or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + if not prevp or prevp.type in OPENING_BRACKETS: # Start of the line or a bracketed expression. # More than one line for the comment. return NO - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - if prevp.parent and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + if prevp.type == token.EQUAL: + if ( + prevp.parent + and prevp.parent.type + in { + syms.typedargslist, + syms.varargslist, + syms.parameters, + syms.arglist, + syms.argument, + } + ): return NO ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap index b049cca61c..57371f8bf4 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap @@ -281,7 +281,8 @@ last_call() ~int and not v1 ^ 123 + v2 | True (~int) and (not ((v1 ^ (123 + v2)) | True)) -+(really ** -(confusing ** ~(operator**-precedence))) --flags & ~select.EPOLLIN and waiters.write_task is not None +++really ** -confusing ** ~operator**-precedence + flags & ~select.EPOLLIN and waiters.write_task is not None -lambda arg: None -lambda a=True: a -lambda a, b, c=True: a @@ -304,8 +305,6 @@ last_call() -) -{"2.7": dead, "3.7": (long_live or die_hard)} -{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} -++really ** -confusing ** ~operator**-precedence -+flags & ~select.EPOLLIN and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +lambda x: True +lambda x: True +lambda x: True @@ -364,17 +363,17 @@ last_call() - 4, - 5, -] +-[ +- 4, +- *a, +- 5, +-] +[1, 2, 3] +[NOT_YET_IMPLEMENTED_ExprStarred] +[NOT_YET_IMPLEMENTED_ExprStarred] +[NOT_YET_IMPLEMENTED_ExprStarred, 4, 5] +[4, NOT_YET_IMPLEMENTED_ExprStarred, 5] [ -- 4, -- *a, -- 5, --] --[ this_is_a_very_long_variable_which_will_force_a_delimiter_split, element, another, @@ -397,8 +396,21 @@ last_call() - k: v - for k, v in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension -} --Python3 > Python2 > COBOL --Life is Life ++NOT_YET_IMPLEMENTED_ExprSetComp ++NOT_YET_IMPLEMENTED_ExprSetComp ++NOT_YET_IMPLEMENTED_ExprSetComp ++NOT_YET_IMPLEMENTED_ExprSetComp ++[i for i in []] ++[i for i in []] ++[i for i in []] ++[i for i 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} ++{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} + Python3 > Python2 > COBOL + Life is Life -call() -call(arg) -call(kwarg="hey") @@ -415,21 +427,6 @@ last_call() -call(a, *gidgets[:2]) -call(**self.screen_kwargs) -call(b, **self.screen_kwargs) -+NOT_YET_IMPLEMENTED_ExprSetComp -+NOT_YET_IMPLEMENTED_ExprSetComp -+NOT_YET_IMPLEMENTED_ExprSetComp -+NOT_YET_IMPLEMENTED_ExprSetComp -+[i for i in []] -+[i for i in []] -+[i for i in []] -+[i for i 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} -+{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_left < NOT_IMPLEMENTED_right -+NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +NOT_IMPLEMENTED_call() +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) @@ -556,19 +553,10 @@ last_call() -g = 1, *"ten" -what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set( - vars_to_remove -+e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+f = 1, NOT_YET_IMPLEMENTED_ExprStarred -+g = 1, NOT_YET_IMPLEMENTED_ExprStarred -+what_is_up_with_those_new_coord_names = ( -+ (coord_names + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) -+ + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - ) +-) -what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( - vars_to_remove -+what_is_up_with_those_new_coord_names = ( -+ (coord_names | NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) -+ - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - ) +-) -result = ( - session.query(models.Customer.id) - .filter( @@ -576,7 +564,13 @@ last_call() - ) - .order_by(models.Customer.id.asc()) - .all() --) ++e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++f = 1, NOT_YET_IMPLEMENTED_ExprStarred ++g = 1, NOT_YET_IMPLEMENTED_ExprStarred ++what_is_up_with_those_new_coord_names = ( ++ (coord_names + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) ++ + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + ) -result = ( - session.query(models.Customer.id) - .filter( @@ -586,7 +580,10 @@ last_call() - models.Customer.id.asc(), - ) - .all() --) ++what_is_up_with_those_new_coord_names = ( ++ (coord_names | NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) ++ - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + ) -Ø = set() -authors.łukasz.say_thanks() +result = NOT_IMPLEMENTED_call() @@ -641,44 +638,29 @@ last_call() ... for j in 1 + (2 + 3): ... -@@ -272,28 +254,16 @@ +@@ -272,7 +254,7 @@ addr_proto, addr_canonname, addr_sockaddr, -) in socket.getaddrinfo("google.com", "http"): +) in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): pass --a = ( -- aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp -- in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz --) --a = ( -- aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp -- not in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz --) --a = ( -- aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp -- is qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz --) --a = ( -- aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp -- is not qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz --) -+a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + a = ( + aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp +@@ -291,9 +273,9 @@ + is not qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz + ) if ( - threading.current_thread() != threading.main_thread() - and threading.current_thread() != threading.main_thread() - or signal.getsignal(signal.SIGINT) != signal.default_int_handler -+ NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+ and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+ or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++ NOT_IMPLEMENTED_call() != NOT_IMPLEMENTED_call() ++ and NOT_IMPLEMENTED_call() != NOT_IMPLEMENTED_call() ++ or NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) != signal.default_int_handler ): return True if ( -@@ -327,13 +297,18 @@ +@@ -327,13 +309,18 @@ ): return True if ( @@ -700,7 +682,7 @@ last_call() ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n ): return True -@@ -341,7 +316,8 @@ +@@ -341,7 +328,8 @@ ~aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e @@ -710,7 +692,7 @@ last_call() ^ aaaaaaaaaaaaaaaa.i << aaaaaaaaaaaaaaaa.k >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n -@@ -366,5 +342,5 @@ +@@ -366,5 +354,5 @@ ^ bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ^ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ) @@ -756,7 +738,7 @@ not great ~int and not v1 ^ 123 + v2 | True (~int) and (not ((v1 ^ (123 + v2)) | True)) +really ** -confusing ** ~operator**-precedence -flags & ~select.EPOLLIN and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +flags & ~select.EPOLLIN and waiters.write_task is not None lambda x: True lambda x: True lambda x: True @@ -825,8 +807,8 @@ NOT_YET_IMPLEMENTED_ExprSetComp {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} -NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +Python3 > Python2 > COBOL +Life is Life NOT_IMPLEMENTED_call() NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) @@ -980,14 +962,26 @@ for ( addr_sockaddr, ) in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): pass -a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +a = ( + aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp + in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz +) +a = ( + aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp + not in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz +) +a = ( + aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp + is qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz +) +a = ( + aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp + is not qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz +) if ( - NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right - and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right - or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + NOT_IMPLEMENTED_call() != NOT_IMPLEMENTED_call() + and NOT_IMPLEMENTED_call() != NOT_IMPLEMENTED_call() + or NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) != signal.default_int_handler ): return True if ( diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap index cc7ee04dd8..b76dbc5ab7 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap @@ -152,21 +152,22 @@ elif unformatted: # Regression test for https://github.com/psf/black/issues/3184. -@@ -52,29 +34,27 @@ - async def call(param): +@@ -53,28 +35,29 @@ if param: # fmt: off -- if param[0:4] in ( + if param[0:4] in ( - "ABCD", "EFGH" - ) : -+ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: ++ "NOT_YET_IMPLEMENTED_STRING", ++ "NOT_YET_IMPLEMENTED_STRING", ++ ): # fmt: on - print ( "This won't be formatted" ) + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - elif param[0:4] in ("ZZZZ",): - print ( "This won't be formatted either" ) -+ elif NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: ++ elif param[0:4] in ("NOT_YET_IMPLEMENTED_STRING",): + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - print("This will be formatted") @@ -189,7 +190,7 @@ elif unformatted: # fmt: on -@@ -82,6 +62,6 @@ +@@ -82,6 +65,6 @@ if x: return x # fmt: off @@ -239,11 +240,14 @@ class A: async def call(param): if param: # fmt: off - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + if param[0:4] in ( + "NOT_YET_IMPLEMENTED_STRING", + "NOT_YET_IMPLEMENTED_STRING", + ): # fmt: on NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - elif NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + elif param[0:4] in ("NOT_YET_IMPLEMENTED_STRING",): NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap index d5245d0799..fb89db8d6f 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap @@ -411,7 +411,7 @@ d={'a':1, - because . the . handling . inside . generate_ignored_nodes() - now . considers . multiple . fmt . directives . within . one . prefix + this = NOT_IMPLEMENTED_call() -+ and_ = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++ and_ = indeed.it is not formatted + NOT_IMPLEMENTED_call() + now.considers.multiple.fmt.directives.within.one.prefix # fmt: on @@ -663,7 +663,7 @@ def on_and_off_broken(): # fmt: on # fmt: off this = NOT_IMPLEMENTED_call() - and_ = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + and_ = indeed.it is not formatted NOT_IMPLEMENTED_call() now.considers.multiple.fmt.directives.within.one.prefix # fmt: on diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap index 2247055281..32b6d86155 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap @@ -22,15 +22,14 @@ else: ```diff --- Black +++ Ruff -@@ -1,9 +1,9 @@ +@@ -1,9 +1,10 @@ a, b, c = 3, 4, 5 if ( -- a == 3 + a == 3 - and b != 9 # fmt: skip -- and c is not None -+ NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+ and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right # fmt: skip -+ and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++ and b ++ != 9 # fmt: skip + and c is not None ): - print("I'm good!") + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) @@ -44,9 +43,10 @@ else: ```py a, b, c = 3, 4, 5 if ( - NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right - and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right # fmt: skip - and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + a == 3 + and b + != 9 # fmt: skip + and c is not None ): NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) else: diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap index 93b9b22acd..a13146444a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap @@ -107,7 +107,7 @@ with hmm_but_this_should_get_two_preceding_newlines(): -if os.name == "posix": - import termios -+if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: ++if os.name == "NOT_YET_IMPLEMENTED_STRING": + NOT_YET_IMPLEMENTED_StmtImport def i_should_be_followed_by_only_one_newline(): @@ -124,7 +124,7 @@ with hmm_but_this_should_get_two_preceding_newlines(): - - def i_should_be_followed_by_only_one_newline(): - pass -+elif NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: ++elif os.name == "NOT_YET_IMPLEMENTED_STRING": + NOT_YET_IMPLEMENTED_StmtTry elif False: @@ -173,12 +173,12 @@ def h(): NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: +if os.name == "NOT_YET_IMPLEMENTED_STRING": NOT_YET_IMPLEMENTED_StmtImport def i_should_be_followed_by_only_one_newline(): pass -elif NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: +elif os.name == "NOT_YET_IMPLEMENTED_STRING": NOT_YET_IMPLEMENTED_StmtTry elif False: diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap index 965322236b..e44027943b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap @@ -94,7 +94,7 @@ some_module.some_function( } tup = ( 1, -@@ -24,45 +24,23 @@ +@@ -24,45 +24,37 @@ def f( a: int = 1, ): @@ -112,9 +112,14 @@ some_module.some_function( - "a": 1, - "b": 2, - }["a"] -- if ( -- a -- == { ++ "NOT_YET_IMPLEMENTED_STRING": 1, ++ "NOT_YET_IMPLEMENTED_STRING": 2, ++ }[ ++ "NOT_YET_IMPLEMENTED_STRING" ++ ] + if ( + a + == { - "a": 1, - "b": 2, - "c": 3, @@ -124,13 +129,18 @@ some_module.some_function( - "g": 7, - "h": 8, - }["a"] -- ): -+ "NOT_YET_IMPLEMENTED_STRING": 1, -+ "NOT_YET_IMPLEMENTED_STRING": 2, -+ }[ -+ "NOT_YET_IMPLEMENTED_STRING" -+ ] -+ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: ++ "NOT_YET_IMPLEMENTED_STRING": 1, ++ "NOT_YET_IMPLEMENTED_STRING": 2, ++ "NOT_YET_IMPLEMENTED_STRING": 3, ++ "NOT_YET_IMPLEMENTED_STRING": 4, ++ "NOT_YET_IMPLEMENTED_STRING": 5, ++ "NOT_YET_IMPLEMENTED_STRING": 6, ++ "NOT_YET_IMPLEMENTED_STRING": 7, ++ "NOT_YET_IMPLEMENTED_STRING": 8, ++ }[ ++ "NOT_YET_IMPLEMENTED_STRING" ++ ] + ): pass @@ -152,7 +162,7 @@ some_module.some_function( } -@@ -80,35 +58,16 @@ +@@ -80,35 +72,16 @@ pass @@ -231,7 +241,21 @@ def f( }[ "NOT_YET_IMPLEMENTED_STRING" ] - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + if ( + a + == { + "NOT_YET_IMPLEMENTED_STRING": 1, + "NOT_YET_IMPLEMENTED_STRING": 2, + "NOT_YET_IMPLEMENTED_STRING": 3, + "NOT_YET_IMPLEMENTED_STRING": 4, + "NOT_YET_IMPLEMENTED_STRING": 5, + "NOT_YET_IMPLEMENTED_STRING": 6, + "NOT_YET_IMPLEMENTED_STRING": 7, + "NOT_YET_IMPLEMENTED_STRING": 8, + }[ + "NOT_YET_IMPLEMENTED_STRING" + ] + ): pass diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap index 70995c951c..867b4314cd 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap @@ -99,13 +99,12 @@ return np.divide( +i = NOT_IMPLEMENTED_call() ** 5 +j = NOT_IMPLEMENTED_call().name ** 5 +k = [i for i in []] -+l = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++l = mod.weights_[0] == NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) m = [([2**63], [1, 2**63])] --n = count <= 10**5 + n = count <= 10**5 -o = settings(max_examples=10**6) -p = {(k, k**2): v**2 for k, v in pairs} -q = [10**i for i in range(6)] -+n = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +o = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} +q = [i for i in []] @@ -131,13 +130,12 @@ return np.divide( +i = NOT_IMPLEMENTED_call() ** 5.0 +j = NOT_IMPLEMENTED_call().name ** 5.0 +k = [i for i in []] -+l = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++l = mod.weights_[0] == NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) m = [([2.0**63.0], [1.0, 2**63.0])] --n = count <= 10**5.0 + n = count <= 10**5.0 -o = settings(max_examples=10**6.0) -p = {(k, k**2): v**2.0 for k, v in pairs} -q = [10.5**i for i in range(6)] -+n = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +o = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} +q = [i for i in []] @@ -187,9 +185,9 @@ h = 5 ** NOT_IMPLEMENTED_call() i = NOT_IMPLEMENTED_call() ** 5 j = NOT_IMPLEMENTED_call().name ** 5 k = [i for i in []] -l = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +l = mod.weights_[0] == NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) m = [([2**63], [1, 2**63])] -n = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +n = count <= 10**5 o = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} q = [i for i in []] @@ -206,9 +204,9 @@ h = 5.0 ** NOT_IMPLEMENTED_call() i = NOT_IMPLEMENTED_call() ** 5.0 j = NOT_IMPLEMENTED_call().name ** 5.0 k = [i for i in []] -l = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +l = mod.weights_[0] == NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) m = [([2.0**63.0], [1.0, 2**63.0])] -n = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right +n = count <= 10**5.0 o = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} q = [i for i in []] diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap index 27eeb9155b..1bf0525ecc 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap @@ -179,21 +179,21 @@ with open("/path/to/file.txt", mode="r") as read_file: -if random.randint(0, 3) == 0: - print("The new line above me is about to be removed!") -+if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: ++if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) == 0: + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -if random.randint(0, 3) == 0: - print("The new lines above me is about to be removed!") -+if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: ++if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) == 0: + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -if random.randint(0, 3) == 0: - if random.uniform(0, 1) > 0.5: - print("Two lines above me are about to be removed!") -+if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: ++if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) == 0: ++ if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) > 0.5: + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) @@ -273,16 +273,16 @@ for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: +if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) == 0: NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: +if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) == 0: NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: +if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) == 0: + if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) > 0.5: NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap index ae15220407..914dc2e5fd 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap @@ -95,10 +95,9 @@ x[ +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[not so_simple : 1 < val <= 10] -slice[(1 for i in range(42)) : x] -slice[:: [i for i in range(42)]] -+slice[not so_simple : NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right] +slice[(i for i in []) : x] +slice[ :: [i for i in []]] @@ -161,7 +160,7 @@ slice[ : -1 :] slice[lambda x: True : lambda x: True] slice[lambda x: True :, None::] slice[1 or 2 : True and False] -slice[not so_simple : NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right] +slice[not so_simple : 1 < val <= 10] slice[(i for i in []) : x] slice[ :: [i for i in []]] diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap index 5aa9c0c161..b270fd1801 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap @@ -65,7 +65,7 @@ assert ( importA 0 -@@ -24,35 +14,15 @@ +@@ -24,35 +14,34 @@ class A: def foo(self): @@ -97,8 +97,27 @@ assert ( - othr.meta_data, - othr.schedule, + return ( -+ NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+ and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++ 1 == 2 ++ and ( ++ name, ++ description, ++ self.default, ++ self.selected, ++ self.auto_generated, ++ self.parameters, ++ self.meta_data, ++ self.schedule, ++ ) ++ == ( ++ name, ++ description, ++ othr.default, ++ othr.selected, ++ othr.auto_generated, ++ othr.parameters, ++ othr.meta_data, ++ othr.schedule, ++ ) ) @@ -134,8 +153,27 @@ class A: def test(self, othr): return ( - NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right - and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + 1 == 2 + and ( + name, + description, + self.default, + self.selected, + self.auto_generated, + self.parameters, + self.meta_data, + self.schedule, + ) + == ( + name, + description, + othr.default, + othr.selected, + othr.auto_generated, + othr.parameters, + othr.meta_data, + othr.schedule, + ) ) diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap index c3a440c26d..f331e7a484 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap @@ -38,13 +38,14 @@ class A: ```diff --- Black +++ Ruff -@@ -1,34 +1,25 @@ +@@ -1,34 +1,32 @@ -if e1234123412341234.winerror not in ( - _winapi.ERROR_SEM_TIMEOUT, - _winapi.ERROR_PIPE_BUSY, -) or _check_timeout(t): +if ( -+ NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++ e1234123412341234.winerror ++ not in (_winapi.ERROR_SEM_TIMEOUT, _winapi.ERROR_PIPE_BUSY) + or NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +): pass @@ -82,7 +83,13 @@ class A: - ) < self.connection.mysql_version < (10, 5, 2): + if ( + self.connection.mysql_is_mariadb -+ and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++ and ( ++ 10, ++ 4, ++ 3, ++ ) ++ < self.connection.mysql_version ++ < (10, 5, 2) + ): pass ``` @@ -91,7 +98,8 @@ class A: ```py if ( - NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + e1234123412341234.winerror + not in (_winapi.ERROR_SEM_TIMEOUT, _winapi.ERROR_PIPE_BUSY) or NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ): pass @@ -112,7 +120,13 @@ class A: def b(self): if ( self.connection.mysql_is_mariadb - and NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + and ( + 10, + 4, + 3, + ) + < self.connection.mysql_version + < (10, 5, 2) ): pass ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens2_py.snap index 2c3f46e706..3186661eb0 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens2_py.snap @@ -23,8 +23,8 @@ if (e123456.get_tk_patchlevel() >= (8, 6, 0, 'final') or - 8, -) <= get_tk_patchlevel() < (8, 6): +if ( -+ NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+ or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right ++ NOT_IMPLEMENTED_call() >= (8, 6, 0, "NOT_YET_IMPLEMENTED_STRING") ++ or (8, 5, 8) <= NOT_IMPLEMENTED_call() < (8, 6) +): pass ``` @@ -33,8 +33,8 @@ if (e123456.get_tk_patchlevel() >= (8, 6, 0, 'final') or ```py if ( - NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right - or NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right + NOT_IMPLEMENTED_call() >= (8, 6, 0, "NOT_YET_IMPLEMENTED_STRING") + or (8, 5, 8) <= NOT_IMPLEMENTED_call() < (8, 6) ): pass ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap index 82106ab6d3..e55f432f29 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap @@ -252,7 +252,13 @@ aaaaaaaaaaaaaa + NOT_YET_IMPLEMENTED_ExprSetComp # But only for expressions that have a statement parent. not (aaaaaaaaaaaaaa + NOT_YET_IMPLEMENTED_ExprSetComp) -[NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right] +[ + a + + [ + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ] + in c, +] # leading comment diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__compare_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__compare_py.snap new file mode 100644 index 0000000000..7abb3e4b1e --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__compare_py.snap @@ -0,0 +1,189 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +--- +## Input +```py +a == b +a != b +a < b +a <= b +a > b +a >= b +a is b +a is not b +a in b +a not in b + +(a == + # comment + b +) + +(a == # comment + b + ) + +a < b > c == d + +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa < bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb > ccccccccccccccccccccccccccccc == ddddddddddddddddddddd + +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa < [ + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ff, +] < [ccccccccccccccccccccccccccccc, dddd] < ddddddddddddddddddddddddddddddddddddddddddd + +return 1 == 2 and ( + name, + description, + self_default, + self_selected, + self_auto_generated, + self_parameters, + self_meta_data, + self_schedule, +) == ( + name, + description, + othr_default, + othr_selected, + othr_auto_generated, + othr_parameters, + othr_meta_data, + othr_schedule, +) + +(name, description, self_default, self_selected, self_auto_generated, self_parameters, self_meta_data, self_schedule) == (name, description, other_default, othr_selected, othr_auto_generated, othr_parameters, othr_meta_data, othr_schedule) +((name, description, self_default, self_selected, self_auto_generated, self_parameters, self_meta_data, self_schedule) == (name, description, other_default, othr_selected, othr_auto_generated, othr_parameters, othr_meta_data, othr_schedule)) + +[ + ( + a + + [ + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ] + >= c + ) +] +``` + + + +## Output +```py +a == b +a != b +a < b +a <= b +a > b +a >= b +a is b +a is not b +a in b +a not in b + +( + a + # comment + == b +) + +( + a # comment + == b +) + +a < b > c == d + +( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + < bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + > ccccccccccccccccccccccccccccc + == ddddddddddddddddddddd +) + +( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + < [bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, ff] + < [ccccccccccccccccccccccccccccc, dddd] + < ddddddddddddddddddddddddddddddddddddddddddd +) + +return ( + 1 == 2 + and ( + name, + description, + self_default, + self_selected, + self_auto_generated, + self_parameters, + self_meta_data, + self_schedule, + ) + == ( + name, + description, + othr_default, + othr_selected, + othr_auto_generated, + othr_parameters, + othr_meta_data, + othr_schedule, + ) +) + +( + name, + description, + self_default, + self_selected, + self_auto_generated, + self_parameters, + self_meta_data, + self_schedule, +) == ( + name, + description, + other_default, + othr_selected, + othr_auto_generated, + othr_parameters, + othr_meta_data, + othr_schedule, +) +( + ( + name, + description, + self_default, + self_selected, + self_auto_generated, + self_parameters, + self_meta_data, + self_schedule, + ) + == ( + name, + description, + other_default, + othr_selected, + othr_auto_generated, + othr_parameters, + othr_meta_data, + othr_schedule, + ) +) + +[ + ( + a + + [ + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ] + >= c + ), +] +``` + + diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap index ef06f0e165..8bd96653d9 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap @@ -74,12 +74,12 @@ x = 3 ## Output ```py -if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: # trailing if condition +if x == y: # trailing if condition pass # trailing `pass` comment # Root `if` trailing comment # Leading elif comment -elif NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: # trailing elif condition +elif x < y: # trailing elif condition pass # `elif` trailing comment @@ -89,11 +89,11 @@ else: # trailing else condition # `else` trailing comment -if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: +if x == y: + if y == z: ... - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + if a == b: ... else: # trailing comment ... diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__trivia_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__trivia_py.snap index df8702bcb0..1d1c0586db 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__trivia_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__trivia_py.snap @@ -62,7 +62,7 @@ class Test: c = 30 -while NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: +while a == 10: ... # trailing comment with one line before @@ -71,7 +71,7 @@ while NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: d = 40 -while NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: +while b == 20: ... # no empty line before From c52aa8f0656f0d17817cbde52c73e362b282a913 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 23 Jun 2023 09:46:05 +0200 Subject: [PATCH 207/447] Basic string formatting ## Summary This PR implements formatting for non-f-string Strings that do not use implicit concatenation. Docstring formatting is out of the scope of this PR. ## Test Plan I added a few tests for simple string literals. ## Performance Ouch. This is hitting performance somewhat hard. This is probably because we now iterate each string a couple of times: 1. To detect if it is an implicit string continuation 2. To detect if the string contains any new lines 3. To detect the preferred quote 4. To normalize the string Edit: I integrated the detection of newlines into the preferred quote detection so that we only iterate the string three time. We can probably do better by merging the implicit string continuation with the quote detection and new line detection by iterating till the end of the string part and returning the offset. We then use our simple tokenizer to skip over any comments or whitespace until we find the first non trivia token. From there we keep continue doing this in a loop until we reach the end o the string. I'll leave this improvement for later. --- Cargo.lock | 1 + crates/ruff_python_ast/src/str.rs | 13 +- crates/ruff_python_formatter/Cargo.toml | 1 + .../test/fixtures/ruff/expression/string.py | 52 +++ .../src/expression/expr_constant.rs | 5 +- .../src/expression/mod.rs | 1 + .../src/expression/string.rs | 318 ++++++++++++++++ crates/ruff_python_formatter/src/lib.rs | 56 +-- ...r__tests__black_test__bracketmatch_py.snap | 5 +- ...black_test__class_methods_new_line_py.snap | 87 +---- ...er__tests__black_test__collections_py.snap | 45 +-- ...tter__tests__black_test__comments2_py.snap | 79 +--- ...tter__tests__black_test__comments3_py.snap | 16 +- ...tter__tests__black_test__comments5_py.snap | 8 +- ...tter__tests__black_test__comments6_py.snap | 4 +- ...tter__tests__black_test__comments9_py.snap | 11 +- ..._test__comments_non_breaking_space_py.snap | 14 +- ...atter__tests__black_test__comments_py.snap | 84 ++--- ...est__composition_no_trailing_comma_py.snap | 58 +-- ...er__tests__black_test__composition_py.snap | 58 +-- ...ing_no_extra_empty_line_before_eof_py.snap | 46 --- ...sts__black_test__docstring_preview_py.snap | 44 +-- ...tter__tests__black_test__docstring_py.snap | 347 ++++++++++-------- ...er__tests__black_test__empty_lines_py.snap | 47 +-- ...ter__tests__black_test__expression_py.snap | 159 ++++---- ...tter__tests__black_test__fmtonoff2_py.snap | 5 +- ...tter__tests__black_test__fmtonoff5_py.snap | 21 +- ...atter__tests__black_test__fmtonoff_py.snap | 118 +++--- ...atter__tests__black_test__fmtskip2_py.snap | 47 +-- ...atter__tests__black_test__fmtskip3_py.snap | 13 +- ...atter__tests__black_test__fmtskip7_py.snap | 10 +- ...tter__tests__black_test__function2_py.snap | 18 +- ...atter__tests__black_test__function_py.snap | 35 +- ...lack_test__function_trailing_comma_py.snap | 107 ++---- ..._tests__black_test__import_spacing_py.snap | 6 +- ...ests__black_test__power_op_spacing_py.snap | 10 +- ...__tests__black_test__remove_parens_py.snap | 13 +- ...tests__black_test__string_prefixes_py.snap | 32 +- ...t__trailing_comma_optional_parens1_py.snap | 4 +- ...t__trailing_comma_optional_parens2_py.snap | 4 +- ...t__trailing_comma_optional_parens3_py.snap | 8 +- ...__trailing_commas_in_leading_parts_py.snap | 14 +- ...ests__ruff_test__expression__slice_py.snap | 46 +-- ...sts__ruff_test__expression__string_py.snap | 119 ++++++ ...ests__ruff_test__expression__tuple_py.snap | 165 +++++---- ...r__tests__ruff_test__statement__if_py.snap | 10 +- 46 files changed, 1278 insertions(+), 1086 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py create mode 100644 crates/ruff_python_formatter/src/expression/string.rs delete mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_no_extra_empty_line_before_eof_py.snap create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__string_py.snap diff --git a/Cargo.lock b/Cargo.lock index 43e96c3a5a..61791812a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2034,6 +2034,7 @@ name = "ruff_python_formatter" version = "0.0.0" dependencies = [ "anyhow", + "bitflags 2.3.1", "clap", "countme", "insta", diff --git a/crates/ruff_python_ast/src/str.rs b/crates/ruff_python_ast/src/str.rs index bc484d8569..5421641c21 100644 --- a/crates/ruff_python_ast/src/str.rs +++ b/crates/ruff_python_ast/src/str.rs @@ -220,11 +220,15 @@ pub fn is_implicit_concatenation(content: &str) -> bool { let mut rest = &content[leading_quote_str.len()..content.len() - trailing_quote_str.len()]; while let Some(index) = rest.find(trailing_quote_str) { let mut chars = rest[..index].chars().rev(); + if let Some('\\') = chars.next() { - // If the quote is double-escaped, then it's _not_ escaped, so the string is - // implicitly concatenated. - if let Some('\\') = chars.next() { - return true; + if chars.next() == Some('\\') { + // Either `\\'` or `\\\'` need to test one more character + + // If the quote is preceded by `//` then it is not escaped, instead the backslash is escaped. + if chars.next() != Some('\\') { + return true; + } } } else { // If the quote is _not_ escaped, then it's implicitly concatenated. @@ -299,5 +303,6 @@ mod tests { // Negative cases with escaped quotes. assert!(!is_implicit_concatenation(r#""abc\"def""#)); + assert!(!is_implicit_concatenation(r#"'\\\' ""'"#)); } } diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index 708b77c8f9..0701907975 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -17,6 +17,7 @@ ruff_python_ast = { path = "../ruff_python_ast" } ruff_text_size = { workspace = true } anyhow = { workspace = true } +bitflags = { workspace = true } clap = { workspace = true } countme = "3.0.1" is-macro = { workspace = true } diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py new file mode 100644 index 0000000000..7ae9032c80 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py @@ -0,0 +1,52 @@ +"' test" +'" test' + +"\" test" +'\' test' + +# Prefer single quotes for string with more double quotes +"' \" \" '' \" \" '" + +# Prefer double quotes for string with more single quotes +'\' " " \'\' " " \'' + +# Prefer double quotes for string with equal amount of single and double quotes +'" \' " " \'\'' +"' \" '' \" \" '" + +"\\' \"\"" +'\\\' ""' + + +u"Test" +U"Test" + +r"Test" +R"Test" + +'This string will not include \ +backslashes or newline characters.' + +if True: + 'This string will not include \ + backslashes or newline characters.' + +"""Multiline +String \" +""" + +'''Multiline +String \' +''' + +'''Multiline +String "" +''' + +'''Multiline +String """ +''' + +'''Multiline +String \"\"\" +''' diff --git a/crates/ruff_python_formatter/src/expression/expr_constant.rs b/crates/ruff_python_formatter/src/expression/expr_constant.rs index bdb9b7117c..d1d7de417b 100644 --- a/crates/ruff_python_formatter/src/expression/expr_constant.rs +++ b/crates/ruff_python_formatter/src/expression/expr_constant.rs @@ -2,6 +2,7 @@ use crate::comments::Comments; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; +use crate::expression::string::FormatString; use crate::prelude::*; use crate::{not_yet_implemented_custom_text, verbatim_text, FormatNodeRule}; use ruff_formatter::write; @@ -28,9 +29,7 @@ impl FormatNodeRule for FormatExprConstant { Constant::Int(_) | Constant::Float(_) | Constant::Complex { .. } => { write!(f, [verbatim_text(item)]) } - Constant::Str(_) => { - not_yet_implemented_custom_text(r#""NOT_YET_IMPLEMENTED_STRING""#).fmt(f) - } + Constant::Str(_) => FormatString::new(item).fmt(f), Constant::Bytes(_) => { not_yet_implemented_custom_text(r#"b"NOT_YET_IMPLEMENTED_BYTE_STRING""#).fmt(f) } diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 46214396fc..06292c6447 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -37,6 +37,7 @@ pub(crate) mod expr_unary_op; pub(crate) mod expr_yield; pub(crate) mod expr_yield_from; pub(crate) mod parentheses; +mod string; #[derive(Default)] pub struct FormatExpr { diff --git a/crates/ruff_python_formatter/src/expression/string.rs b/crates/ruff_python_formatter/src/expression/string.rs new file mode 100644 index 0000000000..116226fc1d --- /dev/null +++ b/crates/ruff_python_formatter/src/expression/string.rs @@ -0,0 +1,318 @@ +use crate::prelude::*; +use crate::{not_yet_implemented_custom_text, QuoteStyle}; +use bitflags::bitflags; +use ruff_formatter::{write, FormatError}; +use ruff_python_ast::str::is_implicit_concatenation; +use ruff_text_size::{TextLen, TextRange, TextSize}; +use rustpython_parser::ast::{ExprConstant, Ranged}; +use std::borrow::Cow; + +pub(super) struct FormatString { + string_range: TextRange, +} + +impl FormatString { + pub(super) fn new(constant: &ExprConstant) -> Self { + debug_assert!(constant.value.is_str()); + Self { + string_range: constant.range(), + } + } +} + +impl Format> for FormatString { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let string_content = f.context().locator().slice(self.string_range); + + if is_implicit_concatenation(string_content) { + not_yet_implemented_custom_text(r#""NOT_YET_IMPLEMENTED" "IMPLICIT_CONCATENATION""#) + .fmt(f) + } else { + FormatStringPart::new(self.string_range).fmt(f) + } + } +} + +struct FormatStringPart { + part_range: TextRange, +} + +impl FormatStringPart { + const fn new(range: TextRange) -> Self { + Self { part_range: range } + } +} + +impl Format> for FormatStringPart { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let string_content = f.context().locator().slice(self.part_range); + + let prefix = StringPrefix::parse(string_content); + let after_prefix = &string_content[usize::from(prefix.text_len())..]; + + let quotes = StringQuotes::parse(after_prefix).ok_or(FormatError::SyntaxError)?; + let relative_raw_content_range = TextRange::new( + prefix.text_len() + quotes.text_len(), + string_content.text_len() - quotes.text_len(), + ); + let raw_content_range = relative_raw_content_range + self.part_range.start(); + + let raw_content = &string_content[relative_raw_content_range]; + let (preferred_quotes, contains_newlines) = preferred_quotes(raw_content, quotes); + + write!(f, [prefix, preferred_quotes])?; + + let normalized = normalize_quotes(raw_content, preferred_quotes); + + match normalized { + Cow::Borrowed(_) => { + source_text_slice(raw_content_range, contains_newlines).fmt(f)?; + } + Cow::Owned(normalized) => { + dynamic_text(&normalized, Some(raw_content_range.start())).fmt(f)?; + } + } + + preferred_quotes.fmt(f) + } +} + +bitflags! { + #[derive(Copy, Clone, Debug)] + struct StringPrefix: u8 { + const UNICODE = 0b0000_0001; + /// `r"test"` + const RAW = 0b0000_0010; + /// `R"test" + const RAW_UPPER = 0b0000_0100; + const BYTE = 0b0000_1000; + const F_STRING = 0b0001_0000; + } +} + +impl StringPrefix { + fn parse(input: &str) -> StringPrefix { + let chars = input.chars(); + let mut prefix = StringPrefix::empty(); + + for c in chars { + let flag = match c { + 'u' | 'U' => StringPrefix::UNICODE, + 'f' | 'F' => StringPrefix::F_STRING, + 'b' | 'B' => StringPrefix::BYTE, + 'r' => StringPrefix::RAW, + 'R' => StringPrefix::RAW_UPPER, + '\'' | '"' => break, + c => { + unreachable!( + "Unexpected character '{c}' terminating the prefix of a string literal" + ); + } + }; + + prefix |= flag; + } + + prefix + } + + const fn text_len(self) -> TextSize { + TextSize::new(self.bits().count_ones()) + } +} + +impl Format> for StringPrefix { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + // Retain the casing for the raw prefix: + // https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#r-strings-and-r-strings + if self.contains(StringPrefix::RAW) { + text("r").fmt(f)?; + } else if self.contains(StringPrefix::RAW_UPPER) { + text("R").fmt(f)?; + } + + if self.contains(StringPrefix::BYTE) { + text("b").fmt(f)?; + } + + if self.contains(StringPrefix::F_STRING) { + text("f").fmt(f)?; + } + + // Remove the unicode prefix `u` if any because it is meaningless in Python 3+. + + Ok(()) + } +} + +/// Detects the preferred quotes for `input`. +/// * single quoted strings: The preferred quote style is the one that requires less escape sequences. +/// * triple quoted strings: Use double quotes except the string contains a sequence of `"""`. +fn preferred_quotes(input: &str, quotes: StringQuotes) -> (StringQuotes, ContainsNewlines) { + let mut contains_newlines = ContainsNewlines::No; + + let preferred_style = if quotes.triple { + let mut use_single_quotes = false; + let mut chars = input.chars().peekable(); + + while let Some(c) = chars.next() { + match c { + '\n' | '\r' => contains_newlines = ContainsNewlines::Yes, + '\\' => { + if matches!(chars.peek(), Some('"' | '\\')) { + chars.next(); + } + } + '"' => { + match chars.peek().copied() { + Some('"') => { + // `""` + chars.next(); + + if chars.peek().copied() == Some('"') { + // `"""` + chars.next(); + use_single_quotes = true; + } + } + Some(_) => { + // Single quote, this is ok + } + None => { + // Trailing quote at the end of the comment + use_single_quotes = true; + } + } + } + _ => continue, + } + } + + if use_single_quotes { + QuoteStyle::Single + } else { + QuoteStyle::Double + } + } else { + let mut single_quotes = 0u32; + let mut double_quotes = 0u32; + + for c in input.chars() { + match c { + '\'' => { + single_quotes += 1; + } + + '"' => { + double_quotes += 1; + } + + '\n' | '\r' => { + contains_newlines = ContainsNewlines::Yes; + } + + _ => continue, + } + } + + if double_quotes > single_quotes { + QuoteStyle::Single + } else { + QuoteStyle::Double + } + }; + + ( + StringQuotes { + triple: quotes.triple, + style: preferred_style, + }, + contains_newlines, + ) +} + +#[derive(Copy, Clone, Debug)] +struct StringQuotes { + triple: bool, + style: QuoteStyle, +} + +impl StringQuotes { + fn parse(input: &str) -> Option { + let mut chars = input.chars(); + + let quote_char = chars.next()?; + let style = QuoteStyle::try_from(quote_char).ok()?; + + let triple = chars.next() == Some(quote_char) && chars.next() == Some(quote_char); + + Some(Self { triple, style }) + } + + const fn text_len(self) -> TextSize { + if self.triple { + TextSize::new(3) + } else { + TextSize::new(1) + } + } +} + +impl Format> for StringQuotes { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let quotes = match (self.style, self.triple) { + (QuoteStyle::Single, false) => "'", + (QuoteStyle::Single, true) => "'''", + (QuoteStyle::Double, false) => "\"", + (QuoteStyle::Double, true) => "\"\"\"", + }; + + text(quotes).fmt(f) + } +} + +/// Adds the necessary quote escapes and removes unnecessary escape sequences when quoting `input` +/// with the provided `style`. +fn normalize_quotes(input: &str, quotes: StringQuotes) -> Cow { + if quotes.triple { + Cow::Borrowed(input) + } else { + // The normalized string if `input` is not yet normalized. + // `output` must remain empty if `input` is already normalized. + let mut output = String::new(); + // Tracks the last index of `input` that has been written to `output`. + // If `last_index` is `0` at the end, then the input is already normalized and can be returned as is. + let mut last_index = 0; + + let style = quotes.style; + let preferred_quote = style.as_char(); + let opposite_quote = style.opposite().as_char(); + + let mut chars = input.char_indices(); + + while let Some((index, c)) = chars.next() { + if c == '\\' { + if let Some((_, next)) = chars.next() { + if next == opposite_quote { + // Remove the escape by ending before the backslash and starting again with the quote + output.push_str(&input[last_index..index]); + last_index = index + '\\'.len_utf8(); + } + } + } else if c == preferred_quote { + // Escape the quote + output.push_str(&input[last_index..index]); + output.push('\\'); + output.push(c); + last_index = index + preferred_quote.len_utf8(); + } + } + + if last_index == 0 { + Cow::Borrowed(input) + } else { + output.push_str(&input[last_index..]); + Cow::Owned(output) + } + } +} diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 0faf824f09..790b08faa9 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -226,6 +226,41 @@ impl Format> for VerbatimText { } } +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum QuoteStyle { + Single, + Double, +} + +impl QuoteStyle { + pub const fn as_char(self) -> char { + match self { + QuoteStyle::Single => '\'', + QuoteStyle::Double => '"', + } + } + + #[must_use] + pub const fn opposite(self) -> QuoteStyle { + match self { + QuoteStyle::Single => QuoteStyle::Double, + QuoteStyle::Double => QuoteStyle::Single, + } + } +} + +impl TryFrom for QuoteStyle { + type Error = (); + + fn try_from(value: char) -> std::result::Result { + match value { + '\'' => Ok(QuoteStyle::Single), + '"' => Ok(QuoteStyle::Double), + _ => Err(()), + } + } +} + #[cfg(test)] mod tests { use anyhow::Result; @@ -342,29 +377,8 @@ if True: let printed = format_module(&content)?; let formatted_code = printed.as_code(); - let reformatted = - format_module(formatted_code).unwrap_or_else(|err| panic!("Expected formatted code to be valid syntax but it contains syntax errors: {err}\n{formatted_code}")); - ensure_stability_when_formatting_twice(formatted_code); - if reformatted.as_code() != formatted_code { - let diff = TextDiff::from_lines(formatted_code, reformatted.as_code()) - .unified_diff() - .header("Formatted once", "Formatted twice") - .to_string(); - panic!( - r#"Reformatting the formatted code a second time resulted in formatting changes. -{diff} - -Formatted once: -{formatted_code} - -Formatted twice: -{}"#, - reformatted.as_code() - ); - } - let snapshot = format!( r#"## Input {} diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__bracketmatch_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__bracketmatch_py.snap index a04acefcee..548945997c 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__bracketmatch_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__bracketmatch_py.snap @@ -20,8 +20,7 @@ lambda x=lambda y={1: 3}: y['x':lambda y: {1: 2}]: x --- Black +++ Ruff @@ -1,4 +1,4 @@ --for ((x in {}) or {})["a"] in x: -+for ((x in {}) or {})["NOT_YET_IMPLEMENTED_STRING"] in x: + for ((x in {}) or {})["a"] in x: pass -pem_spam = lambda l, spam={"x": 3}: not spam.get(l.strip()) -lambda x=lambda y={1: 3}: y["x" : lambda y: {1: 2}]: x @@ -32,7 +31,7 @@ lambda x=lambda y={1: 3}: y['x':lambda y: {1: 2}]: x ## Ruff Output ```py -for ((x in {}) or {})["NOT_YET_IMPLEMENTED_STRING"] in x: +for ((x in {}) or {})["a"] in x: pass pem_spam = lambda x: True lambda x: True diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap index 3ea7548946..d519958e1a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap @@ -113,91 +113,38 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: ```diff --- Black +++ Ruff -@@ -7,7 +7,7 @@ - - - class ClassWithJustTheDocstring: -- """Just a docstring.""" -+ "NOT_YET_IMPLEMENTED_STRING" - - - class ClassWithInit: -@@ -16,7 +16,7 @@ - - - class ClassWithTheDocstringAndInit: -- """Just a docstring.""" -+ "NOT_YET_IMPLEMENTED_STRING" - - def __init__(self): - pass -@@ -30,8 +30,7 @@ - +@@ -31,7 +31,6 @@ class ClassWithInitAndVarsAndDocstring: -- """Test class""" + """Test class""" - -+ "NOT_YET_IMPLEMENTED_STRING" cls_var = 100 def __init__(self): -@@ -53,8 +52,7 @@ - +@@ -54,7 +53,6 @@ class ClassWithDecoInitAndVarsAndDocstring: -- """Test class""" + """Test class""" - -+ "NOT_YET_IMPLEMENTED_STRING" cls_var = 100 @deco -@@ -69,7 +67,7 @@ - - class ClassSimplestWithInnerWithDocstring: - class Inner: -- """Just a docstring.""" -+ "NOT_YET_IMPLEMENTED_STRING" - - def __init__(self): - pass -@@ -83,7 +81,7 @@ - - - class ClassWithJustTheDocstringWithInner: -- """Just a docstring.""" -+ "NOT_YET_IMPLEMENTED_STRING" - - class Inner: - pass -@@ -108,8 +106,7 @@ - +@@ -109,7 +107,6 @@ class ClassWithInitAndVarsAndDocstringWithInner: -- """Test class""" + """Test class""" - -+ "NOT_YET_IMPLEMENTED_STRING" cls_var = 100 class Inner: -@@ -140,8 +137,7 @@ - +@@ -141,7 +138,6 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner: -- """Test class""" + """Test class""" - -+ "NOT_YET_IMPLEMENTED_STRING" cls_var = 100 class Inner: -@@ -153,7 +149,7 @@ - - - class ClassWithDecoInitAndVarsAndDocstringWithInner2: -- """Test class""" -+ "NOT_YET_IMPLEMENTED_STRING" - - class Inner: - pass ``` ## Ruff Output @@ -212,7 +159,7 @@ class ClassWithSingleField: class ClassWithJustTheDocstring: - "NOT_YET_IMPLEMENTED_STRING" + """Just a docstring.""" class ClassWithInit: @@ -221,7 +168,7 @@ class ClassWithInit: class ClassWithTheDocstringAndInit: - "NOT_YET_IMPLEMENTED_STRING" + """Just a docstring.""" def __init__(self): pass @@ -235,7 +182,7 @@ class ClassWithInitAndVars: class ClassWithInitAndVarsAndDocstring: - "NOT_YET_IMPLEMENTED_STRING" + """Test class""" cls_var = 100 def __init__(self): @@ -257,7 +204,7 @@ class ClassWithDecoInitAndVars: class ClassWithDecoInitAndVarsAndDocstring: - "NOT_YET_IMPLEMENTED_STRING" + """Test class""" cls_var = 100 @deco @@ -272,7 +219,7 @@ class ClassSimplestWithInner: class ClassSimplestWithInnerWithDocstring: class Inner: - "NOT_YET_IMPLEMENTED_STRING" + """Just a docstring.""" def __init__(self): pass @@ -286,7 +233,7 @@ class ClassWithSingleFieldWithInner: class ClassWithJustTheDocstringWithInner: - "NOT_YET_IMPLEMENTED_STRING" + """Just a docstring.""" class Inner: pass @@ -311,7 +258,7 @@ class ClassWithInitAndVarsWithInner: class ClassWithInitAndVarsAndDocstringWithInner: - "NOT_YET_IMPLEMENTED_STRING" + """Test class""" cls_var = 100 class Inner: @@ -342,7 +289,7 @@ class ClassWithDecoInitAndVarsWithInner: class ClassWithDecoInitAndVarsAndDocstringWithInner: - "NOT_YET_IMPLEMENTED_STRING" + """Test class""" cls_var = 100 class Inner: @@ -354,7 +301,7 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner: class ClassWithDecoInitAndVarsAndDocstringWithInner2: - "NOT_YET_IMPLEMENTED_STRING" + """Test class""" class Inner: pass diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap index 4dd883ec2f..1c961a36e7 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap @@ -84,7 +84,7 @@ if True: ```diff --- Black +++ Ruff -@@ -1,61 +1,40 @@ +@@ -1,40 +1,22 @@ -import core, time, a +NOT_YET_IMPLEMENTED_StmtImport @@ -133,29 +133,10 @@ if True: +nested = {(1, 2, 3), (4, 5, 6)} nested_no_trailing_comma = {(1, 2, 3), (4, 5, 6)} nested_long_lines = [ -- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -- "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", -- "cccccccccccccccccccccccccccccccccccccccc", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", - (1, 2, 3), -- "dddddddddddddddddddddddddddddddddddddddd", -+ "NOT_YET_IMPLEMENTED_STRING", - ] - { -- "oneple": (1,), -+ "NOT_YET_IMPLEMENTED_STRING": (1,), - } --{"oneple": (1,)} --["ls", "lsoneple/%s" % (foo,)] --x = {"oneple": (1,)} -+{"NOT_YET_IMPLEMENTED_STRING": (1,)} -+["NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING" % (foo,)] -+x = {"NOT_YET_IMPLEMENTED_STRING": (1,)} + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", +@@ -52,10 +34,7 @@ y = { -- "oneple": (1,), -+ "NOT_YET_IMPLEMENTED_STRING": (1,), + "oneple": (1,), } -assert False, ( - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa wraps %s" @@ -233,20 +214,20 @@ y = (NOT_IMPLEMENTED_call(),) nested = {(1, 2, 3), (4, 5, 6)} nested_no_trailing_comma = {(1, 2, 3), (4, 5, 6)} nested_long_lines = [ - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "cccccccccccccccccccccccccccccccccccccccc", (1, 2, 3), - "NOT_YET_IMPLEMENTED_STRING", + "dddddddddddddddddddddddddddddddddddddddd", ] { - "NOT_YET_IMPLEMENTED_STRING": (1,), + "oneple": (1,), } -{"NOT_YET_IMPLEMENTED_STRING": (1,)} -["NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING" % (foo,)] -x = {"NOT_YET_IMPLEMENTED_STRING": (1,)} +{"oneple": (1,)} +["ls", "lsoneple/%s" % (foo,)] +x = {"oneple": (1,)} y = { - "NOT_YET_IMPLEMENTED_STRING": (1,), + "oneple": (1,), } NOT_YET_IMPLEMENTED_StmtAssert diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap index 6e43e9b850..07c15327b0 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap @@ -178,7 +178,7 @@ instruction()#comment with bad spacing ```diff --- Black +++ Ruff -@@ -1,31 +1,27 @@ +@@ -1,9 +1,5 @@ -from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent, # NOT DRY -) @@ -190,43 +190,6 @@ instruction()#comment with bad spacing # Please keep __all__ alphabetized within each category. - __all__ = [ - # Super-special typing primitives. -- "Any", -- "Callable", -- "ClassVar", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", - # ABCs (from collections.abc). -- "AbstractSet", # collections.abc.Set. -- "ByteString", -- "Container", -+ "NOT_YET_IMPLEMENTED_STRING", # collections.abc.Set. -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", - # Concrete collection types. -- "Counter", -- "Deque", -- "Dict", -- "DefaultDict", -- "List", -- "Set", -- "FrozenSet", -- "NamedTuple", # Not really a type. -- "Generator", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", # Not really a type. -+ "NOT_YET_IMPLEMENTED_STRING", - ] - - not_shareables = [ @@ -37,31 +33,35 @@ # builtin types and objects type, @@ -237,8 +200,7 @@ instruction()#comment with bad spacing + NOT_IMPLEMENTED_call(), 42, 100.0, -- "spam", -+ "NOT_YET_IMPLEMENTED_STRING", + "spam", # user-defined types and objects Cheese, - Cheese("Wensleydale"), @@ -247,9 +209,8 @@ instruction()#comment with bad spacing + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), ] --if "PYTHON" in os.environ: + if "PYTHON" in os.environ: - add_compiler(compiler_from_env()) -+if "NOT_YET_IMPLEMENTED_STRING" in os.environ: + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) else: # for compiler in compilers.values(): @@ -400,23 +361,23 @@ NOT_YET_IMPLEMENTED_StmtImportFrom __all__ = [ # Super-special typing primitives. - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", + "Any", + "Callable", + "ClassVar", # ABCs (from collections.abc). - "NOT_YET_IMPLEMENTED_STRING", # collections.abc.Set. - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", + "AbstractSet", # collections.abc.Set. + "ByteString", + "Container", # Concrete collection types. - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", # Not really a type. - "NOT_YET_IMPLEMENTED_STRING", + "Counter", + "Deque", + "Dict", + "DefaultDict", + "List", + "Set", + "FrozenSet", + "NamedTuple", # Not really a type. + "Generator", ] not_shareables = [ @@ -432,14 +393,14 @@ not_shareables = [ NOT_IMPLEMENTED_call(), 42, 100.0, - "NOT_YET_IMPLEMENTED_STRING", + "spam", # user-defined types and objects Cheese, NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), ] -if "NOT_YET_IMPLEMENTED_STRING" in os.environ: +if "PYTHON" in os.environ: NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) else: # for compiler in compilers.values(): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap index ef02e64a10..73e02a1e27 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap @@ -61,13 +61,10 @@ def func(): ```diff --- Black +++ Ruff -@@ -3,46 +3,17 @@ - - # %% - def func(): -- x = """ -- a really long string -- """ +@@ -6,43 +6,16 @@ + x = """ + a really long string + """ - lcomp3 = [ - # This one is actually too long to fit in a single line. - element.split("\n", 1)[0] @@ -76,7 +73,6 @@ def func(): - # right - if element is not None - ] -+ x = "NOT_YET_IMPLEMENTED_STRING" + lcomp3 = [i for i in []] # Capture each of the exceptions in the MultiError along with each of their causes and contexts - if isinstance(exc_value, MultiError): @@ -123,7 +119,9 @@ def func(): # %% def func(): - x = "NOT_YET_IMPLEMENTED_STRING" + x = """ + a really long string + """ lcomp3 = [i for i in []] # Capture each of the exceptions in the MultiError along with each of their causes and contexts if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap index 762929aa96..51320a8e86 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap @@ -144,13 +144,11 @@ if __name__ == "__main__": # leading function comment def decorated1(): ... -@@ -69,5 +63,5 @@ - ... +@@ -70,4 +64,4 @@ --if __name__ == "__main__": + if __name__ == "__main__": - main() -+if __name__ == "NOT_YET_IMPLEMENTED_STRING": + NOT_IMPLEMENTED_call() ``` @@ -222,7 +220,7 @@ def g(): ... -if __name__ == "NOT_YET_IMPLEMENTED_STRING": +if __name__ == "__main__": NOT_IMPLEMENTED_call() ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap index abc2ecaafd..86c3b34f2d 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap @@ -180,7 +180,7 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite -result = ( # aaa - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -) -+result = "NOT_YET_IMPLEMENTED_STRING" # aaa ++result = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # aaa -AAAAAAAAAAAAA = [AAAAAAAAAAAAA] + SHARED_AAAAAAAAAAAAA + USER_AAAAAAAAAAAAA + AAAAAAAAAAAAA # type: ignore +AAAAAAAAAAAAA = ( @@ -297,7 +297,7 @@ def func( c = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -result = "NOT_YET_IMPLEMENTED_STRING" # aaa +result = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # aaa AAAAAAAAAAAAA = ( [AAAAAAAAAAAAA] diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap index 857ff11509..8ae0c972f4 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap @@ -152,15 +152,6 @@ def bar(): ```diff --- Black +++ Ruff -@@ -44,7 +44,7 @@ - - - class ClassWithDocstring: -- """A docstring.""" -+ "NOT_YET_IMPLEMENTED_STRING" - - - # Leading comment after a class with just a docstring @@ -59,7 +59,7 @@ @deco1 # leading 2 @@ -255,7 +246,7 @@ class MyClassWithComplexLeadingComments: class ClassWithDocstring: - "NOT_YET_IMPLEMENTED_STRING" + """A docstring.""" # Leading comment after a class with just a docstring diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap index 976fcc5d8f..6ff36ce6c3 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap @@ -32,7 +32,7 @@ def function(a:int=42): ```diff --- Black +++ Ruff -@@ -1,23 +1,15 @@ +@@ -1,22 +1,17 @@ -from .config import ( - ConfigTypeAttributes, - Int, @@ -54,11 +54,12 @@ def function(a:int=42): - """This docstring is already formatted - a - b -- """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ """ This docstring is already formatted ++ a ++ b + """ # There's a NBSP + 3 spaces before # And 4 spaces on the next line - pass ``` ## Ruff Output @@ -75,7 +76,10 @@ square = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) #  type: Optional[Square] def function(a: int = 42): - "NOT_YET_IMPLEMENTED_STRING" + """ This docstring is already formatted + a + b + """ # There's a NBSP + 3 spaces before # And 4 spaces on the next line pass diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap index ae7d0c0315..cb44cb8f7f 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap @@ -109,21 +109,15 @@ async def wat(): ```diff --- Black +++ Ruff -@@ -4,21 +4,15 @@ - # - # Has many lines. Many, many lines. - # Many, many, many lines. --"""Module docstring. -+"NOT_YET_IMPLEMENTED_STRING" - --Possibly also many, many lines. --""" -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImport +@@ -9,16 +9,13 @@ + 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 @@ -137,15 +131,9 @@ async def wat(): # Some comment before a function. -@@ -30,25 +24,26 @@ - - - def function(default=None): -- """Docstring comes first. -- -- Possibly many lines. -- """ -+ "NOT_YET_IMPLEMENTED_STRING" +@@ -35,20 +32,24 @@ + Possibly many lines. + """ # FIXME: Some comment about why this function is crap but still in production. - import inner_imports + NOT_YET_IMPLEMENTED_StmtImport @@ -166,38 +154,14 @@ async def wat(): # Explains why we use global state. -GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} +GLOBAL_STATE = { -+ "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), -+ "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), -+ "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), ++ "a": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), ++ "b": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), ++ "c": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), +} # Another comment! -@@ -56,7 +51,7 @@ - - - class Foo: -- """Docstring for class Foo. Example from Sphinx docs.""" -+ "NOT_YET_IMPLEMENTED_STRING" - - #: Doc comment for class attribute Foo.bar. - #: It can have multiple lines. -@@ -65,32 +60,31 @@ - flox = 1.5 #: Doc comment for Foo.flox. One line only. - - baz = 2 -- """Docstring for class attribute Foo.baz.""" -+ "NOT_YET_IMPLEMENTED_STRING" - - def __init__(self): - #: Doc comment for instance attribute qux. - self.qux = 3 - - self.spam = 4 -- """Docstring for instance attribute spam.""" -+ "NOT_YET_IMPLEMENTED_STRING" - - +@@ -78,19 +79,18 @@ #'

This is pweave!

@@ -234,7 +198,10 @@ async def wat(): # # Has many lines. Many, many lines. # Many, many, many lines. -"NOT_YET_IMPLEMENTED_STRING" +"""Module docstring. + +Possibly also many, many lines. +""" NOT_YET_IMPLEMENTED_StmtImport NOT_YET_IMPLEMENTED_StmtImport @@ -254,7 +221,10 @@ y = 1 def function(default=None): - "NOT_YET_IMPLEMENTED_STRING" + """Docstring comes first. + + Possibly many lines. + """ # FIXME: Some comment about why this function is crap but still in production. NOT_YET_IMPLEMENTED_StmtImport @@ -270,9 +240,9 @@ def function(default=None): # Explains why we use global state. GLOBAL_STATE = { - "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), - "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), - "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), + "a": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), + "b": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), + "c": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), } @@ -281,7 +251,7 @@ GLOBAL_STATE = { class Foo: - "NOT_YET_IMPLEMENTED_STRING" + """Docstring for class Foo. Example from Sphinx docs.""" #: Doc comment for class attribute Foo.bar. #: It can have multiple lines. @@ -290,14 +260,14 @@ class Foo: flox = 1.5 #: Doc comment for Foo.flox. One line only. baz = 2 - "NOT_YET_IMPLEMENTED_STRING" + """Docstring for class attribute Foo.baz.""" def __init__(self): #: Doc comment for instance attribute qux. self.qux = 3 self.spam = 4 - "NOT_YET_IMPLEMENTED_STRING" + """Docstring for instance attribute spam.""" #'

This is pweave!

diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_no_trailing_comma_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_no_trailing_comma_py.snap index 56529790aa..f47fb08de7 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_no_trailing_comma_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_no_trailing_comma_py.snap @@ -194,7 +194,7 @@ class C: ```diff --- Black +++ Ruff -@@ -1,181 +1,46 @@ +@@ -1,159 +1,42 @@ class C: def test(self) -> None: - with patch("black.out", print): @@ -236,17 +236,11 @@ class C: - # Only send the first n items. - items=items[:num_items] - ) -- return ( -- 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' -- % (test.name, test.filename, lineno, lname, err) + NOT_YET_IMPLEMENTED_StmtWith + xxxxxxxxxxxxxxxx = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ return "NOT_YET_IMPLEMENTED_STRING" % ( -+ test.name, -+ test.filename, -+ lineno, -+ lname, -+ err, + return ( + 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' + % (test.name, test.filename, lineno, lname, err) ) def omitting_trailers(self) -> None: @@ -373,18 +367,19 @@ class C: + NOT_YET_IMPLEMENTED_StmtAssert - dis_c_instance_method = """\ -- %3d 0 LOAD_FAST 1 (x) -- 2 LOAD_CONST 1 (1) -- 4 COMPARE_OP 2 (==) -- 6 LOAD_FAST 0 (self) -- 8 STORE_ATTR 0 (x) -- 10 LOAD_CONST 0 (None) -- 12 RETURN_VALUE ++ dis_c_instance_method = ( ++ """\ + %3d 0 LOAD_FAST 1 (x) + 2 LOAD_CONST 1 (1) + 4 COMPARE_OP 2 (==) +@@ -161,21 +44,8 @@ + 8 STORE_ATTR 0 (x) + 10 LOAD_CONST 0 (None) + 12 RETURN_VALUE - """ % ( - _C.__init__.__code__.co_firstlineno + 1, -+ dis_c_instance_method = "NOT_YET_IMPLEMENTED_STRING" % ( -+ _C.__init__.__code__.co_firstlineno -+ + 1, ++ """ ++ % (_C.__init__.__code__.co_firstlineno + 1,) ) - assert ( @@ -411,12 +406,9 @@ class C: def test(self) -> None: NOT_YET_IMPLEMENTED_StmtWith xxxxxxxxxxxxxxxx = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - return "NOT_YET_IMPLEMENTED_STRING" % ( - test.name, - test.filename, - lineno, - lname, - err, + return ( + 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' + % (test.name, test.filename, lineno, lname, err) ) def omitting_trailers(self) -> None: @@ -447,9 +439,17 @@ class C: NOT_YET_IMPLEMENTED_StmtAssert - dis_c_instance_method = "NOT_YET_IMPLEMENTED_STRING" % ( - _C.__init__.__code__.co_firstlineno - + 1, + dis_c_instance_method = ( + """\ + %3d 0 LOAD_FAST 1 (x) + 2 LOAD_CONST 1 (1) + 4 COMPARE_OP 2 (==) + 6 LOAD_FAST 0 (self) + 8 STORE_ATTR 0 (x) + 10 LOAD_CONST 0 (None) + 12 RETURN_VALUE + """ + % (_C.__init__.__code__.co_firstlineno + 1,) ) NOT_YET_IMPLEMENTED_StmtAssert diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_py.snap index 09dc5f7269..de890ccedd 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_py.snap @@ -194,7 +194,7 @@ class C: ```diff --- Black +++ Ruff -@@ -1,181 +1,46 @@ +@@ -1,159 +1,42 @@ class C: def test(self) -> None: - with patch("black.out", print): @@ -236,17 +236,11 @@ class C: - # Only send the first n items. - items=items[:num_items] - ) -- return ( -- 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' -- % (test.name, test.filename, lineno, lname, err) + NOT_YET_IMPLEMENTED_StmtWith + xxxxxxxxxxxxxxxx = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ return "NOT_YET_IMPLEMENTED_STRING" % ( -+ test.name, -+ test.filename, -+ lineno, -+ lname, -+ err, + return ( + 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' + % (test.name, test.filename, lineno, lname, err) ) def omitting_trailers(self) -> None: @@ -373,18 +367,19 @@ class C: + NOT_YET_IMPLEMENTED_StmtAssert - dis_c_instance_method = """\ -- %3d 0 LOAD_FAST 1 (x) -- 2 LOAD_CONST 1 (1) -- 4 COMPARE_OP 2 (==) -- 6 LOAD_FAST 0 (self) -- 8 STORE_ATTR 0 (x) -- 10 LOAD_CONST 0 (None) -- 12 RETURN_VALUE ++ dis_c_instance_method = ( ++ """\ + %3d 0 LOAD_FAST 1 (x) + 2 LOAD_CONST 1 (1) + 4 COMPARE_OP 2 (==) +@@ -161,21 +44,8 @@ + 8 STORE_ATTR 0 (x) + 10 LOAD_CONST 0 (None) + 12 RETURN_VALUE - """ % ( - _C.__init__.__code__.co_firstlineno + 1, -+ dis_c_instance_method = "NOT_YET_IMPLEMENTED_STRING" % ( -+ _C.__init__.__code__.co_firstlineno -+ + 1, ++ """ ++ % (_C.__init__.__code__.co_firstlineno + 1,) ) - assert ( @@ -411,12 +406,9 @@ class C: def test(self) -> None: NOT_YET_IMPLEMENTED_StmtWith xxxxxxxxxxxxxxxx = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - return "NOT_YET_IMPLEMENTED_STRING" % ( - test.name, - test.filename, - lineno, - lname, - err, + return ( + 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' + % (test.name, test.filename, lineno, lname, err) ) def omitting_trailers(self) -> None: @@ -447,9 +439,17 @@ class C: NOT_YET_IMPLEMENTED_StmtAssert - dis_c_instance_method = "NOT_YET_IMPLEMENTED_STRING" % ( - _C.__init__.__code__.co_firstlineno - + 1, + dis_c_instance_method = ( + """\ + %3d 0 LOAD_FAST 1 (x) + 2 LOAD_CONST 1 (1) + 4 COMPARE_OP 2 (==) + 6 LOAD_FAST 0 (self) + 8 STORE_ATTR 0 (x) + 10 LOAD_CONST 0 (None) + 12 RETURN_VALUE + """ + % (_C.__init__.__code__.co_firstlineno + 1,) ) NOT_YET_IMPLEMENTED_StmtAssert diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_no_extra_empty_line_before_eof_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_no_extra_empty_line_before_eof_py.snap deleted file mode 100644 index d56f37ee25..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_no_extra_empty_line_before_eof_py.snap +++ /dev/null @@ -1,46 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/docstring_no_extra_empty_line_before_eof.py ---- -## Input - -```py -# Make sure when the file ends with class's docstring, -# It doesn't add extra blank lines. -class ClassWithDocstring: - """A docstring.""" -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,4 +1,4 @@ - # Make sure when the file ends with class's docstring, - # It doesn't add extra blank lines. - class ClassWithDocstring: -- """A docstring.""" -+ "NOT_YET_IMPLEMENTED_STRING" -``` - -## Ruff Output - -```py -# Make sure when the file ends with class's docstring, -# It doesn't add extra blank lines. -class ClassWithDocstring: - "NOT_YET_IMPLEMENTED_STRING" -``` - -## Black Output - -```py -# Make sure when the file ends with class's docstring, -# It doesn't add extra blank lines. -class ClassWithDocstring: - """A docstring.""" -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap index 08eea3e67f..296bb4715d 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap @@ -63,10 +63,11 @@ def single_quote_docstring_over_line_limit2(): ```diff --- Black +++ Ruff -@@ -1,48 +1,38 @@ +@@ -1,9 +1,10 @@ def docstring_almost_at_line_limit(): - """long docstring.................................................................""" -+ "NOT_YET_IMPLEMENTED_STRING" ++ """long docstring................................................................. ++ """ def docstring_almost_at_line_limit_with_prefix(): @@ -75,11 +76,7 @@ def single_quote_docstring_over_line_limit2(): def mulitline_docstring_almost_at_line_limit(): -- """long docstring................................................................. -- -- .................................................................................. -- """ -+ "NOT_YET_IMPLEMENTED_STRING" +@@ -14,10 +15,7 @@ def mulitline_docstring_almost_at_line_limit_with_prefix(): @@ -91,8 +88,7 @@ def single_quote_docstring_over_line_limit2(): def docstring_at_line_limit(): -- """long docstring................................................................""" -+ "NOT_YET_IMPLEMENTED_STRING" +@@ -25,7 +23,7 @@ def docstring_at_line_limit_with_prefix(): @@ -101,10 +97,7 @@ def single_quote_docstring_over_line_limit2(): def multiline_docstring_at_line_limit(): -- """first line----------------------------------------------------------------------- -- -- second line----------------------------------------------------------------------""" -+ "NOT_YET_IMPLEMENTED_STRING" +@@ -35,9 +33,7 @@ def multiline_docstring_at_line_limit_with_prefix(): @@ -115,20 +108,14 @@ def single_quote_docstring_over_line_limit2(): def single_quote_docstring_over_line_limit(): -- "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." -+ "NOT_YET_IMPLEMENTED_STRING" - - - def single_quote_docstring_over_line_limit2(): -- "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." -+ "NOT_YET_IMPLEMENTED_STRING" ``` ## Ruff Output ```py def docstring_almost_at_line_limit(): - "NOT_YET_IMPLEMENTED_STRING" + """long docstring................................................................. + """ def docstring_almost_at_line_limit_with_prefix(): @@ -136,7 +123,10 @@ def docstring_almost_at_line_limit_with_prefix(): def mulitline_docstring_almost_at_line_limit(): - "NOT_YET_IMPLEMENTED_STRING" + """long docstring................................................................. + + .................................................................................. + """ def mulitline_docstring_almost_at_line_limit_with_prefix(): @@ -144,7 +134,7 @@ def mulitline_docstring_almost_at_line_limit_with_prefix(): def docstring_at_line_limit(): - "NOT_YET_IMPLEMENTED_STRING" + """long docstring................................................................""" def docstring_at_line_limit_with_prefix(): @@ -152,7 +142,9 @@ def docstring_at_line_limit_with_prefix(): def multiline_docstring_at_line_limit(): - "NOT_YET_IMPLEMENTED_STRING" + """first line----------------------------------------------------------------------- + + second line----------------------------------------------------------------------""" def multiline_docstring_at_line_limit_with_prefix(): @@ -160,11 +152,11 @@ def multiline_docstring_at_line_limit_with_prefix(): def single_quote_docstring_over_line_limit(): - "NOT_YET_IMPLEMENTED_STRING" + "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." def single_quote_docstring_over_line_limit2(): - "NOT_YET_IMPLEMENTED_STRING" + "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap index 784587197d..da177f0353 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap @@ -234,18 +234,21 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): ```diff --- Black +++ Ruff -@@ -1,219 +1,154 @@ +@@ -1,83 +1,85 @@ class MyClass: - """Multiline - class docstring - """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ """ Multiline ++ class docstring ++ """ def method(self): -- """Multiline + """Multiline - method docstring - """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ method docstring ++ """ pass @@ -253,46 +256,53 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): - """This is a docstring with - some lines of text here - """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ """This is a docstring with ++ some lines of text here ++ """ return def bar(): -- """This is another docstring + """This is another docstring - with more lines of text - """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ with more lines of text ++ """ return def baz(): -- '''"This" is a string with some + '''"This" is a string with some - embedded "quotes"''' -+ "NOT_YET_IMPLEMENTED_STRING" ++ embedded "quotes"''' return def troz(): -- """Indentation with tabs + """Indentation with tabs - is just as OK - """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ is just as OK ++ """ return def zort(): -- """Another + """Another - multiline - docstring - """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ multiline ++ docstring ++ """ pass def poit(): -- """ + """ - Lorem ipsum dolor sit amet. -- ++ Lorem ipsum dolor sit amet. + - Consectetur adipiscing elit: - - sed do eiusmod tempor incididunt ut labore - - dolore magna aliqua @@ -300,112 +310,117 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): - - quis nostrud exercitation ullamco laboris nisi - - aliquip ex ea commodo consequat - """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ Consectetur adipiscing elit: ++ - sed do eiusmod tempor incididunt ut labore ++ - dolore magna aliqua ++ - enim ad minim veniam ++ - quis nostrud exercitation ullamco laboris nisi ++ - aliquip ex ea commodo consequat ++ """ pass def under_indent(): -- """ + """ - These lines are indented in a way that does not - make sense. - """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ These lines are indented in a way that does not ++make sense. ++ """ pass def over_indent(): -- """ + """ - This has a shallow indent - - But some lines are deeper - - And the closing quote is too deep -- """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ This has a shallow indent ++ - But some lines are deeper ++ - And the closing quote is too deep + """ pass def single_line(): - """But with a newline after it!""" -+ "NOT_YET_IMPLEMENTED_STRING" ++ """But with a newline after it! ++ ++ """ pass - def this(): -- r""" -- 'hey ho' -- """ -+ "NOT_YET_IMPLEMENTED_STRING" - - - def that(): -- """ "hey yah" """ -+ "NOT_YET_IMPLEMENTED_STRING" - +@@ -93,20 +95,25 @@ def and_that(): -- """ + """ - "hey yah" """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ "hey yah" """ def and_this(): - ''' - "hey yah"''' -+ "NOT_YET_IMPLEMENTED_STRING" ++ ''' ++ "hey yah"''' def multiline_whitespace(): - """ """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ """ ++ ++ ++ ++ ++ """ def oneline_whitespace(): - """ """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ """ """ def empty(): -- """""" -+ "NOT_YET_IMPLEMENTED_STRING" - - - def single_quotes(): -- "testing" -+ "NOT_YET_IMPLEMENTED_STRING" +@@ -118,8 +125,8 @@ def believe_it_or_not_this_is_in_the_py_stdlib(): - ''' - "hey yah"''' -+ "NOT_YET_IMPLEMENTED_STRING" ++ ''' ++"hey yah"''' def ignored_docstring(): -- """a => \ --b""" -+ "NOT_YET_IMPLEMENTED_STRING" +@@ -128,31 +135,31 @@ def single_line_docstring_with_whitespace(): - """This should be stripped""" -+ "NOT_YET_IMPLEMENTED_STRING" ++ """ This should be stripped """ def docstring_with_inline_tabs_and_space_indentation(): -- """hey -- -- tab separated value + """hey + + tab separated value - tab at start of line and then a tab separated value - multiple tabs at the beginning and inline - mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. -+ "NOT_YET_IMPLEMENTED_STRING" - +- - line ends with some tabs -- """ ++ tab at start of line and then a tab separated value ++ multiple tabs at the beginning and inline ++ mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. ++ ++ line ends with some tabs + """ + -- def docstring_with_inline_tabs_and_tab_indentation(): -- """hey -- + """hey + - tab separated value - tab at start of line and then a tab separated value - multiple tabs at the beginning and inline @@ -413,242 +428,274 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): - - line ends with some tabs - """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ tab separated value ++ tab at start of line and then a tab separated value ++ multiple tabs at the beginning and inline ++ mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. ++ ++ line ends with some tabs ++ """ pass - def backslash_space(): -- """\ """ -+ "NOT_YET_IMPLEMENTED_STRING" - - - def multiline_backslash_1(): -- """ -- hey\there\ -- \ """ -+ "NOT_YET_IMPLEMENTED_STRING" - +@@ -168,7 +175,7 @@ def multiline_backslash_2(): -- """ + """ - hey there \ """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ hey there \ """ # Regression test for #3425 - def multiline_backslash_really_long_dont_crash(): -- """ -- hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """ -+ "NOT_YET_IMPLEMENTED_STRING" - +@@ -179,7 +186,7 @@ def multiline_backslash_3(): -- """ + """ - already escaped \\""" -+ "NOT_YET_IMPLEMENTED_STRING" ++ already escaped \\ """ def my_god_its_full_of_stars_1(): -- "I'm sorry Dave\u2001" -+ "NOT_YET_IMPLEMENTED_STRING" - +@@ -188,7 +195,7 @@ # the space below is actually a \u2001, removed in output def my_god_its_full_of_stars_2(): - "I'm sorry Dave" -+ "NOT_YET_IMPLEMENTED_STRING" ++ "I'm sorry Dave " def docstring_almost_at_line_limit(): -- """long docstring.................................................................""" -+ "NOT_YET_IMPLEMENTED_STRING" - - - def docstring_almost_at_line_limit2(): -- """long docstring................................................................. -- -- .................................................................................. -- """ -+ "NOT_YET_IMPLEMENTED_STRING" - - - def docstring_at_line_limit(): -- """long docstring................................................................""" -+ "NOT_YET_IMPLEMENTED_STRING" - - - def multiline_docstring_at_line_limit(): -- """first line----------------------------------------------------------------------- -- -- second line----------------------------------------------------------------------""" -+ "NOT_YET_IMPLEMENTED_STRING" - - - def stable_quote_normalization_with_immediate_inner_single_quote(self): -- """' -- -- -- """ -+ "NOT_YET_IMPLEMENTED_STRING" ``` ## Ruff Output ```py class MyClass: - "NOT_YET_IMPLEMENTED_STRING" + """ Multiline + class docstring + """ def method(self): - "NOT_YET_IMPLEMENTED_STRING" + """Multiline + method docstring + """ pass def foo(): - "NOT_YET_IMPLEMENTED_STRING" + """This is a docstring with + some lines of text here + """ return def bar(): - "NOT_YET_IMPLEMENTED_STRING" + """This is another docstring + with more lines of text + """ return def baz(): - "NOT_YET_IMPLEMENTED_STRING" + '''"This" is a string with some + embedded "quotes"''' return def troz(): - "NOT_YET_IMPLEMENTED_STRING" + """Indentation with tabs + is just as OK + """ return def zort(): - "NOT_YET_IMPLEMENTED_STRING" + """Another + multiline + docstring + """ pass def poit(): - "NOT_YET_IMPLEMENTED_STRING" + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ pass def under_indent(): - "NOT_YET_IMPLEMENTED_STRING" + """ + These lines are indented in a way that does not +make sense. + """ pass def over_indent(): - "NOT_YET_IMPLEMENTED_STRING" + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ pass def single_line(): - "NOT_YET_IMPLEMENTED_STRING" + """But with a newline after it! + + """ pass def this(): - "NOT_YET_IMPLEMENTED_STRING" + r""" + 'hey ho' + """ def that(): - "NOT_YET_IMPLEMENTED_STRING" + """ "hey yah" """ def and_that(): - "NOT_YET_IMPLEMENTED_STRING" + """ + "hey yah" """ def and_this(): - "NOT_YET_IMPLEMENTED_STRING" + ''' + "hey yah"''' def multiline_whitespace(): - "NOT_YET_IMPLEMENTED_STRING" + """ + + + + + """ def oneline_whitespace(): - "NOT_YET_IMPLEMENTED_STRING" + """ """ def empty(): - "NOT_YET_IMPLEMENTED_STRING" + """""" def single_quotes(): - "NOT_YET_IMPLEMENTED_STRING" + "testing" def believe_it_or_not_this_is_in_the_py_stdlib(): - "NOT_YET_IMPLEMENTED_STRING" + ''' +"hey yah"''' def ignored_docstring(): - "NOT_YET_IMPLEMENTED_STRING" + """a => \ +b""" def single_line_docstring_with_whitespace(): - "NOT_YET_IMPLEMENTED_STRING" + """ This should be stripped """ def docstring_with_inline_tabs_and_space_indentation(): - "NOT_YET_IMPLEMENTED_STRING" + """hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ def docstring_with_inline_tabs_and_tab_indentation(): - "NOT_YET_IMPLEMENTED_STRING" + """hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ pass def backslash_space(): - "NOT_YET_IMPLEMENTED_STRING" + """\ """ def multiline_backslash_1(): - "NOT_YET_IMPLEMENTED_STRING" + """ + hey\there\ + \ """ def multiline_backslash_2(): - "NOT_YET_IMPLEMENTED_STRING" + """ + hey there \ """ # Regression test for #3425 def multiline_backslash_really_long_dont_crash(): - "NOT_YET_IMPLEMENTED_STRING" + """ + hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """ def multiline_backslash_3(): - "NOT_YET_IMPLEMENTED_STRING" + """ + already escaped \\ """ def my_god_its_full_of_stars_1(): - "NOT_YET_IMPLEMENTED_STRING" + "I'm sorry Dave\u2001" # the space below is actually a \u2001, removed in output def my_god_its_full_of_stars_2(): - "NOT_YET_IMPLEMENTED_STRING" + "I'm sorry Dave " def docstring_almost_at_line_limit(): - "NOT_YET_IMPLEMENTED_STRING" + """long docstring.................................................................""" def docstring_almost_at_line_limit2(): - "NOT_YET_IMPLEMENTED_STRING" + """long docstring................................................................. + + .................................................................................. + """ def docstring_at_line_limit(): - "NOT_YET_IMPLEMENTED_STRING" + """long docstring................................................................""" def multiline_docstring_at_line_limit(): - "NOT_YET_IMPLEMENTED_STRING" + """first line----------------------------------------------------------------------- + + second line----------------------------------------------------------------------""" def stable_quote_normalization_with_immediate_inner_single_quote(self): - "NOT_YET_IMPLEMENTED_STRING" + """' + + + """ ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap index 4bb4e71127..874c26f461 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap @@ -105,23 +105,7 @@ def g(): ```diff --- Black +++ Ruff -@@ -1,11 +1,11 @@ --"""Docstring.""" -+"NOT_YET_IMPLEMENTED_STRING" - - - # leading comment - def f(): -- NO = "" -- SPACE = " " -- DOUBLESPACE = " " -+ NO = "NOT_YET_IMPLEMENTED_STRING" -+ SPACE = "NOT_YET_IMPLEMENTED_STRING" -+ DOUBLESPACE = "NOT_YET_IMPLEMENTED_STRING" - - t = leaf.type - p = leaf.parent # trailing comment -@@ -16,44 +16,51 @@ +@@ -16,32 +16,40 @@ if t == token.COMMENT: # another trailing comment return DOUBLESPACE @@ -178,21 +162,14 @@ def g(): return NO - ############################################################################### +@@ -49,7 +57,6 @@ # SECTION BECAUSE SECTIONS ############################################################################### + - - def g(): -- NO = "" -- SPACE = " " -- DOUBLESPACE = " " -+ NO = "NOT_YET_IMPLEMENTED_STRING" -+ SPACE = "NOT_YET_IMPLEMENTED_STRING" -+ DOUBLESPACE = "NOT_YET_IMPLEMENTED_STRING" - - t = leaf.type - p = leaf.parent + NO = "" + SPACE = " " @@ -67,11 +74,11 @@ return DOUBLESPACE @@ -235,14 +212,14 @@ def g(): ## Ruff Output ```py -"NOT_YET_IMPLEMENTED_STRING" +"""Docstring.""" # leading comment def f(): - NO = "NOT_YET_IMPLEMENTED_STRING" - SPACE = "NOT_YET_IMPLEMENTED_STRING" - DOUBLESPACE = "NOT_YET_IMPLEMENTED_STRING" + NO = "" + SPACE = " " + DOUBLESPACE = " " t = leaf.type p = leaf.parent # trailing comment @@ -295,9 +272,9 @@ def f(): ############################################################################### def g(): - NO = "NOT_YET_IMPLEMENTED_STRING" - SPACE = "NOT_YET_IMPLEMENTED_STRING" - DOUBLESPACE = "NOT_YET_IMPLEMENTED_STRING" + NO = "" + SPACE = " " + DOUBLESPACE = " " t = leaf.type p = leaf.parent diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap index 57371f8bf4..d64a1abf8a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap @@ -268,15 +268,14 @@ last_call() --- Black +++ Ruff @@ -1,5 +1,6 @@ --"some_string" --b"\\xa3" +... -+"NOT_YET_IMPLEMENTED_STRING" + "some_string" +-b"\\xa3" +b"NOT_YET_IMPLEMENTED_BYTE_STRING" Name None True -@@ -30,98 +31,90 @@ +@@ -30,98 +31,83 @@ -1 ~int and not v1 ^ 123 + v2 | True (~int) and (not ((v1 ^ (123 + v2)) | True)) @@ -303,8 +302,6 @@ last_call() - if (1 if super_long_test_name else 2) - else (str or bytes or None) -) --{"2.7": dead, "3.7": (long_live or die_hard)} --{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} +lambda x: True +lambda x: True +lambda x: True @@ -318,31 +315,24 @@ last_call() +NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false +NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false +(NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false) -+{ -+ "NOT_YET_IMPLEMENTED_STRING": dead, -+ "NOT_YET_IMPLEMENTED_STRING": (long_live or die_hard), -+} -+{ -+ "NOT_YET_IMPLEMENTED_STRING": dead, -+ "NOT_YET_IMPLEMENTED_STRING": (long_live or die_hard), -+ **{"NOT_YET_IMPLEMENTED_STRING": verygood}, -+} + {"2.7": dead, "3.7": (long_live or die_hard)} + {"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} {**a, **b, **c} -{"2.7", "3.6", "3.7", "3.8", "3.9", ("4.0" if gilectomy else "3.10")} -({"a": "b"}, (True or False), (+value), "string", b"bytes") or None +{ -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", ++ "2.7", ++ "3.6", ++ "3.7", ++ "3.8", ++ "3.9", + (NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false), +} +( -+ {"NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING"}, ++ {"a": "b"}, + (True or False), + (+value), -+ "NOT_YET_IMPLEMENTED_STRING", ++ "string", + b"NOT_YET_IMPLEMENTED_BYTE_STRING", +) or None () @@ -363,17 +353,17 @@ last_call() - 4, - 5, -] --[ -- 4, -- *a, -- 5, --] +[1, 2, 3] +[NOT_YET_IMPLEMENTED_ExprStarred] +[NOT_YET_IMPLEMENTED_ExprStarred] +[NOT_YET_IMPLEMENTED_ExprStarred, 4, 5] +[4, NOT_YET_IMPLEMENTED_ExprStarred, 5] [ +- 4, +- *a, +- 5, +-] +-[ this_is_a_very_long_variable_which_will_force_a_delimiter_split, element, another, @@ -443,7 +433,7 @@ last_call() (1).real (1.0).real ....__class__ -@@ -130,34 +123,28 @@ +@@ -130,34 +116,28 @@ tuple[str, ...] tuple[str, int, float, dict[str, int]] tuple[ @@ -451,6 +441,9 @@ last_call() - int, - float, - dict[str, int], +-] +-very_long_variable_name_filters: t.List[ +- t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], + ( + str, + int, @@ -458,9 +451,6 @@ last_call() + dict[str, int], + ) ] --very_long_variable_name_filters: t.List[ -- t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], --] -xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore - sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) -) @@ -491,7 +481,7 @@ last_call() numpy[0, :] numpy[:, i] numpy[0, :2] -@@ -171,62 +158,59 @@ +@@ -171,25 +151,32 @@ numpy[1 : c + 1, c] numpy[-(c + 1) :, d] numpy[:, l[-2]] @@ -499,19 +489,15 @@ last_call() +numpy[:, :: -1] numpy[np.newaxis, :] -(str or None) if (sys.version_info[0] > (3,)) else (str or bytes or None) --{"2.7": dead, "3.7": long_live or die_hard} --{"2.7", "3.6", "3.7", "3.8", "3.9", "4.0" if gilectomy else "3.10"} +NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + {"2.7": dead, "3.7": long_live or die_hard} +-{"2.7", "3.6", "3.7", "3.8", "3.9", "4.0" if gilectomy else "3.10"} +{ -+ "NOT_YET_IMPLEMENTED_STRING": dead, -+ "NOT_YET_IMPLEMENTED_STRING": long_live or die_hard, -+} -+{ -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", ++ "2.7", ++ "3.6", ++ "3.7", ++ "3.8", ++ "3.9", + NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, +} [1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C] @@ -529,22 +515,16 @@ last_call() +(i for i in []) +(NOT_YET_IMPLEMENTED_ExprStarred,) { -- "id": "1", -- "type": "type", + "id": "1", + "type": "type", - "started_at": now(), - "ended_at": now() + timedelta(days=10), -- "priority": 1, -- "import_session_id": 1, -+ "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(), -+ "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call() -+ + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), -+ "NOT_YET_IMPLEMENTED_STRING": 1, -+ "NOT_YET_IMPLEMENTED_STRING": 1, ++ "started_at": NOT_IMPLEMENTED_call(), ++ "ended_at": NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), + "priority": 1, + "import_session_id": 1, **kwargs, - } - a = (1,) +@@ -198,35 +185,21 @@ b = (1,) c = 1 d = (1,) + a + (2,) @@ -593,7 +573,7 @@ last_call() mapping = { A: 0.25 * (10.0 / 12), B: 0.1 * (10.0 / 12), -@@ -236,31 +220,29 @@ +@@ -236,31 +209,29 @@ def gen(): @@ -638,7 +618,7 @@ last_call() ... for j in 1 + (2 + 3): ... -@@ -272,7 +254,7 @@ +@@ -272,7 +243,7 @@ addr_proto, addr_canonname, addr_sockaddr, @@ -647,7 +627,7 @@ last_call() pass a = ( aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp -@@ -291,9 +273,9 @@ +@@ -291,9 +262,9 @@ is not qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz ) if ( @@ -660,7 +640,7 @@ last_call() ): return True if ( -@@ -327,13 +309,18 @@ +@@ -327,13 +298,18 @@ ): return True if ( @@ -682,7 +662,7 @@ last_call() ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n ): return True -@@ -341,7 +328,8 @@ +@@ -341,7 +317,8 @@ ~aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e @@ -692,7 +672,7 @@ last_call() ^ aaaaaaaaaaaaaaaa.i << aaaaaaaaaaaaaaaa.k >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n -@@ -366,5 +354,5 @@ +@@ -366,5 +343,5 @@ ^ bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ^ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ) @@ -705,7 +685,7 @@ last_call() ```py ... -"NOT_YET_IMPLEMENTED_STRING" +"some_string" b"NOT_YET_IMPLEMENTED_BYTE_STRING" Name None @@ -752,29 +732,22 @@ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false (NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false) -{ - "NOT_YET_IMPLEMENTED_STRING": dead, - "NOT_YET_IMPLEMENTED_STRING": (long_live or die_hard), -} -{ - "NOT_YET_IMPLEMENTED_STRING": dead, - "NOT_YET_IMPLEMENTED_STRING": (long_live or die_hard), - **{"NOT_YET_IMPLEMENTED_STRING": verygood}, -} +{"2.7": dead, "3.7": (long_live or die_hard)} +{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} {**a, **b, **c} { - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", + "2.7", + "3.6", + "3.7", + "3.8", + "3.9", (NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false), } ( - {"NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING"}, + {"a": "b"}, (True or False), (+value), - "NOT_YET_IMPLEMENTED_STRING", + "string", b"NOT_YET_IMPLEMENTED_BYTE_STRING", ) or None () @@ -867,16 +840,13 @@ numpy[:, l[-2]] numpy[:, :: -1] numpy[np.newaxis, :] NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false +{"2.7": dead, "3.7": long_live or die_hard} { - "NOT_YET_IMPLEMENTED_STRING": dead, - "NOT_YET_IMPLEMENTED_STRING": long_live or die_hard, -} -{ - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", + "2.7", + "3.6", + "3.7", + "3.8", + "3.9", NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, } [1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C] @@ -889,13 +859,12 @@ SomeName (i for i in []) (NOT_YET_IMPLEMENTED_ExprStarred,) { - "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call(), - "NOT_YET_IMPLEMENTED_STRING": NOT_IMPLEMENTED_call() - + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), - "NOT_YET_IMPLEMENTED_STRING": 1, - "NOT_YET_IMPLEMENTED_STRING": 1, + "id": "1", + "type": "type", + "started_at": NOT_IMPLEMENTED_call(), + "ended_at": NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), + "priority": 1, + "import_session_id": 1, **kwargs, } a = (1,) diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap index 8790bc64bf..076c32ce50 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap @@ -90,9 +90,8 @@ def test_calculate_fades(): + def verify_fader(test): -- """Hey, ho.""" + """Hey, ho.""" - assert test.passed() -+ "NOT_YET_IMPLEMENTED_STRING" + NOT_YET_IMPLEMENTED_StmtAssert + @@ -137,7 +136,7 @@ def verify_fader(test): def verify_fader(test): - "NOT_YET_IMPLEMENTED_STRING" + """Hey, ho.""" NOT_YET_IMPLEMENTED_StmtAssert diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap index b76dbc5ab7..55baa4ec3a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap @@ -152,22 +152,20 @@ elif unformatted: # Regression test for https://github.com/psf/black/issues/3184. -@@ -53,28 +35,29 @@ +@@ -52,29 +34,27 @@ + async def call(param): if param: # fmt: off - if param[0:4] in ( +- if param[0:4] in ( - "ABCD", "EFGH" - ) : -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ ): ++ if param[0:4] in ("ABCD", "EFGH"): # fmt: on - print ( "This won't be formatted" ) + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -- elif param[0:4] in ("ZZZZ",): + elif param[0:4] in ("ZZZZ",): - print ( "This won't be formatted either" ) -+ elif param[0:4] in ("NOT_YET_IMPLEMENTED_STRING",): + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - print("This will be formatted") @@ -190,7 +188,7 @@ elif unformatted: # fmt: on -@@ -82,6 +65,6 @@ +@@ -82,6 +62,6 @@ if x: return x # fmt: off @@ -240,14 +238,11 @@ class A: async def call(param): if param: # fmt: off - if param[0:4] in ( - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - ): + if param[0:4] in ("ABCD", "EFGH"): # fmt: on NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - elif param[0:4] in ("NOT_YET_IMPLEMENTED_STRING",): + elif param[0:4] in ("ZZZZ",): NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap index fb89db8d6f..3bf9990399 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap @@ -203,14 +203,14 @@ d={'a':1, #!/usr/bin/env python3 -import asyncio -import sys -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImport - +- -from third_party import X, Y, Z -+NOT_YET_IMPLEMENTED_StmtImportFrom ++NOT_YET_IMPLEMENTED_StmtImport ++NOT_YET_IMPLEMENTED_StmtImport -from library import some_connection, some_decorator -- ++NOT_YET_IMPLEMENTED_StmtImportFrom + +NOT_YET_IMPLEMENTED_StmtImportFrom # fmt: off -from third_party import (X, @@ -253,7 +253,7 @@ d={'a':1, - async with some_connection() as conn: - await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2) - await asyncio.sleep(1) -+ "NOT_YET_IMPLEMENTED_STRING" ++ "Single-line docstring. Multiline is harder to reformat." + NOT_YET_IMPLEMENTED_StmtAsyncWith + await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + @@ -269,7 +269,7 @@ d={'a':1, +def function_signature_stress_test( + number: int, + no_annotation=None, -+ text: str = "NOT_YET_IMPLEMENTED_STRING", ++ text: str = "default", + *, + debug: bool = False, + **kwargs, @@ -289,24 +289,22 @@ d={'a':1, + e=True, + f=-1, + g=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, -+ h="NOT_YET_IMPLEMENTED_STRING", -+ i="NOT_YET_IMPLEMENTED_STRING", ++ h="", ++ i=r"", +): + offset = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + NOT_YET_IMPLEMENTED_StmtAssert def spaces_types( -@@ -51,76 +72,71 @@ +@@ -51,68 +72,66 @@ d: dict = {}, e: bool = True, f: int = -1, - g: int = 1 if False else 2, -- h: str = "", -- i: str = r"", + g: int = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, -+ h: str = "NOT_YET_IMPLEMENTED_STRING", -+ i: str = "NOT_YET_IMPLEMENTED_STRING", + h: str = "", + i: str = r"", ): ... @@ -319,7 +317,7 @@ d={'a':1, something = { # fmt: off - key: 'value', -+ key: "NOT_YET_IMPLEMENTED_STRING", ++ key: "value", } @@ -332,8 +330,8 @@ d={'a':1, - goes + here, - andhere, + ( -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", ++ "some big and", ++ "complex subscript", + # fmt: on + goes + + here, @@ -347,7 +345,7 @@ d={'a':1, - from hello import a, b - 'unformatted' + NOT_YET_IMPLEMENTED_StmtImportFrom -+ "NOT_YET_IMPLEMENTED_STRING" ++ "unformatted" # fmt: on @@ -356,7 +354,7 @@ d={'a':1, - a , b = *hello - 'unformatted' + a, b = NOT_YET_IMPLEMENTED_ExprStarred -+ "NOT_YET_IMPLEMENTED_STRING" ++ "unformatted" # fmt: on @@ -365,15 +363,14 @@ d={'a':1, - yield hello - 'unformatted' + NOT_YET_IMPLEMENTED_ExprYield -+ "NOT_YET_IMPLEMENTED_STRING" ++ "unformatted" # fmt: on -- "formatted" -+ "NOT_YET_IMPLEMENTED_STRING" + "formatted" # fmt: off - ( yield hello ) - 'unformatted' + (NOT_YET_IMPLEMENTED_ExprYield) -+ "NOT_YET_IMPLEMENTED_STRING" ++ "unformatted" # fmt: on @@ -389,21 +386,8 @@ d={'a':1, # fmt: on - def off_and_on_without_data(): -- """All comments here are technically on the same prefix. -- -- The comments between will be formatted. This is a known limitation. -- """ -+ "NOT_YET_IMPLEMENTED_STRING" - # fmt: off - - # hey, that won't work -@@ -130,13 +146,13 @@ - - - def on_and_off_broken(): -- """Another known limitation.""" -+ "NOT_YET_IMPLEMENTED_STRING" +@@ -133,10 +152,10 @@ + """Another known limitation.""" # fmt: on # fmt: off - this=should.not_be.formatted() @@ -417,7 +401,7 @@ d={'a':1, # fmt: on # fmt: off # ...but comments still get reformatted even though they should not be -@@ -145,80 +161,21 @@ +@@ -145,43 +164,11 @@ def long_lines(): if True: @@ -429,13 +413,11 @@ d={'a':1, - implicit_default=True, - ) - ) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - # fmt: off +- # fmt: off - a = ( - unnecessary_bracket() - ) -+ a = NOT_IMPLEMENTED_call() - # fmt: on +- # fmt: on - _type_comment_re = re.compile( - r""" - ^ @@ -456,16 +438,17 @@ d={'a':1, - ) - $ - """, -- # fmt: off ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + # fmt: off - re.MULTILINE|re.VERBOSE -- # fmt: on ++ a = NOT_IMPLEMENTED_call() + # fmt: on - ) + _type_comment_re = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def single_literal_yapf_disable(): -- """Black does not support this.""" -+ "NOT_YET_IMPLEMENTED_STRING" +@@ -189,36 +176,9 @@ BAZ = {(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)} # yapf: disable @@ -505,7 +488,7 @@ d={'a':1, -d={'a':1, - 'b':2} +l = [1, 2, 3] -+d = {"NOT_YET_IMPLEMENTED_STRING": 1, "NOT_YET_IMPLEMENTED_STRING": 2} ++d = {"a": 1, "b": 2} ``` ## Ruff Output @@ -544,7 +527,7 @@ def func_no_args(): async def coroutine(arg, exec=False): - "NOT_YET_IMPLEMENTED_STRING" + "Single-line docstring. Multiline is harder to reformat." NOT_YET_IMPLEMENTED_StmtAsyncWith await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) @@ -554,7 +537,7 @@ async def coroutine(arg, exec=False): def function_signature_stress_test( number: int, no_annotation=None, - text: str = "NOT_YET_IMPLEMENTED_STRING", + text: str = "default", *, debug: bool = False, **kwargs, @@ -571,8 +554,8 @@ def spaces( e=True, f=-1, g=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, - h="NOT_YET_IMPLEMENTED_STRING", - i="NOT_YET_IMPLEMENTED_STRING", + h="", + i=r"", ): offset = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) NOT_YET_IMPLEMENTED_StmtAssert @@ -586,8 +569,8 @@ def spaces_types( e: bool = True, f: int = -1, g: int = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, - h: str = "NOT_YET_IMPLEMENTED_STRING", - i: str = "NOT_YET_IMPLEMENTED_STRING", + h: str = "", + i: str = r"", ): ... @@ -598,7 +581,7 @@ def spaces2(result=NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)): something = { # fmt: off - key: "NOT_YET_IMPLEMENTED_STRING", + key: "value", } @@ -606,8 +589,8 @@ def subscriptlist(): atom[ # fmt: off ( - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", + "some big and", + "complex subscript", # fmt: on goes + here, @@ -619,26 +602,26 @@ def subscriptlist(): def import_as_names(): # fmt: off NOT_YET_IMPLEMENTED_StmtImportFrom - "NOT_YET_IMPLEMENTED_STRING" + "unformatted" # fmt: on def testlist_star_expr(): # fmt: off a, b = NOT_YET_IMPLEMENTED_ExprStarred - "NOT_YET_IMPLEMENTED_STRING" + "unformatted" # fmt: on def yield_expr(): # fmt: off NOT_YET_IMPLEMENTED_ExprYield - "NOT_YET_IMPLEMENTED_STRING" + "unformatted" # fmt: on - "NOT_YET_IMPLEMENTED_STRING" + "formatted" # fmt: off (NOT_YET_IMPLEMENTED_ExprYield) - "NOT_YET_IMPLEMENTED_STRING" + "unformatted" # fmt: on @@ -649,7 +632,10 @@ def example(session): def off_and_on_without_data(): - "NOT_YET_IMPLEMENTED_STRING" + """All comments here are technically on the same prefix. + + The comments between will be formatted. This is a known limitation. + """ # fmt: off # hey, that won't work @@ -659,7 +645,7 @@ def off_and_on_without_data(): def on_and_off_broken(): - "NOT_YET_IMPLEMENTED_STRING" + """Another known limitation.""" # fmt: on # fmt: off this = NOT_IMPLEMENTED_call() @@ -682,7 +668,7 @@ def long_lines(): def single_literal_yapf_disable(): - "NOT_YET_IMPLEMENTED_STRING" + """Black does not support this.""" BAZ = {(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)} # yapf: disable @@ -691,7 +677,7 @@ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) NOT_YET_IMPLEMENTED_ExprYield # No formatting to the end of the file l = [1, 2, 3] -d = {"NOT_YET_IMPLEMENTED_STRING": 1, "NOT_YET_IMPLEMENTED_STRING": 2} +d = {"a": 1, "b": 2} ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip2_py.snap index 79cc4331e4..13bf3277f2 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip2_py.snap @@ -16,49 +16,38 @@ l3 = ["I have", "trailing comma", "so I should be braked",] ```diff --- Black +++ Ruff -@@ -1,11 +1,15 @@ - l1 = [ -- "This list should be broken up", -- "into multiple lines", -- "because it is way too long", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", +@@ -3,9 +3,9 @@ + "into multiple lines", + "because it is way too long", ] -l2 = ["But this list shouldn't", "even though it also has", "way too many characters in it"] # fmt: skip -+l2 = [ -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+] # fmt: skip - l3 = [ +-l3 = [ - "I have", - "trailing comma", - "so I should be braked", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", - ] +-] ++l2 = [ ++ "But this list shouldn't", ++ "even though it also has", ++ "way too many characters in it", ++] # fmt: skip ++l3 = ["I have", "trailing comma", "so I should be braked"] ``` ## Ruff Output ```py l1 = [ - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", + "This list should be broken up", + "into multiple lines", + "because it is way too long", ] l2 = [ - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", + "But this list shouldn't", + "even though it also has", + "way too many characters in it", ] # fmt: skip -l3 = [ - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", -] +l3 = ["I have", "trailing comma", "so I should be braked"] ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip3_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip3_py.snap index 4b45e63eaa..2e54d4b721 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip3_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip3_py.snap @@ -20,7 +20,7 @@ f = ["This is a very long line that should be formatted into a clearer line ", " ```diff --- Black +++ Ruff -@@ -1,10 +1,7 @@ +@@ -1,7 +1,7 @@ a = 3 # fmt: off -b, c = 1, 2 @@ -29,11 +29,7 @@ f = ["This is a very long line that should be formatted into a clearer line ", " +d = 6 # fmt: skip e = 5 # fmt: on --f = [ -- "This is a very long line that should be formatted into a clearer line ", -- "by rearranging.", --] -+f = ["NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING"] + f = [ ``` ## Ruff Output @@ -45,7 +41,10 @@ b, c = 1, 2 d = 6 # fmt: skip e = 5 # fmt: on -f = ["NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING"] +f = [ + "This is a very long line that should be formatted into a clearer line ", + "by rearranging.", +] ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip7_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip7_py.snap index f088f1dd20..4eb0c61744 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip7_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip7_py.snap @@ -18,22 +18,20 @@ d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasu --- Black +++ Ruff @@ -1,4 +1,4 @@ --a = "this is some code" + a = "this is some code" -b = 5 # fmt:skip -+a = "NOT_YET_IMPLEMENTED_STRING" +b = 5 # fmt:skip c = 9 # fmt: skip --d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip -+d = "NOT_YET_IMPLEMENTED_STRING" # fmt:skip + d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip ``` ## Ruff Output ```py -a = "NOT_YET_IMPLEMENTED_STRING" +a = "this is some code" b = 5 # fmt:skip c = 9 # fmt: skip -d = "NOT_YET_IMPLEMENTED_STRING" # fmt:skip +d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap index a13146444a..2a6e53604d 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap @@ -66,7 +66,7 @@ with hmm_but_this_should_get_two_preceding_newlines(): ```diff --- Black +++ Ruff -@@ -2,64 +2,41 @@ +@@ -2,17 +2,9 @@ a, **kwargs, ) -> A: @@ -87,9 +87,7 @@ with hmm_but_this_should_get_two_preceding_newlines(): def g(): -- "Docstring." -+ "NOT_YET_IMPLEMENTED_STRING" - +@@ -21,45 +13,30 @@ def inner(): pass @@ -105,15 +103,14 @@ with hmm_but_this_should_get_two_preceding_newlines(): + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) --if os.name == "posix": + if os.name == "posix": - import termios -+if os.name == "NOT_YET_IMPLEMENTED_STRING": + NOT_YET_IMPLEMENTED_StmtImport def i_should_be_followed_by_only_one_newline(): pass - --elif os.name == "nt": + elif os.name == "nt": - try: - import msvcrt - @@ -124,7 +121,6 @@ with hmm_but_this_should_get_two_preceding_newlines(): - - def i_should_be_followed_by_only_one_newline(): - pass -+elif os.name == "NOT_YET_IMPLEMENTED_STRING": + NOT_YET_IMPLEMENTED_StmtTry elif False: @@ -158,7 +154,7 @@ def f( def g(): - "NOT_YET_IMPLEMENTED_STRING" + "Docstring." def inner(): pass @@ -173,12 +169,12 @@ def h(): NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -if os.name == "NOT_YET_IMPLEMENTED_STRING": +if os.name == "posix": NOT_YET_IMPLEMENTED_StmtImport def i_should_be_followed_by_only_one_newline(): pass -elif os.name == "NOT_YET_IMPLEMENTED_STRING": +elif os.name == "nt": NOT_YET_IMPLEMENTED_StmtTry elif False: diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap index f51aeec357..da23fa784a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap @@ -116,17 +116,17 @@ def __await__(): return (yield) +NOT_YET_IMPLEMENTED_StmtImport -from third_party import X, Y, Z -- --from library import some_connection, some_decorator +NOT_YET_IMPLEMENTED_StmtImportFrom +-from library import some_connection, some_decorator +- -f"trigger 3.6 mode" +NOT_YET_IMPLEMENTED_StmtImportFrom +NOT_YET_IMPLEMENTED_ExprJoinedStr def func_no_args(): -@@ -14,39 +13,48 @@ +@@ -14,25 +13,24 @@ b c if True: @@ -145,11 +145,10 @@ def __await__(): return (yield) async def coroutine(arg, exec=False): -- "Single-line docstring. Multiline is harder to reformat." + "Single-line docstring. Multiline is harder to reformat." - async with some_connection() as conn: - await conn.do_what_i_mean("SELECT bobby, tables FROM xkcd", timeout=2) - await asyncio.sleep(1) -+ "NOT_YET_IMPLEMENTED_STRING" + NOT_YET_IMPLEMENTED_StmtAsyncWith + await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) @@ -160,9 +159,7 @@ def __await__(): return (yield) def function_signature_stress_test( number: int, no_annotation=None, -- text: str = "default", -+ text: str = "NOT_YET_IMPLEMENTED_STRING", - *, +@@ -41,12 +39,22 @@ debug: bool = False, **kwargs, ) -> str: @@ -181,8 +178,8 @@ def __await__(): return (yield) + e=True, + f=-1, + g=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, -+ h="NOT_YET_IMPLEMENTED_STRING", -+ i="NOT_YET_IMPLEMENTED_STRING", ++ h="", ++ i=r"", +): + offset = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + NOT_YET_IMPLEMENTED_StmtAssert @@ -194,11 +191,9 @@ def __await__(): return (yield) e: bool = True, f: int = -1, - g: int = 1 if False else 2, -- h: str = "", -- i: str = r"", + g: int = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, -+ h: str = "NOT_YET_IMPLEMENTED_STRING", -+ i: str = "NOT_YET_IMPLEMENTED_STRING", + h: str = "", + i: str = r"", ): ... @@ -317,7 +312,7 @@ def func_no_args(): async def coroutine(arg, exec=False): - "NOT_YET_IMPLEMENTED_STRING" + "Single-line docstring. Multiline is harder to reformat." NOT_YET_IMPLEMENTED_StmtAsyncWith await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) @@ -327,7 +322,7 @@ async def coroutine(arg, exec=False): def function_signature_stress_test( number: int, no_annotation=None, - text: str = "NOT_YET_IMPLEMENTED_STRING", + text: str = "default", *, debug: bool = False, **kwargs, @@ -343,8 +338,8 @@ def spaces( e=True, f=-1, g=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, - h="NOT_YET_IMPLEMENTED_STRING", - i="NOT_YET_IMPLEMENTED_STRING", + h="", + i=r"", ): offset = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) NOT_YET_IMPLEMENTED_StmtAssert @@ -358,8 +353,8 @@ def spaces_types( e: bool = True, f: int = -1, g: int = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, - h: str = "NOT_YET_IMPLEMENTED_STRING", - i: str = "NOT_YET_IMPLEMENTED_STRING", + h: str = "", + i: str = r"", ): ... diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap index e44027943b..e117c52399 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap @@ -74,27 +74,7 @@ some_module.some_function( ```diff --- Black +++ Ruff -@@ -2,7 +2,7 @@ - a, - ): - d = { -- "key": "value", -+ "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", - } - tup = (1,) - -@@ -12,8 +12,8 @@ - b, - ): - d = { -- "key": "value", -- "key2": "value2", -+ "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", - } - tup = ( - 1, -@@ -24,45 +24,37 @@ +@@ -24,18 +24,14 @@ def f( a: int = 1, ): @@ -109,36 +89,22 @@ some_module.some_function( + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) x = { -- "a": 1, -- "b": 2, + "a": 1, + "b": 2, - }["a"] -+ "NOT_YET_IMPLEMENTED_STRING": 1, -+ "NOT_YET_IMPLEMENTED_STRING": 2, + }[ -+ "NOT_YET_IMPLEMENTED_STRING" ++ "a" + ] if ( a == { -- "a": 1, -- "b": 2, -- "c": 3, -- "d": 4, -- "e": 5, -- "f": 6, -- "g": 7, -- "h": 8, +@@ -47,23 +43,17 @@ + "f": 6, + "g": 7, + "h": 8, - }["a"] -+ "NOT_YET_IMPLEMENTED_STRING": 1, -+ "NOT_YET_IMPLEMENTED_STRING": 2, -+ "NOT_YET_IMPLEMENTED_STRING": 3, -+ "NOT_YET_IMPLEMENTED_STRING": 4, -+ "NOT_YET_IMPLEMENTED_STRING": 5, -+ "NOT_YET_IMPLEMENTED_STRING": 6, -+ "NOT_YET_IMPLEMENTED_STRING": 7, -+ "NOT_YET_IMPLEMENTED_STRING": 8, + }[ -+ "NOT_YET_IMPLEMENTED_STRING" ++ "a" + ] ): pass @@ -147,8 +113,7 @@ some_module.some_function( -def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> ( - Set["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"] -): -+def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set["NOT_YET_IMPLEMENTED_STRING"]: - json = { +- json = { - "k": { - "k2": { - "k3": [ @@ -156,13 +121,15 @@ some_module.some_function( - ] - } - } -+ "NOT_YET_IMPLEMENTED_STRING": { -+ "NOT_YET_IMPLEMENTED_STRING": {"NOT_YET_IMPLEMENTED_STRING": [1]}, -+ }, - } +- } ++def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ ++ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ++]: ++ json = {"k": {"k2": {"k3": [1]}}} -@@ -80,35 +72,16 @@ + # The type annotation shouldn't get a trailing comma since that would change its type. +@@ -80,35 +70,16 @@ pass @@ -211,7 +178,7 @@ def f( a, ): d = { - "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", + "key": "value", } tup = (1,) @@ -221,8 +188,8 @@ def f2( b, ): d = { - "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING": "NOT_YET_IMPLEMENTED_STRING", + "key": "value", + "key2": "value2", } tup = ( 1, @@ -236,35 +203,33 @@ def f( NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) x = { - "NOT_YET_IMPLEMENTED_STRING": 1, - "NOT_YET_IMPLEMENTED_STRING": 2, + "a": 1, + "b": 2, }[ - "NOT_YET_IMPLEMENTED_STRING" + "a" ] if ( a == { - "NOT_YET_IMPLEMENTED_STRING": 1, - "NOT_YET_IMPLEMENTED_STRING": 2, - "NOT_YET_IMPLEMENTED_STRING": 3, - "NOT_YET_IMPLEMENTED_STRING": 4, - "NOT_YET_IMPLEMENTED_STRING": 5, - "NOT_YET_IMPLEMENTED_STRING": 6, - "NOT_YET_IMPLEMENTED_STRING": 7, - "NOT_YET_IMPLEMENTED_STRING": 8, + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + "f": 6, + "g": 7, + "h": 8, }[ - "NOT_YET_IMPLEMENTED_STRING" + "a" ] ): pass -def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set["NOT_YET_IMPLEMENTED_STRING"]: - json = { - "NOT_YET_IMPLEMENTED_STRING": { - "NOT_YET_IMPLEMENTED_STRING": {"NOT_YET_IMPLEMENTED_STRING": [1]}, - }, - } +def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +]: + json = {"k": {"k2": {"k3": [1]}}} # The type annotation shouldn't get a trailing comma since that would change its type. diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap index df1dfc81f6..30c49898e4 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap @@ -62,9 +62,7 @@ __all__ = ( ```diff --- Black +++ Ruff -@@ -1,54 +1,32 @@ --"""The asyncio package, tracking PEP 3156.""" -+"NOT_YET_IMPLEMENTED_STRING" +@@ -2,53 +2,31 @@ # flake8: noqa @@ -142,7 +140,7 @@ __all__ = ( ## Ruff Output ```py -"NOT_YET_IMPLEMENTED_STRING" +"""The asyncio package, tracking PEP 3156.""" # flake8: noqa diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap index 867b4314cd..693a6f7001 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap @@ -83,10 +83,9 @@ return np.divide( -b = 5 ** f() +b = 5 ** NOT_IMPLEMENTED_call() c = -(5**2) --d = 5 ** f["hi"] + d = 5 ** f["hi"] -e = lazy(lambda **kwargs: 5) -f = f() ** 5 -+d = 5 ** f["NOT_YET_IMPLEMENTED_STRING"] +e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +f = NOT_IMPLEMENTED_call() ** 5 g = a.b**c.d @@ -114,10 +113,9 @@ return np.divide( -b = 5.0 ** f() +b = 5.0 ** NOT_IMPLEMENTED_call() c = -(5.0**2.0) --d = 5.0 ** f["hi"] + d = 5.0 ** f["hi"] -e = lazy(lambda **kwargs: 5) -f = f() ** 5.0 -+d = 5.0 ** f["NOT_YET_IMPLEMENTED_STRING"] +e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +f = NOT_IMPLEMENTED_call() ** 5.0 g = a.b**c.d @@ -177,7 +175,7 @@ def function_dont_replace_spaces(): a = 5**~4 b = 5 ** NOT_IMPLEMENTED_call() c = -(5**2) -d = 5 ** f["NOT_YET_IMPLEMENTED_STRING"] +d = 5 ** f["hi"] e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) f = NOT_IMPLEMENTED_call() ** 5 g = a.b**c.d @@ -196,7 +194,7 @@ r = x**y a = 5.0**~4.0 b = 5.0 ** NOT_IMPLEMENTED_call() c = -(5.0**2.0) -d = 5.0 ** f["NOT_YET_IMPLEMENTED_STRING"] +d = 5.0 ** f["hi"] e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) f = NOT_IMPLEMENTED_call() ** 5.0 g = a.b**c.d diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap index edf95a4f94..c9d1141cbf 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap @@ -68,7 +68,7 @@ def example8(): ```diff --- Black +++ Ruff -@@ -1,24 +1,16 @@ +@@ -1,20 +1,12 @@ x = 1 x = 1.2 @@ -91,11 +91,6 @@ def example8(): def example(): -- return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -+ return "NOT_YET_IMPLEMENTED_STRING" - - - def example1(): @@ -30,15 +22,11 @@ @@ -103,7 +98,7 @@ def example8(): - return ( - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - ) -+ return "NOT_YET_IMPLEMENTED_STRING" ++ return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" def example3(): @@ -169,7 +164,7 @@ async def show_status(): def example(): - return "NOT_YET_IMPLEMENTED_STRING" + return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" def example1(): @@ -181,7 +176,7 @@ def example1point5(): def example2(): - return "NOT_YET_IMPLEMENTED_STRING" + return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" def example3(): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap index 9298adaab4..1af332dee3 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap @@ -33,19 +33,16 @@ def docstring_multiline(): ```diff --- Black +++ Ruff -@@ -1,20 +1,36 @@ +@@ -1,13 +1,31 @@ #!/usr/bin/env python3 --name = "Łukasz" + name = "Łukasz" -(f"hello {name}", f"hello {name}") -(b"", b"") --("", "") --(r"", R"") -+name = "NOT_YET_IMPLEMENTED_STRING" +(NOT_YET_IMPLEMENTED_ExprJoinedStr, NOT_YET_IMPLEMENTED_ExprJoinedStr) +(b"NOT_YET_IMPLEMENTED_BYTE_STRING", b"NOT_YET_IMPLEMENTED_BYTE_STRING") -+("NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING") -+("NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING") + ("", "") + (r"", R"") -(rf"", rf"", Rf"", Rf"", rf"", rf"", Rf"", Rf"") -(rb"", rb"", Rb"", Rb"", rb"", rb"", Rb"", Rb"") @@ -72,15 +69,6 @@ def docstring_multiline(): def docstring_singleline(): -- R"""2020 was one hell of a year. The good news is that we were able to""" -+ "NOT_YET_IMPLEMENTED_STRING" - - - def docstring_multiline(): -- R""" -- clear out all of the issues opened in that time :p -- """ -+ "NOT_YET_IMPLEMENTED_STRING" ``` ## Ruff Output @@ -88,11 +76,11 @@ def docstring_multiline(): ```py #!/usr/bin/env python3 -name = "NOT_YET_IMPLEMENTED_STRING" +name = "Łukasz" (NOT_YET_IMPLEMENTED_ExprJoinedStr, NOT_YET_IMPLEMENTED_ExprJoinedStr) (b"NOT_YET_IMPLEMENTED_BYTE_STRING", b"NOT_YET_IMPLEMENTED_BYTE_STRING") -("NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING") -("NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING") +("", "") +(r"", R"") ( NOT_YET_IMPLEMENTED_ExprJoinedStr, @@ -117,11 +105,13 @@ name = "NOT_YET_IMPLEMENTED_STRING" def docstring_singleline(): - "NOT_YET_IMPLEMENTED_STRING" + R"""2020 was one hell of a year. The good news is that we were able to""" def docstring_multiline(): - "NOT_YET_IMPLEMENTED_STRING" + R""" + clear out all of the issues opened in that time :p + """ ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap index f331e7a484..70f66ad05c 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap @@ -70,7 +70,7 @@ class A: - self.min_length, - ) % {"min_length": self.min_length} + return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) % { -+ "NOT_YET_IMPLEMENTED_STRING": self.min_length, ++ "min_length": self.min_length, + } @@ -112,7 +112,7 @@ if x: class X: def get_help_text(self): return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) % { - "NOT_YET_IMPLEMENTED_STRING": self.min_length, + "min_length": self.min_length, } diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens2_py.snap index 3186661eb0..122f1f4cfb 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens2_py.snap @@ -23,7 +23,7 @@ if (e123456.get_tk_patchlevel() >= (8, 6, 0, 'final') or - 8, -) <= get_tk_patchlevel() < (8, 6): +if ( -+ NOT_IMPLEMENTED_call() >= (8, 6, 0, "NOT_YET_IMPLEMENTED_STRING") ++ NOT_IMPLEMENTED_call() >= (8, 6, 0, "final") + or (8, 5, 8) <= NOT_IMPLEMENTED_call() < (8, 6) +): pass @@ -33,7 +33,7 @@ if (e123456.get_tk_patchlevel() >= (8, 6, 0, 'final') or ```py if ( - NOT_IMPLEMENTED_call() >= (8, 6, 0, "NOT_YET_IMPLEMENTED_STRING") + NOT_IMPLEMENTED_call() >= (8, 6, 0, "final") or (8, 5, 8) <= NOT_IMPLEMENTED_call() < (8, 6) ): pass diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens3_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens3_py.snap index 4108c2434f..106c7c2554 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens3_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens3_py.snap @@ -31,8 +31,8 @@ if True: - "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", - ) % {"reported_username": reported_username, "report_reason": report_reason} + return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) % { -+ "NOT_YET_IMPLEMENTED_STRING": reported_username, -+ "NOT_YET_IMPLEMENTED_STRING": report_reason, ++ "reported_username": reported_username, ++ "report_reason": report_reason, + } ``` @@ -43,8 +43,8 @@ if True: if True: if True: return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) % { - "NOT_YET_IMPLEMENTED_STRING": reported_username, - "NOT_YET_IMPLEMENTED_STRING": report_reason, + "reported_username": reported_username, + "report_reason": report_reason, } ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap index 3ef4708db1..b6cdaf967b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap @@ -46,7 +46,7 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( ```diff --- Black +++ Ruff -@@ -1,50 +1,30 @@ +@@ -1,50 +1,26 @@ -zero( - one, -).two( @@ -86,11 +86,7 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( - }, - api_key=api_key, - )["extensions"]["sdk"]["token"] -+ return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)["NOT_YET_IMPLEMENTED_STRING"][ -+ "NOT_YET_IMPLEMENTED_STRING" -+ ][ -+ "NOT_YET_IMPLEMENTED_STRING" -+ ] ++ return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)["extensions"]["sdk"]["token"] # Edge case where a bug in a working-in-progress version of @@ -130,11 +126,7 @@ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Example from https://github.com/psf/black/issues/3229 def refresh_token(self, device_family, refresh_token, api_key): - return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)["NOT_YET_IMPLEMENTED_STRING"][ - "NOT_YET_IMPLEMENTED_STRING" - ][ - "NOT_YET_IMPLEMENTED_STRING" - ] + return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)["extensions"]["sdk"]["token"] # Edge case where a bug in a working-in-progress version of diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__slice_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__slice_py.snap index 4fa305f243..80370410e7 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__slice_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__slice_py.snap @@ -93,13 +93,13 @@ e210 = "e"[a() : 1 :] ## Output ```py # Handle comments both when lower and upper exist and when they don't -a1 = "NOT_YET_IMPLEMENTED_STRING"[ +a1 = "a"[ # a 1 # b : # c 2 # d ] -a2 = "NOT_YET_IMPLEMENTED_STRING"[ +a2 = "a"[ # a # b : # c @@ -107,7 +107,7 @@ a2 = "NOT_YET_IMPLEMENTED_STRING"[ ] # Check all places where comments can exist -b1 = "NOT_YET_IMPLEMENTED_STRING"[ # a +b1 = "b"[ # a # b 1 # c # d @@ -122,33 +122,33 @@ b1 = "NOT_YET_IMPLEMENTED_STRING"[ # a ] # Handle the spacing from the colon correctly with upper leading comments -c1 = "NOT_YET_IMPLEMENTED_STRING"[ +c1 = "c"[ 1: # e # f 2 ] -c2 = "NOT_YET_IMPLEMENTED_STRING"[ +c2 = "c"[ 1: # e 2 ] -c3 = "NOT_YET_IMPLEMENTED_STRING"[ +c3 = "c"[ 1: # f 2 ] -c4 = "NOT_YET_IMPLEMENTED_STRING"[ +c4 = "c"[ 1: # f 2 ] # End of line comments -d1 = "NOT_YET_IMPLEMENTED_STRING"[ # comment +d1 = "d"[ # comment : ] -d2 = "NOT_YET_IMPLEMENTED_STRING"[ # comment +d2 = "d"[ # comment 1: ] -d3 = "NOT_YET_IMPLEMENTED_STRING"[ +d3 = "d"[ 1 # comment : ] @@ -159,19 +159,19 @@ def a(): ... -e00 = "NOT_YET_IMPLEMENTED_STRING"[:] -e01 = "NOT_YET_IMPLEMENTED_STRING"[:1] -e02 = "NOT_YET_IMPLEMENTED_STRING"[ : NOT_IMPLEMENTED_call()] -e10 = "NOT_YET_IMPLEMENTED_STRING"[1:] -e11 = "NOT_YET_IMPLEMENTED_STRING"[1:1] -e12 = "NOT_YET_IMPLEMENTED_STRING"[1 : NOT_IMPLEMENTED_call()] -e20 = "NOT_YET_IMPLEMENTED_STRING"[NOT_IMPLEMENTED_call() :] -e21 = "NOT_YET_IMPLEMENTED_STRING"[NOT_IMPLEMENTED_call() : 1] -e22 = "NOT_YET_IMPLEMENTED_STRING"[NOT_IMPLEMENTED_call() : NOT_IMPLEMENTED_call()] -e200 = "NOT_YET_IMPLEMENTED_STRING"[NOT_IMPLEMENTED_call() : :] -e201 = "NOT_YET_IMPLEMENTED_STRING"[NOT_IMPLEMENTED_call() :: 1] -e202 = "NOT_YET_IMPLEMENTED_STRING"[NOT_IMPLEMENTED_call() :: NOT_IMPLEMENTED_call()] -e210 = "NOT_YET_IMPLEMENTED_STRING"[NOT_IMPLEMENTED_call() : 1 :] +e00 = "e"[:] +e01 = "e"[:1] +e02 = "e"[ : NOT_IMPLEMENTED_call()] +e10 = "e"[1:] +e11 = "e"[1:1] +e12 = "e"[1 : NOT_IMPLEMENTED_call()] +e20 = "e"[NOT_IMPLEMENTED_call() :] +e21 = "e"[NOT_IMPLEMENTED_call() : 1] +e22 = "e"[NOT_IMPLEMENTED_call() : NOT_IMPLEMENTED_call()] +e200 = "e"[NOT_IMPLEMENTED_call() : :] +e201 = "e"[NOT_IMPLEMENTED_call() :: 1] +e202 = "e"[NOT_IMPLEMENTED_call() :: NOT_IMPLEMENTED_call()] +e210 = "e"[NOT_IMPLEMENTED_call() : 1 :] ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__string_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__string_py.snap new file mode 100644 index 0000000000..d57de901e1 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__string_py.snap @@ -0,0 +1,119 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +--- +## Input +```py +"' test" +'" test' + +"\" test" +'\' test' + +# Prefer single quotes for string with more double quotes +"' \" \" '' \" \" '" + +# Prefer double quotes for string with more single quotes +'\' " " \'\' " " \'' + +# Prefer double quotes for string with equal amount of single and double quotes +'" \' " " \'\'' +"' \" '' \" \" '" + +"\\' \"\"" +'\\\' ""' + + +u"Test" +U"Test" + +r"Test" +R"Test" + +'This string will not include \ +backslashes or newline characters.' + +if True: + 'This string will not include \ + backslashes or newline characters.' + +"""Multiline +String \" +""" + +'''Multiline +String \' +''' + +'''Multiline +String "" +''' + +'''Multiline +String """ +''' + +'''Multiline +String \"\"\" +''' +``` + + + +## Output +```py +"' test" +'" test' + +'" test' +"' test" + +# Prefer single quotes for string with more double quotes +"' \" \" '' \" \" '" + +# Prefer double quotes for string with more single quotes +"' \" \" '' \" \" '" + +# Prefer double quotes for string with equal amount of single and double quotes +"\" ' \" \" ''" +"' \" '' \" \" '" + +'\\\' ""' +'\\\' ""' + + +"Test" +"Test" + +r"Test" +R"Test" + +"This string will not include \ +backslashes or newline characters." + +if True: + "This string will not include \ + backslashes or newline characters." + +"""Multiline +String \" +""" + +"""Multiline +String \' +""" + +"""Multiline +String "" +""" + +'''Multiline +String """ +''' + +"""Multiline +String \"\"\" +""" +``` + + diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__tuple_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__tuple_py.snap index 70dab6420a..cce6cccafc 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__tuple_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__tuple_py.snap @@ -77,98 +77,88 @@ a4 = ((1, 2), 3) # Wrapping parentheses checks b1 = ( - ("NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING"), - ( - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - ), - ("NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING"), - ("NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING"), + ("Michael", "Ende"), + ("Der", "satanarchäolügenialkohöllische", "Wunschpunsch"), + ("Beelzebub", "Irrwitzer"), + ("Tyrannja", "Vamperl"), ) b2 = ( - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", + "akjdshflkjahdslkfjlasfdahjlfds", + "ljklsadhflakfdajflahfdlajfhafldkjalfj", + "ljklsadhflakfdajflahfdlajfhafldkjalf2", ) b3 = ( - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", + "The", + "Night", + "of", + "Wishes:", + "Or", + "the", + "Satanarchaeolidealcohellish", + "Notion", + "Potion", ) # Nested wrapping parentheses check c1 = ( - ("NOT_YET_IMPLEMENTED_STRING"), + ("cicero"), ( - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - ), - ( - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", + "Qui", + "autem,", + "si", + "maxime", + "hoc", + "placeat,", + "moderatius", + "tamen", + "id", + "uolunt", + "fieri,", + "difficilem", + "quandam", + "temperantiam", + "postulant", + "in", + "eo,", + "quod", + "semel", + "admissum", + "coerceri", + "reprimique", + "non", + "potest,", + "ut", + "propemodum", + "iustioribus", + "utamur", + "illis,", + "qui", + "omnino", + "auocent", + "a", + "philosophia,", + "quam", + "his,", + "qui", + "rebus", + "infinitis", + "modum", + "constituant", + "in", + "reque", + "eo", + "meliore,", + "quo", + "maior", + "sit,", + "mediocritatem", + "desiderent.", ), + ("de", "finibus", "bonorum", "et", "malorum"), ) # Deeply nested parentheses -d1 = ((("NOT_YET_IMPLEMENTED_STRING",),),) +d1 = ((("3D",),),) d2 = ( ( ( @@ -197,7 +187,7 @@ d2 = ( ( ( ( - "NOT_YET_IMPLEMENTED_STRING", + "¯\_(ツ)_/¯", ), ), ), @@ -234,7 +224,10 @@ e2 = ( 2, ) e3 = (1,) -e4 = ("NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING") +e4 = ( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor", + "incididunt", +) # Empty tuples and comments f1 = ( @@ -260,8 +253,14 @@ g2 = ( # Ensure the correct number of parentheses h1 = (1, 2) -h2 = (1, "NOT_YET_IMPLEMENTED_STRING") -h3 = 1, "NOT_YET_IMPLEMENTED_STRING" +h2 = ( + 1, + "qweiurpoiqwurepqiurpqirpuqoiwrupqoirupqoirupqoiurpqiorupwqiourpqurpqurpqurpqurpqurpqurüqurqpuriq", +) +h3 = ( + 1, + "qweiurpoiqwurepqiurpqirpuqoiwrupqoirupqoirupqoiurpqiorupwqiourpqurpqurpqurpqurpqurpqurüqurqpuriq", +) ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap index 8bd96653d9..a7e33556d9 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap @@ -115,23 +115,23 @@ else: # instead of the else. # Originally found in https://github.com/python/cpython/blob/ab3823a97bdeefb0266b3c8d493f7f6223ce3686/Lib/dataclasses.py#L539 -if "NOT_YET_IMPLEMENTED_STRING": +if "if 1": pass -elif "NOT_YET_IMPLEMENTED_STRING": +elif "elif 1": pass # Don't drop this comment 1 x = 1 -if "NOT_YET_IMPLEMENTED_STRING": +if "if 2": pass -elif "NOT_YET_IMPLEMENTED_STRING": +elif "elif 2": pass else: pass # Don't drop this comment 2 x = 2 -if "NOT_YET_IMPLEMENTED_STRING": +if "if 3": pass else: pass From 930f03de989c5c88a5c68f255dae570b52d87492 Mon Sep 17 00:00:00 2001 From: konstin Date: Fri, 23 Jun 2023 10:07:28 +0200 Subject: [PATCH 208/447] Don't mistake a following if for an elif (#5296) In the following code, the comment used to get wrongly associated with the `if False` since it looked like an elif. This fixes it by checking the indentation and adding a regression test ```python if True: pass else: # Comment if False: pass pass ``` Originally found in https://github.com/gradio-app/gradio/blob/1570b94a02d23d051ae137e0063974fd8a48b34e/gradio/external.py#L478 --- .../test/fixtures/ruff/statement/if.py | 9 +++ .../src/comments/placement.rs | 71 ++++++++++++------- ...r__tests__ruff_test__statement__if_py.snap | 18 +++++ 3 files changed, 73 insertions(+), 25 deletions(-) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py index 91303ef592..e737ebbab1 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py @@ -62,3 +62,12 @@ else: pass # Don't drop this comment 3 x = 3 + +# Regression test for a following if that could get confused for an elif +# Originally found in https://github.com/gradio-app/gradio/blob/1570b94a02d23d051ae137e0063974fd8a48b34e/gradio/external.py#L478 +if True: + pass +else: # Comment + if False: + pass + pass diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index e1cded005f..ca0fec261e 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -335,7 +335,52 @@ fn handle_in_between_bodies_end_of_line_comment<'a>( return CommentPlacement::Default(comment); } - if !locator.contains_line_break(TextRange::new(preceding.end(), comment.slice().start())) { + if locator.contains_line_break(TextRange::new(preceding.end(), comment.slice().start())) { + // The `elif` or except handlers have their own body to which we can attach the trailing comment + // ```python + // if test: + // a + // elif c: # comment + // b + // ``` + if following.is_except_handler() { + return CommentPlacement::trailing(following, comment); + } else if following.is_stmt_if() { + // We have to exclude for following if statements that are not elif by checking the + // indentation + // ```python + // if True: + // pass + // else: # Comment + // if False: + // pass + // pass + // ``` + let base_if_indent = + whitespace::indentation_at_offset(locator, following.range().start()); + let maybe_elif_indent = whitespace::indentation_at_offset( + locator, + comment.enclosing_node().range().start(), + ); + if base_if_indent == maybe_elif_indent { + return CommentPlacement::trailing(following, comment); + } + } + // There are no bodies for the "else" branch and other bodies that are represented as a `Vec`. + // This means, there's no good place to attach the comments to. + // Make this a dangling comments and manually format the comment in + // in the enclosing node's formatting logic. For `try`, it's the formatters responsibility + // to correctly identify the comments for the `finally` and `orelse` block by looking + // at the comment's range. + // + // ```python + // while x == y: + // pass + // else: # trailing + // print("nooop") + // ``` + CommentPlacement::dangling(comment.enclosing_node(), comment) + } else { // Trailing comment of the preceding statement // ```python // while test: @@ -357,30 +402,6 @@ fn handle_in_between_bodies_end_of_line_comment<'a>( } else { CommentPlacement::trailing(preceding, comment) } - } else if following.is_stmt_if() || following.is_except_handler() { - // The `elif` or except handlers have their own body to which we can attach the trailing comment - // ```python - // if test: - // a - // elif c: # comment - // b - // ``` - CommentPlacement::trailing(following, comment) - } else { - // There are no bodies for the "else" branch and other bodies that are represented as a `Vec`. - // This means, there's no good place to attach the comments to. - // Make this a dangling comments and manually format the comment in - // in the enclosing node's formatting logic. For `try`, it's the formatters responsibility - // to correctly identify the comments for the `finally` and `orelse` block by looking - // at the comment's range. - // - // ```python - // while x == y: - // pass - // else: # trailing - // print("nooop") - // ``` - CommentPlacement::dangling(comment.enclosing_node(), comment) } } else { CommentPlacement::Default(comment) diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap index a7e33556d9..1c41ddd972 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap @@ -68,6 +68,15 @@ else: pass # Don't drop this comment 3 x = 3 + +# Regression test for a following if that could get confused for an elif +# Originally found in https://github.com/gradio-app/gradio/blob/1570b94a02d23d051ae137e0063974fd8a48b34e/gradio/external.py#L478 +if True: + pass +else: # Comment + if False: + pass + pass ``` @@ -137,6 +146,15 @@ else: pass # Don't drop this comment 3 x = 3 + +# Regression test for a following if that could get confused for an elif +# Originally found in https://github.com/gradio-app/gradio/blob/1570b94a02d23d051ae137e0063974fd8a48b34e/gradio/external.py#L478 +if True: + pass +else: # Comment + if False: + pass + pass ``` From 2dfa6ff58d1ef00c803743fb176b5b75646f153b Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 23 Jun 2023 11:50:24 +0200 Subject: [PATCH 209/447] Fix unstable set comprehension formatting (#5327) --- .../test/fixtures/ruff/expression/binary.py | 5 +++++ .../src/expression/expr_set_comp.rs | 11 ++++++++--- ...matter__tests__black_test__expression_py.snap | 16 ++++++++-------- ..._tests__ruff_test__expression__binary_py.snap | 16 ++++++++++++++-- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py index 314f83c1ea..73529824ed 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py @@ -187,3 +187,8 @@ if ( a + b ): ... + + +# Unstable formatting in https://github.com/realtyem/synapse-unraid/blob/unraid_develop/synapse/handlers/presence.py +for user_id in set(target_user_ids) - {u.user_id for u in updates}: + updates.append(UserPresenceState.default(user_id)) diff --git a/crates/ruff_python_formatter/src/expression/expr_set_comp.rs b/crates/ruff_python_formatter/src/expression/expr_set_comp.rs index a73ede1c68..d5174782e0 100644 --- a/crates/ruff_python_formatter/src/expression/expr_set_comp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_set_comp.rs @@ -2,7 +2,7 @@ use crate::comments::Comments; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; +use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; use rustpython_parser::ast::ExprSetComp; @@ -10,8 +10,13 @@ use rustpython_parser::ast::ExprSetComp; pub struct FormatExprSetComp; impl FormatNodeRule for FormatExprSetComp { - fn fmt_fields(&self, item: &ExprSetComp, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + fn fmt_fields(&self, _item: &ExprSetComp, f: &mut PyFormatter) -> FormatResult<()> { + write!( + f, + [not_yet_implemented_custom_text( + "{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set}" + )] + ) } } diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap index d64a1abf8a..1499731809 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap @@ -386,10 +386,10 @@ last_call() - k: v - for k, v in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension -} -+NOT_YET_IMPLEMENTED_ExprSetComp -+NOT_YET_IMPLEMENTED_ExprSetComp -+NOT_YET_IMPLEMENTED_ExprSetComp -+NOT_YET_IMPLEMENTED_ExprSetComp ++{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} ++{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} +[i for i in []] +[i for i in []] +[i for i in []] @@ -767,10 +767,10 @@ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false another, NOT_YET_IMPLEMENTED_ExprStarred, ] -NOT_YET_IMPLEMENTED_ExprSetComp -NOT_YET_IMPLEMENTED_ExprSetComp -NOT_YET_IMPLEMENTED_ExprSetComp -NOT_YET_IMPLEMENTED_ExprSetComp +{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} +{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} [i for i in []] [i for i in []] [i for i in []] diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap index e55f432f29..f79bc76fa1 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap @@ -193,6 +193,11 @@ if ( a + b ): ... + + +# Unstable formatting in https://github.com/realtyem/synapse-unraid/blob/unraid_develop/synapse/handlers/presence.py +for user_id in set(target_user_ids) - {u.user_id for u in updates}: + updates.append(UserPresenceState.default(user_id)) ``` @@ -241,7 +246,7 @@ aaaaaaaaaaaaaa + { } aaaaaaaaaaaaaa + [i for i in []] aaaaaaaaaaaaaa + (i for i in []) -aaaaaaaaaaaaaa + NOT_YET_IMPLEMENTED_ExprSetComp +aaaaaaaaaaaaaa + {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} # Wraps it in parentheses if it needs to break both left and right ( @@ -251,7 +256,7 @@ aaaaaaaaaaaaaa + NOT_YET_IMPLEMENTED_ExprSetComp # But only for expressions that have a statement parent. -not (aaaaaaaaaaaaaa + NOT_YET_IMPLEMENTED_ExprSetComp) +not (aaaaaaaaaaaaaa + {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set}) [ a + [ @@ -432,6 +437,13 @@ if ( + b ): ... + + +# Unstable formatting in https://github.com/realtyem/synapse-unraid/blob/unraid_develop/synapse/handlers/presence.py +for ( + user_id +) in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set}: + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` From 1c638264b2d6fc7b6ac3107fb6742d49c1649a1d Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 23 Jun 2023 15:40:35 +0200 Subject: [PATCH 210/447] Keep track of when files are last seen in the cache (#5214) ## Summary And remove cached files that we haven't seen for a certain period of time, currently 30 days. For the last seen timestamp we actually use an `u64`, it's smaller on disk than `SystemTime` (which size is OS dependent) and fits in an `AtomicU64` which we can use to update it without locks. ## Test Plan Added a new unit test, run by `cargo test`. --- .../fixtures/cache_remove_old_files/source.py | 4 + crates/ruff_cli/src/cache.rs | 188 +++++++++++++----- crates/ruff_cli/src/diagnostics.rs | 12 +- 3 files changed, 146 insertions(+), 58 deletions(-) create mode 100644 crates/ruff_cli/resources/test/fixtures/cache_remove_old_files/source.py diff --git a/crates/ruff_cli/resources/test/fixtures/cache_remove_old_files/source.py b/crates/ruff_cli/resources/test/fixtures/cache_remove_old_files/source.py new file mode 100644 index 0000000000..7e397f06e5 --- /dev/null +++ b/crates/ruff_cli/resources/test/fixtures/cache_remove_old_files/source.py @@ -0,0 +1,4 @@ +# NOTE: sync with cache::invalidation test +a = 1 + +__all__ = list(["a", "b"]) diff --git a/crates/ruff_cli/src/cache.rs b/crates/ruff_cli/src/cache.rs index 9c4aa3db73..fe614e7f8a 100644 --- a/crates/ruff_cli/src/cache.rs +++ b/crates/ruff_cli/src/cache.rs @@ -3,8 +3,9 @@ use std::fs::{self, File}; use std::hash::Hasher; use std::io::{self, BufReader, BufWriter, Write}; use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Mutex; -use std::time::SystemTime; +use std::time::{Duration, SystemTime}; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; @@ -20,6 +21,9 @@ use ruff_text_size::{TextRange, TextSize}; use crate::diagnostics::Diagnostics; +/// Maximum duration for which we keep a file in cache that hasn't been seen. +const MAX_LAST_SEEN: Duration = Duration::from_secs(30 * 24 * 60 * 60); // 30 days. + /// [`Path`] that is relative to the package root in [`PackageCache`]. pub(crate) type RelativePath = Path; /// [`PathBuf`] that is relative to the package root in [`PackageCache`]. @@ -33,6 +37,7 @@ pub(crate) type RelativePathBuf = PathBuf; /// /// This type manages the cache file, reading it from disk and writing it back /// to disk (if required). +#[derive(Debug)] pub(crate) struct Cache { /// Location of the cache. path: PathBuf, @@ -44,6 +49,9 @@ pub(crate) struct Cache { /// `package.files` but are outdated. This gets merged with `package.files` /// when the cache is written back to disk in [`Cache::store`]. new_files: Mutex>, + /// The "current" timestamp used as cache for the updates of + /// [`FileCache::last_seen`] + last_seen_cache: u64, } impl Cache { @@ -94,26 +102,32 @@ impl Cache { ); package.files.clear(); } - Cache { - path, - package, - new_files: Mutex::new(HashMap::new()), - } + Cache::new(path, package) } /// Create an empty `Cache`. fn empty(path: PathBuf, package_root: PathBuf) -> Cache { + let package = PackageCache { + package_root, + files: HashMap::new(), + }; + Cache::new(path, package) + } + + #[allow(clippy::cast_possible_truncation)] + fn new(path: PathBuf, package: PackageCache) -> Cache { Cache { path, - package: PackageCache { - package_root, - files: HashMap::new(), - }, + package, new_files: Mutex::new(HashMap::new()), + // SAFETY: this will be truncated to the year ~2554 (so don't use + // this code after that!). + last_seen_cache: SystemTime::UNIX_EPOCH.elapsed().unwrap().as_millis() as u64, } } /// Store the cache to disk, if it has been changed. + #[allow(clippy::cast_possible_truncation)] pub(crate) fn store(mut self) -> Result<()> { let new_files = self.new_files.into_inner().unwrap(); if new_files.is_empty() { @@ -122,7 +136,14 @@ impl Cache { return Ok(()); } - // Add/overwrite the changes made. + // Remove cached files that we haven't seen in a while. + let now = self.last_seen_cache; + self.package.files.retain(|_, file| { + // SAFETY: this will be truncated to the year ~2554. + (now - *file.last_seen.get_mut()) <= MAX_LAST_SEEN.as_millis() as u64 + }); + + // Apply any changes made and keep track of when we last saw files. self.package.files.extend(new_files); let file = File::create(&self.path) @@ -161,51 +182,19 @@ impl Cache { return None; } + file.last_seen.store(self.last_seen_cache, Ordering::SeqCst); + Some(file) } /// Add or update a file cache at `path` relative to the package root. - pub(crate) fn update(&self, path: RelativePathBuf, file: FileCache) { - self.new_files.lock().unwrap().insert(path, file); - } -} - -/// On disk representation of a cache of a package. -#[derive(Deserialize, Debug, Serialize)] -struct PackageCache { - /// Path to the root of the package. - /// - /// Usually this is a directory, but it can also be a single file in case of - /// single file "packages", e.g. scripts. - package_root: PathBuf, - /// Mapping of source file path to it's cached data. - files: HashMap, -} - -/// On disk representation of the cache per source file. -#[derive(Clone, Deserialize, Debug, Serialize)] -pub(crate) struct FileCache { - /// Timestamp when the file was last modified before the (cached) check. - last_modified: SystemTime, - /// Imports made. - imports: ImportMap, - /// Diagnostic messages. - messages: Vec, - /// Source code of the file. - /// - /// # Notes - /// - /// This will be empty if `messages` is empty. - source: String, -} - -impl FileCache { - /// Create a new source file cache. - pub(crate) fn new( + pub(crate) fn update( + &self, + path: RelativePathBuf, last_modified: SystemTime, messages: &[Message], imports: &ImportMap, - ) -> FileCache { + ) { let source = if let Some(msg) = messages.first() { msg.file.source_text().to_owned() } else { @@ -229,14 +218,52 @@ impl FileCache { }) .collect(); - FileCache { + let file = FileCache { last_modified, + last_seen: AtomicU64::new(self.last_seen_cache), imports: imports.clone(), messages, source, - } + }; + self.new_files.lock().unwrap().insert(path, file); } +} +/// On disk representation of a cache of a package. +#[derive(Deserialize, Debug, Serialize)] +struct PackageCache { + /// Path to the root of the package. + /// + /// Usually this is a directory, but it can also be a single file in case of + /// single file "packages", e.g. scripts. + package_root: PathBuf, + /// Mapping of source file path to it's cached data. + files: HashMap, +} + +/// On disk representation of the cache per source file. +#[derive(Deserialize, Debug, Serialize)] +pub(crate) struct FileCache { + /// Timestamp when the file was last modified before the (cached) check. + last_modified: SystemTime, + /// Timestamp when we last linted this file. + /// + /// Represented as the number of milliseconds since Unix epoch. This will + /// break in 1970 + ~584 years (~2554). + last_seen: AtomicU64, + /// Imports made. + imports: ImportMap, + /// Diagnostic messages. + messages: Vec, + /// Source code of the file. + /// + /// # Notes + /// + /// This will be empty if `messages` is empty. + source: String, +} + +impl FileCache { /// Convert the file cache into `Diagnostics`, using `path` as file name. pub(crate) fn as_diagnostics(&self, path: &Path) -> Diagnostics { let messages = if self.messages.is_empty() { @@ -259,7 +286,7 @@ impl FileCache { } /// On disk representation of a diagnostic message. -#[derive(Clone, Deserialize, Debug, Serialize)] +#[derive(Deserialize, Debug, Serialize)] struct CacheMessage { kind: DiagnosticKind, /// Range into the message's [`FileCache::source`]. @@ -303,12 +330,15 @@ mod tests { use std::env::temp_dir; use std::fs; use std::io::{self, Write}; - use std::path::Path; + use std::path::{Path, PathBuf}; + use std::sync::atomic::AtomicU64; + use std::time::SystemTime; use ruff::settings::{flags, AllSettings}; use ruff_cache::CACHE_DIR_NAME; + use ruff_python_ast::imports::ImportMap; - use crate::cache::{self, Cache}; + use crate::cache::{self, Cache, FileCache}; use crate::diagnostics::{lint_path, Diagnostics}; #[test] @@ -497,4 +527,54 @@ mod tests { assert!(expected_diagnostics == got_diagnostics); } } + + #[test] + fn remove_old_files() { + let mut cache_dir = temp_dir(); + cache_dir.push("ruff_tests/cache_remove_old_files"); + let _ = fs::remove_dir_all(&cache_dir); + cache::init(&cache_dir).unwrap(); + + let settings = AllSettings::default(); + let package_root = + fs::canonicalize("resources/test/fixtures/cache_remove_old_files").unwrap(); + let mut cache = Cache::open(&cache_dir, package_root.clone(), &settings.lib); + assert_eq!(cache.new_files.lock().unwrap().len(), 0); + + // Add a file to the cache that hasn't been linted or seen since the + // '70s! + cache.package.files.insert( + PathBuf::from("old.py"), + FileCache { + last_modified: SystemTime::UNIX_EPOCH, + last_seen: AtomicU64::new(123), + imports: ImportMap::new(), + messages: Vec::new(), + source: String::new(), + }, + ); + + // Now actually lint a file. + let path = package_root.join("source.py"); + lint_path( + &path, + Some(&package_root), + &settings, + Some(&cache), + flags::Noqa::Enabled, + flags::FixMode::Generate, + ) + .unwrap(); + + // Storing the cache should remove the old (`old.py`) file. + cache.store().unwrap(); + // So we when we open the cache again it shouldn't contain `old.py`. + let cache = Cache::open(&cache_dir, package_root, &settings.lib); + + assert_eq!(cache.package.files.len(), 1, "didn't remove the old file"); + assert!( + !cache.package.files.contains_key(&path), + "removed the wrong file" + ); + } } diff --git a/crates/ruff_cli/src/diagnostics.rs b/crates/ruff_cli/src/diagnostics.rs index 1da81ed4cd..03f5527a5f 100644 --- a/crates/ruff_cli/src/diagnostics.rs +++ b/crates/ruff_cli/src/diagnostics.rs @@ -25,7 +25,7 @@ use ruff_python_ast::imports::ImportMap; use ruff_python_ast::source_code::{LineIndex, SourceCode, SourceFileBuilder}; use ruff_python_stdlib::path::is_project_toml; -use crate::cache::{Cache, FileCache}; +use crate::cache::Cache; #[derive(Debug, Default, PartialEq)] pub(crate) struct Diagnostics { @@ -208,10 +208,14 @@ pub(crate) fn lint_path( let imports = imports.unwrap_or_default(); if let Some((cache, relative_path, file_last_modified)) = caching { + // We don't cache parsing errors. if parse_error.is_none() { - // We don't cache parsing error. - let file_cache = FileCache::new(file_last_modified, &messages, &imports); - cache.update(relative_path.to_owned(), file_cache); + cache.update( + relative_path.to_owned(), + file_last_modified, + &messages, + &imports, + ); } } From 2f03159c8b959daf3d3bca7dfac443899419e972 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 23 Jun 2023 09:50:10 -0400 Subject: [PATCH 211/447] Use SSH clones in `update_schemastore.py` (#5322) --- scripts/update_schemastore.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/update_schemastore.py b/scripts/update_schemastore.py index 685fa7bbff..b060db31c0 100644 --- a/scripts/update_schemastore.py +++ b/scripts/update_schemastore.py @@ -11,8 +11,8 @@ from pathlib import Path from subprocess import check_call, check_output from tempfile import TemporaryDirectory -schemastore_fork = "https://github.com/astral-sh/schemastore" -schemastore_upstream = "https://github.com/SchemaStore/schemastore" +schemastore_fork = "git@github.com:astral-sh/schemastore.git" +schemastore_upstream = "git@github.com:SchemaStore/schemastore.git" ruff_repo = "https://github.com/astral-sh/ruff" root = Path( check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip(), From f85eb709e25b3a6d1e8e69a999d90991f3add9e6 Mon Sep 17 00:00:00 2001 From: James Berry Date: Fri, 23 Jun 2023 09:54:54 -0400 Subject: [PATCH 212/447] Visit AugAssign target after value (#5325) ## Summary When visiting AugAssign in evaluation order, the AugAssign `target` should be visited after it's `value`. Based on my testing, the pseudo code for `a += b` is effectively: ```python tmp = a a = tmp.__iadd__(b) ``` That is, an ideal traversal order would look something like this: 1. load a 2. b 3. op 4. store a But, there is only a single AST node which captures `a` in the statement `a += b`, so it cannot be traversed both before and after the traversal of `b` and the `op`. Nonetheless, I think traversing `a` after `b` and the `op` makes the most sense for a number of reasons: 1. All the other assignment expressions traverse their `value`s before their `target`s. Having `AugAssign` traverse in the same order would be more consistent. 2. Within the AST, the `ctx` of the `target` for an `AugAssign` is `Store` (though technically this is a `Load` and `Store` operation, the AST only indicates it as a `Store`). Since the the store portion of the `AugAssign` occurs last, I think it makes sense to traverse the `target` last as well. The effect of this is marginal, but it may have an impact on the behavior of #5271. --- crates/ruff_python_ast/src/visitor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_python_ast/src/visitor.rs b/crates/ruff_python_ast/src/visitor.rs index 9cf9d7786b..85c2d27f3c 100644 --- a/crates/ruff_python_ast/src/visitor.rs +++ b/crates/ruff_python_ast/src/visitor.rs @@ -167,9 +167,9 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { value, range: _range, }) => { - visitor.visit_expr(target); - visitor.visit_operator(op); visitor.visit_expr(value); + visitor.visit_operator(op); + visitor.visit_expr(target); } Stmt::AnnAssign(ast::StmtAnnAssign { target, From cb580f960f0adeb13d0bf17414a4642dfa69b1b2 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 23 Jun 2023 18:11:41 +0200 Subject: [PATCH 213/447] Make small tweaks to the profiling documentation (#5335) --- CONTRIBUTING.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 43c143cf5f..153b43fa39 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -327,22 +327,18 @@ git clone --branch 3.10 https://github.com/python/cpython.git crates/ruff/resour To benchmark the release build: ```shell -cargo build --release && hyperfine --ignore-failure --warmup 10 \ - "./target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache" \ - "./target/release/ruff ./crates/ruff/resources/test/cpython/" +cargo build --release && hyperfine --warmup 10 \ + "./target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache -e" \ + "./target/release/ruff ./crates/ruff/resources/test/cpython/ -e" Benchmark 1: ./target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache Time (mean ± σ): 293.8 ms ± 3.2 ms [User: 2384.6 ms, System: 90.3 ms] Range (min … max): 289.9 ms … 301.6 ms 10 runs - Warning: Ignoring non-zero exit code. - Benchmark 2: ./target/release/ruff ./crates/ruff/resources/test/cpython/ Time (mean ± σ): 48.0 ms ± 3.1 ms [User: 65.2 ms, System: 124.7 ms] Range (min … max): 45.0 ms … 66.7 ms 62 runs - Warning: Ignoring non-zero exit code. - Summary './target/release/ruff ./crates/ruff/resources/test/cpython/' ran 6.12 ± 0.41 times faster than './target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache' @@ -503,7 +499,7 @@ examples. Install `perf` and build `ruff_benchmark` with the `release-debug` profile and then run it with perf ```shell -cargo bench -p ruff_benchmark --no-run --profile=release-debug && perf record -g -F 9999 cargo bench -p ruff_benchmark --profile=release-debug -- --profile-time=1 +cargo bench -p ruff_benchmark --no-run --profile=release-debug && perf record --call-graph dwarf -F 9999 cargo bench -p ruff_benchmark --profile=release-debug -- --profile-time=1 ``` You can also use the `ruff_dev` launcher to run `ruff check` multiple times on a repository to From 4b65446de6c69dd69300ff506a9b8dd9b7642ef4 Mon Sep 17 00:00:00 2001 From: konstin Date: Fri, 23 Jun 2023 18:53:55 +0200 Subject: [PATCH 214/447] Refactor magic trailing comma (#5339) ## Summary This is small refactoring to reuse the code that detects the magic trailing comma across functions. I make this change now to avoid copying code in a later PR. @MichaReiser is planning on making a larger refactoring later that integrates with the join nodes builder ## Test Plan No functional changes. The magic trailing comma behaviour is checked by the fixtures. --- crates/ruff_python_formatter/src/builders.rs | 16 +++++++++-- .../src/expression/expr_dict.rs | 13 ++------- .../src/expression/expr_tuple.rs | 14 +++------- .../src/statement/stmt_class_def.rs | 27 +++++++------------ 4 files changed, 28 insertions(+), 42 deletions(-) diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 847df4d3c5..8d4e11db83 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -1,8 +1,9 @@ use crate::context::NodeLevel; use crate::prelude::*; -use crate::trivia::{lines_after, skip_trailing_trivia}; +use crate::trivia::{first_non_trivia_token, lines_after, skip_trailing_trivia, Token, TokenKind}; +use crate::USE_MAGIC_TRAILING_COMMA; use ruff_formatter::write; -use ruff_text_size::TextSize; +use ruff_text_size::{TextRange, TextSize}; use rustpython_parser::ast::Ranged; /// Provides Python specific extensions to [`Formatter`]. @@ -145,6 +146,17 @@ impl<'fmt, 'ast, 'buf> JoinNodesBuilder<'fmt, 'ast, 'buf> { } } +pub(crate) fn use_magic_trailing_comma(f: &mut PyFormatter, range: TextRange) -> bool { + USE_MAGIC_TRAILING_COMMA + && matches!( + first_non_trivia_token(range.end(), f.context().contents()), + Some(Token { + kind: TokenKind::Comma, + .. + }) + ) +} + #[cfg(test)] mod tests { use crate::comments::Comments; diff --git a/crates/ruff_python_formatter/src/expression/expr_dict.rs b/crates/ruff_python_formatter/src/expression/expr_dict.rs index e40574ee24..1a56058e8e 100644 --- a/crates/ruff_python_formatter/src/expression/expr_dict.rs +++ b/crates/ruff_python_formatter/src/expression/expr_dict.rs @@ -1,12 +1,10 @@ +use crate::builders::use_magic_trailing_comma; use crate::comments::{dangling_node_comments, leading_comments, Comments}; use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; use crate::prelude::*; -use crate::trivia::Token; -use crate::trivia::{first_non_trivia_token, TokenKind}; -use crate::USE_MAGIC_TRAILING_COMMA; use crate::{FormatNodeRule, PyFormatter}; use ruff_formatter::format_args; use ruff_formatter::{write, Buffer, FormatResult}; @@ -69,14 +67,7 @@ impl FormatNodeRule for FormatExprDict { } [.., last] => last, }; - let magic_trailing_comma = USE_MAGIC_TRAILING_COMMA - && matches!( - first_non_trivia_token(last.range().end(), f.context().contents()), - Some(Token { - kind: TokenKind::Comma, - .. - }) - ); + let magic_trailing_comma = use_magic_trailing_comma(f, last.range()); debug_assert_eq!(keys.len(), values.len()); diff --git a/crates/ruff_python_formatter/src/expression/expr_tuple.rs b/crates/ruff_python_formatter/src/expression/expr_tuple.rs index cc60fd57ea..a08a9a4b51 100644 --- a/crates/ruff_python_formatter/src/expression/expr_tuple.rs +++ b/crates/ruff_python_formatter/src/expression/expr_tuple.rs @@ -1,11 +1,10 @@ +use crate::builders::use_magic_trailing_comma; use crate::comments::{dangling_node_comments, Comments}; use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; -use crate::trivia::Token; -use crate::trivia::{first_non_trivia_token, TokenKind}; -use crate::{AsFormat, FormatNodeRule, FormattedIterExt, PyFormatter, USE_MAGIC_TRAILING_COMMA}; +use crate::{AsFormat, FormatNodeRule, FormattedIterExt, PyFormatter}; use ruff_formatter::formatter::Formatter; use ruff_formatter::prelude::{ block_indent, group, if_group_breaks, soft_block_indent, soft_line_break_or_space, text, @@ -88,14 +87,7 @@ impl FormatNodeRule for FormatExprTuple { [.., last] => last, }; - let magic_trailing_comma = USE_MAGIC_TRAILING_COMMA - && matches!( - first_non_trivia_token(last.range().end(), f.context().contents()), - Some(Token { - kind: TokenKind::Comma, - .. - }) - ); + let magic_trailing_comma = use_magic_trailing_comma(f, last.range()); if magic_trailing_comma { // A magic trailing comma forces us to print in expanded mode since we have more than diff --git a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs index e8784719c8..078f34051c 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs @@ -1,8 +1,8 @@ +use crate::builders::use_magic_trailing_comma; use crate::comments::trailing_comments; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; -use crate::trivia::{first_non_trivia_token, SimpleTokenizer, Token, TokenKind}; -use crate::USE_MAGIC_TRAILING_COMMA; +use crate::trivia::{SimpleTokenizer, TokenKind}; use ruff_formatter::{format_args, write}; use ruff_text_size::TextRange; use rustpython_parser::ast::{Expr, Keyword, Ranged, StmtClassDef}; @@ -115,22 +115,13 @@ impl Format> for FormatInheritanceClause<'_> { if_group_breaks(&text(",")).fmt(f)?; - if USE_MAGIC_TRAILING_COMMA { - let last_end = keywords - .last() - .map(Keyword::end) - .or_else(|| bases.last().map(Expr::end)) - .unwrap(); - - if matches!( - first_non_trivia_token(last_end, f.context().contents()), - Some(Token { - kind: TokenKind::Comma, - .. - }) - ) { - hard_line_break().fmt(f)?; - } + let last = keywords + .last() + .map(Keyword::range) + .or_else(|| bases.last().map(Expr::range)) + .unwrap(); + if use_magic_trailing_comma(f, last) { + hard_line_break().fmt(f)?; } Ok(()) From f45d1c2b84b281714d7eb096f7dd73ef7b30e289 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 23 Jun 2023 15:59:03 -0400 Subject: [PATCH 215/447] Remove HashMap and HashSet for known-standard-library detection (#5345) ## Summary This is a lot more concise and probably much more performant (with fewer instructions). --- Cargo.lock | 4 - crates/ruff/src/rules/isort/categorize.rs | 8 +- crates/ruff/src/settings/types.rs | 8 + crates/ruff_python_stdlib/Cargo.toml | 2 - crates/ruff_python_stdlib/src/sys.rs | 1379 ++++---------------- scripts/generate_known_standard_library.py | 74 +- 6 files changed, 326 insertions(+), 1149 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 61791812a6..2453977a0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2072,10 +2072,6 @@ dependencies = [ [[package]] name = "ruff_python_stdlib" version = "0.0.0" -dependencies = [ - "once_cell", - "rustc-hash", -] [[package]] name = "ruff_python_whitespace" diff --git a/crates/ruff/src/rules/isort/categorize.rs b/crates/ruff/src/rules/isort/categorize.rs index ac6348476d..c9ec34447a 100644 --- a/crates/ruff/src/rules/isort/categorize.rs +++ b/crates/ruff/src/rules/isort/categorize.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use strum_macros::EnumIter; use ruff_macros::CacheKey; -use ruff_python_stdlib::sys::KNOWN_STANDARD_LIBRARY; +use ruff_python_stdlib::sys::is_known_standard_library; use crate::settings::types::PythonVersion; use crate::warn_user_once; @@ -82,11 +82,7 @@ pub(crate) fn categorize<'a>( (&ImportSection::Known(ImportType::Future), Reason::Future) } else if let Some((import_type, reason)) = known_modules.categorize(module_name) { (import_type, reason) - } else if KNOWN_STANDARD_LIBRARY - .get(&target_version.as_tuple()) - .unwrap() - .contains(module_base) - { + } else if is_known_standard_library(target_version.minor(), module_base) { ( &ImportSection::Known(ImportType::StandardLibrary), Reason::KnownStandardLibrary, diff --git a/crates/ruff/src/settings/types.rs b/crates/ruff/src/settings/types.rs index ca813bcb0a..5395ce9ec5 100644 --- a/crates/ruff/src/settings/types.rs +++ b/crates/ruff/src/settings/types.rs @@ -52,6 +52,14 @@ impl PythonVersion { } } + pub const fn major(&self) -> u32 { + self.as_tuple().0 + } + + pub const fn minor(&self) -> u32 { + self.as_tuple().1 + } + pub fn get_minimum_supported_version(requires_version: &VersionSpecifiers) -> Option { let mut minimum_version = None; for python_version in PythonVersion::iter() { diff --git a/crates/ruff_python_stdlib/Cargo.toml b/crates/ruff_python_stdlib/Cargo.toml index 91e43d5377..167b9fad04 100644 --- a/crates/ruff_python_stdlib/Cargo.toml +++ b/crates/ruff_python_stdlib/Cargo.toml @@ -13,5 +13,3 @@ license = { workspace = true } [lib] [dependencies] -once_cell = { workspace = true } -rustc-hash = { workspace = true } diff --git a/crates/ruff_python_stdlib/src/sys.rs b/crates/ruff_python_stdlib/src/sys.rs index 791b7db2d9..c6d28db289 100644 --- a/crates/ruff_python_stdlib/src/sys.rs +++ b/crates/ruff_python_stdlib/src/sys.rs @@ -1,1107 +1,276 @@ //! This file is generated by `scripts/generate_known_standard_library.py` -use once_cell::sync::Lazy; -use rustc_hash::{FxHashMap, FxHashSet}; -// See: https://pycqa.github.io/isort/docs/configuration/options.html#known-standard-library -pub static KNOWN_STANDARD_LIBRARY: Lazy>> = - Lazy::new(|| { - FxHashMap::from_iter([ - ( - (3, 7), - FxHashSet::from_iter([ - "_ast", - "_dummy_thread", - "_thread", - "abc", - "aifc", - "argparse", - "array", - "ast", - "asynchat", - "asyncio", - "asyncore", - "atexit", - "audioop", - "base64", - "bdb", - "binascii", - "binhex", - "bisect", - "builtins", - "bz2", - "cProfile", - "calendar", - "cgi", - "cgitb", - "chunk", - "cmath", - "cmd", - "code", - "codecs", - "codeop", - "collections", - "colorsys", - "compileall", - "concurrent", - "configparser", - "contextlib", - "contextvars", - "copy", - "copyreg", - "crypt", - "csv", - "ctypes", - "curses", - "dataclasses", - "datetime", - "dbm", - "decimal", - "difflib", - "dis", - "distutils", - "doctest", - "dummy_threading", - "email", - "encodings", - "ensurepip", - "enum", - "errno", - "faulthandler", - "fcntl", - "filecmp", - "fileinput", - "fnmatch", - "formatter", - "fractions", - "ftplib", - "functools", - "gc", - "getopt", - "getpass", - "gettext", - "glob", - "grp", - "gzip", - "hashlib", - "heapq", - "hmac", - "html", - "http", - "imaplib", - "imghdr", - "imp", - "importlib", - "inspect", - "io", - "ipaddress", - "itertools", - "json", - "keyword", - "lib2to3", - "linecache", - "locale", - "logging", - "lzma", - "macpath", - "mailbox", - "mailcap", - "marshal", - "math", - "mimetypes", - "mmap", - "modulefinder", - "msilib", - "msvcrt", - "multiprocessing", - "netrc", - "nis", - "nntplib", - "ntpath", - "numbers", - "operator", - "optparse", - "os", - "ossaudiodev", - "parser", - "pathlib", - "pdb", - "pickle", - "pickletools", - "pipes", - "pkgutil", - "platform", - "plistlib", - "poplib", - "posix", - "posixpath", - "pprint", - "profile", - "pstats", - "pty", - "pwd", - "py_compile", - "pyclbr", - "pydoc", - "queue", - "quopri", - "random", - "re", - "readline", - "reprlib", - "resource", - "rlcompleter", - "runpy", - "sched", - "secrets", - "select", - "selectors", - "shelve", - "shlex", - "shutil", - "signal", - "site", - "smtpd", - "smtplib", - "sndhdr", - "socket", - "socketserver", - "spwd", - "sqlite3", - "sre", - "sre_compile", - "sre_constants", - "sre_parse", - "ssl", - "stat", - "statistics", - "string", - "stringprep", - "struct", - "subprocess", - "sunau", - "symbol", - "symtable", - "sys", - "sysconfig", - "syslog", - "tabnanny", - "tarfile", - "telnetlib", - "tempfile", - "termios", - "test", - "textwrap", - "threading", - "time", - "timeit", - "tkinter", - "token", - "tokenize", - "trace", - "traceback", - "tracemalloc", - "tty", - "turtle", - "turtledemo", - "types", - "typing", - "unicodedata", - "unittest", - "urllib", - "uu", - "uuid", - "venv", - "warnings", - "wave", - "weakref", - "webbrowser", - "winreg", - "winsound", - "wsgiref", - "xdrlib", - "xml", - "xmlrpc", - "zipapp", - "zipfile", - "zipimport", - "zlib", - ]), - ), - ( - (3, 8), - FxHashSet::from_iter([ - "_ast", - "_dummy_thread", - "_thread", - "abc", - "aifc", - "argparse", - "array", - "ast", - "asynchat", - "asyncio", - "asyncore", - "atexit", - "audioop", - "base64", - "bdb", - "binascii", - "binhex", - "bisect", - "builtins", - "bz2", - "cProfile", - "calendar", - "cgi", - "cgitb", - "chunk", - "cmath", - "cmd", - "code", - "codecs", - "codeop", - "collections", - "colorsys", - "compileall", - "concurrent", - "configparser", - "contextlib", - "contextvars", - "copy", - "copyreg", - "crypt", - "csv", - "ctypes", - "curses", - "dataclasses", - "datetime", - "dbm", - "decimal", - "difflib", - "dis", - "distutils", - "doctest", - "dummy_threading", - "email", - "encodings", - "ensurepip", - "enum", - "errno", - "faulthandler", - "fcntl", - "filecmp", - "fileinput", - "fnmatch", - "formatter", - "fractions", - "ftplib", - "functools", - "gc", - "getopt", - "getpass", - "gettext", - "glob", - "grp", - "gzip", - "hashlib", - "heapq", - "hmac", - "html", - "http", - "imaplib", - "imghdr", - "imp", - "importlib", - "inspect", - "io", - "ipaddress", - "itertools", - "json", - "keyword", - "lib2to3", - "linecache", - "locale", - "logging", - "lzma", - "mailbox", - "mailcap", - "marshal", - "math", - "mimetypes", - "mmap", - "modulefinder", - "msilib", - "msvcrt", - "multiprocessing", - "netrc", - "nis", - "nntplib", - "ntpath", - "numbers", - "operator", - "optparse", - "os", - "ossaudiodev", - "parser", - "pathlib", - "pdb", - "pickle", - "pickletools", - "pipes", - "pkgutil", - "platform", - "plistlib", - "poplib", - "posix", - "posixpath", - "pprint", - "profile", - "pstats", - "pty", - "pwd", - "py_compile", - "pyclbr", - "pydoc", - "queue", - "quopri", - "random", - "re", - "readline", - "reprlib", - "resource", - "rlcompleter", - "runpy", - "sched", - "secrets", - "select", - "selectors", - "shelve", - "shlex", - "shutil", - "signal", - "site", - "smtpd", - "smtplib", - "sndhdr", - "socket", - "socketserver", - "spwd", - "sqlite3", - "sre", - "sre_compile", - "sre_constants", - "sre_parse", - "ssl", - "stat", - "statistics", - "string", - "stringprep", - "struct", - "subprocess", - "sunau", - "symbol", - "symtable", - "sys", - "sysconfig", - "syslog", - "tabnanny", - "tarfile", - "telnetlib", - "tempfile", - "termios", - "test", - "textwrap", - "threading", - "time", - "timeit", - "tkinter", - "token", - "tokenize", - "trace", - "traceback", - "tracemalloc", - "tty", - "turtle", - "turtledemo", - "types", - "typing", - "unicodedata", - "unittest", - "urllib", - "uu", - "uuid", - "venv", - "warnings", - "wave", - "weakref", - "webbrowser", - "winreg", - "winsound", - "wsgiref", - "xdrlib", - "xml", - "xmlrpc", - "zipapp", - "zipfile", - "zipimport", - "zlib", - ]), - ), - ( - (3, 9), - FxHashSet::from_iter([ - "_ast", - "_thread", - "abc", - "aifc", - "argparse", - "array", - "ast", - "asynchat", - "asyncio", - "asyncore", - "atexit", - "audioop", - "base64", - "bdb", - "binascii", - "binhex", - "bisect", - "builtins", - "bz2", - "cProfile", - "calendar", - "cgi", - "cgitb", - "chunk", - "cmath", - "cmd", - "code", - "codecs", - "codeop", - "collections", - "colorsys", - "compileall", - "concurrent", - "configparser", - "contextlib", - "contextvars", - "copy", - "copyreg", - "crypt", - "csv", - "ctypes", - "curses", - "dataclasses", - "datetime", - "dbm", - "decimal", - "difflib", - "dis", - "distutils", - "doctest", - "email", - "encodings", - "ensurepip", - "enum", - "errno", - "faulthandler", - "fcntl", - "filecmp", - "fileinput", - "fnmatch", - "formatter", - "fractions", - "ftplib", - "functools", - "gc", - "getopt", - "getpass", - "gettext", - "glob", - "graphlib", - "grp", - "gzip", - "hashlib", - "heapq", - "hmac", - "html", - "http", - "imaplib", - "imghdr", - "imp", - "importlib", - "inspect", - "io", - "ipaddress", - "itertools", - "json", - "keyword", - "lib2to3", - "linecache", - "locale", - "logging", - "lzma", - "mailbox", - "mailcap", - "marshal", - "math", - "mimetypes", - "mmap", - "modulefinder", - "msilib", - "msvcrt", - "multiprocessing", - "netrc", - "nis", - "nntplib", - "ntpath", - "numbers", - "operator", - "optparse", - "os", - "ossaudiodev", - "parser", - "pathlib", - "pdb", - "pickle", - "pickletools", - "pipes", - "pkgutil", - "platform", - "plistlib", - "poplib", - "posix", - "posixpath", - "pprint", - "profile", - "pstats", - "pty", - "pwd", - "py_compile", - "pyclbr", - "pydoc", - "queue", - "quopri", - "random", - "re", - "readline", - "reprlib", - "resource", - "rlcompleter", - "runpy", - "sched", - "secrets", - "select", - "selectors", - "shelve", - "shlex", - "shutil", - "signal", - "site", - "smtpd", - "smtplib", - "sndhdr", - "socket", - "socketserver", - "spwd", - "sqlite3", - "sre", - "sre_compile", - "sre_constants", - "sre_parse", - "ssl", - "stat", - "statistics", - "string", - "stringprep", - "struct", - "subprocess", - "sunau", - "symbol", - "symtable", - "sys", - "sysconfig", - "syslog", - "tabnanny", - "tarfile", - "telnetlib", - "tempfile", - "termios", - "test", - "textwrap", - "threading", - "time", - "timeit", - "tkinter", - "token", - "tokenize", - "trace", - "traceback", - "tracemalloc", - "tty", - "turtle", - "turtledemo", - "types", - "typing", - "unicodedata", - "unittest", - "urllib", - "uu", - "uuid", - "venv", - "warnings", - "wave", - "weakref", - "webbrowser", - "winreg", - "winsound", - "wsgiref", - "xdrlib", - "xml", - "xmlrpc", - "zipapp", - "zipfile", - "zipimport", - "zlib", - "zoneinfo", - ]), - ), - ( - (3, 10), - FxHashSet::from_iter([ - "_ast", - "_thread", - "abc", - "aifc", - "argparse", - "array", - "ast", - "asynchat", - "asyncio", - "asyncore", - "atexit", - "audioop", - "base64", - "bdb", - "binascii", - "binhex", - "bisect", - "builtins", - "bz2", - "cProfile", - "calendar", - "cgi", - "cgitb", - "chunk", - "cmath", - "cmd", - "code", - "codecs", - "codeop", - "collections", - "colorsys", - "compileall", - "concurrent", - "configparser", - "contextlib", - "contextvars", - "copy", - "copyreg", - "crypt", - "csv", - "ctypes", - "curses", - "dataclasses", - "datetime", - "dbm", - "decimal", - "difflib", - "dis", - "distutils", - "doctest", - "email", - "encodings", - "ensurepip", - "enum", - "errno", - "faulthandler", - "fcntl", - "filecmp", - "fileinput", - "fnmatch", - "fractions", - "ftplib", - "functools", - "gc", - "getopt", - "getpass", - "gettext", - "glob", - "graphlib", - "grp", - "gzip", - "hashlib", - "heapq", - "hmac", - "html", - "http", - "idlelib", - "imaplib", - "imghdr", - "imp", - "importlib", - "inspect", - "io", - "ipaddress", - "itertools", - "json", - "keyword", - "lib2to3", - "linecache", - "locale", - "logging", - "lzma", - "mailbox", - "mailcap", - "marshal", - "math", - "mimetypes", - "mmap", - "modulefinder", - "msilib", - "msvcrt", - "multiprocessing", - "netrc", - "nis", - "nntplib", - "ntpath", - "numbers", - "operator", - "optparse", - "os", - "ossaudiodev", - "pathlib", - "pdb", - "pickle", - "pickletools", - "pipes", - "pkgutil", - "platform", - "plistlib", - "poplib", - "posix", - "posixpath", - "pprint", - "profile", - "pstats", - "pty", - "pwd", - "py_compile", - "pyclbr", - "pydoc", - "queue", - "quopri", - "random", - "re", - "readline", - "reprlib", - "resource", - "rlcompleter", - "runpy", - "sched", - "secrets", - "select", - "selectors", - "shelve", - "shlex", - "shutil", - "signal", - "site", - "smtpd", - "smtplib", - "sndhdr", - "socket", - "socketserver", - "spwd", - "sqlite3", - "sre", - "sre_compile", - "sre_constants", - "sre_parse", - "ssl", - "stat", - "statistics", - "string", - "stringprep", - "struct", - "subprocess", - "sunau", - "symtable", - "sys", - "sysconfig", - "syslog", - "tabnanny", - "tarfile", - "telnetlib", - "tempfile", - "termios", - "test", - "textwrap", - "threading", - "time", - "timeit", - "tkinter", - "token", - "tokenize", - "trace", - "traceback", - "tracemalloc", - "tty", - "turtle", - "turtledemo", - "types", - "typing", - "unicodedata", - "unittest", - "urllib", - "uu", - "uuid", - "venv", - "warnings", - "wave", - "weakref", - "webbrowser", - "winreg", - "winsound", - "wsgiref", - "xdrlib", - "xml", - "xmlrpc", - "zipapp", - "zipfile", - "zipimport", - "zlib", - "zoneinfo", - ]), - ), - ( - (3, 11), - FxHashSet::from_iter([ - "_ast", - "_thread", - "abc", - "aifc", - "argparse", - "array", - "ast", - "asynchat", - "asyncio", - "asyncore", - "atexit", - "audioop", - "base64", - "bdb", - "binascii", - "bisect", - "builtins", - "bz2", - "cProfile", - "calendar", - "cgi", - "cgitb", - "chunk", - "cmath", - "cmd", - "code", - "codecs", - "codeop", - "collections", - "colorsys", - "compileall", - "concurrent", - "configparser", - "contextlib", - "contextvars", - "copy", - "copyreg", - "crypt", - "csv", - "ctypes", - "curses", - "dataclasses", - "datetime", - "dbm", - "decimal", - "difflib", - "dis", - "distutils", - "doctest", - "email", - "encodings", - "ensurepip", - "enum", - "errno", - "faulthandler", - "fcntl", - "filecmp", - "fileinput", - "fnmatch", - "fractions", - "ftplib", - "functools", - "gc", - "getopt", - "getpass", - "gettext", - "glob", - "graphlib", - "grp", - "gzip", - "hashlib", - "heapq", - "hmac", - "html", - "http", - "idlelib", - "imaplib", - "imghdr", - "imp", - "importlib", - "inspect", - "io", - "ipaddress", - "itertools", - "json", - "keyword", - "lib2to3", - "linecache", - "locale", - "logging", - "lzma", - "mailbox", - "mailcap", - "marshal", - "math", - "mimetypes", - "mmap", - "modulefinder", - "msilib", - "msvcrt", - "multiprocessing", - "netrc", - "nis", - "nntplib", - "ntpath", - "numbers", - "operator", - "optparse", - "os", - "ossaudiodev", - "pathlib", - "pdb", - "pickle", - "pickletools", - "pipes", - "pkgutil", - "platform", - "plistlib", - "poplib", - "posix", - "posixpath", - "pprint", - "profile", - "pstats", - "pty", - "pwd", - "py_compile", - "pyclbr", - "pydoc", - "queue", - "quopri", - "random", - "re", - "readline", - "reprlib", - "resource", - "rlcompleter", - "runpy", - "sched", - "secrets", - "select", - "selectors", - "shelve", - "shlex", - "shutil", - "signal", - "site", - "smtpd", - "smtplib", - "sndhdr", - "socket", - "socketserver", - "spwd", - "sqlite3", - "sre", - "sre_compile", - "sre_constants", - "sre_parse", - "ssl", - "stat", - "statistics", - "string", - "stringprep", - "struct", - "subprocess", - "sunau", - "symtable", - "sys", - "sysconfig", - "syslog", - "tabnanny", - "tarfile", - "telnetlib", - "tempfile", - "termios", - "test", - "textwrap", - "threading", - "time", - "timeit", - "tkinter", - "token", - "tokenize", - "tomllib", - "trace", - "traceback", - "tracemalloc", - "tty", - "turtle", - "turtledemo", - "types", - "typing", - "unicodedata", - "unittest", - "urllib", - "uu", - "uuid", - "venv", - "warnings", - "wave", - "weakref", - "webbrowser", - "winreg", - "winsound", - "wsgiref", - "xdrlib", - "xml", - "xmlrpc", - "zipapp", - "zipfile", - "zipimport", - "zlib", - "zoneinfo", - ]), - ), - ]) - }); +pub fn is_known_standard_library(minor_version: u32, module: &str) -> bool { + matches!( + (minor_version, module), + ( + _, + "_ast" + | "_thread" + | "abc" + | "aifc" + | "argparse" + | "array" + | "ast" + | "asyncio" + | "atexit" + | "audioop" + | "base64" + | "bdb" + | "binascii" + | "bisect" + | "builtins" + | "bz2" + | "cProfile" + | "calendar" + | "cgi" + | "cgitb" + | "chunk" + | "cmath" + | "cmd" + | "code" + | "codecs" + | "codeop" + | "collections" + | "colorsys" + | "compileall" + | "concurrent" + | "configparser" + | "contextlib" + | "contextvars" + | "copy" + | "copyreg" + | "crypt" + | "csv" + | "ctypes" + | "curses" + | "dataclasses" + | "datetime" + | "dbm" + | "decimal" + | "difflib" + | "dis" + | "doctest" + | "email" + | "encodings" + | "ensurepip" + | "enum" + | "errno" + | "faulthandler" + | "fcntl" + | "filecmp" + | "fileinput" + | "fnmatch" + | "fractions" + | "ftplib" + | "functools" + | "gc" + | "getopt" + | "getpass" + | "gettext" + | "glob" + | "grp" + | "gzip" + | "hashlib" + | "heapq" + | "hmac" + | "html" + | "http" + | "imaplib" + | "imghdr" + | "importlib" + | "inspect" + | "io" + | "ipaddress" + | "itertools" + | "json" + | "keyword" + | "lib2to3" + | "linecache" + | "locale" + | "logging" + | "lzma" + | "mailbox" + | "mailcap" + | "marshal" + | "math" + | "mimetypes" + | "mmap" + | "modulefinder" + | "msilib" + | "msvcrt" + | "multiprocessing" + | "netrc" + | "nis" + | "nntplib" + | "ntpath" + | "numbers" + | "operator" + | "optparse" + | "os" + | "ossaudiodev" + | "pathlib" + | "pdb" + | "pickle" + | "pickletools" + | "pipes" + | "pkgutil" + | "platform" + | "plistlib" + | "poplib" + | "posix" + | "posixpath" + | "pprint" + | "profile" + | "pstats" + | "pty" + | "pwd" + | "py_compile" + | "pyclbr" + | "pydoc" + | "queue" + | "quopri" + | "random" + | "re" + | "readline" + | "reprlib" + | "resource" + | "rlcompleter" + | "runpy" + | "sched" + | "secrets" + | "select" + | "selectors" + | "shelve" + | "shlex" + | "shutil" + | "signal" + | "site" + | "smtplib" + | "sndhdr" + | "socket" + | "socketserver" + | "spwd" + | "sqlite3" + | "sre" + | "sre_compile" + | "sre_constants" + | "sre_parse" + | "ssl" + | "stat" + | "statistics" + | "string" + | "stringprep" + | "struct" + | "subprocess" + | "sunau" + | "symtable" + | "sys" + | "sysconfig" + | "syslog" + | "tabnanny" + | "tarfile" + | "telnetlib" + | "tempfile" + | "termios" + | "test" + | "textwrap" + | "threading" + | "time" + | "timeit" + | "tkinter" + | "token" + | "tokenize" + | "trace" + | "traceback" + | "tracemalloc" + | "tty" + | "turtle" + | "turtledemo" + | "types" + | "typing" + | "unicodedata" + | "unittest" + | "urllib" + | "uu" + | "uuid" + | "venv" + | "warnings" + | "wave" + | "weakref" + | "webbrowser" + | "winreg" + | "winsound" + | "wsgiref" + | "xdrlib" + | "xml" + | "xmlrpc" + | "zipapp" + | "zipfile" + | "zipimport" + | "zlib" + ) | ( + 7, + "_dummy_thread" + | "asynchat" + | "asyncore" + | "binhex" + | "distutils" + | "dummy_threading" + | "formatter" + | "imp" + | "macpath" + | "parser" + | "smtpd" + | "symbol" + ) | ( + 8, + "_dummy_thread" + | "asynchat" + | "asyncore" + | "binhex" + | "distutils" + | "dummy_threading" + | "formatter" + | "imp" + | "parser" + | "smtpd" + | "symbol" + ) | ( + 9, + "asynchat" + | "asyncore" + | "binhex" + | "distutils" + | "formatter" + | "graphlib" + | "imp" + | "parser" + | "smtpd" + | "symbol" + | "zoneinfo" + ) | ( + 10, + "asynchat" + | "asyncore" + | "binhex" + | "distutils" + | "graphlib" + | "idlelib" + | "imp" + | "smtpd" + | "zoneinfo" + ) | ( + 11, + "asynchat" + | "asyncore" + | "distutils" + | "graphlib" + | "idlelib" + | "imp" + | "smtpd" + | "tomllib" + | "zoneinfo" + ) | (12, "graphlib" | "idlelib" | "tomllib" | "zoneinfo") + ) +} diff --git a/scripts/generate_known_standard_library.py b/scripts/generate_known_standard_library.py index 5d9c66c58b..f35a714d0e 100644 --- a/scripts/generate_known_standard_library.py +++ b/scripts/generate_known_standard_library.py @@ -12,13 +12,14 @@ from pathlib import Path from sphinx.ext.intersphinx import fetch_inventory URL = "https://docs.python.org/{}/objects.inv" -PATH = Path("crates") / "ruff_python" / "src" / "sys.rs" +PATH = Path("crates") / "ruff_python_stdlib" / "src" / "sys.rs" VERSIONS: list[tuple[int, int]] = [ (3, 7), (3, 8), (3, 9), (3, 10), (3, 11), + (3, 12), ] @@ -37,18 +38,16 @@ with PATH.open("w") as f: f.write( """\ //! This file is generated by `scripts/generate_known_standard_library.py` -use once_cell::sync::Lazy; -use rustc_hash::{FxHashMap, FxHashSet}; -// See: https://pycqa.github.io/isort/docs/configuration/options.html#known-standard-library -pub static KNOWN_STANDARD_LIBRARY: Lazy>> = - Lazy::new(|| { - FxHashMap::from_iter([ -""", # noqa: E501 +pub fn is_known_standard_library(minor_version: u32, module: &str) -> bool { + matches!((minor_version, module), +""", ) - for major, minor in VERSIONS: - version = f"{major}.{minor}" - url = URL.format(version) + + modules_by_version = {} + + for major_version, minor_version in VERSIONS: + url = URL.format(f"{major_version}.{minor_version}") invdata = fetch_inventory(FakeApp(), "", url) modules = { @@ -60,33 +59,44 @@ pub static KNOWN_STANDARD_LIBRARY: Lazy 0: + f.write(" | ") + f.write(f'"{module}"') + f.write(")") + f.write("\n") + + # Next, add any version-specific modules. + for _major_version, minor_version in VERSIONS: + version_modules = set.difference( + modules_by_version[minor_version], + ubiquitous_modules, ) + + f.write(" | ") + f.write(f"({minor_version}, ") + for i, module in enumerate(sorted(version_modules)): + if i > 0: + f.write(" | ") + f.write(f'"{module}"') + f.write(")") + f.write("\n") + f.write( """\ - ]) - }); -""", + ) +} + """, ) From 6ba9d5d5a4d28e9239eb6541656c38878d0759dd Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 23 Jun 2023 22:39:47 +0200 Subject: [PATCH 216/447] Upgrade RustPython (#5334) --- Cargo.lock | 12 +- Cargo.toml | 19 +- .../pylint/rules/compare_to_empty_string.rs | 2 +- crates/ruff_python_ast/src/node.rs | 462 +++++++++--------- .../src/source_code/generator.rs | 41 +- 5 files changed, 265 insertions(+), 271 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2453977a0a..2dd4861dd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2102,7 +2102,7 @@ dependencies = [ [[package]] name = "ruff_text_size" version = "0.0.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=f60e204b73b95bdb6ce87ccd0de34081b4a17c11#f60e204b73b95bdb6ce87ccd0de34081b4a17c11" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8078663b6c914c1cb86993e427764f7c422fc12c#8078663b6c914c1cb86993e427764f7c422fc12c" dependencies = [ "schemars", "serde", @@ -2180,7 +2180,7 @@ dependencies = [ [[package]] name = "rustpython-ast" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=f60e204b73b95bdb6ce87ccd0de34081b4a17c11#f60e204b73b95bdb6ce87ccd0de34081b4a17c11" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8078663b6c914c1cb86993e427764f7c422fc12c#8078663b6c914c1cb86993e427764f7c422fc12c" dependencies = [ "is-macro", "num-bigint", @@ -2191,7 +2191,7 @@ dependencies = [ [[package]] name = "rustpython-format" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=f60e204b73b95bdb6ce87ccd0de34081b4a17c11#f60e204b73b95bdb6ce87ccd0de34081b4a17c11" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8078663b6c914c1cb86993e427764f7c422fc12c#8078663b6c914c1cb86993e427764f7c422fc12c" dependencies = [ "bitflags 2.3.1", "itertools", @@ -2203,7 +2203,7 @@ dependencies = [ [[package]] name = "rustpython-literal" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=f60e204b73b95bdb6ce87ccd0de34081b4a17c11#f60e204b73b95bdb6ce87ccd0de34081b4a17c11" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8078663b6c914c1cb86993e427764f7c422fc12c#8078663b6c914c1cb86993e427764f7c422fc12c" dependencies = [ "hexf-parse", "is-macro", @@ -2215,7 +2215,7 @@ dependencies = [ [[package]] name = "rustpython-parser" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=f60e204b73b95bdb6ce87ccd0de34081b4a17c11#f60e204b73b95bdb6ce87ccd0de34081b4a17c11" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8078663b6c914c1cb86993e427764f7c422fc12c#8078663b6c914c1cb86993e427764f7c422fc12c" dependencies = [ "anyhow", "is-macro", @@ -2238,7 +2238,7 @@ dependencies = [ [[package]] name = "rustpython-parser-core" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=f60e204b73b95bdb6ce87ccd0de34081b4a17c11#f60e204b73b95bdb6ce87ccd0de34081b4a17c11" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8078663b6c914c1cb86993e427764f7c422fc12c#8078663b6c914c1cb86993e427764f7c422fc12c" dependencies = [ "is-macro", "memchr", diff --git a/Cargo.toml b/Cargo.toml index c4d9205169..4066cb9c1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,16 +49,15 @@ toml = { version = "0.7.2" } # v0.0.1 libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "80e4c1399f95e5beb532fdd1e209ad2dbb470438" } -# v0.0.3 -ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "f60e204b73b95bdb6ce87ccd0de34081b4a17c11" } -# v0.0.3 -rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "f60e204b73b95bdb6ce87ccd0de34081b4a17c11" , default-features = false, features = ["all-nodes-with-ranges", "num-bigint"]} -# v0.0.3 -rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "f60e204b73b95bdb6ce87ccd0de34081b4a17c11", default-features = false, features = ["num-bigint"] } -# v0.0.3 -rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "f60e204b73b95bdb6ce87ccd0de34081b4a17c11", default-features = false } -# v0.0.3 -rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "f60e204b73b95bdb6ce87ccd0de34081b4a17c11" , default-features = false, features = ["full-lexer", "all-nodes-with-ranges", "num-bigint"] } + +# Please tag the RustPython version everytime you update its revision here. +# Tagging the version ensures that older ruff versions continue to build from source even when we rebase our RustPython fork. +# Current tag: v0.0.6 +ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8078663b6c914c1cb86993e427764f7c422fc12c" } +rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8078663b6c914c1cb86993e427764f7c422fc12c" , default-features = false, features = ["num-bigint"]} +rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8078663b6c914c1cb86993e427764f7c422fc12c", default-features = false, features = ["num-bigint"] } +rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8078663b6c914c1cb86993e427764f7c422fc12c", default-features = false } +rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8078663b6c914c1cb86993e427764f7c422fc12c" , default-features = false, features = ["full-lexer", "num-bigint"] } [profile.release] lto = "fat" diff --git a/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs b/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs index 078feea363..3e2b96e0a4 100644 --- a/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs +++ b/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs @@ -68,7 +68,7 @@ pub(crate) fn compare_to_empty_string( let mut first = true; for ((lhs, rhs), op) in std::iter::once(left) .chain(comparators.iter()) - .tuple_windows::<(&Expr<_>, &Expr<_>)>() + .tuple_windows::<(&Expr, &Expr)>() .zip(ops) { if let Ok(op) = EmptyStringCmpOp::try_from(op) { diff --git a/crates/ruff_python_ast/src/node.rs b/crates/ruff_python_ast/src/node.rs index 9928fa5d2a..c5b50e71eb 100644 --- a/crates/ruff_python_ast/src/node.rs +++ b/crates/ruff_python_ast/src/node.rs @@ -17,83 +17,83 @@ pub trait AstNode: Ranged { #[derive(Clone, Debug, is_macro::Is, PartialEq)] pub enum AnyNode { - ModModule(ModModule), - ModInteractive(ModInteractive), - ModExpression(ModExpression), - ModFunctionType(ModFunctionType), - StmtFunctionDef(StmtFunctionDef), - StmtAsyncFunctionDef(StmtAsyncFunctionDef), - StmtClassDef(StmtClassDef), - StmtReturn(StmtReturn), - StmtDelete(StmtDelete), - StmtAssign(StmtAssign), - StmtAugAssign(StmtAugAssign), - StmtAnnAssign(StmtAnnAssign), - StmtFor(StmtFor), - StmtAsyncFor(StmtAsyncFor), - StmtWhile(StmtWhile), - StmtIf(StmtIf), - StmtWith(StmtWith), - StmtAsyncWith(StmtAsyncWith), - StmtMatch(StmtMatch), - StmtRaise(StmtRaise), - StmtTry(StmtTry), - StmtTryStar(StmtTryStar), - StmtAssert(StmtAssert), - StmtImport(StmtImport), - StmtImportFrom(StmtImportFrom), - StmtGlobal(StmtGlobal), - StmtNonlocal(StmtNonlocal), - StmtExpr(StmtExpr), - StmtPass(StmtPass), - StmtBreak(StmtBreak), - StmtContinue(StmtContinue), - ExprBoolOp(ExprBoolOp), - ExprNamedExpr(ExprNamedExpr), - ExprBinOp(ExprBinOp), - ExprUnaryOp(ExprUnaryOp), - ExprLambda(ExprLambda), - ExprIfExp(ExprIfExp), - ExprDict(ExprDict), - ExprSet(ExprSet), - ExprListComp(ExprListComp), - ExprSetComp(ExprSetComp), - ExprDictComp(ExprDictComp), - ExprGeneratorExp(ExprGeneratorExp), - ExprAwait(ExprAwait), - ExprYield(ExprYield), - ExprYieldFrom(ExprYieldFrom), - ExprCompare(ExprCompare), - ExprCall(ExprCall), - ExprFormattedValue(ExprFormattedValue), - ExprJoinedStr(ExprJoinedStr), - ExprConstant(ExprConstant), - ExprAttribute(ExprAttribute), - ExprSubscript(ExprSubscript), - ExprStarred(ExprStarred), - ExprName(ExprName), - ExprList(ExprList), - ExprTuple(ExprTuple), - ExprSlice(ExprSlice), - ExceptHandlerExceptHandler(ExceptHandlerExceptHandler), - PatternMatchValue(PatternMatchValue), - PatternMatchSingleton(PatternMatchSingleton), - PatternMatchSequence(PatternMatchSequence), - PatternMatchMapping(PatternMatchMapping), - PatternMatchClass(PatternMatchClass), - PatternMatchStar(PatternMatchStar), - PatternMatchAs(PatternMatchAs), - PatternMatchOr(PatternMatchOr), - TypeIgnoreTypeIgnore(TypeIgnoreTypeIgnore), - Comprehension(Comprehension), - Arguments(Arguments), - Arg(Arg), - ArgWithDefault(ArgWithDefault), - Keyword(Keyword), - Alias(Alias), - WithItem(WithItem), - MatchCase(MatchCase), - Decorator(Decorator), + ModModule(ModModule), + ModInteractive(ModInteractive), + ModExpression(ModExpression), + ModFunctionType(ModFunctionType), + StmtFunctionDef(StmtFunctionDef), + StmtAsyncFunctionDef(StmtAsyncFunctionDef), + StmtClassDef(StmtClassDef), + StmtReturn(StmtReturn), + StmtDelete(StmtDelete), + StmtAssign(StmtAssign), + StmtAugAssign(StmtAugAssign), + StmtAnnAssign(StmtAnnAssign), + StmtFor(StmtFor), + StmtAsyncFor(StmtAsyncFor), + StmtWhile(StmtWhile), + StmtIf(StmtIf), + StmtWith(StmtWith), + StmtAsyncWith(StmtAsyncWith), + StmtMatch(StmtMatch), + StmtRaise(StmtRaise), + StmtTry(StmtTry), + StmtTryStar(StmtTryStar), + StmtAssert(StmtAssert), + StmtImport(StmtImport), + StmtImportFrom(StmtImportFrom), + StmtGlobal(StmtGlobal), + StmtNonlocal(StmtNonlocal), + StmtExpr(StmtExpr), + StmtPass(StmtPass), + StmtBreak(StmtBreak), + StmtContinue(StmtContinue), + ExprBoolOp(ExprBoolOp), + ExprNamedExpr(ExprNamedExpr), + ExprBinOp(ExprBinOp), + ExprUnaryOp(ExprUnaryOp), + ExprLambda(ExprLambda), + ExprIfExp(ExprIfExp), + ExprDict(ExprDict), + ExprSet(ExprSet), + ExprListComp(ExprListComp), + ExprSetComp(ExprSetComp), + ExprDictComp(ExprDictComp), + ExprGeneratorExp(ExprGeneratorExp), + ExprAwait(ExprAwait), + ExprYield(ExprYield), + ExprYieldFrom(ExprYieldFrom), + ExprCompare(ExprCompare), + ExprCall(ExprCall), + ExprFormattedValue(ExprFormattedValue), + ExprJoinedStr(ExprJoinedStr), + ExprConstant(ExprConstant), + ExprAttribute(ExprAttribute), + ExprSubscript(ExprSubscript), + ExprStarred(ExprStarred), + ExprName(ExprName), + ExprList(ExprList), + ExprTuple(ExprTuple), + ExprSlice(ExprSlice), + ExceptHandlerExceptHandler(ExceptHandlerExceptHandler), + PatternMatchValue(PatternMatchValue), + PatternMatchSingleton(PatternMatchSingleton), + PatternMatchSequence(PatternMatchSequence), + PatternMatchMapping(PatternMatchMapping), + PatternMatchClass(PatternMatchClass), + PatternMatchStar(PatternMatchStar), + PatternMatchAs(PatternMatchAs), + PatternMatchOr(PatternMatchOr), + TypeIgnoreTypeIgnore(TypeIgnoreTypeIgnore), + Comprehension(Comprehension), + Arguments(Arguments), + Arg(Arg), + ArgWithDefault(ArgWithDefault), + Keyword(Keyword), + Alias(Alias), + WithItem(WithItem), + MatchCase(MatchCase), + Decorator(Decorator), } impl AnyNode { @@ -707,7 +707,7 @@ impl AnyNode { } } -impl AstNode for ModModule { +impl AstNode for ModModule { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -735,7 +735,7 @@ impl AstNode for ModModule { AnyNode::from(self) } } -impl AstNode for ModInteractive { +impl AstNode for ModInteractive { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -763,7 +763,7 @@ impl AstNode for ModInteractive { AnyNode::from(self) } } -impl AstNode for ModExpression { +impl AstNode for ModExpression { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -791,7 +791,7 @@ impl AstNode for ModExpression { AnyNode::from(self) } } -impl AstNode for ModFunctionType { +impl AstNode for ModFunctionType { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -819,7 +819,7 @@ impl AstNode for ModFunctionType { AnyNode::from(self) } } -impl AstNode for StmtFunctionDef { +impl AstNode for StmtFunctionDef { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -847,7 +847,7 @@ impl AstNode for StmtFunctionDef { AnyNode::from(self) } } -impl AstNode for StmtAsyncFunctionDef { +impl AstNode for StmtAsyncFunctionDef { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -875,7 +875,7 @@ impl AstNode for StmtAsyncFunctionDef { AnyNode::from(self) } } -impl AstNode for StmtClassDef { +impl AstNode for StmtClassDef { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -903,7 +903,7 @@ impl AstNode for StmtClassDef { AnyNode::from(self) } } -impl AstNode for StmtReturn { +impl AstNode for StmtReturn { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -931,7 +931,7 @@ impl AstNode for StmtReturn { AnyNode::from(self) } } -impl AstNode for StmtDelete { +impl AstNode for StmtDelete { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -959,7 +959,7 @@ impl AstNode for StmtDelete { AnyNode::from(self) } } -impl AstNode for StmtAssign { +impl AstNode for StmtAssign { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -987,7 +987,7 @@ impl AstNode for StmtAssign { AnyNode::from(self) } } -impl AstNode for StmtAugAssign { +impl AstNode for StmtAugAssign { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1015,7 +1015,7 @@ impl AstNode for StmtAugAssign { AnyNode::from(self) } } -impl AstNode for StmtAnnAssign { +impl AstNode for StmtAnnAssign { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1043,7 +1043,7 @@ impl AstNode for StmtAnnAssign { AnyNode::from(self) } } -impl AstNode for StmtFor { +impl AstNode for StmtFor { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1071,7 +1071,7 @@ impl AstNode for StmtFor { AnyNode::from(self) } } -impl AstNode for StmtAsyncFor { +impl AstNode for StmtAsyncFor { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1099,7 +1099,7 @@ impl AstNode for StmtAsyncFor { AnyNode::from(self) } } -impl AstNode for StmtWhile { +impl AstNode for StmtWhile { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1127,7 +1127,7 @@ impl AstNode for StmtWhile { AnyNode::from(self) } } -impl AstNode for StmtIf { +impl AstNode for StmtIf { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1155,7 +1155,7 @@ impl AstNode for StmtIf { AnyNode::from(self) } } -impl AstNode for StmtWith { +impl AstNode for StmtWith { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1183,7 +1183,7 @@ impl AstNode for StmtWith { AnyNode::from(self) } } -impl AstNode for StmtAsyncWith { +impl AstNode for StmtAsyncWith { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1211,7 +1211,7 @@ impl AstNode for StmtAsyncWith { AnyNode::from(self) } } -impl AstNode for StmtMatch { +impl AstNode for StmtMatch { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1239,7 +1239,7 @@ impl AstNode for StmtMatch { AnyNode::from(self) } } -impl AstNode for StmtRaise { +impl AstNode for StmtRaise { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1267,7 +1267,7 @@ impl AstNode for StmtRaise { AnyNode::from(self) } } -impl AstNode for StmtTry { +impl AstNode for StmtTry { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1295,7 +1295,7 @@ impl AstNode for StmtTry { AnyNode::from(self) } } -impl AstNode for StmtTryStar { +impl AstNode for StmtTryStar { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1323,7 +1323,7 @@ impl AstNode for StmtTryStar { AnyNode::from(self) } } -impl AstNode for StmtAssert { +impl AstNode for StmtAssert { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1351,7 +1351,7 @@ impl AstNode for StmtAssert { AnyNode::from(self) } } -impl AstNode for StmtImport { +impl AstNode for StmtImport { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1379,7 +1379,7 @@ impl AstNode for StmtImport { AnyNode::from(self) } } -impl AstNode for StmtImportFrom { +impl AstNode for StmtImportFrom { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1407,7 +1407,7 @@ impl AstNode for StmtImportFrom { AnyNode::from(self) } } -impl AstNode for StmtGlobal { +impl AstNode for StmtGlobal { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1435,7 +1435,7 @@ impl AstNode for StmtGlobal { AnyNode::from(self) } } -impl AstNode for StmtNonlocal { +impl AstNode for StmtNonlocal { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1463,7 +1463,7 @@ impl AstNode for StmtNonlocal { AnyNode::from(self) } } -impl AstNode for StmtExpr { +impl AstNode for StmtExpr { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1491,7 +1491,7 @@ impl AstNode for StmtExpr { AnyNode::from(self) } } -impl AstNode for StmtPass { +impl AstNode for StmtPass { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1519,7 +1519,7 @@ impl AstNode for StmtPass { AnyNode::from(self) } } -impl AstNode for StmtBreak { +impl AstNode for StmtBreak { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1547,7 +1547,7 @@ impl AstNode for StmtBreak { AnyNode::from(self) } } -impl AstNode for StmtContinue { +impl AstNode for StmtContinue { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1575,7 +1575,7 @@ impl AstNode for StmtContinue { AnyNode::from(self) } } -impl AstNode for ExprBoolOp { +impl AstNode for ExprBoolOp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1603,7 +1603,7 @@ impl AstNode for ExprBoolOp { AnyNode::from(self) } } -impl AstNode for ExprNamedExpr { +impl AstNode for ExprNamedExpr { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1631,7 +1631,7 @@ impl AstNode for ExprNamedExpr { AnyNode::from(self) } } -impl AstNode for ExprBinOp { +impl AstNode for ExprBinOp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1659,7 +1659,7 @@ impl AstNode for ExprBinOp { AnyNode::from(self) } } -impl AstNode for ExprUnaryOp { +impl AstNode for ExprUnaryOp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1687,7 +1687,7 @@ impl AstNode for ExprUnaryOp { AnyNode::from(self) } } -impl AstNode for ExprLambda { +impl AstNode for ExprLambda { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1715,7 +1715,7 @@ impl AstNode for ExprLambda { AnyNode::from(self) } } -impl AstNode for ExprIfExp { +impl AstNode for ExprIfExp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1743,7 +1743,7 @@ impl AstNode for ExprIfExp { AnyNode::from(self) } } -impl AstNode for ExprDict { +impl AstNode for ExprDict { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1771,7 +1771,7 @@ impl AstNode for ExprDict { AnyNode::from(self) } } -impl AstNode for ExprSet { +impl AstNode for ExprSet { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1799,7 +1799,7 @@ impl AstNode for ExprSet { AnyNode::from(self) } } -impl AstNode for ExprListComp { +impl AstNode for ExprListComp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1827,7 +1827,7 @@ impl AstNode for ExprListComp { AnyNode::from(self) } } -impl AstNode for ExprSetComp { +impl AstNode for ExprSetComp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1855,7 +1855,7 @@ impl AstNode for ExprSetComp { AnyNode::from(self) } } -impl AstNode for ExprDictComp { +impl AstNode for ExprDictComp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1883,7 +1883,7 @@ impl AstNode for ExprDictComp { AnyNode::from(self) } } -impl AstNode for ExprGeneratorExp { +impl AstNode for ExprGeneratorExp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1911,7 +1911,7 @@ impl AstNode for ExprGeneratorExp { AnyNode::from(self) } } -impl AstNode for ExprAwait { +impl AstNode for ExprAwait { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1939,7 +1939,7 @@ impl AstNode for ExprAwait { AnyNode::from(self) } } -impl AstNode for ExprYield { +impl AstNode for ExprYield { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1967,7 +1967,7 @@ impl AstNode for ExprYield { AnyNode::from(self) } } -impl AstNode for ExprYieldFrom { +impl AstNode for ExprYieldFrom { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1995,7 +1995,7 @@ impl AstNode for ExprYieldFrom { AnyNode::from(self) } } -impl AstNode for ExprCompare { +impl AstNode for ExprCompare { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2023,7 +2023,7 @@ impl AstNode for ExprCompare { AnyNode::from(self) } } -impl AstNode for ExprCall { +impl AstNode for ExprCall { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2051,7 +2051,7 @@ impl AstNode for ExprCall { AnyNode::from(self) } } -impl AstNode for ExprFormattedValue { +impl AstNode for ExprFormattedValue { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2079,7 +2079,7 @@ impl AstNode for ExprFormattedValue { AnyNode::from(self) } } -impl AstNode for ExprJoinedStr { +impl AstNode for ExprJoinedStr { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2107,7 +2107,7 @@ impl AstNode for ExprJoinedStr { AnyNode::from(self) } } -impl AstNode for ExprConstant { +impl AstNode for ExprConstant { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2135,7 +2135,7 @@ impl AstNode for ExprConstant { AnyNode::from(self) } } -impl AstNode for ExprAttribute { +impl AstNode for ExprAttribute { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2163,7 +2163,7 @@ impl AstNode for ExprAttribute { AnyNode::from(self) } } -impl AstNode for ExprSubscript { +impl AstNode for ExprSubscript { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2191,7 +2191,7 @@ impl AstNode for ExprSubscript { AnyNode::from(self) } } -impl AstNode for ExprStarred { +impl AstNode for ExprStarred { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2219,7 +2219,7 @@ impl AstNode for ExprStarred { AnyNode::from(self) } } -impl AstNode for ExprName { +impl AstNode for ExprName { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2247,7 +2247,7 @@ impl AstNode for ExprName { AnyNode::from(self) } } -impl AstNode for ExprList { +impl AstNode for ExprList { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2275,7 +2275,7 @@ impl AstNode for ExprList { AnyNode::from(self) } } -impl AstNode for ExprTuple { +impl AstNode for ExprTuple { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2303,7 +2303,7 @@ impl AstNode for ExprTuple { AnyNode::from(self) } } -impl AstNode for ExprSlice { +impl AstNode for ExprSlice { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2331,7 +2331,7 @@ impl AstNode for ExprSlice { AnyNode::from(self) } } -impl AstNode for ExceptHandlerExceptHandler { +impl AstNode for ExceptHandlerExceptHandler { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2359,7 +2359,7 @@ impl AstNode for ExceptHandlerExceptHandler { AnyNode::from(self) } } -impl AstNode for PatternMatchValue { +impl AstNode for PatternMatchValue { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2387,7 +2387,7 @@ impl AstNode for PatternMatchValue { AnyNode::from(self) } } -impl AstNode for PatternMatchSingleton { +impl AstNode for PatternMatchSingleton { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2415,7 +2415,7 @@ impl AstNode for PatternMatchSingleton { AnyNode::from(self) } } -impl AstNode for PatternMatchSequence { +impl AstNode for PatternMatchSequence { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2443,7 +2443,7 @@ impl AstNode for PatternMatchSequence { AnyNode::from(self) } } -impl AstNode for PatternMatchMapping { +impl AstNode for PatternMatchMapping { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2471,7 +2471,7 @@ impl AstNode for PatternMatchMapping { AnyNode::from(self) } } -impl AstNode for PatternMatchClass { +impl AstNode for PatternMatchClass { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2499,7 +2499,7 @@ impl AstNode for PatternMatchClass { AnyNode::from(self) } } -impl AstNode for PatternMatchStar { +impl AstNode for PatternMatchStar { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2527,7 +2527,7 @@ impl AstNode for PatternMatchStar { AnyNode::from(self) } } -impl AstNode for PatternMatchAs { +impl AstNode for PatternMatchAs { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2555,7 +2555,7 @@ impl AstNode for PatternMatchAs { AnyNode::from(self) } } -impl AstNode for PatternMatchOr { +impl AstNode for PatternMatchOr { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2583,7 +2583,7 @@ impl AstNode for PatternMatchOr { AnyNode::from(self) } } -impl AstNode for TypeIgnoreTypeIgnore { +impl AstNode for TypeIgnoreTypeIgnore { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2612,7 +2612,7 @@ impl AstNode for TypeIgnoreTypeIgnore { } } -impl AstNode for Comprehension { +impl AstNode for Comprehension { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2640,7 +2640,7 @@ impl AstNode for Comprehension { AnyNode::from(self) } } -impl AstNode for Arguments { +impl AstNode for Arguments { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2668,7 +2668,7 @@ impl AstNode for Arguments { AnyNode::from(self) } } -impl AstNode for Arg { +impl AstNode for Arg { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2696,7 +2696,7 @@ impl AstNode for Arg { AnyNode::from(self) } } -impl AstNode for ArgWithDefault { +impl AstNode for ArgWithDefault { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2724,7 +2724,7 @@ impl AstNode for ArgWithDefault { AnyNode::from(self) } } -impl AstNode for Keyword { +impl AstNode for Keyword { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2752,7 +2752,7 @@ impl AstNode for Keyword { AnyNode::from(self) } } -impl AstNode for Alias { +impl AstNode for Alias { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2780,7 +2780,7 @@ impl AstNode for Alias { AnyNode::from(self) } } -impl AstNode for WithItem { +impl AstNode for WithItem { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2808,7 +2808,7 @@ impl AstNode for WithItem { AnyNode::from(self) } } -impl AstNode for MatchCase { +impl AstNode for MatchCase { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2837,7 +2837,7 @@ impl AstNode for MatchCase { } } -impl AstNode for Decorator { +impl AstNode for Decorator { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -3516,83 +3516,83 @@ impl Ranged for AnyNode { #[derive(Copy, Clone, Debug, is_macro::Is, PartialEq)] pub enum AnyNodeRef<'a> { - ModModule(&'a ModModule), - ModInteractive(&'a ModInteractive), - ModExpression(&'a ModExpression), - ModFunctionType(&'a ModFunctionType), - StmtFunctionDef(&'a StmtFunctionDef), - StmtAsyncFunctionDef(&'a StmtAsyncFunctionDef), - StmtClassDef(&'a StmtClassDef), - StmtReturn(&'a StmtReturn), - StmtDelete(&'a StmtDelete), - StmtAssign(&'a StmtAssign), - StmtAugAssign(&'a StmtAugAssign), - StmtAnnAssign(&'a StmtAnnAssign), - StmtFor(&'a StmtFor), - StmtAsyncFor(&'a StmtAsyncFor), - StmtWhile(&'a StmtWhile), - StmtIf(&'a StmtIf), - StmtWith(&'a StmtWith), - StmtAsyncWith(&'a StmtAsyncWith), - StmtMatch(&'a StmtMatch), - StmtRaise(&'a StmtRaise), - StmtTry(&'a StmtTry), - StmtTryStar(&'a StmtTryStar), - StmtAssert(&'a StmtAssert), - StmtImport(&'a StmtImport), - StmtImportFrom(&'a StmtImportFrom), - StmtGlobal(&'a StmtGlobal), - StmtNonlocal(&'a StmtNonlocal), - StmtExpr(&'a StmtExpr), - StmtPass(&'a StmtPass), - StmtBreak(&'a StmtBreak), - StmtContinue(&'a StmtContinue), - ExprBoolOp(&'a ExprBoolOp), - ExprNamedExpr(&'a ExprNamedExpr), - ExprBinOp(&'a ExprBinOp), - ExprUnaryOp(&'a ExprUnaryOp), - ExprLambda(&'a ExprLambda), - ExprIfExp(&'a ExprIfExp), - ExprDict(&'a ExprDict), - ExprSet(&'a ExprSet), - ExprListComp(&'a ExprListComp), - ExprSetComp(&'a ExprSetComp), - ExprDictComp(&'a ExprDictComp), - ExprGeneratorExp(&'a ExprGeneratorExp), - ExprAwait(&'a ExprAwait), - ExprYield(&'a ExprYield), - ExprYieldFrom(&'a ExprYieldFrom), - ExprCompare(&'a ExprCompare), - ExprCall(&'a ExprCall), - ExprFormattedValue(&'a ExprFormattedValue), - ExprJoinedStr(&'a ExprJoinedStr), - ExprConstant(&'a ExprConstant), - ExprAttribute(&'a ExprAttribute), - ExprSubscript(&'a ExprSubscript), - ExprStarred(&'a ExprStarred), - ExprName(&'a ExprName), - ExprList(&'a ExprList), - ExprTuple(&'a ExprTuple), - ExprSlice(&'a ExprSlice), - ExceptHandlerExceptHandler(&'a ExceptHandlerExceptHandler), - PatternMatchValue(&'a PatternMatchValue), - PatternMatchSingleton(&'a PatternMatchSingleton), - PatternMatchSequence(&'a PatternMatchSequence), - PatternMatchMapping(&'a PatternMatchMapping), - PatternMatchClass(&'a PatternMatchClass), - PatternMatchStar(&'a PatternMatchStar), - PatternMatchAs(&'a PatternMatchAs), - PatternMatchOr(&'a PatternMatchOr), - TypeIgnoreTypeIgnore(&'a TypeIgnoreTypeIgnore), - Comprehension(&'a Comprehension), - Arguments(&'a Arguments), - Arg(&'a Arg), - ArgWithDefault(&'a ArgWithDefault), - Keyword(&'a Keyword), - Alias(&'a Alias), - WithItem(&'a WithItem), - MatchCase(&'a MatchCase), - Decorator(&'a Decorator), + ModModule(&'a ModModule), + ModInteractive(&'a ModInteractive), + ModExpression(&'a ModExpression), + ModFunctionType(&'a ModFunctionType), + StmtFunctionDef(&'a StmtFunctionDef), + StmtAsyncFunctionDef(&'a StmtAsyncFunctionDef), + StmtClassDef(&'a StmtClassDef), + StmtReturn(&'a StmtReturn), + StmtDelete(&'a StmtDelete), + StmtAssign(&'a StmtAssign), + StmtAugAssign(&'a StmtAugAssign), + StmtAnnAssign(&'a StmtAnnAssign), + StmtFor(&'a StmtFor), + StmtAsyncFor(&'a StmtAsyncFor), + StmtWhile(&'a StmtWhile), + StmtIf(&'a StmtIf), + StmtWith(&'a StmtWith), + StmtAsyncWith(&'a StmtAsyncWith), + StmtMatch(&'a StmtMatch), + StmtRaise(&'a StmtRaise), + StmtTry(&'a StmtTry), + StmtTryStar(&'a StmtTryStar), + StmtAssert(&'a StmtAssert), + StmtImport(&'a StmtImport), + StmtImportFrom(&'a StmtImportFrom), + StmtGlobal(&'a StmtGlobal), + StmtNonlocal(&'a StmtNonlocal), + StmtExpr(&'a StmtExpr), + StmtPass(&'a StmtPass), + StmtBreak(&'a StmtBreak), + StmtContinue(&'a StmtContinue), + ExprBoolOp(&'a ExprBoolOp), + ExprNamedExpr(&'a ExprNamedExpr), + ExprBinOp(&'a ExprBinOp), + ExprUnaryOp(&'a ExprUnaryOp), + ExprLambda(&'a ExprLambda), + ExprIfExp(&'a ExprIfExp), + ExprDict(&'a ExprDict), + ExprSet(&'a ExprSet), + ExprListComp(&'a ExprListComp), + ExprSetComp(&'a ExprSetComp), + ExprDictComp(&'a ExprDictComp), + ExprGeneratorExp(&'a ExprGeneratorExp), + ExprAwait(&'a ExprAwait), + ExprYield(&'a ExprYield), + ExprYieldFrom(&'a ExprYieldFrom), + ExprCompare(&'a ExprCompare), + ExprCall(&'a ExprCall), + ExprFormattedValue(&'a ExprFormattedValue), + ExprJoinedStr(&'a ExprJoinedStr), + ExprConstant(&'a ExprConstant), + ExprAttribute(&'a ExprAttribute), + ExprSubscript(&'a ExprSubscript), + ExprStarred(&'a ExprStarred), + ExprName(&'a ExprName), + ExprList(&'a ExprList), + ExprTuple(&'a ExprTuple), + ExprSlice(&'a ExprSlice), + ExceptHandlerExceptHandler(&'a ExceptHandlerExceptHandler), + PatternMatchValue(&'a PatternMatchValue), + PatternMatchSingleton(&'a PatternMatchSingleton), + PatternMatchSequence(&'a PatternMatchSequence), + PatternMatchMapping(&'a PatternMatchMapping), + PatternMatchClass(&'a PatternMatchClass), + PatternMatchStar(&'a PatternMatchStar), + PatternMatchAs(&'a PatternMatchAs), + PatternMatchOr(&'a PatternMatchOr), + TypeIgnoreTypeIgnore(&'a TypeIgnoreTypeIgnore), + Comprehension(&'a Comprehension), + Arguments(&'a Arguments), + Arg(&'a Arg), + ArgWithDefault(&'a ArgWithDefault), + Keyword(&'a Keyword), + Alias(&'a Alias), + WithItem(&'a WithItem), + MatchCase(&'a MatchCase), + Decorator(&'a Decorator), } impl AnyNodeRef<'_> { diff --git a/crates/ruff_python_ast/src/source_code/generator.rs b/crates/ruff_python_ast/src/source_code/generator.rs index 0b3ec2f5d5..e406122e2e 100644 --- a/crates/ruff_python_ast/src/source_code/generator.rs +++ b/crates/ruff_python_ast/src/source_code/generator.rs @@ -132,7 +132,7 @@ impl<'a> Generator<'a> { } } - fn body(&mut self, stmts: &[Stmt]) { + fn body(&mut self, stmts: &[Stmt]) { self.indent_depth = self.indent_depth.saturating_add(1); for stmt in stmts { self.unparse_stmt(stmt); @@ -184,13 +184,13 @@ impl<'a> Generator<'a> { self.buffer } - pub fn unparse_suite(&mut self, suite: &Suite) { + pub fn unparse_suite(&mut self, suite: &Suite) { for stmt in suite { self.unparse_stmt(stmt); } } - pub(crate) fn unparse_stmt(&mut self, ast: &Stmt) { + pub(crate) fn unparse_stmt(&mut self, ast: &Stmt) { macro_rules! statement { ($body:block) => {{ self.newline(); @@ -467,7 +467,7 @@ impl<'a> Generator<'a> { }); self.body(body); - let mut orelse_: &[Stmt] = orelse; + let mut orelse_: &[Stmt] = orelse; loop { if orelse_.len() == 1 && matches!(orelse_[0], Stmt::If(_)) { if let Stmt::If(ast::StmtIf { @@ -718,7 +718,7 @@ impl<'a> Generator<'a> { } } - fn unparse_except_handler(&mut self, ast: &ExceptHandler, star: bool) { + fn unparse_except_handler(&mut self, ast: &ExceptHandler, star: bool) { match ast { ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, @@ -744,7 +744,7 @@ impl<'a> Generator<'a> { } } - fn unparse_pattern(&mut self, ast: &Pattern) { + fn unparse_pattern(&mut self, ast: &Pattern) { match ast { Pattern::MatchValue(ast::PatternMatchValue { value, @@ -831,7 +831,7 @@ impl<'a> Generator<'a> { } } - fn unparse_match_case(&mut self, ast: &MatchCase) { + fn unparse_match_case(&mut self, ast: &MatchCase) { self.p("case "); self.unparse_pattern(&ast.pattern); if let Some(guard) = &ast.guard { @@ -842,7 +842,7 @@ impl<'a> Generator<'a> { self.body(&ast.body); } - pub(crate) fn unparse_expr(&mut self, ast: &Expr, level: u8) { + pub(crate) fn unparse_expr(&mut self, ast: &Expr, level: u8) { macro_rules! opprec { ($opty:ident, $x:expr, $enu:path, $($var:ident($op:literal, $prec:ident)),*$(,)?) => { match $x { @@ -1289,7 +1289,7 @@ impl<'a> Generator<'a> { } } - fn unparse_args(&mut self, args: &Arguments) { + fn unparse_args(&mut self, args: &Arguments) { let mut first = true; for (i, arg_with_default) in args.posonlyargs.iter().chain(&args.args).enumerate() { self.p_delim(&mut first, ", "); @@ -1314,7 +1314,7 @@ impl<'a> Generator<'a> { } } - fn unparse_arg(&mut self, arg: &Arg) { + fn unparse_arg(&mut self, arg: &Arg) { self.p_id(&arg.arg); if let Some(ann) = &arg.annotation { self.p(": "); @@ -1322,7 +1322,7 @@ impl<'a> Generator<'a> { } } - fn unparse_arg_with_default(&mut self, arg_with_default: &ArgWithDefault) { + fn unparse_arg_with_default(&mut self, arg_with_default: &ArgWithDefault) { self.unparse_arg(&arg_with_default.def); if let Some(default) = &arg_with_default.default { self.p("="); @@ -1330,7 +1330,7 @@ impl<'a> Generator<'a> { } } - fn unparse_comp(&mut self, generators: &[Comprehension]) { + fn unparse_comp(&mut self, generators: &[Comprehension]) { for comp in generators { self.p(if comp.is_async { " async for " @@ -1347,18 +1347,13 @@ impl<'a> Generator<'a> { } } - fn unparse_fstring_body(&mut self, values: &[Expr], is_spec: bool) { + fn unparse_fstring_body(&mut self, values: &[Expr], is_spec: bool) { for value in values { self.unparse_fstring_elem(value, is_spec); } } - fn unparse_formatted( - &mut self, - val: &Expr, - conversion: ConversionFlag, - spec: Option<&Expr>, - ) { + fn unparse_formatted(&mut self, val: &Expr, conversion: ConversionFlag, spec: Option<&Expr>) { let mut generator = Generator::new(self.indent, self.quote, self.line_ending); generator.unparse_expr(val, precedence::FORMATTED_VALUE); let brace = if generator.buffer.starts_with('{') { @@ -1384,7 +1379,7 @@ impl<'a> Generator<'a> { self.p("}"); } - fn unparse_fstring_elem(&mut self, expr: &Expr, is_spec: bool) { + fn unparse_fstring_elem(&mut self, expr: &Expr, is_spec: bool) { match expr { Expr::Constant(ast::ExprConstant { value, .. }) => { if let Constant::Str(s) = value { @@ -1414,7 +1409,7 @@ impl<'a> Generator<'a> { self.p(&s); } - fn unparse_joinedstr(&mut self, values: &[Expr], is_spec: bool) { + fn unparse_joinedstr(&mut self, values: &[Expr], is_spec: bool) { if is_spec { self.unparse_fstring_body(values, is_spec); } else { @@ -1433,7 +1428,7 @@ impl<'a> Generator<'a> { } } - fn unparse_alias(&mut self, alias: &Alias) { + fn unparse_alias(&mut self, alias: &Alias) { self.p_id(&alias.name); if let Some(asname) = &alias.asname { self.p(" as "); @@ -1441,7 +1436,7 @@ impl<'a> Generator<'a> { } } - fn unparse_with_item(&mut self, with_item: &WithItem) { + fn unparse_with_item(&mut self, with_item: &WithItem) { self.unparse_expr(&with_item.context_expr, precedence::MAX); if let Some(optional_vars) = &with_item.optional_vars { self.p(" as "); From d3d69a031e3070a892e41b41bc191b6d0d11ea88 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 23 Jun 2023 23:03:05 +0200 Subject: [PATCH 217/447] Add `JoinCommaSeparatedBuilder` (#5342) --- crates/ruff_python_formatter/src/builders.rs | 101 ++++++++++++++++-- .../src/expression/expr_dict.rs | 77 ++++++------- .../src/expression/expr_tuple.rs | 89 ++++++--------- .../src/statement/stmt_class_def.rs | 25 +---- 4 files changed, 164 insertions(+), 128 deletions(-) diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 8d4e11db83..50a060e423 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -3,7 +3,7 @@ use crate::prelude::*; use crate::trivia::{first_non_trivia_token, lines_after, skip_trailing_trivia, Token, TokenKind}; use crate::USE_MAGIC_TRAILING_COMMA; use ruff_formatter::write; -use ruff_text_size::{TextRange, TextSize}; +use ruff_text_size::TextSize; use rustpython_parser::ast::Ranged; /// Provides Python specific extensions to [`Formatter`]. @@ -16,12 +16,21 @@ pub(crate) trait PyFormatterExtensions<'ast, 'buf> { /// * [`NodeLevel::CompoundStatement`]: Up to one empty line /// * [`NodeLevel::Expression`]: No empty lines fn join_nodes<'fmt>(&'fmt mut self, level: NodeLevel) -> JoinNodesBuilder<'fmt, 'ast, 'buf>; + + /// A builder that separates each element by a `,` and a [`soft_line_break_or_space`]. + /// It emits a trailing `,` that is only shown if the enclosing group expands. It forces the enclosing + /// group to expand if the last item has a trailing `comma` and the magical comma option is enabled. + fn join_comma_separated<'fmt>(&'fmt mut self) -> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf>; } impl<'buf, 'ast> PyFormatterExtensions<'ast, 'buf> for PyFormatter<'ast, 'buf> { fn join_nodes<'fmt>(&'fmt mut self, level: NodeLevel) -> JoinNodesBuilder<'fmt, 'ast, 'buf> { JoinNodesBuilder::new(self, level) } + + fn join_comma_separated<'fmt>(&'fmt mut self) -> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { + JoinCommaSeparatedBuilder::new(self) + } } #[must_use = "must eventually call `finish()` on the builder."] @@ -146,15 +155,87 @@ impl<'fmt, 'ast, 'buf> JoinNodesBuilder<'fmt, 'ast, 'buf> { } } -pub(crate) fn use_magic_trailing_comma(f: &mut PyFormatter, range: TextRange) -> bool { - USE_MAGIC_TRAILING_COMMA - && matches!( - first_non_trivia_token(range.end(), f.context().contents()), - Some(Token { - kind: TokenKind::Comma, - .. - }) - ) +pub(crate) struct JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { + result: FormatResult<()>, + fmt: &'fmt mut PyFormatter<'ast, 'buf>, + last_end: Option, +} + +impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { + fn new(f: &'fmt mut PyFormatter<'ast, 'buf>) -> Self { + Self { + fmt: f, + result: Ok(()), + last_end: None, + } + } + + pub(crate) fn entry( + &mut self, + node: &T, + content: &dyn Format>, + ) -> &mut Self + where + T: Ranged, + { + self.result = self.result.and_then(|_| { + if self.last_end.is_some() { + write!(self.fmt, [text(","), soft_line_break_or_space()])?; + } + + self.last_end = Some(node.end()); + + content.fmt(self.fmt) + }); + + self + } + + #[allow(unused)] + pub(crate) fn entries(&mut self, entries: I) -> &mut Self + where + T: Ranged, + F: Format>, + I: Iterator, + { + for (node, content) in entries { + self.entry(&node, &content); + } + + self + } + + pub(crate) fn nodes<'a, T, I>(&mut self, entries: I) -> &mut Self + where + T: Ranged + AsFormat> + 'a, + I: Iterator, + { + for node in entries { + self.entry(node, &node.format()); + } + + self + } + + pub(crate) fn finish(&mut self) -> FormatResult<()> { + if let Some(last_end) = self.last_end.take() { + if_group_breaks(&text(",")).fmt(self.fmt)?; + + if USE_MAGIC_TRAILING_COMMA + && matches!( + first_non_trivia_token(last_end, self.fmt.context().contents()), + Some(Token { + kind: TokenKind::Comma, + .. + }) + ) + { + expand_parent().fmt(self.fmt)?; + } + } + + Ok(()) + } } #[cfg(test)] diff --git a/crates/ruff_python_formatter/src/expression/expr_dict.rs b/crates/ruff_python_formatter/src/expression/expr_dict.rs index 1a56058e8e..c3263a1b0b 100644 --- a/crates/ruff_python_formatter/src/expression/expr_dict.rs +++ b/crates/ruff_python_formatter/src/expression/expr_dict.rs @@ -1,14 +1,12 @@ -use crate::builders::use_magic_trailing_comma; use crate::comments::{dangling_node_comments, leading_comments, Comments}; -use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; use crate::prelude::*; -use crate::{FormatNodeRule, PyFormatter}; -use ruff_formatter::format_args; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::FormatNodeRule; +use ruff_formatter::{format_args, write}; use ruff_python_ast::prelude::Ranged; +use ruff_text_size::TextRange; use rustpython_parser::ast::{Expr, ExprDict}; #[derive(Default)] @@ -19,6 +17,16 @@ struct KeyValuePair<'a> { value: &'a Expr, } +impl Ranged for KeyValuePair<'_> { + fn range(&self) -> TextRange { + if let Some(key) = self.key { + TextRange::new(key.start(), self.value.end()) + } else { + self.value.range() + } + } +} + impl Format> for KeyValuePair<'_> { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { if let Some(key) = self.key { @@ -54,57 +62,42 @@ impl FormatNodeRule for FormatExprDict { values, } = item; - let last = match &values[..] { - [] => { - return write!( - f, - [ - &text("{"), - block_indent(&dangling_node_comments(item)), - &text("}"), - ] - ); - } - [.., last] => last, - }; - let magic_trailing_comma = use_magic_trailing_comma(f, last.range()); - debug_assert_eq!(keys.len(), values.len()); - let joined = format_with(|f| { - f.join_with(format_args!(text(","), soft_line_break_or_space())) - .entries( - keys.iter() - .zip(values) - .map(|(key, value)| KeyValuePair { key, value }), - ) - .finish() - }); + if values.is_empty() { + return write!( + f, + [ + &text("{"), + block_indent(&dangling_node_comments(item)), + &text("}"), + ] + ); + } - let block = if magic_trailing_comma { - block_indent - } else { - soft_block_indent - }; + let format_pairs = format_with(|f| { + let mut joiner = f.join_comma_separated(); + + for (key, value) in keys.iter().zip(values) { + let key_value_pair = KeyValuePair { key, value }; + joiner.entry(&key_value_pair, &key_value_pair); + } + + joiner.finish() + }); write!( f, [group(&format_args![ text("{"), - block(&format_args![joined, if_group_breaks(&text(",")),]), + soft_block_indent(&format_pairs), text("}") ])] ) } fn fmt_dangling_comments(&self, _node: &ExprDict, _f: &mut PyFormatter) -> FormatResult<()> { - // TODO(konstin): Reactivate when string formatting works, currently a source of unstable - // formatting, e.g. - // ```python - // coverage_ignore_c_items = { - // # 'cfunction': [...] - // } - // ``` + // Handled by `fmt_fields` Ok(()) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_tuple.rs b/crates/ruff_python_formatter/src/expression/expr_tuple.rs index a08a9a4b51..57c78170cd 100644 --- a/crates/ruff_python_formatter/src/expression/expr_tuple.rs +++ b/crates/ruff_python_formatter/src/expression/expr_tuple.rs @@ -1,14 +1,12 @@ -use crate::builders::use_magic_trailing_comma; +use crate::builders::PyFormatterExtensions; use crate::comments::{dangling_node_comments, Comments}; use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; -use crate::{AsFormat, FormatNodeRule, FormattedIterExt, PyFormatter}; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; use ruff_formatter::formatter::Formatter; -use ruff_formatter::prelude::{ - block_indent, group, if_group_breaks, soft_block_indent, soft_line_break_or_space, text, -}; +use ruff_formatter::prelude::{block_indent, group, if_group_breaks, soft_block_indent, text}; use ruff_formatter::{format_args, write, Buffer, Format, FormatResult, FormatRuleWithOptions}; use ruff_python_ast::prelude::{Expr, Ranged}; use ruff_text_size::TextRange; @@ -61,9 +59,9 @@ impl FormatNodeRule for FormatExprTuple { } = item; // Handle the edge cases of an empty tuple and a tuple with one element - let last = match &elts[..] { + match elts.as_slice() { [] => { - return write!( + write!( f, [ // An empty tuple always needs parentheses, but does not have a comma @@ -71,10 +69,10 @@ impl FormatNodeRule for FormatExprTuple { block_indent(&dangling_node_comments(item)), &text(")"), ] - ); + ) } [single] => { - return write!( + write!( f, [group(&format_args![ // A single element tuple always needs parentheses and a trailing comma @@ -82,55 +80,38 @@ impl FormatNodeRule for FormatExprTuple { soft_block_indent(&format_args![single.format(), &text(",")]), &text(")"), ])] - ); + ) } - [.., last] => last, - }; - - let magic_trailing_comma = use_magic_trailing_comma(f, last.range()); - - if magic_trailing_comma { - // A magic trailing comma forces us to print in expanded mode since we have more than - // one element - write!( - f, - [ - // An expanded group always needs parentheses - &text("("), - block_indent(&ExprSequence::new(elts)), - &text(")"), - ] - )?; - } else if is_parenthesized(*range, elts, f) - && self.parentheses != TupleParentheses::StripInsideForLoop - { // If the tuple has parentheses, we generally want to keep them. The exception are for // loops, see `TupleParentheses::StripInsideForLoop` doc comment. // // Unlike other expression parentheses, tuple parentheses are part of the range of the // tuple itself. - write!( - f, - [group(&format_args![ - // If there were previously parentheses, keep them - &text("("), - soft_block_indent(&ExprSequence::new(elts)), - &text(")"), - ])] - )?; - } else { - write!( - f, - [group(&format_args![ - // If there were previously no parentheses, add them only if the group breaks - if_group_breaks(&text("(")), - soft_block_indent(&ExprSequence::new(elts)), - if_group_breaks(&text(")")), - ])] - )?; + elts if is_parenthesized(*range, elts, f) + && self.parentheses != TupleParentheses::StripInsideForLoop => + { + write!( + f, + [group(&format_args![ + // If there were previously parentheses, keep them + &text("("), + soft_block_indent(&ExprSequence::new(elts)), + &text(")"), + ])] + ) + } + elts => { + write!( + f, + [group(&format_args![ + // If there were previously no parentheses, add them only if the group breaks + if_group_breaks(&text("(")), + soft_block_indent(&ExprSequence::new(elts)), + if_group_breaks(&text(")")), + ])] + ) + } } - - Ok(()) } fn fmt_dangling_comments(&self, _node: &ExprTuple, _f: &mut PyFormatter) -> FormatResult<()> { @@ -152,11 +133,7 @@ impl<'a> ExprSequence<'a> { impl Format> for ExprSequence<'_> { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { - f.join_with(&format_args!(text(","), soft_line_break_or_space())) - .entries(self.elts.iter().formatted()) - .finish()?; - // Black style has a trailing comma on the last entry of an expanded group - write!(f, [if_group_breaks(&text(","))]) + f.join_comma_separated().nodes(self.elts.iter()).finish() } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs index 078f34051c..3fba6d4a55 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs @@ -1,11 +1,10 @@ -use crate::builders::use_magic_trailing_comma; use crate::comments::trailing_comments; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; use crate::trivia::{SimpleTokenizer, TokenKind}; use ruff_formatter::{format_args, write}; use ruff_text_size::TextRange; -use rustpython_parser::ast::{Expr, Keyword, Ranged, StmtClassDef}; +use rustpython_parser::ast::{Ranged, StmtClassDef}; #[derive(Default)] pub struct FormatStmtClassDef; @@ -80,10 +79,9 @@ impl Format> for FormatInheritanceClause<'_> { .. } = self.class_definition; - let separator = format_with(|f| write!(f, [text(","), soft_line_break_or_space()])); let source = f.context().contents(); - let mut joiner = f.join_with(&separator); + let mut joiner = f.join_comma_separated(); if let Some((first, rest)) = bases.split_first() { // Manually handle parentheses for the first expression because the logic in `FormatExpr` @@ -107,23 +105,10 @@ impl Format> for FormatInheritanceClause<'_> { Parenthesize::Never }; - joiner.entry(&first.format().with_options(parenthesize)); - joiner.entries(rest.iter().formatted()); + joiner.entry(first, &first.format().with_options(parenthesize)); + joiner.nodes(rest.iter()); } - joiner.entries(keywords.iter().formatted()).finish()?; - - if_group_breaks(&text(",")).fmt(f)?; - - let last = keywords - .last() - .map(Keyword::range) - .or_else(|| bases.last().map(Expr::range)) - .unwrap(); - if use_magic_trailing_comma(f, last) { - hard_line_break().fmt(f)?; - } - - Ok(()) + joiner.nodes(keywords.iter()).finish() } } From adf5cb5ff776a9b344225f8909e6310469c3d6ad Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Sat, 24 Jun 2023 04:21:09 +0530 Subject: [PATCH 218/447] Ignore type aliases for RUF013 (#5344) ## Summary Ignore type aliases for RUF013 to avoid flagging false positives: ```python from typing import Optional MaybeInt = Optional[int] def f(arg: MaybeInt = None): pass ``` But, at the expense of having false negatives: ```python Text = str | bytes def f(arg: Text = None): pass ``` ## Test Plan `cargo test` fixes: #5295 --- .../resources/test/fixtures/ruff/RUF013_0.py | 20 +++ .../src/rules/ruff/rules/implicit_optional.rs | 147 +++++++++++++++--- 2 files changed, 142 insertions(+), 25 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py b/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py index 371c57da11..21c5908979 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py @@ -221,3 +221,23 @@ def f(arg: Union["No" "ne", "int"] = None): # Avoid flagging when there's a parse error in the forward reference def f(arg: Union["<>", "int"] = None): pass + + +# Type aliases + +Text = str | bytes + + +def f(arg: Text = None): + pass + + +def f(arg: "Text" = None): + pass + + +from custom_typing import MaybeInt + + +def f(arg: MaybeInt = None): + pass diff --git a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs index 459d7eb451..2ceb04365d 100644 --- a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs +++ b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs @@ -6,10 +6,12 @@ use rustpython_parser::ast::{self, ArgWithDefault, Arguments, Constant, Expr, Op use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::call_path::CallPath; use ruff_python_ast::helpers::is_const_none; use ruff_python_ast::source_code::Locator; use ruff_python_ast::typing::parse_type_annotation; use ruff_python_semantic::SemanticModel; +use ruff_python_stdlib::sys::is_known_standard_library; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; @@ -58,6 +60,18 @@ use crate::settings::types::PythonVersion; /// pass /// ``` /// +/// ## Limitations +/// +/// Type aliases are not supported and could result in false negatives. +/// For example, the following code will not be flagged: +/// ```python +/// Text = str | bytes +/// +/// +/// def foo(arg: Text = None): +/// pass +/// ``` +/// /// ## Options /// - `target-version` /// @@ -141,6 +155,18 @@ impl<'a> Iterator for PEP604UnionIterator<'a> { } } +/// Returns `true` if the given call path is a known type. +/// +/// A known type is either a builtin type, any object from the standard library, +/// or a type from the `typing_extensions` module. +fn is_known_type(call_path: &CallPath, target_version: PythonVersion) -> bool { + match call_path.as_slice() { + ["" | "typing_extensions", ..] => true, + [module, ..] => is_known_standard_library(target_version.minor(), module), + _ => false, + } +} + #[derive(Debug)] enum TypingTarget<'a> { None, @@ -154,7 +180,12 @@ enum TypingTarget<'a> { } impl<'a> TypingTarget<'a> { - fn try_from_expr(expr: &'a Expr, semantic: &SemanticModel, locator: &Locator) -> Option { + fn try_from_expr( + expr: &'a Expr, + semantic: &SemanticModel, + locator: &Locator, + target_version: PythonVersion, + ) -> Option { match expr { Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { if semantic.match_typing_expr(value, "Optional") { @@ -170,7 +201,20 @@ impl<'a> TypingTarget<'a> { } else if semantic.match_typing_expr(value, "Annotated") { elements.first().map(TypingTarget::Annotated) } else { - None + semantic.resolve_call_path(value).map_or( + // If we can't resolve the call path, it must be defined + // in the same file, so we assume it's `Any` as it could + // be a type alias. + Some(TypingTarget::Any), + |call_path| { + if is_known_type(&call_path, target_version) { + None + } else { + // If it's not a known type, we assume it's `Any`. + Some(TypingTarget::Any) + } + }, + ) } } Expr::BinOp(..) => Some(TypingTarget::Union( @@ -189,54 +233,67 @@ impl<'a> TypingTarget<'a> { .map_or(Some(TypingTarget::Any), |(expr, _)| { Some(TypingTarget::ForwardReference(expr)) }), - _ => semantic.resolve_call_path(expr).and_then(|call_path| { - if semantic.match_typing_call_path(&call_path, "Any") { - Some(TypingTarget::Any) - } else if matches!(call_path.as_slice(), ["" | "builtins", "object"]) { - Some(TypingTarget::Object) - } else { - None - } - }), + _ => semantic.resolve_call_path(expr).map_or( + // If we can't resolve the call path, it must be defined in the + // same file, so we assume it's `Any` as it could be a type alias. + Some(TypingTarget::Any), + |call_path| { + if semantic.match_typing_call_path(&call_path, "Any") { + Some(TypingTarget::Any) + } else if matches!(call_path.as_slice(), ["" | "builtins", "object"]) { + Some(TypingTarget::Object) + } else if !is_known_type(&call_path, target_version) { + // If it's not a known type, we assume it's `Any`. + Some(TypingTarget::Any) + } else { + None + } + }, + ), } } /// Check if the [`TypingTarget`] explicitly allows `None`. - fn contains_none(&self, semantic: &SemanticModel, locator: &Locator) -> bool { + fn contains_none( + &self, + semantic: &SemanticModel, + locator: &Locator, + target_version: PythonVersion, + ) -> bool { match self { TypingTarget::None | TypingTarget::Optional | TypingTarget::Any | TypingTarget::Object => true, TypingTarget::Literal(elements) => elements.iter().any(|element| { - let Some(new_target) = TypingTarget::try_from_expr(element, semantic, locator) 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), + 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) else { + let Some(new_target) = TypingTarget::try_from_expr(element, semantic, locator, target_version) else { return false; }; - new_target.contains_none(semantic, locator) + new_target.contains_none(semantic, locator, target_version) }), TypingTarget::Annotated(element) => { - let Some(new_target) = TypingTarget::try_from_expr(element, semantic, locator) else { + let Some(new_target) = TypingTarget::try_from_expr(element, semantic, locator, target_version) else { return false; }; - new_target.contains_none(semantic, locator) + new_target.contains_none(semantic, locator, target_version) } TypingTarget::ForwardReference(expr) => { - let Some(new_target) = TypingTarget::try_from_expr(expr, semantic, locator) else { + let Some(new_target) = TypingTarget::try_from_expr(expr, semantic, locator, target_version) else { return false; }; - new_target.contains_none(semantic, locator) + new_target.contains_none(semantic, locator, target_version) } } } @@ -253,8 +310,9 @@ fn type_hint_explicitly_allows_none<'a>( annotation: &'a Expr, semantic: &SemanticModel, locator: &Locator, + target_version: PythonVersion, ) -> Option<&'a Expr> { - let Some(target) = TypingTarget::try_from_expr(annotation, semantic, locator) else { + let Some(target) = TypingTarget::try_from_expr(annotation, semantic, locator, target_version) else { return Some(annotation); }; match target { @@ -264,9 +322,11 @@ fn type_hint_explicitly_allows_none<'a>( // return the inner type if it doesn't allow `None`. If `Annotated` // is found nested inside another type, then the outer type should // be returned. - TypingTarget::Annotated(expr) => type_hint_explicitly_allows_none(expr, semantic, locator), + TypingTarget::Annotated(expr) => { + type_hint_explicitly_allows_none(expr, semantic, locator, target_version) + } _ => { - if target.contains_none(semantic, locator) { + if target.contains_none(semantic, locator, target_version) { None } else { Some(annotation) @@ -350,7 +410,7 @@ 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) 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(); @@ -366,7 +426,7 @@ 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) 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(); @@ -380,3 +440,40 @@ pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) { } } } + +#[cfg(test)] +mod tests { + use ruff_python_ast::call_path::CallPath; + + use crate::settings::types::PythonVersion; + + use super::is_known_type; + + #[test] + fn test_is_known_type() { + assert!(is_known_type( + &CallPath::from_slice(&["", "int"]), + PythonVersion::Py311 + )); + assert!(is_known_type( + &CallPath::from_slice(&["builtins", "int"]), + PythonVersion::Py311 + )); + assert!(is_known_type( + &CallPath::from_slice(&["typing", "Optional"]), + PythonVersion::Py311 + )); + assert!(is_known_type( + &CallPath::from_slice(&["typing_extensions", "Literal"]), + PythonVersion::Py311 + )); + assert!(is_known_type( + &CallPath::from_slice(&["zoneinfo", "ZoneInfo"]), + PythonVersion::Py311 + )); + assert!(!is_known_type( + &CallPath::from_slice(&["zoneinfo", "ZoneInfo"]), + PythonVersion::Py38 + )); + } +} From e0a507e48ebad7bc400f76290535673efece9229 Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Fri, 23 Jun 2023 17:54:43 -0500 Subject: [PATCH 219/447] Add Applicability to flake8_simplify (#5348) --- .../rules/flake8_simplify/rules/ast_bool_op.rs | 18 ++++++------------ .../rules/flake8_simplify/rules/ast_expr.rs | 6 ++---- .../src/rules/flake8_simplify/rules/ast_if.rs | 15 +++++---------- .../rules/flake8_simplify/rules/ast_ifexp.rs | 12 ++++-------- .../flake8_simplify/rules/ast_unary_op.rs | 12 ++++-------- .../rules/flake8_simplify/rules/ast_with.rs | 3 +-- .../rules/suppressible_exception.rs | 3 +-- ...ake8_simplify__tests__SIM201_SIM201.py.snap | 6 +++--- ...ake8_simplify__tests__SIM910_SIM910.py.snap | 10 +++++----- 9 files changed, 31 insertions(+), 54 deletions(-) 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 ebf3fbc285..4bf4e77d08 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 @@ -418,8 +418,7 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) { // Populate the `Fix`. Replace the _entire_ `BoolOp`. Note that if we have // multiple duplicates, the fixes will conflict. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&bool_op), expr.range(), ))); @@ -530,8 +529,7 @@ pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) { }; node.into() }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&in_expr), expr.range(), ))); @@ -583,8 +581,7 @@ pub(crate) fn expr_and_not_expr(checker: &mut Checker, expr: &Expr) { expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( "False".to_string(), expr.range(), ))); @@ -638,8 +635,7 @@ pub(crate) fn expr_or_not_expr(checker: &mut Checker, expr: &Expr) { expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( "True".to_string(), expr.range(), ))); @@ -762,8 +758,7 @@ pub(crate) fn expr_or_true(checker: &mut Checker, expr: &Expr) { edit.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(edit)); + diagnostic.set_fix(Fix::suggested(edit)); } checker.diagnostics.push(diagnostic); } @@ -780,8 +775,7 @@ pub(crate) fn expr_and_false(checker: &mut Checker, expr: &Expr) { edit.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(edit)); + diagnostic.set_fix(Fix::suggested(edit)); } checker.diagnostics.push(diagnostic); } 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 45424d41a9..d1a570a0f2 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs @@ -174,8 +174,7 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) { range: TextRange::default(), }; let new_env_var = node.into(); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&new_env_var), slice.range(), ))); @@ -229,8 +228,7 @@ pub(crate) fn dict_get_with_none_default(checker: &mut Checker, expr: &Expr) { ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( expected, expr.range(), ))); 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 8a3105919d..9466328880 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs @@ -385,8 +385,7 @@ pub(crate) fn nested_if_statements( <= checker.settings.line_length }) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(edit)); + diagnostic.set_fix(Fix::suggested(edit)); } } Err(err) => error!("Failed to fix nested if: {err}"), @@ -457,8 +456,7 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) { value: Some(test.clone()), range: TextRange::default(), }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().stmt(&node.into()), stmt.range(), ))); @@ -480,8 +478,7 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) { value: Some(Box::new(node1.into())), range: TextRange::default(), }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().stmt(&node2.into()), stmt.range(), ))); @@ -621,8 +618,7 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: O ); if checker.patch(diagnostic.kind.rule()) { if !has_comments(stmt, checker.locator) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( contents, stmt.range(), ))); @@ -978,8 +974,7 @@ pub(crate) fn use_dict_get_with_default( ); if checker.patch(diagnostic.kind.rule()) { if !has_comments(stmt, checker.locator) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( contents, stmt.range(), ))); 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 ffbc5d2032..eee0b0d1d1 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs @@ -162,8 +162,7 @@ pub(crate) fn explicit_true_false_in_ifexpr( ); if checker.patch(diagnostic.kind.rule()) { if matches!(test, Expr::Compare(_)) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&test.clone()), expr.range(), ))); @@ -179,8 +178,7 @@ pub(crate) fn explicit_true_false_in_ifexpr( keywords: vec![], range: TextRange::default(), }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&node1.into()), expr.range(), ))); @@ -223,8 +221,7 @@ pub(crate) fn explicit_false_true_in_ifexpr( operand: Box::new(node), range: TextRange::default(), }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&node1.into()), expr.range(), ))); @@ -275,8 +272,7 @@ pub(crate) fn twisted_arms_in_ifexpr( orelse: Box::new(node), range: TextRange::default(), }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&node3.into()), expr.range(), ))); 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 a24505d9e7..f0385b7560 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 @@ -178,8 +178,7 @@ pub(crate) fn negation_with_equal_op( comparators: comparators.clone(), range: TextRange::default(), }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( checker.generator().expr(&node.into()), expr.range(), ))); @@ -231,8 +230,7 @@ pub(crate) fn negation_with_not_equal_op( comparators: comparators.clone(), range: TextRange::default(), }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&node.into()), expr.range(), ))); @@ -260,8 +258,7 @@ pub(crate) fn double_negation(checker: &mut Checker, expr: &Expr, op: UnaryOp, o ); if checker.patch(diagnostic.kind.rule()) { if checker.semantic().in_boolean_test() { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(operand), expr.range(), ))); @@ -277,8 +274,7 @@ pub(crate) fn double_negation(checker: &mut Checker, expr: &Expr, op: UnaryOp, o keywords: vec![], range: TextRange::default(), }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&node1.into()), expr.range(), ))); diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs index d3c1642a70..8029e2a97b 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs @@ -148,8 +148,7 @@ pub(crate) fn multiple_with_statements( <= checker.settings.line_length }) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(edit)); + diagnostic.set_fix(Fix::suggested(edit)); } } Err(err) => error!("Failed to fix nested with: {err}"), diff --git a/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs b/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs index 9d5d984f88..a025d17601 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs @@ -130,8 +130,7 @@ pub(crate) fn suppressible_exception( ); let handler_line_begin = checker.locator.line_start(handler.start()); let remove_handler = Edit::deletion(handler_line_begin, handler.end()); - #[allow(deprecated)] - Ok(Fix::unspecified_edits( + Ok(Fix::suggested_edits( import_edit, [replace_try, remove_handler], )) diff --git a/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM201_SIM201.py.snap b/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM201_SIM201.py.snap index f7a55e6697..cff98673a5 100644 --- a/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM201_SIM201.py.snap +++ b/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM201_SIM201.py.snap @@ -10,7 +10,7 @@ SIM201.py:2:4: SIM201 [*] Use `a != b` instead of `not a == b` | = help: Replace with `!=` operator -ℹ Suggested fix +ℹ Fix 1 1 | # SIM201 2 |-if not a == b: 2 |+if a != b: @@ -27,7 +27,7 @@ SIM201.py:6:4: SIM201 [*] Use `a != b + c` instead of `not a == b + c` | = help: Replace with `!=` operator -ℹ Suggested fix +ℹ Fix 3 3 | pass 4 4 | 5 5 | # SIM201 @@ -46,7 +46,7 @@ SIM201.py:10:4: SIM201 [*] Use `a + b != c` instead of `not a + b == c` | = help: Replace with `!=` operator -ℹ Suggested fix +ℹ Fix 7 7 | pass 8 8 | 9 9 | # SIM201 diff --git a/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM910_SIM910.py.snap b/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM910_SIM910.py.snap index 66f8de5edb..ec96791486 100644 --- a/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM910_SIM910.py.snap +++ b/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM910_SIM910.py.snap @@ -11,7 +11,7 @@ SIM910.py:2:1: SIM910 [*] Use `{}.get(key)` instead of `{}.get(key, None)` | = help: Replace `{}.get(key, None)` with `{}.get(key)` -ℹ Suggested fix +ℹ Fix 1 1 | # SIM910 2 |-{}.get(key, None) 2 |+{}.get(key) @@ -29,7 +29,7 @@ SIM910.py:5:1: SIM910 [*] Use `{}.get("key")` instead of `{}.get("key", None)` | = help: Replace `{}.get("key", None)` with `{}.get("key")` -ℹ Suggested fix +ℹ Fix 2 2 | {}.get(key, None) 3 3 | 4 4 | # SIM910 @@ -48,7 +48,7 @@ SIM910.py:20:9: SIM910 [*] Use `{}.get(key)` instead of `{}.get(key, None)` | = help: Replace `{}.get(key, None)` with `{}.get(key)` -ℹ Suggested fix +ℹ Fix 17 17 | {}.get("key", False) 18 18 | 19 19 | # SIM910 @@ -68,7 +68,7 @@ SIM910.py:24:5: SIM910 [*] Use `{}.get(key)` instead of `{}.get(key, None)` | = help: Replace `{}.get(key, None)` with `{}.get(key)` -ℹ Suggested fix +ℹ Fix 21 21 | pass 22 22 | 23 23 | # SIM910 @@ -86,7 +86,7 @@ SIM910.py:27:1: SIM910 [*] Use `({}).get(key)` instead of `({}).get(key, None)` | = help: Replace `({}).get(key, None)` with `({}).get(key)` -ℹ Suggested fix +ℹ Fix 24 24 | a = {}.get(key, None) 25 25 | 26 26 | # SIM910 From 0ce38b650ec3a8a05a849d229a41ea9a99610e2a Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 25 Jun 2023 14:35:07 -0700 Subject: [PATCH 220/447] Change W605 autofix to use raw strings if possible (#5352) Fixes #5061. --- .../test/fixtures/pycodestyle/W605_0.py | 3 ++ .../rules/invalid_escape_sequence.rs | 31 ++++++++++---- ...s__pycodestyle__tests__W605_W605_0.py.snap | 40 ++++++++++++++----- ...s__pycodestyle__tests__W605_W605_1.py.snap | 24 +++++------ 4 files changed, 69 insertions(+), 29 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pycodestyle/W605_0.py b/crates/ruff/resources/test/fixtures/pycodestyle/W605_0.py index 5322443e87..ef350df454 100644 --- a/crates/ruff/resources/test/fixtures/pycodestyle/W605_0.py +++ b/crates/ruff/resources/test/fixtures/pycodestyle/W605_0.py @@ -19,6 +19,9 @@ with \_ somewhere in the middle """ +#: W605:1:38 +value = 'new line\nand invalid escape \_ here' + #: Okay regex = r'\.png$' regex = '\\.png$' diff --git a/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs b/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs index 792ec845ec..65a73bbf51 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs @@ -79,6 +79,8 @@ pub(crate) fn invalid_escape_sequence( let mut chars_iter = body.char_indices().peekable(); + let mut contains_valid_escape_sequence = false; + while let Some((i, c)) = chars_iter.next() { if c != '\\' { continue; @@ -101,20 +103,35 @@ pub(crate) fn invalid_escape_sequence( // If the next character is a valid escape sequence, skip. if VALID_ESCAPE_SEQUENCES.contains(next_char) { + contains_valid_escape_sequence = true; continue; } let location = start_offset + TextSize::try_from(i).unwrap(); let range = TextRange::at(location, next_char.text_len() + TextSize::from(1)); - let mut diagnostic = Diagnostic::new(InvalidEscapeSequence(*next_char), range); - if autofix { - diagnostic.set_fix(Fix::automatic(Edit::insertion( - r"\".to_string(), - range.start() + TextSize::from(1), - ))); - } + let diagnostic = Diagnostic::new(InvalidEscapeSequence(*next_char), range); diagnostics.push(diagnostic); } + + if autofix { + if contains_valid_escape_sequence { + // Escape with backslash + for diagnostic in &mut diagnostics { + diagnostic.set_fix(Fix::automatic(Edit::insertion( + r"\".to_string(), + diagnostic.range().start() + TextSize::from(1), + ))); + } + } else { + // Turn into raw string + for diagnostic in &mut diagnostics { + diagnostic.set_fix(Fix::automatic(Edit::insertion( + "r".to_string(), + range.start() + TextSize::try_from(quote_pos).unwrap(), + ))); + } + } + } } diagnostics diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_0.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_0.py.snap index fb5b4e8950..e863c676fd 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_0.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_0.py.snap @@ -14,7 +14,7 @@ W605_0.py:2:10: W605 [*] Invalid escape sequence: `\.` ℹ Fix 1 1 | #: W605:1:10 2 |-regex = '\.png$' - 2 |+regex = '\\.png$' + 2 |+regex = r'\.png$' 3 3 | 4 4 | #: W605:2:1 5 5 | regex = ''' @@ -30,14 +30,14 @@ W605_0.py:6:1: W605 [*] Invalid escape sequence: `\.` = help: Add backslash to escape sequence ℹ Fix +2 2 | regex = '\.png$' 3 3 | 4 4 | #: W605:2:1 -5 5 | regex = ''' -6 |-\.png$ - 6 |+\\.png$ +5 |-regex = ''' + 5 |+regex = r''' +6 6 | \.png$ 7 7 | ''' 8 8 | -9 9 | #: W605:2:6 W605_0.py:11:6: W605 [*] Invalid escape sequence: `\_` | @@ -54,7 +54,7 @@ W605_0.py:11:6: W605 [*] Invalid escape sequence: `\_` 9 9 | #: W605:2:6 10 10 | f( 11 |- '\_' - 11 |+ '\\_' + 11 |+ r'\_' 12 12 | ) 13 13 | 14 14 | #: W605:4:6 @@ -71,13 +71,33 @@ W605_0.py:18:6: W605 [*] Invalid escape sequence: `\_` = help: Add backslash to escape sequence ℹ Fix -15 15 | """ +12 12 | ) +13 13 | +14 14 | #: W605:4:6 +15 |-""" + 15 |+r""" 16 16 | multi-line 17 17 | literal -18 |-with \_ somewhere - 18 |+with \\_ somewhere -19 19 | in the middle +18 18 | with \_ somewhere + +W605_0.py:23:39: W605 [*] Invalid escape sequence: `\_` + | +22 | #: W605:1:38 +23 | value = 'new line\nand invalid escape \_ here' + | ^^ W605 +24 | +25 | #: Okay + | + = help: Add backslash to escape sequence + +ℹ Fix 20 20 | """ 21 21 | +22 22 | #: W605:1:38 +23 |-value = 'new line\nand invalid escape \_ here' + 23 |+value = 'new line\nand invalid escape \\_ here' +24 24 | +25 25 | #: Okay +26 26 | regex = r'\.png$' diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_1.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_1.py.snap index b6e26e233a..e3e8807ca4 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_1.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_1.py.snap @@ -14,7 +14,7 @@ W605_1.py:2:10: W605 [*] Invalid escape sequence: `\.` ℹ Fix 1 1 | #: W605:1:10 2 |-regex = '\.png$' - 2 |+regex = '\\.png$' + 2 |+regex = r'\.png$' 3 3 | 4 4 | #: W605:2:1 5 5 | regex = ''' @@ -30,14 +30,14 @@ W605_1.py:6:1: W605 [*] Invalid escape sequence: `\.` = help: Add backslash to escape sequence ℹ Fix +2 2 | regex = '\.png$' 3 3 | 4 4 | #: W605:2:1 -5 5 | regex = ''' -6 |-\.png$ - 6 |+\\.png$ +5 |-regex = ''' + 5 |+regex = r''' +6 6 | \.png$ 7 7 | ''' 8 8 | -9 9 | #: W605:2:6 W605_1.py:11:6: W605 [*] Invalid escape sequence: `\_` | @@ -54,7 +54,7 @@ W605_1.py:11:6: W605 [*] Invalid escape sequence: `\_` 9 9 | #: W605:2:6 10 10 | f( 11 |- '\_' - 11 |+ '\\_' + 11 |+ r'\_' 12 12 | ) 13 13 | 14 14 | #: W605:4:6 @@ -71,13 +71,13 @@ W605_1.py:18:6: W605 [*] Invalid escape sequence: `\_` = help: Add backslash to escape sequence ℹ Fix -15 15 | """ +12 12 | ) +13 13 | +14 14 | #: W605:4:6 +15 |-""" + 15 |+r""" 16 16 | multi-line 17 17 | literal -18 |-with \_ somewhere - 18 |+with \\_ somewhere -19 19 | in the middle -20 20 | """ -21 21 | +18 18 | with \_ somewhere From 1ef4eee089c8bd87abf5fb97cb56ae7bb56ae235 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 25 Jun 2023 18:10:08 -0400 Subject: [PATCH 221/447] Add space when migrating to raw string (#5358) ## Summary We had to do this for f-strings too -- if we add a prefix to `"foo"` in `return"foo"`, we also need to add a leading space. --- .../test/fixtures/pycodestyle/W605_0.py | 5 ++++ .../test/fixtures/pycodestyle/W605_1.py | 5 ++++ .../rules/invalid_escape_sequence.rs | 23 ++++++++++++---- ...s__pycodestyle__tests__W605_W605_0.py.snap | 27 ++++++++++++++++--- ...s__pycodestyle__tests__W605_W605_1.py.snap | 21 +++++++++++++++ 5 files changed, 72 insertions(+), 9 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pycodestyle/W605_0.py b/crates/ruff/resources/test/fixtures/pycodestyle/W605_0.py index ef350df454..287e71a1d9 100644 --- a/crates/ruff/resources/test/fixtures/pycodestyle/W605_0.py +++ b/crates/ruff/resources/test/fixtures/pycodestyle/W605_0.py @@ -22,6 +22,11 @@ in the middle #: W605:1:38 value = 'new line\nand invalid escape \_ here' + +def f(): + #: W605:1:11 + return'\.png$' + #: Okay regex = r'\.png$' regex = '\\.png$' diff --git a/crates/ruff/resources/test/fixtures/pycodestyle/W605_1.py b/crates/ruff/resources/test/fixtures/pycodestyle/W605_1.py index 5322443e87..20bf0ea14c 100644 --- a/crates/ruff/resources/test/fixtures/pycodestyle/W605_1.py +++ b/crates/ruff/resources/test/fixtures/pycodestyle/W605_1.py @@ -19,6 +19,11 @@ with \_ somewhere in the middle """ + +def f(): + #: W605:1:11 + return'\.png$' + #: Okay regex = r'\.png$' regex = '\\.png$' diff --git a/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs b/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs index 65a73bbf51..9a57ec84d8 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs @@ -70,10 +70,10 @@ pub(crate) fn invalid_escape_sequence( return diagnostics; }; let quote_pos = text.find(quote).unwrap(); - let prefix = text[..quote_pos].to_lowercase(); + let prefix = &text[..quote_pos]; let body = &text[quote_pos + quote.len()..text.len() - quote.len()]; - if !prefix.contains('r') { + if !prefix.contains(['r', 'R']) { let start_offset = range.start() + TextSize::try_from(quote_pos).unwrap() + quote.text_len(); @@ -115,7 +115,7 @@ pub(crate) fn invalid_escape_sequence( if autofix { if contains_valid_escape_sequence { - // Escape with backslash + // Escape with backslash. for diagnostic in &mut diagnostics { diagnostic.set_fix(Fix::automatic(Edit::insertion( r"\".to_string(), @@ -123,10 +123,23 @@ pub(crate) fn invalid_escape_sequence( ))); } } else { - // Turn into raw string + // Turn into raw string. for diagnostic in &mut diagnostics { + // If necessary, add a space between any leading keyword (`return`, `yield`, + // `assert`, etc.) and the string. For example, `return"foo"` is valid, but + // `returnr"foo"` is not. + let requires_space = locator + .slice(TextRange::up_to(range.start())) + .chars() + .last() + .map_or(false, |char| char.is_ascii_alphabetic()); + diagnostic.set_fix(Fix::automatic(Edit::insertion( - "r".to_string(), + if requires_space { + " r".to_string() + } else { + "r".to_string() + }, range.start() + TextSize::try_from(quote_pos).unwrap(), ))); } diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_0.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_0.py.snap index e863c676fd..7a4317fb36 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_0.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_0.py.snap @@ -85,8 +85,6 @@ W605_0.py:23:39: W605 [*] Invalid escape sequence: `\_` 22 | #: W605:1:38 23 | value = 'new line\nand invalid escape \_ here' | ^^ W605 -24 | -25 | #: Okay | = help: Add backslash to escape sequence @@ -97,7 +95,28 @@ W605_0.py:23:39: W605 [*] Invalid escape sequence: `\_` 23 |-value = 'new line\nand invalid escape \_ here' 23 |+value = 'new line\nand invalid escape \\_ here' 24 24 | -25 25 | #: Okay -26 26 | regex = r'\.png$' +25 25 | +26 26 | def f(): + +W605_0.py:28:12: W605 [*] Invalid escape sequence: `\.` + | +26 | def f(): +27 | #: W605:1:11 +28 | return'\.png$' + | ^^ W605 +29 | +30 | #: Okay + | + = help: Add backslash to escape sequence + +ℹ Fix +25 25 | +26 26 | def f(): +27 27 | #: W605:1:11 +28 |- return'\.png$' + 28 |+ return r'\.png$' +29 29 | +30 30 | #: Okay +31 31 | regex = r'\.png$' diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_1.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_1.py.snap index e3e8807ca4..6b6f5939fb 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_1.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_1.py.snap @@ -80,4 +80,25 @@ W605_1.py:18:6: W605 [*] Invalid escape sequence: `\_` 17 17 | literal 18 18 | with \_ somewhere +W605_1.py:25:12: W605 [*] Invalid escape sequence: `\.` + | +23 | def f(): +24 | #: W605:1:11 +25 | return'\.png$' + | ^^ W605 +26 | +27 | #: Okay + | + = help: Add backslash to escape sequence + +ℹ Fix +22 22 | +23 23 | def f(): +24 24 | #: W605:1:11 +25 |- return'\.png$' + 25 |+ return r'\.png$' +26 26 | +27 27 | #: Okay +28 28 | regex = r'\.png$' + From b2337631564211c483d174db24f8b69cfd28b20a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 25 Jun 2023 18:16:59 -0400 Subject: [PATCH 222/447] Run `cargo update` (#5357) --- Cargo.lock | 356 ++++++++++++++++++++++++++++------------------------- 1 file changed, 186 insertions(+), 170 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2dd4861dd6..dd8b620bbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,9 +25,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] @@ -86,15 +86,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" [[package]] name = "anstyle-parse" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" dependencies = [ "utf8parse", ] @@ -165,12 +165,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.2" @@ -194,9 +188,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84" +checksum = "6dbe3c979c178231552ecba20214a8272df4e09f232a87aef4320cf06539aded" [[package]] name = "bstr" @@ -297,9 +291,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.1" +version = "4.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ed2379f8603fa2b7509891660e802b88c70a79a6427a70abb5968054de2c28" +checksum = "d9394150f5b4273a1763355bd1c2ec54cc5a2593f790587bcd6b2c947cfa9211" dependencies = [ "clap_builder", "clap_derive", @@ -308,9 +302,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.1" +version = "4.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72394f3339a76daf211e57d4bcb374410f3965dcc606dd0e03738c7888766980" +checksum = "9a78fbdd3cc2914ddf37ba444114bc7765bbdcb55ec9cbe6fa054f0137400717" dependencies = [ "anstream", "anstyle", @@ -362,14 +356,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.3.1" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e9ef9a08ee1c0e1f2e162121665ac45ac3783b0f897db7244ae75ad9a8f65b" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -526,9 +520,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.14" +version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ "autocfg", "cfg-if", @@ -539,9 +533,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if", ] @@ -583,7 +577,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -594,7 +588,7 @@ checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" dependencies = [ "darling_core", "quote", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -680,6 +674,12 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "equivalent" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" + [[package]] name = "errno" version = "0.3.1" @@ -768,9 +768,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -786,9 +786,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", @@ -826,6 +826,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + [[package]] name = "heck" version = "0.4.1" @@ -870,9 +876,9 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" [[package]] name = "iana-time-zone" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -899,9 +905,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -941,10 +947,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", "serde", ] +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + [[package]] name = "inotify" version = "0.9.6" @@ -967,9 +983,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.29.0" +version = "1.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a28d25139df397cbca21408bb742cf6837e04cdbebf1b07b760caf971d6a972" +checksum = "28491f7753051e5704d4d0ae7860d45fae3238d7d235bc4289dcd45c48d3cec3" dependencies = [ "console", "lazy_static", @@ -1040,9 +1056,9 @@ checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "js-sys" -version = "0.3.63" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] @@ -1111,9 +1127,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.144" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "libcst" @@ -1163,9 +1179,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "log" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" [[package]] name = "matches" @@ -1181,9 +1197,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memoffset" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] @@ -1324,9 +1340,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.2" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "oorandom" @@ -1342,9 +1358,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "os_str_bytes" -version = "6.5.0" +version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" +checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" dependencies = [ "memchr", ] @@ -1417,22 +1433,21 @@ checksum = "9fa00462b37ead6d11a82c9d568b26682d78e0477dc02d1966c013af80969739" [[package]] name = "pep440_rs" -version = "0.3.9" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe1d15693a11422cfa7d401b00dc9ae9fb8edbfbcb711a77130663f4ddf67650" +checksum = "b05bf2c44c4cd12f03b2c3ca095f3aa21f44e43c16021c332e511884719705be" dependencies = [ "lazy_static", "regex", "serde", - "tracing", "unicode-width", ] [[package]] name = "pep508_rs" -version = "0.1.5" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969679a29dfdc8278a449f75b3dd45edf57e649bd59f7502429c2840751c46d8" +checksum = "c0713d7bb861ca2b7d4c50a38e1f31a4b63a2e2df35ef1e5855cc29e108453e2" dependencies = [ "once_cell", "pep440_rs", @@ -1446,15 +1461,15 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "phf" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ "phf_macros", "phf_shared", @@ -1462,9 +1477,9 @@ dependencies = [ [[package]] name = "phf_codegen" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" dependencies = [ "phf_generator", "phf_shared", @@ -1472,9 +1487,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ "phf_shared", "rand", @@ -1482,22 +1497,22 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92aacdc5f16768709a569e913f7451034034178b05bdc8acda226659a3dccc66" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" dependencies = [ "phf_generator", "phf_shared", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.22", ] [[package]] name = "phf_shared" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" dependencies = [ "siphasher", ] @@ -1510,9 +1525,9 @@ checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "plotters" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" dependencies = [ "num-traits", "plotters-backend", @@ -1523,15 +1538,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" [[package]] name = "plotters-svg" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" dependencies = [ "plotters-backend", ] @@ -1613,20 +1628,20 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.59" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" +checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" dependencies = [ "unicode-ident", ] [[package]] name = "pyproject-toml" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04dbbb336bd88583943c7cd973a32fed323578243a7569f40cb0c7da673321b" +checksum = "ee79feaa9d31e1c417e34219e610b67db4e786ce9b49d77dda549640abb9dc5f" dependencies = [ - "indexmap", + "indexmap 1.9.3", "pep440_rs", "pep508_rs", "serde", @@ -1640,7 +1655,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b909fe9bf2abb1e3d6a97c9189a37c8105c61d03dca9ce6aace023e7d682bd" dependencies = [ "chrono", - "indexmap", + "indexmap 1.9.3", "nextest-workspace-hack", "quick-xml", "thiserror", @@ -1733,11 +1748,11 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.3" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ - "aho-corasick 1.0.1", + "aho-corasick 1.0.2", "memchr", "regex-syntax", ] @@ -1797,7 +1812,7 @@ version = "0.0.275" dependencies = [ "annotate-snippets 0.9.1", "anyhow", - "bitflags 2.3.1", + "bitflags 2.3.2", "chrono", "clap", "colored", @@ -1897,7 +1912,7 @@ dependencies = [ "assert_cmd", "atty", "bincode", - "bitflags 2.3.1", + "bitflags 2.3.2", "cachedir", "chrono", "clap", @@ -2002,7 +2017,7 @@ dependencies = [ "proc-macro2", "quote", "ruff_textwrap", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -2010,7 +2025,7 @@ name = "ruff_python_ast" version = "0.0.0" dependencies = [ "anyhow", - "bitflags 2.3.1", + "bitflags 2.3.2", "insta", "is-macro", "itertools", @@ -2034,7 +2049,7 @@ name = "ruff_python_formatter" version = "0.0.0" dependencies = [ "anyhow", - "bitflags 2.3.1", + "bitflags 2.3.2", "clap", "countme", "insta", @@ -2056,7 +2071,7 @@ dependencies = [ name = "ruff_python_semantic" version = "0.0.0" dependencies = [ - "bitflags 2.3.1", + "bitflags 2.3.2", "is-macro", "nohash-hasher", "num-traits", @@ -2096,7 +2111,7 @@ dependencies = [ "glob", "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -2153,9 +2168,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.37.19" +version = "0.37.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" dependencies = [ "bitflags 1.3.2", "errno", @@ -2167,14 +2182,24 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.8" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +checksum = "e32ca28af694bc1bbf399c33a516dbdf1c90090b8ab23c2bc24f834aa2247f5f" dependencies = [ "log", "ring", + "rustls-webpki", "sct", - "webpki", +] + +[[package]] +name = "rustls-webpki" +version = "0.100.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +dependencies = [ + "ring", + "untrusted", ] [[package]] @@ -2193,7 +2218,7 @@ name = "rustpython-format" version = "0.2.0" source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8078663b6c914c1cb86993e427764f7c422fc12c#8078663b6c914c1cb86993e427764f7c422fc12c" dependencies = [ - "bitflags 2.3.1", + "bitflags 2.3.2", "itertools", "num-bigint", "num-traits", @@ -2320,9 +2345,9 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.163" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" dependencies = [ "serde_derive", ] @@ -2340,13 +2365,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -2362,9 +2387,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" dependencies = [ "itoa", "ryu", @@ -2373,9 +2398,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" dependencies = [ "serde", ] @@ -2386,14 +2411,14 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f02d8aa6e3c385bf084924f660ce2a3a6bd333ba55b35e8590b321f35d88513" dependencies = [ - "base64 0.21.2", + "base64", "chrono", "hex", - "indexmap", + "indexmap 1.9.3", "serde", "serde_json", "serde_with_macros", - "time 0.3.21", + "time 0.3.22", ] [[package]] @@ -2405,7 +2430,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -2488,9 +2513,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.18" +version = "2.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616" dependencies = [ "proc-macro2", "quote", @@ -2508,15 +2533,16 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.5.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" dependencies = [ + "autocfg", "cfg-if", "fastrand", "redox_syscall 0.3.5", "rustix", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -2590,7 +2616,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -2636,9 +2662,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" dependencies = [ "itoa", "serde", @@ -2697,9 +2723,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" +checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240" dependencies = [ "serde", "serde_spanned", @@ -2709,20 +2735,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.10" +version = "0.19.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" +checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" dependencies = [ - "indexmap", + "indexmap 2.0.0", "serde", "serde_spanned", "toml_datetime", @@ -2744,13 +2770,13 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -2875,25 +2901,25 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "ureq" -version = "2.6.2" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d" +checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9" dependencies = [ - "base64 0.13.1", + "base64", "flate2", "log", "once_cell", "rustls", + "rustls-webpki", "url", - "webpki", "webpki-roots", ] [[package]] name = "url" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", "idna", @@ -2909,9 +2935,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.3.3" +version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" +checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81" [[package]] name = "version_check" @@ -2952,9 +2978,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2962,24 +2988,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.36" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ "cfg-if", "js-sys", @@ -2989,9 +3015,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2999,28 +3025,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasm-bindgen-test" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e636f3a428ff62b3742ebc3c70e254dfe12b8c2b469d688ea59cdd4abcf502" +checksum = "6e6e302a7ea94f83a6d09e78e7dc7d9ca7b186bc2829c24a22d0753efd680671" dependencies = [ "console_error_panic_hook", "js-sys", @@ -3032,9 +3058,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f18c1fad2f7c4958e7bcce014fa212f59a65d5e3721d0f77e6c0b27ede936ba3" +checksum = "ecb993dd8c836930ed130e020e77d9b2e65dd0fbab1b67c790b0f5d80b11a575" dependencies = [ "proc-macro2", "quote", @@ -3042,31 +3068,21 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.63" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" dependencies = [ - "webpki", + "rustls-webpki", ] [[package]] @@ -3263,9 +3279,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" dependencies = [ "memchr", ] From 1fe4073b561e17a1ada124749de0148039328cc4 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 25 Jun 2023 18:20:31 -0400 Subject: [PATCH 223/447] Update the `invalid-escape-sequence` rule (#5359) Just a couple small tweaks based on reading the rule with fresh eyes and new best-practices. --- .../rules/invalid_escape_sequence.rs | 188 ++++++++++-------- 1 file changed, 100 insertions(+), 88 deletions(-) diff --git a/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs b/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs index 9a57ec84d8..f58bd5855d 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs @@ -1,10 +1,9 @@ -use anyhow::{bail, Result}; -use log::error; use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::Locator; +use ruff_python_ast::str::{leading_quote, trailing_quote}; /// ## What it does /// Checks for invalid escape sequences. @@ -21,6 +20,9 @@ use ruff_python_ast::source_code::Locator; /// ```python /// regex = r"\.png$" /// ``` +/// +/// ## References +/// - [Python documentation: String and Bytes literals](https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals) #[violation] pub struct InvalidEscapeSequence(char); @@ -36,24 +38,6 @@ impl AlwaysAutofixableViolation for InvalidEscapeSequence { } } -// See: https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals -const VALID_ESCAPE_SEQUENCES: &[char; 23] = &[ - '\n', '\\', '\'', '"', 'a', 'b', 'f', 'n', 'r', 't', 'v', '0', '1', '2', '3', '4', '5', '6', - '7', 'x', // Escape sequences only recognized in string literals - 'N', 'u', 'U', -]; - -/// Return the quotation markers used for a String token. -fn extract_quote(text: &str) -> Result<&str> { - for quote in ["'''", "\"\"\"", "'", "\""] { - if text.ends_with(quote) { - return Ok(quote); - } - } - - bail!("Unable to find quotation mark for String token") -} - /// W605 pub(crate) fn invalid_escape_sequence( locator: &Locator, @@ -65,84 +49,112 @@ pub(crate) fn invalid_escape_sequence( let text = locator.slice(range); // Determine whether the string is single- or triple-quoted. - let Ok(quote) = extract_quote(text) else { - error!("Unable to find quotation mark for string token"); + let Some(leading_quote) = leading_quote(text) else { return diagnostics; }; - let quote_pos = text.find(quote).unwrap(); - let prefix = &text[..quote_pos]; - let body = &text[quote_pos + quote.len()..text.len() - quote.len()]; + let Some(trailing_quote) = trailing_quote(text) else { + return diagnostics; + }; + let body = &text[leading_quote.len()..text.len() - trailing_quote.len()]; - if !prefix.contains(['r', 'R']) { - let start_offset = - range.start() + TextSize::try_from(quote_pos).unwrap() + quote.text_len(); + if leading_quote.contains(['r', 'R']) { + return diagnostics; + } - let mut chars_iter = body.char_indices().peekable(); + let start_offset = range.start() + TextSize::try_from(leading_quote.len()).unwrap(); - let mut contains_valid_escape_sequence = false; + let mut chars_iter = body.char_indices().peekable(); - while let Some((i, c)) = chars_iter.next() { - if c != '\\' { - continue; - } + let mut contains_valid_escape_sequence = false; - // If the previous character was also a backslash, skip. - if i > 0 && body.as_bytes()[i - 1] == b'\\' { - continue; - } - - // If we're at the end of the file, skip. - let Some((_, next_char)) = chars_iter.peek() else { - continue; - }; - - // If we're at the end of the line, skip - if matches!(next_char, '\n' | '\r') { - continue; - } - - // If the next character is a valid escape sequence, skip. - if VALID_ESCAPE_SEQUENCES.contains(next_char) { - contains_valid_escape_sequence = true; - continue; - } - - let location = start_offset + TextSize::try_from(i).unwrap(); - let range = TextRange::at(location, next_char.text_len() + TextSize::from(1)); - let diagnostic = Diagnostic::new(InvalidEscapeSequence(*next_char), range); - diagnostics.push(diagnostic); + while let Some((i, c)) = chars_iter.next() { + if c != '\\' { + continue; } - if autofix { - if contains_valid_escape_sequence { - // Escape with backslash. - for diagnostic in &mut diagnostics { - diagnostic.set_fix(Fix::automatic(Edit::insertion( - r"\".to_string(), - diagnostic.range().start() + TextSize::from(1), - ))); - } - } else { - // Turn into raw string. - for diagnostic in &mut diagnostics { - // If necessary, add a space between any leading keyword (`return`, `yield`, - // `assert`, etc.) and the string. For example, `return"foo"` is valid, but - // `returnr"foo"` is not. - let requires_space = locator - .slice(TextRange::up_to(range.start())) - .chars() - .last() - .map_or(false, |char| char.is_ascii_alphabetic()); + // If the previous character was also a backslash, skip. + if i > 0 && body.as_bytes()[i - 1] == b'\\' { + continue; + } - diagnostic.set_fix(Fix::automatic(Edit::insertion( - if requires_space { - " r".to_string() - } else { - "r".to_string() - }, - range.start() + TextSize::try_from(quote_pos).unwrap(), - ))); - } + // If we're at the end of the file, skip. + let Some((_, next_char)) = chars_iter.peek() else { + continue; + }; + + // If we're at the end of the line, skip + if matches!(next_char, '\n' | '\r') { + continue; + } + + // If the next character is a valid escape sequence, skip. + // See: https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals. + if matches!( + next_char, + '\n' + | '\\' + | '\'' + | '"' + | 'a' + | 'b' + | 'f' + | 'n' + | 'r' + | 't' + | 'v' + | '0' + | '1' + | '2' + | '3' + | '4' + | '5' + | '6' + | '7' + | 'x' + // Escape sequences only recognized in string literals + | 'N' + | 'u' + | 'U' + ) { + contains_valid_escape_sequence = true; + continue; + } + + let location = start_offset + TextSize::try_from(i).unwrap(); + let range = TextRange::at(location, next_char.text_len() + TextSize::from(1)); + let diagnostic = Diagnostic::new(InvalidEscapeSequence(*next_char), range); + diagnostics.push(diagnostic); + } + + if autofix { + if contains_valid_escape_sequence { + // Escape with backslash. + for diagnostic in &mut diagnostics { + diagnostic.set_fix(Fix::automatic(Edit::insertion( + r"\".to_string(), + diagnostic.range().start() + TextSize::from(1), + ))); + } + } else { + // Turn into raw string. + for diagnostic in &mut diagnostics { + // If necessary, add a space between any leading keyword (`return`, `yield`, + // `assert`, etc.) and the string. For example, `return"foo"` is valid, but + // `returnr"foo"` is not. + let requires_space = locator + .slice(TextRange::up_to(range.start())) + .chars() + .last() + .map_or(false, |char| char.is_ascii_alphabetic()); + + diagnostic.set_fix(Fix::automatic(Edit::insertion( + if requires_space { + " r".to_string() + } else { + "r".to_string() + }, + range.start(), + ))); } } } From fd0c3faa705b9a50df74410ba7c2e38bb7cb1cf1 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Sun, 25 Jun 2023 23:34:03 +0100 Subject: [PATCH 224/447] Add documentation to rules that check docstring quotes (`D3XX`) (#5351) ## Summary Add documentation to the `D3XX` rules that check for issues with docstring quotes. Related to #2646. ## Test Plan `python scripts/check_docs_formatted.py` --- .../src/rules/pydocstyle/rules/backslashes.rs | 36 +++++++++++++++++++ .../rules/pydocstyle/rules/triple_quotes.rs | 25 +++++++++++++ .../rules/collection_literal_concatenation.rs | 2 +- scripts/check_docs_formatted.py | 1 + 4 files changed, 63 insertions(+), 1 deletion(-) diff --git a/crates/ruff/src/rules/pydocstyle/rules/backslashes.rs b/crates/ruff/src/rules/pydocstyle/rules/backslashes.rs index 7bd2b326fd..a1ece3daae 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/backslashes.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/backslashes.rs @@ -7,6 +7,42 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; +/// ## What it does +/// Checks for docstrings that include backslashes, but are not defined as +/// raw string literals. +/// +/// ## Why is this bad? +/// In Python, backslashes are typically used to escape characters in strings. +/// In raw strings (those prefixed with an `r`), however, backslashes are +/// treated as literal characters. +/// +/// [PEP 257](https://peps.python.org/pep-0257/#what-is-a-docstring) recommends +/// the use of raw strings (i.e., `r"""raw triple double quotes"""`) for +/// docstrings that include backslashes. The use of a raw string ensures that +/// any backslashes are treated as literal characters, and not as escape +/// sequences, which avoids confusion. +/// +/// ## Example +/// ```python +/// def foobar(): +/// """Docstring for foo\bar.""" +/// +/// +/// foobar.__doc__ # "Docstring for foar." +/// ``` +/// +/// Use instead: +/// ```python +/// def foobar(): +/// r"""Docstring for foo\bar.""" +/// +/// +/// foobar.__doc__ # "Docstring for foo\bar." +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [Python documentation: String and Bytes literals](https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals) #[violation] pub struct EscapeSequenceInDocstring; diff --git a/crates/ruff/src/rules/pydocstyle/rules/triple_quotes.rs b/crates/ruff/src/rules/pydocstyle/rules/triple_quotes.rs index 5cf020ce38..c1ccb64c99 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/triple_quotes.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/triple_quotes.rs @@ -4,6 +4,31 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; +/// ## What it does +/// Checks for docstrings that use `'''triple single quotes'''` instead of +/// `"""triple double quotes"""`. +/// +/// ## Why is this bad? +/// [PEP 257](https://peps.python.org/pep-0257/#what-is-a-docstring) recommends +/// the use of `"""triple double quotes"""` for docstrings, to ensure +/// consistency. +/// +/// ## Example +/// ```python +/// def kos_root(): +/// '''Return the pathname of the KOS root directory.''' +/// ``` +/// +/// Use instead: +/// ```python +/// def kos_root(): +/// """Return the pathname of the KOS root directory.""" +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct TripleSingleQuotes; 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 86b51129d3..367c4ee026 100644 --- a/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs +++ b/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs @@ -37,7 +37,7 @@ use crate::registry::AsRule; /// /// ## References /// - [PEP 448 – Additional Unpacking Generalizations](https://peps.python.org/pep-0448/) -/// - [Python docs: Sequence Types — `list`, `tuple`, `range`](https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range) +/// - [Python documentation: Sequence Types — `list`, `tuple`, `range`](https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range) #[violation] pub struct CollectionLiteralConcatenation { expr: String, diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index 7256f28c7c..0c2680460c 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -52,6 +52,7 @@ KNOWN_FORMATTING_VIOLATIONS = [ "shebang-leading-whitespace", "too-few-spaces-before-inline-comment", "trailing-comma-on-bare-tuple", + "triple-single-quotes", "unexpected-indentation-comment", "unicode-kind-prefix", "unnecessary-class-parentheses", From 19c221a2d26d930e1c55276182255572d16dbd58 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 25 Jun 2023 21:57:52 -0400 Subject: [PATCH 225/447] Use matches for `os-error-alias` (#5361) --- .../src/rules/pyupgrade/rules/os_error_alias.rs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs index 09f7821919..6739f5b680 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs @@ -54,21 +54,14 @@ impl AlwaysAutofixableViolation for OSErrorAlias { } } -const ALIASES: &[(&str, &str)] = &[ - ("", "EnvironmentError"), - ("", "IOError"), - ("", "WindowsError"), - ("mmap", "error"), - ("select", "error"), - ("socket", "error"), -]; - /// Return `true` if an [`Expr`] is an alias of `OSError`. fn is_alias(expr: &Expr, semantic: &SemanticModel) -> bool { semantic.resolve_call_path(expr).map_or(false, |call_path| { - ALIASES - .iter() - .any(|(module, member)| call_path.as_slice() == [*module, *member]) + matches!( + call_path.as_slice(), + ["", "EnvironmentError" | "IOError" | "WindowsError"] + | ["mmap" | "select" | "socket", "error"] + ) }) } From 18c73c1f9bf97562f4a9b3afe6dfad482d11937f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 25 Jun 2023 21:58:20 -0400 Subject: [PATCH 226/447] Improve backslash-detection rule for docstrings (#5360) --- Cargo.lock | 1 + crates/ruff/Cargo.toml | 1 + crates/ruff/src/docstrings/mod.rs | 1 - .../src/rules/pydocstyle/rules/backslashes.rs | 17 ++++--- .../pydocstyle/rules/non_imperative_mood.rs | 40 +++++++++------- .../rules/pydocstyle/rules/triple_quotes.rs | 46 +++++++++++++------ 6 files changed, 67 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dd8b620bbc..1645b94509 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1827,6 +1827,7 @@ dependencies = [ "itertools", "libcst", "log", + "memchr", "natord", "nohash-hasher", "num-bigint", diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 8dee42ef80..59643a1736 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -42,6 +42,7 @@ is-macro = { workspace = true } itertools = { workspace = true } libcst = { workspace = true } log = { workspace = true } +memchr = { workspace = true } natord = { version = "1.0.9" } nohash-hasher = { workspace = true } num-bigint = { workspace = true } diff --git a/crates/ruff/src/docstrings/mod.rs b/crates/ruff/src/docstrings/mod.rs index 3103aa5a62..97b44784b5 100644 --- a/crates/ruff/src/docstrings/mod.rs +++ b/crates/ruff/src/docstrings/mod.rs @@ -18,7 +18,6 @@ pub(crate) struct Docstring<'a> { pub(crate) expr: &'a Expr, /// The content of the docstring, including the leading and trailing quotes. pub(crate) contents: &'a str, - /// The range of the docstring body (without the quotes). The range is relative to [`Self::contents`]. pub(crate) body_range: TextRange, pub(crate) indentation: &'a str, diff --git a/crates/ruff/src/rules/pydocstyle/rules/backslashes.rs b/crates/ruff/src/rules/pydocstyle/rules/backslashes.rs index a1ece3daae..9bc972a4e5 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/backslashes.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/backslashes.rs @@ -1,5 +1,4 @@ -use once_cell::sync::Lazy; -use regex::Regex; +use memchr::memchr_iter; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -53,18 +52,22 @@ impl Violation for EscapeSequenceInDocstring { } } -static BACKSLASH_REGEX: Lazy = Lazy::new(|| Regex::new(r"\\[^(\r\n|\n)uN]").unwrap()); - /// D301 pub(crate) fn backslashes(checker: &mut Checker, docstring: &Docstring) { - let contents = docstring.contents; + let body = docstring.body(); // Docstring is already raw. - if contents.starts_with('r') || contents.starts_with("ur") { + if body.starts_with('r') || body.starts_with("ur") { return; } - if BACKSLASH_REGEX.is_match(contents) { + // Docstring contains at least one backslash. + let bytes = body.as_bytes(); + if memchr_iter(b'\\', bytes).any(|position| { + let escaped_char = bytes.get(position.saturating_add(1)); + // Allow continuations (backslashes followed by newlines) and Unicode escapes. + !matches!(escaped_char, Some(b'\r' | b'\n' | b'u' | b'N')) + }) { checker.diagnostics.push(Diagnostic::new( EscapeSequenceInDocstring, docstring.range(), diff --git a/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs b/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs index ecda3d504a..d4d80dbdc2 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs @@ -17,6 +17,19 @@ use crate::rules::pydocstyle::helpers::normalize_word; static MOOD: Lazy = Lazy::new(Mood::new); +#[violation] +pub struct NonImperativeMood { + first_line: String, +} + +impl Violation for NonImperativeMood { + #[derive_message_formats] + fn message(&self) -> String { + let NonImperativeMood { first_line } = self; + format!("First line of docstring should be in imperative mood: \"{first_line}\"") + } +} + /// D401 pub(crate) fn non_imperative_mood( checker: &mut Checker, @@ -52,31 +65,26 @@ pub(crate) fn non_imperative_mood( let body = docstring.body(); // Find first line, disregarding whitespace. - let line = match body.trim().universal_newlines().next() { + let first_line = match body.trim().universal_newlines().next() { Some(line) => line.as_str().trim(), None => return, }; + // Find the first word on that line and normalize it to lower-case. - let first_word_norm = match line.split_whitespace().next() { + let first_word_norm = match first_line.split_whitespace().next() { Some(word) => normalize_word(word), None => return, }; if first_word_norm.is_empty() { return; } - if let Some(false) = MOOD.is_imperative(&first_word_norm) { - let diagnostic = Diagnostic::new(NonImperativeMood(line.to_string()), docstring.range()); - checker.diagnostics.push(diagnostic); - } -} - -#[violation] -pub struct NonImperativeMood(pub String); - -impl Violation for NonImperativeMood { - #[derive_message_formats] - fn message(&self) -> String { - let NonImperativeMood(first_line) = self; - format!("First line of docstring should be in imperative mood: \"{first_line}\"") + + if matches!(MOOD.is_imperative(&first_word_norm), Some(false)) { + checker.diagnostics.push(Diagnostic::new( + NonImperativeMood { + first_line: first_line.to_string(), + }, + docstring.range(), + )); } } diff --git a/crates/ruff/src/rules/pydocstyle/rules/triple_quotes.rs b/crates/ruff/src/rules/pydocstyle/rules/triple_quotes.rs index c1ccb64c99..caa7d59872 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/triple_quotes.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/triple_quotes.rs @@ -1,5 +1,6 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::source_code::Quote; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; @@ -30,32 +31,47 @@ use crate::docstrings::Docstring; /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] -pub struct TripleSingleQuotes; +pub struct TripleSingleQuotes { + expected_quote: Quote, +} impl Violation for TripleSingleQuotes { #[derive_message_formats] fn message(&self) -> String { - format!(r#"Use triple double quotes `"""`"#) + let TripleSingleQuotes { expected_quote } = self; + match expected_quote { + Quote::Double => format!(r#"Use triple double quotes `"""`"#), + Quote::Single => format!(r#"Use triple single quotes `'''`"#), + } } } /// D300 pub(crate) fn triple_quotes(checker: &mut Checker, docstring: &Docstring) { - let body = docstring.body(); + let leading_quote = docstring.leading_quote(); - let leading_quote = docstring.leading_quote().to_ascii_lowercase(); - - let starts_with_triple = if body.contains("\"\"\"") { - matches!(leading_quote.as_str(), "'''" | "u'''" | "r'''" | "ur'''") + let expected_quote = if docstring.body().contains("\"\"\"") { + Quote::Single } else { - matches!( - leading_quote.as_str(), - "\"\"\"" | "u\"\"\"" | "r\"\"\"" | "ur\"\"\"" - ) + Quote::Double }; - if !starts_with_triple { - checker - .diagnostics - .push(Diagnostic::new(TripleSingleQuotes, docstring.range())); + + match expected_quote { + Quote::Single => { + if !leading_quote.ends_with("'''") { + checker.diagnostics.push(Diagnostic::new( + TripleSingleQuotes { expected_quote }, + docstring.range(), + )); + } + } + Quote::Double => { + if !leading_quote.ends_with("\"\"\"") { + checker.diagnostics.push(Diagnostic::new( + TripleSingleQuotes { expected_quote }, + docstring.range(), + )); + } + } } } From dce6a046b09570ab121e254ec6845b4fd1fe48ce Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 25 Jun 2023 22:42:12 -0400 Subject: [PATCH 227/447] Add tests for escape-sequence-in-docstring (#5362) ## Summary Looks like I added a regression in #5360. This PR fixes it and adds dedicated tests to avoid it in the future. --- .../test/fixtures/pydocstyle/D301.py | 29 +++++++++++++++++++ crates/ruff/src/rules/pydocstyle/mod.rs | 1 + .../src/rules/pydocstyle/rules/backslashes.rs | 6 ++-- ...ules__pydocstyle__tests__D301_D301.py.snap | 18 ++++++++++++ 4 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/pydocstyle/D301.py create mode 100644 crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D301_D301.py.snap diff --git a/crates/ruff/resources/test/fixtures/pydocstyle/D301.py b/crates/ruff/resources/test/fixtures/pydocstyle/D301.py new file mode 100644 index 0000000000..1e6c8eef07 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pydocstyle/D301.py @@ -0,0 +1,29 @@ +def double_quotes_backslash(): + """Sum\\mary.""" + + +def double_quotes_backslash_raw(): + r"""Sum\mary.""" + + +def double_quotes_backslash_uppercase(): + R"""Sum\\mary.""" + + +def make_unique_pod_id(pod_id: str) -> str | None: + r""" + Generate a unique Pod name. + + Kubernetes pod names must consist of one or more lowercase + rfc1035/rfc1123 labels separated by '.' with a maximum length of 253 + characters. + + Name must pass the following regex for validation + ``^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`` + + For more details, see: + https://github.com/kubernetes/kubernetes/blob/release-1.1/docs/design/identifiers.md + + :param pod_id: requested pod name + :return: ``str`` valid Pod name of appropriate length + """ diff --git a/crates/ruff/src/rules/pydocstyle/mod.rs b/crates/ruff/src/rules/pydocstyle/mod.rs index 9dc0e45fa5..ca0035e418 100644 --- a/crates/ruff/src/rules/pydocstyle/mod.rs +++ b/crates/ruff/src/rules/pydocstyle/mod.rs @@ -82,6 +82,7 @@ mod tests { #[test_case(Rule::SectionUnderlineNotOverIndented, Path::new("sections.py"))] #[test_case(Rule::OverloadWithDocstring, Path::new("D.py"))] #[test_case(Rule::EscapeSequenceInDocstring, Path::new("D.py"))] + #[test_case(Rule::EscapeSequenceInDocstring, Path::new("D301.py"))] #[test_case(Rule::TripleSingleQuotes, Path::new("D.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); diff --git a/crates/ruff/src/rules/pydocstyle/rules/backslashes.rs b/crates/ruff/src/rules/pydocstyle/rules/backslashes.rs index 9bc972a4e5..88cbdf2b58 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/backslashes.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/backslashes.rs @@ -54,14 +54,14 @@ impl Violation for EscapeSequenceInDocstring { /// D301 pub(crate) fn backslashes(checker: &mut Checker, docstring: &Docstring) { - let body = docstring.body(); - // Docstring is already raw. - if body.starts_with('r') || body.starts_with("ur") { + let contents = docstring.contents; + if contents.starts_with('r') || contents.starts_with("ur") { return; } // Docstring contains at least one backslash. + let body = docstring.body(); let bytes = body.as_bytes(); if memchr_iter(b'\\', bytes).any(|position| { let escaped_char = bytes.get(position.saturating_add(1)); diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D301_D301.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D301_D301.py.snap new file mode 100644 index 0000000000..9f8919505c --- /dev/null +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D301_D301.py.snap @@ -0,0 +1,18 @@ +--- +source: crates/ruff/src/rules/pydocstyle/mod.rs +--- +D301.py:2:5: D301 Use `r"""` if any backslashes in a docstring + | +1 | def double_quotes_backslash(): +2 | """Sum\\mary.""" + | ^^^^^^^^^^^^^^^^ D301 + | + +D301.py:10:5: D301 Use `r"""` if any backslashes in a docstring + | + 9 | def double_quotes_backslash_uppercase(): +10 | R"""Sum\\mary.""" + | ^^^^^^^^^^^^^^^^^ D301 + | + + From 8879927b9a339e92a3d00ed0df1627eaa070e541 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 26 Jun 2023 10:46:18 +0200 Subject: [PATCH 228/447] Use `insta::glob` instead of `fixture` macro (#5364) --- .pre-commit-config.yaml | 2 +- Cargo.lock | 13 +- Cargo.toml | 2 +- crates/ruff_python_formatter/Cargo.toml | 4 +- crates/ruff_python_formatter/src/lib.rs | 177 +------- .../ruff_python_formatter/tests/fixtures.rs | 187 ++++++++ ...tribute_access_on_number_literals.py.snap} | 3 +- ...compatibility@beginning_backslash.py.snap} | 3 +- .../black_compatibility@bracketmatch.py.snap} | 3 +- ...patibility@class_methods_new_line.py.snap} | 3 +- .../black_compatibility@collections.py.snap} | 3 +- ...ity@comment_after_escaped_newline.py.snap} | 3 +- .../black_compatibility@comments.py.snap} | 3 +- .../black_compatibility@comments2.py.snap} | 3 +- .../black_compatibility@comments3.py.snap} | 3 +- .../black_compatibility@comments4.py.snap} | 3 +- .../black_compatibility@comments5.py.snap} | 3 +- .../black_compatibility@comments6.py.snap} | 3 +- .../black_compatibility@comments9.py.snap} | 3 +- ...ility@comments_non_breaking_space.py.snap} | 3 +- .../black_compatibility@composition.py.snap} | 3 +- ...ity@composition_no_trailing_comma.py.snap} | 3 +- .../black_compatibility@docstring.py.snap} | 3 +- ...k_compatibility@docstring_preview.py.snap} | 3 +- .../black_compatibility@empty_lines.py.snap} | 3 +- .../black_compatibility@expression.py.snap} | 3 +- .../black_compatibility@fmtonoff.py.snap} | 3 +- .../black_compatibility@fmtonoff2.py.snap} | 3 +- .../black_compatibility@fmtonoff3.py.snap} | 3 +- .../black_compatibility@fmtonoff4.py.snap} | 3 +- .../black_compatibility@fmtonoff5.py.snap} | 3 +- .../black_compatibility@fmtskip.py.snap} | 3 +- .../black_compatibility@fmtskip2.py.snap} | 3 +- .../black_compatibility@fmtskip3.py.snap} | 3 +- .../black_compatibility@fmtskip4.py.snap} | 3 +- .../black_compatibility@fmtskip5.py.snap} | 3 +- .../black_compatibility@fmtskip6.py.snap} | 3 +- .../black_compatibility@fmtskip7.py.snap} | 3 +- .../black_compatibility@fmtskip8.py.snap} | 3 +- .../black_compatibility@fstring.py.snap} | 3 +- .../black_compatibility@function.py.snap} | 3 +- .../black_compatibility@function2.py.snap} | 3 +- ...atibility@function_trailing_comma.py.snap} | 3 +- ...lack_compatibility@import_spacing.py.snap} | 3 +- ...mpatibility@one_element_subscript.py.snap} | 3 +- ...ck_compatibility@power_op_spacing.py.snap} | 3 +- ...lity@prefer_rhs_split_reformatted.py.snap} | 3 +- ...compatibility@remove_await_parens.py.snap} | 3 +- ...ompatibility@remove_except_parens.py.snap} | 3 +- ...compatibility@remove_for_brackets.py.snap} | 3 +- ...ove_newline_after_code_block_open.py.snap} | 3 +- ...black_compatibility@remove_parens.py.snap} | 3 +- ...bility@return_annotation_brackets.py.snap} | 3 +- ...ibility@skip_magic_trailing_comma.py.snap} | 3 +- .../black_compatibility@slices.py.snap} | 3 +- ...ack_compatibility@string_prefixes.py.snap} | 3 +- .../black_compatibility@torture.py.snap} | 3 +- ...y@trailing_comma_optional_parens1.py.snap} | 3 +- ...y@trailing_comma_optional_parens2.py.snap} | 3 +- ...y@trailing_comma_optional_parens3.py.snap} | 3 +- ...@trailing_commas_in_leading_parts.py.snap} | 3 +- .../black_compatibility@tupleassign.py.snap} | 3 +- .../format@expression__attribute.py.snap} | 4 +- .../format@expression__binary.py.snap} | 4 +- ...mat@expression__boolean_operation.py.snap} | 4 +- .../format@expression__compare.py.snap} | 4 +- .../format@expression__dict.py.snap} | 4 +- .../format@expression__list.py.snap} | 4 +- .../format@expression__slice.py.snap} | 4 +- .../format@expression__string.py.snap} | 4 +- .../format@expression__tuple.py.snap} | 4 +- .../format@expression__unary.py.snap} | 4 +- .../format@statement__assign.py.snap} | 4 +- .../format@statement__break.py.snap} | 4 +- ...ormat@statement__class_definition.py.snap} | 4 +- .../snapshots/format@statement__for.py.snap} | 4 +- .../format@statement__function.py.snap} | 4 +- .../snapshots/format@statement__if.py.snap} | 4 +- .../format@statement__while.py.snap} | 4 +- .../snapshots/format@trivia.py.snap} | 4 +- crates/ruff_testing_macros/Cargo.toml | 22 - crates/ruff_testing_macros/src/lib.rs | 403 ------------------ 82 files changed, 285 insertions(+), 765 deletions(-) create mode 100644 crates/ruff_python_formatter/tests/fixtures.rs rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap => tests/snapshots/black_compatibility@attribute_access_on_number_literals.py.snap} (97%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__beginning_backslash_py.snap => tests/snapshots/black_compatibility@beginning_backslash.py.snap} (85%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__bracketmatch_py.snap => tests/snapshots/black_compatibility@bracketmatch.py.snap} (92%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap => tests/snapshots/black_compatibility@class_methods_new_line.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap => tests/snapshots/black_compatibility@collections.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__comment_after_escaped_newline_py.snap => tests/snapshots/black_compatibility@comment_after_escaped_newline.py.snap} (91%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap => tests/snapshots/black_compatibility@comments.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap => tests/snapshots/black_compatibility@comments2.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap => tests/snapshots/black_compatibility@comments3.py.snap} (98%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap => tests/snapshots/black_compatibility@comments4.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap => tests/snapshots/black_compatibility@comments5.py.snap} (98%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap => tests/snapshots/black_compatibility@comments6.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap => tests/snapshots/black_compatibility@comments9.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap => tests/snapshots/black_compatibility@comments_non_breaking_space.py.snap} (97%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__composition_py.snap => tests/snapshots/black_compatibility@composition.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__composition_no_trailing_comma_py.snap => tests/snapshots/black_compatibility@composition_no_trailing_comma.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap => tests/snapshots/black_compatibility@docstring.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap => tests/snapshots/black_compatibility@docstring_preview.py.snap} (98%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap => tests/snapshots/black_compatibility@empty_lines.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap => tests/snapshots/black_compatibility@expression.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap => tests/snapshots/black_compatibility@fmtonoff.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap => tests/snapshots/black_compatibility@fmtonoff2.py.snap} (97%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff3_py.snap => tests/snapshots/black_compatibility@fmtonoff3.py.snap} (91%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff4_py.snap => tests/snapshots/black_compatibility@fmtonoff4.py.snap} (93%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap => tests/snapshots/black_compatibility@fmtonoff5.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__fmtskip_py.snap => tests/snapshots/black_compatibility@fmtskip.py.snap} (86%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__fmtskip2_py.snap => tests/snapshots/black_compatibility@fmtskip2.py.snap} (95%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__fmtskip3_py.snap => tests/snapshots/black_compatibility@fmtskip3.py.snap} (92%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__fmtskip4_py.snap => tests/snapshots/black_compatibility@fmtskip4.py.snap} (86%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap => tests/snapshots/black_compatibility@fmtskip5.py.snap} (93%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__fmtskip6_py.snap => tests/snapshots/black_compatibility@fmtskip6.py.snap} (92%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__fmtskip7_py.snap => tests/snapshots/black_compatibility@fmtskip7.py.snap} (93%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap => tests/snapshots/black_compatibility@fmtskip8.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__fstring_py.snap => tests/snapshots/black_compatibility@fstring.py.snap} (96%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap => tests/snapshots/black_compatibility@function.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap => tests/snapshots/black_compatibility@function2.py.snap} (98%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap => tests/snapshots/black_compatibility@function_trailing_comma.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap => tests/snapshots/black_compatibility@import_spacing.py.snap} (98%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__one_element_subscript_py.snap => tests/snapshots/black_compatibility@one_element_subscript.py.snap} (96%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap => tests/snapshots/black_compatibility@power_op_spacing.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__prefer_rhs_split_reformatted_py.snap => tests/snapshots/black_compatibility@prefer_rhs_split_reformatted.py.snap} (97%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap => tests/snapshots/black_compatibility@remove_await_parens.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__remove_except_parens_py.snap => tests/snapshots/black_compatibility@remove_except_parens.py.snap} (98%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap => tests/snapshots/black_compatibility@remove_for_brackets.py.snap} (98%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap => tests/snapshots/black_compatibility@remove_newline_after_code_block_open.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap => tests/snapshots/black_compatibility@remove_parens.py.snap} (98%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__return_annotation_brackets_py.snap => tests/snapshots/black_compatibility@return_annotation_brackets.py.snap} (99%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap => tests/snapshots/black_compatibility@skip_magic_trailing_comma.py.snap} (98%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap => tests/snapshots/black_compatibility@slices.py.snap} (98%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap => tests/snapshots/black_compatibility@string_prefixes.py.snap} (97%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap => tests/snapshots/black_compatibility@torture.py.snap} (98%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap => tests/snapshots/black_compatibility@trailing_comma_optional_parens1.py.snap} (98%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens2_py.snap => tests/snapshots/black_compatibility@trailing_comma_optional_parens2.py.snap} (92%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens3_py.snap => tests/snapshots/black_compatibility@trailing_comma_optional_parens3.py.snap} (96%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap => tests/snapshots/black_compatibility@trailing_commas_in_leading_parts.py.snap} (98%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap => tests/snapshots/black_compatibility@tupleassign.py.snap} (95%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__ruff_test__expression__attribute_py.snap => tests/snapshots/format@expression__attribute.py.snap} (87%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap => tests/snapshots/format@expression__binary.py.snap} (98%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__ruff_test__expression__boolean_operation_py.snap => tests/snapshots/format@expression__boolean_operation.py.snap} (94%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__ruff_test__expression__compare_py.snap => tests/snapshots/format@expression__compare.py.snap} (95%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__ruff_test__expression__dict_py.snap => tests/snapshots/format@expression__dict.py.snap} (92%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__ruff_test__expression__list_py.snap => tests/snapshots/format@expression__list.py.snap} (79%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__ruff_test__expression__slice_py.snap => tests/snapshots/format@expression__slice.py.snap} (93%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__ruff_test__expression__string_py.snap => tests/snapshots/format@expression__string.py.snap} (90%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__ruff_test__expression__tuple_py.snap => tests/snapshots/format@expression__tuple.py.snap} (98%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__ruff_test__expression__unary_py.snap => tests/snapshots/format@expression__unary.py.snap} (97%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__ruff_test__statement__assign_py.snap => tests/snapshots/format@statement__assign.py.snap} (84%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__ruff_test__statement__break_py.snap => tests/snapshots/format@statement__break.py.snap} (66%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__ruff_test__statement__class_definition_py.snap => tests/snapshots/format@statement__class_definition.py.snap} (90%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__ruff_test__statement__for_py.snap => tests/snapshots/format@statement__for.py.snap} (91%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__ruff_test__statement__function_py.snap => tests/snapshots/format@statement__function.py.snap} (98%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap => tests/snapshots/format@statement__if.py.snap} (95%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__ruff_test__statement__while_py.snap => tests/snapshots/format@statement__while.py.snap} (91%) rename crates/ruff_python_formatter/{src/snapshots/ruff_python_formatter__tests__ruff_test__trivia_py.snap => tests/snapshots/format@trivia.py.snap} (87%) delete mode 100644 crates/ruff_testing_macros/Cargo.toml delete mode 100644 crates/ruff_testing_macros/src/lib.rs diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3331ffbd7f..a33c8353c7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ exclude: | crates/ruff/src/rules/.*/snapshots/.*| crates/ruff_cli/resources/.*| crates/ruff_python_formatter/resources/.*| - crates/ruff_python_formatter/src/snapshots/.* + crates/ruff_python_formatter/tests/snapshots/.* )$ repos: diff --git a/Cargo.lock b/Cargo.lock index 1645b94509..91d79253be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -988,9 +988,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28491f7753051e5704d4d0ae7860d45fae3238d7d235bc4289dcd45c48d3cec3" dependencies = [ "console", + "globset", "lazy_static", "linked-hash-map", "similar", + "walkdir", "yaml-rust", ] @@ -2060,7 +2062,6 @@ dependencies = [ "ruff_formatter", "ruff_python_ast", "ruff_python_whitespace", - "ruff_testing_macros", "ruff_text_size", "rustc-hash", "rustpython-parser", @@ -2105,16 +2106,6 @@ dependencies = [ "rustpython-parser", ] -[[package]] -name = "ruff_testing_macros" -version = "0.0.0" -dependencies = [ - "glob", - "proc-macro2", - "quote", - "syn 2.0.22", -] - [[package]] name = "ruff_text_size" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 4066cb9c1e..047c7994ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ filetime = { version = "0.2.20" } glob = { version = "0.3.1" } globset = { version = "0.4.10" } ignore = { version = "0.4.20" } -insta = { version = "1.28.0" } +insta = { version = "1.30.0" } is-macro = { version = "0.2.2" } itertools = { version = "0.10.5" } log = { version = "0.4.17" } diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index 0701907975..91ba277792 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -27,8 +27,6 @@ rustc-hash = { workspace = true } rustpython-parser = { workspace = true } [dev-dependencies] -ruff_testing_macros = { path = "../ruff_testing_macros" } - -insta = { workspace = true, features = [] } +insta = { workspace = true, features = ["glob"] } test-case = { workspace = true } similar = { workspace = true } diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 790b08faa9..93197b2ca2 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -263,18 +263,12 @@ impl TryFrom for QuoteStyle { #[cfg(test)] mod tests { + use crate::{format_module, format_node}; use anyhow::Result; use insta::assert_snapshot; use ruff_python_ast::source_code::CommentRangesBuilder; - use ruff_testing_macros::fixture; use rustpython_parser::lexer::lex; use rustpython_parser::{parse_tokens, Mode}; - use similar::TextDiff; - use std::fmt::{Formatter, Write}; - use std::fs; - use std::path::Path; - - use crate::{format_module, format_node}; /// Very basic test intentionally kept very similar to the CLI #[test] @@ -295,138 +289,6 @@ if True: Ok(()) } - #[fixture(pattern = "resources/test/fixtures/black/**/*.py")] - #[test] - fn black_test(input_path: &Path) -> Result<()> { - let content = fs::read_to_string(input_path)?; - - let printed = format_module(&content)?; - - let expected_path = input_path.with_extension("py.expect"); - let expected_output = fs::read_to_string(&expected_path) - .unwrap_or_else(|_| panic!("Expected Black output file '{expected_path:?}' to exist")); - - let formatted_code = printed.as_code(); - - ensure_stability_when_formatting_twice(formatted_code); - - if formatted_code == expected_output { - // Black and Ruff formatting matches. Delete any existing snapshot files because the Black output - // already perfectly captures the expected output. - // The following code mimics insta's logic generating the snapshot name for a test. - let workspace_path = std::env::var("CARGO_MANIFEST_DIR").unwrap(); - let snapshot_name = insta::_function_name!() - .strip_prefix(&format!("{}::", module_path!())) - .unwrap(); - let module_path = module_path!().replace("::", "__"); - - let snapshot_path = Path::new(&workspace_path) - .join("src/snapshots") - .join(format!( - "{module_path}__{}.snap", - snapshot_name.replace(&['/', '\\'][..], "__") - )); - - if snapshot_path.exists() && snapshot_path.is_file() { - // SAFETY: This is a convenience feature. That's why we don't want to abort - // when deleting a no longer needed snapshot fails. - fs::remove_file(&snapshot_path).ok(); - } - - let new_snapshot_path = snapshot_path.with_extension("snap.new"); - if new_snapshot_path.exists() && new_snapshot_path.is_file() { - // SAFETY: This is a convenience feature. That's why we don't want to abort - // when deleting a no longer needed snapshot fails. - fs::remove_file(&new_snapshot_path).ok(); - } - } else { - // Black and Ruff have different formatting. Write out a snapshot that covers the differences - // today. - let mut snapshot = String::new(); - write!(snapshot, "{}", Header::new("Input"))?; - write!(snapshot, "{}", CodeFrame::new("py", &content))?; - - write!(snapshot, "{}", Header::new("Black Differences"))?; - - let diff = TextDiff::from_lines(expected_output.as_str(), formatted_code) - .unified_diff() - .header("Black", "Ruff") - .to_string(); - - write!(snapshot, "{}", CodeFrame::new("diff", &diff))?; - - write!(snapshot, "{}", Header::new("Ruff Output"))?; - write!(snapshot, "{}", CodeFrame::new("py", formatted_code))?; - - write!(snapshot, "{}", Header::new("Black Output"))?; - write!(snapshot, "{}", CodeFrame::new("py", &expected_output))?; - - insta::with_settings!({ omit_expression => false, input_file => input_path }, { - insta::assert_snapshot!(snapshot); - }); - } - - Ok(()) - } - - #[fixture(pattern = "resources/test/fixtures/ruff/**/*.py")] - #[test] - fn ruff_test(input_path: &Path) -> Result<()> { - let content = fs::read_to_string(input_path)?; - - let printed = format_module(&content)?; - let formatted_code = printed.as_code(); - - ensure_stability_when_formatting_twice(formatted_code); - - let snapshot = format!( - r#"## Input -{} - -## Output -{}"#, - CodeFrame::new("py", &content), - CodeFrame::new("py", formatted_code) - ); - assert_snapshot!(snapshot); - - Ok(()) - } - - /// Format another time and make sure that there are no changes anymore - fn ensure_stability_when_formatting_twice(formatted_code: &str) { - let reformatted = match format_module(formatted_code) { - Ok(reformatted) => reformatted, - Err(err) => { - panic!( - "Expected formatted code to be valid syntax: {err}:\ - \n---\n{formatted_code}---\n", - ); - } - }; - - if reformatted.as_code() != formatted_code { - let diff = TextDiff::from_lines(formatted_code, reformatted.as_code()) - .unified_diff() - .header("Formatted once", "Formatted twice") - .to_string(); - panic!( - r#"Reformatting the formatted code a second time resulted in formatting changes. ---- -{diff}--- - -Formatted once: ---- -{formatted_code}--- - -Formatted twice: ---- -{}---"#, - reformatted.as_code() - ); - } - } - /// Use this test to debug the formatting of some snipped #[ignore] #[test] @@ -549,41 +411,4 @@ if [ assert_snapshot!(output.print().expect("Printing to succeed").as_code()); } - - struct Header<'a> { - title: &'a str, - } - - impl<'a> Header<'a> { - fn new(title: &'a str) -> Self { - Self { title } - } - } - - impl std::fmt::Display for Header<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "## {}", self.title)?; - writeln!(f) - } - } - - struct CodeFrame<'a> { - language: &'a str, - code: &'a str, - } - - impl<'a> CodeFrame<'a> { - fn new(language: &'a str, code: &'a str) -> Self { - Self { language, code } - } - } - - impl std::fmt::Display for CodeFrame<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "```{}", self.language)?; - write!(f, "{}", self.code)?; - writeln!(f, "```")?; - writeln!(f) - } - } } diff --git a/crates/ruff_python_formatter/tests/fixtures.rs b/crates/ruff_python_formatter/tests/fixtures.rs new file mode 100644 index 0000000000..edfc112a86 --- /dev/null +++ b/crates/ruff_python_formatter/tests/fixtures.rs @@ -0,0 +1,187 @@ +use ruff_python_formatter::format_module; +use similar::TextDiff; +use std::fmt::{Formatter, Write}; +use std::fs; +use std::path::Path; + +#[test] +fn black_compatibility() { + let test_file = |input_path: &Path| { + let content = fs::read_to_string(input_path).unwrap(); + + let printed = format_module(&content).expect("Formatting to succeed"); + + let expected_path = input_path.with_extension("py.expect"); + let expected_output = fs::read_to_string(&expected_path) + .unwrap_or_else(|_| panic!("Expected Black output file '{expected_path:?}' to exist")); + + let formatted_code = printed.as_code(); + + ensure_stability_when_formatting_twice(formatted_code); + + if formatted_code == expected_output { + // Black and Ruff formatting matches. Delete any existing snapshot files because the Black output + // already perfectly captures the expected output. + // The following code mimics insta's logic generating the snapshot name for a test. + let workspace_path = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let snapshot_name = insta::_function_name!() + .strip_prefix(&format!("{}::", module_path!())) + .unwrap(); + let module_path = module_path!().replace("::", "__"); + + let snapshot_path = Path::new(&workspace_path) + .join("src/snapshots") + .join(format!( + "{module_path}__{}.snap", + snapshot_name.replace(&['/', '\\'][..], "__") + )); + + if snapshot_path.exists() && snapshot_path.is_file() { + // SAFETY: This is a convenience feature. That's why we don't want to abort + // when deleting a no longer needed snapshot fails. + fs::remove_file(&snapshot_path).ok(); + } + + let new_snapshot_path = snapshot_path.with_extension("snap.new"); + if new_snapshot_path.exists() && new_snapshot_path.is_file() { + // SAFETY: This is a convenience feature. That's why we don't want to abort + // when deleting a no longer needed snapshot fails. + fs::remove_file(&new_snapshot_path).ok(); + } + } else { + // Black and Ruff have different formatting. Write out a snapshot that covers the differences + // today. + let mut snapshot = String::new(); + write!(snapshot, "{}", Header::new("Input")).unwrap(); + write!(snapshot, "{}", CodeFrame::new("py", &content)).unwrap(); + + write!(snapshot, "{}", Header::new("Black Differences")).unwrap(); + + let diff = TextDiff::from_lines(expected_output.as_str(), formatted_code) + .unified_diff() + .header("Black", "Ruff") + .to_string(); + + write!(snapshot, "{}", CodeFrame::new("diff", &diff)).unwrap(); + + write!(snapshot, "{}", Header::new("Ruff Output")).unwrap(); + write!(snapshot, "{}", CodeFrame::new("py", formatted_code)).unwrap(); + + write!(snapshot, "{}", Header::new("Black Output")).unwrap(); + write!(snapshot, "{}", CodeFrame::new("py", &expected_output)).unwrap(); + + insta::with_settings!({ + omit_expression => true, + input_file => input_path, + prepend_module_to_snapshot => false, + }, { + insta::assert_snapshot!(snapshot); + }); + } + }; + + insta::glob!("../resources", "test/fixtures/black/**/*.py", test_file); +} + +#[test] +fn format() { + let test_file = |input_path: &Path| { + let content = fs::read_to_string(input_path).unwrap(); + + let printed = format_module(&content).expect("Formatting to succeed"); + let formatted_code = printed.as_code(); + + ensure_stability_when_formatting_twice(formatted_code); + + let snapshot = format!( + r#"## Input +{} + +## Output +{}"#, + CodeFrame::new("py", &content), + CodeFrame::new("py", formatted_code) + ); + + insta::with_settings!({ + omit_expression => true, + input_file => input_path, + prepend_module_to_snapshot => false, + }, { + insta::assert_snapshot!(snapshot); + }); + }; + + insta::glob!("../resources", "test/fixtures/ruff/**/*.py", test_file); +} + +/// Format another time and make sure that there are no changes anymore +fn ensure_stability_when_formatting_twice(formatted_code: &str) { + let reformatted = match format_module(formatted_code) { + Ok(reformatted) => reformatted, + Err(err) => { + panic!( + "Expected formatted code to be valid syntax: {err}:\ + \n---\n{formatted_code}---\n", + ); + } + }; + + if reformatted.as_code() != formatted_code { + let diff = TextDiff::from_lines(formatted_code, reformatted.as_code()) + .unified_diff() + .header("Formatted once", "Formatted twice") + .to_string(); + panic!( + r#"Reformatting the formatted code a second time resulted in formatting changes. +--- +{diff}--- + +Formatted once: +--- +{formatted_code}--- + +Formatted twice: +--- +{}---"#, + reformatted.as_code() + ); + } +} + +struct Header<'a> { + title: &'a str, +} + +impl<'a> Header<'a> { + fn new(title: &'a str) -> Self { + Self { title } + } +} + +impl std::fmt::Display for Header<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "## {}", self.title)?; + writeln!(f) + } +} + +struct CodeFrame<'a> { + language: &'a str, + code: &'a str, +} + +impl<'a> CodeFrame<'a> { + fn new(language: &'a str, code: &'a str) -> Self { + Self { language, code } + } +} + +impl std::fmt::Display for CodeFrame<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "```{}", self.language)?; + write!(f, "{}", self.code)?; + writeln!(f, "```")?; + writeln!(f) + } +} diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@attribute_access_on_number_literals.py.snap similarity index 97% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@attribute_access_on_number_literals.py.snap index 62da55712c..cfa4c7cd05 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@attribute_access_on_number_literals.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/attribute_access_on_number_literals.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__beginning_backslash_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@beginning_backslash.py.snap similarity index 85% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__beginning_backslash_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@beginning_backslash.py.snap index 6b56a193c0..1bd0c3f921 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__beginning_backslash_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@beginning_backslash.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/beginning_backslash.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__bracketmatch_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@bracketmatch.py.snap similarity index 92% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__bracketmatch_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@bracketmatch.py.snap index 548945997c..ae89b4e872 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__bracketmatch_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@bracketmatch.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/bracketmatch.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@class_methods_new_line.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@class_methods_new_line.py.snap index d519958e1a..436b6923ff 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@class_methods_new_line.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/class_methods_new_line.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@collections.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@collections.py.snap index 1c961a36e7..6e6ec5275c 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@collections.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/collections.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comment_after_escaped_newline_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comment_after_escaped_newline.py.snap similarity index 91% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comment_after_escaped_newline_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@comment_after_escaped_newline.py.snap index 92e670eafa..5a209c5759 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comment_after_escaped_newline_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comment_after_escaped_newline.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comment_after_escaped_newline.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap index cb44cb8f7f..1317f43548 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments2.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments2.py.snap index 07c15327b0..2778d433ad 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments2.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments2.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments3.py.snap similarity index 98% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments3.py.snap index 73e02a1e27..5bd61dca47 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments3.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments3.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments4.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments4.py.snap index 620587b2e6..7b3c1751cc 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments4.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments4.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments5.py.snap similarity index 98% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments5.py.snap index 51320a8e86..269d9cdd25 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments5.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments5.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments6.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments6.py.snap index 86c3b34f2d..120d15c6fe 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments6.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments6.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments9.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments9.py.snap index 8ae0c972f4..b6b4ce39f0 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments9.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments9.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments_non_breaking_space.py.snap similarity index 97% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments_non_breaking_space.py.snap index 6ff36ce6c3..cae2d7d069 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments_non_breaking_space.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments_non_breaking_space.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition.py.snap index de890ccedd..25490c1887 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/composition.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_no_trailing_comma_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition_no_trailing_comma.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_no_trailing_comma_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition_no_trailing_comma.py.snap index f47fb08de7..30953137ad 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_no_trailing_comma_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition_no_trailing_comma.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/composition_no_trailing_comma.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@docstring.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@docstring.py.snap index da177f0353..60a50df3a5 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@docstring.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/docstring.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@docstring_preview.py.snap similarity index 98% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@docstring_preview.py.snap index 296bb4715d..3101537b9d 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@docstring_preview.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/docstring_preview.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@empty_lines.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@empty_lines.py.snap index 874c26f461..b8e1bf9f55 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@empty_lines.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/empty_lines.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap index 1499731809..47e1d61f92 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/expression.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap index 3bf9990399..baf9eaa6af 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff2.py.snap similarity index 97% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff2.py.snap index 076c32ce50..548b61e58d 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff2.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff2.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff3_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff3.py.snap similarity index 91% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff3_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff3.py.snap index 1d24dd47e2..9dacee57d5 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff3_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff3.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff3.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff4_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff4.py.snap similarity index 93% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff4_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff4.py.snap index 9b5d673289..63a650ff45 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff4_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff4.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff4.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff5.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff5.py.snap index 55baa4ec3a..3bbc9eaea4 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff5.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff5.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip.py.snap similarity index 86% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip.py.snap index c4deeaed9c..3c9f58710b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip2_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip2.py.snap similarity index 95% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip2_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip2.py.snap index 13bf3277f2..487290c418 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip2_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip2.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip2.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip3_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip3.py.snap similarity index 92% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip3_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip3.py.snap index 2e54d4b721..002454e8da 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip3_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip3.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip3.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip4_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip4.py.snap similarity index 86% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip4_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip4.py.snap index 6d4c587187..93b2d701b1 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip4_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip4.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip4.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip5.py.snap similarity index 93% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip5.py.snap index 32b6d86155..156c1d3712 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip5.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip5.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip6_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip6.py.snap similarity index 92% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip6_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip6.py.snap index 5a73724ef4..5c22c86651 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip6_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip6.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip6.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip7_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip7.py.snap similarity index 93% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip7_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip7.py.snap index 4eb0c61744..cd13d57248 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip7_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip7.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip7.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap index 408c2b92e9..8e54712280 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip8.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fstring_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fstring.py.snap similarity index 96% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fstring_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@fstring.py.snap index d442a2cf0f..852778fb08 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fstring_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fstring.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@function.py.snap index da23fa784a..2be379cb33 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function2.py.snap similarity index 98% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@function2.py.snap index 2a6e53604d..d70e4ac234 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function2.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function2.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function_trailing_comma.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@function_trailing_comma.py.snap index e117c52399..e98815a64f 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function_trailing_comma.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function_trailing_comma.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@import_spacing.py.snap similarity index 98% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@import_spacing.py.snap index 30c49898e4..6b2e7df8f3 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@import_spacing.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/import_spacing.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__one_element_subscript_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@one_element_subscript.py.snap similarity index 96% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__one_element_subscript_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@one_element_subscript.py.snap index 66e37c3bad..68bddbd301 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__one_element_subscript_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@one_element_subscript.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/one_element_subscript.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@power_op_spacing.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@power_op_spacing.py.snap index 693a6f7001..ea7c78d2a9 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@power_op_spacing.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/power_op_spacing.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__prefer_rhs_split_reformatted_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@prefer_rhs_split_reformatted.py.snap similarity index 97% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__prefer_rhs_split_reformatted_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@prefer_rhs_split_reformatted.py.snap index db5e55f09b..855222248f 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__prefer_rhs_split_reformatted_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@prefer_rhs_split_reformatted.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/prefer_rhs_split_reformatted.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_await_parens.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_await_parens.py.snap index b13ce8545b..d702a291b9 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_await_parens.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/remove_await_parens.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_except_parens_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_except_parens.py.snap similarity index 98% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_except_parens_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_except_parens.py.snap index 680584a6d7..a845bf4af2 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_except_parens_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_except_parens.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/remove_except_parens.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_for_brackets.py.snap similarity index 98% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_for_brackets.py.snap index ab7e8ada6d..87a792c38b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_for_brackets.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/remove_for_brackets.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_newline_after_code_block_open.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_newline_after_code_block_open.py.snap index 1bf0525ecc..05400a9c8d 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_newline_after_code_block_open.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/remove_newline_after_code_block_open.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_parens.py.snap similarity index 98% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_parens.py.snap index c9d1141cbf..1ed5be6a16 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_parens.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/remove_parens.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__return_annotation_brackets_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@return_annotation_brackets.py.snap similarity index 99% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__return_annotation_brackets_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@return_annotation_brackets.py.snap index db50309690..664ee153eb 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__return_annotation_brackets_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@return_annotation_brackets.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/return_annotation_brackets.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@skip_magic_trailing_comma.py.snap similarity index 98% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@skip_magic_trailing_comma.py.snap index 227fddc38c..4fccb92505 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@skip_magic_trailing_comma.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/skip_magic_trailing_comma.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@slices.py.snap similarity index 98% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@slices.py.snap index 914dc2e5fd..ec47eb18f9 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@slices.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@string_prefixes.py.snap similarity index 97% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@string_prefixes.py.snap index 1af332dee3..93c5a7f651 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@string_prefixes.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/string_prefixes.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@torture.py.snap similarity index 98% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@torture.py.snap index b270fd1801..ec5eeae618 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@torture.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/torture.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens1.py.snap similarity index 98% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens1.py.snap index 70f66ad05c..177fde22db 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens1.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_comma_optional_parens1.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens2_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens2.py.snap similarity index 92% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens2_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens2.py.snap index 122f1f4cfb..c4dddaa5b1 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens2_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens2.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_comma_optional_parens2.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens3_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens3.py.snap similarity index 96% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens3_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens3.py.snap index 106c7c2554..3a574ac557 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens3_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens3.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_comma_optional_parens3.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_commas_in_leading_parts.py.snap similarity index 98% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_commas_in_leading_parts.py.snap index b6cdaf967b..e799084c77 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_commas_in_leading_parts.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_commas_in_leading_parts.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@tupleassign.py.snap similarity index 95% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@tupleassign.py.snap index d67608accf..d54c56272e 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@tupleassign.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/tupleassign.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__attribute_py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__attribute.py.snap similarity index 87% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__attribute_py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@expression__attribute.py.snap index 991210a185..82792c7d3c 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__attribute_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__attribute.py.snap @@ -1,6 +1,6 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/attribute.py --- ## Input ```py diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap similarity index 98% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap index f79bc76fa1..a84c4a7201 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap @@ -1,6 +1,6 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py --- ## Input ```py diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__boolean_operation_py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__boolean_operation.py.snap similarity index 94% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__boolean_operation_py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@expression__boolean_operation.py.snap index cfc2bd43fe..0b98b8df25 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__boolean_operation_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__boolean_operation.py.snap @@ -1,6 +1,6 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/boolean_operation.py --- ## Input ```py diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__compare_py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__compare.py.snap similarity index 95% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__compare_py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@expression__compare.py.snap index 7abb3e4b1e..34e1cfbb3f 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__compare_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__compare.py.snap @@ -1,6 +1,6 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/compare.py --- ## Input ```py diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__dict_py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__dict.py.snap similarity index 92% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__dict_py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@expression__dict.py.snap index d59022c544..52a5965a90 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__dict_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__dict.py.snap @@ -1,6 +1,6 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/dict.py --- ## Input ```py diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__list_py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__list.py.snap similarity index 79% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__list_py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@expression__list.py.snap index 6ecef01df1..892d93b62e 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__list_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__list.py.snap @@ -1,6 +1,6 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list.py --- ## Input ```py diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__slice_py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__slice.py.snap similarity index 93% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__slice_py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@expression__slice.py.snap index 80370410e7..e316dd5218 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__slice_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__slice.py.snap @@ -1,6 +1,6 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/slice.py --- ## Input ```py diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__string_py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap similarity index 90% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__string_py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap index d57de901e1..4a2e6ed032 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__string_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap @@ -1,6 +1,6 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py --- ## Input ```py diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__tuple_py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__tuple.py.snap similarity index 98% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__tuple_py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@expression__tuple.py.snap index cce6cccafc..2ddb3cd9ad 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__tuple_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__tuple.py.snap @@ -1,6 +1,6 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tuple.py --- ## Input ```py diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__unary_py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap similarity index 97% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__unary_py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap index 48e8852375..f461d88180 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__unary_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap @@ -1,6 +1,6 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py --- ## Input ```py diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__assign_py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__assign.py.snap similarity index 84% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__assign_py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@statement__assign.py.snap index b4237a40c7..4d02cf905c 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__assign_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__assign.py.snap @@ -1,6 +1,6 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assign.py --- ## Input ```py diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__break_py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__break.py.snap similarity index 66% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__break_py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@statement__break.py.snap index 3087672f75..d48901046e 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__break_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__break.py.snap @@ -1,6 +1,6 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/break.py --- ## Input ```py diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__class_definition_py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap similarity index 90% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__class_definition_py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap index 24adda2166..49aa2a4879 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__class_definition_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap @@ -1,6 +1,6 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py --- ## Input ```py diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__for_py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__for.py.snap similarity index 91% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__for_py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@statement__for.py.snap index b176abbfb0..a240652d9c 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__for_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__for.py.snap @@ -1,6 +1,6 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/for.py --- ## Input ```py diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__function_py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__function.py.snap similarity index 98% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__function_py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@statement__function.py.snap index c2f44bcdba..00c72398e5 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__function_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__function.py.snap @@ -1,6 +1,6 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py --- ## Input ```py diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__if.py.snap similarity index 95% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@statement__if.py.snap index 1c41ddd972..6292de6914 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__if.py.snap @@ -1,6 +1,6 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py --- ## Input ```py diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__while_py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__while.py.snap similarity index 91% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__while_py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@statement__while.py.snap index 785c69cb8e..799424b4ea 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__while_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__while.py.snap @@ -1,6 +1,6 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/while.py --- ## Input ```py diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__trivia_py.snap b/crates/ruff_python_formatter/tests/snapshots/format@trivia.py.snap similarity index 87% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__trivia_py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@trivia.py.snap index 1d1c0586db..773e564e68 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__trivia_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@trivia.py.snap @@ -1,6 +1,6 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/trivia.py --- ## Input ```py diff --git a/crates/ruff_testing_macros/Cargo.toml b/crates/ruff_testing_macros/Cargo.toml deleted file mode 100644 index 49daf16a71..0000000000 --- a/crates/ruff_testing_macros/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "ruff_testing_macros" -version = "0.0.0" -publish = false -authors = { workspace = true } -edition = { workspace = true } -rust-version = { workspace = true } -homepage = { workspace = true } -documentation = { workspace = true } -repository = { workspace = true } -license = { workspace = true } - -[lib] -proc-macro = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -glob = { workspace = true } -proc-macro2 = { workspace = true } -quote = { workspace = true } -syn = { workspace = true, features = ["extra-traits", "full"] } diff --git a/crates/ruff_testing_macros/src/lib.rs b/crates/ruff_testing_macros/src/lib.rs deleted file mode 100644 index 23815540a1..0000000000 --- a/crates/ruff_testing_macros/src/lib.rs +++ /dev/null @@ -1,403 +0,0 @@ -use proc_macro::TokenStream; -use std::borrow::Cow; -use std::collections::BTreeMap; -use std::env; -use std::path::{Component, PathBuf}; - -use glob::{glob, Pattern}; -use proc_macro2::Span; -use quote::{format_ident, quote}; -use syn::parse::{Parse, ParseStream}; -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::{bracketed, parse_macro_input, parse_quote, Error, FnArg, ItemFn, LitStr, Pat, Token}; - -#[derive(Debug)] -struct FixtureConfiguration { - pattern: Pattern, - pattern_span: Span, - exclude: Vec, -} - -struct Arg { - name: syn::Ident, - value: ArgValue, -} - -impl Parse for Arg { - fn parse(input: ParseStream) -> syn::Result { - let name = input.parse()?; - let _equal_token: Token![=] = input.parse()?; - let value = input.parse()?; - - Ok(Self { name, value }) - } -} - -enum ArgValue { - LitStr(LitStr), - List(Punctuated), -} - -impl Parse for ArgValue { - fn parse(input: ParseStream) -> syn::Result { - let value = if input.peek(syn::token::Bracket) { - let inner; - _ = bracketed!(inner in input); - - let values = inner.parse_terminated( - |parser| { - let value: LitStr = parser.parse()?; - Ok(value) - }, - Token![,], - )?; - ArgValue::List(values) - } else { - ArgValue::LitStr(input.parse()?) - }; - - Ok(value) - } -} - -impl Parse for FixtureConfiguration { - fn parse(input: ParseStream) -> syn::Result { - let args: Punctuated<_, Token![,]> = input.parse_terminated(Arg::parse, Token![,])?; - - let mut pattern = None; - let mut exclude = None; - - for arg in args { - match arg.name.to_string().as_str() { - "pattern" => match arg.value { - ArgValue::LitStr(value) => { - pattern = Some(try_parse_pattern(&value)?); - } - ArgValue::List(list) => { - return Err(Error::new( - list.span(), - "The pattern must be a string literal", - )) - } - }, - - "exclude" => { - match arg.value { - ArgValue::LitStr(lit) => return Err(Error::new( - lit.span(), - "The exclude argument must be an array of globs: 'exclude=[\"a.py\"]", - )), - ArgValue::List(list) => { - let mut exclude_patterns = Vec::with_capacity(list.len()); - - for pattern in list { - let (pattern, _) = try_parse_pattern(&pattern)?; - exclude_patterns.push(pattern); - } - - exclude = Some(exclude_patterns); - } - } - } - - _ => { - return Err(Error::new( - arg.name.span(), - format!("Unknown argument {}.", arg.name), - )); - } - } - } - - let exclude = exclude.unwrap_or_default(); - - match pattern { - None => Err(Error::new( - input.span(), - "'fixture' macro must have a pattern attribute", - )), - Some((pattern, pattern_span)) => Ok(Self { - pattern, - pattern_span, - exclude, - }), - } - } -} - -fn try_parse_pattern(pattern_lit: &LitStr) -> syn::Result<(Pattern, Span)> { - let raw_pattern = pattern_lit.value(); - match Pattern::new(&raw_pattern) { - Ok(pattern) => Ok((pattern, pattern_lit.span())), - Err(err) => Err(Error::new( - pattern_lit.span(), - format!("'{raw_pattern}' is not a valid glob pattern: '{}'", err.msg), - )), - } -} - -/// Generates a test for each file that matches the specified pattern. -/// -/// The attributed function must have exactly one argument of the type `&Path`. -/// The `#[test]` attribute must come after the `#[fixture]` argument or `test` will complain -/// that your function can not have any arguments. -/// -/// ## Examples -/// -/// Creates a test for every python file file in the `fixtures` directory. -/// -/// ```ignore -/// #[fixture(pattern="fixtures/*.py")] -/// #[test] -/// fn my_test(path: &Path) -> std::io::Result<()> { -/// // ... implement the test -/// Ok(()) -/// } -/// ``` -/// -/// ### Excluding Files -/// -/// You can exclude files by specifying optional `exclude` patterns. -/// -/// ```ignore -/// #[fixture(pattern="fixtures/*.py", exclude=["a_*.py"])] -/// #[test] -/// fn my_test(path: &Path) -> std::io::Result<()> { -/// // ... implement the test -/// Ok(()) -/// } -/// ``` -/// -/// Creates tests for each python file in the `fixtures` directory except for files matching the `a_*.py` pattern. -#[proc_macro_attribute] -pub fn fixture(attribute: TokenStream, item: TokenStream) -> TokenStream { - let test_fn = parse_macro_input!(item as ItemFn); - let configuration = parse_macro_input!(attribute as FixtureConfiguration); - - let result = generate_fixtures(test_fn, &configuration); - - let stream = match result { - Ok(output) => output, - Err(err) => err.to_compile_error(), - }; - - TokenStream::from(stream) -} - -fn generate_fixtures( - mut test_fn: ItemFn, - configuration: &FixtureConfiguration, -) -> syn::Result { - // Remove the fixtures attribute - test_fn - .attrs - .retain(|attr| !attr.path().is_ident("fixtures")); - - // Extract the name of the only argument of the test function. - let last_arg = test_fn.sig.inputs.last(); - let path_ident = match (test_fn.sig.inputs.len(), last_arg) { - (1, Some(last_arg)) => match last_arg { - FnArg::Typed(typed) => match typed.pat.as_ref() { - Pat::Ident(ident) => ident.ident.clone(), - pat => { - return Err(Error::new( - pat.span(), - "#[fixture] function argument name must be an identifier", - )); - } - }, - FnArg::Receiver(receiver) => { - return Err(Error::new( - receiver.span(), - "#[fixture] function argument name must be an identifier", - )); - } - }, - _ => { - return Err(Error::new( - test_fn.sig.inputs.span(), - "#[fixture] function must have exactly one argument with the type '&Path'", - )); - } - }; - - // Remove all arguments - test_fn.sig.inputs.clear(); - - let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect( - "#[fixture] requires CARGO_MANIFEST_DIR to be set during the build to resolve the relative paths to the test files.", - )); - - let pattern = if configuration.pattern.as_str().starts_with('/') { - Cow::from(configuration.pattern.as_str()) - } else { - Cow::from(format!( - "{}/{}", - crate_dir - .to_str() - .expect("CARGO_MANIFEST_DIR must point to a directory with a UTF8 path"), - configuration.pattern.as_str() - )) - }; - - let files = glob(&pattern).expect("Pattern to be valid").flatten(); - let mut modules = Modules::default(); - - for file in files { - if configuration - .exclude - .iter() - .any(|exclude| exclude.matches_path(&file)) - { - continue; - } - - let mut test_fn = test_fn.clone(); - - let test_name = file - .file_name() - // SAFETY: Glob only matches on file names. - .unwrap() - .to_str() - .expect("Expected path to be valid UTF8") - .replace('.', "_"); - - test_fn.sig.ident = format_ident!("{test_name}"); - - let path = file.as_os_str().to_str().unwrap(); - - test_fn.block.stmts.insert( - 0, - parse_quote!(let #path_ident = std::path::Path::new(#path);), - ); - - modules.push_test(Test { - path: file, - test_fn, - }); - } - - if modules.is_empty() { - return Err(Error::new( - configuration.pattern_span, - "No file matches the specified glob pattern", - )); - } - - let root = find_highest_common_ancestor_module(&modules.root); - - root.generate(&test_fn.sig.ident.to_string()) -} - -fn find_highest_common_ancestor_module(module: &Module) -> &Module { - let children = &module.children; - - if children.len() == 1 { - let (_, child) = children.iter().next().unwrap(); - - match child { - Child::Module(common_child) => find_highest_common_ancestor_module(common_child), - Child::Test(_) => module, - } - } else { - module - } -} - -#[derive(Debug)] -struct Test { - path: PathBuf, - test_fn: ItemFn, -} - -impl Test { - fn generate(&self, _: &str) -> proc_macro2::TokenStream { - let test_fn = &self.test_fn; - quote!(#test_fn) - } -} - -#[derive(Debug, Default)] -struct Module { - children: BTreeMap, -} - -impl Module { - fn generate(&self, name: &str) -> syn::Result { - let mut inner = Vec::with_capacity(self.children.len()); - - for (name, child) in &self.children { - inner.push(child.generate(name)?); - } - - let module_ident = format_ident!("{name}"); - - Ok(quote!( - mod #module_ident { - use super::*; - - #(#inner)* - } - )) - } -} - -#[derive(Debug)] -enum Child { - Module(Module), - Test(Test), -} - -impl Child { - fn generate(&self, name: &str) -> syn::Result { - match self { - Child::Module(module) => module.generate(name), - Child::Test(test) => Ok(test.generate(name)), - } - } -} - -#[derive(Debug, Default)] -struct Modules { - root: Module, -} - -impl Modules { - fn push_test(&mut self, test: Test) { - let mut components = test - .path - .as_path() - .components() - .skip_while(|c| matches!(c, Component::RootDir)) - .peekable(); - - let mut current = &mut self.root; - while let Some(component) = components.next() { - let name = component.as_os_str().to_str().unwrap(); - // A directory - if components.peek().is_some() { - let name = component.as_os_str().to_str().unwrap(); - let entry = current.children.entry(name.to_owned()); - - match entry.or_insert_with(|| Child::Module(Module::default())) { - Child::Module(module) => { - current = module; - } - Child::Test(_) => { - unreachable!() - } - } - } else { - // We reached the final component, insert the test - drop(components); - current.children.insert(name.to_owned(), Child::Test(test)); - break; - } - } - } - - fn is_empty(&self) -> bool { - self.root.children.is_empty() - } -} From a52cd47c7fbfb4cd161c1ead905a1314c3707c92 Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 26 Jun 2023 11:13:07 +0200 Subject: [PATCH 229/447] Fix attribute chain own line comments (#5340) ## Motation Previously, ```python x = ( a1 .a2 # a . # b # c a3 ) ``` got formatted as ```python x = a1.a2 # a . # b # c a3 ``` which is invalid syntax. This fixes that. ## Summary This implements a basic form of attribute chaining () by checking if any inner attribute access contains an own line comment, and if this is the case, adds parentheses around the outermost attribute access while disabling parentheses for all inner attribute expressions. We want to replace this with an implementation that uses recursion or a stack while formatting instead of in `needs_parentheses` and also includes calls rather sooner than later, but i'm fixing this now because i'm uncomfortable with having known invalid syntax generation in the formatter. ## Test Plan I added new fixtures. --- .../fixtures/ruff/expression/attribute.py | 76 +++++++++- .../src/comments/placement.rs | 14 +- .../src/expression/expr_attribute.rs | 53 +++++-- .../format@expression__attribute.py.snap | 133 +++++++++++++++++- 4 files changed, 258 insertions(+), 18 deletions(-) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/attribute.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/attribute.py index bdc18f4696..24bea5ca42 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/attribute.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/attribute.py @@ -1,3 +1,7 @@ +from argparse import Namespace + +a = Namespace() + ( a # comment @@ -26,4 +30,74 @@ ) -aaaaaaaaaaaaaaaaaaaaa.lllllllllllllllllllllllllllloooooooooong.chaaaaaaaaaaaaaaaaaaaaaaiiiiiiiiiiiiiiiiiiiiiiinnnnnnnn.ooooooooooooooooooooooooofffffffff.aaaaaaaaaattr +a.aaaaaaaaaaaaaaaaaaaaa.lllllllllllllllllllllllllllloooooooooong.chaaaaaaaaaaaaaaaaaaaaaaiiiiiiiiiiiiiiiiiiiiiiinnnnnnnn.ooooooooooooooooooooooooofffffffff.aaaaaaaaaattr + + +# Test that we add parentheses around the outermost attribute access in an attribute +# chain if and only if we need them, that is if there are own line comments inside +# the chain. +x1 = ( + a + .b + # comment 1 + . # comment 2 + # comment 3 + c + .d +) + +x20 = ( + a + .b +) +x21 = ( + # leading name own line + a # trailing name end-of-line + .b +) +x22 = ( + a + # outermost leading own line + .b # outermost trailing end-of-line +) + +x31 = ( + a + # own line between nodes 1 + .b +) +x321 = ( + a + . # end-of-line dot comment + b +) +x322 = ( + a + . # end-of-line dot comment 2 + b + .c +) +x331 = ( + a. + # own line between nodes 3 + b +) +x332 = ( + "" + # own line between nodes + .find +) + +x8 = ( + (a + a) + .b +) + +x51 = ( + a.b.c +) +x52 = a.askjdfahdlskjflsajfadhsaf.akjdsf.aksjdlfadhaljsashdfljaf.askjdflhasfdlashdlfaskjfd.asdkjfksahdfkjafs +x53 = ( + a.askjdfahdlskjflsajfadhsaf.akjdsf.aksjdlfadhaljsashdfljaf.askjdflhasfdlashdlfaskjfd.asdkjfksahdfkjafs +) + diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index ca0fec261e..0e801c7096 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -1077,7 +1077,19 @@ fn handle_attribute_comment<'a>( .expect("Expected the `.` character after the value"); if TextRange::new(dot.end(), attribute.attr.start()).contains(comment.slice().start()) { - CommentPlacement::dangling(attribute.into(), comment) + if comment.line_position().is_end_of_line() { + // Attach to node with b + // ```python + // x322 = ( + // a + // . # end-of-line dot comment 2 + // b + // ) + // ``` + CommentPlacement::trailing(comment.enclosing_node(), comment) + } else { + CommentPlacement::dangling(attribute.into(), comment) + } } else { CommentPlacement::Default(comment) } diff --git a/crates/ruff_python_formatter/src/expression/expr_attribute.rs b/crates/ruff_python_formatter/src/expression/expr_attribute.rs index fd43e9f20a..16ccaa330c 100644 --- a/crates/ruff_python_formatter/src/expression/expr_attribute.rs +++ b/crates/ruff_python_formatter/src/expression/expr_attribute.rs @@ -27,25 +27,28 @@ impl FormatNodeRule for FormatExprAttribute { }) ); - if needs_parentheses { - value.format().with_options(Parenthesize::Always).fmt(f)?; - } else { - value.format().fmt(f)?; - } - let comments = f.context().comments().clone(); - - if comments.has_trailing_own_line_comments(value.as_ref()) { - hard_line_break().fmt(f)?; - } - let dangling_comments = comments.dangling_comments(item); - let leading_attribute_comments_start = dangling_comments.partition_point(|comment| comment.line_position().is_end_of_line()); let (trailing_dot_comments, leading_attribute_comments) = dangling_comments.split_at(leading_attribute_comments_start); + if needs_parentheses { + value.format().with_options(Parenthesize::Always).fmt(f)?; + } else if let Expr::Attribute(expr_attribute) = value.as_ref() { + // We're in a attribute chain (`a.b.c`). The outermost node adds parentheses if + // required, the inner ones don't need them so we skip the `Expr` formatting that + // normally adds the parentheses. + expr_attribute.format().fmt(f)?; + } else { + value.format().fmt(f)?; + } + + if comments.has_trailing_own_line_comments(value.as_ref()) { + hard_line_break().fmt(f)?; + } + write!( f, [ @@ -68,6 +71,28 @@ impl FormatNodeRule for FormatExprAttribute { } } +/// Checks if there are any own line comments in an attribute chain (a.b.c). This method is +/// recursive up to the innermost expression that the attribute chain starts behind. +fn has_breaking_comments_attribute_chain( + expr_attribute: &ExprAttribute, + comments: &Comments, +) -> bool { + if comments + .dangling_comments(expr_attribute) + .iter() + .any(|comment| comment.line_position().is_own_line()) + || comments.has_trailing_own_line_comments(expr_attribute) + { + return true; + } + + if let Expr::Attribute(inner) = expr_attribute.value.as_ref() { + return has_breaking_comments_attribute_chain(inner, comments); + } + + return comments.has_trailing_own_line_comments(expr_attribute.value.as_ref()); +} + impl NeedsParentheses for ExprAttribute { fn needs_parentheses( &self, @@ -75,6 +100,10 @@ impl NeedsParentheses for ExprAttribute { source: &str, comments: &Comments, ) -> Parentheses { + if has_breaking_comments_attribute_chain(self, comments) { + return Parentheses::Always; + } + match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { Parentheses::Optional => Parentheses::Never, parentheses => parentheses, 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 82792c7d3c..a1871573c8 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 @@ -4,6 +4,10 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression --- ## Input ```py +from argparse import Namespace + +a = Namespace() + ( a # comment @@ -32,13 +36,87 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression ) -aaaaaaaaaaaaaaaaaaaaa.lllllllllllllllllllllllllllloooooooooong.chaaaaaaaaaaaaaaaaaaaaaaiiiiiiiiiiiiiiiiiiiiiiinnnnnnnn.ooooooooooooooooooooooooofffffffff.aaaaaaaaaattr +a.aaaaaaaaaaaaaaaaaaaaa.lllllllllllllllllllllllllllloooooooooong.chaaaaaaaaaaaaaaaaaaaaaaiiiiiiiiiiiiiiiiiiiiiiinnnnnnnn.ooooooooooooooooooooooooofffffffff.aaaaaaaaaattr + + +# Test that we add parentheses around the outermost attribute access in an attribute +# chain if and only if we need them, that is if there are own line comments inside +# the chain. +x1 = ( + a + .b + # comment 1 + . # comment 2 + # comment 3 + c + .d +) + +x20 = ( + a + .b +) +x21 = ( + # leading name own line + a # trailing name end-of-line + .b +) +x22 = ( + a + # outermost leading own line + .b # outermost trailing end-of-line +) + +x31 = ( + a + # own line between nodes 1 + .b +) +x321 = ( + a + . # end-of-line dot comment + b +) +x322 = ( + a + . # end-of-line dot comment 2 + b + .c +) +x331 = ( + a. + # own line between nodes 3 + b +) +x332 = ( + "" + # own line between nodes + .find +) + +x8 = ( + (a + a) + .b +) + +x51 = ( + a.b.c +) +x52 = a.askjdfahdlskjflsajfadhsaf.akjdsf.aksjdlfadhaljsashdfljaf.askjdflhasfdlashdlfaskjfd.asdkjfksahdfkjafs +x53 = ( + a.askjdfahdlskjflsajfadhsaf.akjdsf.aksjdlfadhaljsashdfljaf.askjdflhasfdlashdlfaskjfd.asdkjfksahdfkjafs +) + ``` ## Output ```py +NOT_YET_IMPLEMENTED_StmtImportFrom + +a = NOT_IMPLEMENTED_call() + ( a # comment @@ -61,13 +139,60 @@ aaaaaaaaaaaaaaaaaaaaa.lllllllllllllllllllllllllllloooooooooong.chaaaaaaaaaaaaaaa ( a # comment - . # trailing dot comment + . # in between - b # trailing identifier comment + b # trailing dot comment # trailing identifier comment ) -aaaaaaaaaaaaaaaaaaaaa.lllllllllllllllllllllllllllloooooooooong.chaaaaaaaaaaaaaaaaaaaaaaiiiiiiiiiiiiiiiiiiiiiiinnnnnnnn.ooooooooooooooooooooooooofffffffff.aaaaaaaaaattr +a.aaaaaaaaaaaaaaaaaaaaa.lllllllllllllllllllllllllllloooooooooong.chaaaaaaaaaaaaaaaaaaaaaaiiiiiiiiiiiiiiiiiiiiiiinnnnnnnn.ooooooooooooooooooooooooofffffffff.aaaaaaaaaattr + + +# Test that we add parentheses around the outermost attribute access in an attribute +# chain if and only if we need them, that is if there are own line comments inside +# the chain. +x1 = ( + a.b + # comment 1 + . + # comment 3 + c.d # comment 2 +) + +x20 = a.b +x21 = ( + # leading name own line + a.b # trailing name end-of-line +) +x22 = ( + a + # outermost leading own line + .b # outermost trailing end-of-line +) + +x31 = ( + a + # own line between nodes 1 + .b +) +x321 = a.b # end-of-line dot comment +x322 = a.b.c # end-of-line dot comment 2 +x331 = ( + a. + # own line between nodes 3 + b +) +x332 = ( + "" + # own line between nodes + .find +) + +x8 = (a + a).b + +x51 = a.b.c +x52 = a.askjdfahdlskjflsajfadhsaf.akjdsf.aksjdlfadhaljsashdfljaf.askjdflhasfdlashdlfaskjfd.asdkjfksahdfkjafs +x53 = a.askjdfahdlskjflsajfadhsaf.akjdsf.aksjdlfadhaljsashdfljaf.askjdflhasfdlashdlfaskjfd.asdkjfksahdfkjafs ``` From dd0d1afb6601411cba81f1660c4df31daec65a01 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 26 Jun 2023 14:02:17 +0200 Subject: [PATCH 230/447] Create `PyFormatOptions` ## Summary This PR adds a new `PyFormatOptions` struct that stores the python formatter options. The new options aren't used yet, with the exception of magical trailing commas and the options passed to the printer. I'll follow up with more PRs that use the new options (e.g. `QuoteStyle`). ## Test Plan `cargo test` I'll follow up with a new PR that adds support for overriding the options in our fixture tests. --- crates/ruff_benchmark/benches/formatter.rs | 7 +- crates/ruff_cli/src/lib.rs | 4 +- .../ruff_dev/src/check_formatter_stability.rs | 6 +- crates/ruff_python_formatter/src/builders.rs | 8 +- crates/ruff_python_formatter/src/cli.rs | 9 +- crates/ruff_python_formatter/src/context.rs | 13 +- crates/ruff_python_formatter/src/lib.rs | 71 +++-------- crates/ruff_python_formatter/src/options.rs | 118 ++++++++++++++++++ .../src/statement/suite.rs | 12 +- .../ruff_python_formatter/tests/fixtures.rs | 10 +- 10 files changed, 170 insertions(+), 88 deletions(-) create mode 100644 crates/ruff_python_formatter/src/options.rs diff --git a/crates/ruff_benchmark/benches/formatter.rs b/crates/ruff_benchmark/benches/formatter.rs index 15698dbba0..74688bed09 100644 --- a/crates/ruff_benchmark/benches/formatter.rs +++ b/crates/ruff_benchmark/benches/formatter.rs @@ -1,6 +1,6 @@ use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; use ruff_benchmark::{TestCase, TestCaseSpeed, TestFile, TestFileDownloadError}; -use ruff_python_formatter::format_module; +use ruff_python_formatter::{format_module, PyFormatOptions}; use std::time::Duration; #[cfg(target_os = "windows")] @@ -50,7 +50,10 @@ fn benchmark_formatter(criterion: &mut Criterion) { BenchmarkId::from_parameter(case.name()), &case, |b, case| { - b.iter(|| format_module(case.code()).expect("Formatting to succeed")); + b.iter(|| { + format_module(case.code(), PyFormatOptions::default()) + .expect("Formatting to succeed") + }); }, ); } diff --git a/crates/ruff_cli/src/lib.rs b/crates/ruff_cli/src/lib.rs index 4b63ad4a07..0f737628d1 100644 --- a/crates/ruff_cli/src/lib.rs +++ b/crates/ruff_cli/src/lib.rs @@ -13,7 +13,7 @@ use ruff::logging::{set_up_logging, LogLevel}; use ruff::settings::types::SerializationFormat; use ruff::settings::{flags, CliSettings}; use ruff::{fs, warn_user_once}; -use ruff_python_formatter::format_module; +use ruff_python_formatter::{format_module, PyFormatOptions}; use crate::args::{Args, CheckArgs, Command}; use crate::commands::run_stdin::read_from_stdin; @@ -137,7 +137,7 @@ fn format(files: &[PathBuf]) -> Result { // dummy, to check that the function was actually called let contents = code.replace("# DEL", ""); // real formatting that is currently a passthrough - format_module(&contents) + format_module(&contents, PyFormatOptions::default()) }; match &files { diff --git a/crates/ruff_dev/src/check_formatter_stability.rs b/crates/ruff_dev/src/check_formatter_stability.rs index 39f90224b2..4f07db548f 100644 --- a/crates/ruff_dev/src/check_formatter_stability.rs +++ b/crates/ruff_dev/src/check_formatter_stability.rs @@ -11,7 +11,7 @@ use ruff::resolver::python_files_in_path; use ruff::settings::types::{FilePattern, FilePatternSet}; use ruff_cli::args::CheckArgs; use ruff_cli::resolve::resolve; -use ruff_python_formatter::format_module; +use ruff_python_formatter::{format_module, PyFormatOptions}; use similar::{ChangeTag, TextDiff}; use std::io::Write; use std::panic::catch_unwind; @@ -276,7 +276,7 @@ impl From for FormatterStabilityError { /// Run the formatter twice on the given file. Does not write back to the file fn check_file(input_path: &Path) -> Result<(), FormatterStabilityError> { let content = fs::read_to_string(input_path).context("Failed to read file")?; - let printed = match format_module(&content) { + let printed = match format_module(&content, PyFormatOptions::default()) { Ok(printed) => printed, Err(err) => { return if err @@ -296,7 +296,7 @@ fn check_file(input_path: &Path) -> Result<(), FormatterStabilityError> { }; let formatted = printed.as_code(); - let reformatted = match format_module(formatted) { + let reformatted = match format_module(formatted, PyFormatOptions::default()) { Ok(reformatted) => reformatted, Err(err) => { return Err(FormatterStabilityError::InvalidSyntax { diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 50a060e423..db52ed9b91 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -1,7 +1,6 @@ use crate::context::NodeLevel; use crate::prelude::*; use crate::trivia::{first_non_trivia_token, lines_after, skip_trailing_trivia, Token, TokenKind}; -use crate::USE_MAGIC_TRAILING_COMMA; use ruff_formatter::write; use ruff_text_size::TextSize; use rustpython_parser::ast::Ranged; @@ -221,7 +220,7 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { if let Some(last_end) = self.last_end.take() { if_group_breaks(&text(",")).fmt(self.fmt)?; - if USE_MAGIC_TRAILING_COMMA + if self.fmt.options().magic_trailing_comma().is_preserve() && matches!( first_non_trivia_token(last_end, self.fmt.context().contents()), Some(Token { @@ -243,8 +242,8 @@ mod tests { use crate::comments::Comments; use crate::context::{NodeLevel, PyFormatContext}; use crate::prelude::*; + use crate::PyFormatOptions; use ruff_formatter::format; - use ruff_formatter::SimpleFormatOptions; use rustpython_parser::ast::ModModule; use rustpython_parser::Parse; @@ -265,8 +264,7 @@ no_leading_newline = 30 let module = ModModule::parse(source, "test.py").unwrap(); - let context = - PyFormatContext::new(SimpleFormatOptions::default(), source, Comments::default()); + let context = PyFormatContext::new(PyFormatOptions::default(), source, Comments::default()); let test_formatter = format_with(|f: &mut PyFormatter| f.join_nodes(level).nodes(&module.body).finish()); diff --git a/crates/ruff_python_formatter/src/cli.rs b/crates/ruff_python_formatter/src/cli.rs index 8a277ece24..15d480377f 100644 --- a/crates/ruff_python_formatter/src/cli.rs +++ b/crates/ruff_python_formatter/src/cli.rs @@ -10,7 +10,7 @@ use rustpython_parser::{parse_tokens, Mode}; use ruff_formatter::SourceCode; use ruff_python_ast::source_code::CommentRangesBuilder; -use crate::format_node; +use crate::{format_node, PyFormatOptions}; #[derive(ValueEnum, Clone, Debug)] pub enum Emit { @@ -57,7 +57,12 @@ pub fn format_and_debug_print(input: &str, cli: &Cli) -> Result { let python_ast = parse_tokens(tokens, Mode::Module, "") .with_context(|| "Syntax error in input")?; - let formatted = format_node(&python_ast, &comment_ranges, input)?; + let formatted = format_node( + &python_ast, + &comment_ranges, + input, + PyFormatOptions::default(), + )?; if cli.print_ir { println!("{}", formatted.document().display(SourceCode::new(input))); } diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index 074d5cc08d..cdf587c35f 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -1,22 +1,19 @@ use crate::comments::Comments; -use ruff_formatter::{FormatContext, SimpleFormatOptions, SourceCode}; +use crate::PyFormatOptions; +use ruff_formatter::{FormatContext, SourceCode}; use ruff_python_ast::source_code::Locator; use std::fmt::{Debug, Formatter}; #[derive(Clone)] pub struct PyFormatContext<'a> { - options: SimpleFormatOptions, + options: PyFormatOptions, contents: &'a str, comments: Comments<'a>, node_level: NodeLevel, } impl<'a> PyFormatContext<'a> { - pub(crate) fn new( - options: SimpleFormatOptions, - contents: &'a str, - comments: Comments<'a>, - ) -> Self { + pub(crate) fn new(options: PyFormatOptions, contents: &'a str, comments: Comments<'a>) -> Self { Self { options, contents, @@ -48,7 +45,7 @@ impl<'a> PyFormatContext<'a> { } impl FormatContext for PyFormatContext<'_> { - type Options = SimpleFormatOptions; + type Options = PyFormatOptions; fn options(&self) -> &Self::Options { &self.options diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 93197b2ca2..4932ca5da0 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -2,10 +2,11 @@ use crate::comments::{ dangling_node_comments, leading_node_comments, trailing_node_comments, Comments, }; use crate::context::PyFormatContext; +pub use crate::options::{MagicTrailingComma, PyFormatOptions, QuoteStyle}; use anyhow::{anyhow, Context, Result}; use ruff_formatter::prelude::*; use ruff_formatter::{format, write}; -use ruff_formatter::{Formatted, IndentStyle, Printed, SimpleFormatOptions, SourceCode}; +use ruff_formatter::{Formatted, Printed, SourceCode}; use ruff_python_ast::node::{AnyNodeRef, AstNode, NodeKind}; use ruff_python_ast::source_code::{CommentRanges, CommentRangesBuilder, Locator}; use ruff_text_size::{TextLen, TextRange}; @@ -21,6 +22,7 @@ pub(crate) mod context; pub(crate) mod expression; mod generated; pub(crate) mod module; +mod options; pub(crate) mod other; pub(crate) mod pattern; mod prelude; @@ -29,10 +31,6 @@ mod trivia; include!("../../ruff_formatter/shared_traits.rs"); -/// TODO(konstin): hook this up to the settings by replacing `SimpleFormatOptions` with a python -/// specific struct. -pub(crate) const USE_MAGIC_TRAILING_COMMA: bool = true; - /// 'ast is the lifetime of the source code (input), 'buf is the lifetime of the buffer (output) pub(crate) type PyFormatter<'ast, 'buf> = Formatter<'buf, PyFormatContext<'ast>>; @@ -86,7 +84,7 @@ where } } -pub fn format_module(contents: &str) -> Result { +pub fn format_module(contents: &str, options: PyFormatOptions) -> Result { // Tokenize once let mut tokens = Vec::new(); let mut comment_ranges = CommentRangesBuilder::default(); @@ -107,7 +105,7 @@ pub fn format_module(contents: &str) -> Result { let python_ast = parse_tokens(tokens, Mode::Module, "") .with_context(|| "Syntax error in input")?; - let formatted = format_node(&python_ast, &comment_ranges, contents)?; + let formatted = format_node(&python_ast, &comment_ranges, contents, options)?; formatted .print() @@ -118,20 +116,14 @@ pub fn format_node<'a>( root: &'a Mod, comment_ranges: &'a CommentRanges, source: &'a str, + options: PyFormatOptions, ) -> FormatResult>> { let comments = Comments::from_ast(root, SourceCode::new(source), comment_ranges); let locator = Locator::new(source); format!( - PyFormatContext::new( - SimpleFormatOptions { - indent_style: IndentStyle::Space(4), - line_width: 88.try_into().unwrap(), - }, - locator.contents(), - comments, - ), + PyFormatContext::new(options, locator.contents(), comments), [root.format()] ) } @@ -226,44 +218,9 @@ impl Format> for VerbatimText { } } -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum QuoteStyle { - Single, - Double, -} - -impl QuoteStyle { - pub const fn as_char(self) -> char { - match self { - QuoteStyle::Single => '\'', - QuoteStyle::Double => '"', - } - } - - #[must_use] - pub const fn opposite(self) -> QuoteStyle { - match self { - QuoteStyle::Single => QuoteStyle::Double, - QuoteStyle::Double => QuoteStyle::Single, - } - } -} - -impl TryFrom for QuoteStyle { - type Error = (); - - fn try_from(value: char) -> std::result::Result { - match value { - '\'' => Ok(QuoteStyle::Single), - '"' => Ok(QuoteStyle::Double), - _ => Err(()), - } - } -} - #[cfg(test)] mod tests { - use crate::{format_module, format_node}; + use crate::{format_module, format_node, PyFormatOptions}; use anyhow::Result; use insta::assert_snapshot; use ruff_python_ast::source_code::CommentRangesBuilder; @@ -284,7 +241,9 @@ if True: pass # trailing "#; - let actual = format_module(input)?.as_code().to_string(); + let actual = format_module(input, PyFormatOptions::default())? + .as_code() + .to_string(); assert_eq!(expected, actual); Ok(()) } @@ -315,7 +274,13 @@ if [ // Parse the AST. let python_ast = parse_tokens(tokens, Mode::Module, "").unwrap(); - let formatted = format_node(&python_ast, &comment_ranges, src).unwrap(); + let formatted = format_node( + &python_ast, + &comment_ranges, + src, + PyFormatOptions::default(), + ) + .unwrap(); // Uncomment the `dbg` to print the IR. // Use `dbg_write!(f, []) instead of `write!(f, [])` in your formatting code to print some IR diff --git a/crates/ruff_python_formatter/src/options.rs b/crates/ruff_python_formatter/src/options.rs new file mode 100644 index 0000000000..e7302a69df --- /dev/null +++ b/crates/ruff_python_formatter/src/options.rs @@ -0,0 +1,118 @@ +use ruff_formatter::printer::{LineEnding, PrinterOptions}; +use ruff_formatter::{FormatOptions, IndentStyle, LineWidth}; + +#[derive(Clone, Debug)] +pub struct PyFormatOptions { + /// Specifies the indent style: + /// * Either a tab + /// * or a specific amount of spaces + indent_style: IndentStyle, + + /// The preferred line width at which the formatter should wrap lines. + line_width: LineWidth, + + /// The preferred quote style to use (single vs double quotes). + quote_style: QuoteStyle, + + /// Whether to expand lists or elements if they have a trailing comma such as `(a, b,)` + magic_trailing_comma: MagicTrailingComma, +} + +impl PyFormatOptions { + pub fn magic_trailing_comma(&self) -> MagicTrailingComma { + self.magic_trailing_comma + } + + pub fn quote_style(&self) -> QuoteStyle { + self.quote_style + } + + pub fn with_quote_style(&mut self, style: QuoteStyle) -> &mut Self { + self.quote_style = style; + self + } + + pub fn with_magic_trailing_comma(&mut self, trailing_comma: MagicTrailingComma) -> &mut Self { + self.magic_trailing_comma = trailing_comma; + self + } +} + +impl FormatOptions for PyFormatOptions { + fn indent_style(&self) -> IndentStyle { + self.indent_style + } + + fn line_width(&self) -> LineWidth { + self.line_width + } + + fn as_print_options(&self) -> PrinterOptions { + PrinterOptions { + tab_width: 4, + print_width: self.line_width.into(), + line_ending: LineEnding::LineFeed, + indent_style: self.indent_style, + } + } +} + +impl Default for PyFormatOptions { + fn default() -> Self { + Self { + indent_style: IndentStyle::Space(4), + line_width: LineWidth::try_from(88).unwrap(), + quote_style: QuoteStyle::default(), + magic_trailing_comma: MagicTrailingComma::default(), + } + } +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub enum QuoteStyle { + Single, + #[default] + Double, +} + +impl QuoteStyle { + pub const fn as_char(self) -> char { + match self { + QuoteStyle::Single => '\'', + QuoteStyle::Double => '"', + } + } + + #[must_use] + pub const fn opposite(self) -> QuoteStyle { + match self { + QuoteStyle::Single => QuoteStyle::Double, + QuoteStyle::Double => QuoteStyle::Single, + } + } +} + +impl TryFrom for QuoteStyle { + type Error = (); + + fn try_from(value: char) -> std::result::Result { + match value { + '\'' => Ok(QuoteStyle::Single), + '"' => Ok(QuoteStyle::Double), + _ => Err(()), + } + } +} + +#[derive(Copy, Clone, Debug, Default)] +pub enum MagicTrailingComma { + #[default] + Preserve, + Skip, +} + +impl MagicTrailingComma { + pub const fn is_preserve(self) -> bool { + matches!(self, Self::Preserve) + } +} diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index 85329fba8c..d16576a6db 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -188,7 +188,8 @@ mod tests { use crate::comments::Comments; use crate::prelude::*; use crate::statement::suite::SuiteLevel; - use ruff_formatter::{format, IndentStyle, SimpleFormatOptions}; + use crate::PyFormatOptions; + use ruff_formatter::format; use rustpython_parser::ast::Suite; use rustpython_parser::Parse; @@ -216,14 +217,7 @@ def trailing_func(): let statements = Suite::parse(source, "test.py").unwrap(); - let context = PyFormatContext::new( - SimpleFormatOptions { - indent_style: IndentStyle::Space(4), - ..SimpleFormatOptions::default() - }, - source, - Comments::default(), - ); + let context = PyFormatContext::new(PyFormatOptions::default(), source, Comments::default()); let test_formatter = format_with(|f: &mut PyFormatter| statements.format().with_options(level).fmt(f)); diff --git a/crates/ruff_python_formatter/tests/fixtures.rs b/crates/ruff_python_formatter/tests/fixtures.rs index edfc112a86..34e41229a9 100644 --- a/crates/ruff_python_formatter/tests/fixtures.rs +++ b/crates/ruff_python_formatter/tests/fixtures.rs @@ -1,4 +1,4 @@ -use ruff_python_formatter::format_module; +use ruff_python_formatter::{format_module, PyFormatOptions}; use similar::TextDiff; use std::fmt::{Formatter, Write}; use std::fs; @@ -9,7 +9,8 @@ fn black_compatibility() { let test_file = |input_path: &Path| { let content = fs::read_to_string(input_path).unwrap(); - let printed = format_module(&content).expect("Formatting to succeed"); + let printed = + format_module(&content, PyFormatOptions::default()).expect("Formatting to succeed"); let expected_path = input_path.with_extension("py.expect"); let expected_output = fs::read_to_string(&expected_path) @@ -88,7 +89,8 @@ fn format() { let test_file = |input_path: &Path| { let content = fs::read_to_string(input_path).unwrap(); - let printed = format_module(&content).expect("Formatting to succeed"); + let printed = + format_module(&content, PyFormatOptions::default()).expect("Formatting to succeed"); let formatted_code = printed.as_code(); ensure_stability_when_formatting_twice(formatted_code); @@ -117,7 +119,7 @@ fn format() { /// Format another time and make sure that there are no changes anymore fn ensure_stability_when_formatting_twice(formatted_code: &str) { - let reformatted = match format_module(formatted_code) { + let reformatted = match format_module(formatted_code, PyFormatOptions::default()) { Ok(reformatted) => reformatted, Err(err) => { panic!( From f18a1f70de329a2700f16a4bf4875b911acf88d1 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 26 Jun 2023 14:15:55 +0200 Subject: [PATCH 231/447] Add tests for skip magic trailing comma ## Summary This PR adds tests that verify that the magic trailing comma is not respected if disabled in the formatter options. Our test setup now allows to create a `.options.json` file that contains an array of configurations that should be tested. ## Test Plan It's all about tests :) --- Cargo.lock | 3 +- crates/ruff_formatter/src/lib.rs | 24 ++--- crates/ruff_python_formatter/Cargo.toml | 17 +++- .../skip_magic_trailing_comma.options.json | 8 ++ .../ruff/skip_magic_trailing_comma.py | 22 +++++ crates/ruff_python_formatter/src/builders.rs | 2 +- crates/ruff_python_formatter/src/options.rs | 38 +++++++- .../ruff_python_formatter/tests/fixtures.rs | 91 +++++++++++++++---- .../format@expression__attribute.py.snap | 3 +- .../format@expression__binary.py.snap | 3 +- ...rmat@expression__boolean_operation.py.snap | 3 +- .../format@expression__compare.py.snap | 3 +- .../snapshots/format@expression__dict.py.snap | 3 +- .../snapshots/format@expression__list.py.snap | 3 +- .../format@expression__slice.py.snap | 3 +- .../format@expression__string.py.snap | 3 +- .../format@expression__tuple.py.snap | 3 +- .../format@expression__unary.py.snap | 3 +- .../format@skip_magic_trailing_comma.py.snap | 88 ++++++++++++++++++ .../format@statement__assign.py.snap | 3 +- .../snapshots/format@statement__break.py.snap | 3 +- ...format@statement__class_definition.py.snap | 3 +- .../snapshots/format@statement__for.py.snap | 3 +- .../format@statement__function.py.snap | 3 +- .../snapshots/format@statement__if.py.snap | 3 +- .../snapshots/format@statement__while.py.snap | 3 +- .../tests/snapshots/format@trivia.py.snap | 3 +- 27 files changed, 268 insertions(+), 79 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.options.json create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap diff --git a/Cargo.lock b/Cargo.lock index 91d79253be..5dd0ae65ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2065,8 +2065,9 @@ dependencies = [ "ruff_text_size", "rustc-hash", "rustpython-parser", + "serde", + "serde_json", "similar", - "test-case", ] [[package]] diff --git a/crates/ruff_formatter/src/lib.rs b/crates/ruff_formatter/src/lib.rs index 4e40deb77d..5c99ebba87 100644 --- a/crates/ruff_formatter/src/lib.rs +++ b/crates/ruff_formatter/src/lib.rs @@ -59,10 +59,8 @@ use std::num::ParseIntError; use std::str::FromStr; #[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Default)] pub enum IndentStyle { /// Tab @@ -112,10 +110,8 @@ impl std::fmt::Display for IndentStyle { /// /// The allowed range of values is 1..=320 #[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct LineWidth(u16); impl LineWidth { @@ -278,10 +274,8 @@ impl FormatOptions for SimpleFormatOptions { /// Lightweight sourcemap marker between source and output tokens #[derive(Debug, Copy, Clone, Eq, PartialEq)] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct SourceMarker { /// Position of the marker in the original source pub source: TextSize, @@ -340,10 +334,8 @@ where pub type PrintResult = Result; #[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct Printed { code: String, range: Option, diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index 91ba277792..e781baf3ef 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -25,8 +25,23 @@ itertools = { workspace = true } once_cell = { workspace = true } rustc-hash = { workspace = true } rustpython-parser = { workspace = true } +serde = { workspace = true, optional = true } [dev-dependencies] +ruff_formatter = { path = "../ruff_formatter", features = ["serde"]} + insta = { workspace = true, features = ["glob"] } -test-case = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } similar = { workspace = true } + +[[test]] +name = "ruff_python_formatter_fixtures" +path = "tests/fixtures.rs" +test = true +required-features = [ "serde" ] + + +[features] +serde = ["dep:serde", "ruff_formatter/serde"] +default = ["serde"] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.options.json new file mode 100644 index 0000000000..f842b9e3ff --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.options.json @@ -0,0 +1,8 @@ +[ + { + "magic_trailing_comma": "respect" + }, + { + "magic_trailing_comma": "ignore" + } +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py new file mode 100644 index 0000000000..8f4c58789a --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py @@ -0,0 +1,22 @@ +( + "First entry", + "Second entry", + "last with trailing comma", +) + +( + "First entry", + "Second entry", + "last without trailing comma" +) + +( + "First entry", + "Second entry", + "third entry", + "fourth entry", + "fifth entry", + "sixt entry", + "seventh entry", + "eigth entry", +) diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index db52ed9b91..d3b64ce34b 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -220,7 +220,7 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { if let Some(last_end) = self.last_end.take() { if_group_breaks(&text(",")).fmt(self.fmt)?; - if self.fmt.options().magic_trailing_comma().is_preserve() + if self.fmt.options().magic_trailing_comma().is_respect() && matches!( first_non_trivia_token(last_end, self.fmt.context().contents()), Some(Token { diff --git a/crates/ruff_python_formatter/src/options.rs b/crates/ruff_python_formatter/src/options.rs index e7302a69df..e886a30b60 100644 --- a/crates/ruff_python_formatter/src/options.rs +++ b/crates/ruff_python_formatter/src/options.rs @@ -2,6 +2,11 @@ use ruff_formatter::printer::{LineEnding, PrinterOptions}; use ruff_formatter::{FormatOptions, IndentStyle, LineWidth}; #[derive(Clone, Debug)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(default) +)] pub struct PyFormatOptions { /// Specifies the indent style: /// * Either a tab @@ -9,6 +14,7 @@ pub struct PyFormatOptions { indent_style: IndentStyle, /// The preferred line width at which the formatter should wrap lines. + #[cfg_attr(feature = "serde", serde(default = "default_line_width"))] line_width: LineWidth, /// The preferred quote style to use (single vs double quotes). @@ -18,6 +24,10 @@ pub struct PyFormatOptions { magic_trailing_comma: MagicTrailingComma, } +fn default_line_width() -> LineWidth { + LineWidth::try_from(88).unwrap() +} + impl PyFormatOptions { pub fn magic_trailing_comma(&self) -> MagicTrailingComma { self.magic_trailing_comma @@ -36,6 +46,16 @@ impl PyFormatOptions { self.magic_trailing_comma = trailing_comma; self } + + pub fn with_indent_style(&mut self, indent_style: IndentStyle) -> &mut Self { + self.indent_style = indent_style; + self + } + + pub fn with_line_width(&mut self, line_width: LineWidth) -> &mut Self { + self.line_width = line_width; + self + } } impl FormatOptions for PyFormatOptions { @@ -69,6 +89,11 @@ impl Default for PyFormatOptions { } #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "kebab-case") +)] pub enum QuoteStyle { Single, #[default] @@ -105,14 +130,19 @@ impl TryFrom for QuoteStyle { } #[derive(Copy, Clone, Debug, Default)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "kebab-case") +)] pub enum MagicTrailingComma { #[default] - Preserve, - Skip, + Respect, + Ignore, } impl MagicTrailingComma { - pub const fn is_preserve(self) -> bool { - matches!(self, Self::Preserve) + pub const fn is_respect(self) -> bool { + matches!(self, Self::Respect) } } diff --git a/crates/ruff_python_formatter/tests/fixtures.rs b/crates/ruff_python_formatter/tests/fixtures.rs index 34e41229a9..43a94051e7 100644 --- a/crates/ruff_python_formatter/tests/fixtures.rs +++ b/crates/ruff_python_formatter/tests/fixtures.rs @@ -1,16 +1,18 @@ +use ruff_formatter::FormatOptions; use ruff_python_formatter::{format_module, PyFormatOptions}; use similar::TextDiff; use std::fmt::{Formatter, Write}; -use std::fs; +use std::io::BufReader; use std::path::Path; +use std::{fmt, fs}; #[test] fn black_compatibility() { let test_file = |input_path: &Path| { let content = fs::read_to_string(input_path).unwrap(); - let printed = - format_module(&content, PyFormatOptions::default()).expect("Formatting to succeed"); + let options = PyFormatOptions::default(); + let printed = format_module(&content, options.clone()).expect("Formatting to succeed"); let expected_path = input_path.with_extension("py.expect"); let expected_output = fs::read_to_string(&expected_path) @@ -18,7 +20,7 @@ fn black_compatibility() { let formatted_code = printed.as_code(); - ensure_stability_when_formatting_twice(formatted_code); + ensure_stability_when_formatting_twice(formatted_code, options); if formatted_code == expected_output { // Black and Ruff formatting matches. Delete any existing snapshot files because the Black output @@ -66,7 +68,7 @@ fn black_compatibility() { write!(snapshot, "{}", CodeFrame::new("diff", &diff)).unwrap(); write!(snapshot, "{}", Header::new("Ruff Output")).unwrap(); - write!(snapshot, "{}", CodeFrame::new("py", formatted_code)).unwrap(); + write!(snapshot, "{}", CodeFrame::new("py", &formatted_code)).unwrap(); write!(snapshot, "{}", Header::new("Black Output")).unwrap(); write!(snapshot, "{}", CodeFrame::new("py", &expected_output)).unwrap(); @@ -89,21 +91,52 @@ fn format() { let test_file = |input_path: &Path| { let content = fs::read_to_string(input_path).unwrap(); - let printed = - format_module(&content, PyFormatOptions::default()).expect("Formatting to succeed"); + let options = PyFormatOptions::default(); + let printed = format_module(&content, options.clone()).expect("Formatting to succeed"); let formatted_code = printed.as_code(); - ensure_stability_when_formatting_twice(formatted_code); + ensure_stability_when_formatting_twice(formatted_code, options); - let snapshot = format!( - r#"## Input -{} + let mut snapshot = format!("## Input\n{}", CodeFrame::new("py", &content)); -## Output -{}"#, - CodeFrame::new("py", &content), - CodeFrame::new("py", formatted_code) - ); + let options_path = input_path.with_extension("options.json"); + if let Ok(options_file) = fs::File::open(options_path) { + let reader = BufReader::new(options_file); + let options: Vec = + serde_json::from_reader(reader).expect("Options to be a valid Json file"); + + writeln!(snapshot, "## Outputs").unwrap(); + + for (i, options) in options.into_iter().enumerate() { + let printed = + format_module(&content, options.clone()).expect("Formatting to succeed"); + let formatted_code = printed.as_code(); + + ensure_stability_when_formatting_twice(formatted_code, options.clone()); + + writeln!( + snapshot, + "### Output {}\n{}{}", + i + 1, + CodeFrame::new("", &DisplayPyOptions(&options)), + CodeFrame::new("py", &formatted_code) + ) + .unwrap(); + } + } else { + let options = PyFormatOptions::default(); + let printed = format_module(&content, options.clone()).expect("Formatting to succeed"); + let formatted_code = printed.as_code(); + + ensure_stability_when_formatting_twice(formatted_code, options); + + writeln!( + snapshot, + "## Output\n{}", + CodeFrame::new("py", &formatted_code) + ) + .unwrap(); + } insta::with_settings!({ omit_expression => true, @@ -118,8 +151,8 @@ fn format() { } /// Format another time and make sure that there are no changes anymore -fn ensure_stability_when_formatting_twice(formatted_code: &str) { - let reformatted = match format_module(formatted_code, PyFormatOptions::default()) { +fn ensure_stability_when_formatting_twice(formatted_code: &str, options: PyFormatOptions) { + let reformatted = match format_module(formatted_code, options) { Ok(reformatted) => reformatted, Err(err) => { panic!( @@ -170,11 +203,11 @@ impl std::fmt::Display for Header<'_> { struct CodeFrame<'a> { language: &'a str, - code: &'a str, + code: &'a dyn std::fmt::Display, } impl<'a> CodeFrame<'a> { - fn new(language: &'a str, code: &'a str) -> Self { + fn new(language: &'a str, code: &'a dyn std::fmt::Display) -> Self { Self { language, code } } } @@ -187,3 +220,21 @@ impl std::fmt::Display for CodeFrame<'_> { writeln!(f) } } + +struct DisplayPyOptions<'a>(&'a PyFormatOptions); + +impl fmt::Display for DisplayPyOptions<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + writeln!( + f, + r#"indent-style = {indent_style} +line-width = {line_width} +quote-style = {quote_style:?} +magic-trailing-comma = {magic_trailing_comma:?}"#, + indent_style = self.0.indent_style(), + line_width = self.0.line_width().value(), + quote_style = self.0.quote_style(), + magic_trailing_comma = self.0.magic_trailing_comma() + ) + } +} 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 a1871573c8..77901f7b5c 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 @@ -109,8 +109,6 @@ x53 = ( ``` - - ## Output ```py NOT_YET_IMPLEMENTED_StmtImportFrom @@ -196,3 +194,4 @@ x53 = a.askjdfahdlskjflsajfadhsaf.akjdsf.aksjdlfadhaljsashdfljaf.askjdflhasfdlas ``` + 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 a84c4a7201..84c5af2a92 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 @@ -200,8 +200,6 @@ for user_id in set(target_user_ids) - {u.user_id for u in updates}: updates.append(UserPresenceState.default(user_id)) ``` - - ## Output ```py ( @@ -447,3 +445,4 @@ for ( ``` + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__boolean_operation.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__boolean_operation.py.snap index 0b98b8df25..ab8e33067f 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__boolean_operation.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__boolean_operation.py.snap @@ -70,8 +70,6 @@ if ( pass ``` - - ## Output ```py if ( @@ -141,3 +139,4 @@ if ( ``` + 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 34e1cfbb3f..3e5eab9002 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 @@ -67,8 +67,6 @@ return 1 == 2 and ( ] ``` - - ## Output ```py a == b @@ -187,3 +185,4 @@ return ( ``` + 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 52a5965a90..25a854128c 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 @@ -64,8 +64,6 @@ a = { } ``` - - ## Output ```py # before @@ -131,3 +129,4 @@ a = { ``` + 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 892d93b62e..8930e0036c 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 @@ -16,8 +16,6 @@ a3 = [ ] ``` - - ## Output ```py # Dangling comment placement in empty lists @@ -33,3 +31,4 @@ a3 = [ ``` + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__slice.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__slice.py.snap index e316dd5218..ffc644c26b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__slice.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__slice.py.snap @@ -88,8 +88,6 @@ e202 = "e"[a() :: a()] e210 = "e"[a() : 1 :] ``` - - ## Output ```py # Handle comments both when lower and upper exist and when they don't @@ -175,3 +173,4 @@ e210 = "e"[NOT_IMPLEMENTED_call() : 1 :] ``` + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap index 4a2e6ed032..7943becf02 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap @@ -58,8 +58,6 @@ String \"\"\" ''' ``` - - ## Output ```py "' test" @@ -117,3 +115,4 @@ String \"\"\" ``` + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__tuple.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__tuple.py.snap index 2ddb3cd9ad..39b0d747e8 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__tuple.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__tuple.py.snap @@ -65,8 +65,6 @@ h2 = ((((1, "qweiurpoiqwurepqiurpqirpuqoiwrupqoirupqoirupqoiurpqiorupwqiourpqurp h3 = 1, "qweiurpoiqwurepqiurpqirpuqoiwrupqoirupqoirupqoiurpqiorupwqiourpqurpqurpqurpqurpqurpqurüqurqpuriq" ``` - - ## Output ```py # Non-wrapping parentheses checks @@ -264,3 +262,4 @@ h3 = ( ``` + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap index f461d88180..69feb3de88 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap @@ -144,8 +144,6 @@ if not \ pass ``` - - ## Output ```py if ( @@ -300,3 +298,4 @@ if not a: ``` + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap new file mode 100644 index 0000000000..e035e69a1c --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap @@ -0,0 +1,88 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py +--- +## Input +```py +( + "First entry", + "Second entry", + "last with trailing comma", +) + +( + "First entry", + "Second entry", + "last without trailing comma" +) + +( + "First entry", + "Second entry", + "third entry", + "fourth entry", + "fifth entry", + "sixt entry", + "seventh entry", + "eigth entry", +) +``` + +## Outputs +### Output 1 +``` +indent-style = Spaces, size: 4 +line-width = 88 +quote-style = Double +magic-trailing-comma = Respect +``` + +```py +( + "First entry", + "Second entry", + "last with trailing comma", +) + +("First entry", "Second entry", "last without trailing comma") + +( + "First entry", + "Second entry", + "third entry", + "fourth entry", + "fifth entry", + "sixt entry", + "seventh entry", + "eigth entry", +) +``` + + +### Output 2 +``` +indent-style = Spaces, size: 4 +line-width = 88 +quote-style = Double +magic-trailing-comma = Ignore +``` + +```py +("First entry", "Second entry", "last with trailing comma") + +("First entry", "Second entry", "last without trailing comma") + +( + "First entry", + "Second entry", + "third entry", + "fourth entry", + "fifth entry", + "sixt entry", + "seventh entry", + "eigth entry", +) +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__assign.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__assign.py.snap index 4d02cf905c..efcd0cca34 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__assign.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__assign.py.snap @@ -16,8 +16,6 @@ a2 = ( a = asdf = fjhalsdljfalflaflapamsakjsdhflakjdslfjhalsdljfalflaflapamsakjsdhflakjdslfjhalsdljfal = 1 ``` - - ## Output ```py # break left hand side @@ -31,3 +29,4 @@ a = asdf = fjhalsdljfalflaflapamsakjsdhflakjdslfjhalsdljfalflaflapamsakjsdhflakj ``` + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__break.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__break.py.snap index d48901046e..1cffa28bec 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__break.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__break.py.snap @@ -11,8 +11,6 @@ while True: # block comment # post comment ``` - - ## Output ```py # leading comment @@ -23,3 +21,4 @@ while True: # block comment ``` + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap index 49aa2a4879..3fbc4be1a5 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap @@ -42,8 +42,6 @@ class Test(Aaaa): # trailing comment pass ``` - - ## Output ```py class Test( @@ -95,3 +93,4 @@ class Test(Aaaa): # trailing comment ``` + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__for.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__for.py.snap index a240652d9c..b350ab75f4 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__for.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__for.py.snap @@ -40,8 +40,6 @@ for x in (): # type: int ... ``` - - ## Output ```py for x in y: # trailing test comment @@ -85,3 +83,4 @@ for x in (): # type: int ``` + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__function.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__function.py.snap index 00c72398e5..341956733f 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__function.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__function.py.snap @@ -239,8 +239,6 @@ def f42( pass ``` - - ## Output ```py # Dangling comments @@ -516,3 +514,4 @@ def f42( ``` + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__if.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__if.py.snap index 6292de6914..9bbe6803f2 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__if.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__if.py.snap @@ -79,8 +79,6 @@ else: # Comment pass ``` - - ## Output ```py if x == y: # trailing if condition @@ -158,3 +156,4 @@ else: # Comment ``` + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__while.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__while.py.snap index 799424b4ea..3aa590915d 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__while.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__while.py.snap @@ -36,8 +36,6 @@ while ( print("Do something") ``` - - ## Output ```py while 34: # trailing test comment @@ -74,3 +72,4 @@ while ( ``` + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@trivia.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@trivia.py.snap index 773e564e68..e9c3d37c5b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@trivia.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@trivia.py.snap @@ -38,8 +38,6 @@ while b == 20: e = 50 # one empty line before ``` - - ## Output ```py # Removes the line above @@ -79,3 +77,4 @@ e = 50 # one empty line before ``` + From 313711aaf9efeffe4bb60e351e90aee5b362cdd1 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 26 Jun 2023 14:24:25 +0200 Subject: [PATCH 232/447] Prefer the configured quote style ## Summary This PR extends the string formatting to respect the configured quote style. ## Test Plan Extended the string test with new cases and set it up to run twice: Once with the `quote_style: Doube`, and once with `quote_style: Single` single and double quotes. --- .../ruff/expression/string.options.json | 8 ++ .../test/fixtures/ruff/expression/string.py | 12 +- .../src/expression/string.rs | 59 +++++++--- crates/ruff_python_formatter/src/options.rs | 2 +- .../format@expression__string.py.snap | 108 +++++++++++++++++- 5 files changed, 165 insertions(+), 24 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.options.json diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.options.json new file mode 100644 index 0000000000..7d6d0512c2 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.options.json @@ -0,0 +1,8 @@ +[ + { + "quote_style": "double" + }, + { + "quote_style": "single" + } +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py index 7ae9032c80..7ddb0f1cdb 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py @@ -12,7 +12,7 @@ # Prefer double quotes for string with equal amount of single and double quotes '" \' " " \'\'' -"' \" '' \" \" '" +"' \" '' \" \"" "\\' \"\"" '\\\' ""' @@ -47,6 +47,16 @@ String "" String """ ''' +'''Multiline +String "''' + +"""Multiline +String ''' +""" + +"""Multiline +String '""" + '''Multiline String \"\"\" ''' diff --git a/crates/ruff_python_formatter/src/expression/string.rs b/crates/ruff_python_formatter/src/expression/string.rs index 116226fc1d..9d27ab87c1 100644 --- a/crates/ruff_python_formatter/src/expression/string.rs +++ b/crates/ruff_python_formatter/src/expression/string.rs @@ -58,7 +58,8 @@ impl Format> for FormatStringPart { let raw_content_range = relative_raw_content_range + self.part_range.start(); let raw_content = &string_content[relative_raw_content_range]; - let (preferred_quotes, contains_newlines) = preferred_quotes(raw_content, quotes); + let (preferred_quotes, contains_newlines) = + preferred_quotes(raw_content, quotes, f.options().quote_style()); write!(f, [prefix, preferred_quotes])?; @@ -148,14 +149,20 @@ impl Format> for StringPrefix { /// Detects the preferred quotes for `input`. /// * single quoted strings: The preferred quote style is the one that requires less escape sequences. /// * triple quoted strings: Use double quotes except the string contains a sequence of `"""`. -fn preferred_quotes(input: &str, quotes: StringQuotes) -> (StringQuotes, ContainsNewlines) { +fn preferred_quotes( + input: &str, + quotes: StringQuotes, + configured_style: QuoteStyle, +) -> (StringQuotes, ContainsNewlines) { let mut contains_newlines = ContainsNewlines::No; let preferred_style = if quotes.triple { - let mut use_single_quotes = false; + // True if the string contains a triple quote sequence of the configured quote style. + let mut uses_triple_quotes = false; let mut chars = input.chars().peekable(); while let Some(c) = chars.next() { + let configured_quote_char = configured_style.as_char(); match c { '\n' | '\r' => contains_newlines = ContainsNewlines::Yes, '\\' => { @@ -163,24 +170,25 @@ fn preferred_quotes(input: &str, quotes: StringQuotes) -> (StringQuotes, Contain chars.next(); } } - '"' => { + // `"` or `'` + c if c == configured_quote_char => { match chars.peek().copied() { - Some('"') => { - // `""` + Some(c) if c == configured_quote_char => { + // `""` or `''` chars.next(); - if chars.peek().copied() == Some('"') { - // `"""` + if chars.peek().copied() == Some(configured_quote_char) { + // `"""` or `'''` chars.next(); - use_single_quotes = true; + uses_triple_quotes = true; } } Some(_) => { - // Single quote, this is ok + // A single quote char, this is ok } None => { // Trailing quote at the end of the comment - use_single_quotes = true; + uses_triple_quotes = true; } } } @@ -188,10 +196,12 @@ fn preferred_quotes(input: &str, quotes: StringQuotes) -> (StringQuotes, Contain } } - if use_single_quotes { - QuoteStyle::Single + if uses_triple_quotes { + // String contains a triple quote sequence of the configured quote style. + // Keep the existing quote style. + quotes.style } else { - QuoteStyle::Double + configured_style } } else { let mut single_quotes = 0u32; @@ -215,10 +225,21 @@ fn preferred_quotes(input: &str, quotes: StringQuotes) -> (StringQuotes, Contain } } - if double_quotes > single_quotes { - QuoteStyle::Single - } else { - QuoteStyle::Double + match configured_style { + QuoteStyle::Single => { + if single_quotes > double_quotes { + QuoteStyle::Double + } else { + QuoteStyle::Single + } + } + QuoteStyle::Double => { + if double_quotes > single_quotes { + QuoteStyle::Single + } else { + QuoteStyle::Double + } + } } }; @@ -286,7 +307,7 @@ fn normalize_quotes(input: &str, quotes: StringQuotes) -> Cow { let style = quotes.style; let preferred_quote = style.as_char(); - let opposite_quote = style.opposite().as_char(); + let opposite_quote = style.invert().as_char(); let mut chars = input.char_indices(); diff --git a/crates/ruff_python_formatter/src/options.rs b/crates/ruff_python_formatter/src/options.rs index e886a30b60..65bcf65552 100644 --- a/crates/ruff_python_formatter/src/options.rs +++ b/crates/ruff_python_formatter/src/options.rs @@ -109,7 +109,7 @@ impl QuoteStyle { } #[must_use] - pub const fn opposite(self) -> QuoteStyle { + pub const fn invert(self) -> QuoteStyle { match self { QuoteStyle::Single => QuoteStyle::Double, QuoteStyle::Double => QuoteStyle::Single, diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap index 7943becf02..6dd87f155f 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap @@ -18,7 +18,7 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression # Prefer double quotes for string with equal amount of single and double quotes '" \' " " \'\'' -"' \" '' \" \" '" +"' \" '' \" \"" "\\' \"\"" '\\\' ""' @@ -53,12 +53,30 @@ String "" String """ ''' +'''Multiline +String "''' + +"""Multiline +String ''' +""" + +"""Multiline +String '""" + '''Multiline String \"\"\" ''' ``` -## Output +## Outputs +### Output 1 +``` +indent-style = Spaces, size: 4 +line-width = 88 +quote-style = Double +magic-trailing-comma = Respect +``` + ```py "' test" '" test' @@ -74,7 +92,7 @@ String \"\"\" # Prefer double quotes for string with equal amount of single and double quotes "\" ' \" \" ''" -"' \" '' \" \" '" +"' \" '' \" \"" '\\\' ""' '\\\' ""' @@ -109,10 +127,94 @@ String "" String """ ''' +'''Multiline +String "''' + +"""Multiline +String ''' +""" + +"""Multiline +String '""" + """Multiline String \"\"\" """ ``` +### Output 2 +``` +indent-style = Spaces, size: 4 +line-width = 88 +quote-style = Single +magic-trailing-comma = Respect +``` + +```py +"' test" +'" test' + +'" test' +"' test" + +# Prefer single quotes for string with more double quotes +'\' " " \'\' " " \'' + +# Prefer double quotes for string with more single quotes +'\' " " \'\' " " \'' + +# Prefer double quotes for string with equal amount of single and double quotes +'" \' " " \'\'' +'\' " \'\' " "' + +'\\\' ""' +'\\\' ""' + + +'Test' +'Test' + +r'Test' +R'Test' + +'This string will not include \ +backslashes or newline characters.' + +if True: + 'This string will not include \ + backslashes or newline characters.' + +'''Multiline +String \" +''' + +'''Multiline +String \' +''' + +'''Multiline +String "" +''' + +'''Multiline +String """ +''' + +'''Multiline +String "''' + +"""Multiline +String ''' +""" + +"""Multiline +String '""" + +'''Multiline +String \"\"\" +''' +``` + + From 49cabca3e741e2ab470bf0424f321972f34758e2 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 26 Jun 2023 14:41:47 +0200 Subject: [PATCH 233/447] Format implicit string continuation (#5328) --- .../test/fixtures/ruff/expression/string.py | 48 +++++ crates/ruff_python_formatter/src/builders.rs | 27 ++- .../src/expression/expr_constant.rs | 35 ++-- .../src/expression/expr_tuple.rs | 21 +- .../src/expression/mod.rs | 20 +- .../src/expression/parentheses.rs | 2 +- .../src/expression/string.rs | 149 ++++++++++++-- .../src/statement/stmt_expr.rs | 11 +- .../format@expression__string.py.snap | 190 ++++++++++++++++++ 9 files changed, 443 insertions(+), 60 deletions(-) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py index 7ddb0f1cdb..fff7aae96c 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py @@ -60,3 +60,51 @@ String '""" '''Multiline String \"\"\" ''' + +# String continuation + +"Let's" "start" "with" "a" "simple" "example" + +"Let's" "start" "with" "a" "simple" "example" "now repeat after me:" "I am confident" "I am confident" "I am confident" "I am confident" "I am confident" + +( + "Let's" "start" "with" "a" "simple" "example" "now repeat after me:" "I am confident" "I am confident" "I am confident" "I am confident" "I am confident" +) + +if ( + a + "Let's" + "start" + "with" + "a" + "simple" + "example" + "now repeat after me:" + "I am confident" + "I am confident" + "I am confident" + "I am confident" + "I am confident" +): + pass + +if "Let's" "start" "with" "a" "simple" "example" "now repeat after me:" "I am confident" "I am confident" "I am confident" "I am confident" "I am confident": + pass + +( + # leading + "a" # trailing part commen + + # leading part comment + + "b" # trailing second part comment + # trailing +) + +test_particular = [ + # squares + '1.00000000100000000025', + '1.0000000000000000000000000100000000000000000000000' #... + '00025', + '1.0000000000000000000000000000000000000000000010000' #... + '0000000000000000000000000000000000000000025', +] diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index d3b64ce34b..962707a160 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -1,10 +1,35 @@ use crate::context::NodeLevel; use crate::prelude::*; use crate::trivia::{first_non_trivia_token, lines_after, skip_trailing_trivia, Token, TokenKind}; -use ruff_formatter::write; +use ruff_formatter::{format_args, write, Argument, Arguments}; use ruff_text_size::TextSize; use rustpython_parser::ast::Ranged; +/// Adds parentheses and indents `content` if it doesn't fit on a line. +pub(crate) fn optional_parentheses<'ast, T>(content: &T) -> OptionalParentheses<'_, 'ast> +where + T: Format>, +{ + OptionalParentheses { + inner: Argument::new(content), + } +} + +pub(crate) struct OptionalParentheses<'a, 'ast> { + inner: Argument<'a, PyFormatContext<'ast>>, +} + +impl<'ast> Format> for OptionalParentheses<'_, 'ast> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + group(&format_args![ + if_group_breaks(&text("(")), + soft_block_indent(&Arguments::from(&self.inner)), + if_group_breaks(&text(")")) + ]) + .fmt(f) + } +} + /// Provides Python specific extensions to [`Formatter`]. pub(crate) trait PyFormatterExtensions<'ast, 'buf> { /// Creates a joiner that inserts the appropriate number of empty lines between two nodes, depending on the diff --git a/crates/ruff_python_formatter/src/expression/expr_constant.rs b/crates/ruff_python_formatter/src/expression/expr_constant.rs index d1d7de417b..68d538ae8a 100644 --- a/crates/ruff_python_formatter/src/expression/expr_constant.rs +++ b/crates/ruff_python_formatter/src/expression/expr_constant.rs @@ -2,14 +2,25 @@ use crate::comments::Comments; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; -use crate::expression::string::FormatString; +use crate::expression::string::{FormatString, StringLayout}; use crate::prelude::*; use crate::{not_yet_implemented_custom_text, verbatim_text, FormatNodeRule}; -use ruff_formatter::write; +use ruff_formatter::{write, FormatRuleWithOptions}; use rustpython_parser::ast::{Constant, ExprConstant}; #[derive(Default)] -pub struct FormatExprConstant; +pub struct FormatExprConstant { + string_layout: StringLayout, +} + +impl FormatRuleWithOptions> for FormatExprConstant { + type Options = StringLayout; + + fn with_options(mut self, options: Self::Options) -> Self { + self.string_layout = options; + self + } +} impl FormatNodeRule for FormatExprConstant { fn fmt_fields(&self, item: &ExprConstant, f: &mut PyFormatter) -> FormatResult<()> { @@ -29,7 +40,7 @@ impl FormatNodeRule for FormatExprConstant { Constant::Int(_) | Constant::Float(_) | Constant::Complex { .. } => { write!(f, [verbatim_text(item)]) } - Constant::Str(_) => FormatString::new(item).fmt(f), + Constant::Str(_) => FormatString::new(item, self.string_layout).fmt(f), Constant::Bytes(_) => { not_yet_implemented_custom_text(r#"b"NOT_YET_IMPLEMENTED_BYTE_STRING""#).fmt(f) } @@ -44,14 +55,6 @@ impl FormatNodeRule for FormatExprConstant { _node: &ExprConstant, _f: &mut PyFormatter, ) -> FormatResult<()> { - // TODO(konstin): Reactivate when string formatting works, currently a source of unstable - // formatting, e.g.: - // magic_methods = ( - // "enter exit " - // # we added divmod and rdivmod here instead of numerics - // # because there is no idivmod - // "divmod rdivmod neg pos abs invert " - // ) Ok(()) } } @@ -64,6 +67,14 @@ impl NeedsParentheses for ExprConstant { comments: &Comments, ) -> Parentheses { match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + Parentheses::Optional if self.value.is_str() && parenthesize.is_if_breaks() => { + // Custom handling that only adds parentheses for implicit concatenated strings. + if parenthesize.is_if_breaks() { + Parentheses::Custom + } else { + Parentheses::Optional + } + } Parentheses::Optional => Parentheses::Never, parentheses => parentheses, } diff --git a/crates/ruff_python_formatter/src/expression/expr_tuple.rs b/crates/ruff_python_formatter/src/expression/expr_tuple.rs index 57c78170cd..875954280e 100644 --- a/crates/ruff_python_formatter/src/expression/expr_tuple.rs +++ b/crates/ruff_python_formatter/src/expression/expr_tuple.rs @@ -1,13 +1,10 @@ -use crate::builders::PyFormatterExtensions; +use crate::builders::optional_parentheses; use crate::comments::{dangling_node_comments, Comments}; -use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; -use crate::{AsFormat, FormatNodeRule, PyFormatter}; -use ruff_formatter::formatter::Formatter; -use ruff_formatter::prelude::{block_indent, group, if_group_breaks, soft_block_indent, text}; -use ruff_formatter::{format_args, write, Buffer, Format, FormatResult, FormatRuleWithOptions}; +use crate::prelude::*; +use ruff_formatter::{format_args, write, FormatRuleWithOptions}; use ruff_python_ast::prelude::{Expr, Ranged}; use ruff_text_size::TextRange; use rustpython_parser::ast::ExprTuple; @@ -100,17 +97,7 @@ impl FormatNodeRule for FormatExprTuple { ])] ) } - elts => { - write!( - f, - [group(&format_args![ - // If there were previously no parentheses, add them only if the group breaks - if_group_breaks(&text("(")), - soft_block_indent(&ExprSequence::new(elts)), - if_group_breaks(&text(")")), - ])] - ) - } + elts => optional_parentheses(&ExprSequence::new(elts)).fmt(f), } } diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 06292c6447..20ba4807ee 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -1,7 +1,9 @@ +use crate::builders::optional_parentheses; use crate::comments::Comments; use crate::context::NodeLevel; use crate::expression::expr_tuple::TupleParentheses; use crate::expression::parentheses::{NeedsParentheses, Parentheses, Parenthesize}; +use crate::expression::string::StringLayout; use crate::prelude::*; use ruff_formatter::{ format_args, write, FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions, @@ -37,7 +39,7 @@ pub(crate) mod expr_unary_op; pub(crate) mod expr_yield; pub(crate) mod expr_yield_from; pub(crate) mod parentheses; -mod string; +pub(crate) mod string; #[derive(Default)] pub struct FormatExpr { @@ -81,7 +83,10 @@ impl FormatRule> for FormatExpr { Expr::Call(expr) => expr.format().fmt(f), Expr::FormattedValue(expr) => expr.format().fmt(f), Expr::JoinedStr(expr) => expr.format().fmt(f), - Expr::Constant(expr) => expr.format().fmt(f), + Expr::Constant(expr) => expr + .format() + .with_options(StringLayout::Default(Some(parentheses))) + .fmt(f), Expr::Attribute(expr) => expr.format().fmt(f), Expr::Subscript(expr) => expr.format().fmt(f), Expr::Starred(expr) => expr.format().fmt(f), @@ -109,16 +114,7 @@ impl FormatRule> for FormatExpr { ) } // Add optional parentheses. Ignore if the item renders parentheses itself. - Parentheses::Optional => { - write!( - f, - [group(&format_args![ - if_group_breaks(&text("(")), - soft_block_indent(&format_expr), - if_group_breaks(&text(")")) - ])] - ) - } + Parentheses::Optional => optional_parentheses(&format_expr).fmt(f), Parentheses::Custom | Parentheses::Never => Format::fmt(&format_expr, f), }; diff --git a/crates/ruff_python_formatter/src/expression/parentheses.rs b/crates/ruff_python_formatter/src/expression/parentheses.rs index c535d65745..3df553f126 100644 --- a/crates/ruff_python_formatter/src/expression/parentheses.rs +++ b/crates/ruff_python_formatter/src/expression/parentheses.rs @@ -104,7 +104,7 @@ pub enum Parentheses { Never, } -fn is_expression_parenthesized(expr: AnyNodeRef, contents: &str) -> bool { +pub(crate) fn is_expression_parenthesized(expr: AnyNodeRef, contents: &str) -> bool { matches!( first_non_trivia_token(expr.end(), contents), Some(Token { diff --git a/crates/ruff_python_formatter/src/expression/string.rs b/crates/ruff_python_formatter/src/expression/string.rs index 9d27ab87c1..055b5d3642 100644 --- a/crates/ruff_python_formatter/src/expression/string.rs +++ b/crates/ruff_python_formatter/src/expression/string.rs @@ -1,35 +1,152 @@ +use crate::builders::optional_parentheses; +use crate::comments::{leading_comments, trailing_comments}; +use crate::expression::parentheses::Parentheses; use crate::prelude::*; -use crate::{not_yet_implemented_custom_text, QuoteStyle}; +use crate::QuoteStyle; use bitflags::bitflags; -use ruff_formatter::{write, FormatError}; +use ruff_formatter::{format_args, write, FormatError}; use ruff_python_ast::str::is_implicit_concatenation; use ruff_text_size::{TextLen, TextRange, TextSize}; use rustpython_parser::ast::{ExprConstant, Ranged}; +use rustpython_parser::lexer::lex_starts_at; +use rustpython_parser::{Mode, Tok}; use std::borrow::Cow; -pub(super) struct FormatString { - string_range: TextRange, +#[derive(Copy, Clone, Debug)] +pub enum StringLayout { + Default(Option), + + /// Enforces that implicit continuation strings are printed on a single line even if they exceed + /// the configured line width. + Flat, } -impl FormatString { - pub(super) fn new(constant: &ExprConstant) -> Self { +impl Default for StringLayout { + fn default() -> Self { + Self::Default(None) + } +} + +pub(super) struct FormatString<'a> { + constant: &'a ExprConstant, + layout: StringLayout, +} + +impl<'a> FormatString<'a> { + pub(super) fn new(constant: &'a ExprConstant, layout: StringLayout) -> Self { debug_assert!(constant.value.is_str()); - Self { - string_range: constant.range(), + Self { constant, layout } + } +} + +impl<'a> Format> for FormatString<'a> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let string_range = self.constant.range(); + let string_content = f.context().locator().slice(string_range); + + if is_implicit_concatenation(string_content) { + let format_continuation = FormatStringContinuation::new(self.constant, self.layout); + + if let StringLayout::Default(Some(Parentheses::Custom)) = self.layout { + optional_parentheses(&format_continuation).fmt(f) + } else { + format_continuation.fmt(f) + } + } else { + FormatStringPart::new(string_range).fmt(f) } } } -impl Format> for FormatString { - fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { - let string_content = f.context().locator().slice(self.string_range); +struct FormatStringContinuation<'a> { + constant: &'a ExprConstant, + layout: StringLayout, +} - if is_implicit_concatenation(string_content) { - not_yet_implemented_custom_text(r#""NOT_YET_IMPLEMENTED" "IMPLICIT_CONCATENATION""#) - .fmt(f) - } else { - FormatStringPart::new(self.string_range).fmt(f) +impl<'a> FormatStringContinuation<'a> { + fn new(constant: &'a ExprConstant, layout: StringLayout) -> Self { + debug_assert!(constant.value.is_str()); + Self { constant, layout } + } +} + +impl Format> for FormatStringContinuation<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let comments = f.context().comments().clone(); + let locator = f.context().locator(); + let mut dangling_comments = comments.dangling_comments(self.constant); + + let string_range = self.constant.range(); + let string_content = locator.slice(string_range); + + // The AST parses implicit concatenation as a single string. + // Call into the lexer to extract the individual chunks and format each string on its own. + // This code does not yet implement the automatic joining of strings that fit on the same line + // because this is a black preview style. + let lexer = lex_starts_at(string_content, Mode::Module, string_range.start()); + + let separator = format_with(|f| match self.layout { + StringLayout::Default(_) => soft_line_break_or_space().fmt(f), + StringLayout::Flat => space().fmt(f), + }); + + let mut joiner = f.join_with(separator); + + for token in lexer { + let (token, token_range) = token.map_err(|_| FormatError::SyntaxError)?; + + match token { + Tok::String { .. } => { + // ```python + // ( + // "a" + // # leading + // "the comment above" + // ) + // ``` + let leading_comments_end = dangling_comments + .partition_point(|comment| comment.slice().start() <= token_range.start()); + + let (leading_part_comments, rest) = + dangling_comments.split_at(leading_comments_end); + + // ```python + // ( + // "a" # trailing comment + // "the comment above" + // ) + // ``` + let trailing_comments_end = rest.partition_point(|comment| { + comment.line_position().is_end_of_line() + && !locator.contains_line_break(TextRange::new( + token_range.end(), + comment.slice().start(), + )) + }); + + let (trailing_part_comments, rest) = rest.split_at(trailing_comments_end); + + joiner.entry(&format_args![ + line_suffix_boundary(), + leading_comments(leading_part_comments), + FormatStringPart::new(token_range), + trailing_comments(trailing_part_comments) + ]); + + dangling_comments = rest; + } + Tok::Comment(_) + | Tok::NonLogicalNewline + | Tok::Newline + | Tok::Indent + | Tok::Dedent => continue, + token => unreachable!("Unexpected token {token:?}"), + } } + + debug_assert!(dangling_comments.is_empty()); + + joiner.finish() } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_expr.rs b/crates/ruff_python_formatter/src/statement/stmt_expr.rs index ffb86ee9df..b0c451fa6a 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_expr.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_expr.rs @@ -1,4 +1,5 @@ -use crate::expression::parentheses::Parenthesize; +use crate::expression::parentheses::{is_expression_parenthesized, Parenthesize}; +use crate::expression::string::StringLayout; use crate::prelude::*; use crate::FormatNodeRule; use rustpython_parser::ast::StmtExpr; @@ -10,6 +11,14 @@ impl FormatNodeRule for FormatStmtExpr { fn fmt_fields(&self, item: &StmtExpr, f: &mut PyFormatter) -> FormatResult<()> { let StmtExpr { value, .. } = item; + if let Some(constant) = value.as_constant_expr() { + if constant.value.is_str() + && !is_expression_parenthesized(value.as_ref().into(), f.context().contents()) + { + return constant.format().with_options(StringLayout::Flat).fmt(f); + } + } + value.format().with_options(Parenthesize::Optional).fmt(f) } } diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap index 6dd87f155f..94fc0a6049 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap @@ -66,6 +66,54 @@ String '""" '''Multiline String \"\"\" ''' + +# String continuation + +"Let's" "start" "with" "a" "simple" "example" + +"Let's" "start" "with" "a" "simple" "example" "now repeat after me:" "I am confident" "I am confident" "I am confident" "I am confident" "I am confident" + +( + "Let's" "start" "with" "a" "simple" "example" "now repeat after me:" "I am confident" "I am confident" "I am confident" "I am confident" "I am confident" +) + +if ( + a + "Let's" + "start" + "with" + "a" + "simple" + "example" + "now repeat after me:" + "I am confident" + "I am confident" + "I am confident" + "I am confident" + "I am confident" +): + pass + +if "Let's" "start" "with" "a" "simple" "example" "now repeat after me:" "I am confident" "I am confident" "I am confident" "I am confident" "I am confident": + pass + +( + # leading + "a" # trailing part commen + + # leading part comment + + "b" # trailing second part comment + # trailing +) + +test_particular = [ + # squares + '1.00000000100000000025', + '1.0000000000000000000000000100000000000000000000000' #... + '00025', + '1.0000000000000000000000000000000000000000000010000' #... + '0000000000000000000000000000000000000000025', +] ``` ## Outputs @@ -140,6 +188,77 @@ String '""" """Multiline String \"\"\" """ + +# String continuation + +"Let's" "start" "with" "a" "simple" "example" + +"Let's" "start" "with" "a" "simple" "example" "now repeat after me:" "I am confident" "I am confident" "I am confident" "I am confident" "I am confident" + +( + "Let's" + "start" + "with" + "a" + "simple" + "example" + "now repeat after me:" + "I am confident" + "I am confident" + "I am confident" + "I am confident" + "I am confident" +) + +if ( + a + + "Let's" + "start" + "with" + "a" + "simple" + "example" + "now repeat after me:" + "I am confident" + "I am confident" + "I am confident" + "I am confident" + "I am confident" +): + pass + +if ( + "Let's" + "start" + "with" + "a" + "simple" + "example" + "now repeat after me:" + "I am confident" + "I am confident" + "I am confident" + "I am confident" + "I am confident" +): + pass + +( + # leading + "a" # trailing part commen + # leading part comment + "b" # trailing second part comment + # trailing +) + +test_particular = [ + # squares + "1.00000000100000000025", + "1.0000000000000000000000000100000000000000000000000" # ... + "00025", + "1.0000000000000000000000000000000000000000000010000" # ... + "0000000000000000000000000000000000000000025", +] ``` @@ -214,6 +333,77 @@ String '""" '''Multiline String \"\"\" ''' + +# String continuation + +"Let's" 'start' 'with' 'a' 'simple' 'example' + +"Let's" 'start' 'with' 'a' 'simple' 'example' 'now repeat after me:' 'I am confident' 'I am confident' 'I am confident' 'I am confident' 'I am confident' + +( + "Let's" + 'start' + 'with' + 'a' + 'simple' + 'example' + 'now repeat after me:' + 'I am confident' + 'I am confident' + 'I am confident' + 'I am confident' + 'I am confident' +) + +if ( + a + + "Let's" + 'start' + 'with' + 'a' + 'simple' + 'example' + 'now repeat after me:' + 'I am confident' + 'I am confident' + 'I am confident' + 'I am confident' + 'I am confident' +): + pass + +if ( + "Let's" + 'start' + 'with' + 'a' + 'simple' + 'example' + 'now repeat after me:' + 'I am confident' + 'I am confident' + 'I am confident' + 'I am confident' + 'I am confident' +): + pass + +( + # leading + 'a' # trailing part commen + # leading part comment + 'b' # trailing second part comment + # trailing +) + +test_particular = [ + # squares + '1.00000000100000000025', + '1.0000000000000000000000000100000000000000000000000' # ... + '00025', + '1.0000000000000000000000000000000000000000000010000' # ... + '0000000000000000000000000000000000000000025', +] ``` From d00559e42abc76f0edbddc7de9bb1c273dbde145 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Mon, 26 Jun 2023 15:09:06 +0100 Subject: [PATCH 234/447] format StmtWith (#5350) --- .../test/fixtures/ruff/statement/with.py | 54 +++++++ .../src/expression/expr_starred.rs | 11 +- .../src/other/with_item.rs | 22 ++- .../src/statement/stmt_with.rs | 35 ++++- .../black_compatibility@comments5.py.snap | 20 +-- .../black_compatibility@composition.py.snap | 50 +++++-- ...lity@composition_no_trailing_comma.py.snap | 50 +++++-- .../black_compatibility@expression.py.snap | 32 ++--- .../black_compatibility@fmtonoff.py.snap | 4 +- .../black_compatibility@fmtskip8.py.snap | 8 +- .../black_compatibility@function2.py.snap | 20 +-- ...move_newline_after_code_block_open.py.snap | 22 ++- .../snapshots/format@statement__with.py.snap | 132 ++++++++++++++++++ 13 files changed, 380 insertions(+), 80 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/with.py create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/with.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/with.py new file mode 100644 index 0000000000..4a664a1d70 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/with.py @@ -0,0 +1,54 @@ +with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: + ... + # trailing + +with a, a: # after colon + ... + # trailing + +with ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, +): + ... + # trailing + + +with ( + a # a + , # comma + b # c + ): # colon + ... + + +with ( + a # a + as # as + b # b + , # comma + c # c + ): # colon + ... # body + # body trailing own + + +with (a,): # magic trailing comma + ... + + +with (a): # should remove brackets + ... + +# TODO: black doesn't wrap this, but maybe we want to anyway? +# 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: + ... + + +# currently unparsable by black: https://github.com/psf/black/issues/3678 +with (name_2 for name_0 in name_4): + pass +with (a, *b): + pass diff --git a/crates/ruff_python_formatter/src/expression/expr_starred.rs b/crates/ruff_python_formatter/src/expression/expr_starred.rs index 14806cb52b..3711deb92e 100644 --- a/crates/ruff_python_formatter/src/expression/expr_starred.rs +++ b/crates/ruff_python_formatter/src/expression/expr_starred.rs @@ -2,7 +2,7 @@ use crate::comments::Comments; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; +use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; use rustpython_parser::ast::ExprStarred; @@ -10,8 +10,13 @@ use rustpython_parser::ast::ExprStarred; pub struct FormatExprStarred; impl FormatNodeRule for FormatExprStarred { - fn fmt_fields(&self, item: &ExprStarred, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + fn fmt_fields(&self, _item: &ExprStarred, f: &mut PyFormatter) -> FormatResult<()> { + write!( + f, + [not_yet_implemented_custom_text( + "*NOT_YET_IMPLEMENTED_ExprStarred" + )] + ) } } diff --git a/crates/ruff_python_formatter/src/other/with_item.rs b/crates/ruff_python_formatter/src/other/with_item.rs index 3b13b582a0..0c3f696e27 100644 --- a/crates/ruff_python_formatter/src/other/with_item.rs +++ b/crates/ruff_python_formatter/src/other/with_item.rs @@ -1,4 +1,6 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; +use crate::expression::parentheses::Parenthesize; +use crate::prelude::*; +use crate::{FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; use rustpython_parser::ast::WithItem; @@ -7,6 +9,22 @@ pub struct FormatWithItem; impl FormatNodeRule for FormatWithItem { fn fmt_fields(&self, item: &WithItem, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let WithItem { + range: _, + context_expr, + optional_vars, + } = item; + + let inner = format_with(|f| { + write!( + f, + [context_expr.format().with_options(Parenthesize::IfBreaks)] + )?; + if let Some(optional_vars) = optional_vars { + write!(f, [space(), text("as"), space(), optional_vars.format()])?; + } + Ok(()) + }); + write!(f, [group(&inner)]) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_with.rs b/crates/ruff_python_formatter/src/statement/stmt_with.rs index a68ee33113..c01b8439f9 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_with.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_with.rs @@ -1,5 +1,9 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; +use crate::builders::optional_parentheses; +use crate::comments::trailing_comments; +use crate::prelude::*; +use crate::{FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::prelude::*; use rustpython_parser::ast::StmtWith; #[derive(Default)] @@ -7,6 +11,33 @@ pub struct FormatStmtWith; impl FormatNodeRule for FormatStmtWith { fn fmt_fields(&self, item: &StmtWith, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let StmtWith { + range: _, + items, + body, + type_comment: _, + } = item; + + let comments = f.context().comments().clone(); + let dangling_comments = comments.dangling_comments(item.as_any_node_ref()); + + let joined_items = format_with(|f| f.join_comma_separated().nodes(items.iter()).finish()); + + write!( + f, + [ + text("with"), + space(), + group(&optional_parentheses(&joined_items)), + text(":"), + trailing_comments(dangling_comments), + block_indent(&body.format()) + ] + ) + } + + fn fmt_dangling_comments(&self, _node: &StmtWith, _f: &mut PyFormatter) -> FormatResult<()> { + // Handled in `fmt_fields` + Ok(()) } } diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments5.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments5.py.snap index 269d9cdd25..68ef84e041 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments5.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments5.py.snap @@ -93,7 +93,7 @@ if __name__ == "__main__": # Comment belongs to the `if` block. # This one belongs to the `while` block. -@@ -8,26 +8,20 @@ +@@ -8,26 +8,21 @@ # This one is properly standalone now. @@ -110,22 +110,23 @@ if __name__ == "__main__": -with open(some_temp_file) as f: - data = f.read() -+NOT_YET_IMPLEMENTED_StmtWith - +- -try: - with open(some_other_file) as w: - w.write(data) -+NOT_YET_IMPLEMENTED_StmtTry ++with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) as f: ++ data = NOT_IMPLEMENTED_call() -except OSError: - print("problems") -- ++NOT_YET_IMPLEMENTED_StmtTry + -import sys +NOT_YET_IMPLEMENTED_StmtImport # leading function comment -@@ -42,7 +36,7 @@ +@@ -42,7 +37,7 @@ # leading 1 @deco1 # leading 2 @@ -134,7 +135,7 @@ if __name__ == "__main__": # leading 3 @deco3 def decorated1(): -@@ -52,7 +46,7 @@ +@@ -52,7 +47,7 @@ # leading 1 @deco1 # leading 2 @@ -143,7 +144,7 @@ if __name__ == "__main__": # leading function comment def decorated1(): ... -@@ -70,4 +64,4 @@ +@@ -70,4 +65,4 @@ if __name__ == "__main__": @@ -173,7 +174,8 @@ for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # and finally we loop around -NOT_YET_IMPLEMENTED_StmtWith +with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) as f: + data = NOT_IMPLEMENTED_call() NOT_YET_IMPLEMENTED_StmtTry diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition.py.snap index 25490c1887..127846ac18 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition.py.snap @@ -193,7 +193,7 @@ class C: ```diff --- Black +++ Ruff -@@ -1,159 +1,42 @@ +@@ -1,23 +1,10 @@ class C: def test(self) -> None: - with patch("black.out", print): @@ -214,18 +214,22 @@ class C: - "2 files reformatted, 2 files left unchanged, 2 files failed to" - " reformat.", - ) -- for i in (a,): -- if ( -- # Rule 1 -- i % 2 == 0 -- # Rule 2 -- and i % 3 == 0 -- ): -- while ( -- # Just a comment ++ with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + for i in (a,): + if ( + # Rule 1 +@@ -27,133 +14,46 @@ + ): + while ( + # Just a comment - call() - # Another -- ): ++ NOT_IMPLEMENTED_call() + ): - print(i) - xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( - push_manager=context.request.resource_manager, @@ -235,7 +239,8 @@ class C: - # Only send the first n items. - items=items[:num_items] - ) -+ NOT_YET_IMPLEMENTED_StmtWith ++ # Another ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + xxxxxxxxxxxxxxxx = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) return ( 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' @@ -371,7 +376,7 @@ class C: %3d 0 LOAD_FAST 1 (x) 2 LOAD_CONST 1 (1) 4 COMPARE_OP 2 (==) -@@ -161,21 +44,8 @@ +@@ -161,21 +61,8 @@ 8 STORE_ATTR 0 (x) 10 LOAD_CONST 0 (None) 12 RETURN_VALUE @@ -403,7 +408,24 @@ class C: ```py class C: def test(self) -> None: - NOT_YET_IMPLEMENTED_StmtWith + with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + for i in (a,): + if ( + # Rule 1 + i % 2 == 0 + # Rule 2 + and i % 3 == 0 + ): + while ( + # Just a comment + NOT_IMPLEMENTED_call() + ): + # Another + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) xxxxxxxxxxxxxxxx = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) 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@composition_no_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition_no_trailing_comma.py.snap index 30953137ad..21c7c8dc7b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition_no_trailing_comma.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition_no_trailing_comma.py.snap @@ -193,7 +193,7 @@ class C: ```diff --- Black +++ Ruff -@@ -1,159 +1,42 @@ +@@ -1,23 +1,10 @@ class C: def test(self) -> None: - with patch("black.out", print): @@ -214,18 +214,22 @@ class C: - "2 files reformatted, 2 files left unchanged, 2 files failed to" - " reformat.", - ) -- for i in (a,): -- if ( -- # Rule 1 -- i % 2 == 0 -- # Rule 2 -- and i % 3 == 0 -- ): -- while ( -- # Just a comment ++ with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + for i in (a,): + if ( + # Rule 1 +@@ -27,133 +14,46 @@ + ): + while ( + # Just a comment - call() - # Another -- ): ++ NOT_IMPLEMENTED_call() + ): - print(i) - xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( - push_manager=context.request.resource_manager, @@ -235,7 +239,8 @@ class C: - # Only send the first n items. - items=items[:num_items] - ) -+ NOT_YET_IMPLEMENTED_StmtWith ++ # Another ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + xxxxxxxxxxxxxxxx = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) return ( 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' @@ -371,7 +376,7 @@ class C: %3d 0 LOAD_FAST 1 (x) 2 LOAD_CONST 1 (1) 4 COMPARE_OP 2 (==) -@@ -161,21 +44,8 @@ +@@ -161,21 +61,8 @@ 8 STORE_ATTR 0 (x) 10 LOAD_CONST 0 (None) 12 RETURN_VALUE @@ -403,7 +408,24 @@ class C: ```py class C: def test(self) -> None: - NOT_YET_IMPLEMENTED_StmtWith + with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + for i in (a,): + if ( + # Rule 1 + i % 2 == 0 + # Rule 2 + and i % 3 == 0 + ): + while ( + # Just a comment + NOT_IMPLEMENTED_call() + ): + # Another + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) xxxxxxxxxxxxxxxx = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) 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@expression.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap index 47e1d61f92..0b3a703534 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap @@ -353,10 +353,10 @@ last_call() - 5, -] +[1, 2, 3] -+[NOT_YET_IMPLEMENTED_ExprStarred] -+[NOT_YET_IMPLEMENTED_ExprStarred] -+[NOT_YET_IMPLEMENTED_ExprStarred, 4, 5] -+[4, NOT_YET_IMPLEMENTED_ExprStarred, 5] ++[*NOT_YET_IMPLEMENTED_ExprStarred] ++[*NOT_YET_IMPLEMENTED_ExprStarred] ++[*NOT_YET_IMPLEMENTED_ExprStarred, 4, 5] ++[4, *NOT_YET_IMPLEMENTED_ExprStarred, 5] [ - 4, - *a, @@ -367,7 +367,7 @@ last_call() element, another, - *more, -+ NOT_YET_IMPLEMENTED_ExprStarred, ++ *NOT_YET_IMPLEMENTED_ExprStarred, ] -{i for i in (1, 2, 3)} -{(i**2) for i in (1, 2, 3)} @@ -512,7 +512,7 @@ last_call() +(i for i in []) +(i for i in []) +(i for i in []) -+(NOT_YET_IMPLEMENTED_ExprStarred,) ++(*NOT_YET_IMPLEMENTED_ExprStarred,) { "id": "1", "type": "type", @@ -544,8 +544,8 @@ last_call() - .order_by(models.Customer.id.asc()) - .all() +e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+f = 1, NOT_YET_IMPLEMENTED_ExprStarred -+g = 1, NOT_YET_IMPLEMENTED_ExprStarred ++f = 1, *NOT_YET_IMPLEMENTED_ExprStarred ++g = 1, *NOT_YET_IMPLEMENTED_ExprStarred +what_is_up_with_those_new_coord_names = ( + (coord_names + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) + + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) @@ -756,15 +756,15 @@ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false [] [1, 2, 3, 4, 5, 6, 7, 8, 9, (10 or A), (11 or B), (12 or C)] [1, 2, 3] -[NOT_YET_IMPLEMENTED_ExprStarred] -[NOT_YET_IMPLEMENTED_ExprStarred] -[NOT_YET_IMPLEMENTED_ExprStarred, 4, 5] -[4, NOT_YET_IMPLEMENTED_ExprStarred, 5] +[*NOT_YET_IMPLEMENTED_ExprStarred] +[*NOT_YET_IMPLEMENTED_ExprStarred] +[*NOT_YET_IMPLEMENTED_ExprStarred, 4, 5] +[4, *NOT_YET_IMPLEMENTED_ExprStarred, 5] [ this_is_a_very_long_variable_which_will_force_a_delimiter_split, element, another, - NOT_YET_IMPLEMENTED_ExprStarred, + *NOT_YET_IMPLEMENTED_ExprStarred, ] {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} @@ -856,7 +856,7 @@ SomeName (i for i in []) (i for i in []) (i for i in []) -(NOT_YET_IMPLEMENTED_ExprStarred,) +(*NOT_YET_IMPLEMENTED_ExprStarred,) { "id": "1", "type": "type", @@ -871,8 +871,8 @@ b = (1,) c = 1 d = (1,) + a + (2,) e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -f = 1, NOT_YET_IMPLEMENTED_ExprStarred -g = 1, NOT_YET_IMPLEMENTED_ExprStarred +f = 1, *NOT_YET_IMPLEMENTED_ExprStarred +g = 1, *NOT_YET_IMPLEMENTED_ExprStarred what_is_up_with_those_new_coord_names = ( (coord_names + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap index baf9eaa6af..4e210b4109 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap @@ -352,7 +352,7 @@ d={'a':1, # fmt: off - a , b = *hello - 'unformatted' -+ a, b = NOT_YET_IMPLEMENTED_ExprStarred ++ a, b = *NOT_YET_IMPLEMENTED_ExprStarred + "unformatted" # fmt: on @@ -607,7 +607,7 @@ def import_as_names(): def testlist_star_expr(): # fmt: off - a, b = NOT_YET_IMPLEMENTED_ExprStarred + a, b = *NOT_YET_IMPLEMENTED_ExprStarred "unformatted" # fmt: on diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap index 8e54712280..5e791205c3 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap @@ -74,7 +74,7 @@ async def test_async_with(): ```diff --- Black +++ Ruff -@@ -1,62 +1,54 @@ +@@ -1,62 +1,55 @@ # Make sure a leading comment is not removed. -def some_func( unformatted, args ): # fmt: skip - print("I am some_func") @@ -155,7 +155,8 @@ async def test_async_with(): -with give_me_context( unformatted, args ): # fmt: skip - print("Do something") -+NOT_YET_IMPLEMENTED_StmtWith # fmt: skip ++with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: skip ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) async def test_async_with(): @@ -216,7 +217,8 @@ async def test_async_for(): NOT_YET_IMPLEMENTED_StmtTry -NOT_YET_IMPLEMENTED_StmtWith # fmt: skip +with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: skip + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) async def test_async_with(): diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function2.py.snap index d70e4ac234..fe0456ee1f 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function2.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function2.py.snap @@ -65,12 +65,13 @@ with hmm_but_this_should_get_two_preceding_newlines(): ```diff --- Black +++ Ruff -@@ -2,17 +2,9 @@ +@@ -2,17 +2,11 @@ a, **kwargs, ) -> A: - with cache_dir(): -- if something: ++ with NOT_IMPLEMENTED_call(): + if something: - result = CliRunner().invoke( - black.main, [str(src1), str(src2), "--diff", "--check"] - ) @@ -80,13 +81,13 @@ with hmm_but_this_should_get_two_preceding_newlines(): - very_long_argument_name2=-very.long.value.for_the_argument, - **kwargs, - ) -+ NOT_YET_IMPLEMENTED_StmtWith ++ result = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # negate top + return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) def g(): -@@ -21,45 +13,30 @@ +@@ -21,45 +15,31 @@ def inner(): pass @@ -136,8 +137,8 @@ with hmm_but_this_should_get_two_preceding_newlines(): - -with hmm_but_this_should_get_two_preceding_newlines(): -- pass -+NOT_YET_IMPLEMENTED_StmtWith ++with NOT_IMPLEMENTED_call(): + pass ``` ## Ruff Output @@ -147,7 +148,9 @@ def f( a, **kwargs, ) -> A: - NOT_YET_IMPLEMENTED_StmtWith + with NOT_IMPLEMENTED_call(): + if something: + result = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # negate top return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) @@ -184,7 +187,8 @@ else: def foo(): pass -NOT_YET_IMPLEMENTED_StmtWith +with NOT_IMPLEMENTED_call(): + pass ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_newline_after_code_block_open.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_newline_after_code_block_open.py.snap index 05400a9c8d..fef152f5d3 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_newline_after_code_block_open.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_newline_after_code_block_open.py.snap @@ -120,7 +120,7 @@ with open("/path/to/file.txt", mode="r") as read_file: ```diff --- Black +++ Ruff -@@ -1,78 +1,74 @@ +@@ -1,78 +1,78 @@ -import random +NOT_YET_IMPLEMENTED_StmtImport @@ -214,18 +214,22 @@ with open("/path/to/file.txt", mode="r") as read_file: -with open("/path/to/file.txt", mode="w") as file: - file.write("The new line above me is about to be removed!") -+NOT_YET_IMPLEMENTED_StmtWith ++with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) as file: ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -with open("/path/to/file.txt", mode="w") as file: - file.write("The new lines above me is about to be removed!") -+NOT_YET_IMPLEMENTED_StmtWith ++with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) as file: ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -with open("/path/to/file.txt", mode="r") as read_file: - with open("/path/to/output_file.txt", mode="w") as write_file: - write_file.writelines(read_file.readlines()) -+NOT_YET_IMPLEMENTED_StmtWith ++with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) as read_file: ++ with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) as write_file: ++ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Ruff Output @@ -298,13 +302,17 @@ while True: NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -NOT_YET_IMPLEMENTED_StmtWith +with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) as file: + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -NOT_YET_IMPLEMENTED_StmtWith +with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) as file: + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -NOT_YET_IMPLEMENTED_StmtWith +with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) as read_file: + with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) as write_file: + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Black Output 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 new file mode 100644 index 0000000000..3eb2c3ee7a --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap @@ -0,0 +1,132 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/with.py +--- +## Input +```py +with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: + ... + # trailing + +with a, a: # after colon + ... + # trailing + +with ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, +): + ... + # trailing + + +with ( + a # a + , # comma + b # c + ): # colon + ... + + +with ( + a # a + as # as + b # b + , # comma + c # c + ): # colon + ... # body + # body trailing own + + +with (a,): # magic trailing comma + ... + + +with (a): # should remove brackets + ... + +# TODO: black doesn't wrap this, but maybe we want to anyway? +# 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: + ... + + +# currently unparsable by black: https://github.com/psf/black/issues/3678 +with (name_2 for name_0 in name_4): + pass +with (a, *b): + pass +``` + +## Output +```py +with ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, +): + ... + # trailing + +with a, a: # after colon + ... + # trailing + +with ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, +): + ... + # trailing + + +with ( + ( + a # a # comma + ), + b, # c +): # colon + ... + + +with ( + ( + a # a # as + ) as b, # b # comma + c, # c +): # colon + ... # body + # body trailing own + + +with ( + a, +): # magic trailing comma + ... + + +with a: # should remove brackets + ... + +# TODO: black doesn't wrap this, but maybe we want to anyway? +# 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, +): + ... + + +# currently unparsable by black: https://github.com/psf/black/issues/3678 +with (i for i in []): + pass +with (a, *NOT_YET_IMPLEMENTED_ExprStarred): + pass +``` + + + From fde3f0937044a69164e074cfa07186b18719f239 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Mon, 26 Jun 2023 15:44:46 +0100 Subject: [PATCH 235/447] Add documentation missing docstring rules (`D1XX`) (#5330) ## Summary Add documentation to the `D1XX` rules that flag missing docstrings. The examples are quite long and docstrings practices vary a lot between projects, so I thought it would be best that the documentation for these rules be their own PR separate to the other `pydocstyle` rules. Related to #2646. ## Test Plan `python scripts/check_docs_formatted.py` --- .../src/rules/pydocstyle/rules/not_missing.rs | 424 ++++++++++++++++++ 1 file changed, 424 insertions(+) diff --git a/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs b/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs index 9db7d71e23..97364cbc44 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs @@ -12,6 +12,56 @@ use ruff_python_semantic::{Definition, Member, MemberKind, Module, ModuleKind}; use crate::checkers::ast::Checker; use crate::registry::Rule; +/// ## What it does +/// Checks for undocumented public module definitions. +/// +/// ## Why is this bad? +/// Public modules should be documented via docstrings to outline their purpose +/// and contents. +/// +/// Generally, module docstrings should describe the purpose of the module and +/// list the classes, exceptions, functions, and other objects that are exported +/// by the module, alongside a one-line summary of each. +/// +/// If the module is a script, the docstring should be usable as its "usage" +/// message. +/// +/// If the codebase adheres to a standard format for module docstrings, follow +/// that format for consistency. +/// +/// ## Example +/// ```python +/// class FasterThanLightError(ZeroDivisionError): +/// ... +/// +/// +/// def calculate_speed(distance: float, time: float) -> float: +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// """Utility functions and classes for calculating speed. +/// +/// This module provides: +/// - FasterThanLightError: exception when FTL speed is calculated; +/// - calculate_speed: calculate speed given distance and time. +/// """ +/// +/// +/// class FasterThanLightError(ZeroDivisionError): +/// ... +/// +/// +/// def calculate_speed(distance: float, time: float) -> float: +/// ... +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct UndocumentedPublicModule; @@ -22,6 +72,79 @@ impl Violation for UndocumentedPublicModule { } } +/// ## What it does +/// Checks for undocumented public class definitions. +/// +/// ## Why is this bad? +/// Public classes should be documented via docstrings to outline their purpose +/// and behavior. +/// +/// Generally, a class docstring should describe the class's purpose and list +/// its public attributes and methods. +/// +/// If the codebase adheres to a standard format for class docstrings, follow +/// that format for consistency. +/// +/// ## Example +/// ```python +/// class Player: +/// def __init__(self, name: str, points: int = 0) -> None: +/// self.name: str = name +/// self.points: int = points +/// +/// def add_points(self, points: int) -> None: +/// self.points += points +/// ``` +/// +/// Use instead (in the NumPy docstring format): +/// ```python +/// class Player: +/// """A player in the game. +/// +/// Attributes +/// ---------- +/// name : str +/// The name of the player. +/// points : int +/// The number of points the player has. +/// +/// Methods +/// ------- +/// add_points(points: int) -> None +/// Add points to the player's score. +/// """ +/// +/// def __init__(self, name: str, points: int = 0) -> None: +/// self.name: str = name +/// self.points: int = points +/// +/// def add_points(self, points: int) -> None: +/// self.points += points +/// ``` +/// +/// Or (in the Google docstring format): +/// ```python +/// class Player: +/// """A player in the game. +/// +/// Attributes: +/// name: The name of the player. +/// points: The number of points the player has. +/// """ +/// +/// def __init__(self, name: str, points: int = 0) -> None: +/// self.name: str = name +/// self.points: int = points +/// +/// def add_points(self, points: int) -> None: +/// self.points += points +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct UndocumentedPublicClass; @@ -32,6 +155,75 @@ impl Violation for UndocumentedPublicClass { } } +/// ## What it does +/// Checks for undocumented public method definitions. +/// +/// ## Why is this bad? +/// Public methods should be documented via docstrings to outline their purpose +/// and behavior. +/// +/// Generally, a method docstring should describe the method's behavior, +/// arguments, side effects, exceptions, return values, and any other +/// information that may be relevant to the user. +/// +/// If the codebase adheres to a standard format for method docstrings, follow +/// that format for consistency. +/// +/// ## Example +/// ```python +/// class Cat(Animal): +/// def greet(self, happy: bool = True): +/// if happy: +/// print("Meow!") +/// else: +/// raise ValueError("Tried to greet an unhappy cat.") +/// ``` +/// +/// Use instead (in the NumPy docstring format): +/// ```python +/// class Cat(Animal): +/// def greet(self, happy: bool = True): +/// """Print a greeting from the cat. +/// +/// Parameters +/// ---------- +/// happy : bool, optional +/// Whether the cat is happy, is True by default. +/// +/// Raises +/// ------ +/// ValueError +/// If the cat is not happy. +/// """ +/// if happy: +/// print("Meow!") +/// else: +/// raise ValueError("Tried to greet an unhappy cat.") +/// ``` +/// +/// Or (in the Google docstring format): +/// ```python +/// class Cat(Animal): +/// def greet(self, happy: bool = True): +/// """Print a greeting from the cat. +/// +/// Args: +/// happy: Whether the cat is happy, is True by default. +/// +/// Raises: +/// ValueError: If the cat is not happy. +/// """ +/// if happy: +/// print("Meow!") +/// else: +/// raise ValueError("Tried to greet an unhappy cat.") +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct UndocumentedPublicMethod; @@ -42,6 +234,82 @@ impl Violation for UndocumentedPublicMethod { } } +/// ## What it does +/// Checks for undocumented public function definitions. +/// +/// ## Why is this bad? +/// Public functions should be documented via docstrings to outline their +/// purpose and behavior. +/// +/// Generally, a function docstring should describe the function's behavior, +/// arguments, side effects, exceptions, return values, and any other +/// information that may be relevant to the user. +/// +/// If the codebase adheres to a standard format for function docstrings, follow +/// that format for consistency. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead (using the NumPy docstring format): +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Or, using the Google docstring format: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args: +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// Returns: +/// Speed as distance divided by time. +/// +/// Raises: +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Style Python Docstrings](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) #[violation] pub struct UndocumentedPublicFunction; @@ -52,6 +320,39 @@ impl Violation for UndocumentedPublicFunction { } } +/// ## What it does +/// Checks for undocumented public package definitions. +/// +/// ## Why is this bad? +/// Public packages should be documented via docstrings to outline their +/// purpose and contents. +/// +/// Generally, package docstrings should list the modules and subpackages that +/// are exported by the package. +/// +/// If the codebase adheres to a standard format for package docstrings, follow +/// that format for consistency. +/// +/// ## Example +/// ```python +/// __all__ = ["Player", "Game"] +/// ``` +/// +/// Use instead: +/// ```python +/// """Game and player management package. +/// +/// This package provides classes for managing players and games. +/// """ +/// +/// __all__ = ["player", "game"] +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Style Python Docstrings](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) #[violation] pub struct UndocumentedPublicPackage; @@ -62,6 +363,50 @@ impl Violation for UndocumentedPublicPackage { } } +/// ## What it does +/// Checks for undocumented magic method definitions. +/// +/// ## Why is this bad? +/// Magic methods (methods with names that start and end with double +/// underscores) are used to implement operator overloading and other special +/// behavior. Such methods should should be documented via docstrings to +/// outline their behavior. +/// +/// Generally, magic method docstrings should describe the method's behavior, +/// arguments, side effects, exceptions, return values, and any other +/// information that may be relevant to the user. +/// +/// If the codebase adheres to a standard format for method docstrings, follow +/// that format for consistency. +/// +/// ## Example +/// ```python +/// class Cat(Animal): +/// def __str__(self) -> str: +/// return f"Cat: {self.name}" +/// +/// +/// cat = Cat("Dusty") +/// print(cat) # "Cat: Dusty" +/// ``` +/// +/// Use instead: +/// ```python +/// class Cat(Animal): +/// def __str__(self) -> str: +/// """Return a string representation of the cat.""" +/// return f"Cat: {self.name}" +/// +/// +/// cat = Cat("Dusty") +/// print(cat) # "Cat: Dusty" +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Style Python Docstrings](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) #[violation] pub struct UndocumentedMagicMethod; @@ -72,6 +417,50 @@ impl Violation for UndocumentedMagicMethod { } } +/// ## What it does +/// Checks for undocumented public class definitions, for nested classes. +/// +/// ## Why is this bad? +/// Public classes should be documented via docstrings to outline their +/// purpose and behavior. +/// +/// Nested classes do not inherit the docstring of their enclosing class, so +/// they should have their own docstrings. +/// +/// If the codebase adheres to a standard format for class docstrings, follow +/// that format for consistency. +/// +/// ## Example +/// ```python +/// class Foo: +/// """Class Foo.""" +/// +/// class Bar: +/// ... +/// +/// +/// bar = Foo.Bar() +/// bar.__doc__ # None +/// ``` +/// +/// Use instead: +/// ```python +/// class Foo: +/// """Class Foo.""" +/// +/// class Bar: +/// """Class Bar.""" +/// +/// +/// bar = Foo.Bar() +/// bar.__doc__ # "Class Bar." +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Style Python Docstrings](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) #[violation] pub struct UndocumentedPublicNestedClass; @@ -82,6 +471,41 @@ impl Violation for UndocumentedPublicNestedClass { } } +/// ## What it does +/// Checks for public `__init__` method definitions that are missing +/// docstrings. +/// +/// ## Why is this bad? +/// Public `__init__` methods are used to initialize objects. `__init__` +/// methods should be documented via docstrings to describe the method's +/// behavior, arguments, side effects, exceptions, and any other information +/// that may be relevant to the user. +/// +/// If the codebase adheres to a standard format for `__init__` method docstrings, +/// follow that format for consistency. +/// +/// ## Example +/// ```python +/// class City: +/// def __init__(self, name: str, population: int) -> None: +/// self.name: str = name +/// self.population: int = population +/// ``` +/// +/// Use instead: +/// ```python +/// class City: +/// def __init__(self, name: str, population: int) -> None: +/// """Initialize a city with a name and population.""" +/// self.name: str = name +/// self.population: int = population +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Style Python Docstrings](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) #[violation] pub struct UndocumentedPublicInit; From baa7264ca41ef8bb7dea91503bb0b9f31fa3c061 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Mon, 26 Jun 2023 16:24:42 +0100 Subject: [PATCH 236/447] Add documentation for `flake8-2020` (#5366) ## Summary Completes the documentation for the `flake8-2020` ruleset. Related to #2646 . ## Test Plan `python scripts/check_docs_formatted.py` --- .../src/rules/flake8_2020/rules/compare.rs | 160 ++++++++++++++++++ .../flake8_2020/rules/name_or_attribute.rs | 29 ++++ .../src/rules/flake8_2020/rules/subscript.rs | 120 +++++++++++++ 3 files changed, 309 insertions(+) diff --git a/crates/ruff/src/rules/flake8_2020/rules/compare.rs b/crates/ruff/src/rules/flake8_2020/rules/compare.rs index 1e083b48a1..55197df851 100644 --- a/crates/ruff/src/rules/flake8_2020/rules/compare.rs +++ b/crates/ruff/src/rules/flake8_2020/rules/compare.rs @@ -9,6 +9,38 @@ use crate::registry::Rule; use super::super::helpers::is_sys; +/// ## What it does +/// Checks for comparisons that test `sys.version` against string literals, +/// such that the comparison will evaluate to `False` on Python 3.10 or later. +/// +/// ## Why is this bad? +/// Comparing `sys.version` to a string is error-prone and may cause subtle +/// bugs, as the comparison will be performed lexicographically, not +/// semantically. For example, `sys.version > "3.9"` will evaluate to `False` +/// when using Python 3.10, as `"3.10"` is lexicographically "less" than +/// `"3.9"`. +/// +/// Instead, use `sys.version_info` to access the current major and minor +/// version numbers as a tuple, which can be compared to other tuples +/// without issue. +/// +/// ## Example +/// ```python +/// import sys +/// +/// sys.version > "3.9" # `False` on Python 3.10. +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// sys.version_info > (3, 9) # `True` on Python 3.10. +/// ``` +/// +/// ## References +/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) +/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] pub struct SysVersionCmpStr3; @@ -19,6 +51,43 @@ impl Violation for SysVersionCmpStr3 { } } +/// ## What it does +/// Checks for equality comparisons against the major version returned by +/// `sys.version_info` (e.g., `sys.version_info[0] == 3`). +/// +/// ## Why is this bad? +/// Using `sys.version_info[0] == 3` to verify that the major version is +/// Python 3 or greater will fail if the major version number is ever +/// incremented (e.g., to Python 4). This is likely unintended, as code +/// that uses this comparison is likely intended to be run on Python 2, +/// but would now run on Python 4 too. +/// +/// Instead, use `>=` to check if the major version number is 3 or greater, +/// to future-proof the code. +/// +/// ## Example +/// ```python +/// import sys +/// +/// if sys.version_info[0] == 3: +/// ... +/// else: +/// print("Python 2") # This will be printed on Python 4. +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// if sys.version_info >= (3,): +/// ... +/// else: +/// print("Python 2") # This will not be printed on Python 4. +/// ``` +/// +/// ## References +/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) +/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] pub struct SysVersionInfo0Eq3; @@ -29,6 +98,36 @@ impl Violation for SysVersionInfo0Eq3 { } } +/// ## What it does +/// Checks for comparisons that test `sys.version_info[1]` against an integer. +/// +/// ## Why is this bad? +/// Comparisons based on the current minor version number alone can cause +/// subtle bugs and would likely lead to unintended effects if the Python +/// major version number were ever incremented (e.g., to Python 4). +/// +/// Instead, compare `sys.version_info` to a tuple, including the major and +/// minor version numbers, to future-proof the code. +/// +/// ## Example +/// ```python +/// import sys +/// +/// if sys.version_info[1] < 7: +/// print("Python 3.6 or earlier.") # This will be printed on Python 4.0. +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// if sys.version_info < (3, 7): +/// print("Python 3.6 or earlier.") +/// ``` +/// +/// ## References +/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) +/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] pub struct SysVersionInfo1CmpInt; @@ -42,6 +141,36 @@ impl Violation for SysVersionInfo1CmpInt { } } +/// ## What it does +/// Checks for comparisons that test `sys.version_info.minor` against an integer. +/// +/// ## Why is this bad? +/// Comparisons based on the current minor version number alone can cause +/// subtle bugs and would likely lead to unintended effects if the Python +/// major version number were ever incremented (e.g., to Python 4). +/// +/// Instead, compare `sys.version_info` to a tuple, including the major and +/// minor version numbers, to future-proof the code. +/// +/// ## Example +/// ```python +/// import sys +/// +/// if sys.version_info.minor < 7: +/// print("Python 3.6 or earlier.") # This will be printed on Python 4.0. +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// if sys.version_info < (3, 7): +/// print("Python 3.6 or earlier.") +/// ``` +/// +/// ## References +/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) +/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] pub struct SysVersionInfoMinorCmpInt; @@ -55,6 +184,37 @@ impl Violation for SysVersionInfoMinorCmpInt { } } +/// ## What it does +/// Checks for comparisons that test `sys.version` against string literals, +/// such that the comparison would fail if the major version number were +/// ever incremented to Python 10 or higher. +/// +/// ## Why is this bad? +/// Comparing `sys.version` to a string is error-prone and may cause subtle +/// bugs, as the comparison will be performed lexicographically, not +/// semantically. +/// +/// Instead, use `sys.version_info` to access the current major and minor +/// version numbers as a tuple, which can be compared to other tuples +/// without issue. +/// +/// ## Example +/// ```python +/// import sys +/// +/// sys.version >= "3" # `False` on Python 10. +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// sys.version_info >= (3,) # `True` on Python 10. +/// ``` +/// +/// ## References +/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) +/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] pub struct SysVersionCmpStr10; diff --git a/crates/ruff/src/rules/flake8_2020/rules/name_or_attribute.rs b/crates/ruff/src/rules/flake8_2020/rules/name_or_attribute.rs index 3ca122af52..47bdd31421 100644 --- a/crates/ruff/src/rules/flake8_2020/rules/name_or_attribute.rs +++ b/crates/ruff/src/rules/flake8_2020/rules/name_or_attribute.rs @@ -5,6 +5,35 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for uses of `six.PY3`. +/// +/// ## Why is this bad? +/// `six.PY3` will evaluate to `False` on Python 4 and greater. This is likely +/// unintended, and may cause code intended to run on Python 2 to run on Python 4 +/// too. +/// +/// Instead, use `not six.PY2` to validate that the current Python major version is +/// _not_ equal to 2, to future-proof the code. +/// +/// ## Example +/// ```python +/// import six +/// +/// six.PY3 # `False` on Python 4. +/// ``` +/// +/// Use instead: +/// ```python +/// import six +/// +/// not six.PY2 # `True` on Python 4. +/// ``` +/// +/// ## References +/// - [PyPI: `six`](https://pypi.org/project/six/) +/// - [Six documentation: `six.PY2`](https://six.readthedocs.io/#six.PY2) +/// - [Six documentation: `six.PY3`](https://six.readthedocs.io/#six.PY3) #[violation] pub struct SixPY3; diff --git a/crates/ruff/src/rules/flake8_2020/rules/subscript.rs b/crates/ruff/src/rules/flake8_2020/rules/subscript.rs index 71d6768c86..0f18cfae68 100644 --- a/crates/ruff/src/rules/flake8_2020/rules/subscript.rs +++ b/crates/ruff/src/rules/flake8_2020/rules/subscript.rs @@ -8,6 +8,36 @@ use crate::checkers::ast::Checker; use crate::registry::Rule; use crate::rules::flake8_2020::helpers::is_sys; +/// ## What it does +/// Checks for uses of `sys.version[:3]`. +/// +/// ## Why is this bad? +/// If the current major or minor version consists of multiple digits, +/// `sys.version[:3]` will truncate the version number (e.g., `"3.10"` would +/// become `"3.1"`). This is likely unintended, and can lead to subtle bugs if +/// the version string is used to test against a specific Python version. +/// +/// Instead, use `sys.version_info` to access the current major and minor +/// version numbers as a tuple, which can be compared to other tuples +/// without issue. +/// +/// ## Example +/// ```python +/// import sys +/// +/// sys.version[:3] # Evaluates to "3.1" on Python 3.10. +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// sys.version_info[:2] # Evaluates to (3, 10) on Python 3.10. +/// ``` +/// +/// ## References +/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) +/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] pub struct SysVersionSlice3; @@ -18,6 +48,36 @@ impl Violation for SysVersionSlice3 { } } +/// ## What it does +/// Checks for uses of `sys.version[2]`. +/// +/// ## Why is this bad? +/// If the current major or minor version consists of multiple digits, +/// `sys.version[2]` will select the first digit of the minor number only +/// (e.g., `"3.10"` would evaluate to `"1"`). This is likely unintended, and +/// can lead to subtle bugs if the version is used to test against a minor +/// version number. +/// +/// Instead, use `sys.version_info.minor` to access the current minor version +/// number. +/// +/// ## Example +/// ```python +/// import sys +/// +/// sys.version[2] # Evaluates to "1" on Python 3.10. +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// f"{sys.version_info.minor}" # Evaluates to "10" on Python 3.10. +/// ``` +/// +/// ## References +/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) +/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] pub struct SysVersion2; @@ -28,6 +88,36 @@ impl Violation for SysVersion2 { } } +/// ## What it does +/// Checks for uses of `sys.version[0]`. +/// +/// ## Why is this bad? +/// If the current major or minor version consists of multiple digits, +/// `sys.version[0]` will select the first digit of the major version number +/// only (e.g., `"3.10"` would evaluate to `"1"`). This is likely unintended, +/// and can lead to subtle bugs if the version string is used to test against a +/// major version number. +/// +/// Instead, use `sys.version_info.major` to access the current major version +/// number. +/// +/// ## Example +/// ```python +/// import sys +/// +/// sys.version[0] # If using Python 10, this evaluates to "1". +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// f"{sys.version_info.major}" # If using Python 10, this evaluates to "10". +/// ``` +/// +/// ## References +/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) +/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] pub struct SysVersion0; @@ -38,6 +128,36 @@ impl Violation for SysVersion0 { } } +/// ## What it does +/// Checks for uses of `sys.version[:1]`. +/// +/// ## Why is this bad? +/// If the major version number consists of more than one digit, this will +/// select the first digit of the major version number only (e.g., `"10.0"` +/// would evaluate to `"1"`). This is likely unintended, and can lead to subtle +/// bugs in future versions of Python if the version string is used to test +/// against a specific major version number. +/// +/// Instead, use `sys.version_info.major` to access the current major version +/// number. +/// +/// ## Example +/// ```python +/// import sys +/// +/// sys.version[:1] # If using Python 10, this evaluates to "1". +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// f"{sys.version_info.major}" # If using Python 10, this evaluates to "10". +/// ``` +/// +/// ## References +/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) +/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] pub struct SysVersionSlice1; From fa1b85b3da77f079307e6c639fe7a382545a545c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 26 Jun 2023 11:43:49 -0400 Subject: [PATCH 237/447] Remove prelude from `ruff_python_ast` (#5369) ## Summary Per @MichaReiser, this is causing more confusion than it is helpful. --- .../rules/airflow/rules/task_variable_name.rs | 2 +- .../rules/collections_named_tuple.rs | 2 +- .../rules/iter_method_return_iterable.rs | 2 +- .../rules/no_return_argument_annotation.rs | 2 +- .../flake8_pytest_style/rules/fixture.rs | 2 +- .../rules/no_slots_in_namedtuple_subclass.rs | 2 +- .../perflint/rules/incorrect_dict_iterator.rs | 2 +- .../perflint/rules/unnecessary_list_cast.rs | 2 +- crates/ruff_python_ast/src/lib.rs | 1 - crates/ruff_python_ast/src/node.rs | 958 +++++++++--------- crates/ruff_python_ast/src/prelude.rs | 3 - crates/ruff_python_ast/src/visitor.rs | 5 +- .../ruff_python_ast/src/visitor/preorder.rs | 140 +-- .../src/comments/debug.rs | 21 +- .../src/comments/format.rs | 13 +- .../ruff_python_formatter/src/comments/mod.rs | 29 +- .../src/comments/placement.rs | 92 +- .../src/comments/visitor.rs | 18 +- .../src/expression/binary_like.rs | 22 +- .../src/expression/expr_compare.rs | 2 +- .../src/expression/expr_dict.rs | 2 +- .../src/expression/expr_slice.rs | 2 +- .../src/expression/expr_tuple.rs | 2 +- .../src/expression/expr_unary_op.rs | 2 +- .../src/statement/stmt_assign.rs | 2 +- .../src/statement/stmt_with.rs | 8 +- fuzz/Cargo.toml | 3 + fuzz/fuzz_targets/ruff_parse_simple.rs | 3 +- 28 files changed, 688 insertions(+), 656 deletions(-) delete mode 100644 crates/ruff_python_ast/src/prelude.rs diff --git a/crates/ruff/src/rules/airflow/rules/task_variable_name.rs b/crates/ruff/src/rules/airflow/rules/task_variable_name.rs index 419053c2c3..8e9930e564 100644 --- a/crates/ruff/src/rules/airflow/rules/task_variable_name.rs +++ b/crates/ruff/src/rules/airflow/rules/task_variable_name.rs @@ -3,7 +3,7 @@ use rustpython_parser::ast::{Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::prelude::Constant; +use rustpython_parser::ast::Constant; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs b/crates/ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs index 9accde17c3..d9d32e81f2 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::Expr; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::prelude::Ranged; +use rustpython_parser::ast::Ranged; use crate::checkers::ast::Checker; 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 db3aa58bed..09347b72d2 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 @@ -3,8 +3,8 @@ use rustpython_parser::ast::{Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::prelude::Expr; use ruff_python_semantic::{Definition, Member, MemberKind}; +use rustpython_parser::ast::Expr; use crate::checkers::ast::Checker; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs b/crates/ruff/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs index 354067c031..669e3ba046 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs @@ -4,7 +4,7 @@ use rustpython_parser::ast::Ranged; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::prelude::Arguments; +use rustpython_parser::ast::Arguments; use crate::checkers::ast::Checker; use crate::settings::types::PythonVersion::Py311; 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 a34832a93f..ba447ce25f 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs @@ -2,6 +2,7 @@ use std::fmt; use anyhow::Result; use ruff_text_size::{TextLen, TextRange, TextSize}; +use rustpython_parser::ast::Decorator; use rustpython_parser::ast::{self, ArgWithDefault, Arguments, Expr, Keyword, Ranged, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; @@ -10,7 +11,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::prelude::Decorator; use ruff_python_ast::source_code::Locator; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs index 37ec291eab..913d5b0a1e 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs @@ -4,7 +4,7 @@ use rustpython_parser::ast::{Expr, StmtClassDef}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::identifier::Identifier; -use ruff_python_ast::prelude::Stmt; +use rustpython_parser::ast::Stmt; use crate::checkers::ast::Checker; use crate::rules::flake8_slots::rules::helpers::has_slots; 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 842c207e45..c2af990708 100644 --- a/crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs +++ b/crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs @@ -6,8 +6,8 @@ use rustpython_parser::{ast, lexer, Mode, Tok}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::prelude::Ranged; use ruff_python_ast::source_code::Locator; +use rustpython_parser::ast::Ranged; use crate::checkers::ast::Checker; use crate::registry::AsRule; 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 57f2f49caa..9eaaea1dc5 100644 --- a/crates/ruff/src/rules/perflint/rules/unnecessary_list_cast.rs +++ b/crates/ruff/src/rules/perflint/rules/unnecessary_list_cast.rs @@ -3,7 +3,7 @@ use rustpython_parser::ast::{self, Expr}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::prelude::Stmt; +use rustpython_parser::ast::Stmt; use crate::checkers::ast::Checker; use crate::registry::AsRule; diff --git a/crates/ruff_python_ast/src/lib.rs b/crates/ruff_python_ast/src/lib.rs index 275e62aeab..72928ec9e9 100644 --- a/crates/ruff_python_ast/src/lib.rs +++ b/crates/ruff_python_ast/src/lib.rs @@ -9,7 +9,6 @@ pub mod helpers; pub mod identifier; pub mod imports; pub mod node; -pub mod prelude; pub mod relocate; pub mod source_code; pub mod statement_visitor; diff --git a/crates/ruff_python_ast/src/node.rs b/crates/ruff_python_ast/src/node.rs index c5b50e71eb..cc752a7a1f 100644 --- a/crates/ruff_python_ast/src/node.rs +++ b/crates/ruff_python_ast/src/node.rs @@ -1,5 +1,9 @@ -use crate::prelude::*; use ruff_text_size::TextRange; +use rustpython_ast::{ + Alias, Arg, ArgWithDefault, Arguments, Comprehension, Decorator, ExceptHandler, Keyword, + MatchCase, Mod, Pattern, Stmt, TypeIgnore, WithItem, +}; +use rustpython_parser::ast::{self, Expr, Ranged}; use std::ptr::NonNull; pub trait AstNode: Ranged { @@ -17,74 +21,74 @@ pub trait AstNode: Ranged { #[derive(Clone, Debug, is_macro::Is, PartialEq)] pub enum AnyNode { - ModModule(ModModule), - ModInteractive(ModInteractive), - ModExpression(ModExpression), - ModFunctionType(ModFunctionType), - StmtFunctionDef(StmtFunctionDef), - StmtAsyncFunctionDef(StmtAsyncFunctionDef), - StmtClassDef(StmtClassDef), - StmtReturn(StmtReturn), - StmtDelete(StmtDelete), - StmtAssign(StmtAssign), - StmtAugAssign(StmtAugAssign), - StmtAnnAssign(StmtAnnAssign), - StmtFor(StmtFor), - StmtAsyncFor(StmtAsyncFor), - StmtWhile(StmtWhile), - StmtIf(StmtIf), - StmtWith(StmtWith), - StmtAsyncWith(StmtAsyncWith), - StmtMatch(StmtMatch), - StmtRaise(StmtRaise), - StmtTry(StmtTry), - StmtTryStar(StmtTryStar), - StmtAssert(StmtAssert), - StmtImport(StmtImport), - StmtImportFrom(StmtImportFrom), - StmtGlobal(StmtGlobal), - StmtNonlocal(StmtNonlocal), - StmtExpr(StmtExpr), - StmtPass(StmtPass), - StmtBreak(StmtBreak), - StmtContinue(StmtContinue), - ExprBoolOp(ExprBoolOp), - ExprNamedExpr(ExprNamedExpr), - ExprBinOp(ExprBinOp), - ExprUnaryOp(ExprUnaryOp), - ExprLambda(ExprLambda), - ExprIfExp(ExprIfExp), - ExprDict(ExprDict), - ExprSet(ExprSet), - ExprListComp(ExprListComp), - ExprSetComp(ExprSetComp), - ExprDictComp(ExprDictComp), - ExprGeneratorExp(ExprGeneratorExp), - ExprAwait(ExprAwait), - ExprYield(ExprYield), - ExprYieldFrom(ExprYieldFrom), - ExprCompare(ExprCompare), - ExprCall(ExprCall), - ExprFormattedValue(ExprFormattedValue), - ExprJoinedStr(ExprJoinedStr), - ExprConstant(ExprConstant), - ExprAttribute(ExprAttribute), - ExprSubscript(ExprSubscript), - ExprStarred(ExprStarred), - ExprName(ExprName), - ExprList(ExprList), - ExprTuple(ExprTuple), - ExprSlice(ExprSlice), - ExceptHandlerExceptHandler(ExceptHandlerExceptHandler), - PatternMatchValue(PatternMatchValue), - PatternMatchSingleton(PatternMatchSingleton), - PatternMatchSequence(PatternMatchSequence), - PatternMatchMapping(PatternMatchMapping), - PatternMatchClass(PatternMatchClass), - PatternMatchStar(PatternMatchStar), - PatternMatchAs(PatternMatchAs), - PatternMatchOr(PatternMatchOr), - TypeIgnoreTypeIgnore(TypeIgnoreTypeIgnore), + ModModule(ast::ModModule), + ModInteractive(ast::ModInteractive), + ModExpression(ast::ModExpression), + ModFunctionType(ast::ModFunctionType), + StmtFunctionDef(ast::StmtFunctionDef), + StmtAsyncFunctionDef(ast::StmtAsyncFunctionDef), + StmtClassDef(ast::StmtClassDef), + StmtReturn(ast::StmtReturn), + StmtDelete(ast::StmtDelete), + StmtAssign(ast::StmtAssign), + StmtAugAssign(ast::StmtAugAssign), + StmtAnnAssign(ast::StmtAnnAssign), + StmtFor(ast::StmtFor), + StmtAsyncFor(ast::StmtAsyncFor), + StmtWhile(ast::StmtWhile), + StmtIf(ast::StmtIf), + StmtWith(ast::StmtWith), + StmtAsyncWith(ast::StmtAsyncWith), + StmtMatch(ast::StmtMatch), + StmtRaise(ast::StmtRaise), + StmtTry(ast::StmtTry), + StmtTryStar(ast::StmtTryStar), + StmtAssert(ast::StmtAssert), + StmtImport(ast::StmtImport), + StmtImportFrom(ast::StmtImportFrom), + StmtGlobal(ast::StmtGlobal), + StmtNonlocal(ast::StmtNonlocal), + StmtExpr(ast::StmtExpr), + StmtPass(ast::StmtPass), + StmtBreak(ast::StmtBreak), + StmtContinue(ast::StmtContinue), + ExprBoolOp(ast::ExprBoolOp), + ExprNamedExpr(ast::ExprNamedExpr), + ExprBinOp(ast::ExprBinOp), + ExprUnaryOp(ast::ExprUnaryOp), + ExprLambda(ast::ExprLambda), + ExprIfExp(ast::ExprIfExp), + ExprDict(ast::ExprDict), + ExprSet(ast::ExprSet), + ExprListComp(ast::ExprListComp), + ExprSetComp(ast::ExprSetComp), + ExprDictComp(ast::ExprDictComp), + ExprGeneratorExp(ast::ExprGeneratorExp), + ExprAwait(ast::ExprAwait), + ExprYield(ast::ExprYield), + ExprYieldFrom(ast::ExprYieldFrom), + ExprCompare(ast::ExprCompare), + ExprCall(ast::ExprCall), + ExprFormattedValue(ast::ExprFormattedValue), + ExprJoinedStr(ast::ExprJoinedStr), + ExprConstant(ast::ExprConstant), + ExprAttribute(ast::ExprAttribute), + ExprSubscript(ast::ExprSubscript), + ExprStarred(ast::ExprStarred), + ExprName(ast::ExprName), + ExprList(ast::ExprList), + ExprTuple(ast::ExprTuple), + ExprSlice(ast::ExprSlice), + ExceptHandlerExceptHandler(ast::ExceptHandlerExceptHandler), + PatternMatchValue(ast::PatternMatchValue), + PatternMatchSingleton(ast::PatternMatchSingleton), + PatternMatchSequence(ast::PatternMatchSequence), + PatternMatchMapping(ast::PatternMatchMapping), + PatternMatchClass(ast::PatternMatchClass), + PatternMatchStar(ast::PatternMatchStar), + PatternMatchAs(ast::PatternMatchAs), + PatternMatchOr(ast::PatternMatchOr), + TypeIgnoreTypeIgnore(ast::TypeIgnoreTypeIgnore), Comprehension(Comprehension), Arguments(Arguments), Arg(Arg), @@ -707,7 +711,7 @@ impl AnyNode { } } -impl AstNode for ModModule { +impl AstNode for ast::ModModule { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -735,7 +739,7 @@ impl AstNode for ModModule { AnyNode::from(self) } } -impl AstNode for ModInteractive { +impl AstNode for ast::ModInteractive { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -763,7 +767,7 @@ impl AstNode for ModInteractive { AnyNode::from(self) } } -impl AstNode for ModExpression { +impl AstNode for ast::ModExpression { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -791,7 +795,7 @@ impl AstNode for ModExpression { AnyNode::from(self) } } -impl AstNode for ModFunctionType { +impl AstNode for ast::ModFunctionType { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -819,7 +823,7 @@ impl AstNode for ModFunctionType { AnyNode::from(self) } } -impl AstNode for StmtFunctionDef { +impl AstNode for ast::StmtFunctionDef { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -847,7 +851,7 @@ impl AstNode for StmtFunctionDef { AnyNode::from(self) } } -impl AstNode for StmtAsyncFunctionDef { +impl AstNode for ast::StmtAsyncFunctionDef { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -875,7 +879,7 @@ impl AstNode for StmtAsyncFunctionDef { AnyNode::from(self) } } -impl AstNode for StmtClassDef { +impl AstNode for ast::StmtClassDef { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -903,7 +907,7 @@ impl AstNode for StmtClassDef { AnyNode::from(self) } } -impl AstNode for StmtReturn { +impl AstNode for ast::StmtReturn { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -931,7 +935,7 @@ impl AstNode for StmtReturn { AnyNode::from(self) } } -impl AstNode for StmtDelete { +impl AstNode for ast::StmtDelete { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -959,7 +963,7 @@ impl AstNode for StmtDelete { AnyNode::from(self) } } -impl AstNode for StmtAssign { +impl AstNode for ast::StmtAssign { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -987,7 +991,7 @@ impl AstNode for StmtAssign { AnyNode::from(self) } } -impl AstNode for StmtAugAssign { +impl AstNode for ast::StmtAugAssign { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1015,7 +1019,7 @@ impl AstNode for StmtAugAssign { AnyNode::from(self) } } -impl AstNode for StmtAnnAssign { +impl AstNode for ast::StmtAnnAssign { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1043,7 +1047,7 @@ impl AstNode for StmtAnnAssign { AnyNode::from(self) } } -impl AstNode for StmtFor { +impl AstNode for ast::StmtFor { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1071,7 +1075,7 @@ impl AstNode for StmtFor { AnyNode::from(self) } } -impl AstNode for StmtAsyncFor { +impl AstNode for ast::StmtAsyncFor { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1099,7 +1103,7 @@ impl AstNode for StmtAsyncFor { AnyNode::from(self) } } -impl AstNode for StmtWhile { +impl AstNode for ast::StmtWhile { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1127,7 +1131,7 @@ impl AstNode for StmtWhile { AnyNode::from(self) } } -impl AstNode for StmtIf { +impl AstNode for ast::StmtIf { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1155,7 +1159,7 @@ impl AstNode for StmtIf { AnyNode::from(self) } } -impl AstNode for StmtWith { +impl AstNode for ast::StmtWith { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1183,7 +1187,7 @@ impl AstNode for StmtWith { AnyNode::from(self) } } -impl AstNode for StmtAsyncWith { +impl AstNode for ast::StmtAsyncWith { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1211,7 +1215,7 @@ impl AstNode for StmtAsyncWith { AnyNode::from(self) } } -impl AstNode for StmtMatch { +impl AstNode for ast::StmtMatch { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1239,7 +1243,7 @@ impl AstNode for StmtMatch { AnyNode::from(self) } } -impl AstNode for StmtRaise { +impl AstNode for ast::StmtRaise { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1267,7 +1271,7 @@ impl AstNode for StmtRaise { AnyNode::from(self) } } -impl AstNode for StmtTry { +impl AstNode for ast::StmtTry { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1295,7 +1299,7 @@ impl AstNode for StmtTry { AnyNode::from(self) } } -impl AstNode for StmtTryStar { +impl AstNode for ast::StmtTryStar { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1323,7 +1327,7 @@ impl AstNode for StmtTryStar { AnyNode::from(self) } } -impl AstNode for StmtAssert { +impl AstNode for ast::StmtAssert { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1351,7 +1355,7 @@ impl AstNode for StmtAssert { AnyNode::from(self) } } -impl AstNode for StmtImport { +impl AstNode for ast::StmtImport { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1379,7 +1383,7 @@ impl AstNode for StmtImport { AnyNode::from(self) } } -impl AstNode for StmtImportFrom { +impl AstNode for ast::StmtImportFrom { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1407,7 +1411,7 @@ impl AstNode for StmtImportFrom { AnyNode::from(self) } } -impl AstNode for StmtGlobal { +impl AstNode for ast::StmtGlobal { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1435,7 +1439,7 @@ impl AstNode for StmtGlobal { AnyNode::from(self) } } -impl AstNode for StmtNonlocal { +impl AstNode for ast::StmtNonlocal { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1463,7 +1467,7 @@ impl AstNode for StmtNonlocal { AnyNode::from(self) } } -impl AstNode for StmtExpr { +impl AstNode for ast::StmtExpr { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1491,7 +1495,7 @@ impl AstNode for StmtExpr { AnyNode::from(self) } } -impl AstNode for StmtPass { +impl AstNode for ast::StmtPass { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1519,7 +1523,7 @@ impl AstNode for StmtPass { AnyNode::from(self) } } -impl AstNode for StmtBreak { +impl AstNode for ast::StmtBreak { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1547,7 +1551,7 @@ impl AstNode for StmtBreak { AnyNode::from(self) } } -impl AstNode for StmtContinue { +impl AstNode for ast::StmtContinue { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1575,7 +1579,7 @@ impl AstNode for StmtContinue { AnyNode::from(self) } } -impl AstNode for ExprBoolOp { +impl AstNode for ast::ExprBoolOp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1603,7 +1607,7 @@ impl AstNode for ExprBoolOp { AnyNode::from(self) } } -impl AstNode for ExprNamedExpr { +impl AstNode for ast::ExprNamedExpr { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1631,7 +1635,7 @@ impl AstNode for ExprNamedExpr { AnyNode::from(self) } } -impl AstNode for ExprBinOp { +impl AstNode for ast::ExprBinOp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1659,7 +1663,7 @@ impl AstNode for ExprBinOp { AnyNode::from(self) } } -impl AstNode for ExprUnaryOp { +impl AstNode for ast::ExprUnaryOp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1687,7 +1691,7 @@ impl AstNode for ExprUnaryOp { AnyNode::from(self) } } -impl AstNode for ExprLambda { +impl AstNode for ast::ExprLambda { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1715,7 +1719,7 @@ impl AstNode for ExprLambda { AnyNode::from(self) } } -impl AstNode for ExprIfExp { +impl AstNode for ast::ExprIfExp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1743,7 +1747,7 @@ impl AstNode for ExprIfExp { AnyNode::from(self) } } -impl AstNode for ExprDict { +impl AstNode for ast::ExprDict { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1771,7 +1775,7 @@ impl AstNode for ExprDict { AnyNode::from(self) } } -impl AstNode for ExprSet { +impl AstNode for ast::ExprSet { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1799,7 +1803,7 @@ impl AstNode for ExprSet { AnyNode::from(self) } } -impl AstNode for ExprListComp { +impl AstNode for ast::ExprListComp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1827,7 +1831,7 @@ impl AstNode for ExprListComp { AnyNode::from(self) } } -impl AstNode for ExprSetComp { +impl AstNode for ast::ExprSetComp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1855,7 +1859,7 @@ impl AstNode for ExprSetComp { AnyNode::from(self) } } -impl AstNode for ExprDictComp { +impl AstNode for ast::ExprDictComp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1883,7 +1887,7 @@ impl AstNode for ExprDictComp { AnyNode::from(self) } } -impl AstNode for ExprGeneratorExp { +impl AstNode for ast::ExprGeneratorExp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1911,7 +1915,7 @@ impl AstNode for ExprGeneratorExp { AnyNode::from(self) } } -impl AstNode for ExprAwait { +impl AstNode for ast::ExprAwait { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1939,7 +1943,7 @@ impl AstNode for ExprAwait { AnyNode::from(self) } } -impl AstNode for ExprYield { +impl AstNode for ast::ExprYield { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1967,7 +1971,7 @@ impl AstNode for ExprYield { AnyNode::from(self) } } -impl AstNode for ExprYieldFrom { +impl AstNode for ast::ExprYieldFrom { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1995,7 +1999,7 @@ impl AstNode for ExprYieldFrom { AnyNode::from(self) } } -impl AstNode for ExprCompare { +impl AstNode for ast::ExprCompare { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2023,7 +2027,7 @@ impl AstNode for ExprCompare { AnyNode::from(self) } } -impl AstNode for ExprCall { +impl AstNode for ast::ExprCall { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2051,7 +2055,7 @@ impl AstNode for ExprCall { AnyNode::from(self) } } -impl AstNode for ExprFormattedValue { +impl AstNode for ast::ExprFormattedValue { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2079,7 +2083,7 @@ impl AstNode for ExprFormattedValue { AnyNode::from(self) } } -impl AstNode for ExprJoinedStr { +impl AstNode for ast::ExprJoinedStr { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2107,7 +2111,7 @@ impl AstNode for ExprJoinedStr { AnyNode::from(self) } } -impl AstNode for ExprConstant { +impl AstNode for ast::ExprConstant { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2135,7 +2139,7 @@ impl AstNode for ExprConstant { AnyNode::from(self) } } -impl AstNode for ExprAttribute { +impl AstNode for ast::ExprAttribute { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2163,7 +2167,7 @@ impl AstNode for ExprAttribute { AnyNode::from(self) } } -impl AstNode for ExprSubscript { +impl AstNode for ast::ExprSubscript { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2191,7 +2195,7 @@ impl AstNode for ExprSubscript { AnyNode::from(self) } } -impl AstNode for ExprStarred { +impl AstNode for ast::ExprStarred { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2219,7 +2223,7 @@ impl AstNode for ExprStarred { AnyNode::from(self) } } -impl AstNode for ExprName { +impl AstNode for ast::ExprName { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2247,7 +2251,7 @@ impl AstNode for ExprName { AnyNode::from(self) } } -impl AstNode for ExprList { +impl AstNode for ast::ExprList { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2275,7 +2279,7 @@ impl AstNode for ExprList { AnyNode::from(self) } } -impl AstNode for ExprTuple { +impl AstNode for ast::ExprTuple { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2303,7 +2307,7 @@ impl AstNode for ExprTuple { AnyNode::from(self) } } -impl AstNode for ExprSlice { +impl AstNode for ast::ExprSlice { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2331,7 +2335,7 @@ impl AstNode for ExprSlice { AnyNode::from(self) } } -impl AstNode for ExceptHandlerExceptHandler { +impl AstNode for ast::ExceptHandlerExceptHandler { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2359,7 +2363,7 @@ impl AstNode for ExceptHandlerExceptHandler { AnyNode::from(self) } } -impl AstNode for PatternMatchValue { +impl AstNode for ast::PatternMatchValue { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2387,7 +2391,7 @@ impl AstNode for PatternMatchValue { AnyNode::from(self) } } -impl AstNode for PatternMatchSingleton { +impl AstNode for ast::PatternMatchSingleton { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2415,7 +2419,7 @@ impl AstNode for PatternMatchSingleton { AnyNode::from(self) } } -impl AstNode for PatternMatchSequence { +impl AstNode for ast::PatternMatchSequence { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2443,7 +2447,7 @@ impl AstNode for PatternMatchSequence { AnyNode::from(self) } } -impl AstNode for PatternMatchMapping { +impl AstNode for ast::PatternMatchMapping { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2471,7 +2475,7 @@ impl AstNode for PatternMatchMapping { AnyNode::from(self) } } -impl AstNode for PatternMatchClass { +impl AstNode for ast::PatternMatchClass { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2499,7 +2503,7 @@ impl AstNode for PatternMatchClass { AnyNode::from(self) } } -impl AstNode for PatternMatchStar { +impl AstNode for ast::PatternMatchStar { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2527,7 +2531,7 @@ impl AstNode for PatternMatchStar { AnyNode::from(self) } } -impl AstNode for PatternMatchAs { +impl AstNode for ast::PatternMatchAs { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2555,7 +2559,7 @@ impl AstNode for PatternMatchAs { AnyNode::from(self) } } -impl AstNode for PatternMatchOr { +impl AstNode for ast::PatternMatchOr { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2583,7 +2587,7 @@ impl AstNode for PatternMatchOr { AnyNode::from(self) } } -impl AstNode for TypeIgnoreTypeIgnore { +impl AstNode for ast::TypeIgnoreTypeIgnore { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2976,410 +2980,410 @@ impl From for AnyNode { } } -impl From for AnyNode { - fn from(node: ModModule) -> Self { +impl From for AnyNode { + fn from(node: ast::ModModule) -> Self { AnyNode::ModModule(node) } } -impl From for AnyNode { - fn from(node: ModInteractive) -> Self { +impl From for AnyNode { + fn from(node: ast::ModInteractive) -> Self { AnyNode::ModInteractive(node) } } -impl From for AnyNode { - fn from(node: ModExpression) -> Self { +impl From for AnyNode { + fn from(node: ast::ModExpression) -> Self { AnyNode::ModExpression(node) } } -impl From for AnyNode { - fn from(node: ModFunctionType) -> Self { +impl From for AnyNode { + fn from(node: ast::ModFunctionType) -> Self { AnyNode::ModFunctionType(node) } } -impl From for AnyNode { - fn from(node: StmtFunctionDef) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtFunctionDef) -> Self { AnyNode::StmtFunctionDef(node) } } -impl From for AnyNode { - fn from(node: StmtAsyncFunctionDef) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtAsyncFunctionDef) -> Self { AnyNode::StmtAsyncFunctionDef(node) } } -impl From for AnyNode { - fn from(node: StmtClassDef) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtClassDef) -> Self { AnyNode::StmtClassDef(node) } } -impl From for AnyNode { - fn from(node: StmtReturn) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtReturn) -> Self { AnyNode::StmtReturn(node) } } -impl From for AnyNode { - fn from(node: StmtDelete) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtDelete) -> Self { AnyNode::StmtDelete(node) } } -impl From for AnyNode { - fn from(node: StmtAssign) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtAssign) -> Self { AnyNode::StmtAssign(node) } } -impl From for AnyNode { - fn from(node: StmtAugAssign) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtAugAssign) -> Self { AnyNode::StmtAugAssign(node) } } -impl From for AnyNode { - fn from(node: StmtAnnAssign) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtAnnAssign) -> Self { AnyNode::StmtAnnAssign(node) } } -impl From for AnyNode { - fn from(node: StmtFor) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtFor) -> Self { AnyNode::StmtFor(node) } } -impl From for AnyNode { - fn from(node: StmtAsyncFor) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtAsyncFor) -> Self { AnyNode::StmtAsyncFor(node) } } -impl From for AnyNode { - fn from(node: StmtWhile) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtWhile) -> Self { AnyNode::StmtWhile(node) } } -impl From for AnyNode { - fn from(node: StmtIf) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtIf) -> Self { AnyNode::StmtIf(node) } } -impl From for AnyNode { - fn from(node: StmtWith) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtWith) -> Self { AnyNode::StmtWith(node) } } -impl From for AnyNode { - fn from(node: StmtAsyncWith) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtAsyncWith) -> Self { AnyNode::StmtAsyncWith(node) } } -impl From for AnyNode { - fn from(node: StmtMatch) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtMatch) -> Self { AnyNode::StmtMatch(node) } } -impl From for AnyNode { - fn from(node: StmtRaise) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtRaise) -> Self { AnyNode::StmtRaise(node) } } -impl From for AnyNode { - fn from(node: StmtTry) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtTry) -> Self { AnyNode::StmtTry(node) } } -impl From for AnyNode { - fn from(node: StmtTryStar) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtTryStar) -> Self { AnyNode::StmtTryStar(node) } } -impl From for AnyNode { - fn from(node: StmtAssert) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtAssert) -> Self { AnyNode::StmtAssert(node) } } -impl From for AnyNode { - fn from(node: StmtImport) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtImport) -> Self { AnyNode::StmtImport(node) } } -impl From for AnyNode { - fn from(node: StmtImportFrom) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtImportFrom) -> Self { AnyNode::StmtImportFrom(node) } } -impl From for AnyNode { - fn from(node: StmtGlobal) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtGlobal) -> Self { AnyNode::StmtGlobal(node) } } -impl From for AnyNode { - fn from(node: StmtNonlocal) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtNonlocal) -> Self { AnyNode::StmtNonlocal(node) } } -impl From for AnyNode { - fn from(node: StmtExpr) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtExpr) -> Self { AnyNode::StmtExpr(node) } } -impl From for AnyNode { - fn from(node: StmtPass) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtPass) -> Self { AnyNode::StmtPass(node) } } -impl From for AnyNode { - fn from(node: StmtBreak) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtBreak) -> Self { AnyNode::StmtBreak(node) } } -impl From for AnyNode { - fn from(node: StmtContinue) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtContinue) -> Self { AnyNode::StmtContinue(node) } } -impl From for AnyNode { - fn from(node: ExprBoolOp) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprBoolOp) -> Self { AnyNode::ExprBoolOp(node) } } -impl From for AnyNode { - fn from(node: ExprNamedExpr) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprNamedExpr) -> Self { AnyNode::ExprNamedExpr(node) } } -impl From for AnyNode { - fn from(node: ExprBinOp) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprBinOp) -> Self { AnyNode::ExprBinOp(node) } } -impl From for AnyNode { - fn from(node: ExprUnaryOp) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprUnaryOp) -> Self { AnyNode::ExprUnaryOp(node) } } -impl From for AnyNode { - fn from(node: ExprLambda) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprLambda) -> Self { AnyNode::ExprLambda(node) } } -impl From for AnyNode { - fn from(node: ExprIfExp) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprIfExp) -> Self { AnyNode::ExprIfExp(node) } } -impl From for AnyNode { - fn from(node: ExprDict) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprDict) -> Self { AnyNode::ExprDict(node) } } -impl From for AnyNode { - fn from(node: ExprSet) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprSet) -> Self { AnyNode::ExprSet(node) } } -impl From for AnyNode { - fn from(node: ExprListComp) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprListComp) -> Self { AnyNode::ExprListComp(node) } } -impl From for AnyNode { - fn from(node: ExprSetComp) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprSetComp) -> Self { AnyNode::ExprSetComp(node) } } -impl From for AnyNode { - fn from(node: ExprDictComp) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprDictComp) -> Self { AnyNode::ExprDictComp(node) } } -impl From for AnyNode { - fn from(node: ExprGeneratorExp) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprGeneratorExp) -> Self { AnyNode::ExprGeneratorExp(node) } } -impl From for AnyNode { - fn from(node: ExprAwait) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprAwait) -> Self { AnyNode::ExprAwait(node) } } -impl From for AnyNode { - fn from(node: ExprYield) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprYield) -> Self { AnyNode::ExprYield(node) } } -impl From for AnyNode { - fn from(node: ExprYieldFrom) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprYieldFrom) -> Self { AnyNode::ExprYieldFrom(node) } } -impl From for AnyNode { - fn from(node: ExprCompare) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprCompare) -> Self { AnyNode::ExprCompare(node) } } -impl From for AnyNode { - fn from(node: ExprCall) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprCall) -> Self { AnyNode::ExprCall(node) } } -impl From for AnyNode { - fn from(node: ExprFormattedValue) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprFormattedValue) -> Self { AnyNode::ExprFormattedValue(node) } } -impl From for AnyNode { - fn from(node: ExprJoinedStr) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprJoinedStr) -> Self { AnyNode::ExprJoinedStr(node) } } -impl From for AnyNode { - fn from(node: ExprConstant) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprConstant) -> Self { AnyNode::ExprConstant(node) } } -impl From for AnyNode { - fn from(node: ExprAttribute) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprAttribute) -> Self { AnyNode::ExprAttribute(node) } } -impl From for AnyNode { - fn from(node: ExprSubscript) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprSubscript) -> Self { AnyNode::ExprSubscript(node) } } -impl From for AnyNode { - fn from(node: ExprStarred) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprStarred) -> Self { AnyNode::ExprStarred(node) } } -impl From for AnyNode { - fn from(node: ExprName) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprName) -> Self { AnyNode::ExprName(node) } } -impl From for AnyNode { - fn from(node: ExprList) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprList) -> Self { AnyNode::ExprList(node) } } -impl From for AnyNode { - fn from(node: ExprTuple) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprTuple) -> Self { AnyNode::ExprTuple(node) } } -impl From for AnyNode { - fn from(node: ExprSlice) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprSlice) -> Self { AnyNode::ExprSlice(node) } } -impl From for AnyNode { - fn from(node: ExceptHandlerExceptHandler) -> Self { +impl From for AnyNode { + fn from(node: ast::ExceptHandlerExceptHandler) -> Self { AnyNode::ExceptHandlerExceptHandler(node) } } -impl From for AnyNode { - fn from(node: PatternMatchValue) -> Self { +impl From for AnyNode { + fn from(node: ast::PatternMatchValue) -> Self { AnyNode::PatternMatchValue(node) } } -impl From for AnyNode { - fn from(node: PatternMatchSingleton) -> Self { +impl From for AnyNode { + fn from(node: ast::PatternMatchSingleton) -> Self { AnyNode::PatternMatchSingleton(node) } } -impl From for AnyNode { - fn from(node: PatternMatchSequence) -> Self { +impl From for AnyNode { + fn from(node: ast::PatternMatchSequence) -> Self { AnyNode::PatternMatchSequence(node) } } -impl From for AnyNode { - fn from(node: PatternMatchMapping) -> Self { +impl From for AnyNode { + fn from(node: ast::PatternMatchMapping) -> Self { AnyNode::PatternMatchMapping(node) } } -impl From for AnyNode { - fn from(node: PatternMatchClass) -> Self { +impl From for AnyNode { + fn from(node: ast::PatternMatchClass) -> Self { AnyNode::PatternMatchClass(node) } } -impl From for AnyNode { - fn from(node: PatternMatchStar) -> Self { +impl From for AnyNode { + fn from(node: ast::PatternMatchStar) -> Self { AnyNode::PatternMatchStar(node) } } -impl From for AnyNode { - fn from(node: PatternMatchAs) -> Self { +impl From for AnyNode { + fn from(node: ast::PatternMatchAs) -> Self { AnyNode::PatternMatchAs(node) } } -impl From for AnyNode { - fn from(node: PatternMatchOr) -> Self { +impl From for AnyNode { + fn from(node: ast::PatternMatchOr) -> Self { AnyNode::PatternMatchOr(node) } } -impl From for AnyNode { - fn from(node: TypeIgnoreTypeIgnore) -> Self { +impl From for AnyNode { + fn from(node: ast::TypeIgnoreTypeIgnore) -> Self { AnyNode::TypeIgnoreTypeIgnore(node) } } @@ -3516,74 +3520,74 @@ impl Ranged for AnyNode { #[derive(Copy, Clone, Debug, is_macro::Is, PartialEq)] pub enum AnyNodeRef<'a> { - ModModule(&'a ModModule), - ModInteractive(&'a ModInteractive), - ModExpression(&'a ModExpression), - ModFunctionType(&'a ModFunctionType), - StmtFunctionDef(&'a StmtFunctionDef), - StmtAsyncFunctionDef(&'a StmtAsyncFunctionDef), - StmtClassDef(&'a StmtClassDef), - StmtReturn(&'a StmtReturn), - StmtDelete(&'a StmtDelete), - StmtAssign(&'a StmtAssign), - StmtAugAssign(&'a StmtAugAssign), - StmtAnnAssign(&'a StmtAnnAssign), - StmtFor(&'a StmtFor), - StmtAsyncFor(&'a StmtAsyncFor), - StmtWhile(&'a StmtWhile), - StmtIf(&'a StmtIf), - StmtWith(&'a StmtWith), - StmtAsyncWith(&'a StmtAsyncWith), - StmtMatch(&'a StmtMatch), - StmtRaise(&'a StmtRaise), - StmtTry(&'a StmtTry), - StmtTryStar(&'a StmtTryStar), - StmtAssert(&'a StmtAssert), - StmtImport(&'a StmtImport), - StmtImportFrom(&'a StmtImportFrom), - StmtGlobal(&'a StmtGlobal), - StmtNonlocal(&'a StmtNonlocal), - StmtExpr(&'a StmtExpr), - StmtPass(&'a StmtPass), - StmtBreak(&'a StmtBreak), - StmtContinue(&'a StmtContinue), - ExprBoolOp(&'a ExprBoolOp), - ExprNamedExpr(&'a ExprNamedExpr), - ExprBinOp(&'a ExprBinOp), - ExprUnaryOp(&'a ExprUnaryOp), - ExprLambda(&'a ExprLambda), - ExprIfExp(&'a ExprIfExp), - ExprDict(&'a ExprDict), - ExprSet(&'a ExprSet), - ExprListComp(&'a ExprListComp), - ExprSetComp(&'a ExprSetComp), - ExprDictComp(&'a ExprDictComp), - ExprGeneratorExp(&'a ExprGeneratorExp), - ExprAwait(&'a ExprAwait), - ExprYield(&'a ExprYield), - ExprYieldFrom(&'a ExprYieldFrom), - ExprCompare(&'a ExprCompare), - ExprCall(&'a ExprCall), - ExprFormattedValue(&'a ExprFormattedValue), - ExprJoinedStr(&'a ExprJoinedStr), - ExprConstant(&'a ExprConstant), - ExprAttribute(&'a ExprAttribute), - ExprSubscript(&'a ExprSubscript), - ExprStarred(&'a ExprStarred), - ExprName(&'a ExprName), - ExprList(&'a ExprList), - ExprTuple(&'a ExprTuple), - ExprSlice(&'a ExprSlice), - ExceptHandlerExceptHandler(&'a ExceptHandlerExceptHandler), - PatternMatchValue(&'a PatternMatchValue), - PatternMatchSingleton(&'a PatternMatchSingleton), - PatternMatchSequence(&'a PatternMatchSequence), - PatternMatchMapping(&'a PatternMatchMapping), - PatternMatchClass(&'a PatternMatchClass), - PatternMatchStar(&'a PatternMatchStar), - PatternMatchAs(&'a PatternMatchAs), - PatternMatchOr(&'a PatternMatchOr), - TypeIgnoreTypeIgnore(&'a TypeIgnoreTypeIgnore), + ModModule(&'a ast::ModModule), + ModInteractive(&'a ast::ModInteractive), + ModExpression(&'a ast::ModExpression), + ModFunctionType(&'a ast::ModFunctionType), + StmtFunctionDef(&'a ast::StmtFunctionDef), + StmtAsyncFunctionDef(&'a ast::StmtAsyncFunctionDef), + StmtClassDef(&'a ast::StmtClassDef), + StmtReturn(&'a ast::StmtReturn), + StmtDelete(&'a ast::StmtDelete), + StmtAssign(&'a ast::StmtAssign), + StmtAugAssign(&'a ast::StmtAugAssign), + StmtAnnAssign(&'a ast::StmtAnnAssign), + StmtFor(&'a ast::StmtFor), + StmtAsyncFor(&'a ast::StmtAsyncFor), + StmtWhile(&'a ast::StmtWhile), + StmtIf(&'a ast::StmtIf), + StmtWith(&'a ast::StmtWith), + StmtAsyncWith(&'a ast::StmtAsyncWith), + StmtMatch(&'a ast::StmtMatch), + StmtRaise(&'a ast::StmtRaise), + StmtTry(&'a ast::StmtTry), + StmtTryStar(&'a ast::StmtTryStar), + StmtAssert(&'a ast::StmtAssert), + StmtImport(&'a ast::StmtImport), + StmtImportFrom(&'a ast::StmtImportFrom), + StmtGlobal(&'a ast::StmtGlobal), + StmtNonlocal(&'a ast::StmtNonlocal), + StmtExpr(&'a ast::StmtExpr), + StmtPass(&'a ast::StmtPass), + StmtBreak(&'a ast::StmtBreak), + StmtContinue(&'a ast::StmtContinue), + ExprBoolOp(&'a ast::ExprBoolOp), + ExprNamedExpr(&'a ast::ExprNamedExpr), + ExprBinOp(&'a ast::ExprBinOp), + ExprUnaryOp(&'a ast::ExprUnaryOp), + ExprLambda(&'a ast::ExprLambda), + ExprIfExp(&'a ast::ExprIfExp), + ExprDict(&'a ast::ExprDict), + ExprSet(&'a ast::ExprSet), + ExprListComp(&'a ast::ExprListComp), + ExprSetComp(&'a ast::ExprSetComp), + ExprDictComp(&'a ast::ExprDictComp), + ExprGeneratorExp(&'a ast::ExprGeneratorExp), + ExprAwait(&'a ast::ExprAwait), + ExprYield(&'a ast::ExprYield), + ExprYieldFrom(&'a ast::ExprYieldFrom), + ExprCompare(&'a ast::ExprCompare), + ExprCall(&'a ast::ExprCall), + ExprFormattedValue(&'a ast::ExprFormattedValue), + ExprJoinedStr(&'a ast::ExprJoinedStr), + ExprConstant(&'a ast::ExprConstant), + ExprAttribute(&'a ast::ExprAttribute), + ExprSubscript(&'a ast::ExprSubscript), + ExprStarred(&'a ast::ExprStarred), + ExprName(&'a ast::ExprName), + ExprList(&'a ast::ExprList), + ExprTuple(&'a ast::ExprTuple), + ExprSlice(&'a ast::ExprSlice), + ExceptHandlerExceptHandler(&'a ast::ExceptHandlerExceptHandler), + PatternMatchValue(&'a ast::PatternMatchValue), + PatternMatchSingleton(&'a ast::PatternMatchSingleton), + PatternMatchSequence(&'a ast::PatternMatchSequence), + PatternMatchMapping(&'a ast::PatternMatchMapping), + PatternMatchClass(&'a ast::PatternMatchClass), + PatternMatchStar(&'a ast::PatternMatchStar), + PatternMatchAs(&'a ast::PatternMatchAs), + PatternMatchOr(&'a ast::PatternMatchOr), + TypeIgnoreTypeIgnore(&'a ast::TypeIgnoreTypeIgnore), Comprehension(&'a Comprehension), Arguments(&'a Arguments), Arg(&'a Arg), @@ -4281,410 +4285,410 @@ impl AnyNodeRef<'_> { } } -impl<'a> From<&'a ModModule> for AnyNodeRef<'a> { - fn from(node: &'a ModModule) -> Self { +impl<'a> From<&'a ast::ModModule> for AnyNodeRef<'a> { + fn from(node: &'a ast::ModModule) -> Self { AnyNodeRef::ModModule(node) } } -impl<'a> From<&'a ModInteractive> for AnyNodeRef<'a> { - fn from(node: &'a ModInteractive) -> Self { +impl<'a> From<&'a ast::ModInteractive> for AnyNodeRef<'a> { + fn from(node: &'a ast::ModInteractive) -> Self { AnyNodeRef::ModInteractive(node) } } -impl<'a> From<&'a ModExpression> for AnyNodeRef<'a> { - fn from(node: &'a ModExpression) -> Self { +impl<'a> From<&'a ast::ModExpression> for AnyNodeRef<'a> { + fn from(node: &'a ast::ModExpression) -> Self { AnyNodeRef::ModExpression(node) } } -impl<'a> From<&'a ModFunctionType> for AnyNodeRef<'a> { - fn from(node: &'a ModFunctionType) -> Self { +impl<'a> From<&'a ast::ModFunctionType> for AnyNodeRef<'a> { + fn from(node: &'a ast::ModFunctionType) -> Self { AnyNodeRef::ModFunctionType(node) } } -impl<'a> From<&'a StmtFunctionDef> for AnyNodeRef<'a> { - fn from(node: &'a StmtFunctionDef) -> Self { +impl<'a> From<&'a ast::StmtFunctionDef> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtFunctionDef) -> Self { AnyNodeRef::StmtFunctionDef(node) } } -impl<'a> From<&'a StmtAsyncFunctionDef> for AnyNodeRef<'a> { - fn from(node: &'a StmtAsyncFunctionDef) -> Self { +impl<'a> From<&'a ast::StmtAsyncFunctionDef> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtAsyncFunctionDef) -> Self { AnyNodeRef::StmtAsyncFunctionDef(node) } } -impl<'a> From<&'a StmtClassDef> for AnyNodeRef<'a> { - fn from(node: &'a StmtClassDef) -> Self { +impl<'a> From<&'a ast::StmtClassDef> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtClassDef) -> Self { AnyNodeRef::StmtClassDef(node) } } -impl<'a> From<&'a StmtReturn> for AnyNodeRef<'a> { - fn from(node: &'a StmtReturn) -> Self { +impl<'a> From<&'a ast::StmtReturn> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtReturn) -> Self { AnyNodeRef::StmtReturn(node) } } -impl<'a> From<&'a StmtDelete> for AnyNodeRef<'a> { - fn from(node: &'a StmtDelete) -> Self { +impl<'a> From<&'a ast::StmtDelete> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtDelete) -> Self { AnyNodeRef::StmtDelete(node) } } -impl<'a> From<&'a StmtAssign> for AnyNodeRef<'a> { - fn from(node: &'a StmtAssign) -> Self { +impl<'a> From<&'a ast::StmtAssign> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtAssign) -> Self { AnyNodeRef::StmtAssign(node) } } -impl<'a> From<&'a StmtAugAssign> for AnyNodeRef<'a> { - fn from(node: &'a StmtAugAssign) -> Self { +impl<'a> From<&'a ast::StmtAugAssign> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtAugAssign) -> Self { AnyNodeRef::StmtAugAssign(node) } } -impl<'a> From<&'a StmtAnnAssign> for AnyNodeRef<'a> { - fn from(node: &'a StmtAnnAssign) -> Self { +impl<'a> From<&'a ast::StmtAnnAssign> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtAnnAssign) -> Self { AnyNodeRef::StmtAnnAssign(node) } } -impl<'a> From<&'a StmtFor> for AnyNodeRef<'a> { - fn from(node: &'a StmtFor) -> Self { +impl<'a> From<&'a ast::StmtFor> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtFor) -> Self { AnyNodeRef::StmtFor(node) } } -impl<'a> From<&'a StmtAsyncFor> for AnyNodeRef<'a> { - fn from(node: &'a StmtAsyncFor) -> Self { +impl<'a> From<&'a ast::StmtAsyncFor> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtAsyncFor) -> Self { AnyNodeRef::StmtAsyncFor(node) } } -impl<'a> From<&'a StmtWhile> for AnyNodeRef<'a> { - fn from(node: &'a StmtWhile) -> Self { +impl<'a> From<&'a ast::StmtWhile> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtWhile) -> Self { AnyNodeRef::StmtWhile(node) } } -impl<'a> From<&'a StmtIf> for AnyNodeRef<'a> { - fn from(node: &'a StmtIf) -> Self { +impl<'a> From<&'a ast::StmtIf> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtIf) -> Self { AnyNodeRef::StmtIf(node) } } -impl<'a> From<&'a StmtWith> for AnyNodeRef<'a> { - fn from(node: &'a StmtWith) -> Self { +impl<'a> From<&'a ast::StmtWith> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtWith) -> Self { AnyNodeRef::StmtWith(node) } } -impl<'a> From<&'a StmtAsyncWith> for AnyNodeRef<'a> { - fn from(node: &'a StmtAsyncWith) -> Self { +impl<'a> From<&'a ast::StmtAsyncWith> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtAsyncWith) -> Self { AnyNodeRef::StmtAsyncWith(node) } } -impl<'a> From<&'a StmtMatch> for AnyNodeRef<'a> { - fn from(node: &'a StmtMatch) -> Self { +impl<'a> From<&'a ast::StmtMatch> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtMatch) -> Self { AnyNodeRef::StmtMatch(node) } } -impl<'a> From<&'a StmtRaise> for AnyNodeRef<'a> { - fn from(node: &'a StmtRaise) -> Self { +impl<'a> From<&'a ast::StmtRaise> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtRaise) -> Self { AnyNodeRef::StmtRaise(node) } } -impl<'a> From<&'a StmtTry> for AnyNodeRef<'a> { - fn from(node: &'a StmtTry) -> Self { +impl<'a> From<&'a ast::StmtTry> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtTry) -> Self { AnyNodeRef::StmtTry(node) } } -impl<'a> From<&'a StmtTryStar> for AnyNodeRef<'a> { - fn from(node: &'a StmtTryStar) -> Self { +impl<'a> From<&'a ast::StmtTryStar> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtTryStar) -> Self { AnyNodeRef::StmtTryStar(node) } } -impl<'a> From<&'a StmtAssert> for AnyNodeRef<'a> { - fn from(node: &'a StmtAssert) -> Self { +impl<'a> From<&'a ast::StmtAssert> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtAssert) -> Self { AnyNodeRef::StmtAssert(node) } } -impl<'a> From<&'a StmtImport> for AnyNodeRef<'a> { - fn from(node: &'a StmtImport) -> Self { +impl<'a> From<&'a ast::StmtImport> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtImport) -> Self { AnyNodeRef::StmtImport(node) } } -impl<'a> From<&'a StmtImportFrom> for AnyNodeRef<'a> { - fn from(node: &'a StmtImportFrom) -> Self { +impl<'a> From<&'a ast::StmtImportFrom> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtImportFrom) -> Self { AnyNodeRef::StmtImportFrom(node) } } -impl<'a> From<&'a StmtGlobal> for AnyNodeRef<'a> { - fn from(node: &'a StmtGlobal) -> Self { +impl<'a> From<&'a ast::StmtGlobal> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtGlobal) -> Self { AnyNodeRef::StmtGlobal(node) } } -impl<'a> From<&'a StmtNonlocal> for AnyNodeRef<'a> { - fn from(node: &'a StmtNonlocal) -> Self { +impl<'a> From<&'a ast::StmtNonlocal> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtNonlocal) -> Self { AnyNodeRef::StmtNonlocal(node) } } -impl<'a> From<&'a StmtExpr> for AnyNodeRef<'a> { - fn from(node: &'a StmtExpr) -> Self { +impl<'a> From<&'a ast::StmtExpr> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtExpr) -> Self { AnyNodeRef::StmtExpr(node) } } -impl<'a> From<&'a StmtPass> for AnyNodeRef<'a> { - fn from(node: &'a StmtPass) -> Self { +impl<'a> From<&'a ast::StmtPass> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtPass) -> Self { AnyNodeRef::StmtPass(node) } } -impl<'a> From<&'a StmtBreak> for AnyNodeRef<'a> { - fn from(node: &'a StmtBreak) -> Self { +impl<'a> From<&'a ast::StmtBreak> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtBreak) -> Self { AnyNodeRef::StmtBreak(node) } } -impl<'a> From<&'a StmtContinue> for AnyNodeRef<'a> { - fn from(node: &'a StmtContinue) -> Self { +impl<'a> From<&'a ast::StmtContinue> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtContinue) -> Self { AnyNodeRef::StmtContinue(node) } } -impl<'a> From<&'a ExprBoolOp> for AnyNodeRef<'a> { - fn from(node: &'a ExprBoolOp) -> Self { +impl<'a> From<&'a ast::ExprBoolOp> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprBoolOp) -> Self { AnyNodeRef::ExprBoolOp(node) } } -impl<'a> From<&'a ExprNamedExpr> for AnyNodeRef<'a> { - fn from(node: &'a ExprNamedExpr) -> Self { +impl<'a> From<&'a ast::ExprNamedExpr> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprNamedExpr) -> Self { AnyNodeRef::ExprNamedExpr(node) } } -impl<'a> From<&'a ExprBinOp> for AnyNodeRef<'a> { - fn from(node: &'a ExprBinOp) -> Self { +impl<'a> From<&'a ast::ExprBinOp> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprBinOp) -> Self { AnyNodeRef::ExprBinOp(node) } } -impl<'a> From<&'a ExprUnaryOp> for AnyNodeRef<'a> { - fn from(node: &'a ExprUnaryOp) -> Self { +impl<'a> From<&'a ast::ExprUnaryOp> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprUnaryOp) -> Self { AnyNodeRef::ExprUnaryOp(node) } } -impl<'a> From<&'a ExprLambda> for AnyNodeRef<'a> { - fn from(node: &'a ExprLambda) -> Self { +impl<'a> From<&'a ast::ExprLambda> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprLambda) -> Self { AnyNodeRef::ExprLambda(node) } } -impl<'a> From<&'a ExprIfExp> for AnyNodeRef<'a> { - fn from(node: &'a ExprIfExp) -> Self { +impl<'a> From<&'a ast::ExprIfExp> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprIfExp) -> Self { AnyNodeRef::ExprIfExp(node) } } -impl<'a> From<&'a ExprDict> for AnyNodeRef<'a> { - fn from(node: &'a ExprDict) -> Self { +impl<'a> From<&'a ast::ExprDict> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprDict) -> Self { AnyNodeRef::ExprDict(node) } } -impl<'a> From<&'a ExprSet> for AnyNodeRef<'a> { - fn from(node: &'a ExprSet) -> Self { +impl<'a> From<&'a ast::ExprSet> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprSet) -> Self { AnyNodeRef::ExprSet(node) } } -impl<'a> From<&'a ExprListComp> for AnyNodeRef<'a> { - fn from(node: &'a ExprListComp) -> Self { +impl<'a> From<&'a ast::ExprListComp> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprListComp) -> Self { AnyNodeRef::ExprListComp(node) } } -impl<'a> From<&'a ExprSetComp> for AnyNodeRef<'a> { - fn from(node: &'a ExprSetComp) -> Self { +impl<'a> From<&'a ast::ExprSetComp> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprSetComp) -> Self { AnyNodeRef::ExprSetComp(node) } } -impl<'a> From<&'a ExprDictComp> for AnyNodeRef<'a> { - fn from(node: &'a ExprDictComp) -> Self { +impl<'a> From<&'a ast::ExprDictComp> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprDictComp) -> Self { AnyNodeRef::ExprDictComp(node) } } -impl<'a> From<&'a ExprGeneratorExp> for AnyNodeRef<'a> { - fn from(node: &'a ExprGeneratorExp) -> Self { +impl<'a> From<&'a ast::ExprGeneratorExp> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprGeneratorExp) -> Self { AnyNodeRef::ExprGeneratorExp(node) } } -impl<'a> From<&'a ExprAwait> for AnyNodeRef<'a> { - fn from(node: &'a ExprAwait) -> Self { +impl<'a> From<&'a ast::ExprAwait> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprAwait) -> Self { AnyNodeRef::ExprAwait(node) } } -impl<'a> From<&'a ExprYield> for AnyNodeRef<'a> { - fn from(node: &'a ExprYield) -> Self { +impl<'a> From<&'a ast::ExprYield> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprYield) -> Self { AnyNodeRef::ExprYield(node) } } -impl<'a> From<&'a ExprYieldFrom> for AnyNodeRef<'a> { - fn from(node: &'a ExprYieldFrom) -> Self { +impl<'a> From<&'a ast::ExprYieldFrom> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprYieldFrom) -> Self { AnyNodeRef::ExprYieldFrom(node) } } -impl<'a> From<&'a ExprCompare> for AnyNodeRef<'a> { - fn from(node: &'a ExprCompare) -> Self { +impl<'a> From<&'a ast::ExprCompare> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprCompare) -> Self { AnyNodeRef::ExprCompare(node) } } -impl<'a> From<&'a ExprCall> for AnyNodeRef<'a> { - fn from(node: &'a ExprCall) -> Self { +impl<'a> From<&'a ast::ExprCall> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprCall) -> Self { AnyNodeRef::ExprCall(node) } } -impl<'a> From<&'a ExprFormattedValue> for AnyNodeRef<'a> { - fn from(node: &'a ExprFormattedValue) -> Self { +impl<'a> From<&'a ast::ExprFormattedValue> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprFormattedValue) -> Self { AnyNodeRef::ExprFormattedValue(node) } } -impl<'a> From<&'a ExprJoinedStr> for AnyNodeRef<'a> { - fn from(node: &'a ExprJoinedStr) -> Self { +impl<'a> From<&'a ast::ExprJoinedStr> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprJoinedStr) -> Self { AnyNodeRef::ExprJoinedStr(node) } } -impl<'a> From<&'a ExprConstant> for AnyNodeRef<'a> { - fn from(node: &'a ExprConstant) -> Self { +impl<'a> From<&'a ast::ExprConstant> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprConstant) -> Self { AnyNodeRef::ExprConstant(node) } } -impl<'a> From<&'a ExprAttribute> for AnyNodeRef<'a> { - fn from(node: &'a ExprAttribute) -> Self { +impl<'a> From<&'a ast::ExprAttribute> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprAttribute) -> Self { AnyNodeRef::ExprAttribute(node) } } -impl<'a> From<&'a ExprSubscript> for AnyNodeRef<'a> { - fn from(node: &'a ExprSubscript) -> Self { +impl<'a> From<&'a ast::ExprSubscript> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprSubscript) -> Self { AnyNodeRef::ExprSubscript(node) } } -impl<'a> From<&'a ExprStarred> for AnyNodeRef<'a> { - fn from(node: &'a ExprStarred) -> Self { +impl<'a> From<&'a ast::ExprStarred> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprStarred) -> Self { AnyNodeRef::ExprStarred(node) } } -impl<'a> From<&'a ExprName> for AnyNodeRef<'a> { - fn from(node: &'a ExprName) -> Self { +impl<'a> From<&'a ast::ExprName> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprName) -> Self { AnyNodeRef::ExprName(node) } } -impl<'a> From<&'a ExprList> for AnyNodeRef<'a> { - fn from(node: &'a ExprList) -> Self { +impl<'a> From<&'a ast::ExprList> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprList) -> Self { AnyNodeRef::ExprList(node) } } -impl<'a> From<&'a ExprTuple> for AnyNodeRef<'a> { - fn from(node: &'a ExprTuple) -> Self { +impl<'a> From<&'a ast::ExprTuple> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprTuple) -> Self { AnyNodeRef::ExprTuple(node) } } -impl<'a> From<&'a ExprSlice> for AnyNodeRef<'a> { - fn from(node: &'a ExprSlice) -> Self { +impl<'a> From<&'a ast::ExprSlice> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprSlice) -> Self { AnyNodeRef::ExprSlice(node) } } -impl<'a> From<&'a ExceptHandlerExceptHandler> for AnyNodeRef<'a> { - fn from(node: &'a ExceptHandlerExceptHandler) -> Self { +impl<'a> From<&'a ast::ExceptHandlerExceptHandler> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExceptHandlerExceptHandler) -> Self { AnyNodeRef::ExceptHandlerExceptHandler(node) } } -impl<'a> From<&'a PatternMatchValue> for AnyNodeRef<'a> { - fn from(node: &'a PatternMatchValue) -> Self { +impl<'a> From<&'a ast::PatternMatchValue> for AnyNodeRef<'a> { + fn from(node: &'a ast::PatternMatchValue) -> Self { AnyNodeRef::PatternMatchValue(node) } } -impl<'a> From<&'a PatternMatchSingleton> for AnyNodeRef<'a> { - fn from(node: &'a PatternMatchSingleton) -> Self { +impl<'a> From<&'a ast::PatternMatchSingleton> for AnyNodeRef<'a> { + fn from(node: &'a ast::PatternMatchSingleton) -> Self { AnyNodeRef::PatternMatchSingleton(node) } } -impl<'a> From<&'a PatternMatchSequence> for AnyNodeRef<'a> { - fn from(node: &'a PatternMatchSequence) -> Self { +impl<'a> From<&'a ast::PatternMatchSequence> for AnyNodeRef<'a> { + fn from(node: &'a ast::PatternMatchSequence) -> Self { AnyNodeRef::PatternMatchSequence(node) } } -impl<'a> From<&'a PatternMatchMapping> for AnyNodeRef<'a> { - fn from(node: &'a PatternMatchMapping) -> Self { +impl<'a> From<&'a ast::PatternMatchMapping> for AnyNodeRef<'a> { + fn from(node: &'a ast::PatternMatchMapping) -> Self { AnyNodeRef::PatternMatchMapping(node) } } -impl<'a> From<&'a PatternMatchClass> for AnyNodeRef<'a> { - fn from(node: &'a PatternMatchClass) -> Self { +impl<'a> From<&'a ast::PatternMatchClass> for AnyNodeRef<'a> { + fn from(node: &'a ast::PatternMatchClass) -> Self { AnyNodeRef::PatternMatchClass(node) } } -impl<'a> From<&'a PatternMatchStar> for AnyNodeRef<'a> { - fn from(node: &'a PatternMatchStar) -> Self { +impl<'a> From<&'a ast::PatternMatchStar> for AnyNodeRef<'a> { + fn from(node: &'a ast::PatternMatchStar) -> Self { AnyNodeRef::PatternMatchStar(node) } } -impl<'a> From<&'a PatternMatchAs> for AnyNodeRef<'a> { - fn from(node: &'a PatternMatchAs) -> Self { +impl<'a> From<&'a ast::PatternMatchAs> for AnyNodeRef<'a> { + fn from(node: &'a ast::PatternMatchAs) -> Self { AnyNodeRef::PatternMatchAs(node) } } -impl<'a> From<&'a PatternMatchOr> for AnyNodeRef<'a> { - fn from(node: &'a PatternMatchOr) -> Self { +impl<'a> From<&'a ast::PatternMatchOr> for AnyNodeRef<'a> { + fn from(node: &'a ast::PatternMatchOr) -> Self { AnyNodeRef::PatternMatchOr(node) } } -impl<'a> From<&'a TypeIgnoreTypeIgnore> for AnyNodeRef<'a> { - fn from(node: &'a TypeIgnoreTypeIgnore) -> Self { +impl<'a> From<&'a ast::TypeIgnoreTypeIgnore> for AnyNodeRef<'a> { + fn from(node: &'a ast::TypeIgnoreTypeIgnore) -> Self { AnyNodeRef::TypeIgnoreTypeIgnore(node) } } diff --git a/crates/ruff_python_ast/src/prelude.rs b/crates/ruff_python_ast/src/prelude.rs deleted file mode 100644 index b80837d83c..0000000000 --- a/crates/ruff_python_ast/src/prelude.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub use crate::node::AstNode; -pub use rustpython_ast::*; -pub use rustpython_parser::*; diff --git a/crates/ruff_python_ast/src/visitor.rs b/crates/ruff_python_ast/src/visitor.rs index 85c2d27f3c..60cfc2a90c 100644 --- a/crates/ruff_python_ast/src/visitor.rs +++ b/crates/ruff_python_ast/src/visitor.rs @@ -2,10 +2,9 @@ pub mod preorder; -use rustpython_ast::Decorator; use rustpython_parser::ast::{ - self, Alias, Arg, Arguments, BoolOp, CmpOp, Comprehension, Constant, ExceptHandler, Expr, - ExprContext, Keyword, MatchCase, Operator, Pattern, Stmt, UnaryOp, WithItem, + self, Alias, Arg, Arguments, BoolOp, CmpOp, Comprehension, Constant, Decorator, ExceptHandler, + Expr, ExprContext, Keyword, MatchCase, Operator, Pattern, Stmt, UnaryOp, WithItem, }; /// A trait for AST visitors. Visits all nodes in the AST recursively in evaluation-order. diff --git a/crates/ruff_python_ast/src/visitor/preorder.rs b/crates/ruff_python_ast/src/visitor/preorder.rs index 8bf9deabfe..1595ab11ee 100644 --- a/crates/ruff_python_ast/src/visitor/preorder.rs +++ b/crates/ruff_python_ast/src/visitor/preorder.rs @@ -1,4 +1,8 @@ -use crate::prelude::*; +use rustpython_ast::{ArgWithDefault, Mod, TypeIgnore}; +use rustpython_parser::ast::{ + self, Alias, Arg, Arguments, BoolOp, CmpOp, Comprehension, Constant, Decorator, ExceptHandler, + Expr, Keyword, MatchCase, Operator, Pattern, Stmt, UnaryOp, WithItem, +}; /// Visitor that traverses all nodes recursively in pre-order. pub trait PreorderVisitor<'a> { @@ -100,7 +104,7 @@ where V: PreorderVisitor<'a> + ?Sized, { match module { - Mod::Module(ModModule { + Mod::Module(ast::ModModule { body, range: _, type_ignores, @@ -110,9 +114,9 @@ where visitor.visit_type_ignore(ignore); } } - Mod::Interactive(ModInteractive { body, range: _ }) => visitor.visit_body(body), - Mod::Expression(ModExpression { body, range: _ }) => visitor.visit_expr(body), - Mod::FunctionType(ModFunctionType { + Mod::Interactive(ast::ModInteractive { body, range: _ }) => visitor.visit_body(body), + Mod::Expression(ast::ModExpression { body, range: _ }) => visitor.visit_expr(body), + Mod::FunctionType(ast::ModFunctionType { range: _, argtypes, returns, @@ -140,19 +144,19 @@ where V: PreorderVisitor<'a> + ?Sized, { match stmt { - Stmt::Expr(StmtExpr { + Stmt::Expr(ast::StmtExpr { value, range: _range, }) => visitor.visit_expr(value), - Stmt::FunctionDef(StmtFunctionDef { + Stmt::FunctionDef(ast::StmtFunctionDef { args, body, decorator_list, returns, .. }) - | Stmt::AsyncFunctionDef(StmtAsyncFunctionDef { + | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { args, body, decorator_list, @@ -172,7 +176,7 @@ where visitor.visit_body(body); } - Stmt::ClassDef(StmtClassDef { + Stmt::ClassDef(ast::StmtClassDef { bases, keywords, body, @@ -194,7 +198,7 @@ where visitor.visit_body(body); } - Stmt::Return(StmtReturn { + Stmt::Return(ast::StmtReturn { value, range: _range, }) => { @@ -203,7 +207,7 @@ where } } - Stmt::Delete(StmtDelete { + Stmt::Delete(ast::StmtDelete { targets, range: _range, }) => { @@ -212,7 +216,7 @@ where } } - Stmt::Assign(StmtAssign { + Stmt::Assign(ast::StmtAssign { targets, value, range: _, @@ -225,7 +229,7 @@ where visitor.visit_expr(value); } - Stmt::AugAssign(StmtAugAssign { + Stmt::AugAssign(ast::StmtAugAssign { target, op, value, @@ -236,7 +240,7 @@ where visitor.visit_expr(value); } - Stmt::AnnAssign(StmtAnnAssign { + Stmt::AnnAssign(ast::StmtAnnAssign { target, annotation, value, @@ -250,14 +254,14 @@ where } } - Stmt::For(StmtFor { + Stmt::For(ast::StmtFor { target, iter, body, orelse, .. }) - | Stmt::AsyncFor(StmtAsyncFor { + | Stmt::AsyncFor(ast::StmtAsyncFor { target, iter, body, @@ -270,7 +274,7 @@ where visitor.visit_body(orelse); } - Stmt::While(StmtWhile { + Stmt::While(ast::StmtWhile { test, body, orelse, @@ -281,7 +285,7 @@ where visitor.visit_body(orelse); } - Stmt::If(StmtIf { + Stmt::If(ast::StmtIf { test, body, orelse, @@ -292,13 +296,13 @@ where visitor.visit_body(orelse); } - Stmt::With(StmtWith { + Stmt::With(ast::StmtWith { items, body, type_comment: _, range: _, }) - | Stmt::AsyncWith(StmtAsyncWith { + | Stmt::AsyncWith(ast::StmtAsyncWith { items, body, type_comment: _, @@ -310,7 +314,7 @@ where visitor.visit_body(body); } - Stmt::Match(StmtMatch { + Stmt::Match(ast::StmtMatch { subject, cases, range: _range, @@ -321,7 +325,7 @@ where } } - Stmt::Raise(StmtRaise { + Stmt::Raise(ast::StmtRaise { exc, cause, range: _range, @@ -334,14 +338,14 @@ where }; } - Stmt::Try(StmtTry { + Stmt::Try(ast::StmtTry { body, handlers, orelse, finalbody, range: _range, }) - | Stmt::TryStar(StmtTryStar { + | Stmt::TryStar(ast::StmtTryStar { body, handlers, orelse, @@ -356,7 +360,7 @@ where visitor.visit_body(finalbody); } - Stmt::Assert(StmtAssert { + Stmt::Assert(ast::StmtAssert { test, msg, range: _range, @@ -367,7 +371,7 @@ where } } - Stmt::Import(StmtImport { + Stmt::Import(ast::StmtImport { names, range: _range, }) => { @@ -376,7 +380,7 @@ where } } - Stmt::ImportFrom(StmtImportFrom { + Stmt::ImportFrom(ast::StmtImportFrom { range: _, module: _, names, @@ -411,7 +415,7 @@ where V: PreorderVisitor<'a> + ?Sized, { match expr { - Expr::BoolOp(ExprBoolOp { + Expr::BoolOp(ast::ExprBoolOp { op, values, range: _range, @@ -428,7 +432,7 @@ where } }, - Expr::NamedExpr(ExprNamedExpr { + Expr::NamedExpr(ast::ExprNamedExpr { target, value, range: _range, @@ -437,7 +441,7 @@ where visitor.visit_expr(value); } - Expr::BinOp(ExprBinOp { + Expr::BinOp(ast::ExprBinOp { left, op, right, @@ -448,7 +452,7 @@ where visitor.visit_expr(right); } - Expr::UnaryOp(ExprUnaryOp { + Expr::UnaryOp(ast::ExprUnaryOp { op, operand, range: _range, @@ -457,7 +461,7 @@ where visitor.visit_expr(operand); } - Expr::Lambda(ExprLambda { + Expr::Lambda(ast::ExprLambda { args, body, range: _range, @@ -466,7 +470,7 @@ where visitor.visit_expr(body); } - Expr::IfExp(ExprIfExp { + Expr::IfExp(ast::ExprIfExp { test, body, orelse, @@ -477,7 +481,7 @@ where visitor.visit_expr(orelse); } - Expr::Dict(ExprDict { + Expr::Dict(ast::ExprDict { keys, values, range: _range, @@ -490,7 +494,7 @@ where } } - Expr::Set(ExprSet { + Expr::Set(ast::ExprSet { elts, range: _range, }) => { @@ -499,7 +503,7 @@ where } } - Expr::ListComp(ExprListComp { + Expr::ListComp(ast::ExprListComp { elt, generators, range: _range, @@ -510,7 +514,7 @@ where } } - Expr::SetComp(ExprSetComp { + Expr::SetComp(ast::ExprSetComp { elt, generators, range: _range, @@ -521,7 +525,7 @@ where } } - Expr::DictComp(ExprDictComp { + Expr::DictComp(ast::ExprDictComp { key, value, generators, @@ -535,7 +539,7 @@ where } } - Expr::GeneratorExp(ExprGeneratorExp { + Expr::GeneratorExp(ast::ExprGeneratorExp { elt, generators, range: _range, @@ -546,16 +550,16 @@ where } } - Expr::Await(ExprAwait { + Expr::Await(ast::ExprAwait { value, range: _range, }) - | Expr::YieldFrom(ExprYieldFrom { + | Expr::YieldFrom(ast::ExprYieldFrom { value, range: _range, }) => visitor.visit_expr(value), - Expr::Yield(ExprYield { + Expr::Yield(ast::ExprYield { value, range: _range, }) => { @@ -564,7 +568,7 @@ where } } - Expr::Compare(ExprCompare { + Expr::Compare(ast::ExprCompare { left, ops, comparators, @@ -578,7 +582,7 @@ where } } - Expr::Call(ExprCall { + Expr::Call(ast::ExprCall { func, args, keywords, @@ -593,7 +597,7 @@ where } } - Expr::FormattedValue(ExprFormattedValue { + Expr::FormattedValue(ast::ExprFormattedValue { value, format_spec, .. }) => { visitor.visit_expr(value); @@ -603,7 +607,7 @@ where } } - Expr::JoinedStr(ExprJoinedStr { + Expr::JoinedStr(ast::ExprJoinedStr { values, range: _range, }) => { @@ -612,13 +616,13 @@ where } } - Expr::Constant(ExprConstant { + Expr::Constant(ast::ExprConstant { value, range: _, kind: _, }) => visitor.visit_constant(value), - Expr::Attribute(ExprAttribute { + Expr::Attribute(ast::ExprAttribute { value, attr: _, ctx: _, @@ -627,7 +631,7 @@ where visitor.visit_expr(value); } - Expr::Subscript(ExprSubscript { + Expr::Subscript(ast::ExprSubscript { value, slice, ctx: _, @@ -636,7 +640,7 @@ where visitor.visit_expr(value); visitor.visit_expr(slice); } - Expr::Starred(ExprStarred { + Expr::Starred(ast::ExprStarred { value, ctx: _, range: _range, @@ -644,13 +648,13 @@ where visitor.visit_expr(value); } - Expr::Name(ExprName { + Expr::Name(ast::ExprName { id: _, ctx: _, range: _, }) => {} - Expr::List(ExprList { + Expr::List(ast::ExprList { elts, ctx: _, range: _range, @@ -659,7 +663,7 @@ where visitor.visit_expr(expr); } } - Expr::Tuple(ExprTuple { + Expr::Tuple(ast::ExprTuple { elts, ctx: _, range: _range, @@ -669,7 +673,7 @@ where } } - Expr::Slice(ExprSlice { + Expr::Slice(ast::ExprSlice { lower, upper, step, @@ -716,7 +720,7 @@ where V: PreorderVisitor<'a> + ?Sized, { match except_handler { - ExceptHandler::ExceptHandler(ExceptHandlerExceptHandler { + ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { range: _, type_, name: _, @@ -812,19 +816,19 @@ where V: PreorderVisitor<'a> + ?Sized, { match pattern { - Pattern::MatchValue(PatternMatchValue { + Pattern::MatchValue(ast::PatternMatchValue { value, range: _range, }) => visitor.visit_expr(value), - Pattern::MatchSingleton(PatternMatchSingleton { + Pattern::MatchSingleton(ast::PatternMatchSingleton { value, range: _range, }) => { visitor.visit_constant(value); } - Pattern::MatchSequence(PatternMatchSequence { + Pattern::MatchSequence(ast::PatternMatchSequence { patterns, range: _range, }) => { @@ -833,7 +837,7 @@ where } } - Pattern::MatchMapping(PatternMatchMapping { + Pattern::MatchMapping(ast::PatternMatchMapping { keys, patterns, range: _, @@ -845,7 +849,7 @@ where } } - Pattern::MatchClass(PatternMatchClass { + Pattern::MatchClass(ast::PatternMatchClass { cls, patterns, kwd_attrs: _, @@ -864,7 +868,7 @@ where Pattern::MatchStar(_) => {} - Pattern::MatchAs(PatternMatchAs { + Pattern::MatchAs(ast::PatternMatchAs { pattern, range: _, name: _, @@ -874,7 +878,7 @@ where } } - Pattern::MatchOr(PatternMatchOr { + Pattern::MatchOr(ast::PatternMatchOr { patterns, range: _range, }) => { @@ -928,18 +932,20 @@ where #[cfg(test)] mod tests { + use std::fmt::{Debug, Write}; + + use insta::assert_snapshot; + use rustpython_parser::lexer::lex; + use rustpython_parser::{parse_tokens, Mode}; + use crate::node::AnyNodeRef; use crate::visitor::preorder::{ walk_alias, walk_arg, walk_arguments, walk_comprehension, walk_except_handler, walk_expr, walk_keyword, walk_match_case, walk_module, walk_pattern, walk_stmt, walk_type_ignore, walk_with_item, Alias, Arg, Arguments, BoolOp, CmpOp, Comprehension, Constant, ExceptHandler, Expr, Keyword, MatchCase, Mod, Operator, Pattern, PreorderVisitor, Stmt, - String, TypeIgnore, UnaryOp, WithItem, + TypeIgnore, UnaryOp, WithItem, }; - use insta::assert_snapshot; - use rustpython_parser::lexer::lex; - use rustpython_parser::{parse_tokens, Mode}; - use std::fmt::{Debug, Write}; #[test] fn function_arguments() { diff --git a/crates/ruff_python_formatter/src/comments/debug.rs b/crates/ruff_python_formatter/src/comments/debug.rs index a8adfda557..c166f4c561 100644 --- a/crates/ruff_python_formatter/src/comments/debug.rs +++ b/crates/ruff_python_formatter/src/comments/debug.rs @@ -1,9 +1,12 @@ +use std::fmt::{Debug, Formatter, Write}; + +use itertools::Itertools; +use rustpython_parser::ast::Ranged; + +use ruff_formatter::SourceCode; + use crate::comments::node_key::NodeRefEqualityKey; use crate::comments::{CommentsMap, SourceComment}; -use itertools::Itertools; -use ruff_formatter::SourceCode; -use ruff_python_ast::prelude::*; -use std::fmt::{Debug, Formatter, Write}; /// Prints a debug representation of [`SourceComment`] that includes the comment's text pub(crate) struct DebugComment<'a> { @@ -176,14 +179,16 @@ impl Debug for DebugNodeCommentSlice<'_> { #[cfg(test)] mod tests { - use crate::comments::map::MultiMap; - use crate::comments::{CommentLinePosition, Comments, CommentsMap, SourceComment}; use insta::assert_debug_snapshot; - use ruff_formatter::SourceCode; - use ruff_python_ast::node::AnyNode; use ruff_text_size::{TextRange, TextSize}; use rustpython_parser::ast::{StmtBreak, StmtContinue}; + use ruff_formatter::SourceCode; + use ruff_python_ast::node::AnyNode; + + use crate::comments::map::MultiMap; + use crate::comments::{CommentLinePosition, Comments, CommentsMap, SourceComment}; + #[test] fn debug() { let continue_statement = AnyNode::from(StmtContinue { diff --git a/crates/ruff_python_formatter/src/comments/format.rs b/crates/ruff_python_formatter/src/comments/format.rs index 86cc2efbaa..31c7e42cd1 100644 --- a/crates/ruff_python_formatter/src/comments/format.rs +++ b/crates/ruff_python_formatter/src/comments/format.rs @@ -1,12 +1,13 @@ +use ruff_text_size::{TextLen, TextRange, TextSize}; +use rustpython_parser::ast::Ranged; + +use ruff_formatter::{format_args, write, FormatError, SourceCode}; +use ruff_python_ast::node::{AnyNodeRef, AstNode}; + use crate::comments::SourceComment; use crate::context::NodeLevel; use crate::prelude::*; use crate::trivia::{lines_after, lines_before, skip_trailing_trivia}; -use ruff_formatter::{format_args, write, FormatError, SourceCode}; -use ruff_python_ast::node::AnyNodeRef; -use ruff_python_ast::prelude::AstNode; -use ruff_text_size::{TextLen, TextRange, TextSize}; -use rustpython_parser::ast::Ranged; /// Formats the leading comments of a node. pub(crate) fn leading_node_comments(node: &T) -> FormatLeadingComments @@ -69,7 +70,7 @@ where { FormatLeadingAlternateBranchComments { comments, - last_node: last_node.map(std::convert::Into::into), + last_node: last_node.map(Into::into), } } diff --git a/crates/ruff_python_formatter/src/comments/mod.rs b/crates/ruff_python_formatter/src/comments/mod.rs index 7d3aab930f..1c8f00eee9 100644 --- a/crates/ruff_python_formatter/src/comments/mod.rs +++ b/crates/ruff_python_formatter/src/comments/mod.rs @@ -87,10 +87,12 @@ //! //! It is possible to add an additional optional label to [`SourceComment`] If ever the need arises to distinguish two *dangling comments* in the formatting logic, -use crate::comments::debug::{DebugComment, DebugComments}; -use crate::comments::map::MultiMap; -use crate::comments::node_key::NodeRefEqualityKey; -use crate::comments::visitor::CommentsVisitor; +use std::cell::Cell; +use std::fmt::Debug; +use std::rc::Rc; + +use rustpython_parser::ast::Mod; + pub(crate) use format::{ dangling_comments, dangling_node_comments, leading_alternate_branch_comments, leading_comments, leading_node_comments, trailing_comments, trailing_node_comments, @@ -98,10 +100,11 @@ pub(crate) use format::{ use ruff_formatter::{SourceCode, SourceCodeSlice}; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::source_code::CommentRanges; -use rustpython_parser::ast::Mod; -use std::cell::Cell; -use std::fmt::Debug; -use std::rc::Rc; + +use crate::comments::debug::{DebugComment, DebugComments}; +use crate::comments::map::MultiMap; +use crate::comments::node_key::NodeRefEqualityKey; +use crate::comments::visitor::CommentsVisitor; mod debug; mod format; @@ -404,14 +407,16 @@ struct CommentsData<'a> { #[cfg(test)] mod tests { - use crate::comments::Comments; use insta::assert_debug_snapshot; - use ruff_formatter::SourceCode; - use ruff_python_ast::prelude::*; - use ruff_python_ast::source_code::{CommentRanges, CommentRangesBuilder}; + use rustpython_parser::ast::Mod; use rustpython_parser::lexer::lex; use rustpython_parser::{parse_tokens, Mode}; + use ruff_formatter::SourceCode; + use ruff_python_ast::source_code::{CommentRanges, CommentRangesBuilder}; + + use crate::comments::Comments; + struct CommentsTestCase<'a> { module: Mod, comment_ranges: CommentRanges, diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 0e801c7096..f46f9b841a 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -1,16 +1,20 @@ +use std::cmp::Ordering; + +use ruff_text_size::TextRange; +use rustpython_parser::ast; +use rustpython_parser::ast::{Expr, ExprSlice, Ranged}; + +use ruff_python_ast::node::{AnyNodeRef, AstNode}; +use ruff_python_ast::source_code::Locator; +use ruff_python_ast::whitespace; +use ruff_python_whitespace::{PythonWhitespace, UniversalNewlines}; + use crate::comments::visitor::{CommentPlacement, DecoratedComment}; use crate::expression::expr_slice::{assign_comment_in_slice, ExprSliceCommentSection}; use crate::other::arguments::{ assign_argument_separator_comment_placement, find_argument_separators, }; use crate::trivia::{first_non_trivia_token_rev, SimpleTokenizer, Token, TokenKind}; -use ruff_python_ast::node::{AnyNodeRef, AstNode}; -use ruff_python_ast::source_code::Locator; -use ruff_python_ast::whitespace; -use ruff_python_whitespace::{PythonWhitespace, UniversalNewlines}; -use ruff_text_size::TextRange; -use rustpython_parser::ast::{Expr, ExprSlice, Ranged}; -use std::cmp::Ordering; /// Implements the custom comment placement logic. pub(super) fn place_comment<'a>( @@ -574,8 +578,6 @@ fn handle_trailing_end_of_line_condition_comment<'a>( comment: DecoratedComment<'a>, locator: &Locator, ) -> CommentPlacement<'a> { - use ruff_python_ast::prelude::*; - // Must be an end of line comment if comment.line_position().is_own_line() { return CommentPlacement::Default(comment); @@ -587,23 +589,25 @@ fn handle_trailing_end_of_line_condition_comment<'a>( }; let expression_before_colon = match comment.enclosing_node() { - AnyNodeRef::StmtIf(StmtIf { test: expr, .. }) - | AnyNodeRef::StmtWhile(StmtWhile { test: expr, .. }) - | AnyNodeRef::StmtFor(StmtFor { iter: expr, .. }) - | AnyNodeRef::StmtAsyncFor(StmtAsyncFor { iter: expr, .. }) => { + AnyNodeRef::StmtIf(ast::StmtIf { test: expr, .. }) + | AnyNodeRef::StmtWhile(ast::StmtWhile { test: expr, .. }) + | AnyNodeRef::StmtFor(ast::StmtFor { iter: expr, .. }) + | AnyNodeRef::StmtAsyncFor(ast::StmtAsyncFor { iter: expr, .. }) => { Some(AnyNodeRef::from(expr.as_ref())) } - AnyNodeRef::StmtWith(StmtWith { items, .. }) - | AnyNodeRef::StmtAsyncWith(StmtAsyncWith { items, .. }) => { + AnyNodeRef::StmtWith(ast::StmtWith { items, .. }) + | AnyNodeRef::StmtAsyncWith(ast::StmtAsyncWith { items, .. }) => { items.last().map(AnyNodeRef::from) } - AnyNodeRef::StmtFunctionDef(StmtFunctionDef { returns, args, .. }) - | AnyNodeRef::StmtAsyncFunctionDef(StmtAsyncFunctionDef { returns, args, .. }) => returns - .as_deref() - .map(AnyNodeRef::from) - .or_else(|| Some(AnyNodeRef::from(args.as_ref()))), - AnyNodeRef::StmtClassDef(StmtClassDef { + AnyNodeRef::StmtFunctionDef(ast::StmtFunctionDef { returns, args, .. }) + | AnyNodeRef::StmtAsyncFunctionDef(ast::StmtAsyncFunctionDef { returns, args, .. }) => { + returns + .as_deref() + .map(AnyNodeRef::from) + .or_else(|| Some(AnyNodeRef::from(args.as_ref()))) + } + AnyNodeRef::StmtClassDef(ast::StmtClassDef { bases, keywords, .. }) => keywords .last() @@ -1104,21 +1108,21 @@ where } fn last_child_in_body(node: AnyNodeRef) -> Option { - use ruff_python_ast::prelude::*; - let body = match node { - AnyNodeRef::StmtFunctionDef(StmtFunctionDef { body, .. }) - | AnyNodeRef::StmtAsyncFunctionDef(StmtAsyncFunctionDef { body, .. }) - | AnyNodeRef::StmtClassDef(StmtClassDef { body, .. }) - | AnyNodeRef::StmtWith(StmtWith { body, .. }) - | AnyNodeRef::StmtAsyncWith(StmtAsyncWith { body, .. }) - | AnyNodeRef::MatchCase(MatchCase { body, .. }) - | AnyNodeRef::ExceptHandlerExceptHandler(ExceptHandlerExceptHandler { body, .. }) => body, + AnyNodeRef::StmtFunctionDef(ast::StmtFunctionDef { body, .. }) + | AnyNodeRef::StmtAsyncFunctionDef(ast::StmtAsyncFunctionDef { body, .. }) + | AnyNodeRef::StmtClassDef(ast::StmtClassDef { body, .. }) + | AnyNodeRef::StmtWith(ast::StmtWith { body, .. }) + | AnyNodeRef::StmtAsyncWith(ast::StmtAsyncWith { body, .. }) + | AnyNodeRef::MatchCase(ast::MatchCase { body, .. }) + | AnyNodeRef::ExceptHandlerExceptHandler(ast::ExceptHandlerExceptHandler { + body, .. + }) => body, - AnyNodeRef::StmtIf(StmtIf { body, orelse, .. }) - | AnyNodeRef::StmtFor(StmtFor { body, orelse, .. }) - | AnyNodeRef::StmtAsyncFor(StmtAsyncFor { body, orelse, .. }) - | AnyNodeRef::StmtWhile(StmtWhile { body, orelse, .. }) => { + AnyNodeRef::StmtIf(ast::StmtIf { body, orelse, .. }) + | AnyNodeRef::StmtFor(ast::StmtFor { body, orelse, .. }) + | AnyNodeRef::StmtAsyncFor(ast::StmtAsyncFor { body, orelse, .. }) + | AnyNodeRef::StmtWhile(ast::StmtWhile { body, orelse, .. }) => { if orelse.is_empty() { body } else { @@ -1126,18 +1130,18 @@ fn last_child_in_body(node: AnyNodeRef) -> Option { } } - AnyNodeRef::StmtMatch(StmtMatch { cases, .. }) => { + AnyNodeRef::StmtMatch(ast::StmtMatch { cases, .. }) => { return cases.last().map(AnyNodeRef::from) } - AnyNodeRef::StmtTry(StmtTry { + AnyNodeRef::StmtTry(ast::StmtTry { body, handlers, orelse, finalbody, .. }) - | AnyNodeRef::StmtTryStar(StmtTryStar { + | AnyNodeRef::StmtTryStar(ast::StmtTryStar { body, handlers, orelse, @@ -1171,23 +1175,21 @@ fn is_first_statement_in_enclosing_alternate_body( following: AnyNodeRef, enclosing: AnyNodeRef, ) -> bool { - use ruff_python_ast::prelude::*; - match enclosing { - AnyNodeRef::StmtIf(StmtIf { orelse, .. }) - | AnyNodeRef::StmtFor(StmtFor { orelse, .. }) - | AnyNodeRef::StmtAsyncFor(StmtAsyncFor { orelse, .. }) - | AnyNodeRef::StmtWhile(StmtWhile { orelse, .. }) => { + AnyNodeRef::StmtIf(ast::StmtIf { orelse, .. }) + | AnyNodeRef::StmtFor(ast::StmtFor { orelse, .. }) + | AnyNodeRef::StmtAsyncFor(ast::StmtAsyncFor { orelse, .. }) + | AnyNodeRef::StmtWhile(ast::StmtWhile { orelse, .. }) => { are_same_optional(following, orelse.first()) } - AnyNodeRef::StmtTry(StmtTry { + AnyNodeRef::StmtTry(ast::StmtTry { handlers, orelse, finalbody, .. }) - | AnyNodeRef::StmtTryStar(StmtTryStar { + | AnyNodeRef::StmtTryStar(ast::StmtTryStar { handlers, orelse, finalbody, diff --git a/crates/ruff_python_formatter/src/comments/visitor.rs b/crates/ruff_python_formatter/src/comments/visitor.rs index 828cf6e10e..143d381321 100644 --- a/crates/ruff_python_formatter/src/comments/visitor.rs +++ b/crates/ruff_python_formatter/src/comments/visitor.rs @@ -1,17 +1,23 @@ -use crate::comments::node_key::NodeRefEqualityKey; -use crate::comments::placement::place_comment; -use crate::comments::{CommentLinePosition, CommentsMap, SourceComment}; +use std::iter::Peekable; + +use ruff_text_size::{TextRange, TextSize}; +use rustpython_parser::ast::{ + Alias, Arg, ArgWithDefault, Arguments, Comprehension, Decorator, ExceptHandler, Expr, Keyword, + MatchCase, Mod, Pattern, Ranged, Stmt, WithItem, +}; + use ruff_formatter::{SourceCode, SourceCodeSlice}; use ruff_python_ast::node::AnyNodeRef; -use ruff_python_ast::prelude::*; use ruff_python_ast::source_code::{CommentRanges, Locator}; // The interface is designed to only export the members relevant for iterating nodes in // pre-order. #[allow(clippy::wildcard_imports)] use ruff_python_ast::visitor::preorder::*; use ruff_python_whitespace::is_python_whitespace; -use ruff_text_size::TextRange; -use std::iter::Peekable; + +use crate::comments::node_key::NodeRefEqualityKey; +use crate::comments::placement::place_comment; +use crate::comments::{CommentLinePosition, CommentsMap, SourceComment}; /// Visitor extracting the comments from an AST. #[derive(Debug, Clone)] diff --git a/crates/ruff_python_formatter/src/expression/binary_like.rs b/crates/ruff_python_formatter/src/expression/binary_like.rs index eff12b71d9..da124114ed 100644 --- a/crates/ruff_python_formatter/src/expression/binary_like.rs +++ b/crates/ruff_python_formatter/src/expression/binary_like.rs @@ -1,10 +1,12 @@ //! This module provides helper utilities to format an expression that has a left side, an operator, //! and a right side (binary like). +use rustpython_parser::ast::{self, Expr}; + +use ruff_formatter::{format_args, write}; + use crate::expression::parentheses::Parentheses; use crate::prelude::*; -use ruff_formatter::{format_args, write}; -use rustpython_parser::ast::Expr; /// Trait to implement a binary like syntax that has a left operand, an operator, and a right operand. pub(super) trait FormatBinaryLike<'ast> { @@ -133,25 +135,25 @@ pub(super) trait FormatBinaryLike<'ast> { } fn can_break_expr(expr: &Expr) -> bool { - use ruff_python_ast::prelude::*; - match expr { - Expr::Tuple(ExprTuple { + Expr::Tuple(ast::ExprTuple { elts: expressions, .. }) - | Expr::List(ExprList { + | Expr::List(ast::ExprList { elts: expressions, .. }) - | Expr::Set(ExprSet { + | Expr::Set(ast::ExprSet { elts: expressions, .. }) - | Expr::Dict(ExprDict { + | Expr::Dict(ast::ExprDict { values: expressions, .. }) => !expressions.is_empty(), - Expr::Call(ExprCall { args, keywords, .. }) => !(args.is_empty() && keywords.is_empty()), + Expr::Call(ast::ExprCall { args, keywords, .. }) => { + !(args.is_empty() && keywords.is_empty()) + } Expr::ListComp(_) | Expr::SetComp(_) | Expr::DictComp(_) | Expr::GeneratorExp(_) => true, - Expr::UnaryOp(ExprUnaryOp { operand, .. }) => match operand.as_ref() { + Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => match operand.as_ref() { Expr::BinOp(_) => true, _ => can_break_expr(operand.as_ref()), }, diff --git a/crates/ruff_python_formatter/src/expression/expr_compare.rs b/crates/ruff_python_formatter/src/expression/expr_compare.rs index d9031357a0..63bc5628a3 100644 --- a/crates/ruff_python_formatter/src/expression/expr_compare.rs +++ b/crates/ruff_python_formatter/src/expression/expr_compare.rs @@ -8,7 +8,7 @@ use crate::FormatNodeRule; use ruff_formatter::{ write, FormatError, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions, }; -use ruff_python_ast::prelude::Expr; +use rustpython_parser::ast::Expr; use rustpython_parser::ast::{CmpOp, ExprCompare}; #[derive(Default)] diff --git a/crates/ruff_python_formatter/src/expression/expr_dict.rs b/crates/ruff_python_formatter/src/expression/expr_dict.rs index c3263a1b0b..a0a4c57a31 100644 --- a/crates/ruff_python_formatter/src/expression/expr_dict.rs +++ b/crates/ruff_python_formatter/src/expression/expr_dict.rs @@ -5,8 +5,8 @@ use crate::expression::parentheses::{ use crate::prelude::*; use crate::FormatNodeRule; use ruff_formatter::{format_args, write}; -use ruff_python_ast::prelude::Ranged; use ruff_text_size::TextRange; +use rustpython_parser::ast::Ranged; use rustpython_parser::ast::{Expr, ExprDict}; #[derive(Default)] diff --git a/crates/ruff_python_formatter/src/expression/expr_slice.rs b/crates/ruff_python_formatter/src/expression/expr_slice.rs index 2edbcc3146..bc4fbba228 100644 --- a/crates/ruff_python_formatter/src/expression/expr_slice.rs +++ b/crates/ruff_python_formatter/src/expression/expr_slice.rs @@ -8,9 +8,9 @@ use crate::{AsFormat, FormatNodeRule, PyFormatter}; use ruff_formatter::prelude::{hard_line_break, line_suffix_boundary, space, text}; use ruff_formatter::{write, Buffer, Format, FormatError, FormatResult}; use ruff_python_ast::node::AstNode; -use ruff_python_ast::prelude::{Expr, Ranged}; use ruff_text_size::TextRange; use rustpython_parser::ast::ExprSlice; +use rustpython_parser::ast::{Expr, Ranged}; #[derive(Default)] pub struct FormatExprSlice; diff --git a/crates/ruff_python_formatter/src/expression/expr_tuple.rs b/crates/ruff_python_formatter/src/expression/expr_tuple.rs index 875954280e..b7783b549d 100644 --- a/crates/ruff_python_formatter/src/expression/expr_tuple.rs +++ b/crates/ruff_python_formatter/src/expression/expr_tuple.rs @@ -5,9 +5,9 @@ use crate::expression::parentheses::{ }; use crate::prelude::*; use ruff_formatter::{format_args, write, FormatRuleWithOptions}; -use ruff_python_ast::prelude::{Expr, Ranged}; use ruff_text_size::TextRange; use rustpython_parser::ast::ExprTuple; +use rustpython_parser::ast::{Expr, Ranged}; #[derive(Eq, PartialEq, Debug, Default)] pub enum TupleParentheses { diff --git a/crates/ruff_python_formatter/src/expression/expr_unary_op.rs b/crates/ruff_python_formatter/src/expression/expr_unary_op.rs index e55a2db81a..41a7feced2 100644 --- a/crates/ruff_python_formatter/src/expression/expr_unary_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_unary_op.rs @@ -6,8 +6,8 @@ use crate::trivia::{SimpleTokenizer, TokenKind}; use crate::{AsFormat, FormatNodeRule, PyFormatter}; use ruff_formatter::prelude::{hard_line_break, space, text}; use ruff_formatter::{Format, FormatContext, FormatResult}; -use ruff_python_ast::prelude::UnaryOp; use ruff_text_size::{TextLen, TextRange}; +use rustpython_parser::ast::UnaryOp; use rustpython_parser::ast::{ExprUnaryOp, Ranged}; #[derive(Default)] diff --git a/crates/ruff_python_formatter/src/statement/stmt_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_assign.rs index cbc25342a9..eaff2ef1a3 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_assign.rs @@ -4,7 +4,7 @@ use crate::{AsFormat, FormatNodeRule, PyFormatter}; use ruff_formatter::formatter::Formatter; use ruff_formatter::prelude::{space, text}; use ruff_formatter::{write, Buffer, Format, FormatResult}; -use ruff_python_ast::prelude::Expr; +use rustpython_parser::ast::Expr; use rustpython_parser::ast::StmtAssign; // Note: This currently does wrap but not the black way so the types below likely need to be diff --git a/crates/ruff_python_formatter/src/statement/stmt_with.rs b/crates/ruff_python_formatter/src/statement/stmt_with.rs index c01b8439f9..443337d934 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_with.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_with.rs @@ -1,10 +1,12 @@ +use rustpython_parser::ast::StmtWith; + +use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AstNode; + use crate::builders::optional_parentheses; use crate::comments::trailing_comments; use crate::prelude::*; use crate::{FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; -use ruff_python_ast::prelude::*; -use rustpython_parser::ast::StmtWith; #[derive(Default)] pub struct FormatStmtWith; diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 2e00df5938..d1ade6486a 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -24,6 +24,9 @@ ruff_python_ast = { path = "../crates/ruff_python_ast" } ruff_python_formatter = { path = "../crates/ruff_python_formatter" } similar = { version = "2.2.1" } +# Current tag: v0.0.6 +rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8078663b6c914c1cb86993e427764f7c422fc12c" , default-features = false, features = ["full-lexer", "num-bigint"] } + # Prevent this from interfering with workspaces [workspace] members = ["."] diff --git a/fuzz/fuzz_targets/ruff_parse_simple.rs b/fuzz/fuzz_targets/ruff_parse_simple.rs index d649746dcf..9eda53e05f 100644 --- a/fuzz/fuzz_targets/ruff_parse_simple.rs +++ b/fuzz/fuzz_targets/ruff_parse_simple.rs @@ -4,8 +4,9 @@ #![no_main] use libfuzzer_sys::{fuzz_target, Corpus}; -use ruff_python_ast::prelude::{lexer, Mode, Parse, ParseError, Suite}; use ruff_python_ast::source_code::{Generator, Locator, Stylist}; +use rustpython_parser::ast::Suite; +use rustpython_parser::{lexer, Mode, Parse, ParseError}; fn do_fuzz(case: &[u8]) -> Corpus { let Ok(code) = std::str::from_utf8(case) else { return Corpus::Reject; }; From 2fc38d81e6ba85ebb75805b0d7f068945f0d4406 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Mon, 26 Jun 2023 21:22:42 +0530 Subject: [PATCH 238/447] Experimental release for Jupyter notebook integration (#5363) ## Summary Experimental release for Jupyter Notebook integration. Currently, this requires a user to explicitly opt-in using the [include](https://beta.ruff.rs/docs/settings/#include) configuration: ```toml [tool.ruff] include = ["*.py", "*.pyi", "**/pyproject.toml", "*.ipynb"] ``` Or, a user can pass in the file directly: ```sh ruff check path/to/notebook.ipynb ``` For known limitations, please refer #5188 ## Test Plan Following command should work without the `--all-features` flag: ```sh cargo dev round-trip /path/to/notebook.ipynb ``` Following command should work with the above config file along with `select = ["ALL"]`: ```sh cargo run --bin ruff -- check --no-cache --config=../test-repos/openai-cookbook/pyproject.toml --fix ../test-repos/openai-cookbook/ ``` Passing the Jupyter notebook directly: ```sh cargo run --bin ruff -- check --no-cache --isolated --select=ALL --fix ../test-repos/openai-cookbook/examples/Classification_using_embeddings.ipynb ``` --- Cargo.lock | 1 + crates/ruff/Cargo.toml | 1 - crates/ruff/src/jupyter/notebook.rs | 21 --------------------- crates/ruff_cli/Cargo.toml | 3 --- crates/ruff_cli/src/diagnostics.rs | 4 ++-- crates/ruff_dev/Cargo.toml | 1 + crates/ruff_dev/src/round_trip.rs | 3 ++- crates/ruff_python_stdlib/src/path.rs | 16 +++++++++++++++- 8 files changed, 21 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5dd0ae65ee..f52c559219 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1969,6 +1969,7 @@ dependencies = [ "ruff_cli", "ruff_diagnostics", "ruff_python_formatter", + "ruff_python_stdlib", "ruff_textwrap", "rustpython-format", "rustpython-parser", diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 59643a1736..76b09f6474 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -88,4 +88,3 @@ colored = { workspace = true, features = ["no-color"] } [features] default = [] schemars = ["dep:schemars"] -jupyter_notebook = [] diff --git a/crates/ruff/src/jupyter/notebook.rs b/crates/ruff/src/jupyter/notebook.rs index 7e8a74bd0d..91aa62e24d 100644 --- a/crates/ruff/src/jupyter/notebook.rs +++ b/crates/ruff/src/jupyter/notebook.rs @@ -39,15 +39,6 @@ pub fn round_trip(path: &Path) -> anyhow::Result { Ok(String::from_utf8(writer)?) } -/// Return `true` if the [`Path`] appears to be that of a jupyter notebook file (`.ipynb`). -pub fn is_jupyter_notebook(path: &Path) -> bool { - path.extension() - .map_or(false, |ext| ext == JUPYTER_NOTEBOOK_EXT) - // For now this is feature gated here, the long term solution depends on - // https://github.com/astral-sh/ruff/issues/3410 - && cfg!(feature = "jupyter_notebook") -} - impl Cell { /// Return the [`SourceValue`] of the cell. fn source(&self) -> &SourceValue { @@ -452,8 +443,6 @@ mod tests { use test_case::test_case; use crate::jupyter::index::JupyterIndex; - #[cfg(feature = "jupyter_notebook")] - use crate::jupyter::is_jupyter_notebook; use crate::jupyter::schema::Cell; use crate::jupyter::Notebook; use crate::registry::Rule; @@ -512,16 +501,6 @@ mod tests { Ok(()) } - #[test] - #[cfg(feature = "jupyter_notebook")] - fn inclusions() { - let path = Path::new("foo/bar/baz"); - assert!(!is_jupyter_notebook(path)); - - let path = Path::new("foo/bar/baz.ipynb"); - assert!(is_jupyter_notebook(path)); - } - #[test] fn test_concat_notebook() -> Result<()> { let notebook = read_jupyter_notebook(Path::new("valid.ipynb"))?; diff --git a/crates/ruff_cli/Cargo.toml b/crates/ruff_cli/Cargo.toml index d662bb91ce..75fa5a9a11 100644 --- a/crates/ruff_cli/Cargo.toml +++ b/crates/ruff_cli/Cargo.toml @@ -67,9 +67,6 @@ wild = { version = "2" } assert_cmd = { version = "2.0.8" } ureq = { version = "2.6.2", features = [] } -[features] -jupyter_notebook = ["ruff/jupyter_notebook"] - [target.'cfg(target_os = "windows")'.dependencies] mimalloc = "0.1.34" diff --git a/crates/ruff_cli/src/diagnostics.rs b/crates/ruff_cli/src/diagnostics.rs index 03f5527a5f..fe638ad169 100644 --- a/crates/ruff_cli/src/diagnostics.rs +++ b/crates/ruff_cli/src/diagnostics.rs @@ -14,7 +14,7 @@ use rustc_hash::FxHashMap; use similar::TextDiff; use ruff::fs; -use ruff::jupyter::{is_jupyter_notebook, Notebook}; +use ruff::jupyter::Notebook; use ruff::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult}; use ruff::logging::DisplayParseError; use ruff::message::Message; @@ -23,7 +23,7 @@ use ruff::settings::{flags, AllSettings, Settings}; use ruff::source_kind::SourceKind; use ruff_python_ast::imports::ImportMap; use ruff_python_ast::source_code::{LineIndex, SourceCode, SourceFileBuilder}; -use ruff_python_stdlib::path::is_project_toml; +use ruff_python_stdlib::path::{is_jupyter_notebook, is_project_toml}; use crate::cache::Cache; diff --git a/crates/ruff_dev/Cargo.toml b/crates/ruff_dev/Cargo.toml index fa8ee5d7c5..cd84e8368e 100644 --- a/crates/ruff_dev/Cargo.toml +++ b/crates/ruff_dev/Cargo.toml @@ -15,6 +15,7 @@ ruff = { path = "../ruff", features = ["schemars"] } ruff_cli = { path = "../ruff_cli" } ruff_diagnostics = { path = "../ruff_diagnostics" } ruff_python_formatter = { path = "../ruff_python_formatter" } +ruff_python_stdlib = { path = "../ruff_python_stdlib" } ruff_textwrap = { path = "../ruff_textwrap" } anyhow = { workspace = true } diff --git a/crates/ruff_dev/src/round_trip.rs b/crates/ruff_dev/src/round_trip.rs index 75d1cf59e4..394ec4b190 100644 --- a/crates/ruff_dev/src/round_trip.rs +++ b/crates/ruff_dev/src/round_trip.rs @@ -8,6 +8,7 @@ use anyhow::Result; use ruff::jupyter; use ruff::round_trip; +use ruff_python_stdlib::path::is_jupyter_notebook; #[derive(clap::Args)] pub(crate) struct Args { @@ -18,7 +19,7 @@ pub(crate) struct Args { pub(crate) fn main(args: &Args) -> Result<()> { let path = args.file.as_path(); - if jupyter::is_jupyter_notebook(path) { + if is_jupyter_notebook(path) { println!("{}", jupyter::round_trip(path)?); } else { let contents = fs::read_to_string(&args.file)?; diff --git a/crates/ruff_python_stdlib/src/path.rs b/crates/ruff_python_stdlib/src/path.rs index 733c18bb3f..cad9219687 100644 --- a/crates/ruff_python_stdlib/src/path.rs +++ b/crates/ruff_python_stdlib/src/path.rs @@ -17,11 +17,16 @@ pub fn is_python_stub_file(path: &Path) -> bool { path.extension().map_or(false, |ext| ext == "pyi") } +/// Return `true` if the [`Path`] appears to be that of a Jupyter notebook (`.ipynb`). +pub fn is_jupyter_notebook(path: &Path) -> bool { + path.extension().map_or(false, |ext| ext == "ipynb") +} + #[cfg(test)] mod tests { use std::path::Path; - use crate::path::is_python_file; + use crate::path::{is_jupyter_notebook, is_python_file}; #[test] fn inclusions() { @@ -37,4 +42,13 @@ mod tests { let path = Path::new("foo/bar/baz"); assert!(!is_python_file(path)); } + + #[test] + fn test_is_jupyter_notebook() { + let path = Path::new("foo/bar/baz.ipynb"); + assert!(is_jupyter_notebook(path)); + + let path = Path::new("foo/bar/baz.py"); + assert!(!is_jupyter_notebook(path)); + } } From 8a1bb7a5afda27367a1a93b30be9a3b35798d2df Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 26 Jun 2023 11:56:12 -0400 Subject: [PATCH 239/447] Fix version number in playground (#5372) ## Summary `v0.0.275` in the top-right was showing `v0.0.0` at all times. ## Test Plan ![Screen Shot 2023-06-26 at 11 31 16 AM](https://github.com/astral-sh/ruff/assets/1309177/e6cd0e19-6a5f-4b46-a060-54f492524737) --- crates/ruff/src/lib.rs | 2 ++ crates/ruff_wasm/src/lib.rs | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs index f1d884303e..2a69194fad 100644 --- a/crates/ruff/src/lib.rs +++ b/crates/ruff/src/lib.rs @@ -9,6 +9,8 @@ pub use ruff_python_ast::source_code::round_trip; pub use rule_selector::RuleSelector; pub use rules::pycodestyle::rules::IOError; +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); + mod autofix; mod checkers; mod codes; diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index 2a449d0864..b113491b44 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -21,8 +21,6 @@ use ruff::settings::{defaults, flags, Settings}; use ruff_diagnostics::Edit; use ruff_python_ast::source_code::{Indexer, Locator, SourceLocation, Stylist}; -const VERSION: &str = env!("CARGO_PKG_VERSION"); - #[wasm_bindgen(typescript_custom_section)] const TYPES: &'static str = r#" export interface Diagnostic { @@ -87,7 +85,7 @@ pub fn run() { #[wasm_bindgen] #[allow(non_snake_case)] pub fn currentVersion() -> JsValue { - JsValue::from(VERSION) + JsValue::from(ruff::VERSION) } #[wasm_bindgen] From d53b986fd4a02aec8bd4c6eecc1d66cc1d804185 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 26 Jun 2023 12:40:28 -0400 Subject: [PATCH 240/447] Fix autofix capabilities in playground (#5375) ## Summary These had just bitrotted over time -- we were no longer passing along the row-and-column indices, etc. ## Test Plan ![Screen Shot 2023-06-26 at 12 03 41 PM](https://github.com/astral-sh/ruff/assets/1309177/6791330d-010b-45d3-91ef-531d4745193f) --- crates/ruff_wasm/src/lib.rs | 32 ++++++++++++++++++-------- playground/src/Editor/SourceEditor.tsx | 8 +++---- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index b113491b44..1fe7aa89d6 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -18,7 +18,6 @@ use ruff::rules::{ use ruff::settings::configuration::Configuration; use ruff::settings::options::Options; use ruff::settings::{defaults, flags, Settings}; -use ruff_diagnostics::Edit; use ruff_python_ast::source_code::{Indexer, Locator, SourceLocation, Stylist}; #[wasm_bindgen(typescript_custom_section)] @@ -37,7 +36,7 @@ export interface Diagnostic { fix: { message: string | null; edits: { - content: string; + content: string | null; location: { row: number; column: number; @@ -51,12 +50,6 @@ export interface Diagnostic { }; "#; -#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] -pub struct ExpandedFix { - message: Option, - edits: Vec, -} - #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] pub struct ExpandedMessage { pub code: String, @@ -66,6 +59,19 @@ pub struct ExpandedMessage { pub fix: Option, } +#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] +pub struct ExpandedFix { + message: Option, + edits: Vec, +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] +struct ExpandedEdit { + location: SourceLocation, + end_location: SourceLocation, + content: Option, +} + #[wasm_bindgen(start)] pub fn run() { use log::Level; @@ -220,7 +226,15 @@ pub fn check(contents: &str, options: JsValue) -> Result { end_location, fix: message.fix.map(|fix| ExpandedFix { message: message.kind.suggestion, - edits: fix.into_edits(), + edits: fix + .into_edits() + .into_iter() + .map(|edit| ExpandedEdit { + location: source_code.source_location(edit.start()), + end_location: source_code.source_location(edit.end()), + content: edit.content().map(ToString::to_string), + }) + .collect(), }), } }) diff --git a/playground/src/Editor/SourceEditor.tsx b/playground/src/Editor/SourceEditor.tsx index c039cbf302..492b0836cc 100644 --- a/playground/src/Editor/SourceEditor.tsx +++ b/playground/src/Editor/SourceEditor.tsx @@ -54,7 +54,7 @@ export default function SourceEditor({ provideCodeActions: function (model, position) { const actions = diagnostics .filter((check) => position.startLineNumber === check.location.row) - .filter((check) => check.fix) + .filter(({ fix }) => fix) .map((check) => ({ title: check.fix ? check.fix.message @@ -71,11 +71,11 @@ export default function SourceEditor({ edit: { range: { startLineNumber: edit.location.row, - startColumn: edit.location.column + 1, + startColumn: edit.location.column, endLineNumber: edit.end_location.row, - endColumn: edit.end_location.column + 1, + endColumn: edit.end_location.column, }, - text: edit.content, + text: edit.content || "", }, })), } From 190bed124f1a1753dc009749ffd7a7fba146718a Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Mon, 26 Jun 2023 12:34:37 -0500 Subject: [PATCH 241/447] [`perflint`] Implement `try-except-in-loop` (`PERF203`) (#5166) ## Summary Implements PERF203 from #4789, which throws if a `try/except` block is inside of a loop. Not sure if we want to extend the diagnostic to the `except` as well, but I thought that that may get a little messy. We may also want to just throw on the word `try` - open to suggestions though. ## Test Plan `cargo test` --- .../test/fixtures/perflint/PERF203.py | 28 +++++++ crates/ruff/src/checkers/ast/mod.rs | 6 ++ crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/perflint/mod.rs | 1 + crates/ruff/src/rules/perflint/rules/mod.rs | 2 + .../perflint/rules/try_except_in_loop.rs | 74 +++++++++++++++++++ ...__perflint__tests__PERF203_PERF203.py.snap | 28 +++++++ ruff.schema.json | 3 + scripts/pyproject.toml | 1 + 9 files changed, 144 insertions(+) create mode 100644 crates/ruff/resources/test/fixtures/perflint/PERF203.py create mode 100644 crates/ruff/src/rules/perflint/rules/try_except_in_loop.rs create mode 100644 crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF203_PERF203.py.snap diff --git a/crates/ruff/resources/test/fixtures/perflint/PERF203.py b/crates/ruff/resources/test/fixtures/perflint/PERF203.py new file mode 100644 index 0000000000..ec3ca5feee --- /dev/null +++ b/crates/ruff/resources/test/fixtures/perflint/PERF203.py @@ -0,0 +1,28 @@ +for i in range(10): + try: # PERF203 + print(f"{i}") + except: + print("error") + +try: + for i in range(10): + print(f"{i}") +except: + print("error") + +i = 0 +while i < 10: # PERF203 + try: + print(f"{i}") + except: + print("error") + + i += 1 + +try: + i = 0 + while i < 10: + print(f"{i}") + i += 1 +except: + print("error") diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 11f73fa09f..32eab592f3 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1430,6 +1430,9 @@ where if self.enabled(Rule::UselessElseOnLoop) { pylint::rules::useless_else_on_loop(self, stmt, body, orelse); } + if self.enabled(Rule::TryExceptInLoop) { + perflint::rules::try_except_in_loop(self, body); + } } Stmt::For(ast::StmtFor { target, @@ -1477,6 +1480,9 @@ where if self.enabled(Rule::InDictKeys) { flake8_simplify::rules::key_in_dict_for(self, target, iter); } + if self.enabled(Rule::TryExceptInLoop) { + perflint::rules::try_except_in_loop(self, body); + } } if self.enabled(Rule::IncorrectDictIterator) { perflint::rules::incorrect_dict_iterator(self, target, iter); diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 1ea96ca00b..b63fea0b74 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -786,6 +786,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { // perflint (Perflint, "101") => (RuleGroup::Unspecified, rules::perflint::rules::UnnecessaryListCast), (Perflint, "102") => (RuleGroup::Unspecified, rules::perflint::rules::IncorrectDictIterator), + (Perflint, "203") => (RuleGroup::Unspecified, rules::perflint::rules::TryExceptInLoop), // 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 b39aa84fc9..33c9691206 100644 --- a/crates/ruff/src/rules/perflint/mod.rs +++ b/crates/ruff/src/rules/perflint/mod.rs @@ -15,6 +15,7 @@ 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"))] 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/mod.rs b/crates/ruff/src/rules/perflint/rules/mod.rs index cc35c428b0..4af80c1432 100644 --- a/crates/ruff/src/rules/perflint/rules/mod.rs +++ b/crates/ruff/src/rules/perflint/rules/mod.rs @@ -1,5 +1,7 @@ pub(crate) use incorrect_dict_iterator::*; +pub(crate) use try_except_in_loop::*; pub(crate) use unnecessary_list_cast::*; mod incorrect_dict_iterator; +mod try_except_in_loop; mod unnecessary_list_cast; diff --git a/crates/ruff/src/rules/perflint/rules/try_except_in_loop.rs b/crates/ruff/src/rules/perflint/rules/try_except_in_loop.rs new file mode 100644 index 0000000000..746668f311 --- /dev/null +++ b/crates/ruff/src/rules/perflint/rules/try_except_in_loop.rs @@ -0,0 +1,74 @@ +use rustpython_parser::ast::{self, Ranged, Stmt}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; +use crate::settings::types::PythonVersion; + +/// ## What it does +/// Checks for uses of except handling via `try`-`except` within `for` and +/// `while` loops. +/// +/// ## Why is this bad? +/// Exception handling via `try`-`except` blocks incurs some performance +/// overhead, regardless of whether an exception is raised. +/// +/// When possible, refactor your code to put the entire loop into the +/// `try`-`except` block, rather than wrapping each iteration in a separate +/// `try`-`except` block. +/// +/// This rule is only enforced for Python versions prior to 3.11, which +/// introduced "zero cost" exception handling. +/// +/// 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 +/// for i in range(10): +/// try: +/// print(i * i) +/// except: +/// break +/// ``` +/// +/// Use instead: +/// ```python +/// try: +/// for i in range(10): +/// print(i * i) +/// except: +/// break +/// ``` +/// +/// ## Options +/// - `target-version` +#[violation] +pub struct TryExceptInLoop; + +impl Violation for TryExceptInLoop { + #[derive_message_formats] + fn message(&self) -> String { + format!("`try`-`except` within a loop incurs performance overhead") + } +} + +/// PERF203 +pub(crate) fn try_except_in_loop(checker: &mut Checker, body: &[Stmt]) { + if checker.settings.target_version >= PythonVersion::Py311 { + return; + } + + checker.diagnostics.extend(body.iter().filter_map(|stmt| { + if let Stmt::Try(ast::StmtTry { handlers, .. }) = stmt { + handlers + .iter() + .next() + .map(|handler| Diagnostic::new(TryExceptInLoop, handler.range())) + } else { + None + } + })); +} diff --git a/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF203_PERF203.py.snap b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF203_PERF203.py.snap new file mode 100644 index 0000000000..08a287819e --- /dev/null +++ b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF203_PERF203.py.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff/src/rules/perflint/mod.rs +--- +PERF203.py:4:5: PERF203 `try`-`except` within a loop incurs performance overhead + | +2 | try: # PERF203 +3 | print(f"{i}") +4 | except: + | _____^ +5 | | print("error") + | |______________________^ PERF203 +6 | +7 | try: + | + +PERF203.py:17:5: PERF203 `try`-`except` within a loop incurs performance overhead + | +15 | try: +16 | print(f"{i}") +17 | except: + | _____^ +18 | | print("error") + | |______________________^ PERF203 +19 | +20 | i += 1 + | + + diff --git a/ruff.schema.json b/ruff.schema.json index 52ba8fea34..b145c5ad36 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2065,6 +2065,9 @@ "PERF10", "PERF101", "PERF102", + "PERF2", + "PERF20", + "PERF203", "PGH", "PGH0", "PGH00", diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml index 387f364f26..f912d35e1d 100644 --- a/scripts/pyproject.toml +++ b/scripts/pyproject.toml @@ -18,6 +18,7 @@ ignore = [ "G", # flake8-logging "T", # flake8-print "FBT", # flake8-boolean-trap + "PERF", # perflint ] [tool.ruff.isort] From 50a7769d698c93cb23c7b2854bb47d716d87aa0d Mon Sep 17 00:00:00 2001 From: David Szotten Date: Mon, 26 Jun 2023 20:59:01 +0100 Subject: [PATCH 242/447] magic trailing comma for ExprList (#5365) --- .../src/expression/expr_list.rs | 18 +-- .../black_compatibility@collections.py.snap | 20 ++-- .../black_compatibility@comments2.py.snap | 23 ++-- .../black_compatibility@expression.py.snap | 103 ++++++++++-------- .../black_compatibility@fmtonoff3.py.snap | 35 ++++-- .../black_compatibility@fmtskip2.py.snap | 17 +-- .../black_compatibility@fmtskip4.py.snap | 49 --------- ...patibility@function_trailing_comma.py.snap | 37 ++++--- ...ompatibility@one_element_subscript.py.snap | 23 ++-- ...tibility@skip_magic_trailing_comma.py.snap | 19 +++- .../format@expression__compare.py.snap | 5 +- 11 files changed, 163 insertions(+), 186 deletions(-) delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip4.py.snap diff --git a/crates/ruff_python_formatter/src/expression/expr_list.rs b/crates/ruff_python_formatter/src/expression/expr_list.rs index 8e3badce51..16896dac16 100644 --- a/crates/ruff_python_formatter/src/expression/expr_list.rs +++ b/crates/ruff_python_formatter/src/expression/expr_list.rs @@ -52,23 +52,7 @@ impl FormatNodeRule for FormatExprList { "A non-empty expression list has dangling comments" ); - let items = format_with(|f| { - let mut iter = elts.iter(); - - if let Some(first) = iter.next() { - write!(f, [first.format()])?; - } - - for item in iter { - write!(f, [text(","), soft_line_break_or_space(), item.format()])?; - } - - if !elts.is_empty() { - write!(f, [if_group_breaks(&text(","))])?; - } - - Ok(()) - }); + let items = format_with(|f| f.join_comma_separated().nodes(elts.iter()).finish()); write!( f, diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@collections.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@collections.py.snap index 6e6ec5275c..a28e9d0841 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@collections.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@collections.py.snap @@ -145,16 +145,8 @@ if True: # looping over a 1-tuple should also not get wrapped for x in (1,): -@@ -63,14 +42,10 @@ - for (x,) in (1,), (2,), (3,): - pass - --[ -- 1, -- 2, -- 3, --] -+[1, 2, 3] +@@ -70,7 +49,7 @@ + ] division_result_tuple = (6 / 2,) -print("foo %r", (foo.bar,)) @@ -162,7 +154,7 @@ if True: if True: IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = ( -@@ -79,21 +54,6 @@ +@@ -79,21 +58,6 @@ ) if True: @@ -236,7 +228,11 @@ for x in (1,): for (x,) in (1,), (2,), (3,): pass -[1, 2, 3] +[ + 1, + 2, + 3, +] division_result_tuple = (6 / 2,) NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments2.py.snap index 2778d433ad..c975d624f6 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments2.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments2.py.snap @@ -307,7 +307,7 @@ instruction()#comment with bad spacing while True: if False: continue -@@ -141,24 +111,19 @@ +@@ -141,10 +111,7 @@ # and round and round we go # let's return @@ -318,15 +318,8 @@ instruction()#comment with bad spacing + return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) --CONFIG_FILES = ( -- [ -- CONFIG_FILE, -- ] -- + SHARED_CONFIG_FILES -- + USER_CONFIG_FILES --) # type: Final -+CONFIG_FILES = [CONFIG_FILE] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final - + CONFIG_FILES = ( +@@ -158,7 +125,11 @@ class Test: def _init_host(self, parsed) -> None: @@ -339,7 +332,7 @@ instruction()#comment with bad spacing pass -@@ -167,7 +132,7 @@ +@@ -167,7 +138,7 @@ ####################### @@ -469,7 +462,13 @@ def inline_comments_in_brackets_ruin_everything(): return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -CONFIG_FILES = [CONFIG_FILE] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final +CONFIG_FILES = ( + [ + CONFIG_FILE, + ] + + SHARED_CONFIG_FILES + + USER_CONFIG_FILES +) # type: Final class Test: diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap index 0b3a703534..c1c18a580f 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap @@ -274,7 +274,7 @@ last_call() Name None True -@@ -30,98 +31,83 @@ +@@ -30,33 +31,39 @@ -1 ~int and not v1 ^ 123 + v2 | True (~int) and (not ((v1 ^ (123 + v2)) | True)) @@ -337,32 +337,27 @@ last_call() () (1,) (1, 2) - (1, 2, 3) - [] - [1, 2, 3, 4, 5, 6, 7, 8, 9, (10 or A), (11 or B), (12 or C)] --[ -- 1, -- 2, -- 3, --] +@@ -68,60 +75,51 @@ + 2, + 3, + ] -[*a] -[*range(10)] --[ -- *a, -- 4, -- 5, --] -+[1, 2, 3] +[*NOT_YET_IMPLEMENTED_ExprStarred] +[*NOT_YET_IMPLEMENTED_ExprStarred] -+[*NOT_YET_IMPLEMENTED_ExprStarred, 4, 5] -+[4, *NOT_YET_IMPLEMENTED_ExprStarred, 5] [ -- 4, - *a, -- 5, --] --[ ++ *NOT_YET_IMPLEMENTED_ExprStarred, + 4, + 5, + ] + [ + 4, +- *a, ++ *NOT_YET_IMPLEMENTED_ExprStarred, + 5, + ] + [ this_is_a_very_long_variable_which_will_force_a_delimiter_split, element, another, @@ -432,7 +427,7 @@ last_call() (1).real (1.0).real ....__class__ -@@ -130,34 +116,28 @@ +@@ -130,34 +128,28 @@ tuple[str, ...] tuple[str, int, float, dict[str, int]] tuple[ @@ -480,7 +475,7 @@ last_call() numpy[0, :] numpy[:, i] numpy[0, :2] -@@ -171,25 +151,32 @@ +@@ -171,25 +163,32 @@ numpy[1 : c + 1, c] numpy[-(c + 1) :, d] numpy[:, l[-2]] @@ -523,7 +518,7 @@ last_call() "priority": 1, "import_session_id": 1, **kwargs, -@@ -198,35 +185,21 @@ +@@ -198,35 +197,21 @@ b = (1,) c = 1 d = (1,) + a + (2,) @@ -532,17 +527,6 @@ last_call() -g = 1, *"ten" -what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set( - vars_to_remove --) --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( -- models.Customer.account_id == account_id, models.Customer.email == email_address -- ) -- .order_by(models.Customer.id.asc()) -- .all() +e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +f = 1, *NOT_YET_IMPLEMENTED_ExprStarred +g = 1, *NOT_YET_IMPLEMENTED_ExprStarred @@ -550,6 +534,20 @@ last_call() + (coord_names + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) + + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ) +-what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( +- vars_to_remove ++what_is_up_with_those_new_coord_names = ( ++ (coord_names | NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) ++ - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + ) +-result = ( +- session.query(models.Customer.id) +- .filter( +- models.Customer.account_id == account_id, models.Customer.email == email_address +- ) +- .order_by(models.Customer.id.asc()) +- .all() +-) -result = ( - session.query(models.Customer.id) - .filter( @@ -559,10 +557,7 @@ last_call() - models.Customer.id.asc(), - ) - .all() -+what_is_up_with_those_new_coord_names = ( -+ (coord_names | NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) -+ - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - ) +-) -Ø = set() -authors.łukasz.say_thanks() +result = NOT_IMPLEMENTED_call() @@ -572,7 +567,7 @@ last_call() mapping = { A: 0.25 * (10.0 / 12), B: 0.1 * (10.0 / 12), -@@ -236,31 +209,29 @@ +@@ -236,31 +221,29 @@ def gen(): @@ -617,7 +612,7 @@ last_call() ... for j in 1 + (2 + 3): ... -@@ -272,7 +243,7 @@ +@@ -272,7 +255,7 @@ addr_proto, addr_canonname, addr_sockaddr, @@ -626,7 +621,7 @@ last_call() pass a = ( aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp -@@ -291,9 +262,9 @@ +@@ -291,9 +274,9 @@ is not qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz ) if ( @@ -639,7 +634,7 @@ last_call() ): return True if ( -@@ -327,13 +298,18 @@ +@@ -327,13 +310,18 @@ ): return True if ( @@ -661,7 +656,7 @@ last_call() ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n ): return True -@@ -341,7 +317,8 @@ +@@ -341,7 +329,8 @@ ~aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e @@ -671,7 +666,7 @@ last_call() ^ aaaaaaaaaaaaaaaa.i << aaaaaaaaaaaaaaaa.k >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n -@@ -366,5 +343,5 @@ +@@ -366,5 +355,5 @@ ^ bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ^ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ) @@ -755,11 +750,23 @@ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false (1, 2, 3) [] [1, 2, 3, 4, 5, 6, 7, 8, 9, (10 or A), (11 or B), (12 or C)] -[1, 2, 3] +[ + 1, + 2, + 3, +] [*NOT_YET_IMPLEMENTED_ExprStarred] [*NOT_YET_IMPLEMENTED_ExprStarred] -[*NOT_YET_IMPLEMENTED_ExprStarred, 4, 5] -[4, *NOT_YET_IMPLEMENTED_ExprStarred, 5] +[ + *NOT_YET_IMPLEMENTED_ExprStarred, + 4, + 5, +] +[ + 4, + *NOT_YET_IMPLEMENTED_ExprStarred, + 5, +] [ this_is_a_very_long_variable_which_will_force_a_delimiter_split, element, diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff3.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff3.py.snap index 9dacee57d5..42ebc85486 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff3.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff3.py.snap @@ -29,35 +29,50 @@ x = [ ```diff --- Black +++ Ruff -@@ -1,15 +1,9 @@ +@@ -1,14 +1,18 @@ # fmt: off --x = [ + x = [ - 1, 2, - 3, 4, --] -+x = [1, 2, 3, 4] ++ 1, ++ 2, ++ 3, ++ 4, + ] # fmt: on # fmt: off --x = [ + x = [ - 1, 2, - 3, 4, --] -+x = [1, 2, 3, 4] ++ 1, ++ 2, ++ 3, ++ 4, + ] # fmt: on - x = [1, 2, 3, 4] ``` ## Ruff Output ```py # fmt: off -x = [1, 2, 3, 4] +x = [ + 1, + 2, + 3, + 4, +] # fmt: on # fmt: off -x = [1, 2, 3, 4] +x = [ + 1, + 2, + 3, + 4, +] # fmt: on x = [1, 2, 3, 4] diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip2.py.snap index 487290c418..6b686d5412 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip2.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip2.py.snap @@ -15,22 +15,19 @@ l3 = ["I have", "trailing comma", "so I should be braked",] ```diff --- Black +++ Ruff -@@ -3,9 +3,9 @@ +@@ -3,7 +3,11 @@ "into multiple lines", "because it is way too long", ] -l2 = ["But this list shouldn't", "even though it also has", "way too many characters in it"] # fmt: skip --l3 = [ -- "I have", -- "trailing comma", -- "so I should be braked", --] +l2 = [ + "But this list shouldn't", + "even though it also has", + "way too many characters in it", +] # fmt: skip -+l3 = ["I have", "trailing comma", "so I should be braked"] + l3 = [ + "I have", + "trailing comma", ``` ## Ruff Output @@ -46,7 +43,11 @@ l2 = [ "even though it also has", "way too many characters in it", ] # fmt: skip -l3 = ["I have", "trailing comma", "so I should be braked"] +l3 = [ + "I have", + "trailing comma", + "so I should be braked", +] ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip4.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip4.py.snap deleted file mode 100644 index 93b2d701b1..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip4.py.snap +++ /dev/null @@ -1,49 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip4.py ---- -## Input - -```py -a = 2 -# fmt: skip -l = [1, 2, 3,] -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,7 +1,3 @@ - a = 2 - # fmt: skip --l = [ -- 1, -- 2, -- 3, --] -+l = [1, 2, 3] -``` - -## Ruff Output - -```py -a = 2 -# fmt: skip -l = [1, 2, 3] -``` - -## Black Output - -```py -a = 2 -# fmt: skip -l = [ - 1, - 2, - 3, -] -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function_trailing_comma.py.snap index e98815a64f..2a55c63e83 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function_trailing_comma.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function_trailing_comma.py.snap @@ -97,7 +97,7 @@ some_module.some_function( if ( a == { -@@ -47,23 +43,17 @@ +@@ -47,22 +43,24 @@ "f": 6, "g": 7, "h": 8, @@ -112,23 +112,24 @@ some_module.some_function( -def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> ( - Set["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"] -): -- json = { -- "k": { -- "k2": { -- "k3": [ -- 1, -- ] -- } -- } -- } +def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +]: -+ json = {"k": {"k2": {"k3": [1]}}} + json = { + "k": { + "k2": { + "k3": [ + 1, +- ] +- } +- } ++ ], ++ }, ++ }, + } - # The type annotation shouldn't get a trailing comma since that would change its type. -@@ -80,35 +70,16 @@ +@@ -80,35 +78,16 @@ pass @@ -228,7 +229,15 @@ def f( def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ]: - json = {"k": {"k2": {"k3": [1]}}} + json = { + "k": { + "k2": { + "k3": [ + 1, + ], + }, + }, + } # The type annotation shouldn't get a trailing comma since that would change its type. diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@one_element_subscript.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@one_element_subscript.py.snap index 68bddbd301..1db4d103ba 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@one_element_subscript.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@one_element_subscript.py.snap @@ -24,7 +24,7 @@ list_of_types = [tuple[int,],] ```diff --- Black +++ Ruff -@@ -1,22 +1,17 @@ +@@ -1,16 +1,15 @@ # We should not treat the trailing comma # in a single-element subscript. -a: tuple[int,] @@ -48,14 +48,13 @@ list_of_types = [tuple[int,],] ] # Magic commas still work as expected for non-subscripts. --small_list = [ -- 1, --] --list_of_types = [ +@@ -18,5 +17,5 @@ + 1, + ] + list_of_types = [ - tuple[int,], --] -+small_list = [1] -+list_of_types = [tuple[(int,)]] ++ tuple[(int,)], + ] ``` ## Ruff Output @@ -76,8 +75,12 @@ d = tuple[ ] # Magic commas still work as expected for non-subscripts. -small_list = [1] -list_of_types = [tuple[(int,)]] +small_list = [ + 1, +] +list_of_types = [ + tuple[(int,)], +] ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@skip_magic_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@skip_magic_trailing_comma.py.snap index 4fccb92505..ae72b77fdb 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@skip_magic_trailing_comma.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@skip_magic_trailing_comma.py.snap @@ -59,7 +59,7 @@ func( ```diff --- Black +++ Ruff -@@ -1,25 +1,35 @@ +@@ -1,25 +1,39 @@ # We should not remove the trailing comma in a single-element subscript. -a: tuple[int,] -b = tuple[int,] @@ -78,9 +78,14 @@ func( +] # Remove commas for non-subscripts. - small_list = [1] +-small_list = [1] -list_of_types = [tuple[int,]] -+list_of_types = [tuple[(int,)]] ++small_list = [ ++ 1, ++] ++list_of_types = [ ++ tuple[(int,)], ++] small_set = {1} -set_of_types = {tuple[int,]} +set_of_types = {tuple[(int,)]} @@ -124,8 +129,12 @@ d = tuple[ ] # Remove commas for non-subscripts. -small_list = [1] -list_of_types = [tuple[(int,)]] +small_list = [ + 1, +] +list_of_types = [ + tuple[(int,)], +] small_set = {1} set_of_types = {tuple[(int,)]} 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 3e5eab9002..6031fbadf8 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 @@ -102,7 +102,10 @@ a < b > c == d ( aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - < [bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, ff] + < [ + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ff, + ] < [ccccccccccccccccccccccccccccc, dddd] < ddddddddddddddddddddddddddddddddddddddddddd ) From 7f6cb9dfb56f5b61413ab9b7d3e64d06aa2e9955 Mon Sep 17 00:00:00 2001 From: konstin Date: Tue, 27 Jun 2023 11:29:40 +0200 Subject: [PATCH 243/447] Format call expressions (without call chaining) (#5341) ## Summary This formats call expressions with magic trailing comma and parentheses behaviour but without call chaining ## Test Plan Lots of new test fixtures, including some that don't work yet --- Cargo.lock | 12 +- Cargo.toml | 14 +- crates/ruff/src/test.rs | 1 + .../test/fixtures/ruff/statement/call.py | 83 +++++++ .../src/comments/placement.rs | 52 +++-- .../src/expression/expr_call.rs | 84 +++++-- .../src/other/keyword.rs | 9 +- ...ttribute_access_on_number_literals.py.snap | 56 ++--- ..._compatibility@beginning_backslash.py.snap | 39 ---- .../black_compatibility@collections.py.snap | 62 ++--- .../black_compatibility@comments.py.snap | 58 +---- .../black_compatibility@comments2.py.snap | 148 +++++------- .../black_compatibility@comments3.py.snap | 67 +++--- .../black_compatibility@comments4.py.snap | 141 ++++++------ .../black_compatibility@comments5.py.snap | 72 +----- .../black_compatibility@comments6.py.snap | 99 ++++---- .../black_compatibility@comments9.py.snap | 33 +-- ...bility@comments_non_breaking_space.py.snap | 10 +- .../black_compatibility@composition.py.snap | 146 ++++++------ ...lity@composition_no_trailing_comma.py.snap | 146 ++++++------ .../black_compatibility@empty_lines.py.snap | 17 +- .../black_compatibility@expression.py.snap | 216 +++++++----------- .../black_compatibility@fmtonoff.py.snap | 214 ++++++++--------- .../black_compatibility@fmtonoff2.py.snap | 20 +- .../black_compatibility@fmtonoff4.py.snap | 45 ++-- .../black_compatibility@fmtonoff5.py.snap | 107 +++++---- .../black_compatibility@fmtskip5.py.snap | 12 +- .../black_compatibility@fmtskip6.py.snap | 49 ---- .../black_compatibility@fmtskip8.py.snap | 80 +++---- .../black_compatibility@function.py.snap | 174 +++++++------- .../black_compatibility@function2.py.snap | 71 ++---- ...patibility@function_trailing_comma.py.snap | 88 +++---- ...ack_compatibility@power_op_spacing.py.snap | 120 +++++----- ...ility@prefer_rhs_split_reformatted.py.snap | 94 -------- ..._compatibility@remove_await_parens.py.snap | 108 +++++---- ..._compatibility@remove_for_brackets.py.snap | 55 ++--- ...move_newline_after_code_block_open.py.snap | 164 ++++--------- .../black_compatibility@remove_parens.py.snap | 18 +- ...tibility@skip_magic_trailing_comma.py.snap | 56 ++++- .../black_compatibility@slices.py.snap | 18 +- .../black_compatibility@torture.py.snap | 12 +- ...ty@trailing_comma_optional_parens1.py.snap | 35 ++- ...ty@trailing_comma_optional_parens2.py.snap | 8 +- ...ty@trailing_comma_optional_parens3.py.snap | 63 ----- ...y@trailing_commas_in_leading_parts.py.snap | 75 +++--- .../black_compatibility@tupleassign.py.snap | 9 +- .../format@expression__attribute.py.snap | 32 ++- .../format@expression__binary.py.snap | 12 +- ...rmat@expression__boolean_operation.py.snap | 6 +- .../format@expression__slice.py.snap | 18 +- .../snapshots/format@statement__call.py.snap | 175 ++++++++++++++ .../snapshots/format@statement__while.py.snap | 8 +- fuzz/Cargo.toml | 4 +- 53 files changed, 1662 insertions(+), 1853 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/call.py delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@beginning_backslash.py.snap delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip6.py.snap delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@prefer_rhs_split_reformatted.py.snap delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens3.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@statement__call.py.snap diff --git a/Cargo.lock b/Cargo.lock index f52c559219..287b62671a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2111,7 +2111,7 @@ dependencies = [ [[package]] name = "ruff_text_size" version = "0.0.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8078663b6c914c1cb86993e427764f7c422fc12c#8078663b6c914c1cb86993e427764f7c422fc12c" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0" dependencies = [ "schemars", "serde", @@ -2199,7 +2199,7 @@ dependencies = [ [[package]] name = "rustpython-ast" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8078663b6c914c1cb86993e427764f7c422fc12c#8078663b6c914c1cb86993e427764f7c422fc12c" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0" dependencies = [ "is-macro", "num-bigint", @@ -2210,7 +2210,7 @@ dependencies = [ [[package]] name = "rustpython-format" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8078663b6c914c1cb86993e427764f7c422fc12c#8078663b6c914c1cb86993e427764f7c422fc12c" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0" dependencies = [ "bitflags 2.3.2", "itertools", @@ -2222,7 +2222,7 @@ dependencies = [ [[package]] name = "rustpython-literal" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8078663b6c914c1cb86993e427764f7c422fc12c#8078663b6c914c1cb86993e427764f7c422fc12c" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0" dependencies = [ "hexf-parse", "is-macro", @@ -2234,7 +2234,7 @@ dependencies = [ [[package]] name = "rustpython-parser" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8078663b6c914c1cb86993e427764f7c422fc12c#8078663b6c914c1cb86993e427764f7c422fc12c" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0" dependencies = [ "anyhow", "is-macro", @@ -2257,7 +2257,7 @@ dependencies = [ [[package]] name = "rustpython-parser-core" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8078663b6c914c1cb86993e427764f7c422fc12c#8078663b6c914c1cb86993e427764f7c422fc12c" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0" dependencies = [ "is-macro", "memchr", diff --git a/Cargo.toml b/Cargo.toml index 047c7994ce..d48fedbf29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,14 +50,14 @@ toml = { version = "0.7.2" } # v0.0.1 libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "80e4c1399f95e5beb532fdd1e209ad2dbb470438" } -# Please tag the RustPython version everytime you update its revision here. +# Please tag the RustPython version everytime you update its revision here and in fuzz/Cargo.toml # Tagging the version ensures that older ruff versions continue to build from source even when we rebase our RustPython fork. -# Current tag: v0.0.6 -ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8078663b6c914c1cb86993e427764f7c422fc12c" } -rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8078663b6c914c1cb86993e427764f7c422fc12c" , default-features = false, features = ["num-bigint"]} -rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8078663b6c914c1cb86993e427764f7c422fc12c", default-features = false, features = ["num-bigint"] } -rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8078663b6c914c1cb86993e427764f7c422fc12c", default-features = false } -rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8078663b6c914c1cb86993e427764f7c422fc12c" , default-features = false, features = ["full-lexer", "num-bigint"] } +# Current tag: v0.0.7 +ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" } +rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" , default-features = false, features = ["num-bigint"]} +rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0", default-features = false, features = ["num-bigint"] } +rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0", default-features = false } +rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" , default-features = false, features = ["full-lexer", "num-bigint"] } [profile.release] lto = "fat" diff --git a/crates/ruff/src/test.rs b/crates/ruff/src/test.rs index d1682617d1..1c2eb7aefb 100644 --- a/crates/ruff/src/test.rs +++ b/crates/ruff/src/test.rs @@ -15,6 +15,7 @@ use ruff_python_ast::source_code::{Indexer, Locator, SourceFileBuilder, Stylist} use crate::autofix::{fix_file, FixResult}; use crate::directives; +#[cfg(not(fuzzing))] use crate::jupyter::Notebook; use crate::linter::{check_path, LinterResult}; use crate::message::{Emitter, EmitterContext, Message, TextEmitter}; diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/call.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/call.py new file mode 100644 index 0000000000..7a32b6cd28 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/call.py @@ -0,0 +1,83 @@ +from unittest.mock import MagicMock + + +def f(*args, **kwargs): + pass + +this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd = 1 +session = MagicMock() +models = MagicMock() + +f() + +f(1) + +f(x=2) + +f(1, x=2) + +f( + 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 +) + +f( + 1, + mixed_very_long_arguments=1, +) + +f( + this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd, + these_arguments_have_values_that_need_to_break_because_they_are_too_long1=(100000 - 100000000000), + these_arguments_have_values_that_need_to_break_because_they_are_too_long2="akshfdlakjsdfad" + "asdfasdfa", + these_arguments_have_values_that_need_to_break_because_they_are_too_long3=session, +) + +f( + # dangling comment +) + + +f( + only=1, short=1, arguments=1 +) + +f( + hey_this_is_a_long_call, it_has_funny_attributes_that_breaks_into_three_lines=1 +) + +f( + hey_this_is_a_very_long_call=1, it_has_funny_attributes_asdf_asdf=1, too_long_for_the_line=1, really=True +) + +# TODO(konstin): Call chains/fluent interface (https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#call-chains) +result = ( + session.query(models.Customer.id) + .filter( + models.Customer.account_id == 10000, + models.Customer.email == "user@example.org", + ) + .order_by(models.Customer.id.asc()) + .all() +) +# TODO(konstin): Black has this special case for comment placement where everything stays in one line +f( + "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa" +) + +f( + session, + b=1, + ** # oddly placed end-of-line comment + dict() +) +f( + session, + b=1, + ** + # oddly placed own line comment + dict() +) + diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index f46f9b841a..2f72f863d8 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -989,7 +989,7 @@ fn handle_dict_unpacking_comment<'a>( match comment.enclosing_node() { // TODO: can maybe also add AnyNodeRef::Arguments here, but tricky to test due to // https://github.com/astral-sh/ruff/issues/5176 - AnyNodeRef::ExprDict(_) => {} + AnyNodeRef::ExprDict(_) | AnyNodeRef::Keyword(_) => {} _ => { return CommentPlacement::Default(comment); } @@ -1015,12 +1015,22 @@ fn handle_dict_unpacking_comment<'a>( ) .skip_trivia(); + // if the remaining tokens from the previous node are exactly `**`, + // re-assign the comment to the one that follows the stars + let mut count = 0; + // we start from the preceding node but we skip its token for token in tokens.by_ref() { // Skip closing parentheses that are not part of the node range if token.kind == TokenKind::RParen { continue; } + // The Keyword case + if token.kind == TokenKind::Star { + count += 1; + break; + } + // The dict case debug_assert!( matches!( token, @@ -1034,9 +1044,6 @@ fn handle_dict_unpacking_comment<'a>( break; } - // if the remaining tokens from the previous node is exactly `**`, - // re-assign the comment to the one that follows the stars - let mut count = 0; for token in tokens { if token.kind != TokenKind::Star { return CommentPlacement::Default(comment); @@ -1050,19 +1057,19 @@ fn handle_dict_unpacking_comment<'a>( CommentPlacement::Default(comment) } -// Own line comments coming after the node are always dangling comments -// ```python -// ( -// a -// # trailing a comment -// . # dangling comment -// # or this -// b -// ) -// ``` +/// Own line comments coming after the node are always dangling comments +/// ```python +/// ( +/// a +/// # trailing a comment +/// . # dangling comment +/// # or this +/// b +/// ) +/// ``` fn handle_attribute_comment<'a>( comment: DecoratedComment<'a>, - locator: &Locator, + _locator: &Locator, ) -> CommentPlacement<'a> { let Some(attribute) = comment.enclosing_node().expr_attribute() else { return CommentPlacement::Default(comment); @@ -1073,14 +1080,13 @@ fn handle_attribute_comment<'a>( return CommentPlacement::Default(comment); } - let between_value_and_attr = TextRange::new(attribute.value.end(), attribute.attr.start()); - - let dot = SimpleTokenizer::new(locator.contents(), between_value_and_attr) - .skip_trivia() - .next() - .expect("Expected the `.` character after the value"); - - if TextRange::new(dot.end(), attribute.attr.start()).contains(comment.slice().start()) { + if TextRange::new(attribute.value.end(), attribute.attr.start()) + .contains(comment.slice().start()) + { + // ```text + // value . attr + // ^^^^^^^ the range of dangling comments + // ``` if comment.line_position().is_end_of_line() { // Attach to node with b // ```python diff --git a/crates/ruff_python_formatter/src/expression/expr_call.rs b/crates/ruff_python_formatter/src/expression/expr_call.rs index 2e48d6aa56..0c9f7f443d 100644 --- a/crates/ruff_python_formatter/src/expression/expr_call.rs +++ b/crates/ruff_python_formatter/src/expression/expr_call.rs @@ -1,8 +1,10 @@ -use crate::comments::Comments; +use crate::builders::PyFormatterExtensions; +use crate::comments::{dangling_comments, Comments}; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; -use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{format_with, group, soft_block_indent, text}; use ruff_formatter::{write, Buffer, FormatResult}; use rustpython_parser::ast::ExprCall; @@ -11,19 +13,75 @@ pub struct FormatExprCall; impl FormatNodeRule for FormatExprCall { fn fmt_fields(&self, item: &ExprCall, f: &mut PyFormatter) -> FormatResult<()> { - if item.args.is_empty() && item.keywords.is_empty() { - write!( + let ExprCall { + range: _, + func, + args, + keywords, + } = item; + + // We have a case with `f()` without any argument, which is a special case because we can + // have a comment with no node attachment inside: + // ```python + // f( + // # This function has a dangling comment + // ) + // ``` + if args.is_empty() && keywords.is_empty() { + let comments = f.context().comments().clone(); + let comments = comments.dangling_comments(item); + return write!( f, - [not_yet_implemented_custom_text("NOT_IMPLEMENTED_call()")] - ) - } else { - write!( - f, - [not_yet_implemented_custom_text( - "NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)" - )] - ) + [ + func.format(), + text("("), + dangling_comments(comments), + text(")") + ] + ); } + + let all_args = format_with(|f| { + f.join_comma_separated() + .entries( + // We have the parentheses from the call so the arguments never need any + args.iter() + .map(|arg| (arg, arg.format().with_options(Parenthesize::Never))), + ) + .nodes(keywords.iter()) + .finish() + }); + + write!( + f, + [ + func.format(), + text("("), + // The outer group is for things like + // ```python + // get_collection( + // hey_this_is_a_very_long_call, + // it_has_funny_attributes_asdf_asdf, + // too_long_for_the_line, + // really=True, + // ) + // ``` + // The inner group is for things like: + // ```python + // get_collection( + // hey_this_is_a_very_long_call, it_has_funny_attributes_asdf_asdf, really=True + // ) + // ``` + // TODO(konstin): Doesn't work see wrongly formatted test + &group(&soft_block_indent(&group(&all_args))), + text(")") + ] + ) + } + + fn fmt_dangling_comments(&self, _node: &ExprCall, _f: &mut PyFormatter) -> FormatResult<()> { + // Handled in `fmt_fields` + Ok(()) } } diff --git a/crates/ruff_python_formatter/src/other/keyword.rs b/crates/ruff_python_formatter/src/other/keyword.rs index d93a56d66a..47d3db19fc 100644 --- a/crates/ruff_python_formatter/src/other/keyword.rs +++ b/crates/ruff_python_formatter/src/other/keyword.rs @@ -13,10 +13,11 @@ impl FormatNodeRule for FormatKeyword { arg, value, } = item; - if let Some(argument) = arg { - write!(f, [argument.format(), text("=")])?; + if let Some(arg) = arg { + write!(f, [arg.format(), text("="), value.format()]) + } else { + // Comments after the stars are reassigned as trailing value comments + write!(f, [text("**"), value.format()]) } - - value.format().fmt(f) } } diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@attribute_access_on_number_literals.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@attribute_access_on_number_literals.py.snap index cfa4c7cd05..86e302320a 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@attribute_access_on_number_literals.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@attribute_access_on_number_literals.py.snap @@ -34,14 +34,18 @@ y = 100(no) ```diff --- Black +++ Ruff -@@ -1,22 +1,22 @@ --x = (123456789).bit_count() --x = (123456).__abs__() +@@ -1,19 +1,19 @@ + x = (123456789).bit_count() + x = (123456).__abs__() -x = (0.1).is_integer() -x = (1.0).imag -x = (1e1).imag -x = (1e-1).real --x = (123456789.123456789).hex() ++x = (.1).is_integer() ++x = (1.).imag ++x = (1E+1).imag ++x = (1E-1).real + x = (123456789.123456789).hex() -x = (123456789.123456789e123456789).real -x = (123456789e123456789).conjugate() -x = 123456789j.real @@ -49,58 +53,46 @@ y = 100(no) -x = 0xB1ACC.conjugate() -x = 0b1011.conjugate() -x = 0o777.real --x = (0.000000006).hex() --x = -100.0000j -+x = NOT_IMPLEMENTED_call() -+x = NOT_IMPLEMENTED_call() -+x = NOT_IMPLEMENTED_call() -+x = (1.).imag -+x = (1E+1).imag -+x = (1E-1).real -+x = NOT_IMPLEMENTED_call() +x = (123456789.123456789E123456789).real -+x = NOT_IMPLEMENTED_call() ++x = (123456789E123456789).conjugate() +x = 123456789J.real -+x = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+x = NOT_IMPLEMENTED_call() -+x = NOT_IMPLEMENTED_call() ++x = 123456789.123456789J.__add__((0b1011).bit_length()) ++x = (0XB1ACC).conjugate() ++x = (0B1011).conjugate() +x = (0O777).real -+x = NOT_IMPLEMENTED_call() + x = (0.000000006).hex() +-x = -100.0000j +x = -100.0000J if (10).real: ... - - y = 100[no] --y = 100(no) -+y = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Ruff Output ```py -x = NOT_IMPLEMENTED_call() -x = NOT_IMPLEMENTED_call() -x = NOT_IMPLEMENTED_call() +x = (123456789).bit_count() +x = (123456).__abs__() +x = (.1).is_integer() x = (1.).imag x = (1E+1).imag x = (1E-1).real -x = NOT_IMPLEMENTED_call() +x = (123456789.123456789).hex() x = (123456789.123456789E123456789).real -x = NOT_IMPLEMENTED_call() +x = (123456789E123456789).conjugate() x = 123456789J.real -x = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -x = NOT_IMPLEMENTED_call() -x = NOT_IMPLEMENTED_call() +x = 123456789.123456789J.__add__((0b1011).bit_length()) +x = (0XB1ACC).conjugate() +x = (0B1011).conjugate() x = (0O777).real -x = NOT_IMPLEMENTED_call() +x = (0.000000006).hex() x = -100.0000J if (10).real: ... y = 100[no] -y = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +y = 100(no) ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@beginning_backslash.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@beginning_backslash.py.snap deleted file mode 100644 index 1bd0c3f921..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@beginning_backslash.py.snap +++ /dev/null @@ -1,39 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/beginning_backslash.py ---- -## Input - -```py -\ - - - - - -print("hello, world") -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1 +1 @@ --print("hello, world") -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -``` - -## Ruff Output - -```py -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -``` - -## Black Output - -```py -print("hello, world") -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@collections.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@collections.py.snap index a28e9d0841..51c59c4e9d 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@collections.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@collections.py.snap @@ -123,12 +123,11 @@ if True: -} +c = {1, 2, 3} x = (1,) --y = (narf(),) + y = (narf(),) -nested = { - (1, 2, 3), - (4, 5, 6), -} -+y = (NOT_IMPLEMENTED_call(),) +nested = {(1, 2, 3), (4, 5, 6)} nested_no_trailing_comma = {(1, 2, 3), (4, 5, 6)} nested_long_lines = [ @@ -145,40 +144,6 @@ if True: # looping over a 1-tuple should also not get wrapped for x in (1,): -@@ -70,7 +49,7 @@ - ] - - division_result_tuple = (6 / 2,) --print("foo %r", (foo.bar,)) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - if True: - IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = ( -@@ -79,21 +58,6 @@ - ) - - if True: -- ec2client.get_waiter("instance_stopped").wait( -- InstanceIds=[instance.id], -- WaiterConfig={ -- "Delay": 5, -- }, -- ) -- ec2client.get_waiter("instance_stopped").wait( -- InstanceIds=[instance.id], -- WaiterConfig={ -- "Delay": 5, -- }, -- ) -- ec2client.get_waiter("instance_stopped").wait( -- InstanceIds=[instance.id], -- WaiterConfig={ -- "Delay": 5, -- }, -- ) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Ruff Output @@ -201,7 +166,7 @@ a = {1, 2, 3} b = {1, 2, 3} c = {1, 2, 3} x = (1,) -y = (NOT_IMPLEMENTED_call(),) +y = (narf(),) nested = {(1, 2, 3), (4, 5, 6)} nested_no_trailing_comma = {(1, 2, 3), (4, 5, 6)} nested_long_lines = [ @@ -235,7 +200,7 @@ for (x,) in (1,), (2,), (3,): ] division_result_tuple = (6 / 2,) -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +print("foo %r", (foo.bar,)) if True: IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = ( @@ -244,9 +209,24 @@ if True: ) if True: - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + ec2client.get_waiter("instance_stopped").wait( + InstanceIds=[instance.id], + WaiterConfig={ + "Delay": 5, + }, + ) + ec2client.get_waiter("instance_stopped").wait( + InstanceIds=[instance.id], + WaiterConfig={ + "Delay": 5, + }, + ) + ec2client.get_waiter("instance_stopped").wait( + InstanceIds=[instance.id], + WaiterConfig={ + "Delay": 5, + }, + ) ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap index 1317f43548..30b27344f0 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap @@ -130,42 +130,16 @@ async def wat(): # Some comment before a function. -@@ -35,20 +32,24 @@ +@@ -35,7 +32,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(): -+ if NOT_IMPLEMENTED_call(): + if inner_imports.are_evil(): # Explains why we have this if. - # In great detail indeed. -- x = X() -- return x.method1() # type: ignore -+ x = NOT_IMPLEMENTED_call() -+ return NOT_IMPLEMENTED_call() # 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)} -+GLOBAL_STATE = { -+ "a": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), -+ "b": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), -+ "c": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), -+} - - - # Another comment! -@@ -78,19 +79,18 @@ - #'

This is pweave!

- - --@fast(really=True) -+@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +@@ -82,8 +79,7 @@ async def wat(): # This comment, for some reason \ # contains a trailing backslash. @@ -174,12 +148,8 @@ async def wat(): + NOT_YET_IMPLEMENTED_StmtAsyncWith # Some more comments # Comment after ending a block. if result: -- print("A OK", file=sys.stdout) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - # Comment between things. -- print() -+ NOT_IMPLEMENTED_call() - + print("A OK", file=sys.stdout) +@@ -93,4 +89,4 @@ # Some closing comments. # Maybe Vim or Emacs directives for formatting. @@ -227,22 +197,18 @@ def function(default=None): # FIXME: Some comment about why this function is crap but still in production. NOT_YET_IMPLEMENTED_StmtImport - if NOT_IMPLEMENTED_call(): + if inner_imports.are_evil(): # Explains why we have this if. # In great detail indeed. - x = NOT_IMPLEMENTED_call() - return NOT_IMPLEMENTED_call() # type: ignore + 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": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), - "b": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), - "c": NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), -} +GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} # Another comment! @@ -272,16 +238,16 @@ class Foo: #'

This is pweave!

-@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +@fast(really=True) async def wat(): # This comment, for some reason \ # contains a trailing backslash. NOT_YET_IMPLEMENTED_StmtAsyncWith # Some more comments # Comment after ending a block. if result: - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("A OK", file=sys.stdout) # Comment between things. - NOT_IMPLEMENTED_call() + print() # Some closing comments. diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments2.py.snap index c975d624f6..0b4edc1d0f 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments2.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments2.py.snap @@ -189,36 +189,16 @@ instruction()#comment with bad spacing # Please keep __all__ alphabetized within each category. -@@ -37,31 +33,35 @@ - # builtin types and objects - type, - object, -- object(), -- Exception(), -+ NOT_IMPLEMENTED_call(), -+ NOT_IMPLEMENTED_call(), - 42, - 100.0, - "spam", +@@ -45,7 +41,7 @@ # user-defined types and objects Cheese, -- Cheese("Wensleydale"), + Cheese("Wensleydale"), - SubBytes(b"spam"), -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), ++ SubBytes(b"NOT_YET_IMPLEMENTED_BYTE_STRING"), ] if "PYTHON" in os.environ: -- add_compiler(compiler_from_env()) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - else: - # for compiler in compilers.values(): - # add_compiler(compiler) -- add_compiler(compilers[(7.0, 32)]) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - # add_compiler(compilers[(7.1, 64)]) - - +@@ -60,8 +56,12 @@ # Comment before function. def inline_comments_in_brackets_ruin_everything(): if typedargslist: @@ -232,7 +212,7 @@ instruction()#comment with bad spacing children[0], body, children[-1], # type: ignore -@@ -72,14 +72,18 @@ +@@ -72,7 +72,11 @@ body, parameters.children[-1], # )2 ] @@ -245,42 +225,10 @@ instruction()#comment with bad spacing if ( self._proc is not None # has the child process finished? - and self._returncode is None - # the child process has finished, but the - # transport hasn't been notified yet? -- and self._proc.poll() is None -+ and NOT_IMPLEMENTED_call() is None - ): - pass - # no newline before or after -@@ -91,48 +95,14 @@ - ] - - # no newline after -- call( -- arg1, -- arg2, -- """ --short --""", -- arg3=True, -- ) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - ############################################################################ - -- call2( -- # short -- arg1, -- # but -- arg2, -- # multiline -- """ --short --""", -- # yup -- arg3=True, -- ) +@@ -114,25 +118,9 @@ + # yup + arg3=True, + ) - lcomp = [ - element for element in collection if element is not None # yup # yup # right - ] @@ -300,26 +248,25 @@ instruction()#comment with bad spacing - # right - if element is not None - ] -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + lcomp = [i for i in []] + lcomp2 = [i for i in []] + lcomp3 = [i for i in []] while True: if False: continue -@@ -141,10 +111,7 @@ - # and round and round we go - +@@ -143,7 +131,10 @@ # let's return -- return Node( -- syms.simple_stmt, + return Node( + syms.simple_stmt, - [Node(statement, result), Leaf(token.NEWLINE, "\n")], # FIXME: \r\n? -- ) -+ return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ [ ++ Node(statement, result), ++ Leaf(token.NEWLINE, "\n"), # FIXME: \r\n? ++ ], + ) - CONFIG_FILES = ( -@@ -158,7 +125,11 @@ +@@ -158,7 +149,11 @@ class Test: def _init_host(self, parsed) -> None: @@ -327,20 +274,11 @@ instruction()#comment with bad spacing + if ( + parsed.hostname + is None # type: ignore -+ or not NOT_IMPLEMENTED_call() ++ or not parsed.hostname.strip() + ): pass -@@ -167,7 +138,7 @@ - ####################### - - --instruction() # comment with bad spacing -+NOT_IMPLEMENTED_call() # comment with bad spacing - - # END COMMENTS - # MORE END COMMENTS ``` ## Ruff Output @@ -381,23 +319,23 @@ not_shareables = [ # builtin types and objects type, object, - NOT_IMPLEMENTED_call(), - NOT_IMPLEMENTED_call(), + object(), + Exception(), 42, 100.0, "spam", # user-defined types and objects Cheese, - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), + Cheese("Wensleydale"), + SubBytes(b"NOT_YET_IMPLEMENTED_BYTE_STRING"), ] if "PYTHON" in os.environ: - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + add_compiler(compiler_from_env()) else: # for compiler in compilers.values(): # add_compiler(compiler) - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + add_compiler(compilers[(7.0, 32)]) # add_compiler(compilers[(7.1, 64)]) @@ -431,7 +369,7 @@ def inline_comments_in_brackets_ruin_everything(): and self._returncode is None # the child process has finished, but the # transport hasn't been notified yet? - and NOT_IMPLEMENTED_call() is None + and self._proc.poll() is None ): pass # no newline before or after @@ -443,11 +381,29 @@ def inline_comments_in_brackets_ruin_everything(): ] # no newline after - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + call( + arg1, + arg2, + """ +short +""", + arg3=True, + ) ############################################################################ - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + call2( + # short + arg1, + # but + arg2, + # multiline + """ +short +""", + # yup + arg3=True, + ) lcomp = [i for i in []] lcomp2 = [i for i in []] lcomp3 = [i for i in []] @@ -459,7 +415,13 @@ def inline_comments_in_brackets_ruin_everything(): # and round and round we go # let's return - return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + return Node( + syms.simple_stmt, + [ + Node(statement, result), + Leaf(token.NEWLINE, "\n"), # FIXME: \r\n? + ], + ) CONFIG_FILES = ( @@ -476,7 +438,7 @@ class Test: if ( parsed.hostname is None # type: ignore - or not NOT_IMPLEMENTED_call() + or not parsed.hostname.strip() ): pass @@ -486,7 +448,7 @@ class Test: ####################### -NOT_IMPLEMENTED_call() # comment with bad spacing +instruction() # comment with bad spacing # END COMMENTS # MORE END COMMENTS diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments3.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments3.py.snap index 5bd61dca47..a656b77f09 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments3.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments3.py.snap @@ -60,7 +60,7 @@ def func(): ```diff --- Black +++ Ruff -@@ -6,43 +6,16 @@ +@@ -6,14 +6,7 @@ x = """ a really long string """ @@ -74,40 +74,17 @@ def func(): - ] + lcomp3 = [i for i in []] # Capture each of the exceptions in the MultiError along with each of their causes and contexts -- if isinstance(exc_value, MultiError): -+ if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): + if isinstance(exc_value, MultiError): embedded = [] - for exc in exc_value.exceptions: - if exc not in _seen: -- embedded.append( -- # This should be left alone (before) -- traceback.TracebackException.from_exception( -- exc, -- limit=limit, -- lookup_lines=lookup_lines, -- capture_locals=capture_locals, -- # copy the set of _seen exceptions so that duplicates -- # shared between sub-exceptions are not omitted -- _seen=set(_seen), +@@ -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) -- ) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ ), + # This should be left alone (after) + ) - # everything is fine if the expression isn't nested -- traceback.TracebackException.from_exception( -- exc, -- limit=limit, -- lookup_lines=lookup_lines, -- capture_locals=capture_locals, -- # copy the set of _seen exceptions so that duplicates -- # shared between sub-exceptions are not omitted -- _seen=set(_seen), -- ) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - - # %% ``` ## Ruff Output @@ -123,14 +100,34 @@ def func(): """ lcomp3 = [i for i in []] # Capture each of the exceptions in the MultiError along with each of their causes and contexts - if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): + if isinstance(exc_value, MultiError): embedded = [] for exc in exc_value.exceptions: if exc not in _seen: - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + embedded.append( + # This should be left alone (before) + traceback.TracebackException.from_exception( + exc, + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + # 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) + ) # everything is fine if the expression isn't nested - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + traceback.TracebackException.from_exception( + exc, + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + # copy the set of _seen exceptions so that duplicates + # shared between sub-exceptions are not omitted + _seen=set(_seen), + ) # %% diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments4.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments4.py.snap index 7b3c1751cc..2358992e6c 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments4.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments4.py.snap @@ -106,7 +106,7 @@ def foo3(list_a, list_b): ```diff --- Black +++ Ruff -@@ -1,94 +1,28 @@ +@@ -1,9 +1,5 @@ -from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent, # NOT DRY -) @@ -118,72 +118,27 @@ def foo3(list_a, list_b): class C: -- @pytest.mark.parametrize( -- ("post_data", "message"), -- [ -- # metadata_version errors. -- ( -- {}, -- "None is an invalid value for Metadata-Version. Error: This field is" -- " required. see" -- " https://packaging.python.org/specifications/core-metadata", -- ), -- ( -- {"metadata_version": "-1"}, -- "'-1' is an invalid value for Metadata-Version. Error: Unknown Metadata" -- " Version see" -- " https://packaging.python.org/specifications/core-metadata", -- ), -- # name errors. -- ( -- {"metadata_version": "1.2"}, -- "'' is an invalid value for Name. Error: This field is required. see" -- " https://packaging.python.org/specifications/core-metadata", -- ), -- ( -- {"metadata_version": "1.2", "name": "foo-"}, -- "'foo-' is an invalid value for Name. Error: Must start and end with a" -- " letter or numeral and contain only ascii numeric and '.', '_' and" -- " '-'. see https://packaging.python.org/specifications/core-metadata", -- ), -- # version errors. -- ( -- {"metadata_version": "1.2", "name": "example"}, -- "'' is an invalid value for Version. Error: This field is required. see" -- " https://packaging.python.org/specifications/core-metadata", -- ), -- ( -- {"metadata_version": "1.2", "name": "example", "version": "dog"}, -- "'dog' is an invalid value for Version. Error: Must start and end with" -- " a letter or numeral and contain only ascii numeric and '.', '_' and" -- " '-'. see https://packaging.python.org/specifications/core-metadata", -- ), -- ], -- ) -+ @NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - def test_fails_invalid_post_data( - self, pyramid_config, db_request, post_data, message - ): -- pyramid_config.testing_securitypolicy(userid=1) -- db_request.POST = MultiDict(post_data) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ db_request.POST = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - +@@ -58,37 +54,28 @@ def foo(list_a, list_b): -- results = ( + results = ( - User.query.filter(User.foo == "bar") - .filter( # Because foo. -- db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) ++ User.query.filter(User.foo == "bar").filter( # Because foo. + db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) -- # Another comment about the filtering on is_quux goes here. ++ ).filter(User.xyz.is_(None)). + # Another comment about the filtering on is_quux goes here. - .filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))) - .order_by(User.created_at.desc()) - .with_for_update(key_share=True) - .all() - ) -+ results = NOT_IMPLEMENTED_call() ++ filter ++ )(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))).order_by( ++ User.created_at.desc() ++ ).with_for_update(key_share=True).all() return results @@ -196,7 +151,9 @@ def foo3(list_a, list_b): - ) - .filter(User.xyz.is_(None)) - ) -+ return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ return User.query.filter(User.foo == "bar").filter( ++ db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) ++ ).filter(User.xyz.is_(None)) def foo3(list_a, list_b): @@ -204,10 +161,11 @@ def foo3(list_a, list_b): # Standalone comment but weirdly placed. - User.query.filter(User.foo == "bar") - .filter( -- db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) ++ User.query.filter(User.foo == "bar").filter( + db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ ).filter(User.xyz.is_(None)) ) ``` @@ -219,28 +177,81 @@ NOT_YET_IMPLEMENTED_StmtImportFrom class C: - @NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + @pytest.mark.parametrize( + ("post_data", "message"), + [ + # metadata_version errors. + ( + {}, + "None is an invalid value for Metadata-Version. Error: This field is" + " required. see" + " https://packaging.python.org/specifications/core-metadata", + ), + ( + {"metadata_version": "-1"}, + "'-1' is an invalid value for Metadata-Version. Error: Unknown Metadata" + " Version see" + " https://packaging.python.org/specifications/core-metadata", + ), + # name errors. + ( + {"metadata_version": "1.2"}, + "'' is an invalid value for Name. Error: This field is required. see" + " https://packaging.python.org/specifications/core-metadata", + ), + ( + {"metadata_version": "1.2", "name": "foo-"}, + "'foo-' is an invalid value for Name. Error: Must start and end with a" + " letter or numeral and contain only ascii numeric and '.', '_' and" + " '-'. see https://packaging.python.org/specifications/core-metadata", + ), + # version errors. + ( + {"metadata_version": "1.2", "name": "example"}, + "'' is an invalid value for Version. Error: This field is required. see" + " https://packaging.python.org/specifications/core-metadata", + ), + ( + {"metadata_version": "1.2", "name": "example", "version": "dog"}, + "'dog' is an invalid value for Version. Error: Must start and end with" + " a letter or numeral and contain only ascii numeric and '.', '_' and" + " '-'. see https://packaging.python.org/specifications/core-metadata", + ), + ], + ) def test_fails_invalid_post_data( self, pyramid_config, db_request, post_data, message ): - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - db_request.POST = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + pyramid_config.testing_securitypolicy(userid=1) + db_request.POST = MultiDict(post_data) def foo(list_a, list_b): - results = NOT_IMPLEMENTED_call() + results = ( + User.query.filter(User.foo == "bar").filter( # Because foo. + db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) + ).filter(User.xyz.is_(None)). + # Another comment about the filtering on is_quux goes here. + filter + )(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))).order_by( + User.created_at.desc() + ).with_for_update(key_share=True).all() return results def foo2(list_a, list_b): # Standalone comment reasonably placed. - return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + return User.query.filter(User.foo == "bar").filter( + db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) + ).filter(User.xyz.is_(None)) def foo3(list_a, list_b): return ( # Standalone comment but weirdly placed. - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + User.query.filter(User.foo == "bar").filter( + db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) + ).filter(User.xyz.is_(None)) ) ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments5.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments5.py.snap index 68ef84e041..2a8d41d0ae 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments5.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments5.py.snap @@ -85,38 +85,14 @@ if __name__ == "__main__": ```diff --- Black +++ Ruff -@@ -1,6 +1,6 @@ - while True: - if something.changed: -- do.stuff() # trailing comment -+ NOT_IMPLEMENTED_call() # trailing comment - # Comment belongs to the `if` block. - # This one belongs to the `while` block. +@@ -20,14 +20,9 @@ + with open(some_temp_file) as f: + data = f.read() -@@ -8,26 +8,21 @@ - - # This one is properly standalone now. - --for i in range(100): -+for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): - # first we do this - if i % 33 == 0: - break - - # then we do this -- print(i) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - # 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) -+with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) as f: -+ data = NOT_IMPLEMENTED_call() - +- -except OSError: - print("problems") +NOT_YET_IMPLEMENTED_StmtTry @@ -126,30 +102,6 @@ if __name__ == "__main__": # leading function comment -@@ -42,7 +37,7 @@ - # leading 1 - @deco1 - # leading 2 --@deco2(with_args=True) -+@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - # leading 3 - @deco3 - def decorated1(): -@@ -52,7 +47,7 @@ - # leading 1 - @deco1 - # leading 2 --@deco2(with_args=True) -+@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - # leading function comment - def decorated1(): - ... -@@ -70,4 +65,4 @@ - - - if __name__ == "__main__": -- main() -+ NOT_IMPLEMENTED_call() ``` ## Ruff Output @@ -157,7 +109,7 @@ if __name__ == "__main__": ```py while True: if something.changed: - NOT_IMPLEMENTED_call() # trailing comment + do.stuff() # trailing comment # Comment belongs to the `if` block. # This one belongs to the `while` block. @@ -165,17 +117,17 @@ while True: # This one is properly standalone now. -for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): +for i in range(100): # first we do this if i % 33 == 0: break # then we do this - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print(i) # and finally we loop around -with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) as f: - data = NOT_IMPLEMENTED_call() +with open(some_temp_file) as f: + data = f.read() NOT_YET_IMPLEMENTED_StmtTry @@ -194,7 +146,7 @@ def wat(): # leading 1 @deco1 # leading 2 -@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +@deco2(with_args=True) # leading 3 @deco3 def decorated1(): @@ -204,7 +156,7 @@ def decorated1(): # leading 1 @deco1 # leading 2 -@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +@deco2(with_args=True) # leading function comment def decorated1(): ... @@ -222,7 +174,7 @@ def g(): if __name__ == "__main__": - NOT_IMPLEMENTED_call() + main() ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments6.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments6.py.snap index 120d15c6fe..8c11b63f62 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments6.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments6.py.snap @@ -136,44 +136,30 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite def f( -@@ -49,10 +49,11 @@ +@@ -49,9 +49,7 @@ element = 0 # type: int another_element = 1 # type: float another_element_with_long_name = 2 # type: int - another_really_really_long_element_with_a_unnecessarily_long_name_to_describe_what_it_does_enterprise_style = ( - 3 - ) # type: int -- an_element_with_a_long_value = calls() or more_calls() and more() # type: bool + another_really_really_long_element_with_a_unnecessarily_long_name_to_describe_what_it_does_enterprise_style = 3 # type: int -+ an_element_with_a_long_value = ( -+ NOT_IMPLEMENTED_call() -+ or NOT_IMPLEMENTED_call() and NOT_IMPLEMENTED_call() -+ ) # type: bool + an_element_with_a_long_value = calls() or more_calls() and more() # type: bool tup = ( - another_element, -@@ -86,33 +87,20 @@ - def func( - a=some_list[0], # type: int - ): # type: () -> int -- c = call( -- 0.0123, -- 0.0456, -- 0.0789, -- 0.0123, -- 0.0456, -- 0.0789, -- 0.0123, -- 0.0456, -- 0.0789, -- a[-1], # type: ignore -- ) -+ c = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +@@ -100,19 +98,35 @@ + ) -- c = call( + c = call( - "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa" # type: ignore -- ) -+ c = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ "aaaaaaaa", ++ "aaaaaaaa", ++ "aaaaaaaa", ++ "aaaaaaaa", ++ "aaaaaaaa", ++ "aaaaaaaa", ++ "aaaaaaaa", # type: ignore + ) -result = ( # aaa @@ -189,14 +175,21 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite + + AAAAAAAAAAAAA +) # type: ignore --call_to_some_function_asdf( -- foo, + call_to_some_function_asdf( + foo, - [AAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAA, BBBBBBBBBBBB], # type: ignore --) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ [ ++ AAAAAAAAAAAAAAAAAAAAAAA, ++ AAAAAAAAAAAAAAAAAAAAAAA, ++ AAAAAAAAAAAAAAAAAAAAAAA, ++ BBBBBBBBBBBB, ++ ], # type: ignore + ) -aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type] -+aaaaaaaaaaaaa, bbbbbbbbb = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # type: ignore[arg-type] ++aaaaaaaaaaaaa, bbbbbbbbb = map( ++ list, map(itertools.chain.from_iterable, zip(*NOT_YET_IMPLEMENTED_ExprStarred)) ++) # type: ignore[arg-type] ``` ## Ruff Output @@ -254,10 +247,7 @@ def f( another_element = 1 # type: float another_element_with_long_name = 2 # type: int another_really_really_long_element_with_a_unnecessarily_long_name_to_describe_what_it_does_enterprise_style = 3 # type: int - an_element_with_a_long_value = ( - NOT_IMPLEMENTED_call() - or NOT_IMPLEMENTED_call() and NOT_IMPLEMENTED_call() - ) # type: bool + an_element_with_a_long_value = calls() or more_calls() and more() # type: bool tup = ( another_element, @@ -291,9 +281,28 @@ def f( def func( a=some_list[0], # type: int ): # type: () -> int - c = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + c = call( + 0.0123, + 0.0456, + 0.0789, + 0.0123, + 0.0456, + 0.0789, + 0.0123, + 0.0456, + 0.0789, + a[-1], # type: ignore + ) - c = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + c = call( + "aaaaaaaa", + "aaaaaaaa", + "aaaaaaaa", + "aaaaaaaa", + "aaaaaaaa", + "aaaaaaaa", + "aaaaaaaa", # type: ignore + ) result = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # aaa @@ -305,9 +314,19 @@ AAAAAAAAAAAAA = ( + AAAAAAAAAAAAA ) # type: ignore -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +call_to_some_function_asdf( + foo, + [ + AAAAAAAAAAAAAAAAAAAAAAA, + AAAAAAAAAAAAAAAAAAAAAAA, + AAAAAAAAAAAAAAAAAAAAAAA, + BBBBBBBBBBBB, + ], # type: ignore +) -aaaaaaaaaaaaa, bbbbbbbbb = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # type: ignore[arg-type] +aaaaaaaaaaaaa, bbbbbbbbb = map( + list, map(itertools.chain.from_iterable, zip(*NOT_YET_IMPLEMENTED_ExprStarred)) +) # type: ignore[arg-type] ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments9.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments9.py.snap index b6b4ce39f0..2399d84c1b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments9.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments9.py.snap @@ -151,33 +151,6 @@ def bar(): ```diff --- Black +++ Ruff -@@ -59,7 +59,7 @@ - @deco1 - # leading 2 - # leading 2 extra --@deco2(with_args=True) -+@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - # leading 3 - @deco3 - # leading 4 -@@ -73,7 +73,7 @@ - # leading 1 - @deco1 - # leading 2 --@deco2(with_args=True) -+@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - # leading 3 that already has an empty line - @deco3 -@@ -88,7 +88,7 @@ - # leading 1 - @deco1 - # leading 2 --@deco2(with_args=True) -+@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - # leading 3 - @deco3 - @@ -106,7 +106,6 @@ # Another leading comment def another_inline(): @@ -260,7 +233,7 @@ some = statement @deco1 # leading 2 # leading 2 extra -@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +@deco2(with_args=True) # leading 3 @deco3 # leading 4 @@ -274,7 +247,7 @@ some = statement # leading 1 @deco1 # leading 2 -@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +@deco2(with_args=True) # leading 3 that already has an empty line @deco3 @@ -289,7 +262,7 @@ some = statement # leading 1 @deco1 # leading 2 -@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +@deco2(with_args=True) # leading 3 @deco3 diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments_non_breaking_space.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments_non_breaking_space.py.snap index cae2d7d069..12fd5c1269 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments_non_breaking_space.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments_non_breaking_space.py.snap @@ -31,7 +31,7 @@ def function(a:int=42): ```diff --- Black +++ Ruff -@@ -1,22 +1,17 @@ +@@ -1,9 +1,4 @@ -from .config import ( - ConfigTypeAttributes, - Int, @@ -42,11 +42,7 @@ def function(a:int=42): result = 1 # A simple comment result = (1,) # Another one - - result = 1 #  type: ignore - result = 1 # This comment is talking about type: ignore --square = Square(4) #  type: Optional[Square] -+square = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) #  type: Optional[Square] +@@ -14,9 +9,9 @@ def function(a: int = 42): @@ -71,7 +67,7 @@ result = (1,) # Another one result = 1 #  type: ignore result = 1 # This comment is talking about type: ignore -square = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) #  type: Optional[Square] +square = Square(4) #  type: Optional[Square] def function(a: int = 42): diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition.py.snap index 127846ac18..deda69dc2e 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition.py.snap @@ -193,69 +193,41 @@ class C: ```diff --- Black +++ Ruff -@@ -1,23 +1,10 @@ - class C: - def test(self) -> None: -- with patch("black.out", print): -- self.assertEqual( -- unstyle(str(report)), "1 file reformatted, 1 file failed to reformat." -- ) -- self.assertEqual( -- unstyle(str(report)), -- "1 file reformatted, 1 file left unchanged, 1 file failed to reformat.", -- ) -- self.assertEqual( -- unstyle(str(report)), -- "2 files reformatted, 1 file left unchanged, 1 file failed to" -- " reformat.", -- ) -- self.assertEqual( -- unstyle(str(report)), -- "2 files reformatted, 2 files left unchanged, 2 files failed to" -- " reformat.", -- ) -+ with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - for i in (a,): - if ( - # Rule 1 -@@ -27,133 +14,46 @@ - ): +@@ -28,8 +28,8 @@ while ( # Just a comment -- call() -- # Another -+ NOT_IMPLEMENTED_call() - ): -- print(i) -- xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( -- push_manager=context.request.resource_manager, -- max_items_to_push=num_items, -- batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE, -- ).push( -- # Only send the first n items. + call() ++ ): + # Another +- ): + 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] -- ) -+ # Another -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ xxxxxxxxxxxxxxxx = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ items=items[:num_items], + ) return ( 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' - % (test.name, test.filename, lineno, lname, err) - ) - +@@ -47,113 +47,46 @@ def omitting_trailers(self) -> None: -- get_collection( -- hey_this_is_a_very_long_call, it_has_funny_attributes, really=True + get_collection( + hey_this_is_a_very_long_call, it_has_funny_attributes, really=True - )[OneLevelIndex] -- get_collection( -- hey_this_is_a_very_long_call, it_has_funny_attributes, really=True ++ )[ ++ OneLevelIndex ++ ] + get_collection( + hey_this_is_a_very_long_call, it_has_funny_attributes, really=True - )[OneLevelIndex][TwoLevelIndex][ThreeLevelIndex][FourLevelIndex] -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)[OneLevelIndex] -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)[OneLevelIndex][TwoLevelIndex][ ++ )[ ++ OneLevelIndex ++ ][ ++ TwoLevelIndex ++ ][ + ThreeLevelIndex + ][ + FourLevelIndex @@ -263,10 +235,11 @@ class C: d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ 22 ] -- assignment = ( + assignment = ( - some.rather.elaborate.rule() and another.rule.ending_with.index[123] -- ) -+ assignment = NOT_IMPLEMENTED_call() and another.rule.ending_with.index[123] ++ some.rather.elaborate.rule() ++ and another.rule.ending_with.index[123] + ) def easy_asserts(self) -> None: - assert { @@ -376,7 +349,7 @@ class C: %3d 0 LOAD_FAST 1 (x) 2 LOAD_CONST 1 (1) 4 COMPARE_OP 2 (==) -@@ -161,21 +61,8 @@ +@@ -161,21 +94,8 @@ 8 STORE_ATTR 0 (x) 10 LOAD_CONST 0 (None) 12 RETURN_VALUE @@ -408,11 +381,24 @@ class C: ```py class C: def test(self) -> None: - with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + with patch("black.out", print): + self.assertEqual( + unstyle(str(report)), "1 file reformatted, 1 file failed to reformat." + ) + self.assertEqual( + unstyle(str(report)), + "1 file reformatted, 1 file left unchanged, 1 file failed to reformat.", + ) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 1 file left unchanged, 1 file failed to" + " reformat.", + ) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat.", + ) for i in (a,): if ( # Rule 1 @@ -422,19 +408,36 @@ class C: ): while ( # Just a comment - NOT_IMPLEMENTED_call() + call() ): # Another - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - xxxxxxxxxxxxxxxx = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print(i) + xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( + push_manager=context.request.resource_manager, + max_items_to_push=num_items, + batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE, + ).push( + # Only send the first n items. + items=items[:num_items], + ) return ( 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' % (test.name, test.filename, lineno, lname, err) ) def omitting_trailers(self) -> None: - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)[OneLevelIndex] - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)[OneLevelIndex][TwoLevelIndex][ + get_collection( + hey_this_is_a_very_long_call, it_has_funny_attributes, really=True + )[ + OneLevelIndex + ] + get_collection( + hey_this_is_a_very_long_call, it_has_funny_attributes, really=True + )[ + OneLevelIndex + ][ + TwoLevelIndex + ][ ThreeLevelIndex ][ FourLevelIndex @@ -442,7 +445,10 @@ class C: d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ 22 ] - assignment = NOT_IMPLEMENTED_call() and another.rule.ending_with.index[123] + assignment = ( + some.rather.elaborate.rule() + and another.rule.ending_with.index[123] + ) def easy_asserts(self) -> None: NOT_YET_IMPLEMENTED_StmtAssert diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition_no_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition_no_trailing_comma.py.snap index 21c7c8dc7b..993bab559a 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition_no_trailing_comma.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition_no_trailing_comma.py.snap @@ -193,69 +193,41 @@ class C: ```diff --- Black +++ Ruff -@@ -1,23 +1,10 @@ - class C: - def test(self) -> None: -- with patch("black.out", print): -- self.assertEqual( -- unstyle(str(report)), "1 file reformatted, 1 file failed to reformat." -- ) -- self.assertEqual( -- unstyle(str(report)), -- "1 file reformatted, 1 file left unchanged, 1 file failed to reformat.", -- ) -- self.assertEqual( -- unstyle(str(report)), -- "2 files reformatted, 1 file left unchanged, 1 file failed to" -- " reformat.", -- ) -- self.assertEqual( -- unstyle(str(report)), -- "2 files reformatted, 2 files left unchanged, 2 files failed to" -- " reformat.", -- ) -+ with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - for i in (a,): - if ( - # Rule 1 -@@ -27,133 +14,46 @@ - ): +@@ -28,8 +28,8 @@ while ( # Just a comment -- call() -- # Another -+ NOT_IMPLEMENTED_call() - ): -- print(i) -- xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( -- push_manager=context.request.resource_manager, -- max_items_to_push=num_items, -- batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE, -- ).push( -- # Only send the first n items. + call() ++ ): + # Another +- ): + 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] -- ) -+ # Another -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ xxxxxxxxxxxxxxxx = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ items=items[:num_items], + ) return ( 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' - % (test.name, test.filename, lineno, lname, err) - ) - +@@ -47,113 +47,46 @@ def omitting_trailers(self) -> None: -- get_collection( -- hey_this_is_a_very_long_call, it_has_funny_attributes, really=True + get_collection( + hey_this_is_a_very_long_call, it_has_funny_attributes, really=True - )[OneLevelIndex] -- get_collection( -- hey_this_is_a_very_long_call, it_has_funny_attributes, really=True ++ )[ ++ OneLevelIndex ++ ] + get_collection( + hey_this_is_a_very_long_call, it_has_funny_attributes, really=True - )[OneLevelIndex][TwoLevelIndex][ThreeLevelIndex][FourLevelIndex] -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)[OneLevelIndex] -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)[OneLevelIndex][TwoLevelIndex][ ++ )[ ++ OneLevelIndex ++ ][ ++ TwoLevelIndex ++ ][ + ThreeLevelIndex + ][ + FourLevelIndex @@ -263,10 +235,11 @@ class C: d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ 22 ] -- assignment = ( + assignment = ( - some.rather.elaborate.rule() and another.rule.ending_with.index[123] -- ) -+ assignment = NOT_IMPLEMENTED_call() and another.rule.ending_with.index[123] ++ some.rather.elaborate.rule() ++ and another.rule.ending_with.index[123] + ) def easy_asserts(self) -> None: - assert { @@ -376,7 +349,7 @@ class C: %3d 0 LOAD_FAST 1 (x) 2 LOAD_CONST 1 (1) 4 COMPARE_OP 2 (==) -@@ -161,21 +61,8 @@ +@@ -161,21 +94,8 @@ 8 STORE_ATTR 0 (x) 10 LOAD_CONST 0 (None) 12 RETURN_VALUE @@ -408,11 +381,24 @@ class C: ```py class C: def test(self) -> None: - with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + with patch("black.out", print): + self.assertEqual( + unstyle(str(report)), "1 file reformatted, 1 file failed to reformat." + ) + self.assertEqual( + unstyle(str(report)), + "1 file reformatted, 1 file left unchanged, 1 file failed to reformat.", + ) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 1 file left unchanged, 1 file failed to" + " reformat.", + ) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat.", + ) for i in (a,): if ( # Rule 1 @@ -422,19 +408,36 @@ class C: ): while ( # Just a comment - NOT_IMPLEMENTED_call() + call() ): # Another - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - xxxxxxxxxxxxxxxx = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print(i) + xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( + push_manager=context.request.resource_manager, + max_items_to_push=num_items, + batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE, + ).push( + # Only send the first n items. + items=items[:num_items], + ) return ( 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' % (test.name, test.filename, lineno, lname, err) ) def omitting_trailers(self) -> None: - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)[OneLevelIndex] - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)[OneLevelIndex][TwoLevelIndex][ + get_collection( + hey_this_is_a_very_long_call, it_has_funny_attributes, really=True + )[ + OneLevelIndex + ] + get_collection( + hey_this_is_a_very_long_call, it_has_funny_attributes, really=True + )[ + OneLevelIndex + ][ + TwoLevelIndex + ][ ThreeLevelIndex ][ FourLevelIndex @@ -442,7 +445,10 @@ class C: d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ 22 ] - assignment = NOT_IMPLEMENTED_call() and another.rule.ending_with.index[123] + assignment = ( + some.rather.elaborate.rule() + and another.rule.ending_with.index[123] + ) def easy_asserts(self) -> None: NOT_YET_IMPLEMENTED_StmtAssert diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@empty_lines.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@empty_lines.py.snap index b8e1bf9f55..598235141b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@empty_lines.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@empty_lines.py.snap @@ -104,7 +104,7 @@ def g(): ```diff --- Black +++ Ruff -@@ -16,32 +16,40 @@ +@@ -16,7 +16,7 @@ if t == token.COMMENT: # another trailing comment return DOUBLESPACE @@ -113,9 +113,7 @@ def g(): prev = leaf.prev_sibling if not prev: -- prevp = preceding_leaf(p) -+ prevp = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - if not prevp or prevp.type in OPENING_BRACKETS: +@@ -25,23 +25,31 @@ return NO if prevp.type == token.EQUAL: @@ -169,7 +167,7 @@ def g(): def g(): NO = "" SPACE = " " -@@ -67,11 +74,11 @@ +@@ -67,7 +74,7 @@ return DOUBLESPACE # Another comment because more comments @@ -178,11 +176,6 @@ def g(): prev = leaf.prev_sibling if not prev: -- prevp = preceding_leaf(p) -+ prevp = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - if not prevp or prevp.type in OPENING_BRACKETS: - # Start of the line or a bracketed expression. @@ -79,11 +86,15 @@ return NO @@ -233,7 +226,7 @@ def f(): prev = leaf.prev_sibling if not prev: - prevp = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + prevp = preceding_leaf(p) if not prevp or prevp.type in OPENING_BRACKETS: return NO @@ -291,7 +284,7 @@ def g(): prev = leaf.prev_sibling if not prev: - prevp = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + prevp = preceding_leaf(p) if not prevp or prevp.type in OPENING_BRACKETS: # Start of the line or a bracketed expression. diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap index c1c18a580f..9a1f301091 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap @@ -337,7 +337,7 @@ last_call() () (1,) (1, 2) -@@ -68,60 +75,51 @@ +@@ -68,40 +75,37 @@ 2, 3, ] @@ -395,39 +395,19 @@ last_call() +{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} Python3 > Python2 > COBOL Life is Life --call() --call(arg) --call(kwarg="hey") --call(arg, kwarg="hey") --call(arg, another, kwarg="hey", **kwargs) --call( -- this_is_a_very_long_variable_which_will_force_a_delimiter_split, -- arg, -- another, -- kwarg="hey", -- **kwargs, --) # note: no trailing comma pre-3.6 + call() +@@ -116,8 +120,8 @@ + kwarg="hey", + **kwargs, + ) # note: no trailing comma pre-3.6 -call(*gidgets[:2]) -call(a, *gidgets[:2]) --call(**self.screen_kwargs) --call(b, **self.screen_kwargs) -+NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # note: no trailing comma pre-3.6 -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++call(*NOT_YET_IMPLEMENTED_ExprStarred) ++call(a, *NOT_YET_IMPLEMENTED_ExprStarred) + call(**self.screen_kwargs) + call(b, **self.screen_kwargs) lukasz.langa.pl --call.me(maybe) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - (1).real - (1.0).real - ....__class__ -@@ -130,34 +128,28 @@ +@@ -130,34 +134,28 @@ tuple[str, ...] tuple[str, int, float, dict[str, int]] tuple[ @@ -475,7 +455,7 @@ last_call() numpy[0, :] numpy[:, i] numpy[0, :2] -@@ -171,25 +163,32 @@ +@@ -171,20 +169,27 @@ numpy[1 : c + 1, c] numpy[-(c + 1) :, d] numpy[:, l[-2]] @@ -511,34 +491,19 @@ last_call() { "id": "1", "type": "type", -- "started_at": now(), -- "ended_at": now() + timedelta(days=10), -+ "started_at": NOT_IMPLEMENTED_call(), -+ "ended_at": NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), - "priority": 1, - "import_session_id": 1, - **kwargs, -@@ -198,35 +197,21 @@ - b = (1,) +@@ -199,32 +204,22 @@ c = 1 d = (1,) + a + (2,) --e = (1,).count(1) + e = (1,).count(1) -f = 1, *range(10) -g = 1, *"ten" --what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set( -- vars_to_remove -+e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +f = 1, *NOT_YET_IMPLEMENTED_ExprStarred +g = 1, *NOT_YET_IMPLEMENTED_ExprStarred -+what_is_up_with_those_new_coord_names = ( -+ (coord_names + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) -+ + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set( + vars_to_remove ) --what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( -- vars_to_remove -+what_is_up_with_those_new_coord_names = ( -+ (coord_names | NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) -+ - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( + vars_to_remove ) -result = ( - session.query(models.Customer.id) @@ -558,16 +523,18 @@ last_call() - ) - .all() -) --Ø = set() --authors.łukasz.say_thanks() -+result = NOT_IMPLEMENTED_call() -+result = NOT_IMPLEMENTED_call() -+Ø = NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() ++result = session.query(models.Customer.id).filter( ++ models.Customer.account_id == account_id, models.Customer.email == email_address ++).order_by(models.Customer.id.asc()).all() ++result = session.query(models.Customer.id).filter( ++ models.Customer.account_id == account_id, models.Customer.email == email_address ++).order_by( ++ models.Customer.id.asc(), ++).all() + Ø = set() + authors.łukasz.say_thanks() mapping = { - A: 0.25 * (10.0 / 12), - B: 0.1 * (10.0 / 12), -@@ -236,31 +221,29 @@ +@@ -236,29 +231,27 @@ def gen(): @@ -582,8 +549,7 @@ last_call() async def f(): -- await some.complicated[0].call(with_args=(True or (1 is not 1))) -+ await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + await some.complicated[0].call(with_args=(True or (1 is not 1))) -print(*[] or [1]) @@ -594,9 +560,9 @@ last_call() - force=False -), "Short message" -assert parens is TooMany -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++print(*NOT_YET_IMPLEMENTED_ExprStarred) ++print(**NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false) ++print(*NOT_YET_IMPLEMENTED_ExprStarred) +NOT_YET_IMPLEMENTED_StmtAssert +NOT_YET_IMPLEMENTED_StmtAssert +NOT_YET_IMPLEMENTED_StmtAssert @@ -607,34 +573,9 @@ last_call() -for z in (i for i in (1, 2, 3)): +for z in (i for i in []): ... --for i in call(): -+for i in NOT_IMPLEMENTED_call(): + for i in call(): ... - for j in 1 + (2 + 3): - ... -@@ -272,7 +255,7 @@ - addr_proto, - addr_canonname, - addr_sockaddr, --) in socket.getaddrinfo("google.com", "http"): -+) in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): - pass - a = ( - aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp -@@ -291,9 +274,9 @@ - is not qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz - ) - if ( -- threading.current_thread() != threading.main_thread() -- and threading.current_thread() != threading.main_thread() -- or signal.getsignal(signal.SIGINT) != signal.default_int_handler -+ NOT_IMPLEMENTED_call() != NOT_IMPLEMENTED_call() -+ and NOT_IMPLEMENTED_call() != NOT_IMPLEMENTED_call() -+ or NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) != signal.default_int_handler - ): - return True - if ( -@@ -327,13 +310,18 @@ +@@ -327,13 +320,18 @@ ): return True if ( @@ -656,7 +597,7 @@ last_call() ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n ): return True -@@ -341,7 +329,8 @@ +@@ -341,7 +339,8 @@ ~aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e @@ -666,13 +607,6 @@ last_call() ^ aaaaaaaaaaaaaaaa.i << aaaaaaaaaaaaaaaa.k >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n -@@ -366,5 +355,5 @@ - ^ bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - ^ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - ) --last_call() -+NOT_IMPLEMENTED_call() - # standalone comment at ENDMARKER ``` ## Ruff Output @@ -788,18 +722,24 @@ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} Python3 > Python2 > COBOL Life is Life -NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # note: no trailing comma pre-3.6 -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +call() +call(arg) +call(kwarg="hey") +call(arg, kwarg="hey") +call(arg, another, kwarg="hey", **kwargs) +call( + this_is_a_very_long_variable_which_will_force_a_delimiter_split, + arg, + another, + kwarg="hey", + **kwargs, +) # note: no trailing comma pre-3.6 +call(*NOT_YET_IMPLEMENTED_ExprStarred) +call(a, *NOT_YET_IMPLEMENTED_ExprStarred) +call(**self.screen_kwargs) +call(b, **self.screen_kwargs) lukasz.langa.pl -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +call.me(maybe) (1).real (1.0).real ....__class__ @@ -867,8 +807,8 @@ SomeName { "id": "1", "type": "type", - "started_at": NOT_IMPLEMENTED_call(), - "ended_at": NOT_IMPLEMENTED_call() + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), + "started_at": now(), + "ended_at": now() + timedelta(days=10), "priority": 1, "import_session_id": 1, **kwargs, @@ -877,21 +817,25 @@ a = (1,) b = (1,) c = 1 d = (1,) + a + (2,) -e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +e = (1,).count(1) f = 1, *NOT_YET_IMPLEMENTED_ExprStarred g = 1, *NOT_YET_IMPLEMENTED_ExprStarred -what_is_up_with_those_new_coord_names = ( - (coord_names + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) - + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set( + vars_to_remove ) -what_is_up_with_those_new_coord_names = ( - (coord_names | NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) - - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( + vars_to_remove ) -result = NOT_IMPLEMENTED_call() -result = NOT_IMPLEMENTED_call() -Ø = NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() +result = session.query(models.Customer.id).filter( + models.Customer.account_id == account_id, models.Customer.email == email_address +).order_by(models.Customer.id.asc()).all() +result = session.query(models.Customer.id).filter( + models.Customer.account_id == account_id, models.Customer.email == email_address +).order_by( + models.Customer.id.asc(), +).all() +Ø = set() +authors.łukasz.say_thanks() mapping = { A: 0.25 * (10.0 / 12), B: 0.1 * (10.0 / 12), @@ -908,12 +852,12 @@ def gen(): async def f(): - await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + await some.complicated[0].call(with_args=(True or (1 is not 1))) -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +print(*NOT_YET_IMPLEMENTED_ExprStarred) +print(**NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false) +print(*NOT_YET_IMPLEMENTED_ExprStarred) NOT_YET_IMPLEMENTED_StmtAssert NOT_YET_IMPLEMENTED_StmtAssert NOT_YET_IMPLEMENTED_StmtAssert @@ -923,7 +867,7 @@ for y in (): ... for z in (i for i in []): ... -for i in NOT_IMPLEMENTED_call(): +for i in call(): ... for j in 1 + (2 + 3): ... @@ -935,7 +879,7 @@ for ( addr_proto, addr_canonname, addr_sockaddr, -) in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): +) in socket.getaddrinfo("google.com", "http"): pass a = ( aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp @@ -954,9 +898,9 @@ a = ( is not qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz ) if ( - NOT_IMPLEMENTED_call() != NOT_IMPLEMENTED_call() - and NOT_IMPLEMENTED_call() != NOT_IMPLEMENTED_call() - or NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) != signal.default_int_handler + threading.current_thread() != threading.main_thread() + and threading.current_thread() != threading.main_thread() + or signal.getsignal(signal.SIGINT) != signal.default_int_handler ): return True if ( @@ -1035,7 +979,7 @@ bbbb >> bbbb * bbbb ^ bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ^ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ) -NOT_IMPLEMENTED_call() +last_call() # standalone comment at ENDMARKER ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap index 4e210b4109..e8c3771da6 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap @@ -202,11 +202,11 @@ d={'a':1, #!/usr/bin/env python3 -import asyncio -import sys -- --from third_party import X, Y, Z +NOT_YET_IMPLEMENTED_StmtImport +NOT_YET_IMPLEMENTED_StmtImport +-from third_party import X, Y, Z +- -from library import some_connection, some_decorator +NOT_YET_IMPLEMENTED_StmtImportFrom @@ -240,10 +240,10 @@ d={'a':1, + NOT_YET_IMPLEMENTED_StmtRaise + if False: + ... -+ for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ for i in range(10): ++ print(i) + continue -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ exec("new-style exec", {}, {}) + return None + + @@ -254,7 +254,7 @@ d={'a':1, - await asyncio.sleep(1) + "Single-line docstring. Multiline is harder to reformat." + NOT_YET_IMPLEMENTED_StmtAsyncWith -+ await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ await asyncio.sleep(1) + + @asyncio.coroutine @@ -264,7 +264,7 @@ d={'a':1, -) -def function_signature_stress_test(number:int,no_annotation=None,text:str='default',* ,debug:bool=False,**kwargs) -> str: - return text[number:-1] -+@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++@some_decorator(with_args=True, many_args=[1, 2, 3]) +def function_signature_stress_test( + number: int, + no_annotation=None, @@ -291,12 +291,12 @@ d={'a':1, + h="", + i=r"", +): -+ offset = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ offset = attr.ib(default=attr.Factory(lambda x: True)) + NOT_YET_IMPLEMENTED_StmtAssert def spaces_types( -@@ -51,68 +72,66 @@ +@@ -51,7 +72,7 @@ d: dict = {}, e: bool = True, f: int = -1, @@ -305,13 +305,7 @@ d={'a':1, h: str = "", i: str = r"", ): - ... - - --def spaces2(result=_core.Value(None)): -+def spaces2(result=NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)): - ... - +@@ -64,55 +85,55 @@ something = { # fmt: off @@ -381,11 +375,13 @@ d={'a':1, - models.Customer.email == email_address)\ - .order_by(models.Customer.id.asc())\ - .all() -+ result = NOT_IMPLEMENTED_call() ++ result = session.query(models.Customer.id).filter( ++ models.Customer.account_id == account_id, models.Customer.email == email_address ++ ).order_by(models.Customer.id.asc()).all() # fmt: on -@@ -133,10 +152,10 @@ +@@ -133,10 +154,10 @@ """Another known limitation.""" # fmt: on # fmt: off @@ -393,92 +389,41 @@ d={'a':1, - and_=indeed . it is not formatted - because . the . handling . inside . generate_ignored_nodes() - now . considers . multiple . fmt . directives . within . one . prefix -+ this = NOT_IMPLEMENTED_call() ++ this = should.not_be.formatted() + and_ = indeed.it is not formatted -+ NOT_IMPLEMENTED_call() ++ because.the.handling.inside.generate_ignored_nodes() + now.considers.multiple.fmt.directives.within.one.prefix # fmt: on # fmt: off # ...but comments still get reformatted even though they should not be -@@ -145,43 +164,11 @@ - - def long_lines(): - if True: -- typedargslist.extend( -- gen_annotated_params( -- ast_args.kwonlyargs, -- ast_args.kw_defaults, -- parameters, -- implicit_default=True, +@@ -151,12 +172,10 @@ + ast_args.kw_defaults, + parameters, + implicit_default=True, - ) -- ) -- # fmt: off ++ ), + ) + # fmt: off - a = ( - unnecessary_bracket() - ) -- # fmt: on -- _type_comment_re = re.compile( -- r""" -- ^ -- [\t ]* -- \#[ ]type:[ ]* -- (?P -- [^#\t\n]+? -- ) -- (? to match -- # a trailing space which is why we need the silliness below -- (? -- (?:\#[^\n]*)? -- \n? -- ) -- $ -- """, -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ a = unnecessary_bracket() + # fmt: on + _type_comment_re = re.compile( + r""" +@@ -179,7 +198,8 @@ + $ + """, # fmt: off - re.MULTILINE|re.VERBOSE -+ a = NOT_IMPLEMENTED_call() ++ re.MULTILINE ++ | re.VERBOSE, # fmt: on -- ) -+ _type_comment_re = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + ) - - def single_literal_yapf_disable(): -@@ -189,36 +176,9 @@ - BAZ = {(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)} # yapf: disable - - --cfg.rule( -- "Default", -- "address", -- xxxx_xxxx=["xxx-xxxxxx-xxxxxxxxxx"], -- xxxxxx="xx_xxxxx", -- xxxxxxx="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", -- xxxxxxxxx_xxxx=True, -- xxxxxxxx_xxxxxxxxxx=False, -- xxxxxx_xxxxxx=2, -- xxxxxx_xxxxx_xxxxxxxx=70, -- xxxxxx_xxxxxx_xxxxx=True, -- # fmt: off -- xxxxxxx_xxxxxxxxxxxx={ -- "xxxxxxxx": { -- "xxxxxx": False, -- "xxxxxxx": False, -- "xxxx_xxxxxx": "xxxxx", -- }, -- "xxxxxxxx-xxxxx": { -- "xxxxxx": False, -- "xxxxxxx": True, -- "xxxx_xxxxxx": "xxxxxx", -- }, -- }, -- # fmt: on -- xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5, --) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +@@ -217,8 +237,7 @@ + xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5, + ) # fmt: off -yield 'hello' +NOT_YET_IMPLEMENTED_ExprYield @@ -518,21 +463,21 @@ def func_no_args(): NOT_YET_IMPLEMENTED_StmtRaise if False: ... - for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + for i in range(10): + print(i) continue - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + exec("new-style exec", {}, {}) return None async def coroutine(arg, exec=False): "Single-line docstring. Multiline is harder to reformat." NOT_YET_IMPLEMENTED_StmtAsyncWith - await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + await asyncio.sleep(1) @asyncio.coroutine -@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +@some_decorator(with_args=True, many_args=[1, 2, 3]) def function_signature_stress_test( number: int, no_annotation=None, @@ -556,7 +501,7 @@ def spaces( h="", i=r"", ): - offset = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + offset = attr.ib(default=attr.Factory(lambda x: True)) NOT_YET_IMPLEMENTED_StmtAssert @@ -574,7 +519,7 @@ def spaces_types( ... -def spaces2(result=NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)): +def spaces2(result=_core.Value(None)): ... @@ -626,7 +571,9 @@ def yield_expr(): def example(session): # fmt: off - result = NOT_IMPLEMENTED_call() + result = session.query(models.Customer.id).filter( + models.Customer.account_id == account_id, models.Customer.email == email_address + ).order_by(models.Customer.id.asc()).all() # fmt: on @@ -647,9 +594,9 @@ def on_and_off_broken(): """Another known limitation.""" # fmt: on # fmt: off - this = NOT_IMPLEMENTED_call() + this = should.not_be.formatted() and_ = indeed.it is not formatted - NOT_IMPLEMENTED_call() + because.the.handling.inside.generate_ignored_nodes() now.considers.multiple.fmt.directives.within.one.prefix # fmt: on # fmt: off @@ -659,11 +606,42 @@ def on_and_off_broken(): def long_lines(): if True: - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + typedargslist.extend( + gen_annotated_params( + ast_args.kwonlyargs, + ast_args.kw_defaults, + parameters, + implicit_default=True, + ), + ) # fmt: off - a = NOT_IMPLEMENTED_call() + a = unnecessary_bracket() # fmt: on - _type_comment_re = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + _type_comment_re = re.compile( + r""" + ^ + [\t ]* + \#[ ]type:[ ]* + (?P + [^#\t\n]+? + ) + (? to match + # a trailing space which is why we need the silliness below + (? + (?:\#[^\n]*)? + \n? + ) + $ + """, + # fmt: off + re.MULTILINE + | re.VERBOSE, + # fmt: on + ) def single_literal_yapf_disable(): @@ -671,7 +649,33 @@ def single_literal_yapf_disable(): BAZ = {(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)} # yapf: disable -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +cfg.rule( + "Default", + "address", + xxxx_xxxx=["xxx-xxxxxx-xxxxxxxxxx"], + xxxxxx="xx_xxxxx", + xxxxxxx="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + xxxxxxxxx_xxxx=True, + xxxxxxxx_xxxxxxxxxx=False, + xxxxxx_xxxxxx=2, + xxxxxx_xxxxx_xxxxxxxx=70, + xxxxxx_xxxxxx_xxxxx=True, + # fmt: off + xxxxxxx_xxxxxxxxxxxx={ + "xxxxxxxx": { + "xxxxxx": False, + "xxxxxxx": False, + "xxxx_xxxxxx": "xxxxx", + }, + "xxxxxxxx-xxxxx": { + "xxxxxx": False, + "xxxxxxx": True, + "xxxx_xxxxxx": "xxxxxx", + }, + }, + # fmt: on + xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5, +) # fmt: off NOT_YET_IMPLEMENTED_ExprYield # No formatting to the end of the file diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff2.py.snap index 548b61e58d..56b345fae1 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff2.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff2.py.snap @@ -52,7 +52,7 @@ def test_calculate_fades(): ```diff --- Black +++ Ruff -@@ -1,40 +1,38 @@ +@@ -1,40 +1,44 @@ -import pytest +NOT_YET_IMPLEMENTED_StmtImport @@ -68,11 +68,15 @@ def test_calculate_fades(): -@pytest.mark.parametrize('test', [ - - # Test don't manage the volume -- [ ++@pytest.mark.parametrize( ++ "test", + [ - ('stuff', 'in') -- ], ++ # Test don't manage the volume ++ [("stuff", "in")], + ], -]) -+@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++) def test_fader(test): pass @@ -120,7 +124,13 @@ TmEx = 2 # Test data: # Position, Volume, State, TmSt/TmEx/None, [call, [arg1...]] -@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +@pytest.mark.parametrize( + "test", + [ + # Test don't manage the volume + [("stuff", "in")], + ], +) def test_fader(test): pass diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff4.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff4.py.snap index 63a650ff45..89b2436af1 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff4.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff4.py.snap @@ -25,27 +25,30 @@ def f(): pass ```diff --- Black +++ Ruff -@@ -1,20 +1,10 @@ +@@ -1,8 +1,12 @@ # fmt: off -@test([ - 1, 2, - 3, 4, -]) -+@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++@test( ++ [ ++ 1, ++ 2, ++ 3, ++ 4, ++ ], ++) # fmt: on def f(): pass - - --@test( -- [ -- 1, -- 2, -- 3, -- 4, +@@ -14,7 +18,7 @@ + 2, + 3, + 4, - ] --) -+@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ ], + ) def f(): pass ``` @@ -54,13 +57,27 @@ def f(): pass ```py # fmt: off -@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +@test( + [ + 1, + 2, + 3, + 4, + ], +) # fmt: on def f(): pass -@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +@test( + [ + 1, + 2, + 3, + 4, + ], +) def f(): pass ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff5.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff5.py.snap index 3bbc9eaea4..5cc5334320 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff5.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff5.py.snap @@ -96,44 +96,27 @@ elif unformatted: ```diff --- Black +++ Ruff -@@ -1,33 +1,15 @@ - # Regression test for https://github.com/psf/black/issues/3129. --setup( -- entry_points={ -- # fmt: off -- "console_scripts": [ -- "foo-bar" -- "=foo.bar.:main", +@@ -5,8 +5,8 @@ + "console_scripts": [ + "foo-bar" + "=foo.bar.:main", - # fmt: on - ] # Includes an formatted indentation. -- }, --) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - - # Regression test for https://github.com/psf/black/issues/2015. --run( -- # fmt: off -- [ -- "ls", -- "-la", -- ] -- # fmt: on -- + path, -- check=True, --) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - ++ # fmt: on ++ ], # Includes an formatted indentation. + }, + ) +@@ -27,7 +27,7 @@ # Regression test for https://github.com/psf/black/issues/3026. def test_func(): # yapf: disable - if unformatted( args ): -+ if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): ++ if unformatted(args): return True # yapf: enable elif b: -@@ -39,12 +21,12 @@ +@@ -39,10 +39,10 @@ # Regression test for https://github.com/psf/black/issues/2567. if True: # fmt: off @@ -141,17 +124,14 @@ elif unformatted: - # fmt: on - print ( "This won't be formatted" ) - print ( "This won't be formatted either" ) -+ for _ in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): ++ for _ in range(1): + # fmt: on -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ print("This won't be formatted") ++ print("This won't be formatted either") else: -- print("This will be formatted") -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("This will be formatted") - - # Regression test for https://github.com/psf/black/issues/3184. -@@ -52,29 +34,27 @@ +@@ -52,14 +52,12 @@ async def call(param): if param: # fmt: off @@ -161,17 +141,15 @@ elif unformatted: + if param[0:4] in ("ABCD", "EFGH"): # fmt: on - print ( "This won't be formatted" ) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ print("This won't be formatted") elif param[0:4] in ("ZZZZ",): - print ( "This won't be formatted either" ) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ print("This won't be formatted either") -- print("This will be formatted") -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("This will be formatted") - - # Regression test for https://github.com/psf/black/issues/2985. +@@ -68,13 +66,13 @@ class Named(t.Protocol): # fmt: off @property @@ -187,32 +165,49 @@ elif unformatted: # fmt: on -@@ -82,6 +62,6 @@ +@@ -82,6 +80,6 @@ if x: return x # fmt: off -elif unformatted: +elif unformatted: # fmt: on -- will_be_formatted() -+ NOT_IMPLEMENTED_call() + will_be_formatted() ``` ## Ruff Output ```py # Regression test for https://github.com/psf/black/issues/3129. -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +setup( + entry_points={ + # fmt: off + "console_scripts": [ + "foo-bar" + "=foo.bar.:main", + # fmt: on + ], # Includes an formatted indentation. + }, +) # Regression test for https://github.com/psf/black/issues/2015. -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +run( + # fmt: off + [ + "ls", + "-la", + ] + # fmt: on + + path, + check=True, +) # Regression test for https://github.com/psf/black/issues/3026. def test_func(): # yapf: disable - if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): + if unformatted(args): return True # yapf: enable elif b: @@ -224,12 +219,12 @@ def test_func(): # Regression test for https://github.com/psf/black/issues/2567. if True: # fmt: off - for _ in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): + for _ in range(1): # fmt: on - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("This won't be formatted") + print("This won't be formatted either") else: - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("This will be formatted") # Regression test for https://github.com/psf/black/issues/3184. @@ -239,12 +234,12 @@ class A: # fmt: off if param[0:4] in ("ABCD", "EFGH"): # fmt: on - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("This won't be formatted") elif param[0:4] in ("ZZZZ",): - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("This won't be formatted either") - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("This will be formatted") # Regression test for https://github.com/psf/black/issues/2985. @@ -267,7 +262,7 @@ if x: # fmt: off elif unformatted: # fmt: on - NOT_IMPLEMENTED_call() + will_be_formatted() ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip5.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip5.py.snap index 156c1d3712..cb7904beff 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip5.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip5.py.snap @@ -21,7 +21,7 @@ else: ```diff --- Black +++ Ruff -@@ -1,9 +1,10 @@ +@@ -1,7 +1,8 @@ a, b, c = 3, 4, 5 if ( a == 3 @@ -30,11 +30,7 @@ else: + != 9 # fmt: skip and c is not None ): -- print("I'm good!") -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - else: -- print("I'm bad") -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("I'm good!") ``` ## Ruff Output @@ -47,9 +43,9 @@ if ( != 9 # fmt: skip and c is not None ): - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("I'm good!") else: - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("I'm bad") ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip6.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip6.py.snap deleted file mode 100644 index 5c22c86651..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip6.py.snap +++ /dev/null @@ -1,49 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip6.py ---- -## Input - -```py -class A: - def f(self): - for line in range(10): - if True: - pass # fmt: skip -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,5 +1,5 @@ - class A: - def f(self): -- for line in range(10): -+ for line in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): - if True: - pass # fmt: skip -``` - -## Ruff Output - -```py -class A: - def f(self): - for line in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): - if True: - pass # fmt: skip -``` - -## Black Output - -```py -class A: - def f(self): - for line in range(10): - if True: - pass # fmt: skip -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap index 5e791205c3..133a41b6cf 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap @@ -77,65 +77,54 @@ async def test_async_with(): @@ -1,62 +1,55 @@ # Make sure a leading comment is not removed. -def some_func( unformatted, args ): # fmt: skip -- print("I am some_func") +def some_func(unformatted, args): # fmt: skip -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("I am some_func") return 0 # Make sure this comment is not removed. # Make sure a leading comment is not removed. -async def some_async_func( unformatted, args): # fmt: skip -- print("I am some_async_func") -- await asyncio.sleep(1) +async def some_async_func(unformatted, args): # fmt: skip -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("I am some_async_func") + await asyncio.sleep(1) # Make sure a leading comment is not removed. -class SomeClass( Unformatted, SuperClasses ): # fmt: skip - def some_method( self, unformatted, args ): # fmt: skip -- print("I am some_method") +class SomeClass(Unformatted, SuperClasses): # fmt: skip + def some_method(self, unformatted, args): # fmt: skip -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("I am some_method") return 0 - async def some_async_method( self, unformatted, args ): # fmt: skip -- print("I am some_async_method") -- await asyncio.sleep(1) + async def some_async_method(self, unformatted, args): # fmt: skip -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("I am some_async_method") + await asyncio.sleep(1) # Make sure a leading comment is not removed. -if unformatted_call( args ): # fmt: skip -- print("First branch") -+if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: skip -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++if unformatted_call(args): # fmt: skip + print("First branch") # Make sure this is not removed. -elif another_unformatted_call( args ): # fmt: skip -- print("Second branch") ++elif another_unformatted_call(args): # fmt: skip + print("Second branch") -else : # fmt: skip -- print("Last branch") -+elif NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: skip -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +else: # fmt: skip -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("Last branch") -while some_condition( unformatted, args ): # fmt: skip -- print("Do something") -+while NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: skip -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++while some_condition(unformatted, args): # fmt: skip + print("Do something") -for i in some_iter( unformatted, args ): # fmt: skip -- print("Do something") -+for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: skip -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++for i in some_iter(unformatted, args): # fmt: skip + print("Do something") async def test_async_for(): @@ -154,9 +143,8 @@ async def test_async_with(): -with give_me_context( unformatted, args ): # fmt: skip -- print("Do something") -+with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: skip -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++with give_me_context(unformatted, args): # fmt: skip + print("Do something") async def test_async_with(): @@ -170,44 +158,44 @@ async def test_async_with(): ```py # Make sure a leading comment is not removed. def some_func(unformatted, args): # fmt: skip - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("I am some_func") return 0 # Make sure this comment is not removed. # Make sure a leading comment is not removed. async def some_async_func(unformatted, args): # fmt: skip - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("I am some_async_func") + await asyncio.sleep(1) # Make sure a leading comment is not removed. class SomeClass(Unformatted, SuperClasses): # fmt: skip def some_method(self, unformatted, args): # fmt: skip - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("I am some_method") return 0 async def some_async_method(self, unformatted, args): # fmt: skip - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("I am some_async_method") + await asyncio.sleep(1) # Make sure a leading comment is not removed. -if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: skip - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +if unformatted_call(args): # fmt: skip + print("First branch") # Make sure this is not removed. -elif NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: skip - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +elif another_unformatted_call(args): # fmt: skip + print("Second branch") else: # fmt: skip - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("Last branch") -while NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: skip - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +while some_condition(unformatted, args): # fmt: skip + print("Do something") -for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: skip - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +for i in some_iter(unformatted, args): # fmt: skip + print("Do something") async def test_async_for(): @@ -217,8 +205,8 @@ async def test_async_for(): NOT_YET_IMPLEMENTED_StmtTry -with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): # fmt: skip - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +with give_me_context(unformatted, args): # fmt: skip + print("Do something") async def test_async_with(): diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function.py.snap index 2be379cb33..7562820a71 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function.py.snap @@ -107,7 +107,7 @@ def __await__(): return (yield) ```diff --- Black +++ Ruff -@@ -1,12 +1,11 @@ +@@ -1,20 +1,19 @@ #!/usr/bin/env python3 -import asyncio -import sys @@ -118,14 +118,14 @@ def __await__(): return (yield) +NOT_YET_IMPLEMENTED_StmtImportFrom -from library import some_connection, some_decorator -- --f"trigger 3.6 mode" +NOT_YET_IMPLEMENTED_StmtImportFrom +NOT_YET_IMPLEMENTED_ExprJoinedStr +-f"trigger 3.6 mode" +- def func_no_args(): -@@ -14,25 +13,24 @@ + a b c if True: @@ -133,31 +133,17 @@ def __await__(): return (yield) + NOT_YET_IMPLEMENTED_StmtRaise if False: ... -- for i in range(10): -- print(i) -+ for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - continue -- exec("new-style exec", {}, {}) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - return None - + for i in range(10): +@@ -26,8 +25,7 @@ async def coroutine(arg, exec=False): "Single-line docstring. Multiline is harder to reformat." - async with some_connection() as conn: - await conn.do_what_i_mean("SELECT bobby, tables FROM xkcd", timeout=2) -- await asyncio.sleep(1) + NOT_YET_IMPLEMENTED_StmtAsyncWith -+ await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + await asyncio.sleep(1) - @asyncio.coroutine --@some_decorator(with_args=True, many_args=[1, 2, 3]) -+@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - def function_signature_stress_test( - number: int, - no_annotation=None, @@ -41,12 +39,22 @@ debug: bool = False, **kwargs, @@ -180,12 +166,12 @@ def __await__(): return (yield) + h="", + i=r"", +): -+ offset = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ offset = attr.ib(default=attr.Factory(lambda x: True)) + NOT_YET_IMPLEMENTED_StmtAssert def spaces_types( -@@ -56,70 +64,26 @@ +@@ -56,7 +64,7 @@ d: dict = {}, e: bool = True, f: int = -1, @@ -194,12 +180,11 @@ def __await__(): return (yield) h: str = "", i: str = r"", ): - ... +@@ -64,19 +72,16 @@ --def spaces2(result=_core.Value(None)): + def spaces2(result=_core.Value(None)): - assert fut is self._read_fut, (fut, self._read_fut) -+def spaces2(result=NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)): + NOT_YET_IMPLEMENTED_StmtAssert @@ -213,57 +198,44 @@ def __await__(): return (yield) - .order_by(models.Customer.id.asc()) - .all() - ) -+ result = NOT_IMPLEMENTED_call() ++ result = session.query(models.Customer.id).filter( ++ models.Customer.account_id ++ == account_id, ++ models.Customer.email ++ == email_address, ++ ).order_by(models.Customer.id.asc()).all() def long_lines(): - if True: -- typedargslist.extend( -- gen_annotated_params( -- ast_args.kwonlyargs, -- ast_args.kw_defaults, -- parameters, -- implicit_default=True, +@@ -87,7 +92,7 @@ + ast_args.kw_defaults, + parameters, + implicit_default=True, - ) -- ) -- typedargslist.extend( -- gen_annotated_params( -- ast_args.kwonlyargs, -- ast_args.kw_defaults, -- parameters, -- implicit_default=True, -- # trailing standalone comment ++ ), + ) + typedargslist.extend( + gen_annotated_params( +@@ -96,7 +101,7 @@ + parameters, + implicit_default=True, + # trailing standalone comment - ) -- ) -- _type_comment_re = re.compile( -- r""" -- ^ -- [\t ]* -- \#[ ]type:[ ]* -- (?P -- [^#\t\n]+? -- ) -- (? to match -- # a trailing space which is why we need the silliness below -- (? -- (?:\#[^\n]*)? -- \n? -- ) -- $ -- """, ++ ), + ) + _type_comment_re = re.compile( + r""" +@@ -118,7 +123,8 @@ + ) + $ + """, - re.MULTILINE | re.VERBOSE, -- ) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ _type_comment_re = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ re.MULTILINE ++ | re.VERBOSE, + ) - def trailing_comma(): -@@ -135,14 +99,8 @@ +@@ -135,14 +141,8 @@ a, **kwargs, ) -> A: @@ -303,21 +275,21 @@ def func_no_args(): NOT_YET_IMPLEMENTED_StmtRaise if False: ... - for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + for i in range(10): + print(i) continue - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + exec("new-style exec", {}, {}) return None async def coroutine(arg, exec=False): "Single-line docstring. Multiline is harder to reformat." NOT_YET_IMPLEMENTED_StmtAsyncWith - await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + await asyncio.sleep(1) @asyncio.coroutine -@NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +@some_decorator(with_args=True, many_args=[1, 2, 3]) def function_signature_stress_test( number: int, no_annotation=None, @@ -340,7 +312,7 @@ def spaces( h="", i=r"", ): - offset = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + offset = attr.ib(default=attr.Factory(lambda x: True)) NOT_YET_IMPLEMENTED_StmtAssert @@ -358,19 +330,61 @@ def spaces_types( ... -def spaces2(result=NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)): +def spaces2(result=_core.Value(None)): NOT_YET_IMPLEMENTED_StmtAssert def example(session): - result = NOT_IMPLEMENTED_call() + result = session.query(models.Customer.id).filter( + models.Customer.account_id + == account_id, + models.Customer.email + == email_address, + ).order_by(models.Customer.id.asc()).all() def long_lines(): if True: - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - _type_comment_re = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + typedargslist.extend( + gen_annotated_params( + ast_args.kwonlyargs, + ast_args.kw_defaults, + parameters, + implicit_default=True, + ), + ) + typedargslist.extend( + gen_annotated_params( + ast_args.kwonlyargs, + ast_args.kw_defaults, + parameters, + implicit_default=True, + # trailing standalone comment + ), + ) + _type_comment_re = re.compile( + r""" + ^ + [\t ]* + \#[ ]type:[ ]* + (?P + [^#\t\n]+? + ) + (? to match + # a trailing space which is why we need the silliness below + (? + (?:\#[^\n]*)? + \n? + ) + $ + """, + re.MULTILINE + | re.VERBOSE, + ) def trailing_comma(): diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function2.py.snap index fe0456ee1f..bcd7954313 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function2.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function2.py.snap @@ -65,42 +65,7 @@ with hmm_but_this_should_get_two_preceding_newlines(): ```diff --- Black +++ Ruff -@@ -2,17 +2,11 @@ - a, - **kwargs, - ) -> A: -- with cache_dir(): -+ with NOT_IMPLEMENTED_call(): - if something: -- result = CliRunner().invoke( -- black.main, [str(src1), str(src2), "--diff", "--check"] -- ) -- limited.append(-limited.pop()) # negate top -- return A( -- very_long_argument_name1=very_long_value_for_the_argument, -- very_long_argument_name2=-very.long.value.for_the_argument, -- **kwargs, -- ) -+ result = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # negate top -+ return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - - def g(): -@@ -21,45 +15,31 @@ - def inner(): - pass - -- print("Inner defs should breathe a little.") -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - - def h(): - def inner(): - pass - -- print("Inner defs should breathe a little.") -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +@@ -32,34 +32,20 @@ if os.name == "posix": @@ -116,28 +81,26 @@ with hmm_but_this_should_get_two_preceding_newlines(): - - def i_should_be_followed_by_only_one_newline(): - pass -- ++ NOT_YET_IMPLEMENTED_StmtTry + - except ImportError: - - def i_should_be_followed_by_only_one_newline(): - pass -+ NOT_YET_IMPLEMENTED_StmtTry - +- elif False: - class IHopeYouAreHavingALovelyDay: def __call__(self): -- print("i_should_be_followed_by_only_one_newline") + print("i_should_be_followed_by_only_one_newline") - -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) else: - def foo(): pass - --with hmm_but_this_should_get_two_preceding_newlines(): -+with NOT_IMPLEMENTED_call(): + with hmm_but_this_should_get_two_preceding_newlines(): pass ``` @@ -148,11 +111,17 @@ def f( a, **kwargs, ) -> A: - with NOT_IMPLEMENTED_call(): + with cache_dir(): if something: - result = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # negate top - return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + result = CliRunner().invoke( + black.main, [str(src1), str(src2), "--diff", "--check"] + ) + limited.append(-limited.pop()) # negate top + return A( + very_long_argument_name1=very_long_value_for_the_argument, + very_long_argument_name2=-very.long.value.for_the_argument, + **kwargs, + ) def g(): @@ -161,14 +130,14 @@ def g(): def inner(): pass - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("Inner defs should breathe a little.") def h(): def inner(): pass - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("Inner defs should breathe a little.") if os.name == "posix": @@ -182,12 +151,12 @@ elif os.name == "nt": elif False: class IHopeYouAreHavingALovelyDay: def __call__(self): - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("i_should_be_followed_by_only_one_newline") else: def foo(): pass -with NOT_IMPLEMENTED_call(): +with hmm_but_this_should_get_two_preceding_newlines(): pass ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function_trailing_comma.py.snap index 2a55c63e83..747b512442 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function_trailing_comma.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function_trailing_comma.py.snap @@ -73,20 +73,16 @@ some_module.some_function( ```diff --- Black +++ Ruff -@@ -24,18 +24,14 @@ - def f( - a: int = 1, - ): -- call( -- arg={ -- "explode": "this", +@@ -27,7 +27,7 @@ + call( + arg={ + "explode": "this", - } -- ) -- call2( -- arg=[1, 2, 3], -- ) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ }, + ) + call2( + arg=[1, 2, 3], +@@ -35,7 +35,9 @@ x = { "a": 1, "b": 2, @@ -97,7 +93,7 @@ some_module.some_function( if ( a == { -@@ -47,22 +43,24 @@ +@@ -47,22 +49,24 @@ "f": 6, "g": 7, "h": 8, @@ -129,7 +125,7 @@ some_module.some_function( } -@@ -80,35 +78,16 @@ +@@ -80,18 +84,14 @@ pass @@ -137,8 +133,9 @@ some_module.some_function( - also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( - this_shouldn_t_get_a_trailing_comma_too - ) --): -+def func() -> NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): ++def func() -> also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( ++ this_shouldn_t_get_a_trailing_comma_too + ): pass @@ -146,29 +143,11 @@ some_module.some_function( - also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( - this_shouldn_t_get_a_trailing_comma_too - ) --): -+def func() -> NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): ++def func() -> also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( ++ this_shouldn_t_get_a_trailing_comma_too + ): pass - - # Make sure inner one-element tuple won't explode --some_module.some_function( -- argument1, (one_element_tuple,), argument4, argument5, argument6 --) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - # Inner trailing comma causes outer to explode --some_module.some_function( -- argument1, -- ( -- one, -- two, -- ), -- argument4, -- argument5, -- argument6, --) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Ruff Output @@ -200,8 +179,14 @@ def f2( def f( a: int = 1, ): - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + call( + arg={ + "explode": "this", + }, + ) + call2( + arg=[1, 2, 3], + ) x = { "a": 1, "b": 2, @@ -254,19 +239,34 @@ def some_method_with_a_really_long_name( pass -def func() -> NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): +def func() -> also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( + this_shouldn_t_get_a_trailing_comma_too +): pass -def func() -> NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): +def func() -> also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( + this_shouldn_t_get_a_trailing_comma_too +): pass # Make sure inner one-element tuple won't explode -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +some_module.some_function( + argument1, (one_element_tuple,), argument4, argument5, argument6 +) # Inner trailing comma causes outer to explode -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +some_module.some_function( + argument1, + ( + one, + two, + ), + argument4, + argument5, + argument6, +) ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@power_op_spacing.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@power_op_spacing.py.snap index ea7c78d2a9..2a803075f6 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@power_op_spacing.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@power_op_spacing.py.snap @@ -75,84 +75,67 @@ return np.divide( ```diff --- Black +++ Ruff -@@ -12,52 +12,45 @@ - - - a = 5**~4 --b = 5 ** f() -+b = 5 ** NOT_IMPLEMENTED_call() +@@ -15,38 +15,38 @@ + b = 5 ** f() c = -(5**2) d = 5 ** f["hi"] -e = lazy(lambda **kwargs: 5) --f = f() ** 5 -+e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+f = NOT_IMPLEMENTED_call() ** 5 ++e = lazy(lambda x: True) + f = f() ** 5 g = a.b**c.d --h = 5 ** funcs.f() --i = funcs.f() ** 5 --j = super().name ** 5 + h = 5 ** funcs.f() + i = funcs.f() ** 5 + j = super().name ** 5 -k = [(2**idx, value) for idx, value in pairs] --l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) -+h = 5 ** NOT_IMPLEMENTED_call() -+i = NOT_IMPLEMENTED_call() ** 5 -+j = NOT_IMPLEMENTED_call().name ** 5 +k = [i for i in []] -+l = mod.weights_[0] == NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + 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) + o = settings(max_examples=10**6) -p = {(k, k**2): v**2 for k, v in pairs} -q = [10**i for i in range(6)] -+o = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} +q = [i for i in []] r = x**y a = 5.0**~4.0 --b = 5.0 ** f() -+b = 5.0 ** NOT_IMPLEMENTED_call() + b = 5.0 ** f() c = -(5.0**2.0) d = 5.0 ** f["hi"] -e = lazy(lambda **kwargs: 5) --f = f() ** 5.0 -+e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+f = NOT_IMPLEMENTED_call() ** 5.0 ++e = lazy(lambda x: True) + f = f() ** 5.0 g = a.b**c.d --h = 5.0 ** funcs.f() --i = funcs.f() ** 5.0 --j = super().name ** 5.0 + h = 5.0 ** funcs.f() + i = funcs.f() ** 5.0 + j = super().name ** 5.0 -k = [(2.0**idx, value) for idx, value in pairs] --l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) -+h = 5.0 ** NOT_IMPLEMENTED_call() -+i = NOT_IMPLEMENTED_call() ** 5.0 -+j = NOT_IMPLEMENTED_call().name ** 5.0 +k = [i for i in []] -+l = mod.weights_[0] == NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + 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) + o = settings(max_examples=10**6.0) -p = {(k, k**2): v**2.0 for k, v in pairs} -q = [10.5**i for i in range(6)] -+o = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} +q = [i for i in []] # WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873) --if hasattr(view, "sum_of_weights"): -- return np.divide( # type: ignore[no-any-return] -- view.variance, # type: ignore[union-attr] -- view.sum_of_weights, # type: ignore[union-attr] -- out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr] +@@ -55,9 +55,11 @@ + view.variance, # type: ignore[union-attr] + view.sum_of_weights, # type: ignore[union-attr] + out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr] - where=view.sum_of_weights**2 > view.sum_of_weights_squared, # type: ignore[union-attr] -- ) -+if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): -+ return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ where=view.sum_of_weights**2 ++ > view.sum_of_weights_squared, # type: ignore[union-attr] + ) --return np.divide( + return np.divide( - where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared, # type: ignore --) -+return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ where=view.sum_of_weights_of_weight_long**2 ++ > view.sum_of_weights_squared, # type: ignore + ) ``` ## Ruff Output @@ -172,48 +155,57 @@ def function_dont_replace_spaces(): a = 5**~4 -b = 5 ** NOT_IMPLEMENTED_call() +b = 5 ** f() c = -(5**2) d = 5 ** f["hi"] -e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -f = NOT_IMPLEMENTED_call() ** 5 +e = lazy(lambda x: True) +f = f() ** 5 g = a.b**c.d -h = 5 ** NOT_IMPLEMENTED_call() -i = NOT_IMPLEMENTED_call() ** 5 -j = NOT_IMPLEMENTED_call().name ** 5 +h = 5 ** funcs.f() +i = funcs.f() ** 5 +j = super().name ** 5 k = [i for i in []] -l = mod.weights_[0] == NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) m = [([2**63], [1, 2**63])] n = count <= 10**5 -o = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +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 []] r = x**y a = 5.0**~4.0 -b = 5.0 ** NOT_IMPLEMENTED_call() +b = 5.0 ** f() c = -(5.0**2.0) d = 5.0 ** f["hi"] -e = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -f = NOT_IMPLEMENTED_call() ** 5.0 +e = lazy(lambda x: True) +f = f() ** 5.0 g = a.b**c.d -h = 5.0 ** NOT_IMPLEMENTED_call() -i = NOT_IMPLEMENTED_call() ** 5.0 -j = NOT_IMPLEMENTED_call().name ** 5.0 +h = 5.0 ** funcs.f() +i = funcs.f() ** 5.0 +j = super().name ** 5.0 k = [i for i in []] -l = mod.weights_[0] == NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +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 = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +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 []] # WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873) -if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): - return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +if hasattr(view, "sum_of_weights"): + return np.divide( # type: ignore[no-any-return] + view.variance, # type: ignore[union-attr] + view.sum_of_weights, # type: ignore[union-attr] + out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr] + where=view.sum_of_weights**2 + > view.sum_of_weights_squared, # type: ignore[union-attr] + ) -return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +return np.divide( + where=view.sum_of_weights_of_weight_long**2 + > view.sum_of_weights_squared, # type: ignore +) ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@prefer_rhs_split_reformatted.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@prefer_rhs_split_reformatted.py.snap deleted file mode 100644 index 855222248f..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@prefer_rhs_split_reformatted.py.snap +++ /dev/null @@ -1,94 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/prefer_rhs_split_reformatted.py ---- -## Input - -```py -# Test cases separate from `prefer_rhs_split.py` that contains unformatted source. - -# Left hand side fits in a single line but will still be exploded by the -# magic trailing comma. -first_value, (m1, m2,), third_value = xxxxxx_yyyyyy_zzzzzz_wwwwww_uuuuuuu_vvvvvvvvvvv( - arg1, - arg2, -) - -# Make when when the left side of assignment plus the opening paren "... = (" is -# exactly line length limit + 1, it won't be split like that. -xxxxxxxxx_yyy_zzzzzzzz[xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)] = 1 -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -9,13 +9,10 @@ - m2, - ), - third_value, --) = xxxxxx_yyyyyy_zzzzzz_wwwwww_uuuuuuu_vvvvvvvvvvv( -- arg1, -- arg2, --) -+) = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - # Make when when the left side of assignment plus the opening paren "... = (" is - # exactly line length limit + 1, it won't be split like that. - xxxxxxxxx_yyy_zzzzzzzz[ -- xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1) -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - ] = 1 -``` - -## Ruff Output - -```py -# Test cases separate from `prefer_rhs_split.py` that contains unformatted source. - -# Left hand side fits in a single line but will still be exploded by the -# magic trailing comma. -( - first_value, - ( - m1, - m2, - ), - third_value, -) = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - -# Make when when the left side of assignment plus the opening paren "... = (" is -# exactly line length limit + 1, it won't be split like that. -xxxxxxxxx_yyy_zzzzzzzz[ - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg), NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -] = 1 -``` - -## Black Output - -```py -# Test cases separate from `prefer_rhs_split.py` that contains unformatted source. - -# Left hand side fits in a single line but will still be exploded by the -# magic trailing comma. -( - first_value, - ( - m1, - m2, - ), - third_value, -) = xxxxxx_yyyyyy_zzzzzz_wwwwww_uuuuuuu_vvvvvvvvvvv( - arg1, - arg2, -) - -# Make when when the left side of assignment plus the opening paren "... = (" is -# exactly line length limit + 1, it won't be split like that. -xxxxxxxxx_yyy_zzzzzzzz[ - xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1) -] = 1 -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_await_parens.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_await_parens.py.snap index d702a291b9..e77d6934e3 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_await_parens.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_await_parens.py.snap @@ -93,31 +93,28 @@ async def main(): ```diff --- Black +++ Ruff -@@ -1,66 +1,57 @@ +@@ -1,4 +1,4 @@ -import asyncio +NOT_YET_IMPLEMENTED_StmtImport # Control example - async def main(): -- await asyncio.sleep(1) -+ await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - +@@ -8,59 +8,70 @@ # Remove brackets for short coroutine/task async def main(): - await asyncio.sleep(1) -+ await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) ++ await (asyncio.sleep(1)) async def main(): - await asyncio.sleep(1) -+ await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) ++ await (asyncio.sleep(1)) async def main(): - await asyncio.sleep(1) -+ await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) ++ await (asyncio.sleep(1)) # Check comments @@ -125,7 +122,7 @@ async def main(): - await asyncio.sleep(1) # Hello + ( + await # Hello -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ asyncio.sleep(1) + ) @@ -133,14 +130,14 @@ async def main(): - await asyncio.sleep(1) # Hello + ( + await ( -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Hello ++ asyncio.sleep(1) # Hello + ) + ) async def main(): - await asyncio.sleep(1) # Hello -+ await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) # Hello ++ await (asyncio.sleep(1)) # Hello # Long lines @@ -153,8 +150,17 @@ async def main(): - asyncio.sleep(1), - asyncio.sleep(1), - asyncio.sleep(1), -- ) -+ await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ ( ++ await asyncio.gather( ++ asyncio.sleep(1), ++ asyncio.sleep(1), ++ asyncio.sleep(1), ++ asyncio.sleep(1), ++ asyncio.sleep(1), ++ asyncio.sleep(1), ++ asyncio.sleep(1), ++ ) + ) # Same as above but with magic trailing comma in function @@ -167,27 +173,27 @@ async def main(): - asyncio.sleep(1), - asyncio.sleep(1), - asyncio.sleep(1), -- ) -+ await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ ( ++ await asyncio.gather( ++ asyncio.sleep(1), ++ asyncio.sleep(1), ++ asyncio.sleep(1), ++ asyncio.sleep(1), ++ asyncio.sleep(1), ++ asyncio.sleep(1), ++ asyncio.sleep(1), ++ ) + ) # Cr@zY Br@ck3Tz async def main(): - await black(1) -+ await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) ++ await (black(1)) # Keep brackets around non power operations and nested awaits -@@ -69,7 +60,7 @@ - - - async def main(): -- await (await asyncio.sleep(1)) -+ await (await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) - - - # It's awaits all the way down... -@@ -78,16 +69,16 @@ +@@ -78,16 +89,16 @@ async def main(): @@ -197,12 +203,12 @@ async def main(): async def main(): - await (await asyncio.sleep(1)) -+ await (await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg))) ++ await (await (asyncio.sleep(1))) async def main(): - await (await (await (await (await asyncio.sleep(1))))) -+ await (await (await (await (await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)))))) ++ await (await (await (await (await (asyncio.sleep(1)))))) async def main(): @@ -218,55 +224,75 @@ NOT_YET_IMPLEMENTED_StmtImport # Control example async def main(): - await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + await asyncio.sleep(1) # Remove brackets for short coroutine/task async def main(): - await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) + await (asyncio.sleep(1)) async def main(): - await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) + await (asyncio.sleep(1)) async def main(): - await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) + await (asyncio.sleep(1)) # Check comments async def main(): ( await # Hello - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + asyncio.sleep(1) ) async def main(): ( await ( - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # Hello + asyncio.sleep(1) # Hello ) ) async def main(): - await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) # Hello + await (asyncio.sleep(1)) # Hello # Long lines async def main(): - await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + ( + await asyncio.gather( + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + ) + ) # Same as above but with magic trailing comma in function async def main(): - await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + ( + await asyncio.gather( + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + ) + ) # Cr@zY Br@ck3Tz async def main(): - await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) + await (black(1)) # Keep brackets around non power operations and nested awaits @@ -275,7 +301,7 @@ async def main(): async def main(): - await (await NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)) + await (await asyncio.sleep(1)) # It's awaits all the way down... @@ -288,11 +314,11 @@ async def main(): async def main(): - await (await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg))) + await (await (asyncio.sleep(1))) async def main(): - await (await (await (await (await (NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)))))) + await (await (await (await (await (asyncio.sleep(1)))))) async def main(): diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_for_brackets.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_for_brackets.py.snap index 87a792c38b..a6a1c1c29f 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_for_brackets.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_for_brackets.py.snap @@ -31,71 +31,56 @@ for (((((k, v))))) in d.items(): ```diff --- Black +++ Ruff -@@ -1,27 +1,22 @@ - # Only remove tuple brackets after `for` --for k, v in d.items(): -- print(k, v) -+for k, v in NOT_IMPLEMENTED_call(): -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - +@@ -5,7 +5,7 @@ # Don't touch tuple brackets after `in` for module in (core, _unicodefun): -- if hasattr(module, "_verify_python3_env"): + if hasattr(module, "_verify_python3_env"): - module._verify_python3_env = lambda: None -+ if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): + module._verify_python3_env = lambda x: True # Brackets remain for long for loop lines for ( - why_would_anyone_choose_to_name_a_loop_variable_with_a_name_this_long, - i_dont_know_but_we_should_still_check_the_behaviour_if_they_do, --) in d.items(): -- print(k, v) -+) in NOT_IMPLEMENTED_call(): -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - --for ( -- k, -- v, +@@ -17,9 +17,7 @@ + for ( + k, + v, -) in ( - dfkasdjfldsjflkdsjflkdsjfdslkfjldsjfgkjdshgkljjdsfldgkhsdofudsfudsofajdslkfjdslkfjldisfjdffjsdlkfjdlkjjkdflskadjldkfjsalkfjdasj.items() -): -- print(k, v) -+for k, v in NOT_IMPLEMENTED_call(): -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++) in dfkasdjfldsjflkdsjflkdsjfdslkfjldsjfgkjdshgkljjdsfldgkhsdofudsfudsofajdslkfjdslkfjldisfjdffjsdlkfjdlkjjkdflskadjldkfjsalkfjdasj.items(): + print(k, v) # Test deeply nested brackets --for k, v in d.items(): -- print(k, v) -+for k, v in NOT_IMPLEMENTED_call(): -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ``` ## Ruff Output ```py # Only remove tuple brackets after `for` -for k, v in NOT_IMPLEMENTED_call(): - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +for k, v in d.items(): + print(k, v) # Don't touch tuple brackets after `in` for module in (core, _unicodefun): - if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): + if hasattr(module, "_verify_python3_env"): module._verify_python3_env = lambda x: True # Brackets remain for long for loop lines for ( why_would_anyone_choose_to_name_a_loop_variable_with_a_name_this_long, i_dont_know_but_we_should_still_check_the_behaviour_if_they_do, -) in NOT_IMPLEMENTED_call(): - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +) in d.items(): + print(k, v) -for k, v in NOT_IMPLEMENTED_call(): - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +for ( + k, + v, +) in dfkasdjfldsjflkdsjflkdsjfdslkfjldsjfgkjdshgkljjdsfldgkhsdofudsfudsofajdslkfjdslkfjldisfjdffjsdlkfjdlkjjkdflskadjldkfjsalkfjdasj.items(): + print(k, v) # Test deeply nested brackets -for k, v in NOT_IMPLEMENTED_call(): - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +for k, v in d.items(): + print(k, v) ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_newline_after_code_block_open.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_newline_after_code_block_open.py.snap index fef152f5d3..f9095f8b6f 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_newline_after_code_block_open.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_newline_after_code_block_open.py.snap @@ -120,116 +120,32 @@ with open("/path/to/file.txt", mode="r") as read_file: ```diff --- Black +++ Ruff -@@ -1,78 +1,78 @@ +@@ -1,4 +1,4 @@ -import random +NOT_YET_IMPLEMENTED_StmtImport def foo1(): -- print("The newline above me should be deleted!") -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +@@ -27,16 +27,16 @@ - def foo2(): -- print("All the newlines above me should be deleted!") -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - - def foo3(): -- print("No newline above me!") -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - -- print("There is a newline above me, and that's OK!") -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - - def foo4(): - # There is a comment here - -- print("The newline above me should not be deleted!") -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - - class Foo: - def bar(self): -- print("The newline above me should be deleted!") -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - --for i in range(5): + for i in range(5): - print(f"{i}) The line above me should be removed!") -+for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ print(NOT_YET_IMPLEMENTED_ExprJoinedStr) --for i in range(5): + for i in range(5): - print(f"{i}) The lines above me should be removed!") -+for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ print(NOT_YET_IMPLEMENTED_ExprJoinedStr) --for i in range(5): -- for j in range(7): + for i in range(5): + for j in range(7): - print(f"{i}) The lines above me should be removed!") -+for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): -+ for j in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ print(NOT_YET_IMPLEMENTED_ExprJoinedStr) --if random.randint(0, 3) == 0: -- print("The new line above me is about to be removed!") -+if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) == 0: -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - --if random.randint(0, 3) == 0: -- print("The new lines above me is about to be removed!") -+if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) == 0: -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - --if random.randint(0, 3) == 0: -- if random.uniform(0, 1) > 0.5: -- print("Two lines above me are about to be removed!") -+if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) == 0: -+ if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) > 0.5: -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - - while True: -- print("The newline above me should be deleted!") -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - - while True: -- print("The newlines above me should be deleted!") -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - - while True: - while False: -- print("The newlines above me should be deleted!") -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - --with open("/path/to/file.txt", mode="w") as file: -- file.write("The new line above me is about to be removed!") -+with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) as file: -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - --with open("/path/to/file.txt", mode="w") as file: -- file.write("The new lines above me is about to be removed!") -+with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) as file: -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - --with open("/path/to/file.txt", mode="r") as read_file: -- with open("/path/to/output_file.txt", mode="w") as write_file: -- write_file.writelines(read_file.readlines()) -+with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) as read_file: -+ with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) as write_file: -+ NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + if random.randint(0, 3) == 0: ``` ## Ruff Output @@ -239,80 +155,80 @@ NOT_YET_IMPLEMENTED_StmtImport def foo1(): - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("The newline above me should be deleted!") def foo2(): - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("All the newlines above me should be deleted!") def foo3(): - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("No newline above me!") - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("There is a newline above me, and that's OK!") def foo4(): # There is a comment here - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("The newline above me should not be deleted!") class Foo: def bar(self): - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("The newline above me should be deleted!") -for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +for i in range(5): + print(NOT_YET_IMPLEMENTED_ExprJoinedStr) -for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +for i in range(5): + print(NOT_YET_IMPLEMENTED_ExprJoinedStr) -for i in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): - for j in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +for i in range(5): + for j in range(7): + print(NOT_YET_IMPLEMENTED_ExprJoinedStr) -if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) == 0: - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +if random.randint(0, 3) == 0: + print("The new line above me is about to be removed!") -if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) == 0: - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +if random.randint(0, 3) == 0: + print("The new lines above me is about to be removed!") -if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) == 0: - if NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) > 0.5: - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +if random.randint(0, 3) == 0: + if random.uniform(0, 1) > 0.5: + print("Two lines above me are about to be removed!") while True: - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("The newline above me should be deleted!") while True: - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("The newlines above me should be deleted!") while True: while False: - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("The newlines above me should be deleted!") -with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) as file: - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +with open("/path/to/file.txt", mode="w") as file: + file.write("The new line above me is about to be removed!") -with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) as file: - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +with open("/path/to/file.txt", mode="w") as file: + file.write("The new lines above me is about to be removed!") -with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) as read_file: - with NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) as write_file: - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +with open("/path/to/file.txt", mode="r") as read_file: + with open("/path/to/output_file.txt", mode="w") as write_file: + write_file.writelines(read_file.readlines()) ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_parens.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_parens.py.snap index 1ed5be6a16..bb59ae6651 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_parens.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_parens.py.snap @@ -67,15 +67,7 @@ def example8(): ```diff --- Black +++ Ruff -@@ -1,20 +1,12 @@ - x = 1 - x = 1.2 - --data = ( -- "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" --).encode() -+data = NOT_IMPLEMENTED_call() - +@@ -8,13 +8,7 @@ async def show_status(): while True: @@ -90,7 +82,7 @@ def example8(): def example(): -@@ -30,15 +22,11 @@ +@@ -30,15 +24,11 @@ def example2(): @@ -108,7 +100,7 @@ def example8(): def example4(): -@@ -50,35 +38,11 @@ +@@ -50,35 +40,11 @@ def example6(): @@ -154,7 +146,9 @@ def example8(): x = 1 x = 1.2 -data = NOT_IMPLEMENTED_call() +data = ( + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +).encode() async def show_status(): diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@skip_magic_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@skip_magic_trailing_comma.py.snap index ae72b77fdb..d87ef4d8a0 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@skip_magic_trailing_comma.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@skip_magic_trailing_comma.py.snap @@ -59,7 +59,7 @@ func( ```diff --- Black +++ Ruff -@@ -1,25 +1,39 @@ +@@ -1,25 +1,58 @@ # We should not remove the trailing comma in a single-element subscript. -a: tuple[int,] -b = tuple[int,] @@ -95,10 +95,20 @@ func( # Trailing commas in multiple chained non-nested parens. -zero(one).two(three).four(five) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++zero( ++ one, ++).two( ++ three, ++).four( ++ five, ++) -func1(arg1).func2(arg2).func3(arg3).func4(arg4).func5(arg5) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++func1(arg1).func2( ++ arg2, ++).func3(arg3).func4( ++ arg4, ++).func5(arg5) -(a, b, c, d) = func1(arg1) and func2(arg2) +( @@ -106,10 +116,19 @@ func( + b, + c, + d, -+) = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) and NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++) = func1(arg1) and func2(arg2) -func(argument1, (one, two), argument4, argument5, argument6) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++func( ++ argument1, ++ ( ++ one, ++ two, ++ ), ++ argument4, ++ argument5, ++ argument6, ++) ``` ## Ruff Output @@ -142,18 +161,37 @@ set_of_types = {tuple[(int,)]} small_tuple = (1,) # Trailing commas in multiple chained non-nested parens. -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +zero( + one, +).two( + three, +).four( + five, +) -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +func1(arg1).func2( + arg2, +).func3(arg3).func4( + arg4, +).func5(arg5) ( a, b, c, d, -) = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) and NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +) = func1(arg1) and func2(arg2) -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +func( + argument1, + ( + one, + two, + ), + argument4, + argument5, + argument6, +) ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@slices.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@slices.py.snap index ec47eb18f9..699c559a0b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@slices.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@slices.py.snap @@ -75,7 +75,7 @@ x[ ```diff --- Black +++ Ruff -@@ -4,30 +4,35 @@ +@@ -4,30 +4,30 @@ slice[d::d] slice[0] slice[-1] @@ -111,16 +111,11 @@ x[ ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] -ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] -+( -+ ham[ -+ : NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) : NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) -+ ], -+ ham[ :: NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)], -+) ++ham[ : upper_fn(x) : step_fn(x)], ham[ :: step_fn(x)] ham[lower + offset : upper + offset] slice[::, ::] -@@ -50,10 +55,14 @@ +@@ -50,10 +50,14 @@ slice[ # A 1 @@ -172,12 +167,7 @@ async def f(): ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:] ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] -( - ham[ - : NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) : NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - ], - ham[ :: NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)], -) +ham[ : upper_fn(x) : step_fn(x)], ham[ :: step_fn(x)] ham[lower + offset : upper + offset] slice[::, ::] diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@torture.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@torture.py.snap index ec5eeae618..246fcc69bc 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@torture.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@torture.py.snap @@ -64,16 +64,14 @@ assert ( importA 0 -@@ -24,35 +14,34 @@ - +@@ -25,34 +15,33 @@ class A: def foo(self): -- for _ in range(10): + for _ in range(10): - aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc( - xxxxxxxxxxxx - ) # pylint: disable=no-member -+ for _ in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): -+ aaaaaaaaaaaaaaaaaaa = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc(xxxxxxxxxxxx) # pylint: disable=no-member def test(self, othr): @@ -146,8 +144,8 @@ importA class A: def foo(self): - for _ in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg): - aaaaaaaaaaaaaaaaaaa = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + for _ in range(10): + aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc(xxxxxxxxxxxx) # pylint: disable=no-member def test(self, othr): diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens1.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens1.py.snap index 177fde22db..35f9fc85aa 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens1.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens1.py.snap @@ -37,7 +37,7 @@ class A: ```diff --- Black +++ Ruff -@@ -1,34 +1,32 @@ +@@ -1,18 +1,16 @@ -if e1234123412341234.winerror not in ( - _winapi.ERROR_SEM_TIMEOUT, - _winapi.ERROR_PIPE_BUSY, @@ -45,7 +45,7 @@ class A: +if ( + e1234123412341234.winerror + not in (_winapi.ERROR_SEM_TIMEOUT, _winapi.ERROR_PIPE_BUSY) -+ or NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++ or _check_timeout(t) +): pass @@ -58,20 +58,14 @@ class A: - ) - + 1 - ) -+ new_id = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + 1 ++ new_id = max( ++ Vegetable.objects.order_by("-id")[0].id, ++ Mineral.objects.order_by("-id")[0].id, ++ ) + 1 class X: - def get_help_text(self): -- return ngettext( -- "Your password must contain at least %(min_length)d character.", -- "Your password must contain at least %(min_length)d characters.", -- self.min_length, -- ) % {"min_length": self.min_length} -+ return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) % { -+ "min_length": self.min_length, -+ } - +@@ -26,9 +24,14 @@ class A: def b(self): @@ -99,20 +93,25 @@ class A: if ( e1234123412341234.winerror not in (_winapi.ERROR_SEM_TIMEOUT, _winapi.ERROR_PIPE_BUSY) - or NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + or _check_timeout(t) ): pass if x: if y: - new_id = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + 1 + new_id = max( + Vegetable.objects.order_by("-id")[0].id, + Mineral.objects.order_by("-id")[0].id, + ) + 1 class X: def get_help_text(self): - return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) % { - "min_length": self.min_length, - } + return ngettext( + "Your password must contain at least %(min_length)d character.", + "Your password must contain at least %(min_length)d characters.", + self.min_length, + ) % {"min_length": self.min_length} class A: diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens2.py.snap index c4dddaa5b1..bc155f8982 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens2.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens2.py.snap @@ -22,8 +22,8 @@ if (e123456.get_tk_patchlevel() >= (8, 6, 0, 'final') or - 8, -) <= get_tk_patchlevel() < (8, 6): +if ( -+ NOT_IMPLEMENTED_call() >= (8, 6, 0, "final") -+ or (8, 5, 8) <= NOT_IMPLEMENTED_call() < (8, 6) ++ e123456.get_tk_patchlevel() >= (8, 6, 0, "final") ++ or (8, 5, 8) <= get_tk_patchlevel() < (8, 6) +): pass ``` @@ -32,8 +32,8 @@ if (e123456.get_tk_patchlevel() >= (8, 6, 0, 'final') or ```py if ( - NOT_IMPLEMENTED_call() >= (8, 6, 0, "final") - or (8, 5, 8) <= NOT_IMPLEMENTED_call() < (8, 6) + e123456.get_tk_patchlevel() >= (8, 6, 0, "final") + or (8, 5, 8) <= get_tk_patchlevel() < (8, 6) ): pass ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens3.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens3.py.snap deleted file mode 100644 index 3a574ac557..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens3.py.snap +++ /dev/null @@ -1,63 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_comma_optional_parens3.py ---- -## Input - -```py -if True: - if True: - if True: - return _( - "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas " - + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", - "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", - ) % {"reported_username": reported_username, "report_reason": report_reason} -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,8 +1,7 @@ - if True: - if True: - if True: -- return _( -- "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas " -- + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", -- "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", -- ) % {"reported_username": reported_username, "report_reason": report_reason} -+ return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) % { -+ "reported_username": reported_username, -+ "report_reason": report_reason, -+ } -``` - -## Ruff Output - -```py -if True: - if True: - if True: - return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) % { - "reported_username": reported_username, - "report_reason": report_reason, - } -``` - -## Black Output - -```py -if True: - if True: - if True: - return _( - "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas " - + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", - "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", - ) % {"reported_username": reported_username, "report_reason": report_reason} -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_commas_in_leading_parts.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_commas_in_leading_parts.py.snap index e799084c77..b50a6c5ff7 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_commas_in_leading_parts.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_commas_in_leading_parts.py.snap @@ -45,47 +45,29 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( ```diff --- Black +++ Ruff -@@ -1,50 +1,26 @@ --zero( -- one, --).two( -- three, --).four( -- five, --) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - --func1(arg1).func2( -- arg2, --).func3(arg3).func4( -- arg4, --).func5(arg5) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - # Inner one-element tuple shouldn't explode --func1(arg1).func2(arg1, (one_tuple,)).func3(arg3) -+NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - - ( - a, +@@ -20,9 +20,7 @@ b, c, d, -) = func1( - arg1 -) and func2(arg2) -+) = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) and NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++) = func1(arg1) and func2(arg2) # Example from https://github.com/psf/black/issues/3229 - def refresh_token(self, device_family, refresh_token, api_key): -- return self.orchestration.refresh_token( -- data={ -- "refreshToken": refresh_token, -- }, -- api_key=api_key, +@@ -32,19 +30,18 @@ + "refreshToken": refresh_token, + }, + api_key=api_key, - )["extensions"]["sdk"]["token"] -+ return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)["extensions"]["sdk"]["token"] ++ )[ ++ "extensions" ++ ][ ++ "sdk" ++ ][ ++ "token" ++ ] # Edge case where a bug in a working-in-progress version of @@ -108,24 +90,45 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( ## Ruff Output ```py -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +zero( + one, +).two( + three, +).four( + five, +) -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +func1(arg1).func2( + arg2, +).func3(arg3).func4( + arg4, +).func5(arg5) # Inner one-element tuple shouldn't explode -NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +func1(arg1).func2(arg1, (one_tuple,)).func3(arg3) ( a, b, c, d, -) = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) and NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +) = func1(arg1) and func2(arg2) # Example from https://github.com/psf/black/issues/3229 def refresh_token(self, device_family, refresh_token, api_key): - return NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)["extensions"]["sdk"]["token"] + return self.orchestration.refresh_token( + data={ + "refreshToken": refresh_token, + }, + api_key=api_key, + )[ + "extensions" + ][ + "sdk" + ][ + "token" + ] # Edge case where a bug in a working-in-progress version of diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@tupleassign.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@tupleassign.py.snap index d54c56272e..6e9dbd02a2 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@tupleassign.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@tupleassign.py.snap @@ -28,10 +28,9 @@ this_will_be_wrapped_in_parens, = struct.unpack(b"12345678901234567890") # This is as well. -(this_will_be_wrapped_in_parens,) = struct.unpack(b"12345678901234567890") -+(this_will_be_wrapped_in_parens,) = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) ++(this_will_be_wrapped_in_parens,) = struct.unpack(b"NOT_YET_IMPLEMENTED_BYTE_STRING") --(a,) = call() -+(a,) = NOT_IMPLEMENTED_call() + (a,) = call() ``` ## Ruff Output @@ -46,9 +45,9 @@ this_will_be_wrapped_in_parens, = struct.unpack(b"12345678901234567890") ) = 1, 2, 3 # This is as well. -(this_will_be_wrapped_in_parens,) = NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +(this_will_be_wrapped_in_parens,) = struct.unpack(b"NOT_YET_IMPLEMENTED_BYTE_STRING") -(a,) = NOT_IMPLEMENTED_call() +(a,) = call() ``` ## Black Output 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 77901f7b5c..bbff73657c 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 @@ -113,31 +113,30 @@ x53 = ( ```py NOT_YET_IMPLEMENTED_StmtImportFrom -a = NOT_IMPLEMENTED_call() +a = Namespace() ( - a + a. # comment - .b # trailing comment + b # trailing comment ) ( - a + a. # comment - .b # trailing dot comment # trailing identifier comment + b # trailing dot comment # trailing identifier comment ) ( - a + a. # comment - .b # trailing identifier comment + b # trailing identifier comment ) ( - a + a. # comment - . # in between b # trailing dot comment # trailing identifier comment ) @@ -150,9 +149,8 @@ a.aaaaaaaaaaaaaaaaaaaaa.lllllllllllllllllllllllllllloooooooooong.chaaaaaaaaaaaaa # chain if and only if we need them, that is if there are own line comments inside # the chain. x1 = ( - a.b + a.b. # comment 1 - . # comment 3 c.d # comment 2 ) @@ -163,15 +161,15 @@ x21 = ( a.b # trailing name end-of-line ) x22 = ( - a + a. # outermost leading own line - .b # outermost trailing end-of-line + b # outermost trailing end-of-line ) x31 = ( - a + a. # own line between nodes 1 - .b + b ) x321 = a.b # end-of-line dot comment x322 = a.b.c # end-of-line dot comment 2 @@ -181,9 +179,9 @@ x331 = ( b ) x332 = ( - "" + "". # own line between nodes - .find + find ) x8 = (a + a).b 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 84c5af2a92..59fcae973c 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 @@ -217,7 +217,9 @@ for user_id in set(target_user_ids) - {u.user_id for u in updates}: # Black breaks the right side first for the following expressions: -aaaaaaaaaaaaaa + NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +aaaaaaaaaaaaaa + caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaal( + argument1, argument2, argument3 +) aaaaaaaaaaaaaa + [ bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, @@ -438,10 +440,10 @@ if ( # Unstable formatting in https://github.com/realtyem/synapse-unraid/blob/unraid_develop/synapse/handlers/presence.py -for ( - user_id -) in NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) - {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set}: - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +for user_id in set( + target_user_ids +) - {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set}: + updates.append(UserPresenceState.default(user_id)) ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__boolean_operation.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__boolean_operation.py.snap index ab8e33067f..979f6fb805 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__boolean_operation.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__boolean_operation.py.snap @@ -78,17 +78,17 @@ if ( and self._returncode # the child process has finished, but the # transport hasn't been notified yet? - and NOT_IMPLEMENTED_call() + and self._proc.poll() ): pass if ( self._proc and self._returncode - and NOT_IMPLEMENTED_call() + and self._proc.poll() and self._proc and self._returncode - and NOT_IMPLEMENTED_call() + and self._proc.poll() ): ... diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__slice.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__slice.py.snap index ffc644c26b..1ea479d8c8 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__slice.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__slice.py.snap @@ -159,17 +159,17 @@ def a(): e00 = "e"[:] e01 = "e"[:1] -e02 = "e"[ : NOT_IMPLEMENTED_call()] +e02 = "e"[ : a()] e10 = "e"[1:] e11 = "e"[1:1] -e12 = "e"[1 : NOT_IMPLEMENTED_call()] -e20 = "e"[NOT_IMPLEMENTED_call() :] -e21 = "e"[NOT_IMPLEMENTED_call() : 1] -e22 = "e"[NOT_IMPLEMENTED_call() : NOT_IMPLEMENTED_call()] -e200 = "e"[NOT_IMPLEMENTED_call() : :] -e201 = "e"[NOT_IMPLEMENTED_call() :: 1] -e202 = "e"[NOT_IMPLEMENTED_call() :: NOT_IMPLEMENTED_call()] -e210 = "e"[NOT_IMPLEMENTED_call() : 1 :] +e12 = "e"[1 : a()] +e20 = "e"[a() :] +e21 = "e"[a() : 1] +e22 = "e"[a() : a()] +e200 = "e"[a() : :] +e201 = "e"[a() :: 1] +e202 = "e"[a() :: a()] +e210 = "e"[a() : 1 :] ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__call.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__call.py.snap new file mode 100644 index 0000000000..7c40b005b6 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__call.py.snap @@ -0,0 +1,175 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/call.py +--- +## Input +```py +from unittest.mock import MagicMock + + +def f(*args, **kwargs): + pass + +this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd = 1 +session = MagicMock() +models = MagicMock() + +f() + +f(1) + +f(x=2) + +f(1, x=2) + +f( + 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 +) + +f( + 1, + mixed_very_long_arguments=1, +) + +f( + this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd, + these_arguments_have_values_that_need_to_break_because_they_are_too_long1=(100000 - 100000000000), + these_arguments_have_values_that_need_to_break_because_they_are_too_long2="akshfdlakjsdfad" + "asdfasdfa", + these_arguments_have_values_that_need_to_break_because_they_are_too_long3=session, +) + +f( + # dangling comment +) + + +f( + only=1, short=1, arguments=1 +) + +f( + hey_this_is_a_long_call, it_has_funny_attributes_that_breaks_into_three_lines=1 +) + +f( + hey_this_is_a_very_long_call=1, it_has_funny_attributes_asdf_asdf=1, too_long_for_the_line=1, really=True +) + +# TODO(konstin): Call chains/fluent interface (https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#call-chains) +result = ( + session.query(models.Customer.id) + .filter( + models.Customer.account_id == 10000, + models.Customer.email == "user@example.org", + ) + .order_by(models.Customer.id.asc()) + .all() +) +# TODO(konstin): Black has this special case for comment placement where everything stays in one line +f( + "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa" +) + +f( + session, + b=1, + ** # oddly placed end-of-line comment + dict() +) +f( + session, + b=1, + ** + # oddly placed own line comment + dict() +) + +``` + +## Output +```py +NOT_YET_IMPLEMENTED_StmtImportFrom + + +def f(*args, **kwargs): + pass + + +this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd = 1 +session = MagicMock() +models = MagicMock() + +f() + +f(1) + +f(x=2) + +f(1, x=2) + +f( + 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, +) + +f( + 1, + mixed_very_long_arguments=1, +) + +f( + this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd, + these_arguments_have_values_that_need_to_break_because_they_are_too_long1=( + 100000 + - 100000000000 + ), + these_arguments_have_values_that_need_to_break_because_they_are_too_long2="akshfdlakjsdfad" + + "asdfasdfa", + these_arguments_have_values_that_need_to_break_because_they_are_too_long3=session, +) + +f() +# dangling comment + + +f(only=1, short=1, arguments=1) + +f(hey_this_is_a_long_call, it_has_funny_attributes_that_breaks_into_three_lines=1) + +f( + hey_this_is_a_very_long_call=1, + it_has_funny_attributes_asdf_asdf=1, + too_long_for_the_line=1, + really=True, +) + +# TODO(konstin): Call chains/fluent interface (https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#call-chains) +result = session.query(models.Customer.id).filter( + models.Customer.account_id + == 10000, + models.Customer.email + == "user@example.org", +).order_by(models.Customer.id.asc()).all() +# TODO(konstin): Black has this special case for comment placement where everything stays in one line +f("aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa") + +f( + session, + b=1, + **dict(), # oddly placed end-of-line comment +) +f( + session, + b=1, + **dict(), + # oddly placed own line comment +) +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__while.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__while.py.snap index 3aa590915d..9014bcdd53 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__while.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__while.py.snap @@ -59,16 +59,16 @@ while ( else: ... -while NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) and anotherCondition or aThirdCondition: # comment - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) +while some_condition(unformatted, args) and anotherCondition or aThirdCondition: # comment + print("Do something") while ( - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) # trailing some condition + some_condition(unformatted, args) # trailing some condition and anotherCondition or aThirdCondition # trailing third condition ): # comment - NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg) + print("Do something") ``` diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index d1ade6486a..6b24114ad4 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -24,8 +24,8 @@ ruff_python_ast = { path = "../crates/ruff_python_ast" } ruff_python_formatter = { path = "../crates/ruff_python_formatter" } similar = { version = "2.2.1" } -# Current tag: v0.0.6 -rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8078663b6c914c1cb86993e427764f7c422fc12c" , default-features = false, features = ["full-lexer", "num-bigint"] } +# Current tag: v0.0.7 +rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" , default-features = false, features = ["full-lexer", "num-bigint"] } # Prevent this from interfering with workspaces [workspace] From 520f4f33c33e5ab7182325d71dc9905f46f4af2a Mon Sep 17 00:00:00 2001 From: konstin Date: Tue, 27 Jun 2023 15:29:20 +0200 Subject: [PATCH 244/447] Fix ruff_dev repeat by removing short argument (#5388) ruff_dev repeat recently broke (i think with the cargo update?): > thread 'main' panicked at 'Command repeat: Short option names must be unique for each argument, but '-n' is in use by both 'no_cache' and 'repeat'' This fixes this by removing the short argument. --- crates/ruff_dev/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_dev/src/main.rs b/crates/ruff_dev/src/main.rs index 0327211a20..243fceb34b 100644 --- a/crates/ruff_dev/src/main.rs +++ b/crates/ruff_dev/src/main.rs @@ -60,7 +60,7 @@ enum Command { #[clap(flatten)] log_level_args: ruff_cli::args::LogLevelArgs, /// Run this many times - #[clap(long, short = 'n')] + #[clap(long)] repeat: usize, }, /// Format a repository twice and ensure that it looks that the first and second formatting From 502e15585dc4d16fb39f0ce589143a3d2db674b1 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 27 Jun 2023 11:33:42 -0400 Subject: [PATCH 245/447] Ignore unpacking in `iteration-over-set` (#5392) Closes #5386. --- .../test/fixtures/pylint/iteration_over_set.py | 3 +++ .../src/rules/pylint/rules/iteration_over_set.rs | 16 +++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pylint/iteration_over_set.py b/crates/ruff/resources/test/fixtures/pylint/iteration_over_set.py index 1b22612e52..901dd3f196 100644 --- a/crates/ruff/resources/test/fixtures/pylint/iteration_over_set.py +++ b/crates/ruff/resources/test/fixtures/pylint/iteration_over_set.py @@ -36,3 +36,6 @@ for item in set(("apples", "lemons", "water")): # set constructor is fine for number in {i for i in range(10)}: # set comprehensions are fine print(number) + +for item in {*numbers_set, 4, 5, 6}: # set unpacking is fine + print(f"I like {item}.") diff --git a/crates/ruff/src/rules/pylint/rules/iteration_over_set.rs b/crates/ruff/src/rules/pylint/rules/iteration_over_set.rs index b98806c7c8..38725f7e4f 100644 --- a/crates/ruff/src/rules/pylint/rules/iteration_over_set.rs +++ b/crates/ruff/src/rules/pylint/rules/iteration_over_set.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{Expr, Ranged}; +use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -38,9 +38,15 @@ impl Violation for IterationOverSet { /// PLC0208 pub(crate) fn iteration_over_set(checker: &mut Checker, expr: &Expr) { - if expr.is_set_expr() { - checker - .diagnostics - .push(Diagnostic::new(IterationOverSet, expr.range())); + let Expr::Set(ast::ExprSet { elts, .. }) = expr else { + return; + }; + + if elts.iter().any(Expr::is_starred_expr) { + return; } + + checker + .diagnostics + .push(Diagnostic::new(IterationOverSet, expr.range())); } From 1ed227a1e0dc18a0e12f90d10757a89883a9eb01 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 27 Jun 2023 12:15:07 -0400 Subject: [PATCH 246/447] Port Pyright's import resolver to Rust (#5381) ## Summary This PR contains the first step towards enabling robust first-party, third-party, and standard library import resolution in Ruff (including support for `typeshed`, stub files, native modules, etc.) by porting Pyright's import resolver to Rust. The strategy taken here was to start with a more-or-less direct port of the Pyright's TypeScript resolver. The code is intentionally similar, and the test suite is effectively a superset of Pyright's test suite for its own resolver. Due to the nature of the port, the code is very, very non-idiomatic for Rust. The code is also entirely unused outside of the test suite, and no effort has been made to integrate it with the rest of the codebase. Future work will include: - Refactoring the code (now that it works) to match Rust and Ruff idioms. - Further testing, in practice, to ensure that the resolver can resolve imports in a complex project, when provided with a virtual environment path. - Caching, to minimize filesystem lookups and redundant resolutions. - Integration into Ruff itself (use Ruff's existing settings, find rules that can make use of robust resolution, etc.) --- Cargo.lock | 37 + LICENSE | 26 + README.md | 4 +- crates/ruff_python_resolver/Cargo.toml | 21 + crates/ruff_python_resolver/src/config.rs | 26 + .../src/execution_environment.rs | 19 + crates/ruff_python_resolver/src/host.rs | 43 + .../src/implicit_imports.rs | 150 ++ .../ruff_python_resolver/src/import_result.rs | 122 ++ crates/ruff_python_resolver/src/lib.rs | 16 + .../src/module_descriptor.rs | 16 + .../ruff_python_resolver/src/native_module.rs | 14 + crates/ruff_python_resolver/src/py_typed.rs | 40 + .../src/python_platform.rs | 7 + .../src/python_version.rs | 24 + crates/ruff_python_resolver/src/resolver.rs | 1497 +++++++++++++++++ crates/ruff_python_resolver/src/search.rs | 282 ++++ 17 files changed, 2343 insertions(+), 1 deletion(-) create mode 100644 crates/ruff_python_resolver/Cargo.toml create mode 100644 crates/ruff_python_resolver/src/config.rs create mode 100644 crates/ruff_python_resolver/src/execution_environment.rs create mode 100644 crates/ruff_python_resolver/src/host.rs create mode 100644 crates/ruff_python_resolver/src/implicit_imports.rs create mode 100644 crates/ruff_python_resolver/src/import_result.rs create mode 100644 crates/ruff_python_resolver/src/lib.rs create mode 100644 crates/ruff_python_resolver/src/module_descriptor.rs create mode 100644 crates/ruff_python_resolver/src/native_module.rs create mode 100644 crates/ruff_python_resolver/src/py_typed.rs create mode 100644 crates/ruff_python_resolver/src/python_platform.rs create mode 100644 crates/ruff_python_resolver/src/python_version.rs create mode 100644 crates/ruff_python_resolver/src/resolver.rs create mode 100644 crates/ruff_python_resolver/src/search.rs diff --git a/Cargo.lock b/Cargo.lock index 287b62671a..57c612d87e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -674,6 +674,19 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "equivalent" version = "1.0.0" @@ -874,6 +887,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "iana-time-zone" version = "0.1.57" @@ -2071,6 +2090,15 @@ dependencies = [ "similar", ] +[[package]] +name = "ruff_python_resolver" +version = "0.0.0" +dependencies = [ + "env_logger", + "log", + "tempfile", +] + [[package]] name = "ruff_python_semantic" version = "0.0.0" @@ -2539,6 +2567,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + [[package]] name = "terminfo" version = "0.8.0" diff --git a/LICENSE b/LICENSE index 68a9d4958c..8ffd09c5f2 100644 --- a/LICENSE +++ b/LICENSE @@ -1224,6 +1224,32 @@ are: SOFTWARE. """ +- Pyright, licensed as follows: + """ + MIT License + + Pyright - A static type checker for the Python language + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + """ + - rust-analyzer/text-size, licensed under the MIT license: """ Permission is hereby granted, free of charge, to any diff --git a/README.md b/README.md index 5c77173aad..46749a3e84 100644 --- a/README.md +++ b/README.md @@ -330,9 +330,11 @@ We're grateful to the maintainers of these tools for their work, and for all the value they've provided to the Python community. Ruff's autoformatter is built on a fork of Rome's [`rome_formatter`](https://github.com/rome/tools/tree/main/crates/rome_formatter), -and again draws on both the APIs and implementation details of [Rome](https://github.com/rome/tools), +and again draws on both API and implementation details from [Rome](https://github.com/rome/tools), [Prettier](https://github.com/prettier/prettier), and [Black](https://github.com/psf/black). +Ruff's import resolver is based on the import resolution algorithm from [Pyright](https://github.com/microsoft/pyright). + Ruff is also influenced by a number of tools outside the Python ecosystem, like [Clippy](https://github.com/rust-lang/rust-clippy) and [ESLint](https://github.com/eslint/eslint). diff --git a/crates/ruff_python_resolver/Cargo.toml b/crates/ruff_python_resolver/Cargo.toml new file mode 100644 index 0000000000..2d454e88cb --- /dev/null +++ b/crates/ruff_python_resolver/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "ruff_python_resolver" +version = "0.0.0" +description = "A Python module resolver for Ruff" +publish = false +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +[lib] + +[dependencies] +log = { workspace = true } + +[dev-dependencies] +env_logger = "0.10.0" +tempfile = "3.6.0" diff --git a/crates/ruff_python_resolver/src/config.rs b/crates/ruff_python_resolver/src/config.rs new file mode 100644 index 0000000000..0ae2790683 --- /dev/null +++ b/crates/ruff_python_resolver/src/config.rs @@ -0,0 +1,26 @@ +use std::path::PathBuf; + +use crate::python_version::PythonVersion; + +pub(crate) struct Config { + /// Path to python interpreter. + pub(crate) python_path: Option, + + /// Path to use for typeshed definitions. + pub(crate) typeshed_path: Option, + + /// Path to custom typings (stub) modules. + pub(crate) stub_path: Option, + + /// Path to a directory containing one or more virtual environment + /// directories. This is used in conjunction with the "venv" name in + /// the config file to identify the python environment used for resolving + /// third-party modules. + pub(crate) venv_path: Option, + + /// Default venv environment. + pub(crate) venv: Option, + + /// Default Python version. Can be overridden by ExecutionEnvironment. + pub(crate) default_python_version: Option, +} diff --git a/crates/ruff_python_resolver/src/execution_environment.rs b/crates/ruff_python_resolver/src/execution_environment.rs new file mode 100644 index 0000000000..b969ddc42b --- /dev/null +++ b/crates/ruff_python_resolver/src/execution_environment.rs @@ -0,0 +1,19 @@ +use std::path::PathBuf; + +use crate::python_platform::PythonPlatform; +use crate::python_version::PythonVersion; + +#[derive(Debug)] +pub(crate) struct ExecutionEnvironment { + /// The root directory of the execution environment. + pub(crate) root: PathBuf, + + /// The Python version of the execution environment. + pub(crate) python_version: PythonVersion, + + /// The Python platform of the execution environment. + pub(crate) python_platform: PythonPlatform, + + /// The extra search paths of the execution environment. + pub(crate) extra_paths: Vec, +} diff --git a/crates/ruff_python_resolver/src/host.rs b/crates/ruff_python_resolver/src/host.rs new file mode 100644 index 0000000000..be9b0a5e60 --- /dev/null +++ b/crates/ruff_python_resolver/src/host.rs @@ -0,0 +1,43 @@ +//! Expose the host environment to the resolver. + +use std::path::PathBuf; + +use crate::python_platform::PythonPlatform; +use crate::python_version::PythonVersion; + +/// A trait to expose the host environment to the resolver. +pub(crate) trait Host { + /// The search paths to use when resolving Python modules. + fn python_search_paths(&self) -> Vec; + + /// The Python version to use when resolving Python modules. + fn python_version(&self) -> PythonVersion; + + /// The OS platform to use when resolving Python modules. + fn python_platform(&self) -> PythonPlatform; +} + +/// A host that exposes a fixed set of search paths. +pub(crate) struct StaticHost { + search_paths: Vec, +} + +impl StaticHost { + pub(crate) fn new(search_paths: Vec) -> Self { + Self { search_paths } + } +} + +impl Host for StaticHost { + fn python_search_paths(&self) -> Vec { + self.search_paths.clone() + } + + fn python_version(&self) -> PythonVersion { + PythonVersion::Py312 + } + + fn python_platform(&self) -> PythonPlatform { + PythonPlatform::Darwin + } +} diff --git a/crates/ruff_python_resolver/src/implicit_imports.rs b/crates/ruff_python_resolver/src/implicit_imports.rs new file mode 100644 index 0000000000..2c42f5450a --- /dev/null +++ b/crates/ruff_python_resolver/src/implicit_imports.rs @@ -0,0 +1,150 @@ +use std::collections::HashMap; +use std::ffi::OsStr; +use std::fs; +use std::path::{Path, PathBuf}; + +use crate::{native_module, py_typed}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct ImplicitImport { + /// Whether the implicit import is a stub file. + pub(crate) is_stub_file: bool, + + /// Whether the implicit import is a native module. + pub(crate) is_native_lib: bool, + + /// The name of the implicit import (e.g., `os`). + pub(crate) name: String, + + /// The path to the implicit import. + pub(crate) path: PathBuf, + + /// The `py.typed` information for the implicit import, if any. + pub(crate) py_typed: Option, +} + +/// Find the "implicit" imports within the namespace package at the given path. +pub(crate) fn find(dir_path: &Path, exclusions: &[&Path]) -> HashMap { + let mut implicit_imports = HashMap::new(); + + // Enumerate all files and directories in the path, expanding links. + let Ok(entries) = fs::read_dir(dir_path) else { + return implicit_imports; + }; + + for entry in entries.flatten() { + let path = entry.path(); + + if exclusions.contains(&path.as_path()) { + continue; + } + + let Ok(file_type) = entry.file_type() else { + continue; + }; + + // TODO(charlie): Support symlinks. + if file_type.is_file() { + // Add implicit file-based modules. + let Some(extension) = path.extension() else { + continue; + }; + + let (file_stem, is_native_lib) = if extension == "py" || extension == "pyi" { + // E.g., `foo.py` becomes `foo`. + let file_stem = path.file_stem().and_then(OsStr::to_str); + let is_native_lib = false; + (file_stem, is_native_lib) + } else if native_module::is_native_module_file_extension(extension) + && !path + .with_extension(format!("{}.py", extension.to_str().unwrap())) + .exists() + && !path + .with_extension(format!("{}.pyi", extension.to_str().unwrap())) + .exists() + { + // E.g., `foo.abi3.so` becomes `foo`. + let file_stem = path + .file_stem() + .and_then(OsStr::to_str) + .and_then(|file_stem| { + file_stem.split_once('.').map(|(file_stem, _)| file_stem) + }); + let is_native_lib = true; + (file_stem, is_native_lib) + } else { + continue; + }; + + let Some(name) = file_stem else { + continue; + }; + + let implicit_import = ImplicitImport { + is_stub_file: extension == "pyi", + is_native_lib, + name: name.to_string(), + path: path.clone(), + py_typed: None, + }; + + // Always prefer stub files over non-stub files. + if implicit_imports + .get(&implicit_import.name) + .map_or(true, |implicit_import| !implicit_import.is_stub_file) + { + implicit_imports.insert(implicit_import.name.clone(), implicit_import); + } + } else if file_type.is_dir() { + // Add implicit directory-based modules. + let py_file_path = path.join("__init__.py"); + let pyi_file_path = path.join("__init__.pyi"); + + let (path, is_stub_file) = if py_file_path.exists() { + (py_file_path, false) + } else if pyi_file_path.exists() { + (pyi_file_path, true) + } else { + continue; + }; + + let Some(name) = path.file_name().and_then(OsStr::to_str) else { + continue; + }; + + let implicit_import = ImplicitImport { + is_stub_file, + is_native_lib: false, + name: name.to_string(), + path: path.clone(), + py_typed: py_typed::get_py_typed_info(&path), + }; + implicit_imports.insert(implicit_import.name.clone(), implicit_import); + } + } + + implicit_imports +} + +/// Filter a map of implicit imports to only include those that were actually imported. +pub(crate) fn filter( + implicit_imports: &HashMap, + imported_symbols: &[String], +) -> Option> { + if implicit_imports.is_empty() || imported_symbols.is_empty() { + return None; + } + + let mut filtered_imports = HashMap::new(); + for implicit_import in implicit_imports.values() { + if imported_symbols.contains(&implicit_import.name) { + filtered_imports.insert(implicit_import.name.clone(), implicit_import.clone()); + } + } + + if filtered_imports.len() == implicit_imports.len() { + return None; + } + + Some(filtered_imports) +} diff --git a/crates/ruff_python_resolver/src/import_result.rs b/crates/ruff_python_resolver/src/import_result.rs new file mode 100644 index 0000000000..6ca7bd245c --- /dev/null +++ b/crates/ruff_python_resolver/src/import_result.rs @@ -0,0 +1,122 @@ +//! Interface that describes the output of the import resolver. + +use crate::implicit_imports::ImplicitImport; +use std::collections::HashMap; +use std::path::PathBuf; + +use crate::py_typed::PyTypedInfo; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[allow(clippy::struct_excessive_bools)] +pub(crate) struct ImportResult { + /// Whether the import name was relative (e.g., ".foo"). + pub(crate) is_relative: bool, + + /// Whether the import was resolved to a file or module. + pub(crate) is_import_found: bool, + + /// The path was partially resolved, but the specific submodule + /// defining the import was not found. For example, `foo.bar` was + /// not found, but `foo` was found. + pub(crate) is_partly_resolved: bool, + + /// The import refers to a namespace package (i.e., a folder without + /// an `__init__.py[i]` file at the final level of resolution). By + /// convention, we insert empty `PathBuf` segments into the resolved + /// paths vector to indicate intermediary namespace packages. + pub(crate) is_namespace_package: bool, + + /// The final resolved directory contains an `__init__.py[i]` file. + pub(crate) is_init_file_present: bool, + + /// The import resolved to a stub (`.pyi`) file within a stub package. + pub(crate) is_stub_package: bool, + + /// The import resolved to a built-in, local, or third-party module. + pub(crate) import_type: ImportType, + + /// A vector of resolved absolute paths for each file in the module + /// name. Typically includes a sequence of `__init__.py` files, followed + /// by the Python file defining the import itself, though the exact + /// structure can vary. For example, namespace packages will be represented + /// by empty `PathBuf` segments in the vector. + /// + /// For example, resolving `import foo.bar` might yield `./foo/__init__.py` and `./foo/bar.py`, + /// or `./foo/__init__.py` and `./foo/bar/__init__.py`. + pub(crate) resolved_paths: Vec, + + /// The search path used to resolve the module. + pub(crate) search_path: Option, + + /// The resolved file is a type hint (i.e., a `.pyi` file), rather + /// than a Python (`.py`) file. + pub(crate) is_stub_file: bool, + + /// The resolved file is a native library. + pub(crate) is_native_lib: bool, + + /// The resolved file is a hint hint (i.e., a `.pyi` file) from + /// `typeshed` in the standard library. + pub(crate) is_stdlib_typeshed_file: bool, + + /// The resolved file is a hint hint (i.e., a `.pyi` file) from + /// `typeshed` in third-party stubs. + pub(crate) is_third_party_typeshed_file: bool, + + /// The resolved file is a type hint (i.e., a `.pyi` file) from + /// the configured typing directory. + pub(crate) is_local_typings_file: bool, + + /// A map from file to resolved path, for all implicitly imported + /// modules that are part of a namespace package. + pub(crate) implicit_imports: HashMap, + + /// Any implicit imports whose symbols were explicitly imported (i.e., via + /// a `from x import y` statement). + pub(crate) filtered_implicit_imports: HashMap, + + /// If the import resolved to a type hint (i.e., a `.pyi` file), then + /// a non-type-hint resolution will be stored here. + pub(crate) non_stub_import_result: Option>, + + /// Information extracted from the `py.typed` in the package used to + /// resolve the import, if any. + pub(crate) py_typed_info: Option, + + /// The directory of the package, if any. + pub(crate) package_directory: Option, +} + +impl ImportResult { + /// An import result that indicates that the import was not found. + pub(crate) fn not_found() -> Self { + Self { + is_relative: false, + is_import_found: false, + is_partly_resolved: false, + is_namespace_package: false, + is_init_file_present: false, + is_stub_package: false, + import_type: ImportType::Local, + resolved_paths: vec![], + search_path: None, + is_stub_file: false, + is_native_lib: false, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: HashMap::default(), + filtered_implicit_imports: HashMap::default(), + non_stub_import_result: None, + py_typed_info: None, + package_directory: None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ImportType { + BuiltIn, + ThirdParty, + Local, +} diff --git a/crates/ruff_python_resolver/src/lib.rs b/crates/ruff_python_resolver/src/lib.rs new file mode 100644 index 0000000000..c1fef3be66 --- /dev/null +++ b/crates/ruff_python_resolver/src/lib.rs @@ -0,0 +1,16 @@ +#![allow(dead_code)] + +mod config; +mod execution_environment; +mod host; +mod implicit_imports; +mod import_result; +mod module_descriptor; +mod native_module; +mod py_typed; +mod python_platform; +mod python_version; +mod resolver; +mod search; + +pub(crate) const SITE_PACKAGES: &str = "site-packages"; diff --git a/crates/ruff_python_resolver/src/module_descriptor.rs b/crates/ruff_python_resolver/src/module_descriptor.rs new file mode 100644 index 0000000000..7d71efafbc --- /dev/null +++ b/crates/ruff_python_resolver/src/module_descriptor.rs @@ -0,0 +1,16 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct ImportModuleDescriptor { + pub(crate) leading_dots: usize, + pub(crate) name_parts: Vec, + pub(crate) imported_symbols: Vec, +} + +impl ImportModuleDescriptor { + pub(crate) fn name(&self) -> String { + format!( + "{}{}", + ".".repeat(self.leading_dots), + &self.name_parts.join(".") + ) + } +} diff --git a/crates/ruff_python_resolver/src/native_module.rs b/crates/ruff_python_resolver/src/native_module.rs new file mode 100644 index 0000000000..110bde8500 --- /dev/null +++ b/crates/ruff_python_resolver/src/native_module.rs @@ -0,0 +1,14 @@ +//! Support for native Python extension modules. + +use std::ffi::OsStr; +use std::path::Path; + +/// Returns `true` if the given file extension is that of a native module. +pub(crate) fn is_native_module_file_extension(file_extension: &OsStr) -> bool { + file_extension == "so" || file_extension == "pyd" || file_extension == "dylib" +} + +/// Returns `true` if the given file name is that of a native module. +pub(crate) fn is_native_module_file_name(_module_name: &Path, _file_name: &Path) -> bool { + todo!() +} diff --git a/crates/ruff_python_resolver/src/py_typed.rs b/crates/ruff_python_resolver/src/py_typed.rs new file mode 100644 index 0000000000..258f801eed --- /dev/null +++ b/crates/ruff_python_resolver/src/py_typed.rs @@ -0,0 +1,40 @@ +//! Support for [PEP 561] (`py.typed` files). +//! +//! [PEP 561]: https://peps.python.org/pep-0561/ + +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct PyTypedInfo { + /// The path to the `py.typed` file. + py_typed_path: PathBuf, + + /// Whether the package is partially typed (as opposed to fully typed). + is_partially_typed: bool, +} + +/// Returns the `py.typed` information for the given directory, if any. +pub(crate) fn get_py_typed_info(dir_path: &Path) -> Option { + let py_typed_path = dir_path.join("py.typed"); + if py_typed_path.is_file() { + // Do a quick sanity check on the size before we attempt to read it. This + // file should always be really small - typically zero bytes in length. + let file_len = py_typed_path.metadata().ok()?.len(); + if file_len < 64 * 1024 { + // PEP 561 doesn't specify the format of "py.typed" in any detail other than + // to say that "If a stub package is partial it MUST include partial\n in a top + // level py.typed file." + let contents = std::fs::read_to_string(&py_typed_path).ok()?; + let is_partially_typed = + contents.contains("partial\n") || contents.contains("partial\r\n"); + Some(PyTypedInfo { + py_typed_path, + is_partially_typed, + }) + } else { + None + } + } else { + None + } +} diff --git a/crates/ruff_python_resolver/src/python_platform.rs b/crates/ruff_python_resolver/src/python_platform.rs new file mode 100644 index 0000000000..8ee2600518 --- /dev/null +++ b/crates/ruff_python_resolver/src/python_platform.rs @@ -0,0 +1,7 @@ +/// Enum to represent a Python platform. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(crate) enum PythonPlatform { + Darwin, + Linux, + Windows, +} diff --git a/crates/ruff_python_resolver/src/python_version.rs b/crates/ruff_python_resolver/src/python_version.rs new file mode 100644 index 0000000000..aeb2a76b75 --- /dev/null +++ b/crates/ruff_python_resolver/src/python_version.rs @@ -0,0 +1,24 @@ +/// Enum to represent a Python version. +#[derive(Debug, Copy, Clone)] +pub(crate) enum PythonVersion { + Py37, + Py38, + Py39, + Py310, + Py311, + Py312, +} + +impl PythonVersion { + /// The directory name (e.g., in a virtual environment) for this Python version. + pub(crate) fn dir(self) -> &'static str { + match self { + PythonVersion::Py37 => "python3.7", + PythonVersion::Py38 => "python3.8", + PythonVersion::Py39 => "python3.9", + PythonVersion::Py310 => "python3.10", + PythonVersion::Py311 => "python3.11", + PythonVersion::Py312 => "python3.12", + } + } +} diff --git a/crates/ruff_python_resolver/src/resolver.rs b/crates/ruff_python_resolver/src/resolver.rs new file mode 100644 index 0000000000..08ff0bc09f --- /dev/null +++ b/crates/ruff_python_resolver/src/resolver.rs @@ -0,0 +1,1497 @@ +//! Resolves Python imports to their corresponding files on disk. + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use log::debug; + +use crate::config::Config; +use crate::execution_environment::ExecutionEnvironment; +use crate::implicit_imports::ImplicitImport; +use crate::import_result::{ImportResult, ImportType}; +use crate::module_descriptor::ImportModuleDescriptor; +use crate::search::get_typeshed_root; +use crate::{host, implicit_imports, native_module, py_typed, search}; + +#[allow(clippy::fn_params_excessive_bools)] +fn _resolve_absolute_import( + root: &Path, + module_descriptor: &ImportModuleDescriptor, + allow_partial: bool, + allow_native_lib: bool, + use_stub_package: bool, + allow_pyi: bool, + look_for_py_typed: bool, +) -> ImportResult { + if use_stub_package { + debug!("Attempting to resolve stub package using root path: {root:?}"); + } else { + debug!("Attempting to resolve using root path: {root:?}"); + } + + // Starting at the specified path, walk the file system to find the specified module. + let mut resolved_paths: Vec = Vec::new(); + let mut dir_path = root.to_path_buf(); + let mut is_namespace_package = false; + let mut is_init_file_present = false; + let mut is_stub_package = false; + let mut is_stub_file = false; + let mut is_native_lib = false; + let mut implicit_imports = HashMap::new(); + let mut package_directory = None; + let mut py_typed_info = None; + + // Ex) `from . import foo` + if module_descriptor.name_parts.is_empty() { + let py_file_path = dir_path.join("__init__.py"); + let pyi_file_path = dir_path.join("__init__.pyi"); + + if allow_pyi && pyi_file_path.is_file() { + debug!("Resolved import with file: {pyi_file_path:?}"); + resolved_paths.push(pyi_file_path.clone()); + } else if py_file_path.is_file() { + debug!("Resolved import with file: {py_file_path:?}"); + resolved_paths.push(py_file_path.clone()); + } else { + debug!("Partially resolved import with directory: {dir_path:?}"); + + // Add an empty path to indicate that the import is partially resolved. + resolved_paths.push(PathBuf::new()); + is_namespace_package = true; + } + + implicit_imports = implicit_imports::find(&dir_path, &[&py_file_path, &pyi_file_path]); + } else { + for (i, part) in module_descriptor.name_parts.iter().enumerate() { + let is_first_part = i == 0; + let is_last_part = i == module_descriptor.name_parts.len() - 1; + + // Extend the directory path with the next segment. + if use_stub_package && is_first_part { + dir_path = dir_path.join(format!("{part}-stubs")); + is_stub_package = true; + } else { + dir_path = dir_path.join(part); + } + + let found_directory = dir_path.is_dir(); + if found_directory { + if is_first_part { + package_directory = Some(dir_path.clone()); + } + + // Look for an `__init__.py[i]` in the directory. + let py_file_path = dir_path.join("__init__.py"); + let pyi_file_path = dir_path.join("__init__.pyi"); + is_init_file_present = false; + + if allow_pyi && pyi_file_path.is_file() { + debug!("Resolved import with file: {pyi_file_path:?}"); + resolved_paths.push(pyi_file_path.clone()); + if is_last_part { + is_stub_file = true; + } + is_init_file_present = true; + } else if py_file_path.is_file() { + debug!("Resolved import with file: {py_file_path:?}"); + resolved_paths.push(py_file_path.clone()); + is_init_file_present = true; + } + + if look_for_py_typed { + py_typed_info = + py_typed_info.or_else(|| py_typed::get_py_typed_info(&dir_path)); + } + + // We haven't reached the end of the import, and we found a matching directory. + // Proceed to the next segment. + if !is_last_part { + if !is_init_file_present { + resolved_paths.push(PathBuf::new()); + is_namespace_package = true; + py_typed_info = None; + } + continue; + } + + if is_init_file_present { + implicit_imports = + implicit_imports::find(&dir_path, &[&py_file_path, &pyi_file_path]); + break; + } + } + + // We couldn't find a matching directory, or the directory didn't contain an + // `__init__.py[i]` file. Look for an `.py[i]` file with the same name as the + // segment, in lieu of a directory. + let py_file_path = dir_path.with_extension("py"); + let pyi_file_path = dir_path.with_extension("pyi"); + + if allow_pyi && pyi_file_path.is_file() { + debug!("Resolved import with file: {pyi_file_path:?}"); + resolved_paths.push(pyi_file_path); + if is_last_part { + is_stub_file = true; + } + } else if py_file_path.is_file() { + debug!("Resolved import with file: {py_file_path:?}"); + resolved_paths.push(py_file_path); + } else { + if allow_native_lib && dir_path.is_dir() { + // We couldn't find a `.py[i]` file; search for a native library. + if let Some(native_lib_path) = dir_path + .read_dir() + .unwrap() + .flatten() + .filter(|entry| entry.file_type().map_or(false, |ft| ft.is_file())) + .find(|entry| { + native_module::is_native_module_file_name(&dir_path, &entry.path()) + }) + { + debug!("Resolved import with file: {native_lib_path:?}"); + is_native_lib = true; + resolved_paths.push(native_lib_path.path()); + } + } + + if !is_native_lib && found_directory { + debug!("Partially resolved import with directory: {dir_path:?}"); + resolved_paths.push(PathBuf::new()); + if is_last_part { + implicit_imports = + implicit_imports::find(&dir_path, &[&py_file_path, &pyi_file_path]); + is_namespace_package = true; + } + } else if is_native_lib { + debug!("Did not find file {py_file_path:?} or {pyi_file_path:?}"); + } + } + break; + } + } + + let import_found = if allow_partial { + !resolved_paths.is_empty() + } else { + resolved_paths.len() == module_descriptor.name_parts.len() + }; + + let is_partly_resolved = + !resolved_paths.is_empty() && resolved_paths.len() < module_descriptor.name_parts.len(); + + ImportResult { + is_relative: false, + is_import_found: import_found, + is_partly_resolved, + is_namespace_package, + is_init_file_present, + is_stub_package, + import_type: ImportType::Local, + resolved_paths, + search_path: Some(root.into()), + is_stub_file, + is_native_lib, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports, + filtered_implicit_imports: HashMap::default(), + non_stub_import_result: None, + py_typed_info, + package_directory, + } +} + +/// Resolve an absolute module import based on the import resolution algorithm +/// defined in [PEP 420]. +/// +/// [PEP 420]: https://peps.python.org/pep-0420/ +#[allow(clippy::fn_params_excessive_bools)] +fn resolve_absolute_import( + root: &Path, + module_descriptor: &ImportModuleDescriptor, + allow_partial: bool, + allow_native_lib: bool, + use_stub_package: bool, + allow_pyi: bool, + look_for_py_typed: bool, +) -> ImportResult { + if allow_pyi && use_stub_package { + // Search for packaged stubs first. PEP 561 indicates that package authors can ship + // stubs separately from the package implementation by appending `-stubs` to its + // top-level directory name. + let import_result = _resolve_absolute_import( + root, + module_descriptor, + allow_partial, + false, + true, + true, + true, + ); + + if import_result.package_directory.is_some() { + // If this is a namespace package that wasn't resolved, assume that + // it's a partial stub package and continue looking for a real package. + if !import_result.is_namespace_package || import_result.is_import_found { + return import_result; + } + } + } + + // Search for a "real" package. + _resolve_absolute_import( + root, + module_descriptor, + allow_partial, + allow_native_lib, + false, + allow_pyi, + look_for_py_typed, + ) +} + +/// Resolve an absolute module import based on the import resolution algorithm, +/// taking into account the various competing files to which the import could +/// resolve. +/// +/// For example, prefers local imports over third-party imports, and stubs over +/// non-stubs. +fn _resolve_best_absolute_import( + execution_environment: &ExecutionEnvironment, + module_descriptor: &ImportModuleDescriptor, + allow_pyi: bool, + config: &Config, + host: &Host, +) -> Option { + let import_name = module_descriptor.name(); + + // Search for local stub files (using `stub_path`). + if allow_pyi { + if let Some(stub_path) = config.stub_path.as_ref() { + debug!("Looking in stub path: {}", stub_path.display()); + + let mut typings_import = resolve_absolute_import( + stub_path, + module_descriptor, + false, + false, + true, + allow_pyi, + false, + ); + + if typings_import.is_import_found { + // Treat stub files as "local". + typings_import.import_type = ImportType::Local; + typings_import.is_local_typings_file = true; + + // If we resolved to a namespace package, ensure that all imported symbols are + // present in the namespace package's "implicit" imports. + if typings_import.is_namespace_package + && typings_import.resolved_paths[typings_import.resolved_paths.len() - 1] + .as_os_str() + .is_empty() + { + if _is_namespace_package_resolved( + module_descriptor, + &typings_import.implicit_imports, + ) { + return Some(typings_import); + } + } else { + return Some(typings_import); + } + } + + return None; + } + } + + // Look in the root directory of the execution environment. + debug!( + "Looking in root directory of execution environment: {}", + execution_environment.root.display() + ); + + let mut local_import = resolve_absolute_import( + &execution_environment.root, + module_descriptor, + false, + true, + true, + allow_pyi, + false, + ); + local_import.import_type = ImportType::Local; + + let mut best_result_so_far = Some(local_import); + + // Look in any extra paths. + for extra_path in &execution_environment.extra_paths { + debug!("Looking in extra path: {}", extra_path.display()); + + let mut local_import = resolve_absolute_import( + extra_path, + module_descriptor, + false, + true, + true, + allow_pyi, + false, + ); + local_import.import_type = ImportType::Local; + + best_result_so_far = Some(_pick_best_import( + best_result_so_far, + local_import, + module_descriptor, + )); + } + + // Look for third-party imports in Python's `sys` path. + for search_path in search::find_python_search_paths(config, host) { + debug!("Looking in Python search path: {}", search_path.display()); + + let mut third_party_import = resolve_absolute_import( + &search_path, + module_descriptor, + false, + true, + true, + allow_pyi, + true, + ); + third_party_import.import_type = ImportType::ThirdParty; + + best_result_so_far = Some(_pick_best_import( + best_result_so_far, + third_party_import, + module_descriptor, + )); + } + + // If a library is fully `py.typed`, prefer the current result. There's one exception: + // we're executing from `typeshed` itself. In that case, use the `typeshed` lookup below, + // rather than favoring `py.typed` libraries. + if let Some(typeshed_root) = get_typeshed_root(config, host) { + debug!( + "Looking in typeshed root directory: {}", + typeshed_root.display() + ); + if typeshed_root != execution_environment.root { + if best_result_so_far.as_ref().map_or(false, |result| { + result.py_typed_info.is_some() && !result.is_partly_resolved + }) { + return best_result_so_far; + } + } + } + + if allow_pyi && !module_descriptor.name_parts.is_empty() { + // Check for a stdlib typeshed file. + debug!("Looking for typeshed stdlib path: {}", import_name); + if let Some(mut typeshed_stdilib_import) = + _find_typeshed_path(module_descriptor, true, config, host) + { + typeshed_stdilib_import.is_stdlib_typeshed_file = true; + return Some(typeshed_stdilib_import); + } + + // Check for a third-party typeshed file. + debug!("Looking for typeshed third-party path: {}", import_name); + if let Some(mut typeshed_third_party_import) = + _find_typeshed_path(module_descriptor, false, config, host) + { + typeshed_third_party_import.is_third_party_typeshed_file = true; + + best_result_so_far = Some(_pick_best_import( + best_result_so_far, + typeshed_third_party_import, + module_descriptor, + )); + } + } + + // We weren't able to find an exact match, so return the best + // partial match. + best_result_so_far +} + +/// Determines whether a namespace package resolves all of the symbols +/// requested in the module descriptor. Namespace packages have no "__init__.py" +/// file, so the only way that symbols can be resolved is if submodules +/// are present. If specific symbols were requested, make sure they +/// are all satisfied by submodules (as listed in the implicit imports). +fn _is_namespace_package_resolved( + module_descriptor: &ImportModuleDescriptor, + implicit_imports: &HashMap, +) -> bool { + if !module_descriptor.imported_symbols.is_empty() { + // Pyright uses `!Array.from(moduleDescriptor.importedSymbols.keys()).some((symbol) => implicitImports.has(symbol))`. + // But that only checks if any of the symbols are in the implicit imports? + for symbol in &module_descriptor.imported_symbols { + if !implicit_imports.contains_key(symbol) { + return false; + } + } + } else if implicit_imports.is_empty() { + return false; + } + true +} + +/// Finds the `typeshed` path for the given module descriptor. +/// +/// Supports both standard library and third-party `typeshed` lookups. +fn _find_typeshed_path( + module_descriptor: &ImportModuleDescriptor, + is_std_lib: bool, + config: &Config, + host: &Host, +) -> Option { + if is_std_lib { + debug!("Looking for typeshed `stdlib` path"); + } else { + debug!("Looking for typeshed `stubs` path"); + } + + let mut typeshed_paths = vec![]; + + if is_std_lib { + if let Some(path) = search::get_stdlib_typeshed_path(config, host) { + typeshed_paths.push(path); + } + } else { + if let Some(paths) = + search::get_third_party_typeshed_package_paths(module_descriptor, config, host) + { + typeshed_paths.extend(paths); + } + } + + for typeshed_path in typeshed_paths { + if typeshed_path.is_dir() { + let mut import_info = resolve_absolute_import( + &typeshed_path, + module_descriptor, + false, + false, + false, + true, + false, + ); + if import_info.is_import_found { + import_info.import_type = if is_std_lib { + ImportType::BuiltIn + } else { + ImportType::ThirdParty + }; + return Some(import_info); + } + } + } + + debug!("Typeshed path not found"); + None +} + +/// Given a current "best" import and a newly discovered result, returns the +/// preferred result. +fn _pick_best_import( + best_import_so_far: Option, + new_import: ImportResult, + module_descriptor: &ImportModuleDescriptor, +) -> ImportResult { + let Some(best_import_so_far) = best_import_so_far else { + return new_import; + }; + + if new_import.is_import_found { + // Prefer traditional over namespace packages. + let so_far_index = best_import_so_far + .resolved_paths + .iter() + .position(|path| !path.as_os_str().is_empty()); + let new_index = new_import + .resolved_paths + .iter() + .position(|path| !path.as_os_str().is_empty()); + if so_far_index != new_index { + match (so_far_index, new_index) { + (None, Some(_)) => return new_import, + (Some(_), None) => return best_import_so_far, + (Some(so_far_index), Some(new_index)) => { + return if so_far_index < new_index { + best_import_so_far + } else { + new_import + } + } + _ => {} + } + } + + // Prefer "found" over "not found". + if !best_import_so_far.is_import_found { + return new_import; + } + + // If both results are namespace imports, prefer the result that resolves all + // imported symbols. + if best_import_so_far.is_namespace_package && new_import.is_namespace_package { + if !module_descriptor.imported_symbols.is_empty() { + if !_is_namespace_package_resolved( + module_descriptor, + &best_import_so_far.implicit_imports, + ) { + if _is_namespace_package_resolved( + module_descriptor, + &new_import.implicit_imports, + ) { + return new_import; + } + + // Prefer the namespace package that has an `__init__.py[i]` file present in the + // final directory over one that does not. + if best_import_so_far.is_init_file_present && !new_import.is_init_file_present { + return best_import_so_far; + } + if !best_import_so_far.is_init_file_present && new_import.is_init_file_present { + return new_import; + } + } + } + } + + // Prefer "py.typed" over "non-py.typed". + if best_import_so_far.py_typed_info.is_some() && new_import.py_typed_info.is_none() { + return best_import_so_far; + } + if best_import_so_far.py_typed_info.is_none() && best_import_so_far.py_typed_info.is_some() + { + return new_import; + } + + // Prefer stub files (`.pyi`) over non-stub files (`.py`). + if best_import_so_far.is_stub_file && !new_import.is_stub_file { + return best_import_so_far; + } + if !best_import_so_far.is_stub_file && new_import.is_stub_file { + return new_import; + } + + // If we're still tied, prefer a shorter resolution path. + if best_import_so_far.resolved_paths.len() > new_import.resolved_paths.len() { + return new_import; + } + } else if new_import.is_partly_resolved { + let so_far_index = best_import_so_far + .resolved_paths + .iter() + .position(|path| !path.as_os_str().is_empty()); + let new_index = new_import + .resolved_paths + .iter() + .position(|path| !path.as_os_str().is_empty()); + if so_far_index != new_index { + match (so_far_index, new_index) { + (None, Some(_)) => return new_import, + (Some(_), None) => return best_import_so_far, + (Some(so_far_index), Some(new_index)) => { + return if so_far_index < new_index { + best_import_so_far + } else { + new_import + } + } + _ => {} + } + } + } + + best_import_so_far +} + +/// Resolve a relative import. +fn _resolve_relative_import( + source_file: &Path, + module_descriptor: &ImportModuleDescriptor, +) -> Option { + // Determine which search path this file is part of. + let mut directory = source_file; + for _ in 0..module_descriptor.leading_dots { + directory = directory.parent()?; + } + + // Now try to match the module parts from the current directory location. + let mut abs_import = resolve_absolute_import( + directory, + module_descriptor, + false, + true, + false, + true, + false, + ); + + if abs_import.is_stub_file { + // If we found a stub for a relative import, only search + // the same folder for the real module. Otherwise, it will + // error out on runtime. + abs_import.non_stub_import_result = Some(Box::new(resolve_absolute_import( + directory, + module_descriptor, + false, + true, + false, + false, + false, + ))); + } + + Some(abs_import) +} + +/// Resolve an absolute or relative import. +fn _resolve_import_strict( + source_file: &Path, + execution_environment: &ExecutionEnvironment, + module_descriptor: &ImportModuleDescriptor, + config: &Config, + host: &Host, +) -> ImportResult { + let import_name = module_descriptor.name(); + + if module_descriptor.leading_dots > 0 { + debug!("Resolving relative import for: {import_name}"); + + let relative_import = _resolve_relative_import(source_file, module_descriptor); + + if let Some(mut relative_import) = relative_import { + relative_import.is_relative = true; + return relative_import; + } + } else { + debug!("Resolving best absolute import for: {import_name}"); + + let best_import = _resolve_best_absolute_import( + execution_environment, + module_descriptor, + true, + config, + host, + ); + + if let Some(mut best_import) = best_import { + if best_import.is_stub_file { + debug!("Resolving best non-stub absolute import for: {import_name}"); + + best_import.non_stub_import_result = Some(Box::new( + _resolve_best_absolute_import( + execution_environment, + module_descriptor, + false, + config, + host, + ) + .unwrap_or_else(ImportResult::not_found), + )); + } + return best_import; + } + } + + ImportResult::not_found() +} + +/// Resolves an import, given the current file and the import descriptor. +fn resolve_import( + source_file: &Path, + execution_environment: &ExecutionEnvironment, + module_descriptor: &ImportModuleDescriptor, + config: &Config, + host: &Host, +) -> ImportResult { + let import_result = _resolve_import_strict( + source_file, + execution_environment, + module_descriptor, + config, + host, + ); + if import_result.is_import_found || module_descriptor.leading_dots > 0 { + return import_result; + } + + // If we weren't able to resolve an absolute import, try resolving it in the + // importing file's directory, then the parent directory, and so on, until the + // import root is reached. + let root = execution_environment.root.as_path(); + if source_file.starts_with(root) { + let mut current = source_file; + while let Some(parent) = current.parent() { + if parent == root { + break; + } + + debug!("Resolving absolute import in parent: {}", parent.display()); + + let mut result = resolve_absolute_import( + parent, + module_descriptor, + false, + false, + false, + true, + false, + ); + + if result.is_import_found { + if let Some(implicit_imports) = implicit_imports::filter( + &result.implicit_imports, + &module_descriptor.imported_symbols, + ) { + result.implicit_imports = implicit_imports; + } + return result; + } + + current = parent; + } + } + + ImportResult::not_found() +} + +#[cfg(test)] +mod tests { + use std::fs::{create_dir_all, File}; + use std::io::{self, Write}; + use std::path::{Path, PathBuf}; + + use log::debug; + use tempfile::TempDir; + + use crate::config::Config; + use crate::execution_environment::ExecutionEnvironment; + use crate::import_result::{ImportResult, ImportType}; + use crate::module_descriptor::ImportModuleDescriptor; + use crate::python_platform::PythonPlatform; + use crate::python_version::PythonVersion; + use crate::resolver::resolve_import; + use crate::{host, SITE_PACKAGES}; + + struct PythonFile { + path: PathBuf, + content: String, + } + + impl PythonFile { + fn new(path: impl Into, content: impl Into) -> Self { + Self { + path: path.into(), + content: content.into(), + } + } + + fn create(&self, dir: impl AsRef) -> io::Result { + let file_path = dir.as_ref().join(self.path.as_path()); + if let Some(parent) = file_path.parent() { + create_dir_all(parent)?; + } + let mut f = File::create(&file_path)?; + f.write_all(self.content.as_bytes())?; + f.sync_all()?; + + Ok(file_path) + } + } + + fn _resolve>( + source_file: impl AsRef, + name: &str, + root: T, + extra_paths: Vec, + library: Option, + stub_path: Option, + typeshed_path: Option, + ) -> ImportResult { + let execution_environment = ExecutionEnvironment { + root: root.into(), + python_version: PythonVersion::Py37, + python_platform: PythonPlatform::Darwin, + extra_paths, + }; + + let module_descriptor = ImportModuleDescriptor { + leading_dots: name.chars().take_while(|c| *c == '.').count(), + name_parts: name + .chars() + .skip_while(|c| *c == '.') + .collect::() + .split('.') + .map(std::string::ToString::to_string) + .collect(), + imported_symbols: Vec::new(), + }; + + let config = Config { + venv_path: None, + venv: None, + python_path: None, + typeshed_path: typeshed_path.map(Into::into), + stub_path: stub_path.map(Into::into), + default_python_version: None, + }; + + let host = host::StaticHost::new(if let Some(library) = library { + vec![library.into()] + } else { + Vec::new() + }); + + resolve_import( + source_file.as_ref(), + &execution_environment, + &module_descriptor, + &config, + &host, + ) + } + + fn resolve>( + source_file: impl AsRef, + name: &str, + root: T, + ) -> ImportResult { + _resolve(source_file, name, root, Vec::new(), None, None, None) + } + + fn resolve_with_extra_paths>( + source_file: impl AsRef, + name: &str, + root: T, + extra_paths: Vec, + ) -> ImportResult { + _resolve(source_file, name, root, extra_paths, None, None, None) + } + + fn resolve_with_library>( + source_file: impl AsRef, + name: &str, + root: T, + library: T, + ) -> ImportResult { + _resolve( + source_file, + name, + root, + Vec::new(), + Some(library), + None, + None, + ) + } + + fn resolve_with_stub_path>( + source_file: impl AsRef, + name: &str, + root: T, + library: T, + stub_path: T, + ) -> ImportResult { + _resolve( + source_file, + name, + root, + Vec::new(), + Some(library), + Some(stub_path), + None, + ) + } + + fn resolve_with_typeshed_path>( + source_file: impl AsRef, + name: &str, + root: T, + library: T, + typeshed_path: T, + ) -> ImportResult { + _resolve( + source_file, + name, + root, + Vec::new(), + Some(library), + None, + Some(typeshed_path), + ) + } + + #[test] + fn partial_stub_file_exists() -> io::Result<()> { + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + + let partial_stub_pyi = + PythonFile::new("myLib-stubs/partialStub.pyi", "def test(): ...").create(&library)?; + PythonFile::new("myLib-stubs/py.typed", "partial\n").create(&library)?; + let partial_stub_py = + PythonFile::new("myLib/partialStub.py", "def test(): ...").create(&library)?; + + let result = resolve_with_library( + partial_stub_py, + "myLib.partialStub", + dir.path(), + library.as_path(), + ); + + assert!(result.is_import_found); + assert!(result.is_stub_file); + assert_eq!(result.import_type, ImportType::ThirdParty); + assert_eq!( + result.resolved_paths, + // TODO(charlie): Pyright matches on `libraryRoot, 'myLib', 'partialStub.pyi'` here. + // But that file doesn't exist. There's some kind of transform. + vec![PathBuf::new(), partial_stub_pyi] + ); + + Ok(()) + } + + #[test] + fn partial_stub_init_exists() -> io::Result<()> { + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + + PythonFile::new("myLib-stubs/py.typed", "partial\n").create(&library)?; + let partial_stub_init_pyi = + PythonFile::new("myLib-stubs/__init__.pyi", "def test(): ...").create(&library)?; + let partial_stub_init_py = + PythonFile::new("myLib/__init__.py", "def test(): ...").create(&library)?; + + let result = + resolve_with_library(partial_stub_init_py, "myLib", dir.path(), library.as_path()); + + assert!(result.is_import_found); + assert!(result.is_stub_file); + assert_eq!(result.import_type, ImportType::ThirdParty); + assert_eq!( + result.resolved_paths, + // TODO(charlie): Pyright matches on `libraryRoot, 'myLib', '__init__.pyi'` here. + // But that file doesn't exist. There's some kind of transform. + vec![partial_stub_init_pyi] + ); + + Ok(()) + } + + #[test] + fn side_by_side_files() -> io::Result<()> { + let dir = TempDir::new()?; + let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + + let my_file = PythonFile::new("myFile.py", "# not used").create(dir.path())?; + let side_by_side_stub_file = + PythonFile::new("myLib-stubs/partialStub.pyi", "def test(): ...").create(&library)?; + PythonFile::new("myLib-stubs/py.typed", "partial\n").create(&library)?; + PythonFile::new("myLib/partialStub.pyi", "# empty").create(&library)?; + PythonFile::new("myLib/partialStub.py", "def test(): pass").create(&library)?; + let partial_stub_file = + PythonFile::new("myLib-stubs/partialStub2.pyi", "def test(): ...").create(&library)?; + PythonFile::new("myLib/partialStub2.py", "def test(): pass").create(&library)?; + + // Stub package wins over original package (per PEP 561 rules). + let side_by_side_result = + resolve_with_library(&my_file, "myLib.partialStub", dir.path(), &library); + assert!(side_by_side_result.is_import_found); + assert!(side_by_side_result.is_stub_file); + assert_eq!( + side_by_side_result.resolved_paths, + vec![PathBuf::new(), side_by_side_stub_file] + ); + + // Side by side stub doesn't completely disable partial stub. + let partial_stub_result = + resolve_with_library(&my_file, "myLib.partialStub2", dir.path(), &library); + assert!(partial_stub_result.is_import_found); + assert!(partial_stub_result.is_stub_file); + assert_eq!( + partial_stub_result.resolved_paths, + vec![PathBuf::new(), partial_stub_file] + ); + + Ok(()) + } + + #[test] + fn stub_package() -> io::Result<()> { + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + + PythonFile::new("myLib-stubs/stub.pyi", "# empty").create(&library)?; + PythonFile::new("myLib-stubs/__init__.pyi", "# empty").create(&library)?; + let partial_stub_py = + PythonFile::new("myLib/partialStub.py", "def test(): ...").create(&library)?; + + let result = resolve_with_library( + partial_stub_py, + "myLib.partialStub", + dir.path(), + library.as_path(), + ); + + // If fully typed stub package exists, that wins over the real package. + assert!(!result.is_import_found); + + Ok(()) + } + + #[test] + fn stub_namespace_package() -> io::Result<()> { + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + + PythonFile::new("myLib-stubs/stub.pyi", "# empty").create(&library)?; + let partial_stub_py = + PythonFile::new("myLib/partialStub.py", "def test(): ...").create(&library)?; + + let result = resolve_with_library( + partial_stub_py.clone(), + "myLib.partialStub", + dir.path(), + library.as_path(), + ); + + // If fully typed stub package exists, that wins over the real package. + assert!(result.is_import_found); + assert!(!result.is_stub_file); + assert_eq!(result.resolved_paths, vec![PathBuf::new(), partial_stub_py]); + + Ok(()) + } + + #[test] + fn stub_in_typing_folder_over_partial_stub_package() -> io::Result<()> { + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + let typing_folder = dir.path().join("typing"); + let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + + PythonFile::new("myLib-stubs/py.typed", "partial\n").create(&library)?; + PythonFile::new("myLib-stubs/__init__.pyi", "").create(&library)?; + let my_lib_pyi = PythonFile::new("myLib.pyi", "# empty").create(&typing_folder)?; + let my_lib_init_py = + PythonFile::new("myLib/__init__.py", "def test(): pass").create(&library)?; + + let result = resolve_with_stub_path( + my_lib_init_py, + "myLib", + dir.path(), + library.as_path(), + typing_folder.as_path(), + ); + + // If the package exists in typing folder, that gets picked up first (so we resolve to + // `myLib.pyi`). + assert!(result.is_import_found); + assert!(result.is_stub_file); + assert_eq!(result.resolved_paths, vec![my_lib_pyi]); + + Ok(()) + } + + #[test] + fn partial_stub_package_in_typing_folder() -> io::Result<()> { + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + let typing_folder = dir.path().join("typing"); + let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + + PythonFile::new("myLib-stubs/py.typed", "partial\n").create(&typing_folder)?; + let my_lib_stubs_init_pyi = PythonFile::new("myLib-stubs/__init__.pyi", "def test(): ...") + .create(&typing_folder)?; + let my_lib_init_py = + PythonFile::new("myLib/__init__.py", "def test(): pass").create(&library)?; + + let result = resolve_with_stub_path( + my_lib_init_py, + "myLib", + dir.path(), + library.as_path(), + typing_folder.as_path(), + ); + + // If the package exists in typing folder, that gets picked up first (so we resolve to + // `myLib.pyi`). + assert!(result.is_import_found); + assert!(result.is_stub_file); + assert_eq!(result.resolved_paths, vec![my_lib_stubs_init_pyi]); + + Ok(()) + } + + #[test] + fn typeshed_folder() -> io::Result<()> { + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + let typeshed_folder = dir.path().join("ts"); + + PythonFile::new("myLib-stubs/py.typed", "partial\n").create(&library)?; + let my_lib_stubs_init_pyi = + PythonFile::new("myLib-stubs/__init__.pyi", "def test(): ...").create(&library)?; + PythonFile::new("stubs/myLibPackage/myLib.pyi", "# empty").create(&typeshed_folder)?; + let my_lib_init_py = + PythonFile::new("myLib/__init__.py", "def test(): pass").create(&library)?; + + let result = resolve_with_typeshed_path( + my_lib_init_py, + "myLib", + dir.path(), + library.as_path(), + typeshed_folder.as_path(), + ); + + // Stub packages win over typeshed. + assert!(result.is_import_found); + assert!(result.is_stub_file); + assert_eq!(result.resolved_paths, vec![my_lib_stubs_init_pyi]); + + Ok(()) + } + + #[test] + fn py_typed_file() -> io::Result<()> { + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + + let partial_stub_init_pyi = + PythonFile::new("myLib-stubs/__init__.pyi", "def test(): ...").create(&library)?; + PythonFile::new("myLib-stubs/py.typed", "partial\n").create(&library)?; + PythonFile::new("myLib/__init__.py", "def test(): pass").create(&library)?; + let package_py_typed = PythonFile::new("myLib/py.typed", "# typed").create(&library)?; + + let result = resolve_with_library(package_py_typed, "myLib", dir.path(), library.as_path()); + + // Partial stub package always overrides original package. + assert!(result.is_import_found); + assert!(result.is_stub_file); + assert_eq!(result.resolved_paths, vec![partial_stub_init_pyi]); + + Ok(()) + } + + #[test] + fn py_typed_library() -> io::Result<()> { + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + let typeshed_folder = dir.path().join("ts"); + + let init_py = PythonFile::new("os/__init__.py", "def test(): ...").create(&library)?; + PythonFile::new("os/py.typed", "").create(&library)?; + let typeshed_init_pyi = + PythonFile::new("stubs/os/os/__init__.pyi", "# empty").create(&typeshed_folder)?; + + let result = resolve_with_typeshed_path( + typeshed_init_pyi, + "os", + dir.path(), + library.as_path(), + typeshed_folder.as_path(), + ); + + assert!(result.is_import_found); + assert_eq!(result.resolved_paths, vec![init_py]); + + Ok(()) + } + + #[test] + fn non_py_typed_library() -> io::Result<()> { + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + let typeshed_folder = dir.path().join("ts"); + + PythonFile::new("os/__init__.py", "def test(): ...").create(&library)?; + let typeshed_init_pyi = + PythonFile::new("stubs/os/os/__init__.pyi", "# empty").create(&typeshed_folder)?; + + let result = resolve_with_typeshed_path( + typeshed_init_pyi.clone(), + "os", + dir.path(), + library.as_path(), + typeshed_folder.as_path(), + ); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::ThirdParty); + assert_eq!(result.resolved_paths, vec![typeshed_init_pyi]); + + Ok(()) + } + + #[test] + fn import_side_by_side_file_root() -> io::Result<()> { + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + + let file1 = PythonFile::new("file1.py", "import file1").create(&dir)?; + let file2 = PythonFile::new("file2.py", "import file2").create(&dir)?; + + let result = resolve(file2, "file1", dir.path()); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!(result.resolved_paths, vec![file1]); + + Ok(()) + } + + #[test] + fn import_side_by_side_file_sub_folder() -> io::Result<()> { + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + + let test_init = PythonFile::new("test/__init__.py", "").create(&dir)?; + let test_file1 = PythonFile::new("test/file1.py", "import file1").create(&dir)?; + let test_file2 = PythonFile::new("test/file2.py", "import file2").create(&dir)?; + + let result = resolve(test_file2, "test.file1", dir.path()); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!(result.resolved_paths, vec![test_init, test_file1]); + + Ok(()) + } + + #[test] + fn import_side_by_side_file_sub_under_src_folder() -> io::Result<()> { + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + + let nested_init = PythonFile::new("src/nested/__init__.py", "").create(&dir)?; + let nested_file1 = PythonFile::new("src/nested/file1.py", "import file1").create(&dir)?; + let nested_file2 = PythonFile::new("src/nested/file2.py", "import file2").create(&dir)?; + + let result = resolve(nested_file2, "nested.file1", dir.path()); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!(result.resolved_paths, vec![nested_init, nested_file1]); + + Ok(()) + } + + #[test] + fn import_file_sub_under_containing_folder() -> io::Result<()> { + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + + let nested_file1 = + PythonFile::new("src/nested/file1.py", "def test1(): ... ").create(&dir)?; + let nested_file2 = + PythonFile::new("src/nested/nested2/file2.py", "def test2(): ...").create(&dir)?; + + let result = resolve(nested_file2, "file1", dir.path()); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!(result.resolved_paths, vec![nested_file1]); + + Ok(()) + } + + #[test] + fn import_side_by_side_file_sub_under_lib_folder() -> io::Result<()> { + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + + PythonFile::new("myLib/file1.py", "def test1(): ...").create(&library)?; + let file2 = PythonFile::new("myLib/file2.py", "def test2(): ...").create(&library)?; + + let result = resolve(file2, "file1", dir.path()); + + debug!("result: {:?}", result); + + assert!(!result.is_import_found); + + Ok(()) + } + + #[test] + fn nested_namespace_package_1() -> io::Result<()> { + // See: https://github.com/microsoft/pyright/issues/5089. + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + + let file = PythonFile::new("package1/a/b/c/d.py", "def f(): pass").create(&dir)?; + let package1_init = PythonFile::new("package1/a/__init__.py", "").create(&dir)?; + let package2_init = PythonFile::new("package2/a/__init__.py", "").create(&dir)?; + + let package1 = dir.path().join("package1"); + let package2 = dir.path().join("package2"); + + let result = resolve_with_extra_paths( + package2_init, + "a.b.c.d", + dir.path(), + vec![package1, package2], + ); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!( + result.resolved_paths, + vec![package1_init, PathBuf::new(), PathBuf::new(), file] + ); + + Ok(()) + } + + #[test] + fn nested_namespace_package_2() -> io::Result<()> { + // See: https://github.com/microsoft/pyright/issues/5089. + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + + let file = PythonFile::new("package1/a/b/c/d.py", "def f(): pass").create(&dir)?; + let package1_init = PythonFile::new("package1/a/b/c/__init__.py", "").create(&dir)?; + let package2_init = PythonFile::new("package2/a/b/c/__init__.py", "").create(&dir)?; + + let package1 = dir.path().join("package1"); + let package2 = dir.path().join("package2"); + + let result = resolve_with_extra_paths( + package2_init, + "a.b.c.d", + dir.path(), + vec![package1, package2], + ); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!( + result.resolved_paths, + vec![PathBuf::new(), PathBuf::new(), package1_init, file] + ); + + Ok(()) + } + + #[test] + fn nested_namespace_package_3() -> io::Result<()> { + // See: https://github.com/microsoft/pyright/issues/5089. + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + + PythonFile::new("package1/a/b/c/d.py", "def f(): pass").create(&dir)?; + let package2_init = PythonFile::new("package2/a/__init__.py", "").create(&dir)?; + + let package1 = dir.path().join("package1"); + let package2 = dir.path().join("package2"); + + let result = resolve_with_extra_paths( + package2_init, + "a.b.c.d", + dir.path(), + vec![package1, package2], + ); + + assert!(!result.is_import_found); + + Ok(()) + } + + #[test] + fn nested_namespace_package_4() -> io::Result<()> { + // See: https://github.com/microsoft/pyright/issues/5089. + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + + PythonFile::new("package1/a/b/__init__.py", "").create(&dir)?; + PythonFile::new("package1/a/b/c.py", "def f(): pass").create(&dir)?; + PythonFile::new("package2/a/__init__.py", "").create(&dir)?; + let package2_a_b_init = PythonFile::new("package2/a/b/__init__.py", "").create(&dir)?; + + let package1 = dir.path().join("package1"); + let package2 = dir.path().join("package2"); + + let result = resolve_with_extra_paths( + package2_a_b_init, + "a.b.c", + dir.path(), + vec![package1, package2], + ); + + assert!(!result.is_import_found); + + Ok(()) + } + + // New tests, don't exist upstream. + #[test] + fn relative_import_side_by_side_file_root() -> io::Result<()> { + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + + let file1 = PythonFile::new("file1.py", "def test(): ...").create(&dir)?; + let file2 = PythonFile::new("file2.py", "def test(): ...").create(&dir)?; + + let result = resolve(file2, ".file1", dir.path()); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!(result.resolved_paths, vec![file1]); + + Ok(()) + } + + #[test] + fn invalid_relative_import_side_by_side_file_root() -> io::Result<()> { + env_logger::builder().is_test(true).try_init().ok(); + + let dir = TempDir::new()?; + + PythonFile::new("file1.py", "def test(): ...").create(&dir)?; + let file2 = PythonFile::new("file2.py", "def test(): ...").create(&dir)?; + + let result = resolve(file2, "..file1", dir.path()); + + assert!(!result.is_import_found); + + Ok(()) + } +} diff --git a/crates/ruff_python_resolver/src/search.rs b/crates/ruff_python_resolver/src/search.rs new file mode 100644 index 0000000000..66145aef78 --- /dev/null +++ b/crates/ruff_python_resolver/src/search.rs @@ -0,0 +1,282 @@ +//! Determine the appropriate search paths for the Python environment. + +use std::collections::HashMap; +use std::ffi::OsStr; +use std::fs; +use std::path::{Path, PathBuf}; + +use log::debug; + +use crate::config::Config; +use crate::module_descriptor::ImportModuleDescriptor; +use crate::python_version::PythonVersion; +use crate::{host, SITE_PACKAGES}; + +/// Find the `site-packages` directory for the specified Python version. +fn find_site_packages_path( + lib_path: &Path, + python_version: Option, +) -> Option { + if lib_path.is_dir() { + debug!( + "Found path `{}`; looking for site-packages", + lib_path.display() + ); + } else { + debug!("Did not find `{}`", lib_path.display()); + } + + let site_packages_path = lib_path.join(SITE_PACKAGES); + if site_packages_path.is_dir() { + debug!("Found path `{}`", site_packages_path.display()); + return Some(site_packages_path); + } + + debug!( + "Did not find `{}`, so looking for Python subdirectory", + site_packages_path.display() + ); + + // There's no `site-packages` directory in the library directory; look for a `python3.X` + // directory instead. + let candidate_dirs: Vec = fs::read_dir(lib_path) + .ok()? + .filter_map(|entry| { + let entry = entry.ok()?; + let metadata = entry.metadata().ok()?; + + if metadata.file_type().is_dir() { + let dir_path = entry.path(); + if dir_path + .file_name() + .and_then(OsStr::to_str)? + .starts_with("python3.") + { + if dir_path.join(SITE_PACKAGES).is_dir() { + return Some(dir_path); + } + } + } else if metadata.file_type().is_symlink() { + let symlink_path = fs::read_link(entry.path()).ok()?; + if symlink_path + .file_name() + .and_then(OsStr::to_str)? + .starts_with("python3.") + { + if symlink_path.join(SITE_PACKAGES).is_dir() { + return Some(symlink_path); + } + } + } + + None + }) + .collect(); + + // If a `python3.X` directory does exist (and `3.X` matches the current Python version), + // prefer it over any other Python directories. + if let Some(python_version) = python_version { + if let Some(preferred_dir) = candidate_dirs.iter().find(|dir| { + dir.file_name() + .and_then(OsStr::to_str) + .map_or(false, |name| name == python_version.dir()) + }) { + debug!("Found path `{}`", preferred_dir.display()); + return Some(preferred_dir.join(SITE_PACKAGES)); + } + } + + // Fallback to the first `python3.X` directory that we found. + let default_dir = candidate_dirs.first()?; + debug!("Found path `{}`", default_dir.display()); + Some(default_dir.join(SITE_PACKAGES)) +} + +fn get_paths_from_pth_files(parent_dir: &Path) -> Vec { + fs::read_dir(parent_dir) + .unwrap() + .flatten() + .filter(|entry| { + // Collect all *.pth files. + let Ok(file_type) = entry.file_type() else { + return false; + }; + file_type.is_file() || file_type.is_symlink() + }) + .map(|entry| entry.path()) + .filter(|path| path.extension() == Some(OsStr::new("pth"))) + .filter(|path| { + // Skip all files that are much larger than expected. + let Ok(metadata) = path.metadata() else { + return false; + }; + let file_len = metadata.len(); + file_len > 0 && file_len < 64 * 1024 + }) + .filter_map(|path| { + let data = fs::read_to_string(&path).ok()?; + for line in data.lines() { + let trimmed_line = line.trim(); + if !trimmed_line.is_empty() + && !trimmed_line.starts_with('#') + && !trimmed_line.starts_with("import") + { + let pth_path = parent_dir.join(trimmed_line); + if pth_path.is_dir() { + return Some(pth_path); + } + } + } + None + }) + .collect() +} + +/// Find the Python search paths for the given virtual environment. +pub(crate) fn find_python_search_paths( + config: &Config, + host: &Host, +) -> Vec { + if let Some(venv_path) = config.venv_path.as_ref() { + if let Some(venv) = config.venv.as_ref() { + let mut found_paths = vec![]; + + for lib_name in ["lib", "Lib", "lib64"] { + let lib_path = venv_path.join(venv).join(lib_name); + if let Some(site_packages_path) = + find_site_packages_path(&lib_path, config.default_python_version) + { + // Add paths from any `.pth` files in each of the `site-packages` directories. + found_paths.extend(get_paths_from_pth_files(&site_packages_path)); + + // Add the `site-packages` directory to the search path. + found_paths.push(site_packages_path); + } + } + + if !found_paths.is_empty() { + found_paths.sort(); + found_paths.dedup(); + + debug!("Found the following `site-packages` dirs"); + for path in &found_paths { + debug!(" {}", path.display()); + } + + return found_paths; + } + } + } + + // Fall back to the Python interpreter. + host.python_search_paths() +} + +/// Determine the relevant Python search paths. +fn get_python_search_paths(config: &Config, host: &Host) -> Vec { + // TODO(charlie): Cache search paths. + find_python_search_paths(config, host) +} + +/// Determine the root of the `typeshed` directory. +pub(crate) fn get_typeshed_root(config: &Config, host: &Host) -> Option { + if let Some(typeshed_path) = config.typeshed_path.as_ref() { + // Did the user specify a typeshed path? + if typeshed_path.is_dir() { + return Some(typeshed_path.clone()); + } + } else { + // If not, we'll look in the Python search paths. + for python_search_path in get_python_search_paths(config, host) { + let possible_typeshed_path = python_search_path.join("typeshed"); + if possible_typeshed_path.is_dir() { + return Some(possible_typeshed_path); + } + } + } + + None +} + +/// Format the expected `typeshed` subdirectory. +fn format_typeshed_subdirectory(typeshed_path: &Path, is_stdlib: bool) -> PathBuf { + typeshed_path.join(if is_stdlib { "stdlib" } else { "stubs" }) +} + +/// Determine the current `typeshed` subdirectory. +fn get_typeshed_subdirectory( + is_stdlib: bool, + config: &Config, + host: &Host, +) -> Option { + let typeshed_path = get_typeshed_root(config, host)?; + let typeshed_path = format_typeshed_subdirectory(&typeshed_path, is_stdlib); + if typeshed_path.is_dir() { + Some(typeshed_path) + } else { + None + } +} + +/// Determine the current `typeshed` subdirectory for the standard library. +pub(crate) fn get_stdlib_typeshed_path( + config: &Config, + host: &Host, +) -> Option { + get_typeshed_subdirectory(true, config, host) +} + +/// Generate a map from PyPI-registered package name to a list of paths +/// containing the package's stubs. +fn build_typeshed_third_party_package_map(third_party_dir: &Path) -> HashMap> { + let mut package_map = HashMap::new(); + + // Iterate over every directory. + for outer_entry in fs::read_dir(third_party_dir).unwrap() { + let outer_entry = outer_entry.unwrap(); + if outer_entry.file_type().unwrap().is_dir() { + // Iterate over any subdirectory children. + for inner_entry in fs::read_dir(outer_entry.path()).unwrap() { + let inner_entry = inner_entry.unwrap(); + + if inner_entry.file_type().unwrap().is_dir() { + package_map + .entry(inner_entry.file_name().to_string_lossy().to_string()) + .or_insert_with(Vec::new) + .push(outer_entry.path()); + } else if inner_entry.file_type().unwrap().is_file() { + if inner_entry + .path() + .extension() + .map_or(false, |extension| extension == "pyi") + { + let stripped_file_name = inner_entry + .path() + .file_stem() + .unwrap() + .to_string_lossy() + .to_string(); + package_map + .entry(stripped_file_name) + .or_insert_with(Vec::new) + .push(outer_entry.path()); + } + } + } + } + } + + package_map +} + +/// Determine the current `typeshed` subdirectory for a third-party package. +pub(crate) fn get_third_party_typeshed_package_paths( + module_descriptor: &ImportModuleDescriptor, + config: &Config, + host: &Host, +) -> Option> { + let typeshed_path = get_typeshed_subdirectory(false, config, host)?; + let package_paths = build_typeshed_third_party_package_map(&typeshed_path); + let first_name_part = module_descriptor.name_parts.first().map(String::as_str)?; + package_paths.get(first_name_part).cloned() +} From 0585e14d3b64cf4df17f979f96990ef75904d994 Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Tue, 27 Jun 2023 11:39:56 -0500 Subject: [PATCH 247/447] Add applicability to flake8_pytest_style (#5389) --- .../flake8_pytest_style/rules/assertion.rs | 3 +-- .../flake8_pytest_style/rules/fixture.rs | 15 +++++-------- .../rules/flake8_pytest_style/rules/marks.rs | 9 +++----- .../flake8_pytest_style/rules/parametrize.rs | 21 +++++++------------ ...e8_pytest_style__tests__PT001_default.snap | 6 +++--- ...st_style__tests__PT001_no_parentheses.snap | 12 +++++------ ...flake8_pytest_style__tests__PT006_csv.snap | 4 ++-- ...e8_pytest_style__tests__PT006_default.snap | 4 ++-- ...lake8_pytest_style__tests__PT006_list.snap | 4 ++-- ...es__flake8_pytest_style__tests__PT022.snap | 2 +- ...e8_pytest_style__tests__PT023_default.snap | 10 ++++----- ...st_style__tests__PT023_no_parentheses.snap | 10 ++++----- ...es__flake8_pytest_style__tests__PT024.snap | 8 +++---- ...es__flake8_pytest_style__tests__PT025.snap | 4 ++-- 14 files changed, 48 insertions(+), 64 deletions(-) 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 e6984f2456..c4272d4a5f 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs @@ -200,8 +200,7 @@ pub(crate) fn unittest_assertion( && !has_comments_in(expr.range(), checker.locator) { if let Ok(stmt) = unittest_assert.generate_assert(args, keywords) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().stmt(&stmt), expr.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 ba447ce25f..7cf8ff64f9 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs @@ -295,8 +295,7 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &D && args.is_empty() && keywords.is_empty() { - #[allow(deprecated)] - let fix = Fix::unspecified(Edit::deletion(func.end(), decorator.end())); + let fix = Fix::automatic(Edit::deletion(func.end(), decorator.end())); pytest_fixture_parentheses( checker, decorator, @@ -346,8 +345,7 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &D if checker.enabled(Rule::PytestFixtureIncorrectParenthesesStyle) && checker.settings.flake8_pytest_style.fixture_parentheses { - #[allow(deprecated)] - let fix = Fix::unspecified(Edit::insertion( + let fix = Fix::automatic(Edit::insertion( Parentheses::Empty.to_string(), decorator.end(), )); @@ -406,8 +404,7 @@ fn check_fixture_returns(checker: &mut Checker, stmt: &Stmt, name: &str, body: & stmt.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( "return".to_string(), TextRange::at(stmt.start(), "yield".text_len()), ))); @@ -486,8 +483,7 @@ fn check_fixture_marks(checker: &mut Checker, decorators: &[Decorator]) { Diagnostic::new(PytestUnnecessaryAsyncioMarkOnFixture, expr.range()); if checker.patch(diagnostic.kind.rule()) { let range = checker.locator.full_lines_range(expr.range()); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(range))); + diagnostic.set_fix(Fix::automatic(Edit::range_deletion(range))); } checker.diagnostics.push(diagnostic); } @@ -499,8 +495,7 @@ fn check_fixture_marks(checker: &mut Checker, decorators: &[Decorator]) { Diagnostic::new(PytestErroneousUseFixturesOnFixture, expr.range()); if checker.patch(diagnostic.kind.rule()) { let line_range = checker.locator.full_lines_range(expr.range()); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(line_range))); + diagnostic.set_fix(Fix::automatic(Edit::range_deletion(line_range))); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/marks.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/marks.rs index 40b34ea309..f13040575f 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/marks.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/marks.rs @@ -83,15 +83,13 @@ fn check_mark_parentheses(checker: &mut Checker, decorator: &Decorator, call_pat && args.is_empty() && keywords.is_empty() { - #[allow(deprecated)] - let fix = Fix::unspecified(Edit::deletion(func.end(), decorator.end())); + let fix = Fix::automatic(Edit::deletion(func.end(), decorator.end())); pytest_mark_parentheses(checker, decorator, call_path, fix, "", "()"); } } _ => { if checker.settings.flake8_pytest_style.mark_parentheses { - #[allow(deprecated)] - let fix = Fix::unspecified(Edit::insertion("()".to_string(), decorator.end())); + let fix = Fix::automatic(Edit::insertion("()".to_string(), decorator.end())); pytest_mark_parentheses(checker, decorator, call_path, fix, "()", ""); } } @@ -114,8 +112,7 @@ fn check_useless_usefixtures(checker: &mut Checker, decorator: &Decorator, call_ if !has_parameters { let mut diagnostic = Diagnostic::new(PytestUseFixturesWithoutParameters, decorator.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(decorator.range()))); + diagnostic.set_fix(Fix::suggested(Edit::range_deletion(decorator.range()))); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/parametrize.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/parametrize.rs index 8db8d75c23..848b5175a3 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/parametrize.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/parametrize.rs @@ -163,8 +163,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) { ctx: ExprContext::Load, range: TextRange::default(), }); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( format!("({})", checker.generator().expr(&node)), name_range, ))); @@ -195,8 +194,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) { ctx: ExprContext::Load, range: TextRange::default(), }); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&node), name_range, ))); @@ -228,8 +226,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) { ctx: ExprContext::Load, range: TextRange::default(), }); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&node), expr.range(), ))); @@ -245,8 +242,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) { ); if checker.patch(diagnostic.kind.rule()) { if let Some(content) = elts_to_csv(elts, checker.generator()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( content, expr.range(), ))); @@ -278,8 +274,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) { ctx: ExprContext::Load, range: TextRange::default(), }); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( format!("({})", checker.generator().expr(&node)), expr.range(), ))); @@ -295,8 +290,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) { ); if checker.patch(diagnostic.kind.rule()) { if let Some(content) = elts_to_csv(elts, checker.generator()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( content, expr.range(), ))); @@ -373,8 +367,7 @@ fn handle_single_name(checker: &mut Checker, expr: &Expr, value: &Expr) { if checker.patch(diagnostic.kind.rule()) { let node = value.clone(); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( checker.generator().expr(&node), expr.range(), ))); diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT001_default.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT001_default.snap index 16ef00156f..ccfd30d007 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT001_default.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT001_default.snap @@ -10,7 +10,7 @@ PT001.py:9:1: PT001 [*] Use `@pytest.fixture()` over `@pytest.fixture` | = help: Add parentheses -ℹ Suggested fix +ℹ Fix 6 6 | # `import pytest` 7 7 | 8 8 | @@ -29,7 +29,7 @@ PT001.py:34:1: PT001 [*] Use `@pytest.fixture()` over `@pytest.fixture` | = help: Add parentheses -ℹ Suggested fix +ℹ Fix 31 31 | # `from pytest import fixture` 32 32 | 33 33 | @@ -48,7 +48,7 @@ PT001.py:59:1: PT001 [*] Use `@pytest.fixture()` over `@pytest.fixture` | = help: Add parentheses -ℹ Suggested fix +ℹ Fix 56 56 | # `from pytest import fixture as aliased` 57 57 | 58 58 | diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT001_no_parentheses.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT001_no_parentheses.snap index ffbff683cf..408075a3cf 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT001_no_parentheses.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT001_no_parentheses.snap @@ -10,7 +10,7 @@ PT001.py:14:1: PT001 [*] Use `@pytest.fixture` over `@pytest.fixture()` | = help: Remove parentheses -ℹ Suggested fix +ℹ Fix 11 11 | return 42 12 12 | 13 13 | @@ -31,7 +31,7 @@ PT001.py:24:1: PT001 [*] Use `@pytest.fixture` over `@pytest.fixture()` | = help: Remove parentheses -ℹ Suggested fix +ℹ Fix 21 21 | return 42 22 22 | 23 23 | @@ -52,7 +52,7 @@ PT001.py:39:1: PT001 [*] Use `@pytest.fixture` over `@pytest.fixture()` | = help: Remove parentheses -ℹ Suggested fix +ℹ Fix 36 36 | return 42 37 37 | 38 38 | @@ -73,7 +73,7 @@ PT001.py:49:1: PT001 [*] Use `@pytest.fixture` over `@pytest.fixture()` | = help: Remove parentheses -ℹ Suggested fix +ℹ Fix 46 46 | return 42 47 47 | 48 48 | @@ -94,7 +94,7 @@ PT001.py:64:1: PT001 [*] Use `@pytest.fixture` over `@pytest.fixture()` | = help: Remove parentheses -ℹ Suggested fix +ℹ Fix 61 61 | return 42 62 62 | 63 63 | @@ -115,7 +115,7 @@ PT001.py:74:1: PT001 [*] Use `@pytest.fixture` over `@pytest.fixture()` | = help: Remove parentheses -ℹ Suggested fix +ℹ Fix 71 71 | return 42 72 72 | 73 73 | diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_csv.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_csv.snap index 33f628f212..47d6acb99c 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_csv.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_csv.snap @@ -29,7 +29,7 @@ PT006.py:29:26: PT006 [*] Wrong name(s) type in `@pytest.mark.parametrize`, expe | = help: Use a `csv` for parameter names -ℹ Suggested fix +ℹ Fix 26 26 | ... 27 27 | 28 28 | @@ -67,7 +67,7 @@ PT006.py:39:26: PT006 [*] Wrong name(s) type in `@pytest.mark.parametrize`, expe | = help: Use a `csv` for parameter names -ℹ Suggested fix +ℹ Fix 36 36 | ... 37 37 | 38 38 | diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_default.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_default.snap index 2aad073076..9ed5a819f4 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_default.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_default.snap @@ -67,7 +67,7 @@ PT006.py:29:26: PT006 [*] Wrong name(s) type in `@pytest.mark.parametrize`, expe | = help: Use a `csv` for parameter names -ℹ Suggested fix +ℹ Fix 26 26 | ... 27 27 | 28 28 | @@ -105,7 +105,7 @@ PT006.py:39:26: PT006 [*] Wrong name(s) type in `@pytest.mark.parametrize`, expe | = help: Use a `csv` for parameter names -ℹ Suggested fix +ℹ Fix 36 36 | ... 37 37 | 38 38 | diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_list.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_list.snap index c052285e34..3cd37fa141 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_list.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_list.snap @@ -86,7 +86,7 @@ PT006.py:29:26: PT006 [*] Wrong name(s) type in `@pytest.mark.parametrize`, expe | = help: Use a `csv` for parameter names -ℹ Suggested fix +ℹ Fix 26 26 | ... 27 27 | 28 28 | @@ -105,7 +105,7 @@ PT006.py:39:26: PT006 [*] Wrong name(s) type in `@pytest.mark.parametrize`, expe | = help: Use a `csv` for parameter names -ℹ Suggested fix +ℹ Fix 36 36 | ... 37 37 | 38 38 | diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT022.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT022.snap index a54bb6a1ff..96594ee97c 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT022.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT022.snap @@ -10,7 +10,7 @@ PT022.py:17:5: PT022 [*] No teardown in fixture `error`, use `return` instead of | = help: Replace `yield` with `return` -ℹ Suggested fix +ℹ Fix 14 14 | @pytest.fixture() 15 15 | def error(): 16 16 | resource = acquire_resource() diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT023_default.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT023_default.snap index 7a0ba21063..03d769bf5a 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT023_default.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT023_default.snap @@ -10,7 +10,7 @@ PT023.py:12:1: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` | = help: Add/remove parentheses -ℹ Suggested fix +ℹ Fix 9 9 | # Without parentheses 10 10 | 11 11 | @@ -29,7 +29,7 @@ PT023.py:17:1: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` | = help: Add/remove parentheses -ℹ Suggested fix +ℹ Fix 14 14 | pass 15 15 | 16 16 | @@ -49,7 +49,7 @@ PT023.py:24:5: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` | = help: Add/remove parentheses -ℹ Suggested fix +ℹ Fix 21 21 | 22 22 | 23 23 | class TestClass: @@ -69,7 +69,7 @@ PT023.py:30:5: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` | = help: Add/remove parentheses -ℹ Suggested fix +ℹ Fix 27 27 | 28 28 | 29 29 | class TestClass: @@ -90,7 +90,7 @@ PT023.py:38:9: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` | = help: Add/remove parentheses -ℹ Suggested fix +ℹ Fix 35 35 | 36 36 | class TestClass: 37 37 | class TestNestedClass: diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT023_no_parentheses.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT023_no_parentheses.snap index c85f180c7b..8a049d4f70 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT023_no_parentheses.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT023_no_parentheses.snap @@ -10,7 +10,7 @@ PT023.py:46:1: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` | = help: Add/remove parentheses -ℹ Suggested fix +ℹ Fix 43 43 | # With parentheses 44 44 | 45 45 | @@ -29,7 +29,7 @@ PT023.py:51:1: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` | = help: Add/remove parentheses -ℹ Suggested fix +ℹ Fix 48 48 | pass 49 49 | 50 50 | @@ -49,7 +49,7 @@ PT023.py:58:5: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` | = help: Add/remove parentheses -ℹ Suggested fix +ℹ Fix 55 55 | 56 56 | 57 57 | class TestClass: @@ -69,7 +69,7 @@ PT023.py:64:5: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` | = help: Add/remove parentheses -ℹ Suggested fix +ℹ Fix 61 61 | 62 62 | 63 63 | class TestClass: @@ -90,7 +90,7 @@ PT023.py:72:9: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` | = help: Add/remove parentheses -ℹ Suggested fix +ℹ Fix 69 69 | 70 70 | class TestClass: 71 71 | class TestNestedClass: diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT024.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT024.snap index ec90561e26..14fd58d9d6 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT024.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT024.snap @@ -10,7 +10,7 @@ PT024.py:14:1: PT024 [*] `pytest.mark.asyncio` is unnecessary for fixtures | = help: Remove `pytest.mark.asyncio` -ℹ Suggested fix +ℹ Fix 11 11 | pass 12 12 | 13 13 | @@ -28,7 +28,7 @@ PT024.py:20:1: PT024 [*] `pytest.mark.asyncio` is unnecessary for fixtures | = help: Remove `pytest.mark.asyncio` -ℹ Suggested fix +ℹ Fix 17 17 | return 0 18 18 | 19 19 | @@ -47,7 +47,7 @@ PT024.py:27:1: PT024 [*] `pytest.mark.asyncio` is unnecessary for fixtures | = help: Remove `pytest.mark.asyncio` -ℹ Suggested fix +ℹ Fix 24 24 | 25 25 | 26 26 | @pytest.fixture() @@ -66,7 +66,7 @@ PT024.py:33:1: PT024 [*] `pytest.mark.asyncio` is unnecessary for fixtures | = help: Remove `pytest.mark.asyncio` -ℹ Suggested fix +ℹ Fix 30 30 | 31 31 | 32 32 | @pytest.fixture() diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT025.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT025.snap index d3f4789403..181945326c 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT025.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT025.snap @@ -10,7 +10,7 @@ PT025.py:9:1: PT025 [*] `pytest.mark.usefixtures` has no effect on fixtures | = help: Remove `pytest.mark.usefixtures` -ℹ Suggested fix +ℹ Fix 6 6 | pass 7 7 | 8 8 | @@ -29,7 +29,7 @@ PT025.py:16:1: PT025 [*] `pytest.mark.usefixtures` has no effect on fixtures | = help: Remove `pytest.mark.usefixtures` -ℹ Suggested fix +ℹ Fix 13 13 | 14 14 | 15 15 | @pytest.fixture() From ff0d0ab7a00b78b75a73106a916adaedce7cf11a Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Tue, 27 Jun 2023 11:40:19 -0500 Subject: [PATCH 248/447] Add applicability to pydocstyle (#5390) --- .../pydocstyle/rules/blank_after_summary.rs | 3 +- .../rules/blank_before_after_class.rs | 9 ++-- .../rules/blank_before_after_function.rs | 6 +-- .../src/rules/pydocstyle/rules/capitalized.rs | 3 +- .../pydocstyle/rules/ends_with_period.rs | 3 +- .../pydocstyle/rules/ends_with_punctuation.rs | 3 +- .../ruff/src/rules/pydocstyle/rules/indent.rs | 9 ++-- .../rules/multi_line_summary_start.rs | 6 +-- .../rules/newline_after_last_paragraph.rs | 3 +- .../rules/no_surrounding_whitespace.rs | 3 +- .../src/rules/pydocstyle/rules/one_liner.rs | 3 +- .../src/rules/pydocstyle/rules/sections.rs | 42 ++++++------------ ...__rules__pydocstyle__tests__D201_D.py.snap | 8 ++-- ...__rules__pydocstyle__tests__D202_D.py.snap | 8 ++-- ...ules__pydocstyle__tests__D202_D202.py.snap | 6 +-- ...__rules__pydocstyle__tests__D203_D.py.snap | 6 +-- ...__rules__pydocstyle__tests__D204_D.py.snap | 4 +- ...__rules__pydocstyle__tests__D205_D.py.snap | 2 +- ...__rules__pydocstyle__tests__D207_D.py.snap | 8 ++-- ...__rules__pydocstyle__tests__D208_D.py.snap | 6 +-- ...__rules__pydocstyle__tests__D209_D.py.snap | 4 +- ...__rules__pydocstyle__tests__D210_D.py.snap | 6 +-- ...__rules__pydocstyle__tests__D211_D.py.snap | 4 +- ...__rules__pydocstyle__tests__D212_D.py.snap | 6 +-- ...__rules__pydocstyle__tests__D213_D.py.snap | 44 +++++++++---------- ...ydocstyle__tests__D214_D214_module.py.snap | 4 +- ...__pydocstyle__tests__D214_sections.py.snap | 2 +- ...__pydocstyle__tests__D215_sections.py.snap | 4 +- ...ules__pydocstyle__tests__D403_D403.py.snap | 2 +- ...__pydocstyle__tests__D405_sections.py.snap | 4 +- ...__pydocstyle__tests__D406_sections.py.snap | 4 +- ...__pydocstyle__tests__D407_sections.py.snap | 34 +++++++------- ...__pydocstyle__tests__D408_sections.py.snap | 2 +- ...__pydocstyle__tests__D409_sections.py.snap | 4 +- ...__pydocstyle__tests__D410_sections.py.snap | 4 +- ...__pydocstyle__tests__D411_sections.py.snap | 6 +-- ...__pydocstyle__tests__D412_sections.py.snap | 2 +- ...__pydocstyle__tests__D413_sections.py.snap | 2 +- ...__rules__pydocstyle__tests__d209_d400.snap | 2 +- 39 files changed, 125 insertions(+), 156 deletions(-) diff --git a/crates/ruff/src/rules/pydocstyle/rules/blank_after_summary.rs b/crates/ruff/src/rules/pydocstyle/rules/blank_after_summary.rs index 423b77f78d..c655df13e0 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/blank_after_summary.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/blank_after_summary.rs @@ -75,8 +75,7 @@ pub(crate) fn blank_after_summary(checker: &mut Checker, docstring: &Docstring) } // Insert one blank line after the summary (replacing any existing lines). - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::replacement( + diagnostic.set_fix(Fix::automatic(Edit::replacement( checker.stylist.line_ending().to_string(), summary_end, blank_end, 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 5b1a46a174..6a71f725b4 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 @@ -97,8 +97,7 @@ pub(crate) fn blank_before_after_class(checker: &mut Checker, docstring: &Docstr ); if checker.patch(diagnostic.kind.rule()) { // Delete the blank line before the class. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::deletion( + diagnostic.set_fix(Fix::automatic(Edit::deletion( blank_lines_start, docstring.start() - docstring.indentation.text_len(), ))); @@ -116,8 +115,7 @@ pub(crate) fn blank_before_after_class(checker: &mut Checker, docstring: &Docstr ); if checker.patch(diagnostic.kind.rule()) { // Insert one blank line before the class. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::replacement( + diagnostic.set_fix(Fix::automatic(Edit::replacement( checker.stylist.line_ending().to_string(), blank_lines_start, docstring.start() - docstring.indentation.text_len(), @@ -163,8 +161,7 @@ pub(crate) fn blank_before_after_class(checker: &mut Checker, docstring: &Docstr ); if checker.patch(diagnostic.kind.rule()) { // Insert a blank line before the class (replacing any existing lines). - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::replacement( + diagnostic.set_fix(Fix::automatic(Edit::replacement( checker.stylist.line_ending().to_string(), first_line_start, blank_lines_end, 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 47aa4e32e1..337d02b789 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 @@ -86,8 +86,7 @@ pub(crate) fn blank_before_after_function(checker: &mut Checker, docstring: &Doc ); if checker.patch(diagnostic.kind.rule()) { // Delete the blank line before the docstring. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::deletion( + diagnostic.set_fix(Fix::automatic(Edit::deletion( blank_lines_start, docstring.start() - docstring.indentation.text_len(), ))); @@ -143,8 +142,7 @@ pub(crate) fn blank_before_after_function(checker: &mut Checker, docstring: &Doc ); if checker.patch(diagnostic.kind.rule()) { // Delete the blank line after the docstring. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::deletion( + diagnostic.set_fix(Fix::automatic(Edit::deletion( first_line_end, blank_lines_end, ))); diff --git a/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs b/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs index 739cf7a44b..7ae7030663 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs @@ -76,8 +76,7 @@ pub(crate) fn capitalized(checker: &mut Checker, docstring: &Docstring) { ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( capitalized_word, TextRange::at(body.start(), first_word.text_len()), ))); diff --git a/crates/ruff/src/rules/pydocstyle/rules/ends_with_period.rs b/crates/ruff/src/rules/pydocstyle/rules/ends_with_period.rs index 85045134a8..a870568d5e 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/ends_with_period.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/ends_with_period.rs @@ -64,8 +64,7 @@ pub(crate) fn ends_with_period(checker: &mut Checker, docstring: &Docstring) { && !trimmed.ends_with(':') && !trimmed.ends_with(';') { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::insertion( + diagnostic.set_fix(Fix::suggested(Edit::insertion( ".".to_string(), line.start() + trimmed.text_len(), ))); diff --git a/crates/ruff/src/rules/pydocstyle/rules/ends_with_punctuation.rs b/crates/ruff/src/rules/pydocstyle/rules/ends_with_punctuation.rs index 8341266f01..bd6302cb9e 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/ends_with_punctuation.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/ends_with_punctuation.rs @@ -61,8 +61,7 @@ pub(crate) fn ends_with_punctuation(checker: &mut Checker, docstring: &Docstring let mut diagnostic = Diagnostic::new(EndsInPunctuation, docstring.range()); // Best-effort autofix: avoid adding a period after other punctuation marks. if checker.patch(diagnostic.kind.rule()) && !trimmed.ends_with([':', ';']) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::insertion( + diagnostic.set_fix(Fix::suggested(Edit::insertion( ".".to_string(), line.start() + trimmed.text_len(), ))); diff --git a/crates/ruff/src/rules/pydocstyle/rules/indent.rs b/crates/ruff/src/rules/pydocstyle/rules/indent.rs index 3be1583909..d9d352dfcd 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/indent.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/indent.rs @@ -91,8 +91,7 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) { let mut diagnostic = Diagnostic::new(UnderIndentation, TextRange::empty(line.start())); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( clean_space(docstring.indentation), TextRange::at(line.start(), line_indent.text_len()), ))); @@ -139,8 +138,7 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) { } else { Edit::range_replacement(indent, over_indented) }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(edit)); + diagnostic.set_fix(Fix::automatic(edit)); } checker.diagnostics.push(diagnostic); } @@ -160,8 +158,7 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) { } else { Edit::range_replacement(indent, range) }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(edit)); + diagnostic.set_fix(Fix::automatic(edit)); } checker.diagnostics.push(diagnostic); } 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 2288843805..98a1eee00f 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 @@ -66,8 +66,7 @@ pub(crate) fn multi_line_summary_start(checker: &mut Checker, docstring: &Docstr // Delete until first non-whitespace char. for line in content_lines { if let Some(end_column) = line.find(|c: char| !c.is_whitespace()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::deletion( + diagnostic.set_fix(Fix::automatic(Edit::deletion( first_line.end(), line.start() + TextSize::try_from(end_column).unwrap(), ))); @@ -114,8 +113,7 @@ pub(crate) fn multi_line_summary_start(checker: &mut Checker, docstring: &Docstr first_line.strip_prefix(prefix).unwrap().trim_start() ); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::replacement( + diagnostic.set_fix(Fix::automatic(Edit::replacement( repl, body.start(), first_line.end(), diff --git a/crates/ruff/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs b/crates/ruff/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs index 499cfb4bae..6345cbb9b0 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs @@ -58,8 +58,7 @@ pub(crate) fn newline_after_last_paragraph(checker: &mut Checker, docstring: &Do checker.stylist.line_ending().as_str(), clean_space(docstring.indentation) ); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::replacement( + diagnostic.set_fix(Fix::automatic(Edit::replacement( content, docstring.expr.end() - num_trailing_quotes - num_trailing_spaces, docstring.expr.end() - num_trailing_quotes, diff --git a/crates/ruff/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs b/crates/ruff/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs index 105df09759..6f4be82526 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs @@ -47,8 +47,7 @@ pub(crate) fn no_surrounding_whitespace(checker: &mut Checker, docstring: &Docst // characters, avoid applying the fix. if !trimmed.ends_with(quote) && !trimmed.starts_with(quote) && !ends_with_backslash(trimmed) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( trimmed.to_string(), TextRange::at(body.start(), line.text_len()), ))); diff --git a/crates/ruff/src/rules/pydocstyle/rules/one_liner.rs b/crates/ruff/src/rules/pydocstyle/rules/one_liner.rs index 616083fc15..bec528b4ed 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/one_liner.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/one_liner.rs @@ -51,8 +51,7 @@ pub(crate) fn one_liner(checker: &mut Checker, docstring: &Docstring) { if !trimmed.ends_with(trailing.chars().last().unwrap()) && !trimmed.starts_with(leading.chars().last().unwrap()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( format!("{leading}{trimmed}{trailing}"), docstring.range(), ))); diff --git a/crates/ruff/src/rules/pydocstyle/rules/sections.rs b/crates/ruff/src/rules/pydocstyle/rules/sections.rs index 2050c5f973..91b94e2033 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/sections.rs @@ -376,8 +376,7 @@ fn blanks_and_section_underline( let range = TextRange::new(context.following_range().start(), blank_lines_end); // Delete any blank lines between the header and the underline. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(range))); + diagnostic.set_fix(Fix::automatic(Edit::range_deletion(range))); } checker.diagnostics.push(diagnostic); } @@ -405,8 +404,7 @@ fn blanks_and_section_underline( "-".repeat(context.section_name().len()), checker.stylist.line_ending().as_str() ); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::replacement( + diagnostic.set_fix(Fix::automatic(Edit::replacement( content, blank_lines_end, non_blank_line.full_end(), @@ -432,8 +430,7 @@ fn blanks_and_section_underline( ); // Replace the existing indentation with whitespace of the appropriate length. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( clean_space(docstring.indentation), range, ))); @@ -472,8 +469,7 @@ fn blanks_and_section_underline( ); if checker.patch(diagnostic.kind.rule()) { // Delete any blank lines between the header and content. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::deletion( + diagnostic.set_fix(Fix::automatic(Edit::deletion( line_after_dashes.start(), blank_lines_after_dashes_end, ))); @@ -507,8 +503,7 @@ fn blanks_and_section_underline( clean_space(docstring.indentation), "-".repeat(context.section_name().len()), ); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::insertion( + diagnostic.set_fix(Fix::automatic(Edit::insertion( content, context.summary_range().end(), ))); @@ -527,8 +522,7 @@ fn blanks_and_section_underline( let range = TextRange::new(context.following_range().start(), blank_lines_end); // Delete any blank lines between the header and content. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(range))); + diagnostic.set_fix(Fix::automatic(Edit::range_deletion(range))); } checker.diagnostics.push(diagnostic); } @@ -553,8 +547,7 @@ fn blanks_and_section_underline( "-".repeat(context.section_name().len()), ); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::insertion( + diagnostic.set_fix(Fix::automatic(Edit::insertion( content, context.summary_range().end(), ))); @@ -591,8 +584,7 @@ fn common_section( // Replace the section title with the capitalized variant. This requires // locating the start and end of the section name. let section_range = context.section_name_range(); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( capitalized_section_name.to_string(), section_range, ))); @@ -615,8 +607,7 @@ fn common_section( let content = clean_space(docstring.indentation); let fix_range = TextRange::at(context.range().start(), leading_space.text_len()); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(if content.is_empty() { + diagnostic.set_fix(Fix::automatic(if content.is_empty() { Edit::range_deletion(fix_range) } else { Edit::range_replacement(content, fix_range) @@ -639,8 +630,7 @@ fn common_section( ); if checker.patch(diagnostic.kind.rule()) { // Add a newline at the beginning of the next section. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::insertion( + diagnostic.set_fix(Fix::automatic(Edit::insertion( line_end.to_string(), next.range().start(), ))); @@ -657,8 +647,7 @@ fn common_section( ); if checker.patch(diagnostic.kind.rule()) { // Add a newline after the section. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::insertion( + diagnostic.set_fix(Fix::automatic(Edit::insertion( format!("{}{}", line_end, docstring.indentation), context.range().end(), ))); @@ -678,8 +667,7 @@ fn common_section( ); if checker.patch(diagnostic.kind.rule()) { // Add a blank line before the section. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::insertion( + diagnostic.set_fix(Fix::automatic(Edit::insertion( line_end.to_string(), context.range().start(), ))); @@ -882,8 +870,7 @@ fn numpy_section( ); if checker.patch(diagnostic.kind.rule()) { let section_range = context.section_name_range(); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(TextRange::at( + diagnostic.set_fix(Fix::automatic(Edit::range_deletion(TextRange::at( section_range.end(), suffix.text_len(), )))); @@ -920,8 +907,7 @@ fn google_section( if checker.patch(diagnostic.kind.rule()) { // Replace the suffix. let section_name_range = context.section_name_range(); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( ":".to_string(), TextRange::at(section_name_range.end(), suffix.text_len()), ))); diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D201_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D201_D.py.snap index 48fb3a6a40..9f8287aa46 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D201_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D201_D.py.snap @@ -10,7 +10,7 @@ D.py:137:5: D201 [*] No blank lines allowed before function docstring (found 1) | = help: Remove blank line(s) before function docstring -ℹ Suggested fix +ℹ Fix 133 133 | 134 134 | @expect('D201: No blank lines allowed before function docstring (found 1)') 135 135 | def leading_space(): @@ -30,7 +30,7 @@ D.py:151:5: D201 [*] No blank lines allowed before function docstring (found 1) | = help: Remove blank line(s) before function docstring -ℹ Suggested fix +ℹ Fix 147 147 | @expect('D201: No blank lines allowed before function docstring (found 1)') 148 148 | @expect('D202: No blank lines allowed after function docstring (found 1)') 149 149 | def trailing_and_leading_space(): @@ -52,7 +52,7 @@ D.py:546:5: D201 [*] No blank lines allowed before function docstring (found 1) | = help: Remove blank line(s) before function docstring -ℹ Suggested fix +ℹ Fix 542 542 | @expect('D201: No blank lines allowed before function docstring (found 1)') 543 543 | @expect('D213: Multi-line docstring summary should start at the second line') 544 544 | def multiline_leading_space(): @@ -76,7 +76,7 @@ D.py:568:5: D201 [*] No blank lines allowed before function docstring (found 1) | = help: Remove blank line(s) before function docstring -ℹ Suggested fix +ℹ Fix 564 564 | @expect('D202: No blank lines allowed after function docstring (found 1)') 565 565 | @expect('D213: Multi-line docstring summary should start at the second line') 566 566 | def multiline_trailing_and_leading_space(): diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D202_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D202_D.py.snap index 94d9380103..e620ddc640 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D202_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D202_D.py.snap @@ -12,7 +12,7 @@ D.py:142:5: D202 [*] No blank lines allowed after function docstring (found 1) | = help: Remove blank line(s) after function docstring -ℹ Suggested fix +ℹ Fix 140 140 | @expect('D202: No blank lines allowed after function docstring (found 1)') 141 141 | def trailing_space(): 142 142 | """Leading space.""" @@ -32,7 +32,7 @@ D.py:151:5: D202 [*] No blank lines allowed after function docstring (found 1) | = help: Remove blank line(s) after function docstring -ℹ Suggested fix +ℹ Fix 149 149 | def trailing_and_leading_space(): 150 150 | 151 151 | """Trailing and leading space.""" @@ -56,7 +56,7 @@ D.py:555:5: D202 [*] No blank lines allowed after function docstring (found 1) | = help: Remove blank line(s) after function docstring -ℹ Suggested fix +ℹ Fix 556 556 | 557 557 | More content. 558 558 | """ @@ -80,7 +80,7 @@ D.py:568:5: D202 [*] No blank lines allowed after function docstring (found 1) | = help: Remove blank line(s) after function docstring -ℹ Suggested fix +ℹ Fix 569 569 | 570 570 | More content. 571 571 | """ diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D202_D202.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D202_D202.py.snap index 543bc5e331..b05850ca5c 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D202_D202.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D202_D202.py.snap @@ -10,7 +10,7 @@ D202.py:57:5: D202 [*] No blank lines allowed after function docstring (found 2) | = help: Remove blank line(s) after function docstring -ℹ Suggested fix +ℹ Fix 55 55 | # D202 56 56 | def outer(): 57 57 | """This is a docstring.""" @@ -29,7 +29,7 @@ D202.py:68:5: D202 [*] No blank lines allowed after function docstring (found 2) | = help: Remove blank line(s) after function docstring -ℹ Suggested fix +ℹ Fix 66 66 | # D202 67 67 | def outer(): 68 68 | """This is a docstring.""" @@ -50,7 +50,7 @@ D202.py:80:5: D202 [*] No blank lines allowed after function docstring (found 1) | = help: Remove blank line(s) after function docstring -ℹ Suggested fix +ℹ Fix 78 78 | # D202 79 79 | def outer(): 80 80 | """This is a docstring.""" diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D203_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D203_D.py.snap index 62629b10ed..154bcd0e52 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D203_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D203_D.py.snap @@ -9,7 +9,7 @@ D.py:161:5: D203 [*] 1 blank line required before class docstring | = help: Insert 1 blank line before class docstring -ℹ Suggested fix +ℹ Fix 158 158 | 159 159 | 160 160 | class LeadingSpaceMissing: @@ -27,7 +27,7 @@ D.py:192:5: D203 [*] 1 blank line required before class docstring | = help: Insert 1 blank line before class docstring -ℹ Suggested fix +ℹ Fix 189 189 | 190 190 | 191 191 | class LeadingAndTrailingSpaceMissing: @@ -54,7 +54,7 @@ D.py:526:5: D203 [*] 1 blank line required before class docstring | = help: Insert 1 blank line before class docstring -ℹ Suggested fix +ℹ Fix 523 523 | # This is reproducing a bug where AttributeError is raised when parsing class 524 524 | # parameters as functions for Google / Numpy conventions. 525 525 | class Blah: # noqa: D203,D213 diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D204_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D204_D.py.snap index 9e7a6ecd8c..3d7025563d 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D204_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D204_D.py.snap @@ -11,7 +11,7 @@ D.py:181:5: D204 [*] 1 blank line required after class docstring | = help: Insert 1 blank line after class docstring -ℹ Suggested fix +ℹ Fix 179 179 | class TrailingSpace: 180 180 | 181 181 | """TrailingSpace.""" @@ -29,7 +29,7 @@ D.py:192:5: D204 [*] 1 blank line required after class docstring | = help: Insert 1 blank line after class docstring -ℹ Suggested fix +ℹ Fix 190 190 | 191 191 | class LeadingAndTrailingSpaceMissing: 192 192 | """Leading and trailing space missing.""" diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D205_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D205_D.py.snap index df41898d6d..266342bb41 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D205_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D205_D.py.snap @@ -29,7 +29,7 @@ D.py:210:5: D205 [*] 1 blank line required between summary line and description | = help: Insert single blank line -ℹ Suggested fix +ℹ Fix 209 209 | def multi_line_two_separating_blanks(): 210 210 | """Summary. 211 211 | diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D207_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D207_D.py.snap index 443c759fb9..9c86c51d99 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D207_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D207_D.py.snap @@ -12,7 +12,7 @@ D.py:232:1: D207 [*] Docstring is under-indented | = help: Increase indentation -ℹ Suggested fix +ℹ Fix 229 229 | def asdfsdf(): 230 230 | """Summary. 231 231 | @@ -31,7 +31,7 @@ D.py:244:1: D207 [*] Docstring is under-indented | = help: Increase indentation -ℹ Suggested fix +ℹ Fix 241 241 | 242 242 | Description. 243 243 | @@ -51,7 +51,7 @@ D.py:440:1: D207 [*] Docstring is under-indented | = help: Increase indentation -ℹ Suggested fix +ℹ Fix 437 437 | @expect('D213: Multi-line docstring summary should start at the second line') 438 438 | def docstring_start_in_same_line(): """First Line. 439 439 | @@ -69,7 +69,7 @@ D.py:441:1: D207 [*] Docstring is under-indented | = help: Increase indentation -ℹ Suggested fix +ℹ Fix 438 438 | def docstring_start_in_same_line(): """First Line. 439 439 | 440 440 | Second Line diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D208_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D208_D.py.snap index 66f91b47bb..7fe7b24f84 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D208_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D208_D.py.snap @@ -12,7 +12,7 @@ D.py:252:1: D208 [*] Docstring is over-indented | = help: Remove over-indentation -ℹ Suggested fix +ℹ Fix 249 249 | def asdfsdsdf24(): 250 250 | """Summary. 251 251 | @@ -31,7 +31,7 @@ D.py:264:1: D208 [*] Docstring is over-indented | = help: Remove over-indentation -ℹ Suggested fix +ℹ Fix 261 261 | 262 262 | Description. 263 263 | @@ -52,7 +52,7 @@ D.py:272:1: D208 [*] Docstring is over-indented | = help: Remove over-indentation -ℹ Suggested fix +ℹ Fix 269 269 | def asdfsdfsdsdsdfsdf24(): 270 270 | """Summary. 271 271 | diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D209_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D209_D.py.snap index 7861e2dbc6..01e3838be0 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D209_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D209_D.py.snap @@ -13,7 +13,7 @@ D.py:281:5: D209 [*] Multi-line docstring closing quotes should be on a separate | = help: Move closing quotes to new line -ℹ Suggested fix +ℹ Fix 280 280 | def asdfljdf24(): 281 281 | """Summary. 282 282 | @@ -36,7 +36,7 @@ D.py:588:5: D209 [*] Multi-line docstring closing quotes should be on a separate | = help: Move closing quotes to new line -ℹ Suggested fix +ℹ Fix 587 587 | def asdfljdjgf24(): 588 588 | """Summary. 589 589 | diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D210_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D210_D.py.snap index ca75b0221e..8e3a805a4d 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D210_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D210_D.py.snap @@ -10,7 +10,7 @@ D.py:288:5: D210 [*] No whitespaces allowed surrounding docstring text | = help: Trim surrounding whitespace -ℹ Suggested fix +ℹ Fix 285 285 | 286 286 | @expect('D210: No whitespaces allowed surrounding docstring text') 287 287 | def endswith(): @@ -29,7 +29,7 @@ D.py:293:5: D210 [*] No whitespaces allowed surrounding docstring text | = help: Trim surrounding whitespace -ℹ Suggested fix +ℹ Fix 290 290 | 291 291 | @expect('D210: No whitespaces allowed surrounding docstring text') 292 292 | def around(): @@ -52,7 +52,7 @@ D.py:299:5: D210 [*] No whitespaces allowed surrounding docstring text | = help: Trim surrounding whitespace -ℹ Suggested fix +ℹ Fix 296 296 | @expect('D210: No whitespaces allowed surrounding docstring text') 297 297 | @expect('D213: Multi-line docstring summary should start at the second line') 298 298 | def multiline(): diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D211_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D211_D.py.snap index a8e21c2805..729ea91914 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D211_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D211_D.py.snap @@ -10,7 +10,7 @@ D.py:170:5: D211 [*] No blank lines allowed before class docstring | = help: Remove blank line(s) before class docstring -ℹ Suggested fix +ℹ Fix 166 166 | 167 167 | 168 168 | class WithLeadingSpace: @@ -29,7 +29,7 @@ D.py:181:5: D211 [*] No blank lines allowed before class docstring | = help: Remove blank line(s) before class docstring -ℹ Suggested fix +ℹ Fix 177 177 | 178 178 | 179 179 | class TrailingSpace: diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D212_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D212_D.py.snap index 1177bbd802..5d2ea74fd8 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D212_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D212_D.py.snap @@ -13,7 +13,7 @@ D.py:129:5: D212 [*] Multi-line docstring summary should start at the first line | = help: Remove whitespace after opening quotes -ℹ Suggested fix +ℹ Fix 126 126 | '(found 3)') 127 127 | @expect('D212: Multi-line docstring summary should start at the first line') 128 128 | def asdlkfasd(): @@ -36,7 +36,7 @@ D.py:597:5: D212 [*] Multi-line docstring summary should start at the first line | = help: Remove whitespace after opening quotes -ℹ Suggested fix +ℹ Fix 594 594 | '(found 3)') 595 595 | @expect('D212: Multi-line docstring summary should start at the first line') 596 596 | def one_liner(): @@ -60,7 +60,7 @@ D.py:624:5: D212 [*] Multi-line docstring summary should start at the first line | = help: Remove whitespace after opening quotes -ℹ Suggested fix +ℹ Fix 621 621 | '(found 3)') 622 622 | @expect('D212: Multi-line docstring summary should start at the first line') 623 623 | def one_liner(): diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D213_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D213_D.py.snap index 6651cc1aec..070f70b2c8 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D213_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D213_D.py.snap @@ -14,7 +14,7 @@ D.py:200:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 197 197 | '(found 0)') 198 198 | @expect('D213: Multi-line docstring summary should start at the second line') 199 199 | def multi_line_zero_separating_blanks(): @@ -40,7 +40,7 @@ D.py:210:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 207 207 | '(found 2)') 208 208 | @expect('D213: Multi-line docstring summary should start at the second line') 209 209 | def multi_line_two_separating_blanks(): @@ -65,7 +65,7 @@ D.py:220:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 217 217 | 218 218 | @expect('D213: Multi-line docstring summary should start at the second line') 219 219 | def multi_line_one_separating_blanks(): @@ -90,7 +90,7 @@ D.py:230:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 227 227 | @expect('D207: Docstring is under-indented') 228 228 | @expect('D213: Multi-line docstring summary should start at the second line') 229 229 | def asdfsdf(): @@ -115,7 +115,7 @@ D.py:240:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 237 237 | @expect('D207: Docstring is under-indented') 238 238 | @expect('D213: Multi-line docstring summary should start at the second line') 239 239 | def asdsdfsdffsdf(): @@ -140,7 +140,7 @@ D.py:250:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 247 247 | @expect('D208: Docstring is over-indented') 248 248 | @expect('D213: Multi-line docstring summary should start at the second line') 249 249 | def asdfsdsdf24(): @@ -165,7 +165,7 @@ D.py:260:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 257 257 | @expect('D208: Docstring is over-indented') 258 258 | @expect('D213: Multi-line docstring summary should start at the second line') 259 259 | def asdfsdsdfsdf24(): @@ -190,7 +190,7 @@ D.py:270:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 267 267 | @expect('D208: Docstring is over-indented') 268 268 | @expect('D213: Multi-line docstring summary should start at the second line') 269 269 | def asdfsdfsdsdsdfsdf24(): @@ -213,7 +213,7 @@ D.py:281:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 278 278 | 'line') 279 279 | @expect('D213: Multi-line docstring summary should start at the second line') 280 280 | def asdfljdf24(): @@ -237,7 +237,7 @@ D.py:299:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 296 296 | @expect('D210: No whitespaces allowed surrounding docstring text') 297 297 | @expect('D213: Multi-line docstring summary should start at the second line') 298 298 | def multiline(): @@ -263,7 +263,7 @@ D.py:343:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 340 340 | 341 341 | @expect('D213: Multi-line docstring summary should start at the second line') 342 342 | def exceptions_of_D301(): @@ -288,7 +288,7 @@ D.py:383:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 380 380 | 381 381 | @expect('D213: Multi-line docstring summary should start at the second line') 382 382 | def new_209(): @@ -313,7 +313,7 @@ D.py:392:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 389 389 | 390 390 | @expect('D213: Multi-line docstring summary should start at the second line') 391 391 | def old_209(): @@ -337,7 +337,7 @@ D.py:438:37: D213 [*] Multi-line docstring summary should start at the second li | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 435 435 | 436 436 | @expect("D207: Docstring is under-indented") 437 437 | @expect('D213: Multi-line docstring summary should start at the second line') @@ -362,7 +362,7 @@ D.py:450:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 447 447 | 448 448 | @expect('D213: Multi-line docstring summary should start at the second line') 449 449 | def a_following_valid_function(x=None): @@ -391,7 +391,7 @@ D.py:526:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 523 523 | # This is reproducing a bug where AttributeError is raised when parsing class 524 524 | # parameters as functions for Google / Numpy conventions. 525 525 | class Blah: # noqa: D203,D213 @@ -415,7 +415,7 @@ D.py:546:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 543 543 | @expect('D213: Multi-line docstring summary should start at the second line') 544 544 | def multiline_leading_space(): 545 545 | @@ -441,7 +441,7 @@ D.py:555:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 552 552 | @expect('D202: No blank lines allowed after function docstring (found 1)') 553 553 | @expect('D213: Multi-line docstring summary should start at the second line') 554 554 | def multiline_trailing_space(): @@ -467,7 +467,7 @@ D.py:568:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 565 565 | @expect('D213: Multi-line docstring summary should start at the second line') 566 566 | def multiline_trailing_and_leading_space(): 567 567 | @@ -490,7 +490,7 @@ D.py:588:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 585 585 | 'line') 586 586 | @expect('D213: Multi-line docstring summary should start at the second line') 587 587 | def asdfljdjgf24(): @@ -513,7 +513,7 @@ D.py:606:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 603 603 | '(found 3)') 604 604 | @expect('D212: Multi-line docstring summary should start at the first line') 605 605 | def one_liner(): @@ -536,7 +536,7 @@ D.py:615:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 612 612 | '(found 3)') 613 613 | @expect('D212: Multi-line docstring summary should start at the first line') 614 614 | def one_liner(): diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D214_D214_module.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D214_D214_module.py.snap index bc822b0bb6..be8ef02628 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D214_D214_module.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D214_D214_module.py.snap @@ -19,7 +19,7 @@ D214_module.py:1:1: D214 [*] Section is over-indented ("Returns") | = help: Remove over-indentation from "Returns" -ℹ Suggested fix +ℹ Fix 1 1 | """A module docstring with D214 violations 2 2 | 3 |- Returns @@ -46,7 +46,7 @@ D214_module.py:1:1: D214 [*] Section is over-indented ("Args") | = help: Remove over-indentation from "Args" -ℹ Suggested fix +ℹ Fix 4 4 | ----- 5 5 | valid returns 6 6 | diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D214_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D214_sections.py.snap index b4e381024b..a1395220a6 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D214_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D214_sections.py.snap @@ -17,7 +17,7 @@ sections.py:144:5: D214 [*] Section is over-indented ("Returns") | = help: Remove over-indentation from "Returns" -ℹ Suggested fix +ℹ Fix 143 143 | def section_overindented(): # noqa: D416 144 144 | """Toggle the gizmo. 145 145 | diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D215_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D215_sections.py.snap index 02edf6f0dd..4c99c69a03 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D215_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D215_sections.py.snap @@ -17,7 +17,7 @@ sections.py:156:5: D215 [*] Section underline is over-indented ("Returns") | = help: Remove over-indentation from "Returns" underline -ℹ Suggested fix +ℹ Fix 156 156 | """Toggle the gizmo. 157 157 | 158 158 | Returns @@ -41,7 +41,7 @@ sections.py:170:5: D215 [*] Section underline is over-indented ("Returns") | = help: Remove over-indentation from "Returns" underline -ℹ Suggested fix +ℹ Fix 170 170 | """Toggle the gizmo. 171 171 | 172 172 | Returns diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D403_D403.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D403_D403.py.snap index 7203275eba..79e1522fd3 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D403_D403.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D403_D403.py.snap @@ -11,7 +11,7 @@ D403.py:2:5: D403 [*] First word of the first line should be capitalized: `this` | = help: Capitalize `this` to `This` -ℹ Suggested fix +ℹ Fix 1 1 | def bad_function(): 2 |- """this docstring is not capitalized""" 2 |+ """This docstring is not capitalized""" diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D405_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D405_sections.py.snap index 26914a769e..3b3ec3f1df 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D405_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D405_sections.py.snap @@ -17,7 +17,7 @@ sections.py:17:5: D405 [*] Section name should be properly capitalized ("returns | = help: Capitalize "returns" -ℹ Suggested fix +ℹ Fix 16 16 | def not_capitalized(): # noqa: D416 17 17 | """Toggle the gizmo. 18 18 | @@ -51,7 +51,7 @@ sections.py:216:5: D405 [*] Section name should be properly capitalized ("Short | = help: Capitalize "Short summary" -ℹ Suggested fix +ℹ Fix 215 215 | def multiple_sections(): # noqa: D416 216 216 | """Toggle the gizmo. 217 217 | diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D406_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D406_sections.py.snap index fb95343722..b87c43b472 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D406_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D406_sections.py.snap @@ -17,7 +17,7 @@ sections.py:30:5: D406 [*] Section name should end with a newline ("Returns") | = help: Add newline after "Returns" -ℹ Suggested fix +ℹ Fix 29 29 | def superfluous_suffix(): # noqa: D416 30 30 | """Toggle the gizmo. 31 31 | @@ -51,7 +51,7 @@ sections.py:216:5: D406 [*] Section name should end with a newline ("Raises") | = help: Add newline after "Raises" -ℹ Suggested fix +ℹ Fix 224 224 | Returns 225 225 | ------ 226 226 | Many many wonderful things. diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D407_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D407_sections.py.snap index 33128a959e..a0fe542d21 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D407_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D407_sections.py.snap @@ -16,7 +16,7 @@ sections.py:42:5: D407 [*] Missing dashed underline after section ("Returns") | = help: Add dashed line under "Returns" -ℹ Suggested fix +ℹ Fix 42 42 | """Toggle the gizmo. 43 43 | 44 44 | Returns @@ -39,7 +39,7 @@ sections.py:54:5: D407 [*] Missing dashed underline after section ("Returns") | = help: Add dashed line under "Returns" -ℹ Suggested fix +ℹ Fix 54 54 | """Toggle the gizmo. 55 55 | 56 56 | Returns @@ -60,7 +60,7 @@ sections.py:65:5: D407 [*] Missing dashed underline after section ("Returns") | = help: Add dashed line under "Returns" -ℹ Suggested fix +ℹ Fix 64 64 | def no_underline_and_no_newline(): # noqa: D416 65 65 | """Toggle the gizmo. 66 66 | @@ -95,7 +95,7 @@ sections.py:216:5: D407 [*] Missing dashed underline after section ("Raises") | = help: Add dashed line under "Raises" -ℹ Suggested fix +ℹ Fix 225 225 | ------ 226 226 | Many many wonderful things. 227 227 | Raises: @@ -124,7 +124,7 @@ sections.py:261:5: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 261 261 | """Toggle the gizmo. 262 262 | 263 263 | Args: @@ -153,7 +153,7 @@ sections.py:261:5: D407 [*] Missing dashed underline after section ("Returns") | = help: Add dashed line under "Returns" -ℹ Suggested fix +ℹ Fix 264 264 | note: A random string. 265 265 | 266 266 | Returns: @@ -182,7 +182,7 @@ sections.py:261:5: D407 [*] Missing dashed underline after section ("Raises") | = help: Add dashed line under "Raises" -ℹ Suggested fix +ℹ Fix 266 266 | Returns: 267 267 | 268 268 | Raises: @@ -206,7 +206,7 @@ sections.py:278:5: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 278 278 | """Toggle the gizmo. 279 279 | 280 280 | Args @@ -233,7 +233,7 @@ sections.py:293:9: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 295 295 | Will this work when referencing x? 296 296 | 297 297 | Args: @@ -257,7 +257,7 @@ sections.py:310:5: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 310 310 | """Toggle the gizmo. 311 311 | 312 312 | Args: @@ -283,7 +283,7 @@ sections.py:322:9: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 322 322 | """Test a valid args section. 323 323 | 324 324 | Args: @@ -309,7 +309,7 @@ sections.py:334:9: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 334 334 | """Test a valid args section. 335 335 | 336 336 | Args: @@ -336,7 +336,7 @@ sections.py:346:9: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 346 346 | """Test a valid args section. 347 347 | 348 348 | Args: @@ -362,7 +362,7 @@ sections.py:359:9: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 359 359 | """Test a valid args section. 360 360 | 361 361 | Args: @@ -388,7 +388,7 @@ sections.py:371:9: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 371 371 | """Test a valid args section. 372 372 | 373 373 | Args: @@ -418,7 +418,7 @@ sections.py:380:9: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 380 380 | """Do stuff. 381 381 | 382 382 | Args: @@ -444,7 +444,7 @@ sections.py:499:9: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 501 501 | Testing this incorrectly indented docstring. 502 502 | 503 503 | Args: diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D408_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D408_sections.py.snap index ae8af56231..4550efa25b 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D408_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D408_sections.py.snap @@ -18,7 +18,7 @@ sections.py:94:5: D408 [*] Section underline should be in the line following the | = help: Add underline to "Returns" -ℹ Suggested fix +ℹ Fix 94 94 | """Toggle the gizmo. 95 95 | 96 96 | Returns diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D409_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D409_sections.py.snap index cbfd7d0938..db7d4e96b3 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D409_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D409_sections.py.snap @@ -17,7 +17,7 @@ sections.py:108:5: D409 [*] Section underline should match the length of its nam | = help: Adjust underline length to match "Returns" -ℹ Suggested fix +ℹ Fix 108 108 | """Toggle the gizmo. 109 109 | 110 110 | Returns @@ -51,7 +51,7 @@ sections.py:216:5: D409 [*] Section underline should match the length of its nam | = help: Adjust underline length to match "Returns" -ℹ Suggested fix +ℹ Fix 222 222 | returns. 223 223 | 224 224 | Returns diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D410_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D410_sections.py.snap index 668b7d7915..e2cda946d5 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D410_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D410_sections.py.snap @@ -22,7 +22,7 @@ sections.py:76:5: D410 [*] Missing blank line after section ("Returns") | = help: Add blank line after "Returns" -ℹ Suggested fix +ℹ Fix 77 77 | 78 78 | Returns 79 79 | ------- @@ -55,7 +55,7 @@ sections.py:216:5: D410 [*] Missing blank line after section ("Returns") | = help: Add blank line after "Returns" -ℹ Suggested fix +ℹ Fix 224 224 | Returns 225 225 | ------ 226 226 | Many many wonderful things. diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D411_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D411_sections.py.snap index 8a17fc197d..1e7cad7261 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D411_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D411_sections.py.snap @@ -22,7 +22,7 @@ sections.py:76:5: D411 [*] Missing blank line before section ("Yields") | = help: Add blank line before "Yields" -ℹ Suggested fix +ℹ Fix 77 77 | 78 78 | Returns 79 79 | ------- @@ -48,7 +48,7 @@ sections.py:131:5: D411 [*] Missing blank line before section ("Returns") | = help: Add blank line before "Returns" -ℹ Suggested fix +ℹ Fix 131 131 | """Toggle the gizmo. 132 132 | 133 133 | The function's description. @@ -81,7 +81,7 @@ sections.py:216:5: D411 [*] Missing blank line before section ("Raises") | = help: Add blank line before "Raises" -ℹ Suggested fix +ℹ Fix 224 224 | Returns 225 225 | ------ 226 226 | Many many wonderful things. diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D412_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D412_sections.py.snap index 551d45f43a..2d32ab4dcc 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D412_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D412_sections.py.snap @@ -25,7 +25,7 @@ sections.py:216:5: D412 [*] No blank lines allowed between a section header and | = help: Remove blank line(s) -ℹ Suggested fix +ℹ Fix 217 217 | 218 218 | Short summary 219 219 | ------------- diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D413_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D413_sections.py.snap index 8cb693bee7..05ae181bc0 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D413_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D413_sections.py.snap @@ -13,7 +13,7 @@ sections.py:65:5: D413 [*] Missing blank line after last section ("Returns") | = help: Add blank line after "Returns" -ℹ Suggested fix +ℹ Fix 64 64 | def no_underline_and_no_newline(): # noqa: D416 65 65 | """Toggle the gizmo. 66 66 | diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__d209_d400.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__d209_d400.snap index cf6e6e6be6..4b0e26c0d5 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__d209_d400.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__d209_d400.snap @@ -11,7 +11,7 @@ D209_D400.py:2:5: D209 [*] Multi-line docstring closing quotes should be on a se | = help: Move closing quotes to new line -ℹ Suggested fix +ℹ Fix 1 1 | def lorem(): 2 2 | """lorem ipsum dolor sit amet consectetur adipiscing elit 3 |- sed do eiusmod tempor incididunt ut labore et dolore magna aliqua""" From 962479d94355cc079509f46363cca142ebc89281 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Tue, 27 Jun 2023 22:20:20 +0530 Subject: [PATCH 249/447] Replace same length equal line with dash line in D407 (#5383) ## Summary Replace same length equal line with dash line in D407 Do we want to update the message and autofix title to reflect this change? ## Test Plan Added test cases for: - Equal line length == dash line length - Equal line length != dash line length fixes: #5378 --- .../test/fixtures/pydocstyle/sections.py | 16 +++++++ .../src/rules/pydocstyle/rules/sections.rs | 27 +++++++++--- ...__pydocstyle__tests__D407_sections.py.snap | 44 +++++++++++++++++++ 3 files changed, 82 insertions(+), 5 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pydocstyle/sections.py b/crates/ruff/resources/test/fixtures/pydocstyle/sections.py index 4bd805065c..fef7f2fba1 100644 --- a/crates/ruff/resources/test/fixtures/pydocstyle/sections.py +++ b/crates/ruff/resources/test/fixtures/pydocstyle/sections.py @@ -513,3 +513,19 @@ def implicit_string_concatenation(): A value of some sort. """"Extra content" + + +def replace_equals_with_dash(): + """Equal length equals should be replaced with dashes. + + Parameters + ========== + """ + + +def replace_equals_with_dash2(): + """Here, the length of equals is not the same. + + Parameters + =========== + """ diff --git a/crates/ruff/src/rules/pydocstyle/rules/sections.rs b/crates/ruff/src/rules/pydocstyle/rules/sections.rs index 91b94e2033..5f7be37f5b 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/sections.rs @@ -13,7 +13,7 @@ use ruff_python_ast::docstrings::{clean_space, leading_space}; use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::is_staticmethod; use ruff_python_semantic::{Definition, Member, MemberKind}; -use ruff_python_whitespace::NewlineWithTrailingNewline; +use ruff_python_whitespace::{NewlineWithTrailingNewline, PythonWhitespace}; use ruff_textwrap::dedent; use crate::checkers::ast::Checker; @@ -488,6 +488,10 @@ fn blanks_and_section_underline( } } } else { + let equal_line_found = non_blank_line + .chars() + .all(|char| char.is_whitespace() || char == '='); + if checker.enabled(Rule::DashedUnderlineAfterSection) { let mut diagnostic = Diagnostic::new( DashedUnderlineAfterSection { @@ -503,10 +507,23 @@ fn blanks_and_section_underline( clean_space(docstring.indentation), "-".repeat(context.section_name().len()), ); - diagnostic.set_fix(Fix::automatic(Edit::insertion( - content, - context.summary_range().end(), - ))); + if equal_line_found + && non_blank_line.trim_whitespace().len() == context.section_name().len() + { + // If an existing underline is an equal sign line of the appropriate length, + // replace it with a dashed line. + diagnostic.set_fix(Fix::automatic(Edit::replacement( + content, + context.summary_range().end(), + non_blank_line.end(), + ))); + } else { + // Otherwise, insert a dashed line after the section header. + diagnostic.set_fix(Fix::automatic(Edit::insertion( + content, + context.summary_range().end(), + ))); + } } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D407_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D407_sections.py.snap index a0fe542d21..f0eac75133 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D407_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D407_sections.py.snap @@ -453,4 +453,48 @@ sections.py:499:9: D407 [*] Missing dashed underline after section ("Args") 505 506 | 506 507 | """ +sections.py:519:5: D407 [*] Missing dashed underline after section ("Parameters") + | +518 | def replace_equals_with_dash(): +519 | """Equal length equals should be replaced with dashes. + | _____^ +520 | | +521 | | Parameters +522 | | ========== +523 | | """ + | |_______^ D407 + | + = help: Add dashed line under "Parameters" + +ℹ Fix +519 519 | """Equal length equals should be replaced with dashes. +520 520 | +521 521 | Parameters +522 |- ========== + 522 |+ ---------- +523 523 | """ +524 524 | +525 525 | + +sections.py:527:5: D407 [*] Missing dashed underline after section ("Parameters") + | +526 | def replace_equals_with_dash2(): +527 | """Here, the length of equals is not the same. + | _____^ +528 | | +529 | | Parameters +530 | | =========== +531 | | """ + | |_______^ D407 + | + = help: Add dashed line under "Parameters" + +ℹ Fix +527 527 | """Here, the length of equals is not the same. +528 528 | +529 529 | Parameters + 530 |+ ---------- +530 531 | =========== +531 532 | """ + From 032b967b055f7fc5dde11b09e9b6c31ef3bb4bbc Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 27 Jun 2023 12:53:47 -0400 Subject: [PATCH 250/447] Enable --watch for Jupyter notebooks (#5394) ## Summary The list of extensions that support watching is hard-coded (unfortunately); this PR adds `.ipynb` to the list. --- crates/ruff_cli/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_cli/src/lib.rs b/crates/ruff_cli/src/lib.rs index 0f737628d1..b7abecc4a1 100644 --- a/crates/ruff_cli/src/lib.rs +++ b/crates/ruff_cli/src/lib.rs @@ -58,7 +58,7 @@ enum ChangeKind { /// Returns `None` if no relevant changes were detected. fn change_detected(paths: &[PathBuf]) -> Option { // If any `.toml` files were modified, return `ChangeKind::Configuration`. Otherwise, return - // `ChangeKind::SourceFile` if any `.py`, `.pyi`, or `.pyw` files were modified. + // `ChangeKind::SourceFile` if any `.py`, `.pyi`, `.pyw`, or `.ipynb` files were modified. let mut source_file = false; for path in paths { if let Some(suffix) = path.extension() { @@ -66,7 +66,7 @@ fn change_detected(paths: &[PathBuf]) -> Option { Some("toml") => { return Some(ChangeKind::Configuration); } - Some("py" | "pyi" | "pyw") => source_file = true, + Some("py" | "pyi" | "pyw" | "ipynb") => source_file = true, _ => {} } } From 035f8993f4c1f0193cee315f4292020d3e5aa786 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Tue, 27 Jun 2023 19:12:21 +0100 Subject: [PATCH 251/447] Complete documentation for `pydocstyle` rules (#5387) ## Summary Completes the documentation for the `pydocstyle` ruleset. Related to #2646. ## Test Plan `python scripts/check_docs_formatted.py` --- .../pydocstyle/rules/blank_after_summary.rs | 34 + .../rules/blank_before_after_class.rs | 100 ++ .../rules/blank_before_after_function.rs | 52 + .../src/rules/pydocstyle/rules/capitalized.rs | 23 + .../pydocstyle/rules/ends_with_period.rs | 32 + .../pydocstyle/rules/ends_with_punctuation.rs | 31 + .../src/rules/pydocstyle/rules/if_needed.rs | 60 + .../ruff/src/rules/pydocstyle/rules/indent.rs | 98 ++ .../rules/multi_line_summary_start.rs | 80 ++ .../rules/newline_after_last_paragraph.rs | 34 + .../rules/pydocstyle/rules/no_signature.rs | 34 + .../rules/no_surrounding_whitespace.rs | 22 + .../pydocstyle/rules/non_imperative_mood.rs | 33 + .../src/rules/pydocstyle/rules/not_empty.rs | 23 + .../src/rules/pydocstyle/rules/not_missing.rs | 1 + .../src/rules/pydocstyle/rules/one_liner.rs | 25 + .../src/rules/pydocstyle/rules/sections.rs | 1014 +++++++++++++++++ .../pydocstyle/rules/starts_with_this.rs | 33 + scripts/check_docs_formatted.py | 6 + 19 files changed, 1735 insertions(+) diff --git a/crates/ruff/src/rules/pydocstyle/rules/blank_after_summary.rs b/crates/ruff/src/rules/pydocstyle/rules/blank_after_summary.rs index c655df13e0..8e939060dc 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/blank_after_summary.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/blank_after_summary.rs @@ -6,6 +6,40 @@ use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::AsRule; +/// ## What it does +/// Checks for docstring summary lines that are not separated from the docstring +/// description by one blank line. +/// +/// ## Why is this bad? +/// [PEP 257] recommends that multi-line docstrings consist of "a summary line +/// just like a one-line docstring, followed by a blank line, followed by a +/// more elaborate description." +/// +/// ## Example +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """Return a sorted copy of the list. +/// Sort the list in ascending order and return a copy of the +/// result using the bubble sort algorithm. +/// """ +/// ``` +/// +/// Use instead: +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the +/// result using the bubble sort algorithm. +/// """ +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) +/// +/// [PEP 257]: https://peps.python.org/pep-0257/ #[violation] pub struct BlankLineAfterSummary { num_lines: usize, 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 6a71f725b4..3d4524d497 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 @@ -10,6 +10,37 @@ use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::{AsRule, Rule}; +/// ## What it does +/// Checks for docstrings on class definitions that are not preceded by a +/// blank line. +/// +/// ## Why is this bad? +/// Use a blank line to separate the docstring from the class definition, for +/// consistency. +/// +/// This rule may not apply to all projects; its applicability is a matter of +/// convention. By default, this rule is disabled when using the `google`, +/// `numpy`, and `pep257` conventions. +/// +/// For an alternative, see [D211]. +/// +/// ## Example +/// ```python +/// class PhotoMetadata: +/// """Metadata about a photo.""" +/// ``` +/// +/// Use instead: +/// ```python +/// class PhotoMetadata: +/// +/// """Metadata about a photo.""" +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// [D211]: https://beta.ruff.rs/docs/rules/blank-line-before-class #[violation] pub struct OneBlankLineBeforeClass { lines: usize, @@ -26,6 +57,44 @@ impl AlwaysAutofixableViolation for OneBlankLineBeforeClass { } } +/// ## What it does +/// Checks for class methods that are not separated from the class's docstring +/// by a blank line. +/// +/// ## Why is this bad? +/// [PEP 257] recommends the use of a blank line to separate a class's +/// docstring its methods. +/// +/// This rule may not apply to all projects; its applicability is a matter of +/// convention. By default, this rule is enabled when using the `google` +/// convention, and disabled when using the `numpy` and `pep257` conventions. +/// +/// ## Example +/// ```python +/// class PhotoMetadata: +/// """Metadata about a photo.""" +/// def __init__(self, file: Path): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// class PhotoMetadata: +/// """Metadata about a photo.""" +/// +/// def __init__(self, file: Path): +/// ... +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) +/// +/// [PEP 257]: https://peps.python.org/pep-0257/ #[violation] pub struct OneBlankLineAfterClass { lines: usize, @@ -42,6 +111,37 @@ impl AlwaysAutofixableViolation for OneBlankLineAfterClass { } } +/// ## What it does +/// Checks for docstrings on class definitions that are preceded by a blank +/// line. +/// +/// ## Why is this bad? +/// Avoid introducing any blank lines between a class definition and its +/// docstring, for consistency. +/// +/// This rule may not apply to all projects; its applicability is a matter of +/// convention. By default, this rule is enabled when using the `google`, +/// `numpy`, and `pep257` conventions. +/// +/// For an alternative, see [D203]. +/// +/// ## Example +/// ```python +/// class PhotoMetadata: +/// """Metadata about a photo.""" +/// ``` +/// +/// Use instead: +/// ```python +/// class PhotoMetadata: +/// +/// """Metadata about a photo.""" +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// [D203]: https://beta.ruff.rs/docs/rules/one-blank-line-before-class #[violation] pub struct BlankLineBeforeClass { lines: usize, 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 337d02b789..44bea30595 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 @@ -12,6 +12,31 @@ use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::{AsRule, Rule}; +/// ## What it does +/// Checks for docstrings on functions that are separated by one or more blank +/// lines from the function definition. +/// +/// ## Why is this bad? +/// Remove any blank lines between the function definition and its docstring, +/// for consistency. +/// +/// ## Example +/// ```python +/// def average(values: list[float]) -> float: +/// +/// """Return the mean of the given values.""" +/// ``` +/// +/// Use instead: +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values.""" +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct NoBlankLineBeforeFunction { num_lines: usize, @@ -29,6 +54,33 @@ impl AlwaysAutofixableViolation for NoBlankLineBeforeFunction { } } +/// ## What it does +/// Checks for docstrings on functions that are separated by one or more blank +/// lines from the function body. +/// +/// ## Why is this bad? +/// Remove any blank lines between the function body and the function +/// docstring, for consistency. +/// +/// ## Example +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values.""" +/// +/// return sum(values) / len(values) +/// ``` +/// +/// Use instead: +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values.""" +/// return sum(values) / len(values) +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct NoBlankLineAfterFunction { num_lines: usize, diff --git a/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs b/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs index 7ae7030663..1e5c110c47 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs @@ -9,6 +9,29 @@ use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::AsRule; +/// ## What it does +/// Checks for docstrings that do not start with a capital letter. +/// +/// ## Why is this bad? +/// The first character in a docstring should be capitalized for, grammatical +/// correctness and consistency. +/// +/// ## Example +/// ```python +/// def average(values: list[float]) -> float: +/// """return the mean of the given values.""" +/// ``` +/// +/// Use instead: +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values.""" +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct FirstLineCapitalized { first_word: String, diff --git a/crates/ruff/src/rules/pydocstyle/rules/ends_with_period.rs b/crates/ruff/src/rules/pydocstyle/rules/ends_with_period.rs index a870568d5e..ba53386fee 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/ends_with_period.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/ends_with_period.rs @@ -11,6 +11,38 @@ use crate::docstrings::Docstring; use crate::registry::AsRule; use crate::rules::pydocstyle::helpers::logical_line; +/// ## What it does +/// Checks for docstrings in which the first line does not end in a period. +/// +/// ## Why is this bad? +/// [PEP 257] recommends that the first line of a docstring is written in the +/// form of a command, ending in a period. +/// +/// This rule may not apply to all projects; its applicability is a matter of +/// convention. By default, this rule is enabled when using the `numpy` and +/// `pep257` conventions, and disabled when using the `google` convention. +/// +/// ## Example +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values""" +/// ``` +/// +/// Use instead: +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values.""" +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) +/// +/// [PEP 257]: https://peps.python.org/pep-0257/ #[violation] pub struct EndsInPeriod; diff --git a/crates/ruff/src/rules/pydocstyle/rules/ends_with_punctuation.rs b/crates/ruff/src/rules/pydocstyle/rules/ends_with_punctuation.rs index bd6302cb9e..9e11eb1c8e 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/ends_with_punctuation.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/ends_with_punctuation.rs @@ -11,6 +11,37 @@ use crate::docstrings::Docstring; use crate::registry::AsRule; use crate::rules::pydocstyle::helpers::logical_line; +/// ## What it does +/// Checks for docstrings in which the first line does not end in a punctuation +/// mark, such as a period, question mark, or exclamation point. +/// +/// ## Why is this bad? +/// The first line of a docstring should end with a period, question mark, or +/// exclamation point, for grammatical correctness and consistency. +/// +/// This rule may not apply to all projects; its applicability is a matter of +/// convention. By default, this rule is enabled when using the `google` +/// convention, and disabled when using the `numpy` and `pep257` conventions. +/// +/// ## Example +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values""" +/// ``` +/// +/// Use instead: +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values.""" +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct EndsInPunctuation; diff --git a/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs b/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs index a109e51ea5..87262ccce1 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs @@ -8,6 +8,66 @@ use ruff_python_semantic::{Definition, Member, MemberKind}; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; +/// ## What it does +/// Checks for `@overload` function definitions that contain a docstring. +/// +/// ## Why is this bad? +/// The `@overload` decorator is used to define multiple compatible signatures +/// for a given function, to support type-checking. A series of `@overload` +/// definitions should be followed by a single non-decorated definition that +/// contains the implementation of the function. +/// +/// `@overload` function definitions should not contain a docstring; instead, +/// the docstring should be placed on the non-decorated definition that contains +/// the implementation. +/// +/// ## Example +/// ```python +/// from typing import overload +/// +/// +/// @overload +/// def factorial(n: int) -> int: +/// """Return the factorial of n.""" +/// +/// +/// @overload +/// def factorial(n: float) -> float: +/// """Return the factorial of n.""" +/// +/// +/// def factorial(n): +/// """Return the factorial of n.""" +/// +/// +/// factorial.__doc__ # "Return the factorial of n." +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import overload +/// +/// +/// @overload +/// def factorial(n: int) -> int: +/// ... +/// +/// +/// @overload +/// def factorial(n: float) -> float: +/// ... +/// +/// +/// def factorial(n): +/// """Return the factorial of n.""" +/// +/// +/// factorial.__doc__ # "Return the factorial of n." +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [Python documentation: `typing.overload`](https://docs.python.org/3/library/typing.html#typing.overload) #[violation] pub struct OverloadWithDocstring; diff --git a/crates/ruff/src/rules/pydocstyle/rules/indent.rs b/crates/ruff/src/rules/pydocstyle/rules/indent.rs index d9d352dfcd..1f8762cae5 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/indent.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/indent.rs @@ -10,6 +10,38 @@ use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::{AsRule, Rule}; +/// ## What it does +/// Checks for docstrings that are indented with tabs. +/// +/// ## Why is this bad? +/// [PEP 8](https://peps.python.org/pep-0008/#tabs-or-spaces) recommends using +/// spaces over tabs for indentation. +/// +/// +/// ## Example +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the bubble +/// sort algorithm. +/// """ +/// ``` +/// +/// Use instead: +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the bubble +/// sort algorithm. +/// """ +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct IndentWithSpaces; @@ -20,6 +52,39 @@ impl Violation for IndentWithSpaces { } } +/// ## What it does +/// Checks for under-indented docstrings. +/// +/// ## Why is this bad? +/// [PEP 257] recommends that docstrings be indented to the same level as their +/// opening quotes. Avoid under-indenting docstrings, for consistency. +/// +/// ## Example +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the bubble sort +/// algorithm. +/// """ +/// ``` +/// +/// Use instead: +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the bubble +/// sort algorithm. +/// """ +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) +/// +/// [PEP 257]: https://peps.python.org/pep-0257/ #[violation] pub struct UnderIndentation; @@ -34,6 +99,39 @@ impl AlwaysAutofixableViolation for UnderIndentation { } } +/// ## What it does +/// Checks for over-indented docstrings. +/// +/// ## Why is this bad? +/// [PEP 257] recommends that docstrings be indented to the same level as their +/// opening quotes. Avoid over-indenting docstrings, for consistency. +/// +/// ## Example +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the +/// bubble sort algorithm. +/// """ +/// ``` +/// +/// Use instead: +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the bubble +/// sort algorithm. +/// """ +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) +/// +/// [PEP 257]: https://peps.python.org/pep-0257/ #[violation] pub struct OverIndentation; 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 98a1eee00f..b470da5ea7 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 @@ -11,6 +11,46 @@ use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::{AsRule, Rule}; +/// ## What it does +/// Checks for docstring summary lines that are not positioned on the first +/// physical line of the docstring. +/// +/// ## Why is this bad? +/// [PEP 257] recommends that multi-line docstrings consist of "a summary line +/// just like a one-line docstring, followed by a blank line, followed by a +/// more elaborate description." +/// +/// The summary line should be located on the first physical line of the +/// docstring, immediately after the opening quotes. +/// +/// This rule may not apply to all projects; its applicability is a matter of +/// convention. By default, this rule is enabled when using the `google` +/// convention, and disabled when using the `numpy` and `pep257` conventions. +/// +/// For an alternative, see [D213]. +/// +/// ## Example +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """ +/// Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the +/// bubble sort algorithm. +/// """ +/// ``` +/// +/// Use instead: +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the bubble +/// sort algorithm. +/// """ +/// ``` +/// +/// [D213]: https://beta.ruff.rs/docs/rules/multi-line-summary-second-line #[violation] pub struct MultiLineSummaryFirstLine; @@ -25,6 +65,46 @@ impl AlwaysAutofixableViolation for MultiLineSummaryFirstLine { } } +/// ## What it does +/// Checks for docstring summary lines that are not positioned on the second +/// physical line of the docstring. +/// +/// ## Why is this bad? +/// [PEP 257] recommends that multi-line docstrings consist of "a summary line +/// just like a one-line docstring, followed by a blank line, followed by a +/// more elaborate description." +/// +/// The summary line should be located on the second physical line of the +/// docstring, immediately after the opening quotes and the blank line. +/// +/// This rule may not apply to all projects; its applicability is a matter of +/// convention. By default, this rule is disabled when using the `google`, +/// `numpy`, and `pep257` conventions. +/// +/// For an alternative, see [D212]. +/// +/// ## Example +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the +/// bubble sort algorithm. +/// """ +/// ``` +/// +/// Use instead: +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """ +/// Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the bubble +/// sort algorithm. +/// """ +/// ``` +/// +/// [D212]: https://beta.ruff.rs/docs/rules/multi-line-summary-first-line #[violation] pub struct MultiLineSummarySecondLine; diff --git a/crates/ruff/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs b/crates/ruff/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs index 6345cbb9b0..384635ea61 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs @@ -10,6 +10,40 @@ use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::AsRule; +/// ## What it does +/// Checks for multi-line docstrings whose closing quotes are not on their +/// own line. +/// +/// ## Why is this bad? +/// [PEP 257] recommends that the closing quotes of a multi-line docstring be +/// on their own line, for consistency and compatibility with documentation +/// tools that may need to parse the docstring. +/// +/// ## Example +/// ```python +/// def sort_list(l: List[int]) -> List[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the +/// bubble sort algorithm.""" +/// ``` +/// +/// Use instead: +/// ```python +/// def sort_list(l: List[int]) -> List[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the bubble +/// sort algorithm. +/// """ +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) +/// +/// [PEP 257]: https://peps.python.org/pep-0257/ #[violation] pub struct NewLineAfterLastParagraph; diff --git a/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs b/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs index 1ae23355ba..486df63860 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs @@ -8,6 +8,40 @@ use ruff_python_whitespace::UniversalNewlines; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; +/// ## What it does +/// Checks for function docstrings that include the function's signature in +/// the summary line. +/// +/// ## Why is this bad? +/// [PEP 257] recommends against including a function's signature in its +/// docstring. Instead, consider using type annotations as a form of +/// documentation for the function's parameters and return value. +/// +/// This rule may not apply to all projects; its applicability is a matter of +/// convention. By default, this rule is enabled when using the `google` and +/// `pep257` conventions, and disabled when using the `numpy` convention. +/// +/// ## Example +/// ```python +/// def foo(a, b): +/// """foo(a: int, b: int) -> list[int]""" +/// ``` +/// +/// Use instead: +/// ```python +/// def foo(a: int, b: int) -> list[int]: +/// """Return a list of a and b.""" +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) +/// +/// [PEP 257]: https://peps.python.org/pep-0257/ #[violation] pub struct NoSignature; diff --git a/crates/ruff/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs b/crates/ruff/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs index 6f4be82526..cd1d4ff3da 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs @@ -9,6 +9,28 @@ use crate::docstrings::Docstring; use crate::registry::AsRule; use crate::rules::pydocstyle::helpers::ends_with_backslash; +/// ## What it does +/// Checks for surrounding whitespace in docstrings. +/// +/// ## Why is this bad? +/// Remove surrounding whitespace from the docstring, for consistency. +/// +/// ## Example +/// ```python +/// def factorial(n: int) -> int: +/// """ Return the factorial of n. """ +/// ``` +/// +/// Use instead: +/// ```python +/// def factorial(n: int) -> int: +/// """Return the factorial of n.""" +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct SurroundingWhitespace; diff --git a/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs b/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs index d4d80dbdc2..3969452fce 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs @@ -17,6 +17,39 @@ use crate::rules::pydocstyle::helpers::normalize_word; static MOOD: Lazy = Lazy::new(Mood::new); +/// ## What it does +/// Checks for docstring first lines that are not in an imperative mood. +/// +/// ## Why is this bad? +/// [PEP 257] recommends that the first line of a docstring be written in the +/// imperative mood, for consistency. +/// +/// Hint: to rewrite the docstring in the imperative, phrase the first line as +/// if it were a command. +/// +/// This rule may not apply to all projects; its applicability is a matter of +/// convention. By default, this rule is enabled when using the `numpy` and +/// `pep257` conventions, and disabled when using the `google` conventions. +/// +/// ## Example +/// ```python +/// def average(values: list[float]) -> float: +/// """Returns the mean of the given values.""" +/// ``` +/// +/// Use instead: +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values.""" +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// +/// [PEP 257]: https://peps.python.org/pep-0257/ #[violation] pub struct NonImperativeMood { first_line: String, diff --git a/crates/ruff/src/rules/pydocstyle/rules/not_empty.rs b/crates/ruff/src/rules/pydocstyle/rules/not_empty.rs index c349336e7c..3c89974b37 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/not_empty.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/not_empty.rs @@ -5,6 +5,29 @@ use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::Rule; +/// ## What it does +/// Checks for empty docstrings. +/// +/// ## Why is this bad? +/// An empty docstring is indicative of incomplete documentation. It should either +/// be removed or replaced with a meaningful docstring. +/// +/// ## Example +/// ```python +/// def average(values: list[float]) -> float: +/// """""" +/// ``` +/// +/// Use instead: +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values.""" +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct EmptyDocstring; diff --git a/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs b/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs index 97364cbc44..cefee351cb 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs @@ -304,6 +304,7 @@ impl Violation for UndocumentedPublicMethod { /// return distance / time /// except ZeroDivisionError as exc: /// raise FasterThanLightError from exc +/// ``` /// /// ## References /// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) diff --git a/crates/ruff/src/rules/pydocstyle/rules/one_liner.rs b/crates/ruff/src/rules/pydocstyle/rules/one_liner.rs index bec528b4ed..7a3ad6980b 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/one_liner.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/one_liner.rs @@ -7,6 +7,31 @@ use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::AsRule; +/// ## What it does +/// Checks for single-line docstrings that are broken across multiple lines. +/// +/// ## Why is this bad? +/// [PEP 257] recommends that docstrings that _can_ fit on one line should be +/// formatted on a single line, for consistency and readability. +/// +/// ## Example +/// ```python +/// def average(values: list[float]) -> float: +/// """ +/// Return the mean of the given values. +/// """ +/// ``` +/// +/// Use instead: +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values.""" +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// +/// [PEP 257]: https://peps.python.org/pep-0257/ #[violation] pub struct FitsOnOneLine; diff --git a/crates/ruff/src/rules/pydocstyle/rules/sections.rs b/crates/ruff/src/rules/pydocstyle/rules/sections.rs index 5f7be37f5b..eb2676b1c9 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/sections.rs @@ -23,6 +23,68 @@ use crate::docstrings::Docstring; use crate::registry::{AsRule, Rule}; use crate::rules::pydocstyle::settings::Convention; +/// ## What it does +/// Checks for over-indented sections in docstrings. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Each section should use consistent indentation, with the section headers +/// matching the indentation of the docstring's opening quotes, and the +/// section bodies being indented one level further. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args: +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// Returns: +/// Speed as distance divided by time. +/// +/// Raises: +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args: +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// Returns: +/// Speed as distance divided by time. +/// +/// Raises: +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct SectionNotOverIndented { name: String, @@ -41,6 +103,86 @@ impl AlwaysAutofixableViolation for SectionNotOverIndented { } } +/// ## What it does +/// Checks for over-indented section underlines in docstrings. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Some docstring formats (like reStructuredText) use underlines to separate +/// section bodies from section headers. +/// +/// Avoid over-indenting the section underlines, as this can cause syntax +/// errors in reStructuredText. +/// +/// By default, this rule is enabled when using the `numpy` convention, and +/// disabled when using the `google` or `pep257` conventions. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[violation] pub struct SectionUnderlineNotOverIndented { name: String, @@ -59,6 +201,67 @@ impl AlwaysAutofixableViolation for SectionUnderlineNotOverIndented { } } +/// ## What it does +/// Checks for section headers in docstrings that do not begin with capital +/// letters. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Section headers should be capitalized, for consistency. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// args: +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// returns: +/// Speed as distance divided by time. +/// +/// raises: +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args: +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// Returns: +/// Speed as distance divided by time. +/// +/// Raises: +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct CapitalizeSectionName { name: String, @@ -77,6 +280,84 @@ impl AlwaysAutofixableViolation for CapitalizeSectionName { } } +/// ## What it does +/// Checks that section headers in docstrings that are not followed by a +/// newline. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Section headers should be followed by a newline, and not by another +/// character (like a colon), for consistency. +/// +/// This rule is enabled when using the `numpy` convention, and disabled +/// when using the `google` or `pep257` conventions. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters: +/// ----------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns: +/// -------- +/// float +/// Speed as distance divided by time. +/// +/// Raises: +/// ------- +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[violation] pub struct NewLineAfterSectionName { name: String, @@ -95,6 +376,84 @@ impl AlwaysAutofixableViolation for NewLineAfterSectionName { } } +/// ## What it does +/// Checks for section headers in docstrings that are not followed by +/// underlines. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Some docstring formats (like reStructuredText) use underlines to separate +/// section bodies from section headers. +/// +/// This rule is enabled when using the `numpy` convention, and disabled +/// when using the `google` or `pep257` conventions. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[violation] pub struct DashedUnderlineAfterSection { name: String, @@ -113,6 +472,90 @@ impl AlwaysAutofixableViolation for DashedUnderlineAfterSection { } } +/// ## What it does +/// Checks for section underlines in docstrings that are not on the line +/// immediately following the section name. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Some docstring formats (like reStructuredText) use underlines to separate +/// section bodies from section headers. +/// +/// When present, section underlines should be positioned on the line +/// immediately following the section header. +/// +/// This rule is enabled when using the `numpy` convention, and disabled +/// when using the `google` or `pep257` conventions. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[violation] pub struct SectionUnderlineAfterName { name: String, @@ -131,6 +574,87 @@ impl AlwaysAutofixableViolation for SectionUnderlineAfterName { } } +/// ## What it does +/// Checks for section underlines in docstrings that do not match the length of +/// the corresponding section header. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Some docstring formats (like reStructuredText) use underlines to separate +/// section bodies from section headers. +/// +/// When present, section underlines should match the length of the +/// corresponding section header. +/// +/// This rule is enabled when using the `numpy` convention, and disabled +/// when using the `google` or `pep257` conventions. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// --- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// --- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// --- +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[violation] pub struct SectionUnderlineMatchesSectionLength { name: String, @@ -149,6 +673,83 @@ impl AlwaysAutofixableViolation for SectionUnderlineMatchesSectionLength { } } +/// ## What it does +/// Checks for docstring sections that are not separated by a single blank +/// line. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Docstring sections should be separated by a blank line, for consistency and +/// compatibility with documentation tooling. +/// +/// This rule is enabled when using the `numpy` and `google` conventions, and +/// disabled when using the `pep257` convention. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Style Guide](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct NoBlankLineAfterSection { name: String, @@ -167,6 +768,81 @@ impl AlwaysAutofixableViolation for NoBlankLineAfterSection { } } +/// ## What it does +/// Checks for docstring sections that are separated by a blank line. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Docstring sections should be separated by a blank line, for consistency and +/// compatibility with documentation tooling. +/// +/// This rule is enabled when using the `numpy` and `google` conventions, and +/// disabled when using the `pep257` convention. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[violation] pub struct NoBlankLineBeforeSection { name: String, @@ -185,6 +861,81 @@ impl AlwaysAutofixableViolation for NoBlankLineBeforeSection { } } +/// ## What it does +/// Checks for missing blank lines after the last section of a multi-line +/// docstring. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// The last section in a docstring should be separated by a blank line, for +/// consistency and compatibility with documentation tooling. +/// +/// This rule is enabled when using the `numpy` convention, and disabled when +/// using the `pep257` and `google` conventions. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light.""" +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[violation] pub struct BlankLineAfterLastSection { name: String, @@ -203,6 +954,79 @@ impl AlwaysAutofixableViolation for BlankLineAfterLastSection { } } +/// ## What it does +/// Checks for docstrings that contain empty sections. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Empty docstring sections are indicative of missing documentation. Empty +/// sections should either be removed or filled in with relevant documentation. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Style Guide](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct EmptyDocstringSection { name: String, @@ -216,6 +1040,69 @@ impl Violation for EmptyDocstringSection { } } +/// ## What it does +/// Checks for docstring section headers that do not end with a colon. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// In a docstring, each section header should end with a colon, for +/// consistency. +/// +/// This rule is enabled when using the `google` convention, and disabled when +/// using the `pep257` and `numpy` conventions. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// Returns +/// Speed as distance divided by time. +/// +/// Raises +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args: +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// Returns: +/// Speed as distance divided by time. +/// +/// Raises: +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [Google Style Guide](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct SectionNameEndsInColon { name: String, @@ -234,6 +1121,70 @@ impl AlwaysAutofixableViolation for SectionNameEndsInColon { } } +/// ## What it does +/// Checks for function docstrings that do not include documentation for all +/// parameters in the function. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Function docstrings often include a section for function arguments, which +/// should include documentation for every argument. Undocumented arguments are +/// indicative of missing documentation. +/// +/// This rule is enabled when using the `google` convention, and disabled when +/// using the `pep257` and `numpy` conventions. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args: +/// distance: Distance traveled. +/// +/// Returns: +/// Speed as distance divided by time. +/// +/// Raises: +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args: +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// Returns: +/// Speed as distance divided by time. +/// +/// Raises: +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct UndocumentedParam { pub names: Vec, @@ -253,6 +1204,69 @@ impl Violation for UndocumentedParam { } } +/// ## What it does +/// Checks for docstring sections that contain blank lines between the section +/// header and the section body. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Docstring sections should not contain blank lines between the section header +/// and the section body, for consistency. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args: +/// +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// Returns: +/// Speed as distance divided by time. +/// +/// Raises: +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args: +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// Returns: +/// Speed as distance divided by time. +/// +/// Raises: +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct BlankLinesBetweenHeaderAndContent { name: String, 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 efe015b802..eccabe28ec 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/starts_with_this.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/starts_with_this.rs @@ -5,6 +5,39 @@ use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::rules::pydocstyle::helpers::normalize_word; +/// ## What it does +/// Checks for docstrings that start with `This`. +/// +/// ## Why is this bad? +/// [PEP 257] recommends that the first line of a docstring be written in the +/// imperative mood, for consistency. +/// +/// Hint: to rewrite the docstring in the imperative, phrase the first line as +/// if it were a command. +/// +/// This rule may not apply to all projects; its applicability is a matter of +/// convention. By default, this rule is enabled when using the `numpy` +/// convention,, and disabled when using the `google` and `pep257` conventions. +/// +/// ## Example +/// ```python +/// def average(values: list[float]) -> float: +/// """This function returns the mean of the given values.""" +/// ``` +/// +/// Use instead: +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values.""" +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// +/// [PEP 257]: https://peps.python.org/pep-0257/ #[violation] pub struct DocstringStartsWithThis; diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index 0c2680460c..be2ef93a46 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -33,6 +33,7 @@ KNOWN_FORMATTING_VIOLATIONS = [ "bad-quotes-inline-string", "bad-quotes-multiline-string", "explicit-string-concatenation", + "indent-with-spaces", "indentation-with-invalid-multiple", "line-too-long", "missing-trailing-comma", @@ -44,15 +45,20 @@ KNOWN_FORMATTING_VIOLATIONS = [ "multiple-spaces-before-operator", "multiple-statements-on-one-line-colon", "multiple-statements-on-one-line-semicolon", + "no-blank-line-before-function", "no-indented-block-comment", "no-space-after-block-comment", "no-space-after-inline-comment", + "one-blank-line-after-class", + "over-indentation", "over-indented", "prohibited-trailing-comma", "shebang-leading-whitespace", + "surrounding-whitespace", "too-few-spaces-before-inline-comment", "trailing-comma-on-bare-tuple", "triple-single-quotes", + "under-indentation", "unexpected-indentation-comment", "unicode-kind-prefix", "unnecessary-class-parentheses", From a0a93a636f4d33d1c1fa568b177e3c49f38a9683 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Tue, 27 Jun 2023 19:33:58 +0100 Subject: [PATCH 252/447] Implement Pylint `single-string-used-for-slots` (`C0205`) as `single-string-slots` (`PLC0205`) (#5399) ## Summary Implement Pylint rule `single-string-used-for-slots` (`C0205`) as `single-string-slots` (`PLC0205`). This rule checks for single strings being assigned to `__slots__`. For example ```python class Foo: __slots__: str = "bar" def __init__(self, bar: str) -> None: self.bar = bar ``` should be ```python class Foo: __slots__: tuple[str, ...] = ("bar",) def __init__(self, bar: str) -> None: self.bar = bar ``` Related to #970. Includes documentation. ## Test Plan `cargo test` --- .../fixtures/pylint/single_string_slots.py | 35 ++++++ crates/ruff/src/checkers/ast/mod.rs | 3 + crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/pylint/mod.rs | 1 + crates/ruff/src/rules/pylint/rules/mod.rs | 2 + .../rules/pylint/rules/single_string_slots.rs | 107 ++++++++++++++++++ ...tests__PLC0205_single_string_slots.py.snap | 32 ++++++ ruff.schema.json | 1 + 8 files changed, 182 insertions(+) create mode 100644 crates/ruff/resources/test/fixtures/pylint/single_string_slots.py create mode 100644 crates/ruff/src/rules/pylint/rules/single_string_slots.rs create mode 100644 crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0205_single_string_slots.py.snap diff --git a/crates/ruff/resources/test/fixtures/pylint/single_string_slots.py b/crates/ruff/resources/test/fixtures/pylint/single_string_slots.py new file mode 100644 index 0000000000..b7a2dac915 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pylint/single_string_slots.py @@ -0,0 +1,35 @@ +# Errors. +class Foo: + __slots__ = "bar" + + def __init__(self, bar): + self.bar = bar + + +class Foo: + __slots__: str = "bar" + + def __init__(self, bar): + self.bar = bar + + +class Foo: + __slots__: str = f"bar" + + def __init__(self, bar): + self.bar = bar + + +# Non-errors. +class Foo: + __slots__ = ("bar",) + + def __init__(self, bar): + self.bar = bar + + +class Foo: + __slots__: tuple[str, ...] = ("bar",) + + def __init__(self, bar): + self.bar = bar diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 32eab592f3..b7b0d1e925 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -770,6 +770,9 @@ where if self.enabled(Rule::NoSlotsInNamedtupleSubclass) { flake8_slots::rules::no_slots_in_namedtuple_subclass(self, stmt, class_def); } + if self.enabled(Rule::SingleStringSlots) { + pylint::rules::single_string_slots(self, class_def); + } } Stmt::Import(ast::StmtImport { names, range: _ }) => { if self.enabled(Rule::MultipleImportsOnOneLine) { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index b63fea0b74..15b334e884 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -156,6 +156,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pyflakes, "901") => (RuleGroup::Unspecified, rules::pyflakes::rules::RaiseNotImplemented), // pylint + (Pylint, "C0205") => (RuleGroup::Unspecified, rules::pylint::rules::SingleStringSlots), (Pylint, "C0414") => (RuleGroup::Unspecified, rules::pylint::rules::UselessImportAlias), (Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString), (Pylint, "C3002") => (RuleGroup::Unspecified, rules::pylint::rules::UnnecessaryDirectLambdaCall), diff --git a/crates/ruff/src/rules/pylint/mod.rs b/crates/ruff/src/rules/pylint/mod.rs index c99c843d06..4f5a5b2f6a 100644 --- a/crates/ruff/src/rules/pylint/mod.rs +++ b/crates/ruff/src/rules/pylint/mod.rs @@ -33,6 +33,7 @@ mod tests { )] #[test_case(Rule::ComparisonWithItself, Path::new("comparison_with_itself.py"))] #[test_case(Rule::ManualFromImport, Path::new("import_aliasing.py"))] + #[test_case(Rule::SingleStringSlots, Path::new("single_string_slots.py"))] #[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_0.py"))] #[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_1.py"))] #[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_2.py"))] diff --git a/crates/ruff/src/rules/pylint/rules/mod.rs b/crates/ruff/src/rules/pylint/rules/mod.rs index dbbca73e1a..84251fb061 100644 --- a/crates/ruff/src/rules/pylint/rules/mod.rs +++ b/crates/ruff/src/rules/pylint/rules/mod.rs @@ -31,6 +31,7 @@ pub(crate) use property_with_parameters::*; pub(crate) use redefined_loop_name::*; pub(crate) use repeated_isinstance_calls::*; pub(crate) use return_in_init::*; +pub(crate) use single_string_slots::*; pub(crate) use sys_exit_alias::*; pub(crate) use too_many_arguments::*; pub(crate) use too_many_branches::*; @@ -77,6 +78,7 @@ mod property_with_parameters; mod redefined_loop_name; mod repeated_isinstance_calls; mod return_in_init; +mod single_string_slots; mod sys_exit_alias; mod too_many_arguments; mod too_many_branches; diff --git a/crates/ruff/src/rules/pylint/rules/single_string_slots.rs b/crates/ruff/src/rules/pylint/rules/single_string_slots.rs new file mode 100644 index 0000000000..964e5512bd --- /dev/null +++ b/crates/ruff/src/rules/pylint/rules/single_string_slots.rs @@ -0,0 +1,107 @@ +use rustpython_parser::ast::{self, Constant, Expr, Stmt, StmtClassDef}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::identifier::Identifier; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for single strings assigned to `__slots__`. +/// +/// ## Why is this bad? +/// In Python, the `__slots__` attribute allows you to explicitly define the +/// attributes (instance variables) that a class can have. By default, Python +/// uses a dictionary to store an object's attributes, which incurs some memory +/// overhead. However, when `__slots__` is defined, Python uses a more compact +/// internal structure to store the object's attributes, resulting in memory +/// savings. +/// +/// Any string iterable may be assigned to `__slots__` (most commonly, a +/// `tuple` of strings). If a string is assigned to `__slots__`, it is +/// interpreted as a single attribute name, rather than an iterable of attribute +/// names. This can cause confusion, as users that iterate over the `__slots__` +/// value may expect to iterate over a sequence of attributes, but would instead +/// iterate over the characters of the string. +/// +/// To use a single string attribute in `__slots__`, wrap the string in an +/// iterable container type, like a `tuple`. +/// +/// ## Example +/// ```python +/// class Person: +/// __slots__: str = "name" +/// +/// def __init__(self, name: string) -> None: +/// self.name = name +/// ``` +/// +/// Use instead: +/// ```python +/// class Person: +/// __slots__: tuple[str, ...] = ("name",) +/// +/// def __init__(self, name: string) -> None: +/// self.name = name +/// ``` +/// +/// ## References +/// - [Python documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots) +#[violation] +pub struct SingleStringSlots; + +impl Violation for SingleStringSlots { + #[derive_message_formats] + fn message(&self) -> String { + format!("Class `__slots__` should be a non-string iterable") + } +} + +/// PLC0205 +pub(crate) fn single_string_slots(checker: &mut Checker, class: &StmtClassDef) { + for stmt in &class.body { + match stmt { + Stmt::Assign(ast::StmtAssign { targets, value, .. }) => { + for target in targets { + if let Expr::Name(ast::ExprName { id, .. }) = target { + if id.as_str() == "__slots__" { + if matches!( + value.as_ref(), + Expr::Constant(ast::ExprConstant { + value: Constant::Str(_), + .. + }) | Expr::JoinedStr(_) + ) { + checker + .diagnostics + .push(Diagnostic::new(SingleStringSlots, stmt.identifier())); + } + } + } + } + } + Stmt::AnnAssign(ast::StmtAnnAssign { + target, + value: Some(value), + .. + }) => { + if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() { + if id.as_str() == "__slots__" { + if matches!( + value.as_ref(), + Expr::Constant(ast::ExprConstant { + value: Constant::Str(_), + .. + }) | Expr::JoinedStr(_) + ) { + checker + .diagnostics + .push(Diagnostic::new(SingleStringSlots, stmt.identifier())); + } + } + } + } + _ => {} + } + } +} diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0205_single_string_slots.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0205_single_string_slots.py.snap new file mode 100644 index 0000000000..b378d106c9 --- /dev/null +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0205_single_string_slots.py.snap @@ -0,0 +1,32 @@ +--- +source: crates/ruff/src/rules/pylint/mod.rs +--- +single_string_slots.py:3:5: PLC0205 Class `__slots__` should be a non-string iterable + | +1 | # Errors. +2 | class Foo: +3 | __slots__ = "bar" + | ^^^^^^^^^^^^^^^^^ PLC0205 +4 | +5 | def __init__(self, bar): + | + +single_string_slots.py:10:5: PLC0205 Class `__slots__` should be a non-string iterable + | + 9 | class Foo: +10 | __slots__: str = "bar" + | ^^^^^^^^^^^^^^^^^^^^^^ PLC0205 +11 | +12 | def __init__(self, bar): + | + +single_string_slots.py:17:5: PLC0205 Class `__slots__` should be a non-string iterable + | +16 | class Foo: +17 | __slots__: str = f"bar" + | ^^^^^^^^^^^^^^^^^^^^^^^ PLC0205 +18 | +19 | def __init__(self, bar): + | + + diff --git a/ruff.schema.json b/ruff.schema.json index b145c5ad36..c6d3d474b7 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2094,6 +2094,7 @@ "PLC0", "PLC02", "PLC020", + "PLC0205", "PLC0208", "PLC04", "PLC041", From 56f73de0cb49e71913b612e38749eb2ef8bf03ca Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 27 Jun 2023 15:27:12 -0400 Subject: [PATCH 253/447] Misc. clean-up for import resolver (#5401) ## Summary Renaming functions, adding documentation, refactoring the test infrastructure a bit. --- crates/ruff_python_resolver/src/config.rs | 8 - crates/ruff_python_resolver/src/resolver.rs | 721 +++++++++++--------- crates/ruff_python_resolver/src/search.rs | 50 +- 3 files changed, 406 insertions(+), 373 deletions(-) diff --git a/crates/ruff_python_resolver/src/config.rs b/crates/ruff_python_resolver/src/config.rs index 0ae2790683..072e44a993 100644 --- a/crates/ruff_python_resolver/src/config.rs +++ b/crates/ruff_python_resolver/src/config.rs @@ -1,11 +1,6 @@ use std::path::PathBuf; -use crate::python_version::PythonVersion; - pub(crate) struct Config { - /// Path to python interpreter. - pub(crate) python_path: Option, - /// Path to use for typeshed definitions. pub(crate) typeshed_path: Option, @@ -20,7 +15,4 @@ pub(crate) struct Config { /// Default venv environment. pub(crate) venv: Option, - - /// Default Python version. Can be overridden by ExecutionEnvironment. - pub(crate) default_python_version: Option, } diff --git a/crates/ruff_python_resolver/src/resolver.rs b/crates/ruff_python_resolver/src/resolver.rs index 08ff0bc09f..93e913a3f8 100644 --- a/crates/ruff_python_resolver/src/resolver.rs +++ b/crates/ruff_python_resolver/src/resolver.rs @@ -10,11 +10,10 @@ use crate::execution_environment::ExecutionEnvironment; use crate::implicit_imports::ImplicitImport; use crate::import_result::{ImportResult, ImportType}; use crate::module_descriptor::ImportModuleDescriptor; -use crate::search::get_typeshed_root; use crate::{host, implicit_imports, native_module, py_typed, search}; #[allow(clippy::fn_params_excessive_bools)] -fn _resolve_absolute_import( +fn resolve_module_descriptor( root: &Path, module_descriptor: &ImportModuleDescriptor, allow_partial: bool, @@ -220,7 +219,7 @@ fn resolve_absolute_import( // Search for packaged stubs first. PEP 561 indicates that package authors can ship // stubs separately from the package implementation by appending `-stubs` to its // top-level directory name. - let import_result = _resolve_absolute_import( + let import_result = resolve_module_descriptor( root, module_descriptor, allow_partial, @@ -240,7 +239,7 @@ fn resolve_absolute_import( } // Search for a "real" package. - _resolve_absolute_import( + resolve_module_descriptor( root, module_descriptor, allow_partial, @@ -257,7 +256,7 @@ fn resolve_absolute_import( /// /// For example, prefers local imports over third-party imports, and stubs over /// non-stubs. -fn _resolve_best_absolute_import( +fn resolve_best_absolute_import( execution_environment: &ExecutionEnvironment, module_descriptor: &ImportModuleDescriptor, allow_pyi: bool, @@ -293,7 +292,7 @@ fn _resolve_best_absolute_import( .as_os_str() .is_empty() { - if _is_namespace_package_resolved( + if is_namespace_package_resolved( module_descriptor, &typings_import.implicit_imports, ) { @@ -342,7 +341,7 @@ fn _resolve_best_absolute_import( ); local_import.import_type = ImportType::Local; - best_result_so_far = Some(_pick_best_import( + best_result_so_far = Some(pick_best_import( best_result_so_far, local_import, module_descriptor, @@ -350,7 +349,7 @@ fn _resolve_best_absolute_import( } // Look for third-party imports in Python's `sys` path. - for search_path in search::find_python_search_paths(config, host) { + for search_path in search::python_search_paths(config, host) { debug!("Looking in Python search path: {}", search_path.display()); let mut third_party_import = resolve_absolute_import( @@ -364,7 +363,7 @@ fn _resolve_best_absolute_import( ); third_party_import.import_type = ImportType::ThirdParty; - best_result_so_far = Some(_pick_best_import( + best_result_so_far = Some(pick_best_import( best_result_so_far, third_party_import, module_descriptor, @@ -374,7 +373,7 @@ fn _resolve_best_absolute_import( // If a library is fully `py.typed`, prefer the current result. There's one exception: // we're executing from `typeshed` itself. In that case, use the `typeshed` lookup below, // rather than favoring `py.typed` libraries. - if let Some(typeshed_root) = get_typeshed_root(config, host) { + if let Some(typeshed_root) = search::typeshed_root(config, host) { debug!( "Looking in typeshed root directory: {}", typeshed_root.display() @@ -392,7 +391,7 @@ fn _resolve_best_absolute_import( // Check for a stdlib typeshed file. debug!("Looking for typeshed stdlib path: {}", import_name); if let Some(mut typeshed_stdilib_import) = - _find_typeshed_path(module_descriptor, true, config, host) + find_typeshed_path(module_descriptor, true, config, host) { typeshed_stdilib_import.is_stdlib_typeshed_file = true; return Some(typeshed_stdilib_import); @@ -401,11 +400,11 @@ fn _resolve_best_absolute_import( // Check for a third-party typeshed file. debug!("Looking for typeshed third-party path: {}", import_name); if let Some(mut typeshed_third_party_import) = - _find_typeshed_path(module_descriptor, false, config, host) + find_typeshed_path(module_descriptor, false, config, host) { typeshed_third_party_import.is_third_party_typeshed_file = true; - best_result_so_far = Some(_pick_best_import( + best_result_so_far = Some(pick_best_import( best_result_so_far, typeshed_third_party_import, module_descriptor, @@ -423,7 +422,7 @@ fn _resolve_best_absolute_import( /// file, so the only way that symbols can be resolved is if submodules /// are present. If specific symbols were requested, make sure they /// are all satisfied by submodules (as listed in the implicit imports). -fn _is_namespace_package_resolved( +fn is_namespace_package_resolved( module_descriptor: &ImportModuleDescriptor, implicit_imports: &HashMap, ) -> bool { @@ -444,7 +443,7 @@ fn _is_namespace_package_resolved( /// Finds the `typeshed` path for the given module descriptor. /// /// Supports both standard library and third-party `typeshed` lookups. -fn _find_typeshed_path( +fn find_typeshed_path( module_descriptor: &ImportModuleDescriptor, is_std_lib: bool, config: &Config, @@ -459,12 +458,12 @@ fn _find_typeshed_path( let mut typeshed_paths = vec![]; if is_std_lib { - if let Some(path) = search::get_stdlib_typeshed_path(config, host) { + if let Some(path) = search::stdlib_typeshed_path(config, host) { typeshed_paths.push(path); } } else { if let Some(paths) = - search::get_third_party_typeshed_package_paths(module_descriptor, config, host) + search::third_party_typeshed_package_paths(module_descriptor, config, host) { typeshed_paths.extend(paths); } @@ -498,7 +497,7 @@ fn _find_typeshed_path( /// Given a current "best" import and a newly discovered result, returns the /// preferred result. -fn _pick_best_import( +fn pick_best_import( best_import_so_far: Option, new_import: ImportResult, module_descriptor: &ImportModuleDescriptor, @@ -541,11 +540,11 @@ fn _pick_best_import( // imported symbols. if best_import_so_far.is_namespace_package && new_import.is_namespace_package { if !module_descriptor.imported_symbols.is_empty() { - if !_is_namespace_package_resolved( + if !is_namespace_package_resolved( module_descriptor, &best_import_so_far.implicit_imports, ) { - if _is_namespace_package_resolved( + if is_namespace_package_resolved( module_descriptor, &new_import.implicit_imports, ) { @@ -614,7 +613,7 @@ fn _pick_best_import( } /// Resolve a relative import. -fn _resolve_relative_import( +fn resolve_relative_import( source_file: &Path, module_descriptor: &ImportModuleDescriptor, ) -> Option { @@ -654,7 +653,7 @@ fn _resolve_relative_import( } /// Resolve an absolute or relative import. -fn _resolve_import_strict( +fn resolve_import_strict( source_file: &Path, execution_environment: &ExecutionEnvironment, module_descriptor: &ImportModuleDescriptor, @@ -666,7 +665,7 @@ fn _resolve_import_strict( if module_descriptor.leading_dots > 0 { debug!("Resolving relative import for: {import_name}"); - let relative_import = _resolve_relative_import(source_file, module_descriptor); + let relative_import = resolve_relative_import(source_file, module_descriptor); if let Some(mut relative_import) = relative_import { relative_import.is_relative = true; @@ -675,7 +674,7 @@ fn _resolve_import_strict( } else { debug!("Resolving best absolute import for: {import_name}"); - let best_import = _resolve_best_absolute_import( + let best_import = resolve_best_absolute_import( execution_environment, module_descriptor, true, @@ -688,7 +687,7 @@ fn _resolve_import_strict( debug!("Resolving best non-stub absolute import for: {import_name}"); best_import.non_stub_import_result = Some(Box::new( - _resolve_best_absolute_import( + resolve_best_absolute_import( execution_environment, module_descriptor, false, @@ -706,6 +705,15 @@ fn _resolve_import_strict( } /// Resolves an import, given the current file and the import descriptor. +/// +/// The algorithm is as follows: +/// +/// 1. If the import is relative, convert it to an absolute import. +/// 2. Find the "best" match for the import, allowing stub files. Search local imports, any +/// configured search paths, the Python path, the typeshed path, etc. +/// 3. If a stub file was found, find the "best" match for the import, disallowing stub files. +/// 4. If the import wasn't resolved, try to resolve it in the parent directory, then the parent's +/// parent, and so on, until the import root is reached. fn resolve_import( source_file: &Path, execution_environment: &ExecutionEnvironment, @@ -713,7 +721,7 @@ fn resolve_import( config: &Config, host: &Host, ) -> ImportResult { - let import_result = _resolve_import_strict( + let import_result = resolve_import_strict( source_file, execution_environment, module_descriptor, @@ -775,48 +783,61 @@ mod tests { use crate::config::Config; use crate::execution_environment::ExecutionEnvironment; + use crate::host; use crate::import_result::{ImportResult, ImportType}; use crate::module_descriptor::ImportModuleDescriptor; use crate::python_platform::PythonPlatform; use crate::python_version::PythonVersion; use crate::resolver::resolve_import; - use crate::{host, SITE_PACKAGES}; - struct PythonFile { - path: PathBuf, - content: String, + /// Create a file at the given path with the given content. + fn create(path: PathBuf, content: &str) -> io::Result { + if let Some(parent) = path.parent() { + create_dir_all(parent)?; + } + let mut f = File::create(&path)?; + f.write_all(content.as_bytes())?; + f.sync_all()?; + + Ok(path) } - impl PythonFile { - fn new(path: impl Into, content: impl Into) -> Self { - Self { - path: path.into(), - content: content.into(), - } - } - - fn create(&self, dir: impl AsRef) -> io::Result { - let file_path = dir.as_ref().join(self.path.as_path()); - if let Some(parent) = file_path.parent() { - create_dir_all(parent)?; - } - let mut f = File::create(&file_path)?; - f.write_all(self.content.as_bytes())?; - f.sync_all()?; - - Ok(file_path) - } + /// Create an empty file at the given path. + fn empty(path: PathBuf) -> io::Result { + create(path, "") } - fn _resolve>( + /// Create a partial `py.typed` file at the given path. + fn partial(path: PathBuf) -> io::Result { + create(path, "partial\n") + } + + /// Create a `py.typed` file at the given path. + fn typed(path: PathBuf) -> io::Result { + create(path, "# typed") + } + + #[derive(Debug, Default)] + struct ResolverOptions { + extra_paths: Vec, + library: Option, + stub_path: Option, + typeshed_path: Option, + } + + fn resolve_options( source_file: impl AsRef, name: &str, - root: T, - extra_paths: Vec, - library: Option, - stub_path: Option, - typeshed_path: Option, + root: impl Into, + options: ResolverOptions, ) -> ImportResult { + let ResolverOptions { + extra_paths, + library, + stub_path, + typeshed_path, + } = options; + let execution_environment = ExecutionEnvironment { root: root.into(), python_version: PythonVersion::Py37, @@ -837,16 +858,14 @@ mod tests { }; let config = Config { + typeshed_path, + stub_path, venv_path: None, venv: None, - python_path: None, - typeshed_path: typeshed_path.map(Into::into), - stub_path: stub_path.map(Into::into), - default_python_version: None, }; let host = host::StaticHost::new(if let Some(library) = library { - vec![library.into()] + vec![library] } else { Vec::new() }); @@ -860,94 +879,32 @@ mod tests { ) } - fn resolve>( - source_file: impl AsRef, - name: &str, - root: T, - ) -> ImportResult { - _resolve(source_file, name, root, Vec::new(), None, None, None) - } - - fn resolve_with_extra_paths>( - source_file: impl AsRef, - name: &str, - root: T, - extra_paths: Vec, - ) -> ImportResult { - _resolve(source_file, name, root, extra_paths, None, None, None) - } - - fn resolve_with_library>( - source_file: impl AsRef, - name: &str, - root: T, - library: T, - ) -> ImportResult { - _resolve( - source_file, - name, - root, - Vec::new(), - Some(library), - None, - None, - ) - } - - fn resolve_with_stub_path>( - source_file: impl AsRef, - name: &str, - root: T, - library: T, - stub_path: T, - ) -> ImportResult { - _resolve( - source_file, - name, - root, - Vec::new(), - Some(library), - Some(stub_path), - None, - ) - } - - fn resolve_with_typeshed_path>( - source_file: impl AsRef, - name: &str, - root: T, - library: T, - typeshed_path: T, - ) -> ImportResult { - _resolve( - source_file, - name, - root, - Vec::new(), - Some(library), - None, - Some(typeshed_path), - ) + fn setup() { + env_logger::builder().is_test(true).try_init().ok(); } #[test] fn partial_stub_file_exists() -> io::Result<()> { - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; - let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); - let partial_stub_pyi = - PythonFile::new("myLib-stubs/partialStub.pyi", "def test(): ...").create(&library)?; - PythonFile::new("myLib-stubs/py.typed", "partial\n").create(&library)?; - let partial_stub_py = - PythonFile::new("myLib/partialStub.py", "def test(): ...").create(&library)?; + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); - let result = resolve_with_library( + partial(library.join("myLib-stubs/py.typed"))?; + let partial_stub_pyi = empty(library.join("myLib-stubs").join("partialStub.pyi"))?; + let partial_stub_py = empty(library.join("myLib/partialStub.py"))?; + + let result = resolve_options( partial_stub_py, "myLib.partialStub", - dir.path(), - library.as_path(), + root, + ResolverOptions { + library: Some(library), + ..Default::default() + }, ); assert!(result.is_import_found); @@ -965,19 +922,27 @@ mod tests { #[test] fn partial_stub_init_exists() -> io::Result<()> { - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; - let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); - PythonFile::new("myLib-stubs/py.typed", "partial\n").create(&library)?; - let partial_stub_init_pyi = - PythonFile::new("myLib-stubs/__init__.pyi", "def test(): ...").create(&library)?; - let partial_stub_init_py = - PythonFile::new("myLib/__init__.py", "def test(): ...").create(&library)?; + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); - let result = - resolve_with_library(partial_stub_init_py, "myLib", dir.path(), library.as_path()); + partial(library.join("myLib-stubs/py.typed"))?; + let partial_stub_init_pyi = empty(library.join("myLib-stubs/__init__.pyi"))?; + let partial_stub_init_py = empty(library.join("myLib/__init__.py"))?; + + let result = resolve_options( + partial_stub_init_py, + "myLib", + root, + ResolverOptions { + library: Some(library), + ..Default::default() + }, + ); assert!(result.is_import_found); assert!(result.is_stub_file); @@ -994,22 +959,30 @@ mod tests { #[test] fn side_by_side_files() -> io::Result<()> { - let dir = TempDir::new()?; - let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); - let my_file = PythonFile::new("myFile.py", "# not used").create(dir.path())?; - let side_by_side_stub_file = - PythonFile::new("myLib-stubs/partialStub.pyi", "def test(): ...").create(&library)?; - PythonFile::new("myLib-stubs/py.typed", "partial\n").create(&library)?; - PythonFile::new("myLib/partialStub.pyi", "# empty").create(&library)?; - PythonFile::new("myLib/partialStub.py", "def test(): pass").create(&library)?; - let partial_stub_file = - PythonFile::new("myLib-stubs/partialStub2.pyi", "def test(): ...").create(&library)?; - PythonFile::new("myLib/partialStub2.py", "def test(): pass").create(&library)?; + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + partial(library.join("myLib-stubs/py.typed"))?; + empty(library.join("myLib/partialStub.pyi"))?; + empty(library.join("myLib/partialStub.py"))?; + empty(library.join("myLib/partialStub2.py"))?; + let my_file = empty(root.join("myFile.py"))?; + let side_by_side_stub_file = empty(library.join("myLib-stubs/partialStub.pyi"))?; + let partial_stub_file = empty(library.join("myLib-stubs/partialStub2.pyi"))?; // Stub package wins over original package (per PEP 561 rules). - let side_by_side_result = - resolve_with_library(&my_file, "myLib.partialStub", dir.path(), &library); + let side_by_side_result = resolve_options( + &my_file, + "myLib.partialStub", + root, + ResolverOptions { + library: Some(library.clone()), + ..Default::default() + }, + ); assert!(side_by_side_result.is_import_found); assert!(side_by_side_result.is_stub_file); assert_eq!( @@ -1018,8 +991,15 @@ mod tests { ); // Side by side stub doesn't completely disable partial stub. - let partial_stub_result = - resolve_with_library(&my_file, "myLib.partialStub2", dir.path(), &library); + let partial_stub_result = resolve_options( + &my_file, + "myLib.partialStub2", + root, + ResolverOptions { + library: Some(library), + ..Default::default() + }, + ); assert!(partial_stub_result.is_import_found); assert!(partial_stub_result.is_stub_file); assert_eq!( @@ -1032,21 +1012,26 @@ mod tests { #[test] fn stub_package() -> io::Result<()> { - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; - let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); - PythonFile::new("myLib-stubs/stub.pyi", "# empty").create(&library)?; - PythonFile::new("myLib-stubs/__init__.pyi", "# empty").create(&library)?; - let partial_stub_py = - PythonFile::new("myLib/partialStub.py", "def test(): ...").create(&library)?; + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); - let result = resolve_with_library( + empty(library.join("myLib-stubs/stub.pyi"))?; + empty(library.join("myLib-stubs/__init__.pyi"))?; + let partial_stub_py = empty(library.join("myLib/partialStub.py"))?; + + let result = resolve_options( partial_stub_py, "myLib.partialStub", - dir.path(), - library.as_path(), + root, + ResolverOptions { + library: Some(library), + ..Default::default() + }, ); // If fully typed stub package exists, that wins over the real package. @@ -1057,20 +1042,25 @@ mod tests { #[test] fn stub_namespace_package() -> io::Result<()> { - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; - let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); - PythonFile::new("myLib-stubs/stub.pyi", "# empty").create(&library)?; - let partial_stub_py = - PythonFile::new("myLib/partialStub.py", "def test(): ...").create(&library)?; + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); - let result = resolve_with_library( + empty(library.join("myLib-stubs/stub.pyi"))?; + let partial_stub_py = empty(library.join("myLib/partialStub.py"))?; + + let result = resolve_options( partial_stub_py.clone(), "myLib.partialStub", - dir.path(), - library.as_path(), + root, + ResolverOptions { + library: Some(library), + ..Default::default() + }, ); // If fully typed stub package exists, that wins over the real package. @@ -1083,24 +1073,29 @@ mod tests { #[test] fn stub_in_typing_folder_over_partial_stub_package() -> io::Result<()> { - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; - let typing_folder = dir.path().join("typing"); - let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + let typing_folder = root.join("typing"); - PythonFile::new("myLib-stubs/py.typed", "partial\n").create(&library)?; - PythonFile::new("myLib-stubs/__init__.pyi", "").create(&library)?; - let my_lib_pyi = PythonFile::new("myLib.pyi", "# empty").create(&typing_folder)?; - let my_lib_init_py = - PythonFile::new("myLib/__init__.py", "def test(): pass").create(&library)?; + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); - let result = resolve_with_stub_path( + partial(library.join("myLib-stubs/py.typed"))?; + empty(library.join("myLib-stubs/__init__.pyi"))?; + let my_lib_pyi = empty(typing_folder.join("myLib.pyi"))?; + let my_lib_init_py = empty(library.join("myLib/__init__.py"))?; + + let result = resolve_options( my_lib_init_py, "myLib", - dir.path(), - library.as_path(), - typing_folder.as_path(), + root, + ResolverOptions { + library: Some(library), + stub_path: Some(typing_folder), + ..Default::default() + }, ); // If the package exists in typing folder, that gets picked up first (so we resolve to @@ -1114,24 +1109,28 @@ mod tests { #[test] fn partial_stub_package_in_typing_folder() -> io::Result<()> { - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; - let typing_folder = dir.path().join("typing"); - let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + let typing_folder = root.join("typing"); - PythonFile::new("myLib-stubs/py.typed", "partial\n").create(&typing_folder)?; - let my_lib_stubs_init_pyi = PythonFile::new("myLib-stubs/__init__.pyi", "def test(): ...") - .create(&typing_folder)?; - let my_lib_init_py = - PythonFile::new("myLib/__init__.py", "def test(): pass").create(&library)?; + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); - let result = resolve_with_stub_path( + partial(typing_folder.join("myLib-stubs/py.typed"))?; + let my_lib_stubs_init_pyi = empty(typing_folder.join("myLib-stubs/__init__.pyi"))?; + let my_lib_init_py = empty(library.join("myLib/__init__.py"))?; + + let result = resolve_options( my_lib_init_py, "myLib", - dir.path(), - library.as_path(), - typing_folder.as_path(), + root, + ResolverOptions { + library: Some(library), + stub_path: Some(typing_folder), + ..Default::default() + }, ); // If the package exists in typing folder, that gets picked up first (so we resolve to @@ -1145,25 +1144,29 @@ mod tests { #[test] fn typeshed_folder() -> io::Result<()> { - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; - let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); - let typeshed_folder = dir.path().join("ts"); + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + let typeshed_folder = root.join("ts"); - PythonFile::new("myLib-stubs/py.typed", "partial\n").create(&library)?; - let my_lib_stubs_init_pyi = - PythonFile::new("myLib-stubs/__init__.pyi", "def test(): ...").create(&library)?; - PythonFile::new("stubs/myLibPackage/myLib.pyi", "# empty").create(&typeshed_folder)?; - let my_lib_init_py = - PythonFile::new("myLib/__init__.py", "def test(): pass").create(&library)?; + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); - let result = resolve_with_typeshed_path( + empty(typeshed_folder.join("stubs/myLibPackage/myLib.pyi"))?; + partial(library.join("myLib-stubs/py.typed"))?; + let my_lib_stubs_init_pyi = empty(library.join("myLib-stubs/__init__.pyi"))?; + let my_lib_init_py = empty(library.join("myLib/__init__.py"))?; + + let result = resolve_options( my_lib_init_py, "myLib", - dir.path(), - library.as_path(), - typeshed_folder.as_path(), + root, + ResolverOptions { + library: Some(library), + typeshed_path: Some(typeshed_folder), + ..Default::default() + }, ); // Stub packages win over typeshed. @@ -1176,18 +1179,28 @@ mod tests { #[test] fn py_typed_file() -> io::Result<()> { - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; - let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); - let partial_stub_init_pyi = - PythonFile::new("myLib-stubs/__init__.pyi", "def test(): ...").create(&library)?; - PythonFile::new("myLib-stubs/py.typed", "partial\n").create(&library)?; - PythonFile::new("myLib/__init__.py", "def test(): pass").create(&library)?; - let package_py_typed = PythonFile::new("myLib/py.typed", "# typed").create(&library)?; + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); - let result = resolve_with_library(package_py_typed, "myLib", dir.path(), library.as_path()); + empty(library.join("myLib/__init__.py"))?; + partial(library.join("myLib-stubs/py.typed"))?; + let partial_stub_init_pyi = empty(library.join("myLib-stubs/__init__.pyi"))?; + let package_py_typed = typed(library.join("myLib/py.typed"))?; + + let result = resolve_options( + package_py_typed, + "myLib", + root, + ResolverOptions { + library: Some(library), + ..Default::default() + }, + ); // Partial stub package always overrides original package. assert!(result.is_import_found); @@ -1199,23 +1212,28 @@ mod tests { #[test] fn py_typed_library() -> io::Result<()> { - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; - let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); - let typeshed_folder = dir.path().join("ts"); + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + let typeshed_folder = root.join("ts"); - let init_py = PythonFile::new("os/__init__.py", "def test(): ...").create(&library)?; - PythonFile::new("os/py.typed", "").create(&library)?; - let typeshed_init_pyi = - PythonFile::new("stubs/os/os/__init__.pyi", "# empty").create(&typeshed_folder)?; + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); - let result = resolve_with_typeshed_path( + typed(library.join("os/py.typed"))?; + let init_py = empty(library.join("os/__init__.py"))?; + let typeshed_init_pyi = empty(typeshed_folder.join("stubs/os/os/__init__.pyi"))?; + + let result = resolve_options( typeshed_init_pyi, "os", - dir.path(), - library.as_path(), - typeshed_folder.as_path(), + root, + ResolverOptions { + library: Some(library), + typeshed_path: Some(typeshed_folder), + ..Default::default() + }, ); assert!(result.is_import_found); @@ -1226,22 +1244,27 @@ mod tests { #[test] fn non_py_typed_library() -> io::Result<()> { - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; - let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); - let typeshed_folder = dir.path().join("ts"); + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + let typeshed_folder = root.join("ts"); - PythonFile::new("os/__init__.py", "def test(): ...").create(&library)?; - let typeshed_init_pyi = - PythonFile::new("stubs/os/os/__init__.pyi", "# empty").create(&typeshed_folder)?; + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); - let result = resolve_with_typeshed_path( + empty(library.join("os/__init__.py"))?; + let typeshed_init_pyi = empty(typeshed_folder.join("stubs/os/os/__init__.pyi"))?; + + let result = resolve_options( typeshed_init_pyi.clone(), "os", - dir.path(), - library.as_path(), - typeshed_folder.as_path(), + root, + ResolverOptions { + library: Some(library), + typeshed_path: Some(typeshed_folder), + ..Default::default() + }, ); assert!(result.is_import_found); @@ -1253,14 +1276,15 @@ mod tests { #[test] fn import_side_by_side_file_root() -> io::Result<()> { - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); - let file1 = PythonFile::new("file1.py", "import file1").create(&dir)?; - let file2 = PythonFile::new("file2.py", "import file2").create(&dir)?; + let file1 = empty(root.join("file1.py"))?; + let file2 = empty(root.join("file2.py"))?; - let result = resolve(file2, "file1", dir.path()); + let result = resolve_options(file2, "file1", root, ResolverOptions::default()); assert!(result.is_import_found); assert_eq!(result.import_type, ImportType::Local); @@ -1271,15 +1295,16 @@ mod tests { #[test] fn import_side_by_side_file_sub_folder() -> io::Result<()> { - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); - let test_init = PythonFile::new("test/__init__.py", "").create(&dir)?; - let test_file1 = PythonFile::new("test/file1.py", "import file1").create(&dir)?; - let test_file2 = PythonFile::new("test/file2.py", "import file2").create(&dir)?; + let test_init = empty(root.join("test/__init__.py"))?; + let test_file1 = empty(root.join("test/file1.py"))?; + let test_file2 = empty(root.join("test/file2.py"))?; - let result = resolve(test_file2, "test.file1", dir.path()); + let result = resolve_options(test_file2, "test.file1", root, ResolverOptions::default()); assert!(result.is_import_found); assert_eq!(result.import_type, ImportType::Local); @@ -1290,15 +1315,21 @@ mod tests { #[test] fn import_side_by_side_file_sub_under_src_folder() -> io::Result<()> { - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); - let nested_init = PythonFile::new("src/nested/__init__.py", "").create(&dir)?; - let nested_file1 = PythonFile::new("src/nested/file1.py", "import file1").create(&dir)?; - let nested_file2 = PythonFile::new("src/nested/file2.py", "import file2").create(&dir)?; + let nested_init = empty(root.join("src/nested/__init__.py"))?; + let nested_file1 = empty(root.join("src/nested/file1.py"))?; + let nested_file2 = empty(root.join("src/nested/file2.py"))?; - let result = resolve(nested_file2, "nested.file1", dir.path()); + let result = resolve_options( + nested_file2, + "nested.file1", + root, + ResolverOptions::default(), + ); assert!(result.is_import_found); assert_eq!(result.import_type, ImportType::Local); @@ -1309,16 +1340,15 @@ mod tests { #[test] fn import_file_sub_under_containing_folder() -> io::Result<()> { - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); - let nested_file1 = - PythonFile::new("src/nested/file1.py", "def test1(): ... ").create(&dir)?; - let nested_file2 = - PythonFile::new("src/nested/nested2/file2.py", "def test2(): ...").create(&dir)?; + let nested_file1 = empty(root.join("src/nested/file1.py"))?; + let nested_file2 = empty(root.join("src/nested/nested2/file2.py"))?; - let result = resolve(nested_file2, "file1", dir.path()); + let result = resolve_options(nested_file2, "file1", root, ResolverOptions::default()); assert!(result.is_import_found); assert_eq!(result.import_type, ImportType::Local); @@ -1329,15 +1359,18 @@ mod tests { #[test] fn import_side_by_side_file_sub_under_lib_folder() -> io::Result<()> { - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; - let library = TempDir::new()?.path().join("lib").join(SITE_PACKAGES); + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); - PythonFile::new("myLib/file1.py", "def test1(): ...").create(&library)?; - let file2 = PythonFile::new("myLib/file2.py", "def test2(): ...").create(&library)?; + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); - let result = resolve(file2, "file1", dir.path()); + empty(library.join("myLib/file1.py"))?; + let file2 = empty(library.join("myLib/file2.py"))?; + + let result = resolve_options(file2, "file1", root, ResolverOptions::default()); debug!("result: {:?}", result); @@ -1349,22 +1382,26 @@ mod tests { #[test] fn nested_namespace_package_1() -> io::Result<()> { // See: https://github.com/microsoft/pyright/issues/5089. - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); - let file = PythonFile::new("package1/a/b/c/d.py", "def f(): pass").create(&dir)?; - let package1_init = PythonFile::new("package1/a/__init__.py", "").create(&dir)?; - let package2_init = PythonFile::new("package2/a/__init__.py", "").create(&dir)?; + let file = empty(root.join("package1/a/b/c/d.py"))?; + let package1_init = empty(root.join("package1/a/__init__.py"))?; + let package2_init = empty(root.join("package2/a/__init__.py"))?; - let package1 = dir.path().join("package1"); - let package2 = dir.path().join("package2"); + let package1 = root.join("package1"); + let package2 = root.join("package2"); - let result = resolve_with_extra_paths( + let result = resolve_options( package2_init, "a.b.c.d", - dir.path(), - vec![package1, package2], + root, + ResolverOptions { + extra_paths: vec![package1, package2], + ..Default::default() + }, ); assert!(result.is_import_found); @@ -1380,22 +1417,26 @@ mod tests { #[test] fn nested_namespace_package_2() -> io::Result<()> { // See: https://github.com/microsoft/pyright/issues/5089. - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); - let file = PythonFile::new("package1/a/b/c/d.py", "def f(): pass").create(&dir)?; - let package1_init = PythonFile::new("package1/a/b/c/__init__.py", "").create(&dir)?; - let package2_init = PythonFile::new("package2/a/b/c/__init__.py", "").create(&dir)?; + let file = empty(root.join("package1/a/b/c/d.py"))?; + let package1_init = empty(root.join("package1/a/b/c/__init__.py"))?; + let package2_init = empty(root.join("package2/a/b/c/__init__.py"))?; - let package1 = dir.path().join("package1"); - let package2 = dir.path().join("package2"); + let package1 = root.join("package1"); + let package2 = root.join("package2"); - let result = resolve_with_extra_paths( + let result = resolve_options( package2_init, "a.b.c.d", - dir.path(), - vec![package1, package2], + root, + ResolverOptions { + extra_paths: vec![package1, package2], + ..Default::default() + }, ); assert!(result.is_import_found); @@ -1411,21 +1452,25 @@ mod tests { #[test] fn nested_namespace_package_3() -> io::Result<()> { // See: https://github.com/microsoft/pyright/issues/5089. - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); - PythonFile::new("package1/a/b/c/d.py", "def f(): pass").create(&dir)?; - let package2_init = PythonFile::new("package2/a/__init__.py", "").create(&dir)?; + empty(root.join("package1/a/b/c/d.py"))?; + let package2_init = empty(root.join("package2/a/__init__.py"))?; - let package1 = dir.path().join("package1"); - let package2 = dir.path().join("package2"); + let package1 = root.join("package1"); + let package2 = root.join("package2"); - let result = resolve_with_extra_paths( + let result = resolve_options( package2_init, "a.b.c.d", - dir.path(), - vec![package1, package2], + root, + ResolverOptions { + extra_paths: vec![package1, package2], + ..Default::default() + }, ); assert!(!result.is_import_found); @@ -1436,23 +1481,27 @@ mod tests { #[test] fn nested_namespace_package_4() -> io::Result<()> { // See: https://github.com/microsoft/pyright/issues/5089. - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); - PythonFile::new("package1/a/b/__init__.py", "").create(&dir)?; - PythonFile::new("package1/a/b/c.py", "def f(): pass").create(&dir)?; - PythonFile::new("package2/a/__init__.py", "").create(&dir)?; - let package2_a_b_init = PythonFile::new("package2/a/b/__init__.py", "").create(&dir)?; + empty(root.join("package1/a/b/__init__.py"))?; + empty(root.join("package1/a/b/c.py"))?; + empty(root.join("package2/a/__init__.py"))?; + let package2_a_b_init = empty(root.join("package2/a/b/__init__.py"))?; - let package1 = dir.path().join("package1"); - let package2 = dir.path().join("package2"); + let package1 = root.join("package1"); + let package2 = root.join("package2"); - let result = resolve_with_extra_paths( + let result = resolve_options( package2_a_b_init, "a.b.c", - dir.path(), - vec![package1, package2], + root, + ResolverOptions { + extra_paths: vec![package1, package2], + ..Default::default() + }, ); assert!(!result.is_import_found); @@ -1463,14 +1512,15 @@ mod tests { // New tests, don't exist upstream. #[test] fn relative_import_side_by_side_file_root() -> io::Result<()> { - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); - let file1 = PythonFile::new("file1.py", "def test(): ...").create(&dir)?; - let file2 = PythonFile::new("file2.py", "def test(): ...").create(&dir)?; + let file1 = empty(root.join("file1.py"))?; + let file2 = empty(root.join("file2.py"))?; - let result = resolve(file2, ".file1", dir.path()); + let result = resolve_options(file2, ".file1", root, ResolverOptions::default()); assert!(result.is_import_found); assert_eq!(result.import_type, ImportType::Local); @@ -1481,14 +1531,15 @@ mod tests { #[test] fn invalid_relative_import_side_by_side_file_root() -> io::Result<()> { - env_logger::builder().is_test(true).try_init().ok(); + setup(); - let dir = TempDir::new()?; + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); - PythonFile::new("file1.py", "def test(): ...").create(&dir)?; - let file2 = PythonFile::new("file2.py", "def test(): ...").create(&dir)?; + empty(root.join("file1.py"))?; + let file2 = empty(root.join("file2.py"))?; - let result = resolve(file2, "..file1", dir.path()); + let result = resolve_options(file2, "..file1", root, ResolverOptions::default()); assert!(!result.is_import_found); diff --git a/crates/ruff_python_resolver/src/search.rs b/crates/ruff_python_resolver/src/search.rs index 66145aef78..9f61b350f0 100644 --- a/crates/ruff_python_resolver/src/search.rs +++ b/crates/ruff_python_resolver/src/search.rs @@ -92,7 +92,7 @@ fn find_site_packages_path( Some(default_dir.join(SITE_PACKAGES)) } -fn get_paths_from_pth_files(parent_dir: &Path) -> Vec { +fn find_paths_from_pth_files(parent_dir: &Path) -> Vec { fs::read_dir(parent_dir) .unwrap() .flatten() @@ -133,21 +133,16 @@ fn get_paths_from_pth_files(parent_dir: &Path) -> Vec { } /// Find the Python search paths for the given virtual environment. -pub(crate) fn find_python_search_paths( - config: &Config, - host: &Host, -) -> Vec { +fn find_python_search_paths(config: &Config, host: &Host) -> Vec { if let Some(venv_path) = config.venv_path.as_ref() { if let Some(venv) = config.venv.as_ref() { let mut found_paths = vec![]; for lib_name in ["lib", "Lib", "lib64"] { let lib_path = venv_path.join(venv).join(lib_name); - if let Some(site_packages_path) = - find_site_packages_path(&lib_path, config.default_python_version) - { + if let Some(site_packages_path) = find_site_packages_path(&lib_path, None) { // Add paths from any `.pth` files in each of the `site-packages` directories. - found_paths.extend(get_paths_from_pth_files(&site_packages_path)); + found_paths.extend(find_paths_from_pth_files(&site_packages_path)); // Add the `site-packages` directory to the search path. found_paths.push(site_packages_path); @@ -173,13 +168,13 @@ pub(crate) fn find_python_search_paths( } /// Determine the relevant Python search paths. -fn get_python_search_paths(config: &Config, host: &Host) -> Vec { +pub(crate) fn python_search_paths(config: &Config, host: &Host) -> Vec { // TODO(charlie): Cache search paths. find_python_search_paths(config, host) } /// Determine the root of the `typeshed` directory. -pub(crate) fn get_typeshed_root(config: &Config, host: &Host) -> Option { +pub(crate) fn typeshed_root(config: &Config, host: &Host) -> Option { if let Some(typeshed_path) = config.typeshed_path.as_ref() { // Did the user specify a typeshed path? if typeshed_path.is_dir() { @@ -187,7 +182,7 @@ pub(crate) fn get_typeshed_root(config: &Config, host: &Host) } } else { // If not, we'll look in the Python search paths. - for python_search_path in get_python_search_paths(config, host) { + for python_search_path in python_search_paths(config, host) { let possible_typeshed_path = python_search_path.join("typeshed"); if possible_typeshed_path.is_dir() { return Some(possible_typeshed_path); @@ -198,19 +193,14 @@ pub(crate) fn get_typeshed_root(config: &Config, host: &Host) None } -/// Format the expected `typeshed` subdirectory. -fn format_typeshed_subdirectory(typeshed_path: &Path, is_stdlib: bool) -> PathBuf { - typeshed_path.join(if is_stdlib { "stdlib" } else { "stubs" }) -} - /// Determine the current `typeshed` subdirectory. -fn get_typeshed_subdirectory( +fn typeshed_subdirectory( is_stdlib: bool, config: &Config, host: &Host, ) -> Option { - let typeshed_path = get_typeshed_root(config, host)?; - let typeshed_path = format_typeshed_subdirectory(&typeshed_path, is_stdlib); + let typeshed_path = + typeshed_root(config, host)?.join(if is_stdlib { "stdlib" } else { "stubs" }); if typeshed_path.is_dir() { Some(typeshed_path) } else { @@ -218,14 +208,6 @@ fn get_typeshed_subdirectory( } } -/// Determine the current `typeshed` subdirectory for the standard library. -pub(crate) fn get_stdlib_typeshed_path( - config: &Config, - host: &Host, -) -> Option { - get_typeshed_subdirectory(true, config, host) -} - /// Generate a map from PyPI-registered package name to a list of paths /// containing the package's stubs. fn build_typeshed_third_party_package_map(third_party_dir: &Path) -> HashMap> { @@ -270,13 +252,21 @@ fn build_typeshed_third_party_package_map(third_party_dir: &Path) -> HashMap( +pub(crate) fn third_party_typeshed_package_paths( module_descriptor: &ImportModuleDescriptor, config: &Config, host: &Host, ) -> Option> { - let typeshed_path = get_typeshed_subdirectory(false, config, host)?; + let typeshed_path = typeshed_subdirectory(false, config, host)?; let package_paths = build_typeshed_third_party_package_map(&typeshed_path); let first_name_part = module_descriptor.name_parts.first().map(String::as_str)?; package_paths.get(first_name_part).cloned() } + +/// Determine the current `typeshed` subdirectory for the standard library. +pub(crate) fn stdlib_typeshed_path( + config: &Config, + host: &Host, +) -> Option { + typeshed_subdirectory(true, config, host) +} From 2c99b268c677b929884d2c51887d5f931b1bea69 Mon Sep 17 00:00:00 2001 From: Marti Raudsepp Date: Wed, 28 Jun 2023 03:19:20 +0300 Subject: [PATCH 254/447] Exclude docstrings from PYI053 (#5405) ## Summary The `Y053` rule of `flake8-pyi` ignores docstrings, it only triggers on other string literals. The separate `Y021/PYI021` rule exists to disallow docstrings. ## Test Plan Added some `# OK` test cases to `PYI053.py(i)` files. --- crates/ruff/resources/test/fixtures/flake8_pyi/PYI053.py | 8 ++++++++ crates/ruff/resources/test/fixtures/flake8_pyi/PYI053.pyi | 6 ++++++ .../rules/flake8_pyi/rules/string_or_bytes_too_long.rs | 6 ++++++ ...ruff__rules__flake8_pyi__tests__PYI053_PYI053.pyi.snap | 5 +++++ 4 files changed, 25 insertions(+) diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI053.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI053.py index 15631d15f2..8b2811eb63 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI053.py +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI053.py @@ -36,3 +36,11 @@ bar: str = "51 character stringgggggggggggggggggggggggggggggggg" baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" + + +class Demo: + """Docstrings are excluded from this rule. Some padding.""" + + +def func() -> None: + """Docstrings are excluded from this rule. Some padding.""" diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI053.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI053.pyi index d2f55531a2..71064d9bdb 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI053.pyi +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI053.pyi @@ -28,3 +28,9 @@ bar: str = "51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI05 baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053 + +class Demo: + """Docstrings are excluded from this rule. Some padding.""" # OK + +def func() -> None: + """Docstrings are excluded from this rule. Some padding.""" # OK diff --git a/crates/ruff/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs b/crates/ruff/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs index cecaed2685..ff5ab572e8 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs @@ -2,6 +2,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_docstring_stmt; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -41,6 +42,11 @@ impl AlwaysAutofixableViolation for StringOrBytesTooLong { /// PYI053 pub(crate) fn string_or_bytes_too_long(checker: &mut Checker, expr: &Expr) { + // Ignore docstrings. + if is_docstring_stmt(checker.semantic().stmt()) { + return; + } + let length = match expr { Expr::Constant(ast::ExprConstant { value: Constant::Str(s), diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI053_PYI053.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI053_PYI053.pyi.snap index 275d11d8b4..951ca3acb1 100644 --- a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI053_PYI053.pyi.snap +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI053_PYI053.pyi.snap @@ -89,6 +89,8 @@ PYI053.pyi:30:14: PYI053 [*] String and bytes literals longer than 50 characters 29 | 30 | qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053 +31 | +32 | class Demo: | = help: Replace with `...` @@ -98,5 +100,8 @@ PYI053.pyi:30:14: PYI053 [*] String and bytes literals longer than 50 characters 29 29 | 30 |-qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053 30 |+qux: bytes = ... # Error: PYI053 +31 31 | +32 32 | class Demo: +33 33 | """Docstrings are excluded from this rule. Some padding.""" # OK From d19324df692aa8a7951e3da709a7945e79396d90 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Wed, 28 Jun 2023 05:57:24 +0530 Subject: [PATCH 255/447] Add Jupyter integration to the docs (#5403) ## Summary Add Jupyter integration to the docs, specifically the Configuration and FAQ sections. ## Test Plan `mkdocs serve` and check that the new sections are visible and functional. fixes: #5396 --- docs/configuration.md | 18 ++++++++++++++++++ docs/faq.md | 18 +++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 20ee6ba11a..3c95d5ca87 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -328,6 +328,24 @@ By default, Ruff will also skip any files that are omitted via `.ignore`, `.giti Files that are passed to `ruff` directly are always linted, regardless of the above criteria. For example, `ruff check /path/to/excluded/file.py` will always lint `file.py`. +## Jupyter Notebook discovery + +Ruff has built-in experimental support for linting [Jupyter Notebooks](https://jupyter.org/). + +To opt in to linting Jupyter Notebook (`.ipynb`) files, add the `*.ipynb` pattern to your +[`include`](settings.md#include) setting, like so: + +```toml +[tool.ruff] +include = ["*.py", "*.pyi", "**/pyproject.toml", "*.ipynb"] +``` + +This will prompt Ruff to discover Jupyter Notebook (`.ipynb`) files in any specified +directories, and lint them accordingly. + +Alternatively, pass the notebook file(s) to `ruff` on the command-line directly. For example, +`ruff check /path/to/notebook.ipynb` will always lint `notebook.ipynb`. + ## Rule selection The set of enabled rules is controlled via the [`select`](settings.md#select) and diff --git a/docs/faq.md b/docs/faq.md index e0fe13380a..8d6e7b3652 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -309,7 +309,23 @@ src = ["../src", "../test"] ## Does Ruff support Jupyter Notebooks? -Ruff is integrated into [nbQA](https://github.com/nbQA-dev/nbQA), a tool for running linters and +Ruff has built-in experimental support for linting [Jupyter Notebooks](https://jupyter.org/). + +To opt in to linting Jupyter Notebook (`.ipynb`) files, add the `*.ipynb` pattern to your +[`include`](settings.md#include) setting, like so: + +```toml +[tool.ruff] +include = ["*.py", "*.pyi", "**/pyproject.toml", "*.ipynb"] +``` + +This will prompt Ruff to discover Jupyter Notebook (`.ipynb`) files in any specified +directories, and lint them accordingly. + +Alternatively, pass the notebook file(s) to `ruff` on the command-line directly. For example, +`ruff check /path/to/notebook.ipynb` will always lint `notebook.ipynb`. + +Ruff also integrates with [nbQA](https://github.com/nbQA-dev/nbQA), a tool for running linters and code formatters over Jupyter Notebooks. After installing `ruff` and `nbqa`, you can run Ruff over a notebook like so: From 2aecaf5060f0ee4cdc87b0ed5a03092c9b80accf Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Wed, 28 Jun 2023 08:54:51 +0530 Subject: [PATCH 256/447] Consider Jupyter index for code frames (`--show-source`) (#5402) ## Summary Consider Jupyter index for code frames (`--show-source`). This solves two problems as mentioned in the linked issue: > Omit any contents from adjoining cells If the Jupyter index is present, we'll use that to check if the surrounding lines belong to the same cell as the content line. If not, we'll skip that line until we either reach the one which does or we reach the content line. > code frame line number If the Jupyter index is present, we'll use that to get the actual start line in corresponding to the computed start index. ## Test Plan `cargo run --bin ruff -- check --no-cache --isolated --select=ALL --show-source /path/to/notebook.ipynb` fixes: #5395 --- crates/ruff/src/message/grouped.rs | 9 ++++- crates/ruff/src/message/text.rs | 61 +++++++++++++++++++++++++----- 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/crates/ruff/src/message/grouped.rs b/crates/ruff/src/message/grouped.rs index 56669f5c67..f6f2cd335c 100644 --- a/crates/ruff/src/message/grouped.rs +++ b/crates/ruff/src/message/grouped.rs @@ -149,7 +149,14 @@ impl Display for DisplayGroupedMessage<'_> { if self.show_source { use std::fmt::Write; let mut padded = PadAdapter::new(f); - writeln!(padded, "{}", MessageCodeFrame { message })?; + writeln!( + padded, + "{}", + MessageCodeFrame { + message, + jupyter_index: self.jupyter_index + } + )?; } Ok(()) diff --git a/crates/ruff/src/message/text.rs b/crates/ruff/src/message/text.rs index d82bb47770..89244f4685 100644 --- a/crates/ruff/src/message/text.rs +++ b/crates/ruff/src/message/text.rs @@ -11,7 +11,7 @@ use ruff_text_size::{TextRange, TextSize}; use ruff_python_ast::source_code::{OneIndexed, SourceLocation}; use crate::fs::relativize_path; -use crate::jupyter::Notebook; +use crate::jupyter::{JupyterIndex, Notebook}; use crate::line_width::{LineWidth, TabSize}; use crate::message::diff::Diff; use crate::message::{Emitter, EmitterContext, Message}; @@ -72,13 +72,13 @@ impl Emitter for TextEmitter { )?; let start_location = message.compute_start_location(); - - // Check if we're working on a jupyter notebook and translate positions with cell accordingly - let diagnostic_location = if let Some(jupyter_index) = context + let jupyter_index = context .source_kind(message.filename()) .and_then(SourceKind::notebook) - .map(Notebook::index) - { + .map(Notebook::index); + + // Check if we're working on a jupyter notebook and translate positions with cell accordingly + let diagnostic_location = if let Some(jupyter_index) = jupyter_index { write!( writer, "cell {cell}{sep}", @@ -114,7 +114,14 @@ impl Emitter for TextEmitter { )?; if self.flags.contains(EmitterFlags::SHOW_SOURCE) { - writeln!(writer, "{}", MessageCodeFrame { message })?; + writeln!( + writer, + "{}", + MessageCodeFrame { + message, + jupyter_index + } + )?; } if self.flags.contains(EmitterFlags::SHOW_FIX_DIFF) { @@ -158,6 +165,7 @@ impl Display for RuleCodeAndBody<'_> { pub(super) struct MessageCodeFrame<'a> { pub(crate) message: &'a Message, + pub(crate) jupyter_index: Option<&'a JupyterIndex>, } impl Display for MessageCodeFrame<'_> { @@ -182,6 +190,20 @@ impl Display for MessageCodeFrame<'_> { let content_start_index = source_code.line_index(range.start()); let mut start_index = content_start_index.saturating_sub(2); + // If we're working on a jupyter notebook, skip the lines which are + // outside of the cell containing the diagnostic. + if let Some(jupyter_index) = self.jupyter_index { + let content_start_cell = jupyter_index + .cell(content_start_index.get()) + .unwrap_or_default(); + while start_index < content_start_index { + if jupyter_index.cell(start_index.get()).unwrap_or_default() == content_start_cell { + break; + } + start_index = start_index.saturating_add(1); + } + } + // Trim leading empty lines. while start_index < content_start_index { if !source_code.line_text(start_index).trim().is_empty() { @@ -195,7 +217,21 @@ impl Display for MessageCodeFrame<'_> { .saturating_add(2) .min(OneIndexed::from_zero_indexed(source_code.line_count())); - // Trim trailing empty lines + // If we're working on a jupyter notebook, skip the lines which are + // outside of the cell containing the diagnostic. + if let Some(jupyter_index) = self.jupyter_index { + let content_end_cell = jupyter_index + .cell(content_end_index.get()) + .unwrap_or_default(); + while end_index < content_end_index { + if jupyter_index.cell(end_index.get()).unwrap_or_default() == content_end_cell { + break; + } + end_index = end_index.saturating_sub(1); + } + } + + // Trim trailing empty lines. while end_index > content_end_index { if !source_code.line_text(end_index).trim().is_empty() { break; @@ -224,7 +260,14 @@ impl Display for MessageCodeFrame<'_> { title: None, slices: vec![Slice { source: &source.text, - line_start: start_index.get(), + line_start: self.jupyter_index.map_or_else( + || start_index.get(), + |jupyter_index| { + jupyter_index + .cell_row(start_index.get()) + .unwrap_or_default() as usize + }, + ), annotations: vec![SourceAnnotation { label: &label, annotation_type: AnnotationType::Error, From 366edc5a3f2fe4cf149c2f72106635b503669b58 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 27 Jun 2023 23:29:56 -0400 Subject: [PATCH 257/447] Fix string annotation in docs (#5411) --- crates/ruff/src/rules/pylint/rules/single_string_slots.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff/src/rules/pylint/rules/single_string_slots.rs b/crates/ruff/src/rules/pylint/rules/single_string_slots.rs index 964e5512bd..54ca42e694 100644 --- a/crates/ruff/src/rules/pylint/rules/single_string_slots.rs +++ b/crates/ruff/src/rules/pylint/rules/single_string_slots.rs @@ -32,7 +32,7 @@ use crate::checkers::ast::Checker; /// class Person: /// __slots__: str = "name" /// -/// def __init__(self, name: string) -> None: +/// def __init__(self, name: str) -> None: /// self.name = name /// ``` /// @@ -41,7 +41,7 @@ use crate::checkers::ast::Checker; /// class Person: /// __slots__: tuple[str, ...] = ("name",) /// -/// def __init__(self, name: string) -> None: +/// def __init__(self, name: str) -> None: /// self.name = name /// ``` /// From 9e2fd0c6206b0c61beec822194e9f28949cc4bc4 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 28 Jun 2023 11:48:52 +0200 Subject: [PATCH 258/447] ruff rule SLOT uses URL to current Python docs (#5412) ## Summary Currently the URL at the bottom of the `ruff rule SLOT00x` output points to Python 3.7 docs. Given that Python 3.7 is now end-of-life (as of yesterday), let's instead point users to the current Python docs. ## Test Plan --- .../rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs | 2 +- .../src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs | 2 +- .../src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs index 913d5b0a1e..de20c1e926 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs @@ -46,7 +46,7 @@ use crate::rules::flake8_slots::rules::helpers::has_slots; /// ``` /// /// ## References -/// - [Python documentation: `__slots__`](https://docs.python.org/3.7/reference/datamodel.html#slots) +/// - [Python documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots) #[violation] pub struct NoSlotsInNamedtupleSubclass; diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs index 8d5aa7f886..2df5535188 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs @@ -37,7 +37,7 @@ use crate::rules::flake8_slots::rules::helpers::has_slots; /// ``` /// /// ## References -/// - [Python documentation: `__slots__`](https://docs.python.org/3.7/reference/datamodel.html#slots) +/// - [Python documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots) #[violation] pub struct NoSlotsInStrSubclass; diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs index bb0cc7e376..98ce10bb56 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs @@ -38,7 +38,7 @@ use crate::rules::flake8_slots::rules::helpers::has_slots; /// ``` /// /// ## References -/// - [Python documentation: `__slots__`](https://docs.python.org/3.7/reference/datamodel.html#slots) +/// - [Python documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots) #[violation] pub struct NoSlotsInTupleSubclass; From 1979103ec014e1fb65dd32bfa6b2e17c4ff98234 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Wed, 28 Jun 2023 11:02:15 +0100 Subject: [PATCH 259/447] Format `StmtTry` (#5222) Co-authored-by: Micha Reiser --- .../test/fixtures/ruff/statement/try.py | 72 ++++++++ .../src/comments/placement.rs | 105 +++++++---- .../other/except_handler_except_handler.rs | 45 ++++- .../src/statement/stmt_try.rs | 103 ++++++++++- .../black_compatibility@comments.py.snap | 20 ++- .../black_compatibility@comments5.py.snap | 21 +-- .../black_compatibility@fmtskip8.py.snap | 20 ++- .../black_compatibility@function2.py.snap | 30 ++-- ...compatibility@remove_except_parens.py.snap | 97 ++++++---- .../black_compatibility@remove_parens.py.snap | 26 +-- .../snapshots/format@statement__try.py.snap | 167 ++++++++++++++++++ 11 files changed, 583 insertions(+), 123 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@statement__try.py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py new file mode 100644 index 0000000000..f50dcab4c4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py @@ -0,0 +1,72 @@ +try: + ... +except: + ... + +try: + ... +except (KeyError): # should remove brackets and be a single line + ... + + +try: # try + ... + # end of body +# before except +except (Exception, ValueError) as exc: # except line + ... +# before except 2 +except KeyError as key: # except line 2 + ... + # in body 2 +# before else +else: + ... +# before finally +finally: + ... + + + +# with line breaks +try: # try + ... + # end of body + +# before except +except (Exception, ValueError) as exc: # except line + ... + +# before except 2 +except KeyError as key: # except line 2 + ... + # in body 2 + +# before else +else: + ... + +# before finally +finally: + ... + + +# with line breaks +try: + ... + +except: + ... + + +try: + ... +except (Exception, Exception, Exception, Exception, Exception, Exception, Exception) as exc: # splits exception over multiple lines + ... + + +try: + ... +except: + a = 10 # trailing comment1 + b = 11 # trailing comment2 diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 2f72f863d8..9e31caddbb 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -29,6 +29,7 @@ pub(super) fn place_comment<'a>( handle_trailing_body_comment, handle_trailing_end_of_line_body_comment, handle_trailing_end_of_line_condition_comment, + handle_trailing_end_of_line_except_comment, handle_module_level_own_line_comment_before_class_or_function_comment, handle_arguments_separator_comment, handle_trailing_binary_expression_left_or_operator_comment, @@ -158,44 +159,52 @@ fn handle_in_between_except_handlers_or_except_handler_and_else_or_finally_comme comment: DecoratedComment<'a>, locator: &Locator, ) -> CommentPlacement<'a> { - if comment.line_position().is_end_of_line() || comment.following_node().is_none() { + if comment.line_position().is_end_of_line() { return CommentPlacement::Default(comment); } - if let Some(AnyNodeRef::ExceptHandlerExceptHandler(except_handler)) = comment.preceding_node() { - // it now depends on the indentation level of the comment if it is a leading comment for e.g. - // the following `elif` or indeed a trailing comment of the previous body's last statement. - let comment_indentation = - whitespace::indentation_at_offset(locator, comment.slice().range().start()) - .map(str::len) - .unwrap_or_default(); + let (Some(AnyNodeRef::ExceptHandlerExceptHandler(preceding_except_handler)), Some(following)) = (comment.preceding_node(), comment.following_node()) else { + return CommentPlacement::Default(comment); + }; - if let Some(except_indentation) = - whitespace::indentation(locator, except_handler).map(str::len) + // it now depends on the indentation level of the comment if it is a leading comment for e.g. + // the following `finally` or indeed a trailing comment of the previous body's last statement. + let comment_indentation = + whitespace::indentation_at_offset(locator, comment.slice().range().start()) + .map(str::len) + .unwrap_or_default(); + + let Some(except_indentation) = + whitespace::indentation(locator, preceding_except_handler).map(str::len) else { - return if comment_indentation <= except_indentation { - // It has equal, or less indent than the `except` handler. It must be a comment - // of the following `finally` or `else` block - // - // ```python - // try: - // pass - // except Exception: - // print("noop") - // # leading - // finally: - // pass - // ``` - // Attach it to the `try` statement. - CommentPlacement::dangling(comment.enclosing_node(), comment) - } else { - // Delegate to `handle_trailing_body_comment` - CommentPlacement::Default(comment) - }; - } + return CommentPlacement::Default(comment); + }; + + if comment_indentation > except_indentation { + // Delegate to `handle_trailing_body_comment` + return CommentPlacement::Default(comment); } - CommentPlacement::Default(comment) + // It has equal, or less indent than the `except` handler. It must be a comment of a subsequent + // except handler or of the following `finally` or `else` block + // + // ```python + // try: + // pass + // except Exception: + // print("noop") + // # leading + // finally: + // pass + // ``` + + if following.is_except_handler() { + // Attach it to the following except handler (which has a node) as leading + CommentPlacement::leading(following, comment) + } else { + // No following except handler; attach it to the `try` statement.as dangling + CommentPlacement::dangling(comment.enclosing_node(), comment) + } } /// Handles own line comments between the last statement and the first statement of two bodies. @@ -668,6 +677,40 @@ fn handle_trailing_end_of_line_condition_comment<'a>( CommentPlacement::Default(comment) } +/// Handles end of line comments after the `:` of an except clause +/// +/// ```python +/// try: +/// ... +/// except: # comment +/// pass +/// ``` +/// +/// It attaches the comment as dangling comment to the enclosing except handler. +fn handle_trailing_end_of_line_except_comment<'a>( + comment: DecoratedComment<'a>, + _locator: &Locator, +) -> CommentPlacement<'a> { + let AnyNodeRef::ExceptHandlerExceptHandler(handler) = comment.enclosing_node() else { + return CommentPlacement::Default(comment); + }; + + // Must be an end of line comment + if comment.line_position().is_own_line() { + return CommentPlacement::Default(comment); + } + + let Some(first_body_statement) = handler.body.first() else { + return CommentPlacement::Default(comment); + }; + + if comment.slice().start() < first_body_statement.range().start() { + CommentPlacement::dangling(comment.enclosing_node(), comment) + } else { + CommentPlacement::Default(comment) + } +} + /// Attaches comments for the positional only arguments separator `/` or the keywords only arguments /// separator `*` as dangling comments to the enclosing [`Arguments`] node. /// diff --git a/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs b/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs index 338b686bc7..2e1c695217 100644 --- a/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs +++ b/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs @@ -1,5 +1,9 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; +use crate::comments::trailing_comments; +use crate::expression::parentheses::Parenthesize; +use crate::prelude::*; +use crate::{FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AstNode; use rustpython_parser::ast::ExceptHandlerExceptHandler; #[derive(Default)] @@ -11,6 +15,43 @@ impl FormatNodeRule for FormatExceptHandlerExceptHan item: &ExceptHandlerExceptHandler, f: &mut PyFormatter, ) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let ExceptHandlerExceptHandler { + range: _, + type_, + name, + body, + } = item; + + let comments_info = f.context().comments().clone(); + let dangling_comments = comments_info.dangling_comments(item.as_any_node_ref()); + + write!(f, [text("except")])?; + + if let Some(type_) = type_ { + write!( + f, + [space(), type_.format().with_options(Parenthesize::IfBreaks)] + )?; + if let Some(name) = name { + write!(f, [space(), text("as"), space(), name.format()])?; + } + } + write!( + f, + [ + text(":"), + trailing_comments(dangling_comments), + block_indent(&body.format()) + ] + ) + } + + fn fmt_dangling_comments( + &self, + _node: &ExceptHandlerExceptHandler, + _f: &mut PyFormatter, + ) -> FormatResult<()> { + // dangling comments are formatted as part of fmt_fields + Ok(()) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_try.rs b/crates/ruff_python_formatter/src/statement/stmt_try.rs index faab829230..701fb8165b 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_try.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_try.rs @@ -1,17 +1,112 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; +use crate::comments; +use crate::comments::leading_alternate_branch_comments; +use crate::comments::SourceComment; +use crate::prelude::*; +use crate::statement::FormatRefWithRule; +use crate::statement::Stmt; +use crate::{FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::StmtTry; +use ruff_python_ast::node::AstNode; +use rustpython_parser::ast::{ExceptHandler, Ranged, StmtTry, Suite}; #[derive(Default)] pub struct FormatStmtTry; +#[derive(Copy, Clone, Default)] +pub struct FormatExceptHandler; + +impl FormatRule> for FormatExceptHandler { + fn fmt( + &self, + item: &ExceptHandler, + f: &mut Formatter>, + ) -> FormatResult<()> { + match item { + ExceptHandler::ExceptHandler(x) => x.format().fmt(f), + } + } +} + +impl<'ast> AsFormat> for ExceptHandler { + type Format<'a> = FormatRefWithRule< + 'a, + ExceptHandler, + FormatExceptHandler, + PyFormatContext<'ast>, + > where Self: 'a; + + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new(self, FormatExceptHandler::default()) + } +} + impl FormatNodeRule for FormatStmtTry { fn fmt_fields(&self, item: &StmtTry, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let StmtTry { + range: _, + body, + handlers, + orelse, + finalbody, + } = item; + + let comments_info = f.context().comments().clone(); + let mut dangling_comments = comments_info.dangling_comments(item.as_any_node_ref()); + + write!(f, [text("try:"), block_indent(&body.format())])?; + + let mut previous_node = body.last(); + + for handler in handlers { + let handler_comments = comments_info.leading_comments(handler); + write!( + f, + [ + leading_alternate_branch_comments(handler_comments, previous_node), + &handler.format() + ] + )?; + previous_node = match handler { + ExceptHandler::ExceptHandler(handler) => handler.body.last(), + }; + } + + (previous_node, dangling_comments) = + format_case("else", orelse, previous_node, dangling_comments, f)?; + + format_case("finally", finalbody, previous_node, dangling_comments, f)?; + + write!(f, [comments::dangling_comments(dangling_comments)]) } fn fmt_dangling_comments(&self, _node: &StmtTry, _f: &mut PyFormatter) -> FormatResult<()> { - // TODO(konstin): Needs node formatting or this leads to unstable formatting + // dangling comments are formatted as part of fmt_fields Ok(()) } } + +fn format_case<'a>( + name: &'static str, + body: &Suite, + previous_node: Option<&Stmt>, + dangling_comments: &'a [SourceComment], + f: &mut PyFormatter, +) -> FormatResult<(Option<&'a Stmt>, &'a [SourceComment])> { + Ok(if let Some(last) = body.last() { + let case_comments_start = + dangling_comments.partition_point(|comment| comment.slice().end() <= last.end()); + let (case_comments, rest) = dangling_comments.split_at(case_comments_start); + write!( + f, + [leading_alternate_branch_comments( + case_comments, + previous_node + )] + )?; + + write!(f, [text(name), text(":"), block_indent(&body.format())])?; + (None, rest) + } else { + (None, dangling_comments) + }) +} diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap index 30b27344f0..ebd4a1b917 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap @@ -108,7 +108,7 @@ async def wat(): ```diff --- Black +++ Ruff -@@ -9,16 +9,13 @@ +@@ -9,16 +9,16 @@ Possibly also many, many lines. """ @@ -122,15 +122,16 @@ async def wat(): +NOT_YET_IMPLEMENTED_StmtImport +NOT_YET_IMPLEMENTED_StmtImportFrom # some noqa comment --try: + try: - import fast --except ImportError: ++ NOT_YET_IMPLEMENTED_StmtImport + except ImportError: - import slow as fast -+NOT_YET_IMPLEMENTED_StmtTry ++ NOT_YET_IMPLEMENTED_StmtImport # Some comment before a function. -@@ -35,7 +32,7 @@ +@@ -35,7 +35,7 @@ Possibly many lines. """ # FIXME: Some comment about why this function is crap but still in production. @@ -139,7 +140,7 @@ async def wat(): if inner_imports.are_evil(): # Explains why we have this if. -@@ -82,8 +79,7 @@ +@@ -82,8 +82,7 @@ async def wat(): # This comment, for some reason \ # contains a trailing backslash. @@ -149,7 +150,7 @@ async def wat(): # Comment after ending a block. if result: print("A OK", file=sys.stdout) -@@ -93,4 +89,4 @@ +@@ -93,4 +92,4 @@ # Some closing comments. # Maybe Vim or Emacs directives for formatting. @@ -178,7 +179,10 @@ NOT_YET_IMPLEMENTED_StmtImport NOT_YET_IMPLEMENTED_StmtImport NOT_YET_IMPLEMENTED_StmtImportFrom # some noqa comment -NOT_YET_IMPLEMENTED_StmtTry +try: + NOT_YET_IMPLEMENTED_StmtImport +except ImportError: + NOT_YET_IMPLEMENTED_StmtImport # Some comment before a function. diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments5.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments5.py.snap index 2a8d41d0ae..28f4426951 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments5.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments5.py.snap @@ -85,17 +85,9 @@ if __name__ == "__main__": ```diff --- Black +++ Ruff -@@ -20,14 +20,9 @@ - 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_StmtTry +@@ -27,7 +27,7 @@ + except OSError: + print("problems") -import sys +NOT_YET_IMPLEMENTED_StmtImport @@ -129,7 +121,12 @@ for i in range(100): with open(some_temp_file) as f: data = f.read() -NOT_YET_IMPLEMENTED_StmtTry +try: + with open(some_other_file) as w: + w.write(data) + +except OSError: + print("problems") NOT_YET_IMPLEMENTED_StmtImport diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap index 133a41b6cf..084370e171 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap @@ -74,7 +74,7 @@ async def test_async_with(): ```diff --- Black +++ Ruff -@@ -1,62 +1,55 @@ +@@ -1,62 +1,61 @@ # Make sure a leading comment is not removed. -def some_func( unformatted, args ): # fmt: skip +def some_func(unformatted, args): # fmt: skip @@ -134,12 +134,16 @@ async def test_async_with(): -try : # fmt: skip -- some_call() ++try: ++ # fmt: skip + some_call() -except UnformattedError as ex: # fmt: skip - handle_exception() -finally : # fmt: skip -- finally_call() -+NOT_YET_IMPLEMENTED_StmtTry ++except UnformattedError as ex: # fmt: skip ++ handle_exception() # fmt: skip ++finally: + finally_call() -with give_me_context( unformatted, args ): # fmt: skip @@ -202,7 +206,13 @@ async def test_async_for(): NOT_YET_IMPLEMENTED_StmtAsyncFor # fmt: skip -NOT_YET_IMPLEMENTED_StmtTry +try: + # fmt: skip + some_call() +except UnformattedError as ex: # fmt: skip + handle_exception() # fmt: skip +finally: + finally_call() with give_me_context(unformatted, args): # fmt: skip diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function2.py.snap index bcd7954313..3fd47e2167 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function2.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function2.py.snap @@ -65,7 +65,7 @@ with hmm_but_this_should_get_two_preceding_newlines(): ```diff --- Black +++ Ruff -@@ -32,34 +32,20 @@ +@@ -32,34 +32,28 @@ if os.name == "posix": @@ -76,18 +76,18 @@ with hmm_but_this_should_get_two_preceding_newlines(): pass - elif os.name == "nt": -- try: + try: - import msvcrt -- -- def i_should_be_followed_by_only_one_newline(): -- pass -+ NOT_YET_IMPLEMENTED_StmtTry ++ NOT_YET_IMPLEMENTED_StmtImport -- except ImportError: -- -- def i_should_be_followed_by_only_one_newline(): -- pass + def i_should_be_followed_by_only_one_newline(): + pass + + except ImportError: - + def i_should_be_followed_by_only_one_newline(): + pass + elif False: - class IHopeYouAreHavingALovelyDay: @@ -146,7 +146,15 @@ if os.name == "posix": def i_should_be_followed_by_only_one_newline(): pass elif os.name == "nt": - NOT_YET_IMPLEMENTED_StmtTry + try: + NOT_YET_IMPLEMENTED_StmtImport + + def i_should_be_followed_by_only_one_newline(): + pass + + except ImportError: + def i_should_be_followed_by_only_one_newline(): + pass elif False: class IHopeYouAreHavingALovelyDay: diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_except_parens.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_except_parens.py.snap index a845bf4af2..5990c185cd 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_except_parens.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_except_parens.py.snap @@ -47,77 +47,100 @@ except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.ov ```diff --- Black +++ Ruff -@@ -1,42 +1,17 @@ - # These brackets are redundant, therefore remove. --try: -- a.something --except AttributeError as err: +@@ -2,7 +2,7 @@ + try: + a.something + except AttributeError as err: - raise err -+NOT_YET_IMPLEMENTED_StmtTry ++ NOT_YET_IMPLEMENTED_StmtRaise # This is tuple of exceptions. # Although this could be replaced with just the exception, - # we do not remove brackets to preserve AST. --try: -- a.something --except (AttributeError,) as err: +@@ -10,28 +10,26 @@ + try: + a.something + except (AttributeError,) as err: - raise err -+NOT_YET_IMPLEMENTED_StmtTry ++ NOT_YET_IMPLEMENTED_StmtRaise # This is a tuple of exceptions. Do not remove brackets. --try: -- a.something --except (AttributeError, ValueError) as err: + try: + a.something + except (AttributeError, ValueError) as err: - raise err -+NOT_YET_IMPLEMENTED_StmtTry ++ NOT_YET_IMPLEMENTED_StmtRaise # Test long variants. --try: -- a.something + try: + a.something -except ( - some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error -) as err: - raise err -+NOT_YET_IMPLEMENTED_StmtTry ++except some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error as err: ++ NOT_YET_IMPLEMENTED_StmtRaise --try: -- a.something --except ( -- some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, --) as err: + try: + a.something + except ( + some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, + ) as err: - raise err -+NOT_YET_IMPLEMENTED_StmtTry ++ NOT_YET_IMPLEMENTED_StmtRaise --try: -- a.something --except ( -- some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, -- some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, --) as err: + try: + a.something +@@ -39,4 +37,4 @@ + some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, + some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, + ) as err: - raise err -+NOT_YET_IMPLEMENTED_StmtTry ++ NOT_YET_IMPLEMENTED_StmtRaise ``` ## Ruff Output ```py # These brackets are redundant, therefore remove. -NOT_YET_IMPLEMENTED_StmtTry +try: + a.something +except AttributeError as err: + NOT_YET_IMPLEMENTED_StmtRaise # This is tuple of exceptions. # Although this could be replaced with just the exception, # we do not remove brackets to preserve AST. -NOT_YET_IMPLEMENTED_StmtTry +try: + a.something +except (AttributeError,) as err: + NOT_YET_IMPLEMENTED_StmtRaise # This is a tuple of exceptions. Do not remove brackets. -NOT_YET_IMPLEMENTED_StmtTry +try: + a.something +except (AttributeError, ValueError) as err: + NOT_YET_IMPLEMENTED_StmtRaise # Test long variants. -NOT_YET_IMPLEMENTED_StmtTry +try: + a.something +except some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error as err: + NOT_YET_IMPLEMENTED_StmtRaise -NOT_YET_IMPLEMENTED_StmtTry +try: + a.something +except ( + some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, +) as err: + NOT_YET_IMPLEMENTED_StmtRaise -NOT_YET_IMPLEMENTED_StmtTry +try: + a.something +except ( + some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, + some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, +) as err: + NOT_YET_IMPLEMENTED_StmtRaise ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_parens.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_parens.py.snap index bb59ae6651..2ba7a34a5a 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_parens.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_parens.py.snap @@ -67,22 +67,18 @@ def example8(): ```diff --- Black +++ Ruff -@@ -8,13 +8,7 @@ - - async def show_status(): +@@ -10,9 +10,7 @@ while True: -- try: -- if report_host: + try: + if report_host: - data = ( - f"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - ).encode() -- except Exception as e: -- pass -+ NOT_YET_IMPLEMENTED_StmtTry ++ data = (NOT_YET_IMPLEMENTED_ExprJoinedStr).encode() + except Exception as e: + pass - - def example(): -@@ -30,15 +24,11 @@ +@@ -30,15 +28,11 @@ def example2(): @@ -100,7 +96,7 @@ def example8(): def example4(): -@@ -50,35 +40,11 @@ +@@ -50,35 +44,11 @@ def example6(): @@ -153,7 +149,11 @@ data = ( async def show_status(): while True: - NOT_YET_IMPLEMENTED_StmtTry + try: + if report_host: + data = (NOT_YET_IMPLEMENTED_ExprJoinedStr).encode() + except Exception as e: + pass def example(): diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__try.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__try.py.snap new file mode 100644 index 0000000000..e0a6fa67b0 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__try.py.snap @@ -0,0 +1,167 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py +--- +## Input +```py +try: + ... +except: + ... + +try: + ... +except (KeyError): # should remove brackets and be a single line + ... + + +try: # try + ... + # end of body +# before except +except (Exception, ValueError) as exc: # except line + ... +# before except 2 +except KeyError as key: # except line 2 + ... + # in body 2 +# before else +else: + ... +# before finally +finally: + ... + + + +# with line breaks +try: # try + ... + # end of body + +# before except +except (Exception, ValueError) as exc: # except line + ... + +# before except 2 +except KeyError as key: # except line 2 + ... + # in body 2 + +# before else +else: + ... + +# before finally +finally: + ... + + +# with line breaks +try: + ... + +except: + ... + + +try: + ... +except (Exception, Exception, Exception, Exception, Exception, Exception, Exception) as exc: # splits exception over multiple lines + ... + + +try: + ... +except: + a = 10 # trailing comment1 + b = 11 # trailing comment2 +``` + +## Output +```py +try: + ... +except: + ... + +try: + ... +except KeyError: # should remove brackets and be a single line + ... + + +try: + # try + ... + # end of body +# before except +except (Exception, ValueError) as exc: # except line + ... +# before except 2 +except KeyError as key: # except line 2 + ... + # in body 2 +# before else +else: + ... +# before finally +finally: + ... + + +# with line breaks +try: + # try + ... + # end of body + +# before except +except (Exception, ValueError) as exc: # except line + ... + +# before except 2 +except KeyError as key: # except line 2 + ... + # in body 2 + +# before else +else: + ... + +# before finally +finally: + ... + + +# with line breaks +try: + ... + +except: + ... + + +try: + ... +except ( + Exception, + Exception, + Exception, + Exception, + Exception, + Exception, + Exception, +) as exc: # splits exception over multiple lines + ... + + +try: + ... +except: + a = 10 # trailing comment1 + b = 11 # trailing comment2 +``` + + + From c7adb9117f09ab63d44b628c97afc0072d3e48c9 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Wed, 28 Jun 2023 11:21:44 +0100 Subject: [PATCH 260/447] format StmtAsyncWith (#5376) Co-authored-by: Micha Reiser --- .../src/statement/stmt_async_with.rs | 16 +++- .../src/statement/stmt_with.rs | 93 +++++++++++++++---- .../black_compatibility@comments.py.snap | 15 +-- .../black_compatibility@fmtonoff.py.snap | 20 ++-- .../black_compatibility@fmtskip8.py.snap | 9 +- .../black_compatibility@function.py.snap | 43 ++++----- 6 files changed, 125 insertions(+), 71 deletions(-) diff --git a/crates/ruff_python_formatter/src/statement/stmt_async_with.rs b/crates/ruff_python_formatter/src/statement/stmt_async_with.rs index 0555642a10..e8d0e0ebd9 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_async_with.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_async_with.rs @@ -1,5 +1,6 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::prelude::*; +use crate::statement::stmt_with::AnyStatementWith; +use crate::FormatNodeRule; use rustpython_parser::ast::StmtAsyncWith; #[derive(Default)] @@ -7,6 +8,15 @@ pub struct FormatStmtAsyncWith; impl FormatNodeRule for FormatStmtAsyncWith { fn fmt_fields(&self, item: &StmtAsyncWith, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + AnyStatementWith::from(item).fmt(f) + } + + fn fmt_dangling_comments( + &self, + _node: &StmtAsyncWith, + _f: &mut PyFormatter, + ) -> FormatResult<()> { + // Handled in `fmt_fields` + Ok(()) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_with.rs b/crates/ruff_python_formatter/src/statement/stmt_with.rs index 443337d934..c4adb3de4d 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_with.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_with.rs @@ -1,29 +1,79 @@ -use rustpython_parser::ast::StmtWith; - use ruff_formatter::{write, Buffer, FormatResult}; -use ruff_python_ast::node::AstNode; +use ruff_python_ast::node::AnyNodeRef; +use ruff_text_size::TextRange; +use rustpython_parser::ast::{Ranged, StmtAsyncWith, StmtWith, Suite, WithItem}; use crate::builders::optional_parentheses; use crate::comments::trailing_comments; use crate::prelude::*; -use crate::{FormatNodeRule, PyFormatter}; +use crate::FormatNodeRule; -#[derive(Default)] -pub struct FormatStmtWith; +pub(super) enum AnyStatementWith<'a> { + With(&'a StmtWith), + AsyncWith(&'a StmtAsyncWith), +} -impl FormatNodeRule for FormatStmtWith { - fn fmt_fields(&self, item: &StmtWith, f: &mut PyFormatter) -> FormatResult<()> { - let StmtWith { - range: _, - items, - body, - type_comment: _, - } = item; +impl<'a> AnyStatementWith<'a> { + const fn is_async(&self) -> bool { + matches!(self, AnyStatementWith::AsyncWith(_)) + } + fn items(&self) -> &[WithItem] { + match self { + AnyStatementWith::With(with) => with.items.as_slice(), + AnyStatementWith::AsyncWith(with) => with.items.as_slice(), + } + } + + fn body(&self) -> &Suite { + match self { + AnyStatementWith::With(with) => &with.body, + AnyStatementWith::AsyncWith(with) => &with.body, + } + } +} + +impl Ranged for AnyStatementWith<'_> { + fn range(&self) -> TextRange { + match self { + AnyStatementWith::With(with) => with.range(), + AnyStatementWith::AsyncWith(with) => with.range(), + } + } +} + +impl<'a> From<&'a StmtWith> for AnyStatementWith<'a> { + fn from(value: &'a StmtWith) -> Self { + AnyStatementWith::With(value) + } +} + +impl<'a> From<&'a StmtAsyncWith> for AnyStatementWith<'a> { + fn from(value: &'a StmtAsyncWith) -> Self { + AnyStatementWith::AsyncWith(value) + } +} + +impl<'a> From<&AnyStatementWith<'a>> for AnyNodeRef<'a> { + fn from(value: &AnyStatementWith<'a>) -> Self { + match value { + AnyStatementWith::With(with) => AnyNodeRef::StmtWith(with), + AnyStatementWith::AsyncWith(with) => AnyNodeRef::StmtAsyncWith(with), + } + } +} + +impl Format> for AnyStatementWith<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { let comments = f.context().comments().clone(); - let dangling_comments = comments.dangling_comments(item.as_any_node_ref()); + let dangling_comments = comments.dangling_comments(self); - let joined_items = format_with(|f| f.join_comma_separated().nodes(items.iter()).finish()); + let joined_items = + format_with(|f| f.join_comma_separated().nodes(self.items().iter()).finish()); + + if self.is_async() { + write!(f, [text("async"), space()])?; + } write!( f, @@ -33,10 +83,19 @@ impl FormatNodeRule for FormatStmtWith { group(&optional_parentheses(&joined_items)), text(":"), trailing_comments(dangling_comments), - block_indent(&body.format()) + block_indent(&self.body().format()) ] ) } +} + +#[derive(Default)] +pub struct FormatStmtWith; + +impl FormatNodeRule for FormatStmtWith { + fn fmt_fields(&self, item: &StmtWith, f: &mut PyFormatter) -> FormatResult<()> { + AnyStatementWith::from(item).fmt(f) + } fn fmt_dangling_comments(&self, _node: &StmtWith, _f: &mut PyFormatter) -> FormatResult<()> { // Handled in `fmt_fields` diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap index ebd4a1b917..f5c40ca6bc 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap @@ -140,17 +140,7 @@ async def wat(): if inner_imports.are_evil(): # Explains why we have this if. -@@ -82,8 +82,7 @@ - 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() -+ NOT_YET_IMPLEMENTED_StmtAsyncWith # Some more comments - # Comment after ending a block. - if result: - print("A OK", file=sys.stdout) -@@ -93,4 +92,4 @@ +@@ -93,4 +93,4 @@ # Some closing comments. # Maybe Vim or Emacs directives for formatting. @@ -246,7 +236,8 @@ class Foo: async def wat(): # This comment, for some reason \ # contains a trailing backslash. - NOT_YET_IMPLEMENTED_StmtAsyncWith # Some more comments + 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) diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap index e8c3771da6..460ce0e39e 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap @@ -221,7 +221,7 @@ d={'a':1, # Comment 1 # Comment 2 -@@ -18,30 +16,53 @@ +@@ -18,30 +16,54 @@ # fmt: off def func_no_args(): @@ -253,7 +253,8 @@ d={'a':1, - await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2) - await asyncio.sleep(1) + "Single-line docstring. Multiline is harder to reformat." -+ NOT_YET_IMPLEMENTED_StmtAsyncWith ++ async with some_connection() as conn: ++ await conn.do_what_i_mean("SELECT bobby, tables FROM xkcd", timeout=2) + await asyncio.sleep(1) + + @@ -296,7 +297,7 @@ d={'a':1, def spaces_types( -@@ -51,7 +72,7 @@ +@@ -51,7 +73,7 @@ d: dict = {}, e: bool = True, f: int = -1, @@ -305,7 +306,7 @@ d={'a':1, h: str = "", i: str = r"", ): -@@ -64,55 +85,55 @@ +@@ -64,55 +86,55 @@ something = { # fmt: off @@ -381,7 +382,7 @@ d={'a':1, # fmt: on -@@ -133,10 +154,10 @@ +@@ -133,10 +155,10 @@ """Another known limitation.""" # fmt: on # fmt: off @@ -396,7 +397,7 @@ d={'a':1, # fmt: on # fmt: off # ...but comments still get reformatted even though they should not be -@@ -151,12 +172,10 @@ +@@ -151,12 +173,10 @@ ast_args.kw_defaults, parameters, implicit_default=True, @@ -411,7 +412,7 @@ d={'a':1, # fmt: on _type_comment_re = re.compile( r""" -@@ -179,7 +198,8 @@ +@@ -179,7 +199,8 @@ $ """, # fmt: off @@ -421,7 +422,7 @@ d={'a':1, # fmt: on ) -@@ -217,8 +237,7 @@ +@@ -217,8 +238,7 @@ xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5, ) # fmt: off @@ -472,7 +473,8 @@ def func_no_args(): async def coroutine(arg, exec=False): "Single-line docstring. Multiline is harder to reformat." - NOT_YET_IMPLEMENTED_StmtAsyncWith + async with some_connection() as conn: + await conn.do_what_i_mean("SELECT bobby, tables FROM xkcd", timeout=2) await asyncio.sleep(1) diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap index 084370e171..4d65173c25 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap @@ -74,7 +74,7 @@ async def test_async_with(): ```diff --- Black +++ Ruff -@@ -1,62 +1,61 @@ +@@ -1,62 +1,62 @@ # Make sure a leading comment is not removed. -def some_func( unformatted, args ): # fmt: skip +def some_func(unformatted, args): # fmt: skip @@ -153,8 +153,8 @@ async def test_async_with(): async def test_async_with(): - async with give_me_async_context( unformatted, args ): # fmt: skip -- print("Do something") -+ NOT_YET_IMPLEMENTED_StmtAsyncWith # fmt: skip ++ async with give_me_async_context(unformatted, args): # fmt: skip + print("Do something") ``` ## Ruff Output @@ -220,7 +220,8 @@ with give_me_context(unformatted, args): # fmt: skip async def test_async_with(): - NOT_YET_IMPLEMENTED_StmtAsyncWith # fmt: skip + async with give_me_async_context(unformatted, args): # fmt: skip + print("Do something") ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function.py.snap index 7562820a71..98e3796d47 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function.py.snap @@ -107,25 +107,25 @@ def __await__(): return (yield) ```diff --- Black +++ Ruff -@@ -1,20 +1,19 @@ +@@ -1,12 +1,11 @@ #!/usr/bin/env python3 -import asyncio -import sys -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImport - +- -from third_party import X, Y, Z -+NOT_YET_IMPLEMENTED_StmtImportFrom ++NOT_YET_IMPLEMENTED_StmtImport ++NOT_YET_IMPLEMENTED_StmtImport -from library import some_connection, some_decorator +NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_ExprJoinedStr -f"trigger 3.6 mode" -- ++NOT_YET_IMPLEMENTED_StmtImportFrom ++NOT_YET_IMPLEMENTED_ExprJoinedStr + def func_no_args(): - a +@@ -14,7 +13,7 @@ b c if True: @@ -134,17 +134,7 @@ def __await__(): return (yield) if False: ... for i in range(10): -@@ -26,8 +25,7 @@ - - async def coroutine(arg, exec=False): - "Single-line docstring. Multiline is harder to reformat." -- async with some_connection() as conn: -- await conn.do_what_i_mean("SELECT bobby, tables FROM xkcd", timeout=2) -+ NOT_YET_IMPLEMENTED_StmtAsyncWith - await asyncio.sleep(1) - - -@@ -41,12 +39,22 @@ +@@ -41,12 +40,22 @@ debug: bool = False, **kwargs, ) -> str: @@ -171,7 +161,7 @@ def __await__(): return (yield) def spaces_types( -@@ -56,7 +64,7 @@ +@@ -56,7 +65,7 @@ d: dict = {}, e: bool = True, f: int = -1, @@ -180,7 +170,7 @@ def __await__(): return (yield) h: str = "", i: str = r"", ): -@@ -64,19 +72,16 @@ +@@ -64,19 +73,16 @@ def spaces2(result=_core.Value(None)): @@ -207,7 +197,7 @@ def __await__(): return (yield) def long_lines(): -@@ -87,7 +92,7 @@ +@@ -87,7 +93,7 @@ ast_args.kw_defaults, parameters, implicit_default=True, @@ -216,7 +206,7 @@ def __await__(): return (yield) ) typedargslist.extend( gen_annotated_params( -@@ -96,7 +101,7 @@ +@@ -96,7 +102,7 @@ parameters, implicit_default=True, # trailing standalone comment @@ -225,7 +215,7 @@ def __await__(): return (yield) ) _type_comment_re = re.compile( r""" -@@ -118,7 +123,8 @@ +@@ -118,7 +124,8 @@ ) $ """, @@ -235,7 +225,7 @@ def __await__(): return (yield) ) -@@ -135,14 +141,8 @@ +@@ -135,14 +142,8 @@ a, **kwargs, ) -> A: @@ -284,7 +274,8 @@ def func_no_args(): async def coroutine(arg, exec=False): "Single-line docstring. Multiline is harder to reformat." - NOT_YET_IMPLEMENTED_StmtAsyncWith + async with some_connection() as conn: + await conn.do_what_i_mean("SELECT bobby, tables FROM xkcd", timeout=2) await asyncio.sleep(1) From b42d76494c6bcdb3ba911dcd87249f5f6f1402aa Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 28 Jun 2023 12:24:13 +0200 Subject: [PATCH 261/447] types.rs: fnmatch url should point to current Python docs (#5413) Like #5412 --- crates/ruff/src/settings/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff/src/settings/types.rs b/crates/ruff/src/settings/types.rs index 5395ce9ec5..82dc6fe34c 100644 --- a/crates/ruff/src/settings/types.rs +++ b/crates/ruff/src/settings/types.rs @@ -269,6 +269,6 @@ impl Deref for Version { /// luckily this not relevant since identifiers don't contains slashes. /// /// For reference pep8-naming uses -/// [`fnmatch`](https://docs.python.org/3.11/library/fnmatch.html) for +/// [`fnmatch`](https://docs.python.org/3/library/fnmatch.html) for /// pattern matching. pub type IdentifierPattern = glob::Pattern; From a68a86e18b3ec9659de47ceea413af31e5372df3 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Wed, 28 Jun 2023 15:55:05 +0530 Subject: [PATCH 262/447] fixup! Consider Jupyter index for code frames (`--show-source`) (#5402) (#5414) --- crates/ruff/src/message/text.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff/src/message/text.rs b/crates/ruff/src/message/text.rs index 89244f4685..237b304d28 100644 --- a/crates/ruff/src/message/text.rs +++ b/crates/ruff/src/message/text.rs @@ -223,7 +223,7 @@ impl Display for MessageCodeFrame<'_> { let content_end_cell = jupyter_index .cell(content_end_index.get()) .unwrap_or_default(); - while end_index < content_end_index { + while end_index > content_end_index { if jupyter_index.cell(end_index.get()).unwrap_or_default() == content_end_cell { break; } From 6587fb844aa84eadf6e6d6e87167eb1f007a0391 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 28 Jun 2023 09:38:51 -0400 Subject: [PATCH 263/447] Add snapshot tests for resolver (#5404) ## Summary This PR adds some snapshot tests for the resolver based on executing resolutions within a "mock" of the Airflow repo (that is: a folder that contains a subset of the repo's files, but all empty, and with an only-partially-complete virtual environment). It's intended to act as a lightweight integration test, to enable us to test resolutions on a "real" project without adding a dependency on Airflow itself. --- .pre-commit-config.yaml | 4 +- Cargo.lock | 1 + crates/ruff_python_resolver/Cargo.toml | 1 + .../resources/test/airflow/README.md | 3 + .../test/airflow/airflow/__init__.py | 1 + .../test/airflow/airflow/api/__init__.py | 1 + .../airflow/airflow/api/common/__init__.py | 1 + .../airflow/airflow/api/common/mark_tasks.py | 14 ++ .../test/airflow/airflow/compat/__init__.py | 16 +++ .../test/airflow/airflow/compat/functools.py | 1 + .../test/airflow/airflow/compat/functools.pyi | 1 + .../test/airflow/airflow/jobs/__init__.py | 0 .../airflow/jobs/scheduler_job_runner.py | 1 + .../airflow/providers/google/__init__.py | 0 .../providers/google/cloud/__init__.py | 0 .../providers/google/cloud/hooks/__init__.py | 0 .../providers/google/cloud/hooks/gcs.py | 1 + .../site-packages/sqlalchemy/__init__.py | 1 + .../site-packages/sqlalchemy/orm/__init__.py | 0 .../site-packages/sqlalchemy/orm/base.py | 1 + .../sqlalchemy/orm/dependency.py | 1 + .../site-packages/sqlalchemy/orm/query.py | 1 + .../src/implicit_imports.rs | 12 +- .../ruff_python_resolver/src/import_result.rs | 12 +- crates/ruff_python_resolver/src/lib.rs | 2 - crates/ruff_python_resolver/src/resolver.rs | 122 +++++++++++++++++- crates/ruff_python_resolver/src/search.rs | 4 +- ..._resolver__tests__airflow_first_party.snap | 33 +++++ ...ver__tests__airflow_namespace_package.snap | 36 ++++++ ...lver__tests__airflow_standard_library.snap | 25 ++++ ...r__resolver__tests__airflow_stub_file.snap | 63 +++++++++ ..._resolver__tests__airflow_third_party.snap | 54 ++++++++ 32 files changed, 391 insertions(+), 22 deletions(-) create mode 100644 crates/ruff_python_resolver/resources/test/airflow/README.md create mode 100644 crates/ruff_python_resolver/resources/test/airflow/airflow/__init__.py create mode 100644 crates/ruff_python_resolver/resources/test/airflow/airflow/api/__init__.py create mode 100644 crates/ruff_python_resolver/resources/test/airflow/airflow/api/common/__init__.py create mode 100644 crates/ruff_python_resolver/resources/test/airflow/airflow/api/common/mark_tasks.py create mode 100644 crates/ruff_python_resolver/resources/test/airflow/airflow/compat/__init__.py create mode 100644 crates/ruff_python_resolver/resources/test/airflow/airflow/compat/functools.py create mode 100644 crates/ruff_python_resolver/resources/test/airflow/airflow/compat/functools.pyi create mode 100644 crates/ruff_python_resolver/resources/test/airflow/airflow/jobs/__init__.py create mode 100644 crates/ruff_python_resolver/resources/test/airflow/airflow/jobs/scheduler_job_runner.py create mode 100644 crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/__init__.py create mode 100644 crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/cloud/__init__.py create mode 100644 crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/cloud/hooks/__init__.py create mode 100644 crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/cloud/hooks/gcs.py create mode 100644 crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/__init__.py create mode 100644 crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/__init__.py create mode 100644 crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/base.py create mode 100644 crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/dependency.py create mode 100644 crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/query.py create mode 100644 crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_first_party.snap create mode 100644 crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_namespace_package.snap create mode 100644 crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_standard_library.snap create mode 100644 crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_stub_file.snap create mode 100644 crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_third_party.snap diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a33c8353c7..5209b8b84b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,9 @@ exclude: | crates/ruff/src/rules/.*/snapshots/.*| crates/ruff_cli/resources/.*| crates/ruff_python_formatter/resources/.*| - crates/ruff_python_formatter/tests/snapshots/.* + crates/ruff_python_formatter/tests/snapshots/.*| + crates/ruff_python_resolver/resources/.*| + crates/ruff_python_resolver/tests/snapshots/.* )$ repos: diff --git a/Cargo.lock b/Cargo.lock index 57c612d87e..f463b1601f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2095,6 +2095,7 @@ name = "ruff_python_resolver" version = "0.0.0" dependencies = [ "env_logger", + "insta", "log", "tempfile", ] diff --git a/crates/ruff_python_resolver/Cargo.toml b/crates/ruff_python_resolver/Cargo.toml index 2d454e88cb..2ec3942f6e 100644 --- a/crates/ruff_python_resolver/Cargo.toml +++ b/crates/ruff_python_resolver/Cargo.toml @@ -19,3 +19,4 @@ log = { workspace = true } [dev-dependencies] env_logger = "0.10.0" tempfile = "3.6.0" +insta = { workspace = true } diff --git a/crates/ruff_python_resolver/resources/test/airflow/README.md b/crates/ruff_python_resolver/resources/test/airflow/README.md new file mode 100644 index 0000000000..803b7c401b --- /dev/null +++ b/crates/ruff_python_resolver/resources/test/airflow/README.md @@ -0,0 +1,3 @@ +# airflow + +This is a mock subset of the Airflow repository, used to test module resolution. diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/crates/ruff_python_resolver/resources/test/airflow/airflow/__init__.py @@ -0,0 +1 @@ + diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/api/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/api/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/crates/ruff_python_resolver/resources/test/airflow/airflow/api/__init__.py @@ -0,0 +1 @@ + diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/api/common/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/api/common/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/crates/ruff_python_resolver/resources/test/airflow/airflow/api/common/__init__.py @@ -0,0 +1 @@ + diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/api/common/mark_tasks.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/api/common/mark_tasks.py new file mode 100644 index 0000000000..9b13b3153e --- /dev/null +++ b/crates/ruff_python_resolver/resources/test/airflow/airflow/api/common/mark_tasks.py @@ -0,0 +1,14 @@ +# Standard library. +import os + +# First-party. +from airflow.jobs.scheduler_job_runner import SchedulerJobRunner + +# Stub file. +from airflow.compat.functools import cached_property + +# Namespace package. +from airflow.providers.google.cloud.hooks.gcs import GCSHook + +# Third-party. +from sqlalchemy.orm import Query diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/compat/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/compat/__init__.py new file mode 100644 index 0000000000..13a83393a9 --- /dev/null +++ b/crates/ruff_python_resolver/resources/test/airflow/airflow/compat/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/compat/functools.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/compat/functools.py new file mode 100644 index 0000000000..75a6dead35 --- /dev/null +++ b/crates/ruff_python_resolver/resources/test/airflow/airflow/compat/functools.py @@ -0,0 +1 @@ +"""Empty file included to support filesystem-based resolver tests.""" diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/compat/functools.pyi b/crates/ruff_python_resolver/resources/test/airflow/airflow/compat/functools.pyi new file mode 100644 index 0000000000..75a6dead35 --- /dev/null +++ b/crates/ruff_python_resolver/resources/test/airflow/airflow/compat/functools.pyi @@ -0,0 +1 @@ +"""Empty file included to support filesystem-based resolver tests.""" diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/jobs/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/jobs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/jobs/scheduler_job_runner.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/jobs/scheduler_job_runner.py new file mode 100644 index 0000000000..75a6dead35 --- /dev/null +++ b/crates/ruff_python_resolver/resources/test/airflow/airflow/jobs/scheduler_job_runner.py @@ -0,0 +1 @@ +"""Empty file included to support filesystem-based resolver tests.""" diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/cloud/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/cloud/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/cloud/hooks/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/cloud/hooks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/cloud/hooks/gcs.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/cloud/hooks/gcs.py new file mode 100644 index 0000000000..75a6dead35 --- /dev/null +++ b/crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/cloud/hooks/gcs.py @@ -0,0 +1 @@ +"""Empty file included to support filesystem-based resolver tests.""" diff --git a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/__init__.py @@ -0,0 +1 @@ + diff --git a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/base.py b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/base.py new file mode 100644 index 0000000000..75a6dead35 --- /dev/null +++ b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/base.py @@ -0,0 +1 @@ +"""Empty file included to support filesystem-based resolver tests.""" diff --git a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/dependency.py b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/dependency.py new file mode 100644 index 0000000000..75a6dead35 --- /dev/null +++ b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/dependency.py @@ -0,0 +1 @@ +"""Empty file included to support filesystem-based resolver tests.""" diff --git a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/query.py b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/query.py new file mode 100644 index 0000000000..75a6dead35 --- /dev/null +++ b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/query.py @@ -0,0 +1 @@ +"""Empty file included to support filesystem-based resolver tests.""" diff --git a/crates/ruff_python_resolver/src/implicit_imports.rs b/crates/ruff_python_resolver/src/implicit_imports.rs index 2c42f5450a..25a7d040c6 100644 --- a/crates/ruff_python_resolver/src/implicit_imports.rs +++ b/crates/ruff_python_resolver/src/implicit_imports.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::BTreeMap; use std::ffi::OsStr; use std::fs; use std::path::{Path, PathBuf}; @@ -24,8 +24,8 @@ pub(crate) struct ImplicitImport { } /// Find the "implicit" imports within the namespace package at the given path. -pub(crate) fn find(dir_path: &Path, exclusions: &[&Path]) -> HashMap { - let mut implicit_imports = HashMap::new(); +pub(crate) fn find(dir_path: &Path, exclusions: &[&Path]) -> BTreeMap { + let mut implicit_imports = BTreeMap::new(); // Enumerate all files and directories in the path, expanding links. let Ok(entries) = fs::read_dir(dir_path) else { @@ -128,14 +128,14 @@ pub(crate) fn find(dir_path: &Path, exclusions: &[&Path]) -> HashMap, + implicit_imports: &BTreeMap, imported_symbols: &[String], -) -> Option> { +) -> Option> { if implicit_imports.is_empty() || imported_symbols.is_empty() { return None; } - let mut filtered_imports = HashMap::new(); + let mut filtered_imports = BTreeMap::new(); for implicit_import in implicit_imports.values() { if imported_symbols.contains(&implicit_import.name) { filtered_imports.insert(implicit_import.name.clone(), implicit_import.clone()); diff --git a/crates/ruff_python_resolver/src/import_result.rs b/crates/ruff_python_resolver/src/import_result.rs index 6ca7bd245c..72161cb3e1 100644 --- a/crates/ruff_python_resolver/src/import_result.rs +++ b/crates/ruff_python_resolver/src/import_result.rs @@ -1,9 +1,9 @@ //! Interface that describes the output of the import resolver. -use crate::implicit_imports::ImplicitImport; -use std::collections::HashMap; +use std::collections::BTreeMap; use std::path::PathBuf; +use crate::implicit_imports::ImplicitImport; use crate::py_typed::PyTypedInfo; #[derive(Debug, Clone, PartialEq, Eq)] @@ -69,11 +69,11 @@ pub(crate) struct ImportResult { /// A map from file to resolved path, for all implicitly imported /// modules that are part of a namespace package. - pub(crate) implicit_imports: HashMap, + pub(crate) implicit_imports: BTreeMap, /// Any implicit imports whose symbols were explicitly imported (i.e., via /// a `from x import y` statement). - pub(crate) filtered_implicit_imports: HashMap, + pub(crate) filtered_implicit_imports: BTreeMap, /// If the import resolved to a type hint (i.e., a `.pyi` file), then /// a non-type-hint resolution will be stored here. @@ -105,8 +105,8 @@ impl ImportResult { is_stdlib_typeshed_file: false, is_third_party_typeshed_file: false, is_local_typings_file: false, - implicit_imports: HashMap::default(), - filtered_implicit_imports: HashMap::default(), + implicit_imports: BTreeMap::default(), + filtered_implicit_imports: BTreeMap::default(), non_stub_import_result: None, py_typed_info: None, package_directory: None, diff --git a/crates/ruff_python_resolver/src/lib.rs b/crates/ruff_python_resolver/src/lib.rs index c1fef3be66..9cc1458b1f 100644 --- a/crates/ruff_python_resolver/src/lib.rs +++ b/crates/ruff_python_resolver/src/lib.rs @@ -12,5 +12,3 @@ mod python_platform; mod python_version; mod resolver; mod search; - -pub(crate) const SITE_PACKAGES: &str = "site-packages"; diff --git a/crates/ruff_python_resolver/src/resolver.rs b/crates/ruff_python_resolver/src/resolver.rs index 93e913a3f8..ee845f6839 100644 --- a/crates/ruff_python_resolver/src/resolver.rs +++ b/crates/ruff_python_resolver/src/resolver.rs @@ -1,6 +1,6 @@ //! Resolves Python imports to their corresponding files on disk. -use std::collections::HashMap; +use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use log::debug; @@ -36,7 +36,7 @@ fn resolve_module_descriptor( let mut is_stub_package = false; let mut is_stub_file = false; let mut is_native_lib = false; - let mut implicit_imports = HashMap::new(); + let mut implicit_imports = BTreeMap::new(); let mut package_directory = None; let mut py_typed_info = None; @@ -194,7 +194,7 @@ fn resolve_module_descriptor( is_third_party_typeshed_file: false, is_local_typings_file: false, implicit_imports, - filtered_implicit_imports: HashMap::default(), + filtered_implicit_imports: BTreeMap::default(), non_stub_import_result: None, py_typed_info, package_directory, @@ -424,7 +424,7 @@ fn resolve_best_absolute_import( /// are all satisfied by submodules (as listed in the implicit imports). fn is_namespace_package_resolved( module_descriptor: &ImportModuleDescriptor, - implicit_imports: &HashMap, + implicit_imports: &BTreeMap, ) -> bool { if !module_descriptor.imported_symbols.is_empty() { // Pyright uses `!Array.from(moduleDescriptor.importedSymbols.keys()).some((symbol) => implicitImports.has(symbol))`. @@ -774,6 +774,7 @@ fn resolve_import( #[cfg(test)] mod tests { + use insta::assert_debug_snapshot; use std::fs::{create_dir_all, File}; use std::io::{self, Write}; use std::path::{Path, PathBuf}; @@ -823,6 +824,8 @@ mod tests { library: Option, stub_path: Option, typeshed_path: Option, + venv_path: Option, + venv: Option, } fn resolve_options( @@ -836,6 +839,8 @@ mod tests { library, stub_path, typeshed_path, + venv_path, + venv, } = options; let execution_environment = ExecutionEnvironment { @@ -860,8 +865,8 @@ mod tests { let config = Config { typeshed_path, stub_path, - venv_path: None, - venv: None, + venv_path, + venv, }; let host = host::StaticHost::new(if let Some(library) = library { @@ -1545,4 +1550,109 @@ mod tests { Ok(()) } + + #[test] + fn airflow_standard_library() { + setup(); + + let root = PathBuf::from("./resources/test/airflow"); + let source_file = root.join("airflow/api/common/mark_tasks.py"); + + let result = resolve_options( + source_file, + "os", + root.clone(), + ResolverOptions { + venv_path: Some(root), + venv: Some(PathBuf::from("venv")), + ..Default::default() + }, + ); + + assert_debug_snapshot!(result); + } + + #[test] + fn airflow_first_party() { + setup(); + + let root = PathBuf::from("./resources/test/airflow"); + let source_file = root.join("airflow/api/common/mark_tasks.py"); + + let result = resolve_options( + source_file, + "airflow.jobs.scheduler_job_runner", + root.clone(), + ResolverOptions { + venv_path: Some(root), + venv: Some(PathBuf::from("venv")), + ..Default::default() + }, + ); + + assert_debug_snapshot!(result); + } + + #[test] + fn airflow_stub_file() { + setup(); + + let root = PathBuf::from("./resources/test/airflow"); + let source_file = root.join("airflow/api/common/mark_tasks.py"); + + let result = resolve_options( + source_file, + "airflow.compat.functools", + root.clone(), + ResolverOptions { + venv_path: Some(root), + venv: Some(PathBuf::from("venv")), + ..Default::default() + }, + ); + + assert_debug_snapshot!(result); + } + + #[test] + fn airflow_namespace_package() { + setup(); + + let root = PathBuf::from("./resources/test/airflow"); + let source_file = root.join("airflow/api/common/mark_tasks.py"); + + let result = resolve_options( + source_file, + "airflow.providers.google.cloud.hooks.gcs", + root.clone(), + ResolverOptions { + venv_path: Some(root), + venv: Some(PathBuf::from("venv")), + ..Default::default() + }, + ); + + assert_debug_snapshot!(result); + } + + #[test] + fn airflow_third_party() { + setup(); + + let root = PathBuf::from("./resources/test/airflow"); + let source_file = root.join("airflow/api/common/mark_tasks.py"); + + let result = resolve_options( + source_file, + "sqlalchemy.orm", + root.clone(), + ResolverOptions { + venv_path: Some(root), + venv: Some(PathBuf::from("venv")), + ..Default::default() + }, + ); + + assert_debug_snapshot!(result); + } } diff --git a/crates/ruff_python_resolver/src/search.rs b/crates/ruff_python_resolver/src/search.rs index 9f61b350f0..57128e6458 100644 --- a/crates/ruff_python_resolver/src/search.rs +++ b/crates/ruff_python_resolver/src/search.rs @@ -8,9 +8,11 @@ use std::path::{Path, PathBuf}; use log::debug; use crate::config::Config; +use crate::host; use crate::module_descriptor::ImportModuleDescriptor; use crate::python_version::PythonVersion; -use crate::{host, SITE_PACKAGES}; + +const SITE_PACKAGES: &str = "site-packages"; /// Find the `site-packages` directory for the specified Python version. fn find_site_packages_path( diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_first_party.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_first_party.snap new file mode 100644 index 0000000000..d2197f6cab --- /dev/null +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_first_party.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff_python_resolver/src/resolver.rs +expression: result +--- +ImportResult { + is_relative: false, + is_import_found: true, + is_partly_resolved: false, + is_namespace_package: false, + is_init_file_present: true, + is_stub_package: false, + import_type: Local, + resolved_paths: [ + "./resources/test/airflow/airflow/__init__.py", + "./resources/test/airflow/airflow/jobs/__init__.py", + "./resources/test/airflow/airflow/jobs/scheduler_job_runner.py", + ], + search_path: Some( + "./resources/test/airflow", + ), + is_stub_file: false, + is_native_lib: false, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: {}, + filtered_implicit_imports: {}, + non_stub_import_result: None, + py_typed_info: None, + package_directory: Some( + "./resources/test/airflow/airflow", + ), +} diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_namespace_package.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_namespace_package.snap new file mode 100644 index 0000000000..15954d4c98 --- /dev/null +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_namespace_package.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_python_resolver/src/resolver.rs +expression: result +--- +ImportResult { + is_relative: false, + is_import_found: true, + is_partly_resolved: false, + is_namespace_package: true, + is_init_file_present: true, + is_stub_package: false, + import_type: Local, + resolved_paths: [ + "./resources/test/airflow/airflow/__init__.py", + "", + "./resources/test/airflow/airflow/providers/google/__init__.py", + "./resources/test/airflow/airflow/providers/google/cloud/__init__.py", + "./resources/test/airflow/airflow/providers/google/cloud/hooks/__init__.py", + "./resources/test/airflow/airflow/providers/google/cloud/hooks/gcs.py", + ], + search_path: Some( + "./resources/test/airflow", + ), + is_stub_file: false, + is_native_lib: false, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: {}, + filtered_implicit_imports: {}, + non_stub_import_result: None, + py_typed_info: None, + package_directory: Some( + "./resources/test/airflow/airflow", + ), +} diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_standard_library.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_standard_library.snap new file mode 100644 index 0000000000..3262ce7f6f --- /dev/null +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_standard_library.snap @@ -0,0 +1,25 @@ +--- +source: crates/ruff_python_resolver/src/resolver.rs +expression: result +--- +ImportResult { + is_relative: false, + is_import_found: false, + is_partly_resolved: false, + is_namespace_package: false, + is_init_file_present: false, + is_stub_package: false, + import_type: Local, + resolved_paths: [], + search_path: None, + is_stub_file: false, + is_native_lib: false, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: {}, + filtered_implicit_imports: {}, + non_stub_import_result: None, + py_typed_info: None, + package_directory: None, +} diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_stub_file.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_stub_file.snap new file mode 100644 index 0000000000..37f26c80d7 --- /dev/null +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_stub_file.snap @@ -0,0 +1,63 @@ +--- +source: crates/ruff_python_resolver/src/resolver.rs +expression: result +--- +ImportResult { + is_relative: false, + is_import_found: true, + is_partly_resolved: false, + is_namespace_package: false, + is_init_file_present: true, + is_stub_package: false, + import_type: Local, + resolved_paths: [ + "./resources/test/airflow/airflow/__init__.py", + "./resources/test/airflow/airflow/compat/__init__.py", + "./resources/test/airflow/airflow/compat/functools.pyi", + ], + search_path: Some( + "./resources/test/airflow", + ), + is_stub_file: true, + is_native_lib: false, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: {}, + filtered_implicit_imports: {}, + non_stub_import_result: Some( + ImportResult { + is_relative: false, + is_import_found: true, + is_partly_resolved: false, + is_namespace_package: false, + is_init_file_present: true, + is_stub_package: false, + import_type: Local, + resolved_paths: [ + "./resources/test/airflow/airflow/__init__.py", + "./resources/test/airflow/airflow/compat/__init__.py", + "./resources/test/airflow/airflow/compat/functools.py", + ], + search_path: Some( + "./resources/test/airflow", + ), + is_stub_file: false, + is_native_lib: false, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: {}, + filtered_implicit_imports: {}, + non_stub_import_result: None, + py_typed_info: None, + package_directory: Some( + "./resources/test/airflow/airflow", + ), + }, + ), + py_typed_info: None, + package_directory: Some( + "./resources/test/airflow/airflow", + ), +} diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_third_party.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_third_party.snap new file mode 100644 index 0000000000..04c953624f --- /dev/null +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_third_party.snap @@ -0,0 +1,54 @@ +--- +source: crates/ruff_python_resolver/src/resolver.rs +expression: result +--- +ImportResult { + is_relative: false, + is_import_found: true, + is_partly_resolved: false, + is_namespace_package: false, + is_init_file_present: true, + is_stub_package: false, + import_type: ThirdParty, + resolved_paths: [ + "./resources/test/airflow/venv/Lib/python3.11/site-packages/sqlalchemy/__init__.py", + "./resources/test/airflow/venv/Lib/python3.11/site-packages/sqlalchemy/orm/__init__.py", + ], + search_path: Some( + "./resources/test/airflow/venv/Lib/python3.11/site-packages", + ), + is_stub_file: false, + is_native_lib: false, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: { + "base": ImplicitImport { + is_stub_file: false, + is_native_lib: false, + name: "base", + path: "./resources/test/airflow/venv/Lib/python3.11/site-packages/sqlalchemy/orm/base.py", + py_typed: None, + }, + "dependency": ImplicitImport { + is_stub_file: false, + is_native_lib: false, + name: "dependency", + path: "./resources/test/airflow/venv/Lib/python3.11/site-packages/sqlalchemy/orm/dependency.py", + py_typed: None, + }, + "query": ImplicitImport { + is_stub_file: false, + is_native_lib: false, + name: "query", + path: "./resources/test/airflow/venv/Lib/python3.11/site-packages/sqlalchemy/orm/query.py", + py_typed: None, + }, + }, + filtered_implicit_imports: {}, + non_stub_import_result: None, + py_typed_info: None, + package_directory: Some( + "./resources/test/airflow/venv/Lib/python3.11/site-packages/sqlalchemy", + ), +} From 979049b2a60598d5ea274c9b19f12501fac79950 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 28 Jun 2023 09:52:20 -0400 Subject: [PATCH 264/447] Make lib iteration platform-specific (#5406) --- crates/ruff_python_resolver/src/python_platform.rs | 13 +++++++++++++ crates/ruff_python_resolver/src/search.rs | 2 +- ...lver__resolver__tests__airflow_third_party.snap | 14 +++++++------- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/crates/ruff_python_resolver/src/python_platform.rs b/crates/ruff_python_resolver/src/python_platform.rs index 8ee2600518..b82ebe256c 100644 --- a/crates/ruff_python_resolver/src/python_platform.rs +++ b/crates/ruff_python_resolver/src/python_platform.rs @@ -5,3 +5,16 @@ pub(crate) enum PythonPlatform { Linux, Windows, } + +impl PythonPlatform { + /// Returns the platform-specific library names. These are the candidate names for the top-level + /// subdirectory within a virtual environment that contains the `site-packages` directory + /// (with a `pythonX.Y` directory in-between). + pub(crate) fn lib_names(&self) -> &[&'static str] { + match self { + PythonPlatform::Darwin => &["lib"], + PythonPlatform::Linux => &["lib", "lib64"], + PythonPlatform::Windows => &["Lib"], + } + } +} diff --git a/crates/ruff_python_resolver/src/search.rs b/crates/ruff_python_resolver/src/search.rs index 57128e6458..01c6114ae4 100644 --- a/crates/ruff_python_resolver/src/search.rs +++ b/crates/ruff_python_resolver/src/search.rs @@ -140,7 +140,7 @@ fn find_python_search_paths(config: &Config, host: &Host) -> V if let Some(venv) = config.venv.as_ref() { let mut found_paths = vec![]; - for lib_name in ["lib", "Lib", "lib64"] { + for lib_name in host.python_platform().lib_names() { let lib_path = venv_path.join(venv).join(lib_name); if let Some(site_packages_path) = find_site_packages_path(&lib_path, None) { // Add paths from any `.pth` files in each of the `site-packages` directories. diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_third_party.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_third_party.snap index 04c953624f..ed13cadb03 100644 --- a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_third_party.snap +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_third_party.snap @@ -11,11 +11,11 @@ ImportResult { is_stub_package: false, import_type: ThirdParty, resolved_paths: [ - "./resources/test/airflow/venv/Lib/python3.11/site-packages/sqlalchemy/__init__.py", - "./resources/test/airflow/venv/Lib/python3.11/site-packages/sqlalchemy/orm/__init__.py", + "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/__init__.py", + "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/__init__.py", ], search_path: Some( - "./resources/test/airflow/venv/Lib/python3.11/site-packages", + "./resources/test/airflow/venv/lib/python3.11/site-packages", ), is_stub_file: false, is_native_lib: false, @@ -27,21 +27,21 @@ ImportResult { is_stub_file: false, is_native_lib: false, name: "base", - path: "./resources/test/airflow/venv/Lib/python3.11/site-packages/sqlalchemy/orm/base.py", + path: "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/base.py", py_typed: None, }, "dependency": ImplicitImport { is_stub_file: false, is_native_lib: false, name: "dependency", - path: "./resources/test/airflow/venv/Lib/python3.11/site-packages/sqlalchemy/orm/dependency.py", + path: "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/dependency.py", py_typed: None, }, "query": ImplicitImport { is_stub_file: false, is_native_lib: false, name: "query", - path: "./resources/test/airflow/venv/Lib/python3.11/site-packages/sqlalchemy/orm/query.py", + path: "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/query.py", py_typed: None, }, }, @@ -49,6 +49,6 @@ ImportResult { non_stub_import_result: None, py_typed_info: None, package_directory: Some( - "./resources/test/airflow/venv/Lib/python3.11/site-packages/sqlalchemy", + "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy", ), } From ea7bb199bc4ce13bde6fa5a27d216744d6e4940e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 28 Jun 2023 10:50:54 -0400 Subject: [PATCH 265/447] Fill-in missing implementation for `is_native_module_file_name` (#5410) ## Summary This was just an oversight -- the last remaining `todo!()` that I never filled in. We clearly don't have any test coverage for it yet, but this mimics the Pyright implementation. --- .../src/implicit_imports.rs | 7 +- .../ruff_python_resolver/src/native_module.rs | 65 ++++++++++++++++++- crates/ruff_python_resolver/src/resolver.rs | 37 +++++++---- 3 files changed, 86 insertions(+), 23 deletions(-) diff --git a/crates/ruff_python_resolver/src/implicit_imports.rs b/crates/ruff_python_resolver/src/implicit_imports.rs index 25a7d040c6..926c947901 100644 --- a/crates/ruff_python_resolver/src/implicit_imports.rs +++ b/crates/ruff_python_resolver/src/implicit_imports.rs @@ -64,12 +64,7 @@ pub(crate) fn find(dir_path: &Path, exclusions: &[&Path]) -> BTreeMap bool { file_extension == "so" || file_extension == "pyd" || file_extension == "dylib" } -/// Returns `true` if the given file name is that of a native module. -pub(crate) fn is_native_module_file_name(_module_name: &Path, _file_name: &Path) -> bool { - todo!() +/// Given a file name, returns the name of the native module it represents. +/// +/// For example, given `foo.abi3.so`, return `foo`. +pub(crate) fn native_module_name(file_name: &Path) -> Option<&str> { + file_name + .file_stem() + .and_then(OsStr::to_str) + .map(|file_stem| { + file_stem + .split_once('.') + .map_or(file_stem, |(file_stem, _)| file_stem) + }) +} + +/// Returns `true` if the given file name is that of a native module with the given name. +pub(crate) fn is_native_module_file_name(module_name: &str, file_name: &Path) -> bool { + // The file name must be that of a native module. + if !file_name + .extension() + .map_or(false, is_native_module_file_extension) + { + return false; + }; + + // The file must represent the module name. + native_module_name(file_name) == Some(module_name) +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + #[test] + fn module_name() { + assert_eq!( + super::native_module_name(&PathBuf::from("foo.so")), + Some("foo") + ); + + assert_eq!( + super::native_module_name(&PathBuf::from("foo.abi3.so")), + Some("foo") + ); + + assert_eq!( + super::native_module_name(&PathBuf::from("foo.cpython-38-x86_64-linux-gnu.so")), + Some("foo") + ); + + assert_eq!( + super::native_module_name(&PathBuf::from("foo.cp39-win_amd64.pyd")), + Some("foo") + ); + } + + #[test] + fn module_file_extension() { + assert!(super::is_native_module_file_extension("so".as_ref())); + assert!(super::is_native_module_file_extension("pyd".as_ref())); + assert!(super::is_native_module_file_extension("dylib".as_ref())); + assert!(!super::is_native_module_file_extension("py".as_ref())); + } } diff --git a/crates/ruff_python_resolver/src/resolver.rs b/crates/ruff_python_resolver/src/resolver.rs index ee845f6839..3cb3eca260 100644 --- a/crates/ruff_python_resolver/src/resolver.rs +++ b/crates/ruff_python_resolver/src/resolver.rs @@ -1,6 +1,7 @@ //! Resolves Python imports to their corresponding files on disk. use std::collections::BTreeMap; +use std::ffi::OsStr; use std::path::{Path, PathBuf}; use log::debug; @@ -138,18 +139,21 @@ fn resolve_module_descriptor( } else { if allow_native_lib && dir_path.is_dir() { // We couldn't find a `.py[i]` file; search for a native library. - if let Some(native_lib_path) = dir_path - .read_dir() - .unwrap() - .flatten() - .filter(|entry| entry.file_type().map_or(false, |ft| ft.is_file())) - .find(|entry| { - native_module::is_native_module_file_name(&dir_path, &entry.path()) - }) - { - debug!("Resolved import with file: {native_lib_path:?}"); - is_native_lib = true; - resolved_paths.push(native_lib_path.path()); + if let Some(module_name) = dir_path.file_name().and_then(OsStr::to_str) { + if let Some(native_lib_path) = dir_path + .read_dir() + .unwrap() + .flatten() + .filter(|entry| entry.file_type().map_or(false, |ft| ft.is_file())) + .map(|entry| entry.path()) + .find(|path| { + native_module::is_native_module_file_name(module_name, path) + }) + { + debug!("Resolved import with file: {native_lib_path:?}"); + is_native_lib = true; + resolved_paths.push(native_lib_path); + } } } @@ -427,8 +431,13 @@ fn is_namespace_package_resolved( implicit_imports: &BTreeMap, ) -> bool { if !module_descriptor.imported_symbols.is_empty() { - // Pyright uses `!Array.from(moduleDescriptor.importedSymbols.keys()).some((symbol) => implicitImports.has(symbol))`. - // But that only checks if any of the symbols are in the implicit imports? + // TODO(charlie): Pyright uses: + // + // ```typescript + // !Array.from(moduleDescriptor.importedSymbols.keys()).some((symbol) => implicitImports.has(symbol))` + // ``` + // + // However, that only checks if _any_ of the symbols are in the implicit imports. for symbol in &module_descriptor.imported_symbols { if !implicit_imports.contains_key(symbol) { return false; From 1d2d015bc57fb3cbe23398785789ae51e92ca219 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 28 Jun 2023 10:52:23 -0400 Subject: [PATCH 266/447] Make standard input detection robust to invalid arguments (#5393) ## Summary This PR fixes a silent failure that manifested itself in https://github.com/astral-sh/ruff-vscode/issues/238. In short, if the user provided invalid arguments to Ruff in the VS Code extension (like `"ruff.args": ["a"]`), then we generated something like the following command: ```console /path/to/ruff --force-exclude --no-cache --no-fix --format json - --fix a --stdin-filename /path/to/file.py ``` Since this contains both `-` and `a` as the "input files", Ruff would treat this as if we're linting the files names `-` and `a`, rather than linting standard input. This PR modifies out standard input detection to force standard input when `--stdin-filename` is present, or at least one file is `-`. (We then warn and ignore the others.) --- crates/ruff_cli/src/lib.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/ruff_cli/src/lib.rs b/crates/ruff_cli/src/lib.rs index b7abecc4a1..90d705b286 100644 --- a/crates/ruff_cli/src/lib.rs +++ b/crates/ruff_cli/src/lib.rs @@ -77,6 +77,27 @@ fn change_detected(paths: &[PathBuf]) -> Option { None } +/// Returns true if the linter should read from standard input. +fn is_stdin(files: &[PathBuf], stdin_filename: Option<&Path>) -> bool { + // If the user provided a `--stdin-filename`, always read from standard input. + if stdin_filename.is_some() { + if let Some(file) = files.iter().find(|file| file.as_path() != Path::new("-")) { + warn_user_once!( + "Ignoring file {} in favor of standard input.", + file.display() + ); + } + return true; + } + + // If the user provided exactly `-`, read from standard input. + if files.len() == 1 && files[0] == Path::new("-") { + return true; + } + + false +} + pub fn run( Args { command, @@ -329,7 +350,7 @@ pub fn check(args: CheckArgs, log_level: LogLevel) -> Result { } } } else { - let is_stdin = cli.files == vec![PathBuf::from("-")]; + let is_stdin = is_stdin(&cli.files, cli.stdin_filename.as_deref()); // Generate lint violations. let diagnostics = if is_stdin { From 4d90a5a9bc8e5fe0d10d5ab6c8163889b484a4c3 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 28 Jun 2023 13:25:37 -0400 Subject: [PATCH 267/447] Move resolver tests out to top-level (#5424) ## Summary These are really tests for the entire crate. --- ...yter__notebook__tests__import_sorting.snap | 12 +- crates/ruff_python_resolver/src/lib.rs | 885 +++++++++++++++++ crates/ruff_python_resolver/src/resolver.rs | 887 +----------------- ...resolver__tests__airflow_first_party.snap} | 2 +- ...er__tests__airflow_namespace_package.snap} | 2 +- ...ver__tests__airflow_standard_library.snap} | 2 +- ...n_resolver__tests__airflow_stub_file.snap} | 2 +- ...resolver__tests__airflow_third_party.snap} | 2 +- 8 files changed, 896 insertions(+), 898 deletions(-) rename crates/ruff_python_resolver/src/snapshots/{ruff_python_resolver__resolver__tests__airflow_first_party.snap => ruff_python_resolver__tests__airflow_first_party.snap} (94%) rename crates/ruff_python_resolver/src/snapshots/{ruff_python_resolver__resolver__tests__airflow_namespace_package.snap => ruff_python_resolver__tests__airflow_namespace_package.snap} (95%) rename crates/ruff_python_resolver/src/snapshots/{ruff_python_resolver__resolver__tests__airflow_standard_library.snap => ruff_python_resolver__tests__airflow_standard_library.snap} (92%) rename crates/ruff_python_resolver/src/snapshots/{ruff_python_resolver__resolver__tests__airflow_stub_file.snap => ruff_python_resolver__tests__airflow_stub_file.snap} (97%) rename crates/ruff_python_resolver/src/snapshots/{ruff_python_resolver__resolver__tests__airflow_third_party.snap => ruff_python_resolver__tests__airflow_third_party.snap} (97%) diff --git a/crates/ruff/src/jupyter/snapshots/ruff__jupyter__notebook__tests__import_sorting.snap b/crates/ruff/src/jupyter/snapshots/ruff__jupyter__notebook__tests__import_sorting.snap index 8470981be3..240556c375 100644 --- a/crates/ruff/src/jupyter/snapshots/ruff__jupyter__notebook__tests__import_sorting.snap +++ b/crates/ruff/src/jupyter/snapshots/ruff__jupyter__notebook__tests__import_sorting.snap @@ -25,14 +25,12 @@ isort.ipynb:cell 1:1:1: I001 [*] Import block is un-sorted or un-formatted isort.ipynb:cell 2:1:1: I001 [*] Import block is un-sorted or un-formatted | -2 | import random -3 | import math -4 | / from typing import Any -5 | | import collections -6 | | # Newline should be added here +1 | / from typing import Any +2 | | import collections +3 | | # Newline should be added here | |_^ I001 -7 | def foo(): -8 | pass +4 | def foo(): +5 | pass | = help: Organize imports diff --git a/crates/ruff_python_resolver/src/lib.rs b/crates/ruff_python_resolver/src/lib.rs index 9cc1458b1f..505c623099 100644 --- a/crates/ruff_python_resolver/src/lib.rs +++ b/crates/ruff_python_resolver/src/lib.rs @@ -12,3 +12,888 @@ mod python_platform; mod python_version; mod resolver; mod search; + +#[cfg(test)] +mod tests { + use insta::assert_debug_snapshot; + use std::fs::{create_dir_all, File}; + use std::io::{self, Write}; + use std::path::{Path, PathBuf}; + + use log::debug; + use tempfile::TempDir; + + use crate::config::Config; + use crate::execution_environment::ExecutionEnvironment; + use crate::host; + use crate::import_result::{ImportResult, ImportType}; + use crate::module_descriptor::ImportModuleDescriptor; + use crate::python_platform::PythonPlatform; + use crate::python_version::PythonVersion; + use crate::resolver::resolve_import; + + /// Create a file at the given path with the given content. + fn create(path: PathBuf, content: &str) -> io::Result { + if let Some(parent) = path.parent() { + create_dir_all(parent)?; + } + let mut f = File::create(&path)?; + f.write_all(content.as_bytes())?; + f.sync_all()?; + + Ok(path) + } + + /// Create an empty file at the given path. + fn empty(path: PathBuf) -> io::Result { + create(path, "") + } + + /// Create a partial `py.typed` file at the given path. + fn partial(path: PathBuf) -> io::Result { + create(path, "partial\n") + } + + /// Create a `py.typed` file at the given path. + fn typed(path: PathBuf) -> io::Result { + create(path, "# typed") + } + + #[derive(Debug, Default)] + struct ResolverOptions { + extra_paths: Vec, + library: Option, + stub_path: Option, + typeshed_path: Option, + venv_path: Option, + venv: Option, + } + + fn resolve_options( + source_file: impl AsRef, + name: &str, + root: impl Into, + options: ResolverOptions, + ) -> ImportResult { + let ResolverOptions { + extra_paths, + library, + stub_path, + typeshed_path, + venv_path, + venv, + } = options; + + let execution_environment = ExecutionEnvironment { + root: root.into(), + python_version: PythonVersion::Py37, + python_platform: PythonPlatform::Darwin, + extra_paths, + }; + + let module_descriptor = ImportModuleDescriptor { + leading_dots: name.chars().take_while(|c| *c == '.').count(), + name_parts: name + .chars() + .skip_while(|c| *c == '.') + .collect::() + .split('.') + .map(std::string::ToString::to_string) + .collect(), + imported_symbols: Vec::new(), + }; + + let config = Config { + typeshed_path, + stub_path, + venv_path, + venv, + }; + + let host = host::StaticHost::new(if let Some(library) = library { + vec![library] + } else { + Vec::new() + }); + + resolve_import( + source_file.as_ref(), + &execution_environment, + &module_descriptor, + &config, + &host, + ) + } + + fn setup() { + env_logger::builder().is_test(true).try_init().ok(); + } + + #[test] + fn partial_stub_file_exists() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + partial(library.join("myLib-stubs/py.typed"))?; + let partial_stub_pyi = empty(library.join("myLib-stubs").join("partialStub.pyi"))?; + let partial_stub_py = empty(library.join("myLib/partialStub.py"))?; + + let result = resolve_options( + partial_stub_py, + "myLib.partialStub", + root, + ResolverOptions { + library: Some(library), + ..Default::default() + }, + ); + + assert!(result.is_import_found); + assert!(result.is_stub_file); + assert_eq!(result.import_type, ImportType::ThirdParty); + assert_eq!( + result.resolved_paths, + // TODO(charlie): Pyright matches on `libraryRoot, 'myLib', 'partialStub.pyi'` here. + // But that file doesn't exist. There's some kind of transform. + vec![PathBuf::new(), partial_stub_pyi] + ); + + Ok(()) + } + + #[test] + fn partial_stub_init_exists() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + partial(library.join("myLib-stubs/py.typed"))?; + let partial_stub_init_pyi = empty(library.join("myLib-stubs/__init__.pyi"))?; + let partial_stub_init_py = empty(library.join("myLib/__init__.py"))?; + + let result = resolve_options( + partial_stub_init_py, + "myLib", + root, + ResolverOptions { + library: Some(library), + ..Default::default() + }, + ); + + assert!(result.is_import_found); + assert!(result.is_stub_file); + assert_eq!(result.import_type, ImportType::ThirdParty); + assert_eq!( + result.resolved_paths, + // TODO(charlie): Pyright matches on `libraryRoot, 'myLib', '__init__.pyi'` here. + // But that file doesn't exist. There's some kind of transform. + vec![partial_stub_init_pyi] + ); + + Ok(()) + } + + #[test] + fn side_by_side_files() -> io::Result<()> { + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + partial(library.join("myLib-stubs/py.typed"))?; + empty(library.join("myLib/partialStub.pyi"))?; + empty(library.join("myLib/partialStub.py"))?; + empty(library.join("myLib/partialStub2.py"))?; + let my_file = empty(root.join("myFile.py"))?; + let side_by_side_stub_file = empty(library.join("myLib-stubs/partialStub.pyi"))?; + let partial_stub_file = empty(library.join("myLib-stubs/partialStub2.pyi"))?; + + // Stub package wins over original package (per PEP 561 rules). + let side_by_side_result = resolve_options( + &my_file, + "myLib.partialStub", + root, + ResolverOptions { + library: Some(library.clone()), + ..Default::default() + }, + ); + assert!(side_by_side_result.is_import_found); + assert!(side_by_side_result.is_stub_file); + assert_eq!( + side_by_side_result.resolved_paths, + vec![PathBuf::new(), side_by_side_stub_file] + ); + + // Side by side stub doesn't completely disable partial stub. + let partial_stub_result = resolve_options( + &my_file, + "myLib.partialStub2", + root, + ResolverOptions { + library: Some(library), + ..Default::default() + }, + ); + assert!(partial_stub_result.is_import_found); + assert!(partial_stub_result.is_stub_file); + assert_eq!( + partial_stub_result.resolved_paths, + vec![PathBuf::new(), partial_stub_file] + ); + + Ok(()) + } + + #[test] + fn stub_package() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + empty(library.join("myLib-stubs/stub.pyi"))?; + empty(library.join("myLib-stubs/__init__.pyi"))?; + let partial_stub_py = empty(library.join("myLib/partialStub.py"))?; + + let result = resolve_options( + partial_stub_py, + "myLib.partialStub", + root, + ResolverOptions { + library: Some(library), + ..Default::default() + }, + ); + + // If fully typed stub package exists, that wins over the real package. + assert!(!result.is_import_found); + + Ok(()) + } + + #[test] + fn stub_namespace_package() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + empty(library.join("myLib-stubs/stub.pyi"))?; + let partial_stub_py = empty(library.join("myLib/partialStub.py"))?; + + let result = resolve_options( + partial_stub_py.clone(), + "myLib.partialStub", + root, + ResolverOptions { + library: Some(library), + ..Default::default() + }, + ); + + // If fully typed stub package exists, that wins over the real package. + assert!(result.is_import_found); + assert!(!result.is_stub_file); + assert_eq!(result.resolved_paths, vec![PathBuf::new(), partial_stub_py]); + + Ok(()) + } + + #[test] + fn stub_in_typing_folder_over_partial_stub_package() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + let typing_folder = root.join("typing"); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + partial(library.join("myLib-stubs/py.typed"))?; + empty(library.join("myLib-stubs/__init__.pyi"))?; + let my_lib_pyi = empty(typing_folder.join("myLib.pyi"))?; + let my_lib_init_py = empty(library.join("myLib/__init__.py"))?; + + let result = resolve_options( + my_lib_init_py, + "myLib", + root, + ResolverOptions { + library: Some(library), + stub_path: Some(typing_folder), + ..Default::default() + }, + ); + + // If the package exists in typing folder, that gets picked up first (so we resolve to + // `myLib.pyi`). + assert!(result.is_import_found); + assert!(result.is_stub_file); + assert_eq!(result.resolved_paths, vec![my_lib_pyi]); + + Ok(()) + } + + #[test] + fn partial_stub_package_in_typing_folder() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + let typing_folder = root.join("typing"); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + partial(typing_folder.join("myLib-stubs/py.typed"))?; + let my_lib_stubs_init_pyi = empty(typing_folder.join("myLib-stubs/__init__.pyi"))?; + let my_lib_init_py = empty(library.join("myLib/__init__.py"))?; + + let result = resolve_options( + my_lib_init_py, + "myLib", + root, + ResolverOptions { + library: Some(library), + stub_path: Some(typing_folder), + ..Default::default() + }, + ); + + // If the package exists in typing folder, that gets picked up first (so we resolve to + // `myLib.pyi`). + assert!(result.is_import_found); + assert!(result.is_stub_file); + assert_eq!(result.resolved_paths, vec![my_lib_stubs_init_pyi]); + + Ok(()) + } + + #[test] + fn typeshed_folder() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + let typeshed_folder = root.join("ts"); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + empty(typeshed_folder.join("stubs/myLibPackage/myLib.pyi"))?; + partial(library.join("myLib-stubs/py.typed"))?; + let my_lib_stubs_init_pyi = empty(library.join("myLib-stubs/__init__.pyi"))?; + let my_lib_init_py = empty(library.join("myLib/__init__.py"))?; + + let result = resolve_options( + my_lib_init_py, + "myLib", + root, + ResolverOptions { + library: Some(library), + typeshed_path: Some(typeshed_folder), + ..Default::default() + }, + ); + + // Stub packages win over typeshed. + assert!(result.is_import_found); + assert!(result.is_stub_file); + assert_eq!(result.resolved_paths, vec![my_lib_stubs_init_pyi]); + + Ok(()) + } + + #[test] + fn py_typed_file() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + empty(library.join("myLib/__init__.py"))?; + partial(library.join("myLib-stubs/py.typed"))?; + let partial_stub_init_pyi = empty(library.join("myLib-stubs/__init__.pyi"))?; + let package_py_typed = typed(library.join("myLib/py.typed"))?; + + let result = resolve_options( + package_py_typed, + "myLib", + root, + ResolverOptions { + library: Some(library), + ..Default::default() + }, + ); + + // Partial stub package always overrides original package. + assert!(result.is_import_found); + assert!(result.is_stub_file); + assert_eq!(result.resolved_paths, vec![partial_stub_init_pyi]); + + Ok(()) + } + + #[test] + fn py_typed_library() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + let typeshed_folder = root.join("ts"); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + typed(library.join("os/py.typed"))?; + let init_py = empty(library.join("os/__init__.py"))?; + let typeshed_init_pyi = empty(typeshed_folder.join("stubs/os/os/__init__.pyi"))?; + + let result = resolve_options( + typeshed_init_pyi, + "os", + root, + ResolverOptions { + library: Some(library), + typeshed_path: Some(typeshed_folder), + ..Default::default() + }, + ); + + assert!(result.is_import_found); + assert_eq!(result.resolved_paths, vec![init_py]); + + Ok(()) + } + + #[test] + fn non_py_typed_library() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + let typeshed_folder = root.join("ts"); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + empty(library.join("os/__init__.py"))?; + let typeshed_init_pyi = empty(typeshed_folder.join("stubs/os/os/__init__.pyi"))?; + + let result = resolve_options( + typeshed_init_pyi.clone(), + "os", + root, + ResolverOptions { + library: Some(library), + typeshed_path: Some(typeshed_folder), + ..Default::default() + }, + ); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::ThirdParty); + assert_eq!(result.resolved_paths, vec![typeshed_init_pyi]); + + Ok(()) + } + + #[test] + fn import_side_by_side_file_root() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let file1 = empty(root.join("file1.py"))?; + let file2 = empty(root.join("file2.py"))?; + + let result = resolve_options(file2, "file1", root, ResolverOptions::default()); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!(result.resolved_paths, vec![file1]); + + Ok(()) + } + + #[test] + fn import_side_by_side_file_sub_folder() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let test_init = empty(root.join("test/__init__.py"))?; + let test_file1 = empty(root.join("test/file1.py"))?; + let test_file2 = empty(root.join("test/file2.py"))?; + + let result = resolve_options(test_file2, "test.file1", root, ResolverOptions::default()); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!(result.resolved_paths, vec![test_init, test_file1]); + + Ok(()) + } + + #[test] + fn import_side_by_side_file_sub_under_src_folder() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let nested_init = empty(root.join("src/nested/__init__.py"))?; + let nested_file1 = empty(root.join("src/nested/file1.py"))?; + let nested_file2 = empty(root.join("src/nested/file2.py"))?; + + let result = resolve_options( + nested_file2, + "nested.file1", + root, + ResolverOptions::default(), + ); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!(result.resolved_paths, vec![nested_init, nested_file1]); + + Ok(()) + } + + #[test] + fn import_file_sub_under_containing_folder() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let nested_file1 = empty(root.join("src/nested/file1.py"))?; + let nested_file2 = empty(root.join("src/nested/nested2/file2.py"))?; + + let result = resolve_options(nested_file2, "file1", root, ResolverOptions::default()); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!(result.resolved_paths, vec![nested_file1]); + + Ok(()) + } + + #[test] + fn import_side_by_side_file_sub_under_lib_folder() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + empty(library.join("myLib/file1.py"))?; + let file2 = empty(library.join("myLib/file2.py"))?; + + let result = resolve_options(file2, "file1", root, ResolverOptions::default()); + + debug!("result: {:?}", result); + + assert!(!result.is_import_found); + + Ok(()) + } + + #[test] + fn nested_namespace_package_1() -> io::Result<()> { + // See: https://github.com/microsoft/pyright/issues/5089. + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let file = empty(root.join("package1/a/b/c/d.py"))?; + let package1_init = empty(root.join("package1/a/__init__.py"))?; + let package2_init = empty(root.join("package2/a/__init__.py"))?; + + let package1 = root.join("package1"); + let package2 = root.join("package2"); + + let result = resolve_options( + package2_init, + "a.b.c.d", + root, + ResolverOptions { + extra_paths: vec![package1, package2], + ..Default::default() + }, + ); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!( + result.resolved_paths, + vec![package1_init, PathBuf::new(), PathBuf::new(), file] + ); + + Ok(()) + } + + #[test] + fn nested_namespace_package_2() -> io::Result<()> { + // See: https://github.com/microsoft/pyright/issues/5089. + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let file = empty(root.join("package1/a/b/c/d.py"))?; + let package1_init = empty(root.join("package1/a/b/c/__init__.py"))?; + let package2_init = empty(root.join("package2/a/b/c/__init__.py"))?; + + let package1 = root.join("package1"); + let package2 = root.join("package2"); + + let result = resolve_options( + package2_init, + "a.b.c.d", + root, + ResolverOptions { + extra_paths: vec![package1, package2], + ..Default::default() + }, + ); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!( + result.resolved_paths, + vec![PathBuf::new(), PathBuf::new(), package1_init, file] + ); + + Ok(()) + } + + #[test] + fn nested_namespace_package_3() -> io::Result<()> { + // See: https://github.com/microsoft/pyright/issues/5089. + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + empty(root.join("package1/a/b/c/d.py"))?; + let package2_init = empty(root.join("package2/a/__init__.py"))?; + + let package1 = root.join("package1"); + let package2 = root.join("package2"); + + let result = resolve_options( + package2_init, + "a.b.c.d", + root, + ResolverOptions { + extra_paths: vec![package1, package2], + ..Default::default() + }, + ); + + assert!(!result.is_import_found); + + Ok(()) + } + + #[test] + fn nested_namespace_package_4() -> io::Result<()> { + // See: https://github.com/microsoft/pyright/issues/5089. + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + empty(root.join("package1/a/b/__init__.py"))?; + empty(root.join("package1/a/b/c.py"))?; + empty(root.join("package2/a/__init__.py"))?; + let package2_a_b_init = empty(root.join("package2/a/b/__init__.py"))?; + + let package1 = root.join("package1"); + let package2 = root.join("package2"); + + let result = resolve_options( + package2_a_b_init, + "a.b.c", + root, + ResolverOptions { + extra_paths: vec![package1, package2], + ..Default::default() + }, + ); + + assert!(!result.is_import_found); + + Ok(()) + } + + // New tests, don't exist upstream. + #[test] + fn relative_import_side_by_side_file_root() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let file1 = empty(root.join("file1.py"))?; + let file2 = empty(root.join("file2.py"))?; + + let result = resolve_options(file2, ".file1", root, ResolverOptions::default()); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!(result.resolved_paths, vec![file1]); + + Ok(()) + } + + #[test] + fn invalid_relative_import_side_by_side_file_root() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + empty(root.join("file1.py"))?; + let file2 = empty(root.join("file2.py"))?; + + let result = resolve_options(file2, "..file1", root, ResolverOptions::default()); + + assert!(!result.is_import_found); + + Ok(()) + } + + #[test] + fn airflow_standard_library() { + setup(); + + let root = PathBuf::from("./resources/test/airflow"); + let source_file = root.join("airflow/api/common/mark_tasks.py"); + + let result = resolve_options( + source_file, + "os", + root.clone(), + ResolverOptions { + venv_path: Some(root), + venv: Some(PathBuf::from("venv")), + ..Default::default() + }, + ); + + assert_debug_snapshot!(result); + } + + #[test] + fn airflow_first_party() { + setup(); + + let root = PathBuf::from("./resources/test/airflow"); + let source_file = root.join("airflow/api/common/mark_tasks.py"); + + let result = resolve_options( + source_file, + "airflow.jobs.scheduler_job_runner", + root.clone(), + ResolverOptions { + venv_path: Some(root), + venv: Some(PathBuf::from("venv")), + ..Default::default() + }, + ); + + assert_debug_snapshot!(result); + } + + #[test] + fn airflow_stub_file() { + setup(); + + let root = PathBuf::from("./resources/test/airflow"); + let source_file = root.join("airflow/api/common/mark_tasks.py"); + + let result = resolve_options( + source_file, + "airflow.compat.functools", + root.clone(), + ResolverOptions { + venv_path: Some(root), + venv: Some(PathBuf::from("venv")), + ..Default::default() + }, + ); + + assert_debug_snapshot!(result); + } + + #[test] + fn airflow_namespace_package() { + setup(); + + let root = PathBuf::from("./resources/test/airflow"); + let source_file = root.join("airflow/api/common/mark_tasks.py"); + + let result = resolve_options( + source_file, + "airflow.providers.google.cloud.hooks.gcs", + root.clone(), + ResolverOptions { + venv_path: Some(root), + venv: Some(PathBuf::from("venv")), + ..Default::default() + }, + ); + + assert_debug_snapshot!(result); + } + + #[test] + fn airflow_third_party() { + setup(); + + let root = PathBuf::from("./resources/test/airflow"); + let source_file = root.join("airflow/api/common/mark_tasks.py"); + + let result = resolve_options( + source_file, + "sqlalchemy.orm", + root.clone(), + ResolverOptions { + venv_path: Some(root), + venv: Some(PathBuf::from("venv")), + ..Default::default() + }, + ); + + assert_debug_snapshot!(result); + } +} diff --git a/crates/ruff_python_resolver/src/resolver.rs b/crates/ruff_python_resolver/src/resolver.rs index 3cb3eca260..e175edff44 100644 --- a/crates/ruff_python_resolver/src/resolver.rs +++ b/crates/ruff_python_resolver/src/resolver.rs @@ -723,7 +723,7 @@ fn resolve_import_strict( /// 3. If a stub file was found, find the "best" match for the import, disallowing stub files. /// 4. If the import wasn't resolved, try to resolve it in the parent directory, then the parent's /// parent, and so on, until the import root is reached. -fn resolve_import( +pub(crate) fn resolve_import( source_file: &Path, execution_environment: &ExecutionEnvironment, module_descriptor: &ImportModuleDescriptor, @@ -780,888 +780,3 @@ fn resolve_import( ImportResult::not_found() } - -#[cfg(test)] -mod tests { - use insta::assert_debug_snapshot; - use std::fs::{create_dir_all, File}; - use std::io::{self, Write}; - use std::path::{Path, PathBuf}; - - use log::debug; - use tempfile::TempDir; - - use crate::config::Config; - use crate::execution_environment::ExecutionEnvironment; - use crate::host; - use crate::import_result::{ImportResult, ImportType}; - use crate::module_descriptor::ImportModuleDescriptor; - use crate::python_platform::PythonPlatform; - use crate::python_version::PythonVersion; - use crate::resolver::resolve_import; - - /// Create a file at the given path with the given content. - fn create(path: PathBuf, content: &str) -> io::Result { - if let Some(parent) = path.parent() { - create_dir_all(parent)?; - } - let mut f = File::create(&path)?; - f.write_all(content.as_bytes())?; - f.sync_all()?; - - Ok(path) - } - - /// Create an empty file at the given path. - fn empty(path: PathBuf) -> io::Result { - create(path, "") - } - - /// Create a partial `py.typed` file at the given path. - fn partial(path: PathBuf) -> io::Result { - create(path, "partial\n") - } - - /// Create a `py.typed` file at the given path. - fn typed(path: PathBuf) -> io::Result { - create(path, "# typed") - } - - #[derive(Debug, Default)] - struct ResolverOptions { - extra_paths: Vec, - library: Option, - stub_path: Option, - typeshed_path: Option, - venv_path: Option, - venv: Option, - } - - fn resolve_options( - source_file: impl AsRef, - name: &str, - root: impl Into, - options: ResolverOptions, - ) -> ImportResult { - let ResolverOptions { - extra_paths, - library, - stub_path, - typeshed_path, - venv_path, - venv, - } = options; - - let execution_environment = ExecutionEnvironment { - root: root.into(), - python_version: PythonVersion::Py37, - python_platform: PythonPlatform::Darwin, - extra_paths, - }; - - let module_descriptor = ImportModuleDescriptor { - leading_dots: name.chars().take_while(|c| *c == '.').count(), - name_parts: name - .chars() - .skip_while(|c| *c == '.') - .collect::() - .split('.') - .map(std::string::ToString::to_string) - .collect(), - imported_symbols: Vec::new(), - }; - - let config = Config { - typeshed_path, - stub_path, - venv_path, - venv, - }; - - let host = host::StaticHost::new(if let Some(library) = library { - vec![library] - } else { - Vec::new() - }); - - resolve_import( - source_file.as_ref(), - &execution_environment, - &module_descriptor, - &config, - &host, - ) - } - - fn setup() { - env_logger::builder().is_test(true).try_init().ok(); - } - - #[test] - fn partial_stub_file_exists() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - partial(library.join("myLib-stubs/py.typed"))?; - let partial_stub_pyi = empty(library.join("myLib-stubs").join("partialStub.pyi"))?; - let partial_stub_py = empty(library.join("myLib/partialStub.py"))?; - - let result = resolve_options( - partial_stub_py, - "myLib.partialStub", - root, - ResolverOptions { - library: Some(library), - ..Default::default() - }, - ); - - assert!(result.is_import_found); - assert!(result.is_stub_file); - assert_eq!(result.import_type, ImportType::ThirdParty); - assert_eq!( - result.resolved_paths, - // TODO(charlie): Pyright matches on `libraryRoot, 'myLib', 'partialStub.pyi'` here. - // But that file doesn't exist. There's some kind of transform. - vec![PathBuf::new(), partial_stub_pyi] - ); - - Ok(()) - } - - #[test] - fn partial_stub_init_exists() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - partial(library.join("myLib-stubs/py.typed"))?; - let partial_stub_init_pyi = empty(library.join("myLib-stubs/__init__.pyi"))?; - let partial_stub_init_py = empty(library.join("myLib/__init__.py"))?; - - let result = resolve_options( - partial_stub_init_py, - "myLib", - root, - ResolverOptions { - library: Some(library), - ..Default::default() - }, - ); - - assert!(result.is_import_found); - assert!(result.is_stub_file); - assert_eq!(result.import_type, ImportType::ThirdParty); - assert_eq!( - result.resolved_paths, - // TODO(charlie): Pyright matches on `libraryRoot, 'myLib', '__init__.pyi'` here. - // But that file doesn't exist. There's some kind of transform. - vec![partial_stub_init_pyi] - ); - - Ok(()) - } - - #[test] - fn side_by_side_files() -> io::Result<()> { - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - partial(library.join("myLib-stubs/py.typed"))?; - empty(library.join("myLib/partialStub.pyi"))?; - empty(library.join("myLib/partialStub.py"))?; - empty(library.join("myLib/partialStub2.py"))?; - let my_file = empty(root.join("myFile.py"))?; - let side_by_side_stub_file = empty(library.join("myLib-stubs/partialStub.pyi"))?; - let partial_stub_file = empty(library.join("myLib-stubs/partialStub2.pyi"))?; - - // Stub package wins over original package (per PEP 561 rules). - let side_by_side_result = resolve_options( - &my_file, - "myLib.partialStub", - root, - ResolverOptions { - library: Some(library.clone()), - ..Default::default() - }, - ); - assert!(side_by_side_result.is_import_found); - assert!(side_by_side_result.is_stub_file); - assert_eq!( - side_by_side_result.resolved_paths, - vec![PathBuf::new(), side_by_side_stub_file] - ); - - // Side by side stub doesn't completely disable partial stub. - let partial_stub_result = resolve_options( - &my_file, - "myLib.partialStub2", - root, - ResolverOptions { - library: Some(library), - ..Default::default() - }, - ); - assert!(partial_stub_result.is_import_found); - assert!(partial_stub_result.is_stub_file); - assert_eq!( - partial_stub_result.resolved_paths, - vec![PathBuf::new(), partial_stub_file] - ); - - Ok(()) - } - - #[test] - fn stub_package() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - empty(library.join("myLib-stubs/stub.pyi"))?; - empty(library.join("myLib-stubs/__init__.pyi"))?; - let partial_stub_py = empty(library.join("myLib/partialStub.py"))?; - - let result = resolve_options( - partial_stub_py, - "myLib.partialStub", - root, - ResolverOptions { - library: Some(library), - ..Default::default() - }, - ); - - // If fully typed stub package exists, that wins over the real package. - assert!(!result.is_import_found); - - Ok(()) - } - - #[test] - fn stub_namespace_package() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - empty(library.join("myLib-stubs/stub.pyi"))?; - let partial_stub_py = empty(library.join("myLib/partialStub.py"))?; - - let result = resolve_options( - partial_stub_py.clone(), - "myLib.partialStub", - root, - ResolverOptions { - library: Some(library), - ..Default::default() - }, - ); - - // If fully typed stub package exists, that wins over the real package. - assert!(result.is_import_found); - assert!(!result.is_stub_file); - assert_eq!(result.resolved_paths, vec![PathBuf::new(), partial_stub_py]); - - Ok(()) - } - - #[test] - fn stub_in_typing_folder_over_partial_stub_package() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - let typing_folder = root.join("typing"); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - partial(library.join("myLib-stubs/py.typed"))?; - empty(library.join("myLib-stubs/__init__.pyi"))?; - let my_lib_pyi = empty(typing_folder.join("myLib.pyi"))?; - let my_lib_init_py = empty(library.join("myLib/__init__.py"))?; - - let result = resolve_options( - my_lib_init_py, - "myLib", - root, - ResolverOptions { - library: Some(library), - stub_path: Some(typing_folder), - ..Default::default() - }, - ); - - // If the package exists in typing folder, that gets picked up first (so we resolve to - // `myLib.pyi`). - assert!(result.is_import_found); - assert!(result.is_stub_file); - assert_eq!(result.resolved_paths, vec![my_lib_pyi]); - - Ok(()) - } - - #[test] - fn partial_stub_package_in_typing_folder() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - let typing_folder = root.join("typing"); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - partial(typing_folder.join("myLib-stubs/py.typed"))?; - let my_lib_stubs_init_pyi = empty(typing_folder.join("myLib-stubs/__init__.pyi"))?; - let my_lib_init_py = empty(library.join("myLib/__init__.py"))?; - - let result = resolve_options( - my_lib_init_py, - "myLib", - root, - ResolverOptions { - library: Some(library), - stub_path: Some(typing_folder), - ..Default::default() - }, - ); - - // If the package exists in typing folder, that gets picked up first (so we resolve to - // `myLib.pyi`). - assert!(result.is_import_found); - assert!(result.is_stub_file); - assert_eq!(result.resolved_paths, vec![my_lib_stubs_init_pyi]); - - Ok(()) - } - - #[test] - fn typeshed_folder() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - let typeshed_folder = root.join("ts"); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - empty(typeshed_folder.join("stubs/myLibPackage/myLib.pyi"))?; - partial(library.join("myLib-stubs/py.typed"))?; - let my_lib_stubs_init_pyi = empty(library.join("myLib-stubs/__init__.pyi"))?; - let my_lib_init_py = empty(library.join("myLib/__init__.py"))?; - - let result = resolve_options( - my_lib_init_py, - "myLib", - root, - ResolverOptions { - library: Some(library), - typeshed_path: Some(typeshed_folder), - ..Default::default() - }, - ); - - // Stub packages win over typeshed. - assert!(result.is_import_found); - assert!(result.is_stub_file); - assert_eq!(result.resolved_paths, vec![my_lib_stubs_init_pyi]); - - Ok(()) - } - - #[test] - fn py_typed_file() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - empty(library.join("myLib/__init__.py"))?; - partial(library.join("myLib-stubs/py.typed"))?; - let partial_stub_init_pyi = empty(library.join("myLib-stubs/__init__.pyi"))?; - let package_py_typed = typed(library.join("myLib/py.typed"))?; - - let result = resolve_options( - package_py_typed, - "myLib", - root, - ResolverOptions { - library: Some(library), - ..Default::default() - }, - ); - - // Partial stub package always overrides original package. - assert!(result.is_import_found); - assert!(result.is_stub_file); - assert_eq!(result.resolved_paths, vec![partial_stub_init_pyi]); - - Ok(()) - } - - #[test] - fn py_typed_library() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - let typeshed_folder = root.join("ts"); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - typed(library.join("os/py.typed"))?; - let init_py = empty(library.join("os/__init__.py"))?; - let typeshed_init_pyi = empty(typeshed_folder.join("stubs/os/os/__init__.pyi"))?; - - let result = resolve_options( - typeshed_init_pyi, - "os", - root, - ResolverOptions { - library: Some(library), - typeshed_path: Some(typeshed_folder), - ..Default::default() - }, - ); - - assert!(result.is_import_found); - assert_eq!(result.resolved_paths, vec![init_py]); - - Ok(()) - } - - #[test] - fn non_py_typed_library() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - let typeshed_folder = root.join("ts"); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - empty(library.join("os/__init__.py"))?; - let typeshed_init_pyi = empty(typeshed_folder.join("stubs/os/os/__init__.pyi"))?; - - let result = resolve_options( - typeshed_init_pyi.clone(), - "os", - root, - ResolverOptions { - library: Some(library), - typeshed_path: Some(typeshed_folder), - ..Default::default() - }, - ); - - assert!(result.is_import_found); - assert_eq!(result.import_type, ImportType::ThirdParty); - assert_eq!(result.resolved_paths, vec![typeshed_init_pyi]); - - Ok(()) - } - - #[test] - fn import_side_by_side_file_root() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let file1 = empty(root.join("file1.py"))?; - let file2 = empty(root.join("file2.py"))?; - - let result = resolve_options(file2, "file1", root, ResolverOptions::default()); - - assert!(result.is_import_found); - assert_eq!(result.import_type, ImportType::Local); - assert_eq!(result.resolved_paths, vec![file1]); - - Ok(()) - } - - #[test] - fn import_side_by_side_file_sub_folder() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let test_init = empty(root.join("test/__init__.py"))?; - let test_file1 = empty(root.join("test/file1.py"))?; - let test_file2 = empty(root.join("test/file2.py"))?; - - let result = resolve_options(test_file2, "test.file1", root, ResolverOptions::default()); - - assert!(result.is_import_found); - assert_eq!(result.import_type, ImportType::Local); - assert_eq!(result.resolved_paths, vec![test_init, test_file1]); - - Ok(()) - } - - #[test] - fn import_side_by_side_file_sub_under_src_folder() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let nested_init = empty(root.join("src/nested/__init__.py"))?; - let nested_file1 = empty(root.join("src/nested/file1.py"))?; - let nested_file2 = empty(root.join("src/nested/file2.py"))?; - - let result = resolve_options( - nested_file2, - "nested.file1", - root, - ResolverOptions::default(), - ); - - assert!(result.is_import_found); - assert_eq!(result.import_type, ImportType::Local); - assert_eq!(result.resolved_paths, vec![nested_init, nested_file1]); - - Ok(()) - } - - #[test] - fn import_file_sub_under_containing_folder() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let nested_file1 = empty(root.join("src/nested/file1.py"))?; - let nested_file2 = empty(root.join("src/nested/nested2/file2.py"))?; - - let result = resolve_options(nested_file2, "file1", root, ResolverOptions::default()); - - assert!(result.is_import_found); - assert_eq!(result.import_type, ImportType::Local); - assert_eq!(result.resolved_paths, vec![nested_file1]); - - Ok(()) - } - - #[test] - fn import_side_by_side_file_sub_under_lib_folder() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - empty(library.join("myLib/file1.py"))?; - let file2 = empty(library.join("myLib/file2.py"))?; - - let result = resolve_options(file2, "file1", root, ResolverOptions::default()); - - debug!("result: {:?}", result); - - assert!(!result.is_import_found); - - Ok(()) - } - - #[test] - fn nested_namespace_package_1() -> io::Result<()> { - // See: https://github.com/microsoft/pyright/issues/5089. - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let file = empty(root.join("package1/a/b/c/d.py"))?; - let package1_init = empty(root.join("package1/a/__init__.py"))?; - let package2_init = empty(root.join("package2/a/__init__.py"))?; - - let package1 = root.join("package1"); - let package2 = root.join("package2"); - - let result = resolve_options( - package2_init, - "a.b.c.d", - root, - ResolverOptions { - extra_paths: vec![package1, package2], - ..Default::default() - }, - ); - - assert!(result.is_import_found); - assert_eq!(result.import_type, ImportType::Local); - assert_eq!( - result.resolved_paths, - vec![package1_init, PathBuf::new(), PathBuf::new(), file] - ); - - Ok(()) - } - - #[test] - fn nested_namespace_package_2() -> io::Result<()> { - // See: https://github.com/microsoft/pyright/issues/5089. - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let file = empty(root.join("package1/a/b/c/d.py"))?; - let package1_init = empty(root.join("package1/a/b/c/__init__.py"))?; - let package2_init = empty(root.join("package2/a/b/c/__init__.py"))?; - - let package1 = root.join("package1"); - let package2 = root.join("package2"); - - let result = resolve_options( - package2_init, - "a.b.c.d", - root, - ResolverOptions { - extra_paths: vec![package1, package2], - ..Default::default() - }, - ); - - assert!(result.is_import_found); - assert_eq!(result.import_type, ImportType::Local); - assert_eq!( - result.resolved_paths, - vec![PathBuf::new(), PathBuf::new(), package1_init, file] - ); - - Ok(()) - } - - #[test] - fn nested_namespace_package_3() -> io::Result<()> { - // See: https://github.com/microsoft/pyright/issues/5089. - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - empty(root.join("package1/a/b/c/d.py"))?; - let package2_init = empty(root.join("package2/a/__init__.py"))?; - - let package1 = root.join("package1"); - let package2 = root.join("package2"); - - let result = resolve_options( - package2_init, - "a.b.c.d", - root, - ResolverOptions { - extra_paths: vec![package1, package2], - ..Default::default() - }, - ); - - assert!(!result.is_import_found); - - Ok(()) - } - - #[test] - fn nested_namespace_package_4() -> io::Result<()> { - // See: https://github.com/microsoft/pyright/issues/5089. - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - empty(root.join("package1/a/b/__init__.py"))?; - empty(root.join("package1/a/b/c.py"))?; - empty(root.join("package2/a/__init__.py"))?; - let package2_a_b_init = empty(root.join("package2/a/b/__init__.py"))?; - - let package1 = root.join("package1"); - let package2 = root.join("package2"); - - let result = resolve_options( - package2_a_b_init, - "a.b.c", - root, - ResolverOptions { - extra_paths: vec![package1, package2], - ..Default::default() - }, - ); - - assert!(!result.is_import_found); - - Ok(()) - } - - // New tests, don't exist upstream. - #[test] - fn relative_import_side_by_side_file_root() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let file1 = empty(root.join("file1.py"))?; - let file2 = empty(root.join("file2.py"))?; - - let result = resolve_options(file2, ".file1", root, ResolverOptions::default()); - - assert!(result.is_import_found); - assert_eq!(result.import_type, ImportType::Local); - assert_eq!(result.resolved_paths, vec![file1]); - - Ok(()) - } - - #[test] - fn invalid_relative_import_side_by_side_file_root() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - empty(root.join("file1.py"))?; - let file2 = empty(root.join("file2.py"))?; - - let result = resolve_options(file2, "..file1", root, ResolverOptions::default()); - - assert!(!result.is_import_found); - - Ok(()) - } - - #[test] - fn airflow_standard_library() { - setup(); - - let root = PathBuf::from("./resources/test/airflow"); - let source_file = root.join("airflow/api/common/mark_tasks.py"); - - let result = resolve_options( - source_file, - "os", - root.clone(), - ResolverOptions { - venv_path: Some(root), - venv: Some(PathBuf::from("venv")), - ..Default::default() - }, - ); - - assert_debug_snapshot!(result); - } - - #[test] - fn airflow_first_party() { - setup(); - - let root = PathBuf::from("./resources/test/airflow"); - let source_file = root.join("airflow/api/common/mark_tasks.py"); - - let result = resolve_options( - source_file, - "airflow.jobs.scheduler_job_runner", - root.clone(), - ResolverOptions { - venv_path: Some(root), - venv: Some(PathBuf::from("venv")), - ..Default::default() - }, - ); - - assert_debug_snapshot!(result); - } - - #[test] - fn airflow_stub_file() { - setup(); - - let root = PathBuf::from("./resources/test/airflow"); - let source_file = root.join("airflow/api/common/mark_tasks.py"); - - let result = resolve_options( - source_file, - "airflow.compat.functools", - root.clone(), - ResolverOptions { - venv_path: Some(root), - venv: Some(PathBuf::from("venv")), - ..Default::default() - }, - ); - - assert_debug_snapshot!(result); - } - - #[test] - fn airflow_namespace_package() { - setup(); - - let root = PathBuf::from("./resources/test/airflow"); - let source_file = root.join("airflow/api/common/mark_tasks.py"); - - let result = resolve_options( - source_file, - "airflow.providers.google.cloud.hooks.gcs", - root.clone(), - ResolverOptions { - venv_path: Some(root), - venv: Some(PathBuf::from("venv")), - ..Default::default() - }, - ); - - assert_debug_snapshot!(result); - } - - #[test] - fn airflow_third_party() { - setup(); - - let root = PathBuf::from("./resources/test/airflow"); - let source_file = root.join("airflow/api/common/mark_tasks.py"); - - let result = resolve_options( - source_file, - "sqlalchemy.orm", - root.clone(), - ResolverOptions { - venv_path: Some(root), - venv: Some(PathBuf::from("venv")), - ..Default::default() - }, - ); - - assert_debug_snapshot!(result); - } -} diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_first_party.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_first_party.snap similarity index 94% rename from crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_first_party.snap rename to crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_first_party.snap index d2197f6cab..864f3e1a12 100644 --- a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_first_party.snap +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_first_party.snap @@ -1,5 +1,5 @@ --- -source: crates/ruff_python_resolver/src/resolver.rs +source: crates/ruff_python_resolver/src/lib.rs expression: result --- ImportResult { diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_namespace_package.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_namespace_package.snap similarity index 95% rename from crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_namespace_package.snap rename to crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_namespace_package.snap index 15954d4c98..d8f9d08207 100644 --- a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_namespace_package.snap +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_namespace_package.snap @@ -1,5 +1,5 @@ --- -source: crates/ruff_python_resolver/src/resolver.rs +source: crates/ruff_python_resolver/src/lib.rs expression: result --- ImportResult { diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_standard_library.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_standard_library.snap similarity index 92% rename from crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_standard_library.snap rename to crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_standard_library.snap index 3262ce7f6f..8429372b42 100644 --- a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_standard_library.snap +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_standard_library.snap @@ -1,5 +1,5 @@ --- -source: crates/ruff_python_resolver/src/resolver.rs +source: crates/ruff_python_resolver/src/lib.rs expression: result --- ImportResult { diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_stub_file.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_stub_file.snap similarity index 97% rename from crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_stub_file.snap rename to crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_stub_file.snap index 37f26c80d7..faf1604f1b 100644 --- a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_stub_file.snap +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_stub_file.snap @@ -1,5 +1,5 @@ --- -source: crates/ruff_python_resolver/src/resolver.rs +source: crates/ruff_python_resolver/src/lib.rs expression: result --- ImportResult { diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_third_party.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_third_party.snap similarity index 97% rename from crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_third_party.snap rename to crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_third_party.snap index ed13cadb03..488711b8f1 100644 --- a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__resolver__tests__airflow_third_party.snap +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_third_party.snap @@ -1,5 +1,5 @@ --- -source: crates/ruff_python_resolver/src/resolver.rs +source: crates/ruff_python_resolver/src/lib.rs expression: result --- ImportResult { From 864f50a3a43b4e332fb62fbfe56016ef36822af9 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 28 Jun 2023 14:06:17 -0400 Subject: [PATCH 268/447] Remove all `unwrap` calls from the resolver (#5426) --- .../src/implicit_imports.rs | 9 +--- .../ruff_python_resolver/src/native_module.rs | 14 ++++- crates/ruff_python_resolver/src/resolver.rs | 20 ++----- crates/ruff_python_resolver/src/search.rs | 54 ++++++++++--------- 4 files changed, 47 insertions(+), 50 deletions(-) diff --git a/crates/ruff_python_resolver/src/implicit_imports.rs b/crates/ruff_python_resolver/src/implicit_imports.rs index 926c947901..76191b87b6 100644 --- a/crates/ruff_python_resolver/src/implicit_imports.rs +++ b/crates/ruff_python_resolver/src/implicit_imports.rs @@ -55,14 +55,7 @@ pub(crate) fn find(dir_path: &Path, exclusions: &[&Path]) -> BTreeMap bool { @@ -36,6 +36,18 @@ pub(crate) fn is_native_module_file_name(module_name: &str, file_name: &Path) -> native_module_name(file_name) == Some(module_name) } +/// Find the native module within the namespace package at the given path. +pub(crate) fn find_native_module(dir_path: &Path) -> Option { + let module_name = dir_path.file_name()?.to_str()?; + dir_path + .read_dir() + .ok()? + .flatten() + .filter(|entry| entry.file_type().map_or(false, |ft| ft.is_file())) + .map(|entry| entry.path()) + .find(|path| is_native_module_file_name(module_name, path)) +} + #[cfg(test)] mod tests { use std::path::PathBuf; diff --git a/crates/ruff_python_resolver/src/resolver.rs b/crates/ruff_python_resolver/src/resolver.rs index e175edff44..d10b7fbe83 100644 --- a/crates/ruff_python_resolver/src/resolver.rs +++ b/crates/ruff_python_resolver/src/resolver.rs @@ -1,7 +1,6 @@ //! Resolves Python imports to their corresponding files on disk. use std::collections::BTreeMap; -use std::ffi::OsStr; use std::path::{Path, PathBuf}; use log::debug; @@ -139,21 +138,10 @@ fn resolve_module_descriptor( } else { if allow_native_lib && dir_path.is_dir() { // We couldn't find a `.py[i]` file; search for a native library. - if let Some(module_name) = dir_path.file_name().and_then(OsStr::to_str) { - if let Some(native_lib_path) = dir_path - .read_dir() - .unwrap() - .flatten() - .filter(|entry| entry.file_type().map_or(false, |ft| ft.is_file())) - .map(|entry| entry.path()) - .find(|path| { - native_module::is_native_module_file_name(module_name, path) - }) - { - debug!("Resolved import with file: {native_lib_path:?}"); - is_native_lib = true; - resolved_paths.push(native_lib_path); - } + if let Some(native_lib_path) = native_module::find_native_module(&dir_path) { + debug!("Resolved import with file: {native_lib_path:?}"); + is_native_lib = true; + resolved_paths.push(native_lib_path); } } diff --git a/crates/ruff_python_resolver/src/search.rs b/crates/ruff_python_resolver/src/search.rs index 01c6114ae4..0539c8296a 100644 --- a/crates/ruff_python_resolver/src/search.rs +++ b/crates/ruff_python_resolver/src/search.rs @@ -2,8 +2,8 @@ use std::collections::HashMap; use std::ffi::OsStr; -use std::fs; use std::path::{Path, PathBuf}; +use std::{fs, io}; use log::debug; @@ -94,9 +94,9 @@ fn find_site_packages_path( Some(default_dir.join(SITE_PACKAGES)) } -fn find_paths_from_pth_files(parent_dir: &Path) -> Vec { - fs::read_dir(parent_dir) - .unwrap() +fn find_paths_from_pth_files(parent_dir: &Path) -> io::Result + '_> { + Ok(parent_dir + .read_dir()? .flatten() .filter(|entry| { // Collect all *.pth files. @@ -130,8 +130,7 @@ fn find_paths_from_pth_files(parent_dir: &Path) -> Vec { } } None - }) - .collect() + })) } /// Find the Python search paths for the given virtual environment. @@ -144,7 +143,9 @@ fn find_python_search_paths(config: &Config, host: &Host) -> V let lib_path = venv_path.join(venv).join(lib_name); if let Some(site_packages_path) = find_site_packages_path(&lib_path, None) { // Add paths from any `.pth` files in each of the `site-packages` directories. - found_paths.extend(find_paths_from_pth_files(&site_packages_path)); + if let Ok(pth_paths) = find_paths_from_pth_files(&site_packages_path) { + found_paths.extend(pth_paths); + } // Add the `site-packages` directory to the search path. found_paths.push(site_packages_path); @@ -212,45 +213,48 @@ fn typeshed_subdirectory( /// Generate a map from PyPI-registered package name to a list of paths /// containing the package's stubs. -fn build_typeshed_third_party_package_map(third_party_dir: &Path) -> HashMap> { +fn build_typeshed_third_party_package_map( + third_party_dir: &Path, +) -> io::Result>> { let mut package_map = HashMap::new(); // Iterate over every directory. - for outer_entry in fs::read_dir(third_party_dir).unwrap() { - let outer_entry = outer_entry.unwrap(); - if outer_entry.file_type().unwrap().is_dir() { + for outer_entry in fs::read_dir(third_party_dir)? { + let outer_entry = outer_entry?; + if outer_entry.file_type()?.is_dir() { // Iterate over any subdirectory children. - for inner_entry in fs::read_dir(outer_entry.path()).unwrap() { - let inner_entry = inner_entry.unwrap(); + for inner_entry in fs::read_dir(outer_entry.path())? { + let inner_entry = inner_entry?; - if inner_entry.file_type().unwrap().is_dir() { + if inner_entry.file_type()?.is_dir() { package_map .entry(inner_entry.file_name().to_string_lossy().to_string()) .or_insert_with(Vec::new) .push(outer_entry.path()); - } else if inner_entry.file_type().unwrap().is_file() { + } else if inner_entry.file_type()?.is_file() { if inner_entry .path() .extension() .map_or(false, |extension| extension == "pyi") { - let stripped_file_name = inner_entry + if let Some(stripped_file_name) = inner_entry .path() .file_stem() - .unwrap() - .to_string_lossy() - .to_string(); - package_map - .entry(stripped_file_name) - .or_insert_with(Vec::new) - .push(outer_entry.path()); + .and_then(std::ffi::OsStr::to_str) + .map(std::string::ToString::to_string) + { + package_map + .entry(stripped_file_name) + .or_insert_with(Vec::new) + .push(outer_entry.path()); + } } } } } } - package_map + Ok(package_map) } /// Determine the current `typeshed` subdirectory for a third-party package. @@ -260,7 +264,7 @@ pub(crate) fn third_party_typeshed_package_paths( host: &Host, ) -> Option> { let typeshed_path = typeshed_subdirectory(false, config, host)?; - let package_paths = build_typeshed_third_party_package_map(&typeshed_path); + let package_paths = build_typeshed_third_party_package_map(&typeshed_path).ok()?; let first_name_part = module_descriptor.name_parts.first().map(String::as_str)?; package_paths.get(first_name_part).cloned() } From 0e12eb3071c39689711c3b1bc6f79b26c05fd0be Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 28 Jun 2023 14:16:39 -0400 Subject: [PATCH 269/447] Add a snapshot test for native module resolution (#5423) --- .../_watchdog_fsevents.cpython-311-darwin.so | 1 + .../site-packages/orjson/__init__.py | 0 .../site-packages/orjson/__init__.pyi | 0 .../orjson/orjson.cpython-311-darwin.so | 1 + .../python3.11/site-packages/orjson/py.typed | 0 crates/ruff_python_resolver/src/lib.rs | 42 +++++++++ .../ruff_python_resolver/src/native_module.rs | 14 +-- crates/ruff_python_resolver/src/resolver.rs | 42 +++++---- ...tests__airflow_explicit_native_module.snap | 29 +++++++ ...tests__airflow_implicit_native_module.snap | 85 +++++++++++++++++++ 10 files changed, 190 insertions(+), 24 deletions(-) create mode 100644 crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/_watchdog_fsevents.cpython-311-darwin.so create mode 100644 crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/__init__.py create mode 100644 crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/__init__.pyi create mode 100644 crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so create mode 100644 crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/py.typed create mode 100644 crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_explicit_native_module.snap create mode 100644 crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_implicit_native_module.snap diff --git a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/_watchdog_fsevents.cpython-311-darwin.so b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/_watchdog_fsevents.cpython-311-darwin.so new file mode 100644 index 0000000000..0dff738002 --- /dev/null +++ b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/_watchdog_fsevents.cpython-311-darwin.so @@ -0,0 +1 @@ +# Empty file included to support filesystem-based resolver tests. diff --git a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/__init__.pyi b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/__init__.pyi new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so new file mode 100644 index 0000000000..0dff738002 --- /dev/null +++ b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so @@ -0,0 +1 @@ +# Empty file included to support filesystem-based resolver tests. diff --git a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/py.typed b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/ruff_python_resolver/src/lib.rs b/crates/ruff_python_resolver/src/lib.rs index 505c623099..c4ef690a39 100644 --- a/crates/ruff_python_resolver/src/lib.rs +++ b/crates/ruff_python_resolver/src/lib.rs @@ -896,4 +896,46 @@ mod tests { assert_debug_snapshot!(result); } + + #[test] + fn airflow_explicit_native_module() { + setup(); + + let root = PathBuf::from("./resources/test/airflow"); + let source_file = root.join("airflow/api/common/mark_tasks.py"); + + let result = resolve_options( + source_file, + "_watchdog_fsevents", + root.clone(), + ResolverOptions { + venv_path: Some(root), + venv: Some(PathBuf::from("venv")), + ..Default::default() + }, + ); + + assert_debug_snapshot!(result); + } + + #[test] + fn airflow_implicit_native_module() { + setup(); + + let root = PathBuf::from("./resources/test/airflow"); + let source_file = root.join("airflow/api/common/mark_tasks.py"); + + let result = resolve_options( + source_file, + "orjson", + root.clone(), + ResolverOptions { + venv_path: Some(root), + venv: Some(PathBuf::from("venv")), + ..Default::default() + }, + ); + + assert_debug_snapshot!(result); + } } diff --git a/crates/ruff_python_resolver/src/native_module.rs b/crates/ruff_python_resolver/src/native_module.rs index 5601cbf12e..065473d6a5 100644 --- a/crates/ruff_python_resolver/src/native_module.rs +++ b/crates/ruff_python_resolver/src/native_module.rs @@ -1,6 +1,7 @@ //! Support for native Python extension modules. use std::ffi::OsStr; +use std::io; use std::path::{Path, PathBuf}; /// Returns `true` if the given file extension is that of a native module. @@ -37,15 +38,16 @@ pub(crate) fn is_native_module_file_name(module_name: &str, file_name: &Path) -> } /// Find the native module within the namespace package at the given path. -pub(crate) fn find_native_module(dir_path: &Path) -> Option { - let module_name = dir_path.file_name()?.to_str()?; - dir_path - .read_dir() - .ok()? +pub(crate) fn find_native_module( + module_name: &str, + dir_path: &Path, +) -> io::Result> { + Ok(dir_path + .read_dir()? .flatten() .filter(|entry| entry.file_type().map_or(false, |ft| ft.is_file())) .map(|entry| entry.path()) - .find(|path| is_native_module_file_name(module_name, path)) + .find(|path| is_native_module_file_name(module_name, path))) } #[cfg(test)] diff --git a/crates/ruff_python_resolver/src/resolver.rs b/crates/ruff_python_resolver/src/resolver.rs index d10b7fbe83..4749f5b7fa 100644 --- a/crates/ruff_python_resolver/src/resolver.rs +++ b/crates/ruff_python_resolver/src/resolver.rs @@ -1,6 +1,7 @@ //! Resolves Python imports to their corresponding files on disk. use std::collections::BTreeMap; +use std::ffi::OsStr; use std::path::{Path, PathBuf}; use log::debug; @@ -66,22 +67,22 @@ fn resolve_module_descriptor( let is_last_part = i == module_descriptor.name_parts.len() - 1; // Extend the directory path with the next segment. - if use_stub_package && is_first_part { - dir_path = dir_path.join(format!("{part}-stubs")); + let module_dir_path = if use_stub_package && is_first_part { is_stub_package = true; + dir_path.join(format!("{part}-stubs")) } else { - dir_path = dir_path.join(part); - } + dir_path.join(part) + }; - let found_directory = dir_path.is_dir(); + let found_directory = module_dir_path.is_dir(); if found_directory { if is_first_part { - package_directory = Some(dir_path.clone()); + package_directory = Some(module_dir_path.clone()); } // Look for an `__init__.py[i]` in the directory. - let py_file_path = dir_path.join("__init__.py"); - let pyi_file_path = dir_path.join("__init__.pyi"); + let py_file_path = module_dir_path.join("__init__.py"); + let pyi_file_path = module_dir_path.join("__init__.pyi"); is_init_file_present = false; if allow_pyi && pyi_file_path.is_file() { @@ -99,7 +100,7 @@ fn resolve_module_descriptor( if look_for_py_typed { py_typed_info = - py_typed_info.or_else(|| py_typed::get_py_typed_info(&dir_path)); + py_typed_info.or_else(|| py_typed::get_py_typed_info(&module_dir_path)); } // We haven't reached the end of the import, and we found a matching directory. @@ -110,12 +111,14 @@ fn resolve_module_descriptor( is_namespace_package = true; py_typed_info = None; } + + dir_path = module_dir_path; continue; } if is_init_file_present { implicit_imports = - implicit_imports::find(&dir_path, &[&py_file_path, &pyi_file_path]); + implicit_imports::find(&module_dir_path, &[&py_file_path, &pyi_file_path]); break; } } @@ -123,8 +126,8 @@ fn resolve_module_descriptor( // We couldn't find a matching directory, or the directory didn't contain an // `__init__.py[i]` file. Look for an `.py[i]` file with the same name as the // segment, in lieu of a directory. - let py_file_path = dir_path.with_extension("py"); - let pyi_file_path = dir_path.with_extension("pyi"); + let py_file_path = module_dir_path.with_extension("py"); + let pyi_file_path = module_dir_path.with_extension("pyi"); if allow_pyi && pyi_file_path.is_file() { debug!("Resolved import with file: {pyi_file_path:?}"); @@ -138,10 +141,14 @@ fn resolve_module_descriptor( } else { if allow_native_lib && dir_path.is_dir() { // We couldn't find a `.py[i]` file; search for a native library. - if let Some(native_lib_path) = native_module::find_native_module(&dir_path) { - debug!("Resolved import with file: {native_lib_path:?}"); - is_native_lib = true; - resolved_paths.push(native_lib_path); + if let Some(module_name) = module_dir_path.file_name().and_then(OsStr::to_str) { + if let Ok(Some(native_lib_path)) = + native_module::find_native_module(module_name, &dir_path) + { + debug!("Resolved import with file: {native_lib_path:?}"); + is_native_lib = true; + resolved_paths.push(native_lib_path); + } } } @@ -153,10 +160,9 @@ fn resolve_module_descriptor( implicit_imports::find(&dir_path, &[&py_file_path, &pyi_file_path]); is_namespace_package = true; } - } else if is_native_lib { - debug!("Did not find file {py_file_path:?} or {pyi_file_path:?}"); } } + break; } } diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_explicit_native_module.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_explicit_native_module.snap new file mode 100644 index 0000000000..6ab4f67891 --- /dev/null +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_explicit_native_module.snap @@ -0,0 +1,29 @@ +--- +source: crates/ruff_python_resolver/src/lib.rs +expression: result +--- +ImportResult { + is_relative: false, + is_import_found: true, + is_partly_resolved: false, + is_namespace_package: false, + is_init_file_present: false, + is_stub_package: false, + import_type: ThirdParty, + resolved_paths: [ + "./resources/test/airflow/venv/lib/python3.11/site-packages/_watchdog_fsevents.cpython-311-darwin.so", + ], + search_path: Some( + "./resources/test/airflow/venv/lib/python3.11/site-packages", + ), + is_stub_file: false, + is_native_lib: true, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: {}, + filtered_implicit_imports: {}, + non_stub_import_result: None, + py_typed_info: None, + package_directory: None, +} diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_implicit_native_module.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_implicit_native_module.snap new file mode 100644 index 0000000000..7f211d89b2 --- /dev/null +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_implicit_native_module.snap @@ -0,0 +1,85 @@ +--- +source: crates/ruff_python_resolver/src/lib.rs +expression: result +--- +ImportResult { + is_relative: false, + is_import_found: true, + is_partly_resolved: false, + is_namespace_package: false, + is_init_file_present: true, + is_stub_package: false, + import_type: ThirdParty, + resolved_paths: [ + "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/__init__.pyi", + ], + search_path: Some( + "./resources/test/airflow/venv/lib/python3.11/site-packages", + ), + is_stub_file: true, + is_native_lib: false, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: { + "orjson": ImplicitImport { + is_stub_file: false, + is_native_lib: true, + name: "orjson", + path: "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so", + py_typed: None, + }, + }, + filtered_implicit_imports: {}, + non_stub_import_result: Some( + ImportResult { + is_relative: false, + is_import_found: true, + is_partly_resolved: false, + is_namespace_package: false, + is_init_file_present: true, + is_stub_package: false, + import_type: ThirdParty, + resolved_paths: [ + "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/__init__.py", + ], + search_path: Some( + "./resources/test/airflow/venv/lib/python3.11/site-packages", + ), + is_stub_file: false, + is_native_lib: false, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: { + "orjson": ImplicitImport { + is_stub_file: false, + is_native_lib: true, + name: "orjson", + path: "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so", + py_typed: None, + }, + }, + filtered_implicit_imports: {}, + non_stub_import_result: None, + py_typed_info: Some( + PyTypedInfo { + py_typed_path: "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/py.typed", + is_partially_typed: false, + }, + ), + package_directory: Some( + "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson", + ), + }, + ), + py_typed_info: Some( + PyTypedInfo { + py_typed_path: "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/py.typed", + is_partially_typed: false, + }, + ), + package_directory: Some( + "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson", + ), +} From 69c4b7fa110ce4439bb992f85fc8e56ce9b63c2b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 28 Jun 2023 14:55:43 -0400 Subject: [PATCH 270/447] Add dedicated `struct` for implicit imports (#5427) ## Summary This was some feedback on a prior PR that I decided to act on separately. --- .../src/implicit_imports.rs | 274 ++++++++++-------- .../ruff_python_resolver/src/import_result.rs | 11 +- crates/ruff_python_resolver/src/resolver.rs | 78 ++--- ...tests__airflow_explicit_native_module.snap | 8 +- ..._resolver__tests__airflow_first_party.snap | 8 +- ...tests__airflow_implicit_native_module.snap | 42 +-- ...ver__tests__airflow_namespace_package.snap | 8 +- ...lver__tests__airflow_standard_library.snap | 8 +- ...on_resolver__tests__airflow_stub_file.snap | 16 +- ..._resolver__tests__airflow_third_party.snap | 47 +-- 10 files changed, 270 insertions(+), 230 deletions(-) diff --git a/crates/ruff_python_resolver/src/implicit_imports.rs b/crates/ruff_python_resolver/src/implicit_imports.rs index 76191b87b6..94bf9a9f2c 100644 --- a/crates/ruff_python_resolver/src/implicit_imports.rs +++ b/crates/ruff_python_resolver/src/implicit_imports.rs @@ -1,10 +1,165 @@ use std::collections::BTreeMap; use std::ffi::OsStr; -use std::fs; +use std::io; use std::path::{Path, PathBuf}; use crate::{native_module, py_typed}; +/// A map of the submodules that are present in a namespace package. +/// +/// Namespace packages lack an `__init__.py` file. So when resolving symbols from a namespace +/// package, the symbols must be present as submodules. This map contains the submodules that are +/// present in the namespace package, keyed by their module name. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub(crate) struct ImplicitImports(BTreeMap); + +impl ImplicitImports { + /// Find the "implicit" imports within the namespace package at the given path. + pub(crate) fn find(dir_path: &Path, exclusions: &[&Path]) -> io::Result { + let mut submodules: BTreeMap = BTreeMap::new(); + + // Enumerate all files and directories in the path, expanding links. + for entry in dir_path.read_dir()?.flatten() { + let file_type = entry.file_type()?; + + let path = entry.path(); + if exclusions.contains(&path.as_path()) { + continue; + } + + // TODO(charlie): Support symlinks. + if file_type.is_file() { + // Add implicit file-based modules. + let Some(extension) = path.extension() else { + continue; + }; + + let (file_stem, is_native_lib) = if extension == "py" || extension == "pyi" { + // E.g., `foo.py` becomes `foo`. + let file_stem = path.file_stem().and_then(OsStr::to_str); + let is_native_lib = false; + (file_stem, is_native_lib) + } else if native_module::is_native_module_file_extension(extension) { + // E.g., `foo.abi3.so` becomes `foo`. + let file_stem = native_module::native_module_name(&path); + let is_native_lib = true; + (file_stem, is_native_lib) + } else { + continue; + }; + + let Some(name) = file_stem else { + continue; + }; + + // Always prefer stub files over non-stub files. + if submodules + .get(name) + .map_or(true, |implicit_import| !implicit_import.is_stub_file) + { + submodules.insert( + name.to_string(), + ImplicitImport { + is_stub_file: extension == "pyi", + is_native_lib, + path, + py_typed: None, + }, + ); + } + } else if file_type.is_dir() { + // Add implicit directory-based modules. + let py_file_path = path.join("__init__.py"); + let pyi_file_path = path.join("__init__.pyi"); + + let (path, is_stub_file) = if py_file_path.exists() { + (py_file_path, false) + } else if pyi_file_path.exists() { + (pyi_file_path, true) + } else { + continue; + }; + + let Some(name) = path.file_name().and_then(OsStr::to_str) else { + continue; + }; + submodules.insert( + name.to_string(), + ImplicitImport { + is_stub_file, + is_native_lib: false, + py_typed: py_typed::get_py_typed_info(&path), + path, + }, + ); + } + } + + Ok(Self(submodules)) + } + + /// Filter [`ImplicitImports`] to only those symbols that were imported. + pub(crate) fn filter(&self, imported_symbols: &[String]) -> Option { + if self.is_empty() || imported_symbols.is_empty() { + return None; + } + + let filtered: BTreeMap = self + .iter() + .filter(|(name, _)| imported_symbols.contains(name)) + .map(|(name, implicit_import)| (name.clone(), implicit_import.clone())) + .collect(); + + if filtered.len() == self.len() { + return None; + } + + Some(Self(filtered)) + } + + /// Returns `true` if the [`ImplicitImports`] resolves all the symbols requested by a + /// module descriptor. + pub(crate) fn resolves_namespace_package(&self, imported_symbols: &[String]) -> bool { + if !imported_symbols.is_empty() { + // TODO(charlie): Pyright uses: + // + // ```typescript + // !Array.from(moduleDescriptor.importedSymbols.keys()).some((symbol) => implicitImports.has(symbol))` + // ``` + // + // However, that only checks if _any_ of the symbols are in the implicit imports. + for symbol in imported_symbols { + if !self.has(symbol) { + return false; + } + } + } else if self.is_empty() { + return false; + } + true + } + + /// Returns `true` if the module is present in the namespace package. + pub(crate) fn has(&self, name: &str) -> bool { + self.0.contains_key(name) + } + + /// Returns the number of implicit imports in the namespace package. + pub(crate) fn len(&self) -> usize { + self.0.len() + } + + /// Returns `true` if there are no implicit imports in the namespace package. + pub(crate) fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Returns an iterator over the implicit imports in the namespace package. + pub(crate) fn iter(&self) -> impl Iterator { + self.0.iter() + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct ImplicitImport { /// Whether the implicit import is a stub file. @@ -13,126 +168,9 @@ pub(crate) struct ImplicitImport { /// Whether the implicit import is a native module. pub(crate) is_native_lib: bool, - /// The name of the implicit import (e.g., `os`). - pub(crate) name: String, - /// The path to the implicit import. pub(crate) path: PathBuf, /// The `py.typed` information for the implicit import, if any. pub(crate) py_typed: Option, } - -/// Find the "implicit" imports within the namespace package at the given path. -pub(crate) fn find(dir_path: &Path, exclusions: &[&Path]) -> BTreeMap { - let mut implicit_imports = BTreeMap::new(); - - // Enumerate all files and directories in the path, expanding links. - let Ok(entries) = fs::read_dir(dir_path) else { - return implicit_imports; - }; - - for entry in entries.flatten() { - let path = entry.path(); - - if exclusions.contains(&path.as_path()) { - continue; - } - - let Ok(file_type) = entry.file_type() else { - continue; - }; - - // TODO(charlie): Support symlinks. - if file_type.is_file() { - // Add implicit file-based modules. - let Some(extension) = path.extension() else { - continue; - }; - - let (file_stem, is_native_lib) = if extension == "py" || extension == "pyi" { - // E.g., `foo.py` becomes `foo`. - let file_stem = path.file_stem().and_then(OsStr::to_str); - let is_native_lib = false; - (file_stem, is_native_lib) - } else if native_module::is_native_module_file_extension(extension) { - // E.g., `foo.abi3.so` becomes `foo`. - let file_stem = native_module::native_module_name(&path); - let is_native_lib = true; - (file_stem, is_native_lib) - } else { - continue; - }; - - let Some(name) = file_stem else { - continue; - }; - - let implicit_import = ImplicitImport { - is_stub_file: extension == "pyi", - is_native_lib, - name: name.to_string(), - path: path.clone(), - py_typed: None, - }; - - // Always prefer stub files over non-stub files. - if implicit_imports - .get(&implicit_import.name) - .map_or(true, |implicit_import| !implicit_import.is_stub_file) - { - implicit_imports.insert(implicit_import.name.clone(), implicit_import); - } - } else if file_type.is_dir() { - // Add implicit directory-based modules. - let py_file_path = path.join("__init__.py"); - let pyi_file_path = path.join("__init__.pyi"); - - let (path, is_stub_file) = if py_file_path.exists() { - (py_file_path, false) - } else if pyi_file_path.exists() { - (pyi_file_path, true) - } else { - continue; - }; - - let Some(name) = path.file_name().and_then(OsStr::to_str) else { - continue; - }; - - let implicit_import = ImplicitImport { - is_stub_file, - is_native_lib: false, - name: name.to_string(), - path: path.clone(), - py_typed: py_typed::get_py_typed_info(&path), - }; - implicit_imports.insert(implicit_import.name.clone(), implicit_import); - } - } - - implicit_imports -} - -/// Filter a map of implicit imports to only include those that were actually imported. -pub(crate) fn filter( - implicit_imports: &BTreeMap, - imported_symbols: &[String], -) -> Option> { - if implicit_imports.is_empty() || imported_symbols.is_empty() { - return None; - } - - let mut filtered_imports = BTreeMap::new(); - for implicit_import in implicit_imports.values() { - if imported_symbols.contains(&implicit_import.name) { - filtered_imports.insert(implicit_import.name.clone(), implicit_import.clone()); - } - } - - if filtered_imports.len() == implicit_imports.len() { - return None; - } - - Some(filtered_imports) -} diff --git a/crates/ruff_python_resolver/src/import_result.rs b/crates/ruff_python_resolver/src/import_result.rs index 72161cb3e1..704781f420 100644 --- a/crates/ruff_python_resolver/src/import_result.rs +++ b/crates/ruff_python_resolver/src/import_result.rs @@ -1,9 +1,8 @@ //! Interface that describes the output of the import resolver. -use std::collections::BTreeMap; use std::path::PathBuf; -use crate::implicit_imports::ImplicitImport; +use crate::implicit_imports::ImplicitImports; use crate::py_typed::PyTypedInfo; #[derive(Debug, Clone, PartialEq, Eq)] @@ -69,11 +68,11 @@ pub(crate) struct ImportResult { /// A map from file to resolved path, for all implicitly imported /// modules that are part of a namespace package. - pub(crate) implicit_imports: BTreeMap, + pub(crate) implicit_imports: ImplicitImports, /// Any implicit imports whose symbols were explicitly imported (i.e., via /// a `from x import y` statement). - pub(crate) filtered_implicit_imports: BTreeMap, + pub(crate) filtered_implicit_imports: ImplicitImports, /// If the import resolved to a type hint (i.e., a `.pyi` file), then /// a non-type-hint resolution will be stored here. @@ -105,8 +104,8 @@ impl ImportResult { is_stdlib_typeshed_file: false, is_third_party_typeshed_file: false, is_local_typings_file: false, - implicit_imports: BTreeMap::default(), - filtered_implicit_imports: BTreeMap::default(), + implicit_imports: ImplicitImports::default(), + filtered_implicit_imports: ImplicitImports::default(), non_stub_import_result: None, py_typed_info: None, package_directory: None, diff --git a/crates/ruff_python_resolver/src/resolver.rs b/crates/ruff_python_resolver/src/resolver.rs index 4749f5b7fa..6c8e076eb9 100644 --- a/crates/ruff_python_resolver/src/resolver.rs +++ b/crates/ruff_python_resolver/src/resolver.rs @@ -1,6 +1,5 @@ //! Resolves Python imports to their corresponding files on disk. -use std::collections::BTreeMap; use std::ffi::OsStr; use std::path::{Path, PathBuf}; @@ -8,10 +7,10 @@ use log::debug; use crate::config::Config; use crate::execution_environment::ExecutionEnvironment; -use crate::implicit_imports::ImplicitImport; +use crate::implicit_imports::ImplicitImports; use crate::import_result::{ImportResult, ImportType}; use crate::module_descriptor::ImportModuleDescriptor; -use crate::{host, implicit_imports, native_module, py_typed, search}; +use crate::{host, native_module, py_typed, search}; #[allow(clippy::fn_params_excessive_bools)] fn resolve_module_descriptor( @@ -37,7 +36,7 @@ fn resolve_module_descriptor( let mut is_stub_package = false; let mut is_stub_file = false; let mut is_native_lib = false; - let mut implicit_imports = BTreeMap::new(); + let mut implicit_imports = None; let mut package_directory = None; let mut py_typed_info = None; @@ -60,7 +59,7 @@ fn resolve_module_descriptor( is_namespace_package = true; } - implicit_imports = implicit_imports::find(&dir_path, &[&py_file_path, &pyi_file_path]); + implicit_imports = ImplicitImports::find(&dir_path, &[&py_file_path, &pyi_file_path]).ok(); } else { for (i, part) in module_descriptor.name_parts.iter().enumerate() { let is_first_part = i == 0; @@ -118,7 +117,8 @@ fn resolve_module_descriptor( if is_init_file_present { implicit_imports = - implicit_imports::find(&module_dir_path, &[&py_file_path, &pyi_file_path]); + ImplicitImports::find(&module_dir_path, &[&py_file_path, &pyi_file_path]) + .ok(); break; } } @@ -157,7 +157,7 @@ fn resolve_module_descriptor( resolved_paths.push(PathBuf::new()); if is_last_part { implicit_imports = - implicit_imports::find(&dir_path, &[&py_file_path, &pyi_file_path]); + ImplicitImports::find(&dir_path, &[&py_file_path, &pyi_file_path]).ok(); is_namespace_package = true; } } @@ -191,8 +191,8 @@ fn resolve_module_descriptor( is_stdlib_typeshed_file: false, is_third_party_typeshed_file: false, is_local_typings_file: false, - implicit_imports, - filtered_implicit_imports: BTreeMap::default(), + implicit_imports: implicit_imports.unwrap_or_default(), + filtered_implicit_imports: ImplicitImports::default(), non_stub_import_result: None, py_typed_info, package_directory, @@ -290,10 +290,10 @@ fn resolve_best_absolute_import( .as_os_str() .is_empty() { - if is_namespace_package_resolved( - module_descriptor, - &typings_import.implicit_imports, - ) { + if typings_import + .implicit_imports + .resolves_namespace_package(&module_descriptor.imported_symbols) + { return Some(typings_import); } } else { @@ -415,34 +415,6 @@ fn resolve_best_absolute_import( best_result_so_far } -/// Determines whether a namespace package resolves all of the symbols -/// requested in the module descriptor. Namespace packages have no "__init__.py" -/// file, so the only way that symbols can be resolved is if submodules -/// are present. If specific symbols were requested, make sure they -/// are all satisfied by submodules (as listed in the implicit imports). -fn is_namespace_package_resolved( - module_descriptor: &ImportModuleDescriptor, - implicit_imports: &BTreeMap, -) -> bool { - if !module_descriptor.imported_symbols.is_empty() { - // TODO(charlie): Pyright uses: - // - // ```typescript - // !Array.from(moduleDescriptor.importedSymbols.keys()).some((symbol) => implicitImports.has(symbol))` - // ``` - // - // However, that only checks if _any_ of the symbols are in the implicit imports. - for symbol in &module_descriptor.imported_symbols { - if !implicit_imports.contains_key(symbol) { - return false; - } - } - } else if implicit_imports.is_empty() { - return false; - } - true -} - /// Finds the `typeshed` path for the given module descriptor. /// /// Supports both standard library and third-party `typeshed` lookups. @@ -543,14 +515,14 @@ fn pick_best_import( // imported symbols. if best_import_so_far.is_namespace_package && new_import.is_namespace_package { if !module_descriptor.imported_symbols.is_empty() { - if !is_namespace_package_resolved( - module_descriptor, - &best_import_so_far.implicit_imports, - ) { - if is_namespace_package_resolved( - module_descriptor, - &new_import.implicit_imports, - ) { + if !best_import_so_far + .implicit_imports + .resolves_namespace_package(&module_descriptor.imported_symbols) + { + if new_import + .implicit_imports + .resolves_namespace_package(&module_descriptor.imported_symbols) + { return new_import; } @@ -759,10 +731,10 @@ pub(crate) fn resolve_import( ); if result.is_import_found { - if let Some(implicit_imports) = implicit_imports::filter( - &result.implicit_imports, - &module_descriptor.imported_symbols, - ) { + if let Some(implicit_imports) = result + .implicit_imports + .filter(&module_descriptor.imported_symbols) + { result.implicit_imports = implicit_imports; } return result; diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_explicit_native_module.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_explicit_native_module.snap index 6ab4f67891..69186a0145 100644 --- a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_explicit_native_module.snap +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_explicit_native_module.snap @@ -21,8 +21,12 @@ ImportResult { is_stdlib_typeshed_file: false, is_third_party_typeshed_file: false, is_local_typings_file: false, - implicit_imports: {}, - filtered_implicit_imports: {}, + implicit_imports: ImplicitImports( + {}, + ), + filtered_implicit_imports: ImplicitImports( + {}, + ), non_stub_import_result: None, py_typed_info: None, package_directory: None, diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_first_party.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_first_party.snap index 864f3e1a12..47e0ef7c88 100644 --- a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_first_party.snap +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_first_party.snap @@ -23,8 +23,12 @@ ImportResult { is_stdlib_typeshed_file: false, is_third_party_typeshed_file: false, is_local_typings_file: false, - implicit_imports: {}, - filtered_implicit_imports: {}, + implicit_imports: ImplicitImports( + {}, + ), + filtered_implicit_imports: ImplicitImports( + {}, + ), non_stub_import_result: None, py_typed_info: None, package_directory: Some( diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_implicit_native_module.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_implicit_native_module.snap index 7f211d89b2..262aa50fc1 100644 --- a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_implicit_native_module.snap +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_implicit_native_module.snap @@ -21,16 +21,19 @@ ImportResult { is_stdlib_typeshed_file: false, is_third_party_typeshed_file: false, is_local_typings_file: false, - implicit_imports: { - "orjson": ImplicitImport { - is_stub_file: false, - is_native_lib: true, - name: "orjson", - path: "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so", - py_typed: None, + implicit_imports: ImplicitImports( + { + "orjson": ImplicitImport { + is_stub_file: false, + is_native_lib: true, + path: "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so", + py_typed: None, + }, }, - }, - filtered_implicit_imports: {}, + ), + filtered_implicit_imports: ImplicitImports( + {}, + ), non_stub_import_result: Some( ImportResult { is_relative: false, @@ -51,16 +54,19 @@ ImportResult { is_stdlib_typeshed_file: false, is_third_party_typeshed_file: false, is_local_typings_file: false, - implicit_imports: { - "orjson": ImplicitImport { - is_stub_file: false, - is_native_lib: true, - name: "orjson", - path: "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so", - py_typed: None, + implicit_imports: ImplicitImports( + { + "orjson": ImplicitImport { + is_stub_file: false, + is_native_lib: true, + path: "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so", + py_typed: None, + }, }, - }, - filtered_implicit_imports: {}, + ), + filtered_implicit_imports: ImplicitImports( + {}, + ), non_stub_import_result: None, py_typed_info: Some( PyTypedInfo { diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_namespace_package.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_namespace_package.snap index d8f9d08207..1c9132b9bf 100644 --- a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_namespace_package.snap +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_namespace_package.snap @@ -26,8 +26,12 @@ ImportResult { is_stdlib_typeshed_file: false, is_third_party_typeshed_file: false, is_local_typings_file: false, - implicit_imports: {}, - filtered_implicit_imports: {}, + implicit_imports: ImplicitImports( + {}, + ), + filtered_implicit_imports: ImplicitImports( + {}, + ), non_stub_import_result: None, py_typed_info: None, package_directory: Some( diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_standard_library.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_standard_library.snap index 8429372b42..47299f3218 100644 --- a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_standard_library.snap +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_standard_library.snap @@ -17,8 +17,12 @@ ImportResult { is_stdlib_typeshed_file: false, is_third_party_typeshed_file: false, is_local_typings_file: false, - implicit_imports: {}, - filtered_implicit_imports: {}, + implicit_imports: ImplicitImports( + {}, + ), + filtered_implicit_imports: ImplicitImports( + {}, + ), non_stub_import_result: None, py_typed_info: None, package_directory: None, diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_stub_file.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_stub_file.snap index faf1604f1b..92d74bcb20 100644 --- a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_stub_file.snap +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_stub_file.snap @@ -23,8 +23,12 @@ ImportResult { is_stdlib_typeshed_file: false, is_third_party_typeshed_file: false, is_local_typings_file: false, - implicit_imports: {}, - filtered_implicit_imports: {}, + implicit_imports: ImplicitImports( + {}, + ), + filtered_implicit_imports: ImplicitImports( + {}, + ), non_stub_import_result: Some( ImportResult { is_relative: false, @@ -47,8 +51,12 @@ ImportResult { is_stdlib_typeshed_file: false, is_third_party_typeshed_file: false, is_local_typings_file: false, - implicit_imports: {}, - filtered_implicit_imports: {}, + implicit_imports: ImplicitImports( + {}, + ), + filtered_implicit_imports: ImplicitImports( + {}, + ), non_stub_import_result: None, py_typed_info: None, package_directory: Some( diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_third_party.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_third_party.snap index 488711b8f1..138f4f71ca 100644 --- a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_third_party.snap +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_third_party.snap @@ -22,30 +22,31 @@ ImportResult { is_stdlib_typeshed_file: false, is_third_party_typeshed_file: false, is_local_typings_file: false, - implicit_imports: { - "base": ImplicitImport { - is_stub_file: false, - is_native_lib: false, - name: "base", - path: "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/base.py", - py_typed: None, + implicit_imports: ImplicitImports( + { + "base": ImplicitImport { + is_stub_file: false, + is_native_lib: false, + path: "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/base.py", + py_typed: None, + }, + "dependency": ImplicitImport { + is_stub_file: false, + is_native_lib: false, + path: "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/dependency.py", + py_typed: None, + }, + "query": ImplicitImport { + is_stub_file: false, + is_native_lib: false, + path: "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/query.py", + py_typed: None, + }, }, - "dependency": ImplicitImport { - is_stub_file: false, - is_native_lib: false, - name: "dependency", - path: "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/dependency.py", - py_typed: None, - }, - "query": ImplicitImport { - is_stub_file: false, - is_native_lib: false, - name: "query", - path: "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/query.py", - py_typed: None, - }, - }, - filtered_implicit_imports: {}, + ), + filtered_implicit_imports: ImplicitImports( + {}, + ), non_stub_import_result: None, py_typed_info: None, package_directory: Some( From c5e20505f80e3fd7adc4b3a191049fbc42bbbeee Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 28 Jun 2023 15:08:10 -0400 Subject: [PATCH 271/447] Remove an unsafe access in the resolver (#5428) --- crates/ruff_python_resolver/src/resolver.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/ruff_python_resolver/src/resolver.rs b/crates/ruff_python_resolver/src/resolver.rs index 6c8e076eb9..86b2d5e5b8 100644 --- a/crates/ruff_python_resolver/src/resolver.rs +++ b/crates/ruff_python_resolver/src/resolver.rs @@ -173,8 +173,11 @@ fn resolve_module_descriptor( resolved_paths.len() == module_descriptor.name_parts.len() }; - let is_partly_resolved = - !resolved_paths.is_empty() && resolved_paths.len() < module_descriptor.name_parts.len(); + let is_partly_resolved = if resolved_paths.is_empty() { + false + } else { + resolved_paths.len() < module_descriptor.name_parts.len() + }; ImportResult { is_relative: false, @@ -286,9 +289,10 @@ fn resolve_best_absolute_import( // If we resolved to a namespace package, ensure that all imported symbols are // present in the namespace package's "implicit" imports. if typings_import.is_namespace_package - && typings_import.resolved_paths[typings_import.resolved_paths.len() - 1] - .as_os_str() - .is_empty() + && typings_import + .resolved_paths + .last() + .map_or(false, |path| path.as_os_str().is_empty()) { if typings_import .implicit_imports From 139a9f757bcfa487ce22a539f153825ef8540567 Mon Sep 17 00:00:00 2001 From: Eric H Date: Wed, 28 Jun 2023 15:22:16 -0600 Subject: [PATCH 272/447] Update default configuration.md to mention C901 rule (#5397) --- docs/configuration.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 3c95d5ca87..a64bef7a50 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -11,12 +11,14 @@ If left unspecified, Ruff's default configuration is equivalent to: ```toml [tool.ruff] -# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. +# Enable the pycodestyle (`E`) and Pyflakes (`F`) rules by default. +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. select = ["E", "F"] ignore = [] # Allow autofix for all enabled rules (when `--fix`) is provided. -fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"] +fixable = ["ALL"] unfixable = [] # Exclude a variety of commonly ignored directories. @@ -53,10 +55,6 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" # Assume Python 3.10. target-version = "py310" - -[tool.ruff.mccabe] -# Unlike Flake8, default to a complexity level of 10. -max-complexity = 10 ``` As an example, the following would configure Ruff to: (1) enforce flake8-bugbear rules, in addition From 0e89c94947691a466686eaa2bee74c6b90cef9f3 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 28 Jun 2023 20:08:18 -0400 Subject: [PATCH 273/447] Run shadowed-variable analyses in deferred handlers (#5181) ## Summary This PR extracts a bunch of complex logic from `add_binding`, instead running the the shadowing rules in the deferred handler, thereby decoupling the binding phase (during which we build up the semantic model) from the analysis phase, and generally making `add_binding` much more focused. This was made possible by improving the semantic model to better handle deletions -- previously, we'd "lose track" of bindings if they were deleted, which made this kind of refactor impossible. ## Test Plan We have good automated coverage for this, but I want to benchmark it separately. --- crates/ruff/src/checkers/ast/mod.rs | 233 ++++++++++-------- crates/ruff/src/rules/pyflakes/mod.rs | 6 +- ...shadowed_global_import_in_local_scope.snap | 9 + ...shadowed_import_shadow_in_local_scope.snap | 15 +- crates/ruff_python_semantic/src/model.rs | 63 +++++ crates/ruff_python_semantic/src/scope.rs | 5 + 6 files changed, 222 insertions(+), 109 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index b7b0d1e925..012d999ea3 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -4153,88 +4153,6 @@ impl<'a> Checker<'a> { // Create the `Binding`. let binding_id = self.semantic.push_binding(range, kind, flags); - let binding = self.semantic.binding(binding_id); - - // Determine whether the binding shadows any existing bindings. - if let Some((stack_index, shadowed_id)) = self - .semantic - .scopes - .ancestors(self.semantic.scope_id) - .enumerate() - .find_map(|(stack_index, scope)| { - scope.get(name).and_then(|binding_id| { - let binding = self.semantic.binding(binding_id); - if binding.is_unbound() { - None - } else { - Some((stack_index, binding_id)) - } - }) - }) - { - let shadowed = self.semantic.binding(shadowed_id); - let in_current_scope = stack_index == 0; - if !shadowed.kind.is_builtin() - && shadowed.source.map_or(true, |left| { - binding.source.map_or(true, |right| { - !branch_detection::different_forks(left, right, &self.semantic.stmts) - }) - }) - { - let shadows_import = matches!( - shadowed.kind, - BindingKind::Import(..) - | BindingKind::FromImport(..) - | BindingKind::SubmoduleImport(..) - | BindingKind::FutureImport - ); - if binding.kind.is_loop_var() && shadows_import { - if self.enabled(Rule::ImportShadowedByLoopVar) { - #[allow(deprecated)] - let line = self.locator.compute_line_index(shadowed.range.start()); - - self.diagnostics.push(Diagnostic::new( - pyflakes::rules::ImportShadowedByLoopVar { - name: name.to_string(), - line, - }, - binding.range, - )); - } - } else if in_current_scope { - if !shadowed.is_used() - && binding.redefines(shadowed) - && (!self.settings.dummy_variable_rgx.is_match(name) || shadows_import) - && !(shadowed.kind.is_function_definition() - && visibility::is_overload( - cast::decorator_list(self.semantic.stmts[shadowed.source.unwrap()]), - &self.semantic, - )) - { - if self.enabled(Rule::RedefinedWhileUnused) { - #[allow(deprecated)] - let line = self.locator.compute_line_index(shadowed.range.start()); - - let mut diagnostic = Diagnostic::new( - pyflakes::rules::RedefinedWhileUnused { - name: name.to_string(), - line, - }, - binding.range, - ); - if let Some(range) = binding.parent_range(&self.semantic) { - diagnostic.set_parent(range.start()); - } - self.diagnostics.push(diagnostic); - } - } - } else if shadows_import && binding.redefines(shadowed) { - self.semantic - .shadowed_bindings - .insert(binding_id, shadowed_id); - } - } - } // If there's an existing binding in this scope, copy its references. if let Some(shadowed_id) = self.semantic.scopes[scope_id].get(name) { @@ -4268,6 +4186,21 @@ impl<'a> Checker<'a> { self.semantic.bindings[binding_id].references = references; } + } else if let Some(shadowed_id) = self + .semantic + .scopes + .ancestors(scope_id) + .skip(1) + .find_map(|scope| scope.get(name)) + { + // Otherwise, if there's an existing binding in a parent scope, mark it as shadowed. + let binding = self.semantic.binding(binding_id); + let shadowed = self.semantic.binding(shadowed_id); + if binding.redefines(shadowed) { + self.semantic + .shadowed_bindings + .insert(binding_id, shadowed_id); + } } // Add the binding to the scope. @@ -4286,7 +4219,7 @@ impl<'a> Checker<'a> { { // Add the builtin to the scope. let binding_id = self.semantic.push_builtin(); - let scope = self.semantic.scope_mut(); + let scope = self.semantic.global_scope_mut(); scope.add(builtin, binding_id); } } @@ -4690,17 +4623,19 @@ impl<'a> Checker<'a> { fn check_deferred_scopes(&mut self) { if !self.any_enabled(&[ - Rule::UnusedImport, Rule::GlobalVariableNotAssigned, - Rule::UndefinedLocalWithImportStarUsage, + Rule::ImportShadowedByLoopVar, Rule::RedefinedWhileUnused, Rule::RuntimeImportInTypeCheckingBlock, Rule::TypingOnlyFirstPartyImport, - Rule::TypingOnlyThirdPartyImport, Rule::TypingOnlyStandardLibraryImport, - Rule::UndefinedExport, + Rule::TypingOnlyThirdPartyImport, Rule::UnaliasedCollectionsAbcSetImport, Rule::UnconventionalImportAlias, + Rule::UndefinedExport, + Rule::UndefinedLocalWithImportStarUsage, + Rule::UndefinedLocalWithImportStarUsage, + Rule::UnusedImport, ]) { return; } @@ -4767,8 +4702,8 @@ impl<'a> Checker<'a> { }; let mut diagnostics: Vec = vec![]; - for scope_id in self.deferred.scopes.iter().rev() { - let scope = &self.semantic.scopes[*scope_id]; + for scope_id in self.deferred.scopes.iter().rev().copied() { + let scope = &self.semantic.scopes[scope_id]; if scope.kind.is_module() { // F822 @@ -4827,21 +4762,123 @@ impl<'a> Checker<'a> { continue; } - // Look for any bindings that were redefined in another scope, and remain - // unused. Note that we only store references in `shadowed_bindings` if - // the bindings are in different scopes. - if self.enabled(Rule::RedefinedWhileUnused) { + // F402 + if self.enabled(Rule::ImportShadowedByLoopVar) { for (name, binding_id) in scope.bindings() { - if let Some(shadowed_id) = self.semantic.shadowed_binding(binding_id) { - let shadowed = self.semantic.binding(shadowed_id); - if shadowed.is_used() { + for shadow in self.semantic.shadowed_bindings(scope_id, binding_id) { + // If the shadowing binding isn't a loop variable, abort. + let binding = &self.semantic.bindings[shadow.binding_id()]; + if !binding.kind.is_loop_var() { + continue; + } + + // If the shadowed binding isn't an import, abort. + let shadowed = &self.semantic.bindings[shadow.shadowed_id()]; + if !matches!( + shadowed.kind, + BindingKind::Import(..) + | BindingKind::FromImport(..) + | BindingKind::SubmoduleImport(..) + | BindingKind::FutureImport + ) { + continue; + } + + // If the bindings are in different forks, abort. + if shadowed.source.map_or(true, |left| { + binding.source.map_or(true, |right| { + branch_detection::different_forks(left, right, &self.semantic.stmts) + }) + }) { continue; } #[allow(deprecated)] let line = self.locator.compute_line_index(shadowed.range.start()); - let binding = self.semantic.binding(binding_id); + self.diagnostics.push(Diagnostic::new( + pyflakes::rules::ImportShadowedByLoopVar { + name: name.to_string(), + line, + }, + binding.range, + )); + } + } + } + + // F811 + if self.enabled(Rule::RedefinedWhileUnused) { + for (name, binding_id) in scope.bindings() { + for shadow in self.semantic.shadowed_bindings(scope_id, binding_id) { + // If the shadowing binding is a loop variable, abort, to avoid overlap + // with F402. + let binding = &self.semantic.bindings[shadow.binding_id()]; + if binding.kind.is_loop_var() { + continue; + } + + // If the shadowed binding is used, abort. + let shadowed = &self.semantic.bindings[shadow.shadowed_id()]; + if shadowed.is_used() { + continue; + } + + // If the shadowing binding isn't considered a "redefinition" of the + // shadowed binding, abort. + if !binding.redefines(shadowed) { + continue; + } + + if shadow.same_scope() { + // If the symbol is a dummy variable, abort, unless the shadowed + // binding is an import. + if !matches!( + shadowed.kind, + BindingKind::Import(..) + | BindingKind::FromImport(..) + | BindingKind::SubmoduleImport(..) + | BindingKind::FutureImport + ) && self.settings.dummy_variable_rgx.is_match(name) + { + continue; + } + + // If this is an overloaded function, abort. + if shadowed.kind.is_function_definition() + && visibility::is_overload( + cast::decorator_list( + self.semantic.stmts[shadowed.source.unwrap()], + ), + &self.semantic, + ) + { + continue; + } + } else { + // Only enforce cross-scope shadowing for imports. + if !matches!( + shadowed.kind, + BindingKind::Import(..) + | BindingKind::FromImport(..) + | BindingKind::SubmoduleImport(..) + | BindingKind::FutureImport + ) { + continue; + } + } + + // If the bindings are in different forks, abort. + if shadowed.source.map_or(true, |left| { + binding.source.map_or(true, |right| { + branch_detection::different_forks(left, right, &self.semantic.stmts) + }) + }) { + continue; + } + + #[allow(deprecated)] + let line = self.locator.compute_line_index(shadowed.range.start()); let mut diagnostic = Diagnostic::new( pyflakes::rules::RedefinedWhileUnused { name: (*name).to_string(), @@ -4863,7 +4900,7 @@ impl<'a> Checker<'a> { } else { self.semantic .scopes - .ancestor_ids(*scope_id) + .ancestor_ids(scope_id) .flat_map(|scope_id| runtime_imports[scope_id.as_usize()].iter()) .copied() .collect() diff --git a/crates/ruff/src/rules/pyflakes/mod.rs b/crates/ruff/src/rules/pyflakes/mod.rs index 243be429ce..0bdbeaee1d 100644 --- a/crates/ruff/src/rules/pyflakes/mod.rs +++ b/crates/ruff/src/rules/pyflakes/mod.rs @@ -282,7 +282,7 @@ mod tests { import os # Despite this `del`, `import os` in `f` should still be flagged as shadowing an unused - # import. (This is a false negative.) + # import. del os "#, "del_shadowed_global_import_in_local_scope" @@ -323,7 +323,7 @@ mod tests { del os def g(): - # `import os` should still be flagged as shadowing an import. + # `import os` doesn't need to be flagged as shadowing an import. os = 1 print(os) "#, @@ -2114,7 +2114,7 @@ mod tests { try: pass except Exception as fu: pass "#, - &[Rule::RedefinedWhileUnused, Rule::UnusedVariable], + &[Rule::UnusedVariable, Rule::RedefinedWhileUnused], ); } diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__del_shadowed_global_import_in_local_scope.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__del_shadowed_global_import_in_local_scope.snap index 37beaccce1..b2b02e92a7 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__del_shadowed_global_import_in_local_scope.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__del_shadowed_global_import_in_local_scope.snap @@ -17,4 +17,13 @@ source: crates/ruff/src/rules/pyflakes/mod.rs 4 3 | def f(): 5 4 | import os +:5:12: F811 Redefinition of unused `os` from line 2 + | +4 | def f(): +5 | import os + | ^^ F811 +6 | +7 | # Despite this `del`, `import os` in `f` should still be flagged as shadowing an unused + | + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__del_shadowed_import_shadow_in_local_scope.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__del_shadowed_import_shadow_in_local_scope.snap index d8e62a9c2e..747002c934 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__del_shadowed_import_shadow_in_local_scope.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__del_shadowed_import_shadow_in_local_scope.snap @@ -17,13 +17,12 @@ source: crates/ruff/src/rules/pyflakes/mod.rs 4 3 | def f(): 5 4 | os = 1 -:12:9: F811 Redefinition of unused `os` from line 2 - | -10 | def g(): -11 | # `import os` should still be flagged as shadowing an import. -12 | os = 1 - | ^^ F811 -13 | print(os) - | +:5:5: F811 Redefinition of unused `os` from line 2 + | +4 | def f(): +5 | os = 1 + | ^^ F811 +6 | print(os) + | diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index d39d36def7..619020ac12 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -1052,6 +1052,69 @@ impl<'a> SemanticModel<'a> { pub const fn future_annotations(&self) -> bool { self.flags.contains(SemanticModelFlags::FUTURE_ANNOTATIONS) } + + /// Return an iterator over all bindings shadowed by the given [`BindingId`], within the + /// containing scope, and across scopes. + pub fn shadowed_bindings( + &self, + scope_id: ScopeId, + binding_id: BindingId, + ) -> impl Iterator + '_ { + let mut first = true; + let mut binding_id = binding_id; + std::iter::from_fn(move || { + // First, check whether this binding is shadowing another binding in a different scope. + if std::mem::take(&mut first) { + if let Some(shadowed_id) = self.shadowed_bindings.get(&binding_id).copied() { + return Some(ShadowedBinding { + binding_id, + shadowed_id, + same_scope: false, + }); + } + } + + // Otherwise, check whether this binding is shadowing another binding in the same scope. + if let Some(shadowed_id) = self.scopes[scope_id].shadowed_binding(binding_id) { + let next = ShadowedBinding { + binding_id, + shadowed_id, + same_scope: true, + }; + + // Advance to the next binding in the scope. + first = true; + binding_id = shadowed_id; + + return Some(next); + } + + None + }) + } +} + +pub struct ShadowedBinding { + /// The binding that is shadowing another binding. + binding_id: BindingId, + /// The binding that is being shadowed. + shadowed_id: BindingId, + /// Whether the shadowing and shadowed bindings are in the same scope. + same_scope: bool, +} + +impl ShadowedBinding { + pub const fn binding_id(&self) -> BindingId { + self.binding_id + } + + pub const fn shadowed_id(&self) -> BindingId { + self.shadowed_id + } + + pub const fn same_scope(&self) -> bool { + self.same_scope + } } bitflags! { diff --git a/crates/ruff_python_semantic/src/scope.rs b/crates/ruff_python_semantic/src/scope.rs index f64148b56e..04efe0e92f 100644 --- a/crates/ruff_python_semantic/src/scope.rs +++ b/crates/ruff_python_semantic/src/scope.rs @@ -126,6 +126,11 @@ impl<'a> Scope<'a> { }) } + /// Returns the ID of the binding that the given binding shadows, if any. + pub fn shadowed_binding(&self, id: BindingId) -> Option { + self.shadowed_bindings.get(&id).copied() + } + /// Adds a reference to a star import (e.g., `from sys import *`) to this scope. pub fn add_star_import(&mut self, import: StarImport<'a>) { self.star_imports.push(import); From 5aa2a90e17fbb259c35fb6644a3c5fd235ebd66f Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Thu, 29 Jun 2023 02:30:11 +0100 Subject: [PATCH 274/447] Add documentation to `flake8-logging-format` rules (#5417) ## Summary Completes the documentation for the `flake8-logging-format` rules. Related to #2646. I included both the `flake8-logging-format` recommendation to use the `extra` keyword and the Pylint recommendation to pass format values as parameters so that formatting is done lazily, as #970 suggests the Pylint logging rules are covered by this ruleset. Using lazy formatting via parameters is probably more common than avoiding formatting entirely in favour of the `extra` argument, regardless. ## Test Plan `python scripts/check_docs_formatted.py` --- ...flake8_logging_format__tests__G001.py.snap | 18 +- ...ake8_logging_format__tests__G101_1.py.snap | 2 +- ...ake8_logging_format__tests__G101_2.py.snap | 2 +- .../rules/flake8_logging_format/violations.rs | 361 +++++++++++++++++- 4 files changed, 370 insertions(+), 13 deletions(-) diff --git a/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G001.py.snap b/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G001.py.snap index ce85d609df..e1ed539d35 100644 --- a/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G001.py.snap +++ b/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G001.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_logging_format/mod.rs --- -G001.py:4:14: G001 Logging statement uses `string.format()` +G001.py:4:14: G001 Logging statement uses `str.format` | 2 | import logging as foo 3 | @@ -11,7 +11,7 @@ G001.py:4:14: G001 Logging statement uses `string.format()` 6 | foo.info("Hello {}".format("World!")) | -G001.py:5:27: G001 Logging statement uses `string.format()` +G001.py:5:27: G001 Logging statement uses `str.format` | 4 | logging.info("Hello {}".format("World!")) 5 | logging.log(logging.INFO, "Hello {}".format("World!")) @@ -20,7 +20,7 @@ G001.py:5:27: G001 Logging statement uses `string.format()` 7 | logging.log(logging.INFO, msg="Hello {}".format("World!")) | -G001.py:6:10: G001 Logging statement uses `string.format()` +G001.py:6:10: G001 Logging statement uses `str.format` | 4 | logging.info("Hello {}".format("World!")) 5 | logging.log(logging.INFO, "Hello {}".format("World!")) @@ -30,7 +30,7 @@ G001.py:6:10: G001 Logging statement uses `string.format()` 8 | logging.log(level=logging.INFO, msg="Hello {}".format("World!")) | -G001.py:7:31: G001 Logging statement uses `string.format()` +G001.py:7:31: G001 Logging statement uses `str.format` | 5 | logging.log(logging.INFO, "Hello {}".format("World!")) 6 | foo.info("Hello {}".format("World!")) @@ -40,7 +40,7 @@ G001.py:7:31: G001 Logging statement uses `string.format()` 9 | logging.log(msg="Hello {}".format("World!"), level=logging.INFO) | -G001.py:8:37: G001 Logging statement uses `string.format()` +G001.py:8:37: G001 Logging statement uses `str.format` | 6 | foo.info("Hello {}".format("World!")) 7 | logging.log(logging.INFO, msg="Hello {}".format("World!")) @@ -49,7 +49,7 @@ G001.py:8:37: G001 Logging statement uses `string.format()` 9 | logging.log(msg="Hello {}".format("World!"), level=logging.INFO) | -G001.py:9:17: G001 Logging statement uses `string.format()` +G001.py:9:17: G001 Logging statement uses `str.format` | 7 | logging.log(logging.INFO, msg="Hello {}".format("World!")) 8 | logging.log(level=logging.INFO, msg="Hello {}".format("World!")) @@ -59,7 +59,7 @@ G001.py:9:17: G001 Logging statement uses `string.format()` 11 | # Flask support | -G001.py:16:31: G001 Logging statement uses `string.format()` +G001.py:16:31: G001 Logging statement uses `str.format` | 14 | from flask import current_app as app 15 | @@ -69,7 +69,7 @@ G001.py:16:31: G001 Logging statement uses `string.format()` 18 | app.logger.log(logging.INFO, "Hello {}".format("World!")) | -G001.py:17:25: G001 Logging statement uses `string.format()` +G001.py:17:25: G001 Logging statement uses `str.format` | 16 | flask.current_app.logger.info("Hello {}".format("World!")) 17 | current_app.logger.info("Hello {}".format("World!")) @@ -77,7 +77,7 @@ G001.py:17:25: G001 Logging statement uses `string.format()` 18 | app.logger.log(logging.INFO, "Hello {}".format("World!")) | -G001.py:18:30: G001 Logging statement uses `string.format()` +G001.py:18:30: G001 Logging statement uses `str.format` | 16 | flask.current_app.logger.info("Hello {}".format("World!")) 17 | current_app.logger.info("Hello {}".format("World!")) diff --git a/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G101_1.py.snap b/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G101_1.py.snap index 4bdc1ccbd7..ba7d9daa0d 100644 --- a/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G101_1.py.snap +++ b/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G101_1.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_logging_format/mod.rs --- -G101_1.py:6:9: G101 Logging statement uses an extra field that clashes with a LogRecord field: `name` +G101_1.py:6:9: G101 Logging statement uses an `extra` field that clashes with a `LogRecord` field: `name` | 4 | "Hello world!", 5 | extra={ diff --git a/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G101_2.py.snap b/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G101_2.py.snap index b1084f13a9..0db9761fbc 100644 --- a/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G101_2.py.snap +++ b/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G101_2.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_logging_format/mod.rs --- -G101_2.py:6:9: G101 Logging statement uses an extra field that clashes with a LogRecord field: `name` +G101_2.py:6:9: G101 Logging statement uses an `extra` field that clashes with a `LogRecord` field: `name` | 4 | "Hello world!", 5 | extra=dict( diff --git a/crates/ruff/src/rules/flake8_logging_format/violations.rs b/crates/ruff/src/rules/flake8_logging_format/violations.rs index 622c9b83ec..3bada9ec59 100644 --- a/crates/ruff/src/rules/flake8_logging_format/violations.rs +++ b/crates/ruff/src/rules/flake8_logging_format/violations.rs @@ -1,16 +1,130 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_macros::{derive_message_formats, violation}; +/// ## What it does +/// Checks for uses of `str.format` to format logging messages. +/// +/// ## Why is this bad? +/// The `logging` module provides a mechanism for passing additional values to +/// be logged using the `extra` keyword argument. This is more consistent, more +/// efficient, and less error-prone than formatting the string directly. +/// +/// Using `str.format` to format a logging message requires that Python eagerly +/// format the string, even if the logging statement is never executed (e.g., +/// if the log level is above the level of the logging statement), whereas +/// using the `extra` keyword argument defers formatting until required. +/// +/// Additionally, the use of `extra` will ensure that the values are made +/// available to all handlers, which can then be configured to log the values +/// in a consistent manner. +/// +/// As an alternative to `extra`, passing values as arguments to the logging +/// method can also be used to defer string formatting until required. +/// +/// ## Example +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info("{} - Something happened".format(user)) +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(user_id)s - %(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info("Something happened", extra={"user_id": user}) +/// ``` +/// +/// Or: +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info("%s - Something happened", user) +/// ``` +/// +/// ## References +/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) +/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization) #[violation] pub struct LoggingStringFormat; impl Violation for LoggingStringFormat { #[derive_message_formats] fn message(&self) -> String { - format!("Logging statement uses `string.format()`") + format!("Logging statement uses `str.format`") } } +/// ## What it does +/// Checks for uses of `printf`-style format strings to format logging +/// messages. +/// +/// ## Why is this bad? +/// The `logging` module provides a mechanism for passing additional values to +/// be logged using the `extra` keyword argument. This is more consistent, more +/// efficient, and less error-prone than formatting the string directly. +/// +/// Using `printf`-style format strings to format a logging message requires +/// that Python eagerly format the string, even if the logging statement is +/// never executed (e.g., if the log level is above the level of the logging +/// statement), whereas using the `extra` keyword argument defers formatting +/// until required. +/// +/// Additionally, the use of `extra` will ensure that the values are made +/// available to all handlers, which can then be configured to log the values +/// in a consistent manner. +/// +/// As an alternative to `extra`, passing values as arguments to the logging +/// method can also be used to defer string formatting until required. +/// +/// ## Example +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info("%s - Something happened" % user) +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(user_id)s - %(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info("Something happened", extra=dict(user_id=user)) +/// ``` +/// +/// Or: +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info("%s - Something happened", user) +/// ``` +/// +/// ## References +/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) +/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization) #[violation] pub struct LoggingPercentFormat; @@ -21,6 +135,63 @@ impl Violation for LoggingPercentFormat { } } +/// ## What it does +/// Checks for uses string concatenation via the `+` operator to format logging +/// messages. +/// +/// ## Why is this bad? +/// The `logging` module provides a mechanism for passing additional values to +/// be logged using the `extra` keyword argument. This is more consistent, more +/// efficient, and less error-prone than formatting the string directly. +/// +/// Using concatenation to format a logging message requires that Python +/// eagerly format the string, even if the logging statement is never executed +/// (e.g., if the log level is above the level of the logging statement), +/// whereas using the `extra` keyword argument defers formatting until required. +/// +/// Additionally, the use of `extra` will ensure that the values are made +/// available to all handlers, which can then be configured to log the values +/// in a consistent manner. +/// +/// As an alternative to `extra`, passing values as arguments to the logging +/// method can also be used to defer string formatting until required. +/// +/// ## Example +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info(user + " - Something happened") +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(user_id)s - %(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info("Something happened", extra=dict(user_id=user)) +/// ``` +/// +/// Or: +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info("%s - Something happened", user) +/// ``` +/// +/// ## References +/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) +/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization) #[violation] pub struct LoggingStringConcat; @@ -31,6 +202,62 @@ impl Violation for LoggingStringConcat { } } +/// ## What it does +/// Checks for uses of f-strings to format logging messages. +/// +/// ## Why is this bad? +/// The `logging` module provides a mechanism for passing additional values to +/// be logged using the `extra` keyword argument. This is more consistent, more +/// efficient, and less error-prone than formatting the string directly. +/// +/// Using f-strings to format a logging message requires that Python eagerly +/// format the string, even if the logging statement is never executed (e.g., +/// if the log level is above the level of the logging statement), whereas +/// using the `extra` keyword argument defers formatting until required. +/// +/// Additionally, the use of `extra` will ensure that the values are made +/// available to all handlers, which can then be configured to log the values +/// in a consistent manner. +/// +/// As an alternative to `extra`, passing values as arguments to the logging +/// method can also be used to defer string formatting until required. +/// +/// ## Example +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info(f"{user} - Something happened") +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(user_id)s - %(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info("Something happened", extra=dict(user_id=user)) +/// ``` +/// +/// Or: +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info("%s - Something happened", user) +/// ``` +/// +/// ## References +/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) +/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization) #[violation] pub struct LoggingFString; @@ -41,6 +268,31 @@ impl Violation for LoggingFString { } } +/// ## What it does +/// Checks for uses of `logging.warn` and `logging.Logger.warn`. +/// +/// ## Why is this bad? +/// `logging.warn` and `logging.Logger.warn` are deprecated in favor of +/// `logging.warning` and `logging.Logger.warning`, which are functionally +/// equivalent. +/// +/// ## Example +/// ```python +/// import logging +/// +/// logging.warn("Something happened") +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// logging.warning("Something happened") +/// ``` +/// +/// ## References +/// - [Python documentation: `logging.warning`](https://docs.python.org/3/library/logging.html#logging.warning) +/// - [Python documentation: `logging.Logger.warning`](https://docs.python.org/3/library/logging.html#logging.Logger.warning) #[violation] pub struct LoggingWarn; @@ -55,6 +307,43 @@ impl AlwaysAutofixableViolation for LoggingWarn { } } +/// ## What it does +/// Checks for `extra` keywords in logging statements that clash with +/// `LogRecord` attributes. +/// +/// ## Why is this bad? +/// The `logging` module provides a mechanism for passing additional values to +/// be logged using the `extra` keyword argument. These values are then passed +/// to the `LogRecord` constructor. +/// +/// Providing a value via `extra` that clashes with one of the attributes of +/// the `LogRecord` constructor will raise a `KeyError` when the `LogRecord` is +/// constructed. +/// +/// ## Example +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(name) - %(message)s", level=logging.INFO) +/// +/// username = "Maria" +/// +/// logging.info("Something happened", extra=dict(name=username)) +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(user_id)s - %(message)s", level=logging.INFO) +/// +/// username = "Maria" +/// +/// logging.info("Something happened", extra=dict(user=username)) +/// ``` +/// +/// ## References +/// - [Python documentation: LogRecord attributes](https://docs.python.org/3/library/logging.html#logrecord-attributes) #[violation] pub struct LoggingExtraAttrClash(pub String); @@ -63,11 +352,44 @@ impl Violation for LoggingExtraAttrClash { fn message(&self) -> String { let LoggingExtraAttrClash(key) = self; format!( - "Logging statement uses an extra field that clashes with a LogRecord field: `{key}`" + "Logging statement uses an `extra` field that clashes with a `LogRecord` field: `{key}`" ) } } +/// ## What it does +/// Checks for uses of `logging.error` that pass `exc_info=True`. +/// +/// ## Why is this bad? +/// Calling `logging.error` with `exc_info=True` is equivalent to calling +/// `logging.exception`. Using `logging.exception` is more concise, more +/// readable, and conveys the intent of the logging statement more clearly. +/// +/// ## Example +/// ```python +/// import logging +/// +/// try: +/// ... +/// except ValueError: +/// logging.error("Exception occurred", exc_info=True) +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// try: +/// ... +/// except ValueError: +/// logging.exception("Exception occurred") +/// ``` +/// +/// ## References +/// - [Python documentation: `logging.exception`](https://docs.python.org/3/library/logging.html#logging.exception) +/// - [Python documentation: `exception`](https://docs.python.org/3/library/logging.html#logging.Logger.exception) +/// - [Python documentation: `logging.error`](https://docs.python.org/3/library/logging.html#logging.error) +/// - [Python documentation: `error`](https://docs.python.org/3/library/logging.html#logging.Logger.error) #[violation] pub struct LoggingExcInfo; @@ -78,6 +400,41 @@ impl Violation for LoggingExcInfo { } } +/// ## What it does +/// Checks for redundant `exc_info` keyword arguments in logging statements. +/// +/// ## Why is this bad? +/// `exc_info` is `True` by default for `logging.exception`, and `False` by +/// default for `logging.error`. +/// +/// Passing `exc_info=True` to `logging.exception` calls is redundant, as is +/// passing `exc_info=False` to `logging.error` calls. +/// +/// ## Example +/// ```python +/// import logging +/// +/// try: +/// ... +/// except ValueError: +/// logging.exception("Exception occurred", exc_info=True) +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// try: +/// ... +/// except ValueError: +/// logging.exception("Exception occurred") +/// ``` +/// +/// ## References +/// - [Python documentation: `logging.exception`](https://docs.python.org/3/library/logging.html#logging.exception) +/// - [Python documentation: `exception`](https://docs.python.org/3/library/logging.html#logging.Logger.exception) +/// - [Python documentation: `logging.error`](https://docs.python.org/3/library/logging.html#logging.error) +/// - [Python documentation: `error`](https://docs.python.org/3/library/logging.html#logging.Logger.error) #[violation] pub struct LoggingRedundantExcInfo; From 72f7f11bacbfdc7dff22f594659f81a3eb51e63b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 28 Jun 2023 21:52:11 -0400 Subject: [PATCH 275/447] Use `matches!` for reserved attribute lookup (#5431) --- .../rules/logging_call.rs | 65 ++++++++++--------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs b/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs index 574ae4c488..cbfebaf121 100644 --- a/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs +++ b/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs @@ -13,30 +13,35 @@ use crate::rules::flake8_logging_format::violations::{ LoggingRedundantExcInfo, LoggingStringConcat, LoggingStringFormat, LoggingWarn, }; -const RESERVED_ATTRS: &[&str; 22] = &[ - "args", - "asctime", - "created", - "exc_info", - "exc_text", - "filename", - "funcName", - "levelname", - "levelno", - "lineno", - "module", - "msecs", - "message", - "msg", - "name", - "pathname", - "process", - "processName", - "relativeCreated", - "stack_info", - "thread", - "threadName", -]; +/// Returns `true` if the attribute is a reserved attribute on the `logging` module's `LogRecord` +/// class. +fn is_reserved_attr(attr: &str) -> bool { + matches!( + attr, + "args" + | "asctime" + | "created" + | "exc_info" + | "exc_text" + | "filename" + | "funcName" + | "levelname" + | "levelno" + | "lineno" + | "module" + | "msecs" + | "message" + | "msg" + | "name" + | "pathname" + | "process" + | "processName" + | "relativeCreated" + | "stack_info" + | "thread" + | "threadName" + ) +} /// Check logging messages for violations. fn check_msg(checker: &mut Checker, msg: &Expr) { @@ -90,13 +95,13 @@ fn check_log_record_attr_clash(checker: &mut Checker, extra: &Keyword) { for key in keys { if let Some(key) = &key { if let Expr::Constant(ast::ExprConstant { - value: Constant::Str(string), + value: Constant::Str(attr), .. }) = key { - if RESERVED_ATTRS.contains(&string.as_str()) { + if is_reserved_attr(attr) { checker.diagnostics.push(Diagnostic::new( - LoggingExtraAttrClash(string.to_string()), + LoggingExtraAttrClash(attr.to_string()), key.range(), )); } @@ -113,10 +118,10 @@ fn check_log_record_attr_clash(checker: &mut Checker, extra: &Keyword) { }) { for keyword in keywords { - if let Some(key) = &keyword.arg { - if RESERVED_ATTRS.contains(&key.as_str()) { + if let Some(attr) = &keyword.arg { + if is_reserved_attr(attr) { checker.diagnostics.push(Diagnostic::new( - LoggingExtraAttrClash(key.to_string()), + LoggingExtraAttrClash(attr.to_string()), keyword.range(), )); } From aa887d5a1d92a8656d4407a6d2e942daadce9f15 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 28 Jun 2023 22:00:06 -0400 Subject: [PATCH 276/447] Use "manual" fixability for E731 in shadowed context (#5430) ## Summary This PR makes E731 a "manual" fix in one other context: when the lambda is shadowing another variable in the scope. Function declarations (with shadowing) cause issues for type checkers, and so rewriting an annotation, e.g., in branches of an `if` statement can lead to failures. Closes https://github.com/astral-sh/ruff/issues/5421. --- .../test/fixtures/pycodestyle/E731.py | 160 ++++-- .../pycodestyle/rules/lambda_assignment.rs | 94 ++-- ...les__pycodestyle__tests__E731_E731.py.snap | 480 ++++++++++-------- 3 files changed, 434 insertions(+), 300 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pycodestyle/E731.py b/crates/ruff/resources/test/fixtures/pycodestyle/E731.py index 4464c0c8b9..c207c7dae9 100644 --- a/crates/ruff/resources/test/fixtures/pycodestyle/E731.py +++ b/crates/ruff/resources/test/fixtures/pycodestyle/E731.py @@ -1,51 +1,135 @@ -#: E731 -f = lambda x: 2 * x -#: E731 -f = lambda x: 2 * x -#: E731 -while False: - this = lambda y, z: 2 * x -#: E731 -f = lambda: (yield 1) -#: E731 -f = lambda: (yield from g()) -#: E731 -class F: +def scope(): + # E731 f = lambda x: 2 * x -f = object() -f.method = lambda: "Method" -f = {} -f["a"] = lambda x: x**2 -f = [] -f.append(lambda x: x**2) -f = g = lambda x: x**2 -lambda: "no-op" +def scope(): + # E731 + f = lambda x: 2 * x -# Annotated -from typing import Callable, ParamSpec -P = ParamSpec("P") +def scope(): + # E731 + while False: + this = lambda y, z: 2 * x + + +def scope(): + # E731 + f = lambda: (yield 1) + + +def scope(): + # E731 + f = lambda: (yield from g()) + + +def scope(): + # OK + f = object() + f.method = lambda: "Method" + + +def scope(): + # OK + f = {} + f["a"] = lambda x: x**2 + + +def scope(): + # OK + f = [] + f.append(lambda x: x**2) + + +def scope(): + # OK + f = g = lambda x: x**2 + + +def scope(): + # OK + lambda: "no-op" + + +class Scope: + # E731 + f = lambda x: 2 * x + + +class Scope: + from typing import Callable + + # E731 + f: Callable[[int], int] = lambda x: 2 * x + + +def scope(): + # E731 + from typing import Callable + + x: Callable[[int], int] + if True: + x = lambda: 1 + else: + x = lambda: 2 + return x + + +def scope(): + # E731 + + from typing import Callable, ParamSpec + + # ParamSpec cannot be used in this context, so do not preserve the annotation. + P = ParamSpec("P") + f: Callable[P, int] = lambda *args: len(args) + + +def scope(): + # E731 + + from typing import Callable + + f: Callable[[], None] = lambda: None + + +def scope(): + # E731 + + from typing import Callable + + f: Callable[..., None] = lambda a, b: None + + +def scope(): + # E731 + + from typing import Callable + + f: Callable[[int], int] = lambda x: 2 * x -# ParamSpec cannot be used in this context, so do not preserve the annotation. -f: Callable[P, int] = lambda *args: len(args) -f: Callable[[], None] = lambda: None -f: Callable[..., None] = lambda a, b: None -f: Callable[[int], int] = lambda x: 2 * x # Let's use the `Callable` type from `collections.abc` instead. -from collections.abc import Callable +def scope(): + # E731 -f: Callable[[str, int], str] = lambda a, b: a * b -f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) -f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] + from collections.abc import Callable + + f: Callable[[str, int], str] = lambda a, b: a * b -# Override `Callable` -class Callable: - pass +def scope(): + # E731 + + from collections.abc import Callable + + f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) -# Do not copy the annotation from here on out. -f: Callable[[str, int], str] = lambda a, b: a * b +def scope(): + # E731 + + from collections.abc import Callable + + f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] diff --git a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs index 8cc70a26c9..8e3f4930e2 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs @@ -63,57 +63,69 @@ pub(crate) fn lambda_assignment( annotation: Option<&Expr>, stmt: &Stmt, ) { - if let Expr::Name(ast::ExprName { id, .. }) = target { - if let Expr::Lambda(ast::ExprLambda { args, body, .. }) = value { - let mut diagnostic = Diagnostic::new( - LambdaAssignment { - name: id.to_string(), - }, - stmt.range(), - ); + let Expr::Name(ast::ExprName { id, .. }) = target else { + return; + }; - // If the assignment is in a class body, it might not be safe - // to replace it because the assignment might be - // carrying a type annotation that will be used by some - // package like dataclasses, which wouldn't consider the - // rewritten function definition to be equivalent. - // See https://github.com/astral-sh/ruff/issues/3046 - if checker.patch(diagnostic.kind.rule()) - && !checker.semantic().scope().kind.is_class() - && !has_leading_content(stmt.start(), checker.locator) - && !has_trailing_content(stmt.end(), checker.locator) + let Expr::Lambda(ast::ExprLambda { args, body, .. }) = value else { + return; + }; + + let mut diagnostic = Diagnostic::new( + LambdaAssignment { + name: id.to_string(), + }, + stmt.range(), + ); + + if checker.patch(diagnostic.kind.rule()) { + if !has_leading_content(stmt.start(), checker.locator) + && !has_trailing_content(stmt.end(), checker.locator) + { + let first_line = checker.locator.line(stmt.start()); + let indentation = leading_indentation(first_line); + let mut indented = String::new(); + for (idx, line) in function( + id, + args, + body, + annotation, + checker.semantic(), + checker.generator(), + ) + .universal_newlines() + .enumerate() { - let first_line = checker.locator.line(stmt.start()); - let indentation = leading_indentation(first_line); - let mut indented = String::new(); - for (idx, line) in function( - id, - args, - body, - annotation, - checker.semantic(), - checker.generator(), - ) - .universal_newlines() - .enumerate() - { - if idx == 0 { - indented.push_str(&line); - } else { - indented.push_str(checker.stylist.line_ending().as_str()); - indented.push_str(indentation); - indented.push_str(&line); - } + if idx == 0 { + indented.push_str(&line); + } else { + indented.push_str(checker.stylist.line_ending().as_str()); + indented.push_str(indentation); + indented.push_str(&line); } + } + + // If the assignment is in a class body, it might not be safe to replace it because the + // assignment might be carrying a type annotation that will be used by some package like + // dataclasses, which wouldn't consider the rewritten function definition to be + // equivalent. Similarly, if the lambda is shadowing a variable in the current scope, + // rewriting it as a function declaration may break type-checking. + // See: https://github.com/astral-sh/ruff/issues/3046 + // See: https://github.com/astral-sh/ruff/issues/5421 + if (annotation.is_some() && checker.semantic().scope().kind.is_class()) + || checker.semantic().scope().has(id) + { + diagnostic.set_fix(Fix::manual(Edit::range_replacement(indented, stmt.range()))); + } else { diagnostic.set_fix(Fix::suggested(Edit::range_replacement( indented, stmt.range(), ))); } - - checker.diagnostics.push(diagnostic); } } + + checker.diagnostics.push(diagnostic); } /// Extract the argument types and return type from a `Callable` annotation. diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E731_E731.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E731_E731.py.snap index 1657c1069e..440cc87ebd 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E731_E731.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E731_E731.py.snap @@ -1,284 +1,322 @@ --- source: crates/ruff/src/rules/pycodestyle/mod.rs --- -E731.py:2:1: E731 [*] Do not assign a `lambda` expression, use a `def` +E731.py:3:5: E731 [*] Do not assign a `lambda` expression, use a `def` | -1 | #: E731 -2 | f = lambda x: 2 * x - | ^^^^^^^^^^^^^^^^^^^ E731 -3 | #: E731 -4 | f = lambda x: 2 * x +1 | def scope(): +2 | # E731 +3 | f = lambda x: 2 * x + | ^^^^^^^^^^^^^^^^^^^ E731 | = help: Rewrite `f` as a `def` ℹ Suggested fix -1 1 | #: E731 -2 |-f = lambda x: 2 * x - 2 |+def f(x): - 3 |+ return 2 * x -3 4 | #: E731 -4 5 | f = lambda x: 2 * x -5 6 | #: E731 +1 1 | def scope(): +2 2 | # E731 +3 |- f = lambda x: 2 * x + 3 |+ def f(x): + 4 |+ return 2 * x +4 5 | +5 6 | +6 7 | def scope(): -E731.py:4:1: E731 [*] Do not assign a `lambda` expression, use a `def` +E731.py:8:5: E731 [*] Do not assign a `lambda` expression, use a `def` | -2 | f = lambda x: 2 * x -3 | #: E731 -4 | f = lambda x: 2 * x - | ^^^^^^^^^^^^^^^^^^^ E731 -5 | #: E731 -6 | while False: +6 | def scope(): +7 | # E731 +8 | f = lambda x: 2 * x + | ^^^^^^^^^^^^^^^^^^^ E731 | = help: Rewrite `f` as a `def` ℹ Suggested fix -1 1 | #: E731 -2 2 | f = lambda x: 2 * x -3 3 | #: E731 -4 |-f = lambda x: 2 * x - 4 |+def f(x): - 5 |+ return 2 * x -5 6 | #: E731 -6 7 | while False: -7 8 | this = lambda y, z: 2 * x +5 5 | +6 6 | def scope(): +7 7 | # E731 +8 |- f = lambda x: 2 * x + 8 |+ def f(x): + 9 |+ return 2 * x +9 10 | +10 11 | +11 12 | def scope(): -E731.py:7:5: E731 [*] Do not assign a `lambda` expression, use a `def` - | -5 | #: E731 -6 | while False: -7 | this = lambda y, z: 2 * x - | ^^^^^^^^^^^^^^^^^^^^^^^^^ E731 -8 | #: E731 -9 | f = lambda: (yield 1) - | - = help: Rewrite `this` as a `def` +E731.py:14:9: E731 [*] Do not assign a `lambda` expression, use a `def` + | +12 | # E731 +13 | while False: +14 | this = lambda y, z: 2 * x + | ^^^^^^^^^^^^^^^^^^^^^^^^^ E731 + | + = help: Rewrite `this` as a `def` ℹ Suggested fix -4 4 | f = lambda x: 2 * x -5 5 | #: E731 -6 6 | while False: -7 |- this = lambda y, z: 2 * x - 7 |+ def this(y, z): - 8 |+ return 2 * x -8 9 | #: E731 -9 10 | f = lambda: (yield 1) -10 11 | #: E731 +11 11 | def scope(): +12 12 | # E731 +13 13 | while False: +14 |- this = lambda y, z: 2 * x + 14 |+ def this(y, z): + 15 |+ return 2 * x +15 16 | +16 17 | +17 18 | def scope(): -E731.py:9:1: E731 [*] Do not assign a `lambda` expression, use a `def` +E731.py:19:5: E731 [*] Do not assign a `lambda` expression, use a `def` | - 7 | this = lambda y, z: 2 * x - 8 | #: E731 - 9 | f = lambda: (yield 1) - | ^^^^^^^^^^^^^^^^^^^^^ E731 -10 | #: E731 -11 | f = lambda: (yield from g()) +17 | def scope(): +18 | # E731 +19 | f = lambda: (yield 1) + | ^^^^^^^^^^^^^^^^^^^^^ E731 | = help: Rewrite `f` as a `def` ℹ Suggested fix -6 6 | while False: -7 7 | this = lambda y, z: 2 * x -8 8 | #: E731 -9 |-f = lambda: (yield 1) - 9 |+def f(): - 10 |+ return (yield 1) -10 11 | #: E731 -11 12 | f = lambda: (yield from g()) -12 13 | #: E731 +16 16 | +17 17 | def scope(): +18 18 | # E731 +19 |- f = lambda: (yield 1) + 19 |+ def f(): + 20 |+ return (yield 1) +20 21 | +21 22 | +22 23 | def scope(): -E731.py:11:1: E731 [*] Do not assign a `lambda` expression, use a `def` +E731.py:24:5: E731 [*] Do not assign a `lambda` expression, use a `def` | - 9 | f = lambda: (yield 1) -10 | #: E731 -11 | f = lambda: (yield from g()) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 -12 | #: E731 -13 | class F: +22 | def scope(): +23 | # E731 +24 | f = lambda: (yield from g()) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 | = help: Rewrite `f` as a `def` ℹ Suggested fix -8 8 | #: E731 -9 9 | f = lambda: (yield 1) -10 10 | #: E731 -11 |-f = lambda: (yield from g()) - 11 |+def f(): - 12 |+ return (yield from g()) -12 13 | #: E731 -13 14 | class F: -14 15 | f = lambda x: 2 * x +21 21 | +22 22 | def scope(): +23 23 | # E731 +24 |- f = lambda: (yield from g()) + 24 |+ def f(): + 25 |+ return (yield from g()) +25 26 | +26 27 | +27 28 | def scope(): -E731.py:14:5: E731 Do not assign a `lambda` expression, use a `def` +E731.py:57:5: E731 [*] Do not assign a `lambda` expression, use a `def` | -12 | #: E731 -13 | class F: -14 | f = lambda x: 2 * x +55 | class Scope: +56 | # E731 +57 | f = lambda x: 2 * x | ^^^^^^^^^^^^^^^^^^^ E731 | = help: Rewrite `f` as a `def` -E731.py:32:1: E731 [*] Do not assign a `lambda` expression, use a `def` +ℹ Suggested fix +54 54 | +55 55 | class Scope: +56 56 | # E731 +57 |- f = lambda x: 2 * x + 57 |+ def f(x): + 58 |+ return 2 * x +58 59 | +59 60 | +60 61 | class Scope: + +E731.py:64:5: E731 [*] Do not assign a `lambda` expression, use a `def` | -31 | # ParamSpec cannot be used in this context, so do not preserve the annotation. -32 | f: Callable[P, int] = lambda *args: len(args) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 -33 | f: Callable[[], None] = lambda: None -34 | f: Callable[..., None] = lambda a, b: None +63 | # E731 +64 | f: Callable[[int], int] = lambda x: 2 * x + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 + | + = help: Rewrite `f` as a `def` + +ℹ Possible fix +61 61 | from typing import Callable +62 62 | +63 63 | # E731 +64 |- f: Callable[[int], int] = lambda x: 2 * x + 64 |+ def f(x: int) -> int: + 65 |+ return 2 * x +65 66 | +66 67 | +67 68 | def scope(): + +E731.py:73:9: E731 [*] Do not assign a `lambda` expression, use a `def` + | +71 | x: Callable[[int], int] +72 | if True: +73 | x = lambda: 1 + | ^^^^^^^^^^^^^ E731 +74 | else: +75 | x = lambda: 2 + | + = help: Rewrite `x` as a `def` + +ℹ Possible fix +70 70 | +71 71 | x: Callable[[int], int] +72 72 | if True: +73 |- x = lambda: 1 + 73 |+ def x(): + 74 |+ return 1 +74 75 | else: +75 76 | x = lambda: 2 +76 77 | return x + +E731.py:75:9: E731 [*] Do not assign a `lambda` expression, use a `def` + | +73 | x = lambda: 1 +74 | else: +75 | x = lambda: 2 + | ^^^^^^^^^^^^^ E731 +76 | return x + | + = help: Rewrite `x` as a `def` + +ℹ Possible fix +72 72 | if True: +73 73 | x = lambda: 1 +74 74 | else: +75 |- x = lambda: 2 + 75 |+ def x(): + 76 |+ return 2 +76 77 | return x +77 78 | +78 79 | + +E731.py:86:5: E731 [*] Do not assign a `lambda` expression, use a `def` + | +84 | # ParamSpec cannot be used in this context, so do not preserve the annotation. +85 | P = ParamSpec("P") +86 | f: Callable[P, int] = lambda *args: len(args) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 | = help: Rewrite `f` as a `def` ℹ Suggested fix -29 29 | P = ParamSpec("P") -30 30 | -31 31 | # ParamSpec cannot be used in this context, so do not preserve the annotation. -32 |-f: Callable[P, int] = lambda *args: len(args) - 32 |+def f(*args): - 33 |+ return len(args) -33 34 | f: Callable[[], None] = lambda: None -34 35 | f: Callable[..., None] = lambda a, b: None -35 36 | f: Callable[[int], int] = lambda x: 2 * x +83 83 | +84 84 | # ParamSpec cannot be used in this context, so do not preserve the annotation. +85 85 | P = ParamSpec("P") +86 |- f: Callable[P, int] = lambda *args: len(args) + 86 |+ def f(*args): + 87 |+ return len(args) +87 88 | +88 89 | +89 90 | def scope(): -E731.py:33:1: E731 [*] Do not assign a `lambda` expression, use a `def` +E731.py:94:5: E731 [*] Do not assign a `lambda` expression, use a `def` | -31 | # ParamSpec cannot be used in this context, so do not preserve the annotation. -32 | f: Callable[P, int] = lambda *args: len(args) -33 | f: Callable[[], None] = lambda: None - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 -34 | f: Callable[..., None] = lambda a, b: None -35 | f: Callable[[int], int] = lambda x: 2 * x +92 | from typing import Callable +93 | +94 | f: Callable[[], None] = lambda: None + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 | = help: Rewrite `f` as a `def` ℹ Suggested fix -30 30 | -31 31 | # ParamSpec cannot be used in this context, so do not preserve the annotation. -32 32 | f: Callable[P, int] = lambda *args: len(args) -33 |-f: Callable[[], None] = lambda: None - 33 |+def f() -> None: - 34 |+ return None -34 35 | f: Callable[..., None] = lambda a, b: None -35 36 | f: Callable[[int], int] = lambda x: 2 * x -36 37 | +91 91 | +92 92 | from typing import Callable +93 93 | +94 |- f: Callable[[], None] = lambda: None + 94 |+ def f() -> None: + 95 |+ return None +95 96 | +96 97 | +97 98 | def scope(): -E731.py:34:1: E731 [*] Do not assign a `lambda` expression, use a `def` - | -32 | f: Callable[P, int] = lambda *args: len(args) -33 | f: Callable[[], None] = lambda: None -34 | f: Callable[..., None] = lambda a, b: None - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 -35 | f: Callable[[int], int] = lambda x: 2 * x - | - = help: Rewrite `f` as a `def` +E731.py:102:5: E731 [*] Do not assign a `lambda` expression, use a `def` + | +100 | from typing import Callable +101 | +102 | f: Callable[..., None] = lambda a, b: None + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 + | + = help: Rewrite `f` as a `def` ℹ Suggested fix -31 31 | # ParamSpec cannot be used in this context, so do not preserve the annotation. -32 32 | f: Callable[P, int] = lambda *args: len(args) -33 33 | f: Callable[[], None] = lambda: None -34 |-f: Callable[..., None] = lambda a, b: None - 34 |+def f(a, b) -> None: - 35 |+ return None -35 36 | f: Callable[[int], int] = lambda x: 2 * x -36 37 | -37 38 | # Let's use the `Callable` type from `collections.abc` instead. +99 99 | +100 100 | from typing import Callable +101 101 | +102 |- f: Callable[..., None] = lambda a, b: None + 102 |+ def f(a, b) -> None: + 103 |+ return None +103 104 | +104 105 | +105 106 | def scope(): -E731.py:35:1: E731 [*] Do not assign a `lambda` expression, use a `def` - | -33 | f: Callable[[], None] = lambda: None -34 | f: Callable[..., None] = lambda a, b: None -35 | f: Callable[[int], int] = lambda x: 2 * x - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 -36 | -37 | # Let's use the `Callable` type from `collections.abc` instead. - | - = help: Rewrite `f` as a `def` +E731.py:110:5: E731 [*] Do not assign a `lambda` expression, use a `def` + | +108 | from typing import Callable +109 | +110 | f: Callable[[int], int] = lambda x: 2 * x + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 + | + = help: Rewrite `f` as a `def` ℹ Suggested fix -32 32 | f: Callable[P, int] = lambda *args: len(args) -33 33 | f: Callable[[], None] = lambda: None -34 34 | f: Callable[..., None] = lambda a, b: None -35 |-f: Callable[[int], int] = lambda x: 2 * x - 35 |+def f(x: int) -> int: - 36 |+ return 2 * x -36 37 | -37 38 | # Let's use the `Callable` type from `collections.abc` instead. -38 39 | from collections.abc import Callable +107 107 | +108 108 | from typing import Callable +109 109 | +110 |- f: Callable[[int], int] = lambda x: 2 * x + 110 |+ def f(x: int) -> int: + 111 |+ return 2 * x +111 112 | +112 113 | +113 114 | # Let's use the `Callable` type from `collections.abc` instead. -E731.py:40:1: E731 [*] Do not assign a `lambda` expression, use a `def` - | -38 | from collections.abc import Callable -39 | -40 | f: Callable[[str, int], str] = lambda a, b: a * b - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 -41 | f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) -42 | f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] - | - = help: Rewrite `f` as a `def` +E731.py:119:5: E731 [*] Do not assign a `lambda` expression, use a `def` + | +117 | from collections.abc import Callable +118 | +119 | f: Callable[[str, int], str] = lambda a, b: a * b + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 + | + = help: Rewrite `f` as a `def` ℹ Suggested fix -37 37 | # Let's use the `Callable` type from `collections.abc` instead. -38 38 | from collections.abc import Callable -39 39 | -40 |-f: Callable[[str, int], str] = lambda a, b: a * b - 40 |+def f(a: str, b: int) -> str: - 41 |+ return a * b -41 42 | f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) -42 43 | f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] -43 44 | +116 116 | +117 117 | from collections.abc import Callable +118 118 | +119 |- f: Callable[[str, int], str] = lambda a, b: a * b + 119 |+ def f(a: str, b: int) -> str: + 120 |+ return a * b +120 121 | +121 122 | +122 123 | def scope(): -E731.py:41:1: E731 [*] Do not assign a `lambda` expression, use a `def` - | -40 | f: Callable[[str, int], str] = lambda a, b: a * b -41 | f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 -42 | f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] - | - = help: Rewrite `f` as a `def` +E731.py:127:5: E731 [*] Do not assign a `lambda` expression, use a `def` + | +125 | from collections.abc import Callable +126 | +127 | f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 + | + = help: Rewrite `f` as a `def` ℹ Suggested fix -38 38 | from collections.abc import Callable -39 39 | -40 40 | f: Callable[[str, int], str] = lambda a, b: a * b -41 |-f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) - 41 |+def f(a: str, b: int) -> tuple[str, int]: - 42 |+ return a, b -42 43 | f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] -43 44 | -44 45 | +124 124 | +125 125 | from collections.abc import Callable +126 126 | +127 |- f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) + 127 |+ def f(a: str, b: int) -> tuple[str, int]: + 128 |+ return a, b +128 129 | +129 130 | +130 131 | def scope(): -E731.py:42:1: E731 [*] Do not assign a `lambda` expression, use a `def` - | -40 | f: Callable[[str, int], str] = lambda a, b: a * b -41 | f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) -42 | f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 - | - = help: Rewrite `f` as a `def` +E731.py:135:5: E731 [*] Do not assign a `lambda` expression, use a `def` + | +133 | from collections.abc import Callable +134 | +135 | f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 + | + = help: Rewrite `f` as a `def` ℹ Suggested fix -39 39 | -40 40 | f: Callable[[str, int], str] = lambda a, b: a * b -41 41 | f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) -42 |-f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] - 42 |+def f(a: str, b: int, /, c: list[str]) -> list[str]: - 43 |+ return [*c, a * b] -43 44 | -44 45 | -45 46 | # Override `Callable` - -E731.py:51:1: E731 [*] Do not assign a `lambda` expression, use a `def` - | -50 | # Do not copy the annotation from here on out. -51 | f: Callable[[str, int], str] = lambda a, b: a * b - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 - | - = help: Rewrite `f` as a `def` - -ℹ Suggested fix -48 48 | -49 49 | -50 50 | # Do not copy the annotation from here on out. -51 |-f: Callable[[str, int], str] = lambda a, b: a * b - 51 |+def f(a, b): - 52 |+ return a * b +132 132 | +133 133 | from collections.abc import Callable +134 134 | +135 |- f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] + 135 |+ def f(a: str, b: int, /, c: list[str]) -> list[str]: + 136 |+ return [*c, a * b] From a973019358480877bd3fa9ab2ef1ce26dec7c23b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 28 Jun 2023 22:42:27 -0400 Subject: [PATCH 277/447] Rewrite a variety of `.contains()` calls as `matches!` statements (#5432) ## Summary These have the potential to be much more efficient, as we've seen in the past. --- .../src/rules/flake8_boolean_trap/helpers.rs | 75 ++++++++------- ...an_default_value_in_function_definition.rs | 7 +- .../rules/check_positional_boolean_in_def.rs | 4 +- .../flake8_pyi/rules/non_self_return_type.rs | 36 +++---- .../rules/flake8_pytest_style/rules/patch.rs | 59 ++++++------ .../flake8_simplify/rules/ast_unary_op.rs | 11 ++- .../rules/future_feature_not_defined.rs | 18 ++-- .../pyupgrade/rules/deprecated_import.rs | 18 ++-- .../rules/unnecessary_builtin_import.rs | 94 +++++++++---------- crates/ruff_python_stdlib/src/future.rs | 30 +++--- 10 files changed, 183 insertions(+), 169 deletions(-) diff --git a/crates/ruff/src/rules/flake8_boolean_trap/helpers.rs b/crates/ruff/src/rules/flake8_boolean_trap/helpers.rs index 2c397470b7..01cc630de6 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/helpers.rs +++ b/crates/ruff/src/rules/flake8_boolean_trap/helpers.rs @@ -4,50 +4,57 @@ use ruff_diagnostics::{Diagnostic, DiagnosticKind}; use crate::checkers::ast::Checker; -pub(super) const FUNC_CALL_NAME_ALLOWLIST: &[&str] = &[ - "append", - "assertEqual", - "assertEquals", - "assertNotEqual", - "assertNotEquals", - "bool", - "bytes", - "count", - "failIfEqual", - "failUnlessEqual", - "float", - "fromkeys", - "get", - "getattr", - "getboolean", - "getfloat", - "getint", - "index", - "insert", - "int", - "param", - "pop", - "remove", - "set_blocking", - "set_enabled", - "setattr", - "__setattr__", - "setdefault", - "str", -]; +/// Returns `true` if a function call is allowed to use a boolean trap. +pub(super) fn is_allowed_func_call(name: &str) -> bool { + matches!( + name, + "append" + | "assertEqual" + | "assertEquals" + | "assertNotEqual" + | "assertNotEquals" + | "bool" + | "bytes" + | "count" + | "failIfEqual" + | "failUnlessEqual" + | "float" + | "fromkeys" + | "get" + | "getattr" + | "getboolean" + | "getfloat" + | "getint" + | "index" + | "insert" + | "int" + | "param" + | "pop" + | "remove" + | "set_blocking" + | "set_enabled" + | "setattr" + | "__setattr__" + | "setdefault" + | "str" + ) +} -pub(super) const FUNC_DEF_NAME_ALLOWLIST: &[&str] = &["__setitem__"]; +/// Returns `true` if a function definition is allowed to use a boolean trap. +pub(super) fn is_allowed_func_def(name: &str) -> bool { + matches!(name, "__setitem__") +} /// Returns `true` if an argument is allowed to use a boolean trap. To return /// `true`, the function name must be explicitly allowed, and the argument must /// be either the first or second argument in the call. pub(super) fn allow_boolean_trap(func: &Expr) -> bool { if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func { - return FUNC_CALL_NAME_ALLOWLIST.contains(&attr.as_ref()); + return is_allowed_func_call(attr); } if let Expr::Name(ast::ExprName { id, .. }) = func { - return FUNC_CALL_NAME_ALLOWLIST.contains(&id.as_ref()); + return is_allowed_func_call(id); } false diff --git a/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_default_value_in_function_definition.rs b/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_default_value_in_function_definition.rs index fda0ff5af4..1e853abb87 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_default_value_in_function_definition.rs +++ b/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_default_value_in_function_definition.rs @@ -1,14 +1,11 @@ use rustpython_parser::ast::{ArgWithDefault, Arguments, Decorator}; use ruff_diagnostics::Violation; - use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::collect_call_path; use crate::checkers::ast::Checker; -use crate::rules::flake8_boolean_trap::helpers::add_if_boolean; - -use super::super::helpers::FUNC_DEF_NAME_ALLOWLIST; +use crate::rules::flake8_boolean_trap::helpers::{add_if_boolean, is_allowed_func_def}; /// ## What it does /// Checks for the use of booleans as default values in function definitions. @@ -64,7 +61,7 @@ pub(crate) fn check_boolean_default_value_in_function_definition( decorator_list: &[Decorator], arguments: &Arguments, ) { - if FUNC_DEF_NAME_ALLOWLIST.contains(&name) { + if is_allowed_func_def(name) { return; } diff --git a/crates/ruff/src/rules/flake8_boolean_trap/rules/check_positional_boolean_in_def.rs b/crates/ruff/src/rules/flake8_boolean_trap/rules/check_positional_boolean_in_def.rs index 57e50e7be7..68dc43238f 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/rules/check_positional_boolean_in_def.rs +++ b/crates/ruff/src/rules/flake8_boolean_trap/rules/check_positional_boolean_in_def.rs @@ -6,7 +6,7 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::collect_call_path; use crate::checkers::ast::Checker; -use crate::rules::flake8_boolean_trap::helpers::FUNC_DEF_NAME_ALLOWLIST; +use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def; /// ## What it does /// Checks for boolean positional arguments in function definitions. @@ -82,7 +82,7 @@ pub(crate) fn check_positional_boolean_in_def( decorator_list: &[Decorator], arguments: &Arguments, ) { - if FUNC_DEF_NAME_ALLOWLIST.contains(&name) { + if is_allowed_func_def(name) { return; } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs b/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs index 04d76b4fb0..56f4cd9da0 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs @@ -155,7 +155,7 @@ pub(crate) fn non_self_return_type( } // In-place methods that are expected to return `Self`. - if INPLACE_BINOP_METHODS.contains(&name) { + if is_inplace_bin_op(name) { if !is_self(returns, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( NonSelfReturnType { @@ -214,21 +214,25 @@ pub(crate) fn non_self_return_type( } } -const INPLACE_BINOP_METHODS: &[&str] = &[ - "__iadd__", - "__isub__", - "__imul__", - "__imatmul__", - "__itruediv__", - "__ifloordiv__", - "__imod__", - "__ipow__", - "__ilshift__", - "__irshift__", - "__iand__", - "__ixor__", - "__ior__", -]; +/// Returns `true` if the method is an in-place binary operator. +fn is_inplace_bin_op(name: &str) -> bool { + matches!( + name, + "__iadd__" + | "__isub__" + | "__imul__" + | "__imatmul__" + | "__itruediv__" + | "__ifloordiv__" + | "__imod__" + | "__ipow__" + | "__ilshift__" + | "__irshift__" + | "__iand__" + | "__ixor__" + | "__ior__" + ) +} /// Return `true` if the given expression resolves to the given name. fn is_name(expr: &Expr, name: &str) -> bool { 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 2bc47f6952..7fb2da8f1f 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/patch.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/patch.rs @@ -3,7 +3,7 @@ use rustpython_parser::ast::{self, Expr, Keyword, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::compose_call_path; +use ruff_python_ast::call_path::collect_call_path; use ruff_python_ast::helpers::{collect_arg_names, SimpleCallArgs}; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; @@ -18,26 +18,6 @@ impl Violation for PytestPatchWithLambda { } } -const PATCH_NAMES: &[&str] = &[ - "mocker.patch", - "class_mocker.patch", - "module_mocker.patch", - "package_mocker.patch", - "session_mocker.patch", - "mock.patch", - "unittest.mock.patch", -]; - -const PATCH_OBJECT_NAMES: &[&str] = &[ - "mocker.patch.object", - "class_mocker.patch.object", - "module_mocker.patch.object", - "package_mocker.patch.object", - "session_mocker.patch.object", - "mock.patch.object", - "unittest.mock.patch.object", -]; - #[derive(Default)] /// Visitor that checks references the argument names in the lambda body. struct LambdaBodyVisitor<'a> { @@ -98,14 +78,35 @@ pub(crate) fn patch_with_lambda( args: &[Expr], keywords: &[Keyword], ) -> Option { - if let Some(call_path) = compose_call_path(call) { - if PATCH_NAMES.contains(&call_path.as_str()) { - check_patch_call(call, args, keywords, 1) - } else if PATCH_OBJECT_NAMES.contains(&call_path.as_str()) { - check_patch_call(call, args, keywords, 2) - } else { - None - } + let call_path = collect_call_path(call)?; + + if matches!( + call_path.as_slice(), + [ + "mocker" + | "class_mocker" + | "module_mocker" + | "package_mocker" + | "session_mocker" + | "mock", + "patch" + ] | ["unittest", "mock", "patch"] + ) { + check_patch_call(call, args, keywords, 1) + } else if matches!( + call_path.as_slice(), + [ + "mocker" + | "class_mocker" + | "module_mocker" + | "package_mocker" + | "session_mocker" + | "mock", + "patch", + "object" + ] | ["unittest", "mock", "patch", "object"] + ) { + check_patch_call(call, args, keywords, 2) } else { None } 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 f0385b7560..05b96f3e19 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 @@ -119,7 +119,12 @@ impl AlwaysAutofixableViolation for DoubleNegation { } } -const DUNDER_METHODS: &[&str] = &["__eq__", "__ne__", "__lt__", "__le__", "__gt__", "__ge__"]; +fn is_dunder_method(name: &str) -> bool { + matches!( + name, + "__eq__" | "__ne__" | "__lt__" | "__le__" | "__gt__" | "__ge__" + ) +} fn is_exception_check(stmt: &Stmt) -> bool { let Stmt::If(ast::StmtIf {test: _, body, orelse: _, range: _ })= stmt else { @@ -159,7 +164,7 @@ pub(crate) fn negation_with_equal_op( | ScopeKind::AsyncFunction(ast::StmtAsyncFunctionDef { name, .. }) = &checker.semantic().scope().kind { - if DUNDER_METHODS.contains(&name.as_str()) { + if is_dunder_method(name) { return; } } @@ -211,7 +216,7 @@ pub(crate) fn negation_with_not_equal_op( | ScopeKind::AsyncFunction(ast::StmtAsyncFunctionDef { name, .. }) = &checker.semantic().scope().kind { - if DUNDER_METHODS.contains(&name.as_str()) { + if is_dunder_method(name) { return; } } diff --git a/crates/ruff/src/rules/pyflakes/rules/future_feature_not_defined.rs b/crates/ruff/src/rules/pyflakes/rules/future_feature_not_defined.rs index 11dc78d70b..a00d5267e3 100644 --- a/crates/ruff/src/rules/pyflakes/rules/future_feature_not_defined.rs +++ b/crates/ruff/src/rules/pyflakes/rules/future_feature_not_defined.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{Alias, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_stdlib::future::ALL_FEATURE_NAMES; +use ruff_python_stdlib::future::is_feature_name; use crate::checkers::ast::Checker; @@ -30,12 +30,14 @@ impl Violation for FutureFeatureNotDefined { } pub(crate) fn future_feature_not_defined(checker: &mut Checker, alias: &Alias) { - if !ALL_FEATURE_NAMES.contains(&alias.name.as_str()) { - checker.diagnostics.push(Diagnostic::new( - FutureFeatureNotDefined { - name: alias.name.to_string(), - }, - alias.range(), - )); + if is_feature_name(&alias.name) { + return; } + + checker.diagnostics.push(Diagnostic::new( + FutureFeatureNotDefined { + name: alias.name.to_string(), + }, + alias.range(), + )); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs index f178e3e8cb..314df7705c 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs @@ -94,15 +94,13 @@ impl Violation for DeprecatedImport { } } -// A list of modules that may involve import rewrites. -const RELEVANT_MODULES: &[&str] = &[ - "collections", - "pipes", - "mypy_extensions", - "typing_extensions", - "typing", - "typing.re", -]; +/// Returns `true` if the module may contain deprecated imports. +fn is_relevant_module(module: &str) -> bool { + matches!( + module, + "collections" | "pipes" | "mypy_extensions" | "typing_extensions" | "typing" | "typing.re" + ) +} // Members of `collections` that were moved to `collections.abc`. const COLLECTIONS_TO_ABC: &[&str] = &[ @@ -560,7 +558,7 @@ pub(crate) fn deprecated_import( return; }; - if !RELEVANT_MODULES.contains(&module) { + if !is_relevant_module(module) { return; } diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs index b66ed7eaaf..60d856bec0 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs @@ -52,37 +52,6 @@ impl AlwaysAutofixableViolation for UnnecessaryBuiltinImport { } } -const BUILTINS: &[&str] = &[ - "*", - "ascii", - "bytes", - "chr", - "dict", - "filter", - "hex", - "input", - "int", - "isinstance", - "list", - "map", - "max", - "min", - "next", - "object", - "oct", - "open", - "pow", - "range", - "round", - "str", - "super", - "zip", -]; -const IO: &[&str] = &["open"]; -const SIX_MOVES_BUILTINS: &[&str] = BUILTINS; -const SIX: &[&str] = &["callable", "next"]; -const SIX_MOVES: &[&str] = &["filter", "input", "map", "range", "zip"]; - /// UP029 pub(crate) fn unnecessary_builtin_import( checker: &mut Checker, @@ -90,26 +59,53 @@ pub(crate) fn unnecessary_builtin_import( module: &str, names: &[Alias], ) { - let deprecated_names = match module { - "builtins" => BUILTINS, - "io" => IO, - "six" => SIX, - "six.moves" => SIX_MOVES, - "six.moves.builtins" => SIX_MOVES_BUILTINS, - _ => return, - }; - - // Do this with a filter? - let mut unused_imports: Vec<&Alias> = vec![]; - for alias in names { - if alias.asname.is_some() { - continue; - } - if deprecated_names.contains(&alias.name.as_str()) { - unused_imports.push(alias); - } + // Ignore irrelevant modules. + if !matches!( + module, + "builtins" | "io" | "six" | "six.moves" | "six.moves.builtins" + ) { + return; } + // Identify unaliased, builtin imports. + let unused_imports: Vec<&Alias> = names + .iter() + .filter(|alias| alias.asname.is_none()) + .filter(|alias| { + matches!( + (module, alias.name.as_str()), + ( + "builtins" | "six.moves.builtins", + "*" | "ascii" + | "bytes" + | "chr" + | "dict" + | "filter" + | "hex" + | "input" + | "int" + | "isinstance" + | "list" + | "map" + | "max" + | "min" + | "next" + | "object" + | "oct" + | "open" + | "pow" + | "range" + | "round" + | "str" + | "super" + | "zip" + ) | ("io", "open") + | ("six", "callable" | "next") + | ("six.moves", "filter" | "input" | "map" | "range" | "zip") + ) + }) + .collect(); + if unused_imports.is_empty() { return; } diff --git a/crates/ruff_python_stdlib/src/future.rs b/crates/ruff_python_stdlib/src/future.rs index 0e7bbb3c0c..b1adaadedf 100644 --- a/crates/ruff_python_stdlib/src/future.rs +++ b/crates/ruff_python_stdlib/src/future.rs @@ -1,13 +1,17 @@ -/// A copy of `__future__.all_feature_names`. -pub const ALL_FEATURE_NAMES: &[&str] = &[ - "nested_scopes", - "generators", - "division", - "absolute_import", - "with_statement", - "print_function", - "unicode_literals", - "barry_as_FLUFL", - "generator_stop", - "annotations", -]; +/// Returns `true` if `name` is a valid `__future__` feature name, as defined by +/// `__future__.all_feature_names`. +pub fn is_feature_name(name: &str) -> bool { + matches!( + name, + "nested_scopes" + | "generators" + | "division" + | "absolute_import" + | "with_statement" + | "print_function" + | "unicode_literals" + | "barry_as_FLUFL" + | "generator_stop" + | "annotations" + ) +} From ca5e10b5eac6fe7c73cd32f08ea25efc39f32cc3 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Thu, 29 Jun 2023 07:07:33 +0100 Subject: [PATCH 278/447] format StmtTryStar (#5418) --- .../test/fixtures/ruff/statement/try.py | 19 +++ .../other/except_handler_except_handler.rs | 34 ++++- .../src/statement/stmt_try.rs | 123 +++++++++++++++--- .../src/statement/stmt_try_star.rs | 13 +- .../snapshots/format@statement__try.py.snap | 39 ++++++ 5 files changed, 205 insertions(+), 23 deletions(-) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py index f50dcab4c4..11d07349bc 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py @@ -70,3 +70,22 @@ try: except: a = 10 # trailing comment1 b = 11 # trailing comment2 + + +# try/except*, mostly the same as try +try: # try + ... + # end of body +# before except +except* (Exception, ValueError) as exc: # except line + ... +# before except 2 +except* KeyError as key: # except line 2 + ... + # in body 2 +# before else +else: + ... +# before finally +finally: + ... diff --git a/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs b/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs index 2e1c695217..66ab84be59 100644 --- a/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs +++ b/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs @@ -2,12 +2,33 @@ use crate::comments::trailing_comments; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; use crate::{FormatNodeRule, PyFormatter}; +use ruff_formatter::FormatRuleWithOptions; use ruff_formatter::{write, Buffer, FormatResult}; use ruff_python_ast::node::AstNode; use rustpython_parser::ast::ExceptHandlerExceptHandler; +#[derive(Copy, Clone, Default)] +pub enum ExceptHandlerKind { + #[default] + Regular, + Starred, +} + #[derive(Default)] -pub struct FormatExceptHandlerExceptHandler; +pub struct FormatExceptHandlerExceptHandler { + except_handler_kind: ExceptHandlerKind, +} + +impl FormatRuleWithOptions> + for FormatExceptHandlerExceptHandler +{ + type Options = ExceptHandlerKind; + + fn with_options(mut self, options: Self::Options) -> Self { + self.except_handler_kind = options; + self + } +} impl FormatNodeRule for FormatExceptHandlerExceptHandler { fn fmt_fields( @@ -25,7 +46,16 @@ impl FormatNodeRule for FormatExceptHandlerExceptHan let comments_info = f.context().comments().clone(); let dangling_comments = comments_info.dangling_comments(item.as_any_node_ref()); - write!(f, [text("except")])?; + write!( + f, + [ + text("except"), + match self.except_handler_kind { + ExceptHandlerKind::Regular => None, + ExceptHandlerKind::Starred => Some(text("*")), + } + ] + )?; if let Some(type_) = type_ { write!( diff --git a/crates/ruff_python_formatter/src/statement/stmt_try.rs b/crates/ruff_python_formatter/src/statement/stmt_try.rs index 701fb8165b..49bcef4387 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_try.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_try.rs @@ -1,19 +1,103 @@ use crate::comments; use crate::comments::leading_alternate_branch_comments; use crate::comments::SourceComment; +use crate::other::except_handler_except_handler::ExceptHandlerKind; use crate::prelude::*; use crate::statement::FormatRefWithRule; use crate::statement::Stmt; use crate::{FormatNodeRule, PyFormatter}; +use ruff_formatter::FormatRuleWithOptions; use ruff_formatter::{write, Buffer, FormatResult}; -use ruff_python_ast::node::AstNode; -use rustpython_parser::ast::{ExceptHandler, Ranged, StmtTry, Suite}; +use ruff_python_ast::node::AnyNodeRef; +use ruff_text_size::TextRange; +use rustpython_parser::ast::{ExceptHandler, Ranged, StmtTry, StmtTryStar, Suite}; + +pub(super) enum AnyStatementTry<'a> { + Try(&'a StmtTry), + TryStar(&'a StmtTryStar), +} +impl<'a> AnyStatementTry<'a> { + const fn except_handler_kind(&self) -> ExceptHandlerKind { + match self { + AnyStatementTry::Try(_) => ExceptHandlerKind::Regular, + AnyStatementTry::TryStar(_) => ExceptHandlerKind::Starred, + } + } + + fn body(&self) -> &Suite { + match self { + AnyStatementTry::Try(try_) => &try_.body, + AnyStatementTry::TryStar(try_) => &try_.body, + } + } + + fn handlers(&self) -> &[ExceptHandler] { + match self { + AnyStatementTry::Try(try_) => try_.handlers.as_slice(), + AnyStatementTry::TryStar(try_) => try_.handlers.as_slice(), + } + } + fn orelse(&self) -> &Suite { + match self { + AnyStatementTry::Try(try_) => &try_.orelse, + AnyStatementTry::TryStar(try_) => &try_.orelse, + } + } + + fn finalbody(&self) -> &Suite { + match self { + AnyStatementTry::Try(try_) => &try_.finalbody, + AnyStatementTry::TryStar(try_) => &try_.finalbody, + } + } +} + +impl Ranged for AnyStatementTry<'_> { + fn range(&self) -> TextRange { + match self { + AnyStatementTry::Try(with) => with.range(), + AnyStatementTry::TryStar(with) => with.range(), + } + } +} + +impl<'a> From<&'a StmtTry> for AnyStatementTry<'a> { + fn from(value: &'a StmtTry) -> Self { + AnyStatementTry::Try(value) + } +} + +impl<'a> From<&'a StmtTryStar> for AnyStatementTry<'a> { + fn from(value: &'a StmtTryStar) -> Self { + AnyStatementTry::TryStar(value) + } +} + +impl<'a> From<&AnyStatementTry<'a>> for AnyNodeRef<'a> { + fn from(value: &AnyStatementTry<'a>) -> Self { + match value { + AnyStatementTry::Try(with) => AnyNodeRef::StmtTry(with), + AnyStatementTry::TryStar(with) => AnyNodeRef::StmtTryStar(with), + } + } +} #[derive(Default)] pub struct FormatStmtTry; #[derive(Copy, Clone, Default)] -pub struct FormatExceptHandler; +pub struct FormatExceptHandler { + except_handler_kind: ExceptHandlerKind, +} + +impl FormatRuleWithOptions> for FormatExceptHandler { + type Options = ExceptHandlerKind; + + fn with_options(mut self, options: Self::Options) -> Self { + self.except_handler_kind = options; + self + } +} impl FormatRule> for FormatExceptHandler { fn fmt( @@ -22,7 +106,9 @@ impl FormatRule> for FormatExceptHandler { f: &mut Formatter>, ) -> FormatResult<()> { match item { - ExceptHandler::ExceptHandler(x) => x.format().fmt(f), + ExceptHandler::ExceptHandler(x) => { + x.format().with_options(self.except_handler_kind).fmt(f) + } } } } @@ -39,19 +125,14 @@ impl<'ast> AsFormat> for ExceptHandler { FormatRefWithRule::new(self, FormatExceptHandler::default()) } } - -impl FormatNodeRule for FormatStmtTry { - fn fmt_fields(&self, item: &StmtTry, f: &mut PyFormatter) -> FormatResult<()> { - let StmtTry { - range: _, - body, - handlers, - orelse, - finalbody, - } = item; - +impl Format> for AnyStatementTry<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { let comments_info = f.context().comments().clone(); - let mut dangling_comments = comments_info.dangling_comments(item.as_any_node_ref()); + let mut dangling_comments = comments_info.dangling_comments(self); + let body = self.body(); + let handlers = self.handlers(); + let orelse = self.orelse(); + let finalbody = self.finalbody(); write!(f, [text("try:"), block_indent(&body.format())])?; @@ -63,7 +144,7 @@ impl FormatNodeRule for FormatStmtTry { f, [ leading_alternate_branch_comments(handler_comments, previous_node), - &handler.format() + &handler.format().with_options(self.except_handler_kind()), ] )?; previous_node = match handler { @@ -78,9 +159,15 @@ impl FormatNodeRule for FormatStmtTry { write!(f, [comments::dangling_comments(dangling_comments)]) } +} + +impl FormatNodeRule for FormatStmtTry { + fn fmt_fields(&self, item: &StmtTry, f: &mut PyFormatter) -> FormatResult<()> { + AnyStatementTry::from(item).fmt(f) + } fn fmt_dangling_comments(&self, _node: &StmtTry, _f: &mut PyFormatter) -> FormatResult<()> { - // dangling comments are formatted as part of fmt_fields + // dangling comments are formatted as part of AnyStatementTry::fmt Ok(()) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_try_star.rs b/crates/ruff_python_formatter/src/statement/stmt_try_star.rs index aa4d45c444..3c61258248 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_try_star.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_try_star.rs @@ -1,5 +1,7 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::statement::stmt_try::AnyStatementTry; +use crate::{FormatNodeRule, PyFormatter}; +use ruff_formatter::Format; +use ruff_formatter::FormatResult; use rustpython_parser::ast::StmtTryStar; #[derive(Default)] @@ -7,6 +9,11 @@ pub struct FormatStmtTryStar; impl FormatNodeRule for FormatStmtTryStar { fn fmt_fields(&self, item: &StmtTryStar, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + AnyStatementTry::from(item).fmt(f) + } + + fn fmt_dangling_comments(&self, _node: &StmtTryStar, _f: &mut PyFormatter) -> FormatResult<()> { + // dangling comments are formatted as part of AnyStatementTry::fmt + Ok(()) } } diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__try.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__try.py.snap index e0a6fa67b0..588c6e5b50 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__try.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__try.py.snap @@ -76,6 +76,25 @@ try: except: a = 10 # trailing comment1 b = 11 # trailing comment2 + + +# try/except*, mostly the same as try +try: # try + ... + # end of body +# before except +except* (Exception, ValueError) as exc: # except line + ... +# before except 2 +except* KeyError as key: # except line 2 + ... + # in body 2 +# before else +else: + ... +# before finally +finally: + ... ``` ## Output @@ -161,6 +180,26 @@ try: except: a = 10 # trailing comment1 b = 11 # trailing comment2 + + +# try/except*, mostly the same as try +try: + # try + ... + # end of body +# before except +except* (Exception, ValueError) as exc: # except line + ... +# before except 2 +except* KeyError as key: # except line 2 + ... + # in body 2 +# before else +else: + ... +# before finally +finally: + ... ``` From 38189ed91338f1e0ac4e120f31d34743c1b9e589 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 29 Jun 2023 08:09:13 +0200 Subject: [PATCH 279/447] Fix invalid printer IR error (#5422) --- .../test/fixtures/ruff/expression/string.py | 10 ++++++ crates/ruff_python_formatter/src/builders.rs | 30 +++++++++-------- .../src/expression/string.rs | 32 ++++++++++++++++--- .../format@expression__string.py.snap | 16 ++++++++++ 4 files changed, 70 insertions(+), 18 deletions(-) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py index fff7aae96c..f9c4fe8b1d 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py @@ -108,3 +108,13 @@ test_particular = [ '1.0000000000000000000000000000000000000000000010000' #... '0000000000000000000000000000000000000000025', ] + +# Parenthesized string continuation with messed up indentation +{ + "key": ( + [], + 'a' + 'b' + 'c' + ) +} diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 962707a160..97a70dbe47 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -242,23 +242,25 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { } pub(crate) fn finish(&mut self) -> FormatResult<()> { - if let Some(last_end) = self.last_end.take() { - if_group_breaks(&text(",")).fmt(self.fmt)?; + 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() - && matches!( - first_non_trivia_token(last_end, self.fmt.context().contents()), - Some(Token { - kind: TokenKind::Comma, - .. - }) - ) - { - expand_parent().fmt(self.fmt)?; + if self.fmt.options().magic_trailing_comma().is_respect() + && matches!( + first_non_trivia_token(last_end, self.fmt.context().contents()), + Some(Token { + kind: TokenKind::Comma, + .. + }) + ) + { + expand_parent().fmt(self.fmt)?; + } } - } - Ok(()) + Ok(()) + }) } } diff --git a/crates/ruff_python_formatter/src/expression/string.rs b/crates/ruff_python_formatter/src/expression/string.rs index 055b5d3642..0362aff005 100644 --- a/crates/ruff_python_formatter/src/expression/string.rs +++ b/crates/ruff_python_formatter/src/expression/string.rs @@ -8,7 +8,7 @@ use ruff_formatter::{format_args, write, FormatError}; use ruff_python_ast::str::is_implicit_concatenation; use ruff_text_size::{TextLen, TextRange, TextSize}; use rustpython_parser::ast::{ExprConstant, Ranged}; -use rustpython_parser::lexer::lex_starts_at; +use rustpython_parser::lexer::{lex_starts_at, LexicalError, LexicalErrorType}; use rustpython_parser::{Mode, Tok}; use std::borrow::Cow; @@ -17,7 +17,7 @@ pub enum StringLayout { Default(Option), /// Enforces that implicit continuation strings are printed on a single line even if they exceed - /// the configured line width. + /// the configured line width. Flat, } @@ -83,7 +83,7 @@ impl Format> for FormatStringContinuation<'_> { // Call into the lexer to extract the individual chunks and format each string on its own. // This code does not yet implement the automatic joining of strings that fit on the same line // because this is a black preview style. - let lexer = lex_starts_at(string_content, Mode::Module, string_range.start()); + let lexer = lex_starts_at(string_content, Mode::Expression, string_range.start()); let separator = format_with(|f| match self.layout { StringLayout::Default(_) => soft_line_break_or_space().fmt(f), @@ -93,7 +93,31 @@ impl Format> for FormatStringContinuation<'_> { let mut joiner = f.join_with(separator); for token in lexer { - let (token, token_range) = token.map_err(|_| FormatError::SyntaxError)?; + let (token, token_range) = match token { + Ok(spanned) => spanned, + Err(LexicalError { + error: LexicalErrorType::IndentationError, + .. + }) => { + // This can happen if the string continuation appears anywhere inside of a parenthesized expression + // because the lexer doesn't know about the parentheses. For example, the following snipped triggers an Indentation error + // ```python + // { + // "key": ( + // [], + // 'a' + // 'b' + // 'c' + // ) + // } + // ``` + // Ignoring the error here is *safe* because we know that the program once parsed to a valid AST. + continue; + } + Err(_) => { + return Err(FormatError::SyntaxError); + } + }; match token { Tok::String { .. } => { diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap index 94fc0a6049..7bdc05dbb5 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap @@ -114,6 +114,16 @@ test_particular = [ '1.0000000000000000000000000000000000000000000010000' #... '0000000000000000000000000000000000000000025', ] + +# Parenthesized string continuation with messed up indentation +{ + "key": ( + [], + 'a' + 'b' + 'c' + ) +} ``` ## Outputs @@ -259,6 +269,9 @@ test_particular = [ "1.0000000000000000000000000000000000000000000010000" # ... "0000000000000000000000000000000000000000025", ] + +# Parenthesized string continuation with messed up indentation +{"key": ([], "a" "b" "c")} ``` @@ -404,6 +417,9 @@ test_particular = [ '1.0000000000000000000000000000000000000000000010000' # ... '0000000000000000000000000000000000000000025', ] + +# Parenthesized string continuation with messed up indentation +{'key': ([], 'a' 'b' 'c')} ``` From 955e9ef8212a84df9e7d1469e615d444dd2eb7c3 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 29 Jun 2023 08:09:26 +0200 Subject: [PATCH 280/447] Fix invalid syntax for binary expression in unary op (#5370) --- Cargo.lock | 1 + crates/ruff_python_formatter/Cargo.toml | 1 + .../test/fixtures/ruff/expression/binary.py | 13 +++ .../test/fixtures/ruff/expression/unary.py | 4 + .../ruff/statement/class_definition.py | 3 + .../src/expression/binary_like.rs | 25 +++-- .../src/expression/expr_bin_op.rs | 93 ++++++++++++------- .../src/expression/expr_bool_op.rs | 6 +- .../src/expression/expr_compare.rs | 6 +- .../src/expression/mod.rs | 2 +- .../black_compatibility@comments6.py.snap | 12 +-- .../black_compatibility@expression.py.snap | 17 +++- .../black_compatibility@fmtonoff.py.snap | 26 +++--- .../black_compatibility@function.py.snap | 21 +---- .../black_compatibility@slices.py.snap | 10 +- .../format@expression__binary.py.snap | 38 ++++++-- .../format@expression__unary.py.snap | 18 +++- .../snapshots/format@statement__call.py.snap | 3 +- ...format@statement__class_definition.py.snap | 14 +++ .../snapshots/format@statement__with.py.snap | 3 +- 20 files changed, 200 insertions(+), 116 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f463b1601f..c0a31643e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2088,6 +2088,7 @@ dependencies = [ "serde", "serde_json", "similar", + "smallvec", ] [[package]] diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index e781baf3ef..a096918fa3 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -26,6 +26,7 @@ once_cell = { workspace = true } rustc-hash = { workspace = true } rustpython-parser = { workspace = true } serde = { workspace = true, optional = true } +smallvec = { workspace = true } [dev-dependencies] ruff_formatter = { path = "../ruff_formatter", features = ["serde"]} diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py index 73529824ed..cbd93631ae 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py @@ -10,6 +10,19 @@ b ) +( + # leading left most comment + aaaaaaaa + + # trailing operator comment + # leading b comment + b # trailing b comment + # trailing b ownline comment + + # trailing second operator comment + # leading c comment + c # trailing c comment + # trailing own line comment + ) + # Black breaks the right side first for the following expressions: aaaaaaaaaaaaaa + caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaal(argument1, argument2, argument3) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py index a88e20fb75..11106d7d23 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py @@ -136,3 +136,7 @@ if ( if not \ a: pass + +# Regression: https://github.com/astral-sh/ruff/issues/5338 +if a and not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py index ec58f89c51..08b8e40e25 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py @@ -32,5 +32,8 @@ class Test((Aaaa)): class Test(aaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccccc + dddddddddddddddddddddd + eeeeeeeee, ffffffffffffffffff, gggggggggggggggggg): pass +class Test(aaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbb * cccccccccccccccccccccccc + dddddddddddddddddddddd + eeeeeeeee, ffffffffffffffffff, gggggggggggggggggg): + pass + class Test(Aaaa): # trailing comment pass diff --git a/crates/ruff_python_formatter/src/expression/binary_like.rs b/crates/ruff_python_formatter/src/expression/binary_like.rs index da124114ed..c934322e55 100644 --- a/crates/ruff_python_formatter/src/expression/binary_like.rs +++ b/crates/ruff_python_formatter/src/expression/binary_like.rs @@ -5,7 +5,7 @@ use rustpython_parser::ast::{self, Expr}; use ruff_formatter::{format_args, write}; -use crate::expression::parentheses::Parentheses; +use crate::expression::parentheses::{is_expression_parenthesized, Parentheses}; use crate::prelude::*; /// Trait to implement a binary like syntax that has a left operand, an operator, and a right operand. @@ -24,7 +24,7 @@ pub(super) trait FormatBinaryLike<'ast> { let right = self.right()?; let layout = if parentheses == Some(Parentheses::Custom) { - self.binary_layout() + self.binary_layout(f.context().contents()) } else { BinaryLayout::Default }; @@ -113,9 +113,9 @@ pub(super) trait FormatBinaryLike<'ast> { } /// Determines which binary layout to use. - fn binary_layout(&self) -> BinaryLayout { + fn binary_layout(&self, source: &str) -> BinaryLayout { if let (Ok(left), Ok(right)) = (self.left(), self.right()) { - BinaryLayout::from_left_right(left, right) + BinaryLayout::from_left_right(left, right, source) } else { BinaryLayout::Default } @@ -134,8 +134,8 @@ pub(super) trait FormatBinaryLike<'ast> { fn operator(&self) -> Self::FormatOperator; } -fn can_break_expr(expr: &Expr) -> bool { - match expr { +fn can_break_expr(expr: &Expr, source: &str) -> bool { + let can_break = match expr { Expr::Tuple(ast::ExprTuple { elts: expressions, .. }) @@ -153,12 +153,11 @@ fn can_break_expr(expr: &Expr) -> bool { !(args.is_empty() && keywords.is_empty()) } Expr::ListComp(_) | Expr::SetComp(_) | Expr::DictComp(_) | Expr::GeneratorExp(_) => true, - Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => match operand.as_ref() { - Expr::BinOp(_) => true, - _ => can_break_expr(operand.as_ref()), - }, + Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => can_break_expr(operand.as_ref(), source), _ => false, - } + }; + + can_break || is_expression_parenthesized(expr.into(), source) } #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -206,8 +205,8 @@ pub(super) enum BinaryLayout { } impl BinaryLayout { - pub(super) fn from_left_right(left: &Expr, right: &Expr) -> BinaryLayout { - match (can_break_expr(left), can_break_expr(right)) { + pub(super) fn from_left_right(left: &Expr, right: &Expr, source: &str) -> BinaryLayout { + match (can_break_expr(left, source), can_break_expr(right, source)) { (false, false) => BinaryLayout::Default, (true, false) => BinaryLayout::ExpandLeft, (false, true) => BinaryLayout::ExpandRight, diff --git a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs index 7c50fdcc51..6e2dc5226f 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs @@ -1,4 +1,4 @@ -use crate::comments::{trailing_comments, Comments}; +use crate::comments::{trailing_comments, trailing_node_comments, Comments}; use crate::expression::binary_like::{BinaryLayout, FormatBinaryLike}; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parenthesize, @@ -10,6 +10,8 @@ use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWi use rustpython_parser::ast::{ Constant, Expr, ExprAttribute, ExprBinOp, ExprConstant, ExprUnaryOp, Operator, UnaryOp, }; +use smallvec::SmallVec; +use std::iter; #[derive(Default)] pub struct FormatExprBinOp { @@ -40,41 +42,68 @@ impl<'ast> FormatBinaryLike<'ast> for ExprBinOp { type FormatOperator = FormatOwnedWithRule>; fn fmt_default(&self, f: &mut PyFormatter<'ast, '_>) -> FormatResult<()> { - let ExprBinOp { - range: _, - left, - op, - right, - } = self; - let comments = f.context().comments().clone(); - let operator_comments = comments.dangling_comments(self); - let needs_space = !is_simple_power_expression(self); - let before_operator_space = if needs_space { - soft_line_break_or_space() - } else { - soft_line_break() - }; + let format_inner = format_with(|f| { + let binary_chain: SmallVec<[&ExprBinOp; 4]> = + iter::successors(Some(self), |parent| parent.left.as_bin_op_expr()).collect(); - write!( - f, - [ - left.format(), - before_operator_space, - op.format(), - trailing_comments(operator_comments), - ] - )?; + // SAFETY: `binary_chain` is guaranteed not to be empty because it always contains the current expression. + let left_most = binary_chain.last().unwrap(); - // Format the operator on its own line if the right side has any leading comments. - if comments.has_leading_comments(right.as_ref()) { - write!(f, [hard_line_break()])?; - } else if needs_space { - write!(f, [space()])?; - } + // Format the left most expression + group(&left_most.left.format()).fmt(f)?; - write!(f, [group(&right.format())]) + // Iterate upwards in the binary expression tree and, for each level, format the operator + // and the right expression. + for current in binary_chain.into_iter().rev() { + let ExprBinOp { + range: _, + left: _, + op, + right, + } = current; + + let operator_comments = comments.dangling_comments(current); + let needs_space = !is_simple_power_expression(current); + + let before_operator_space = if needs_space { + soft_line_break_or_space() + } else { + soft_line_break() + }; + + write!( + f, + [ + before_operator_space, + op.format(), + trailing_comments(operator_comments), + ] + )?; + + // Format the operator on its own line if the right side has any leading comments. + if comments.has_leading_comments(right.as_ref()) || !operator_comments.is_empty() { + hard_line_break().fmt(f)?; + } else if needs_space { + space().fmt(f)?; + } + + group(&right.format()).fmt(f)?; + + // It's necessary to format the trailing comments because the code bypasses + // `FormatNodeRule::fmt` for the nested binary expressions. + // Don't call the formatting function for the most outer binary expression because + // these comments have already been formatted. + if current != self { + trailing_node_comments(current).fmt(f)?; + } + } + + Ok(()) + }); + + group(&format_inner).fmt(f) } fn left(&self) -> FormatResult<&Expr> { @@ -162,7 +191,7 @@ impl NeedsParentheses for ExprBinOp { ) -> Parentheses { match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { Parentheses::Optional => { - if self.binary_layout() == BinaryLayout::Default + if self.binary_layout(source) == BinaryLayout::Default || comments.has_leading_comments(self.right.as_ref()) || comments.has_dangling_comments(self) { 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 e3fd595bab..e9323cf712 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bool_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bool_op.rs @@ -31,9 +31,9 @@ impl FormatNodeRule for FormatExprBoolOp { impl<'ast> FormatBinaryLike<'ast> for ExprBoolOp { type FormatOperator = FormatOwnedWithRule>; - fn binary_layout(&self) -> BinaryLayout { + fn binary_layout(&self, source: &str) -> BinaryLayout { match self.values.as_slice() { - [left, right] => BinaryLayout::from_left_right(left, right), + [left, right] => BinaryLayout::from_left_right(left, right, source), [..] => BinaryLayout::Default, } } @@ -93,7 +93,7 @@ impl NeedsParentheses for ExprBoolOp { comments: &Comments, ) -> Parentheses { match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { - Parentheses::Optional => match self.binary_layout() { + Parentheses::Optional => match self.binary_layout(source) { BinaryLayout::Default => Parentheses::Optional, BinaryLayout::ExpandRight diff --git a/crates/ruff_python_formatter/src/expression/expr_compare.rs b/crates/ruff_python_formatter/src/expression/expr_compare.rs index 63bc5628a3..094344be61 100644 --- a/crates/ruff_python_formatter/src/expression/expr_compare.rs +++ b/crates/ruff_python_formatter/src/expression/expr_compare.rs @@ -34,10 +34,10 @@ impl FormatNodeRule for FormatExprCompare { impl<'ast> FormatBinaryLike<'ast> for ExprCompare { type FormatOperator = FormatOwnedWithRule>; - fn binary_layout(&self) -> BinaryLayout { + fn binary_layout(&self, source: &str) -> BinaryLayout { if self.ops.len() == 1 { match self.comparators.as_slice() { - [right] => BinaryLayout::from_left_right(&self.left, right), + [right] => BinaryLayout::from_left_right(&self.left, right, source), [..] => BinaryLayout::Default, } } else { @@ -102,7 +102,7 @@ impl NeedsParentheses for ExprCompare { comments: &Comments, ) -> Parentheses { match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { - parentheses @ Parentheses::Optional => match self.binary_layout() { + parentheses @ Parentheses::Optional => match self.binary_layout(source) { BinaryLayout::Default => parentheses, BinaryLayout::ExpandRight diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 20ba4807ee..ac3a4d682c 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -10,7 +10,7 @@ use ruff_formatter::{ }; use rustpython_parser::ast::Expr; -mod binary_like; +pub(crate) mod binary_like; pub(crate) mod expr_attribute; pub(crate) mod expr_await; pub(crate) mod expr_bin_op; diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments6.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments6.py.snap index 8c11b63f62..511d9fe1b1 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments6.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments6.py.snap @@ -147,7 +147,7 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite an_element_with_a_long_value = calls() or more_calls() and more() # type: bool tup = ( -@@ -100,19 +98,35 @@ +@@ -100,19 +98,32 @@ ) c = call( @@ -169,10 +169,7 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite -AAAAAAAAAAAAA = [AAAAAAAAAAAAA] + SHARED_AAAAAAAAAAAAA + USER_AAAAAAAAAAAAA + AAAAAAAAAAAAA # type: ignore +AAAAAAAAAAAAA = ( -+ [AAAAAAAAAAAAA] -+ + SHARED_AAAAAAAAAAAAA -+ + USER_AAAAAAAAAAAAA -+ + AAAAAAAAAAAAA ++ [AAAAAAAAAAAAA] + SHARED_AAAAAAAAAAAAA + USER_AAAAAAAAAAAAA + AAAAAAAAAAAAA +) # type: ignore call_to_some_function_asdf( @@ -308,10 +305,7 @@ def func( result = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # aaa AAAAAAAAAAAAA = ( - [AAAAAAAAAAAAA] - + SHARED_AAAAAAAAAAAAA - + USER_AAAAAAAAAAAAA - + AAAAAAAAAAAAA + [AAAAAAAAAAAAA] + SHARED_AAAAAAAAAAAAA + USER_AAAAAAAAAAAAA + AAAAAAAAAAAAA ) # type: ignore call_to_some_function_asdf( diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap index 9a1f301091..a2b53747c5 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap @@ -274,11 +274,20 @@ last_call() Name None True -@@ -30,33 +31,39 @@ +@@ -23,40 +24,46 @@ + 1 >> v2 + 1 % finished + 1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8 +-((1 + v2) - (v3 * 4)) ^ (((5**v6) / 7) // 8) ++(1 + v2 - (v3 * 4)) ^ (5**v6 / 7 // 8) + not great + ~great + +value -1 ~int and not v1 ^ 123 + v2 | True - (~int) and (not ((v1 ^ (123 + v2)) | True)) +-(~int) and (not ((v1 ^ (123 + v2)) | True)) -+(really ** -(confusing ** ~(operator**-precedence))) ++(~int) and (not (v1 ^ (123 + v2) | True)) ++really ** -confusing ** ~operator**-precedence flags & ~select.EPOLLIN and waiters.write_task is not None -lambda arg: None @@ -638,13 +647,13 @@ v1 << 2 1 >> v2 1 % finished 1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8 -((1 + v2) - (v3 * 4)) ^ (((5**v6) / 7) // 8) +(1 + v2 - (v3 * 4)) ^ (5**v6 / 7 // 8) not great ~great +value -1 ~int and not v1 ^ 123 + v2 | True -(~int) and (not ((v1 ^ (123 + v2)) | True)) +(~int) and (not (v1 ^ (123 + v2) | True)) +really ** -confusing ** ~operator**-precedence flags & ~select.EPOLLIN and waiters.write_task is not None lambda x: True diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap index 460ce0e39e..e4f11e148d 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap @@ -202,11 +202,11 @@ d={'a':1, #!/usr/bin/env python3 -import asyncio -import sys +- +-from third_party import X, Y, Z +NOT_YET_IMPLEMENTED_StmtImport +NOT_YET_IMPLEMENTED_StmtImport --from third_party import X, Y, Z -- -from library import some_connection, some_decorator +NOT_YET_IMPLEMENTED_StmtImportFrom @@ -306,7 +306,7 @@ d={'a':1, h: str = "", i: str = r"", ): -@@ -64,55 +86,55 @@ +@@ -64,55 +86,54 @@ something = { # fmt: off @@ -327,8 +327,7 @@ d={'a':1, + "some big and", + "complex subscript", + # fmt: on -+ goes -+ + here, ++ goes + here, + andhere, + ) ] @@ -382,7 +381,7 @@ d={'a':1, # fmt: on -@@ -133,10 +155,10 @@ +@@ -133,10 +154,10 @@ """Another known limitation.""" # fmt: on # fmt: off @@ -397,7 +396,7 @@ d={'a':1, # fmt: on # fmt: off # ...but comments still get reformatted even though they should not be -@@ -151,12 +173,10 @@ +@@ -151,12 +172,10 @@ ast_args.kw_defaults, parameters, implicit_default=True, @@ -412,17 +411,16 @@ d={'a':1, # fmt: on _type_comment_re = re.compile( r""" -@@ -179,7 +199,8 @@ +@@ -179,7 +198,7 @@ $ """, # fmt: off - re.MULTILINE|re.VERBOSE -+ re.MULTILINE -+ | re.VERBOSE, ++ re.MULTILINE | re.VERBOSE, # fmt: on ) -@@ -217,8 +238,7 @@ +@@ -217,8 +236,7 @@ xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5, ) # fmt: off @@ -538,8 +536,7 @@ def subscriptlist(): "some big and", "complex subscript", # fmt: on - goes - + here, + goes + here, andhere, ) ] @@ -640,8 +637,7 @@ def long_lines(): $ """, # fmt: off - re.MULTILINE - | re.VERBOSE, + re.MULTILINE | re.VERBOSE, # fmt: on ) diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function.py.snap index 98e3796d47..960b474e66 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@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 @@ -215,17 +215,7 @@ def __await__(): return (yield) ) _type_comment_re = re.compile( r""" -@@ -118,7 +124,8 @@ - ) - $ - """, -- re.MULTILINE | re.VERBOSE, -+ re.MULTILINE -+ | re.VERBOSE, - ) - - -@@ -135,14 +142,8 @@ +@@ -135,14 +141,8 @@ a, **kwargs, ) -> A: @@ -373,8 +363,7 @@ def long_lines(): ) $ """, - re.MULTILINE - | re.VERBOSE, + re.MULTILINE | re.VERBOSE, ) diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@slices.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@slices.py.snap index 699c559a0b..50113ee3a3 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@slices.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@slices.py.snap @@ -115,12 +115,13 @@ x[ ham[lower + offset : upper + offset] slice[::, ::] -@@ -50,10 +50,14 @@ +@@ -49,11 +49,14 @@ + slice[ # A - 1 +- 1 - + 2 : -+ + 2 : ++ 1 + 2 : # B - 3 : + 3 : @@ -189,8 +190,7 @@ slice[ slice[ # A - 1 - + 2 : + 1 + 2 : # B 3 : # C 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 59fcae973c..670762dfe9 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 @@ -16,6 +16,19 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression b ) +( + # leading left most comment + aaaaaaaa + + # trailing operator comment + # leading b comment + b # trailing b comment + # trailing b ownline comment + + # trailing second operator comment + # leading c comment + c # trailing c comment + # trailing own line comment + ) + # Black breaks the right side first for the following expressions: aaaaaaaaaaaaaa + caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaal(argument1, argument2, argument3) @@ -204,7 +217,8 @@ for user_id in set(target_user_ids) - {u.user_id for u in updates}: ```py ( aaaaaaaa - + b # trailing operator comment # trailing right comment + + # trailing operator comment + b # trailing right comment ) @@ -215,6 +229,19 @@ for user_id in set(target_user_ids) - {u.user_id for u in updates}: b ) +( + # leading left most comment + aaaaaaaa + + # trailing operator comment + # leading b comment + b # trailing b comment + # trailing b ownline comment + + # trailing second operator comment + # leading c comment + c # trailing c comment + # trailing own line comment +) + # Black breaks the right side first for the following expressions: aaaaaaaaaaaaaa + caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaal( @@ -269,8 +296,7 @@ not (aaaaaaaaaaaaaa + {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_se # leading comment ( # comment - content - + b + content + b ) @@ -417,8 +443,7 @@ if ( & ( # comment - a - + b + a + b ) ): ... @@ -433,8 +458,7 @@ if ( ] & # comment - a - + b + a + b ): ... diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap index 69feb3de88..b31b763052 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap @@ -142,6 +142,10 @@ if ( if not \ a: pass + +# Regression: https://github.com/astral-sh/ruff/issues/5338 +if a and not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: + ... ``` ## Output @@ -253,10 +257,8 @@ if aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & ( pass if ( - not ( - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb - ) + not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ): pass @@ -295,6 +297,14 @@ if ( if not a: pass + +# Regression: https://github.com/astral-sh/ruff/issues/5338 +if ( + a + and not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +): + ... ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__call.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__call.py.snap index 7c40b005b6..f59509126f 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__call.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__call.py.snap @@ -125,8 +125,7 @@ f( f( this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd, these_arguments_have_values_that_need_to_break_because_they_are_too_long1=( - 100000 - - 100000000000 + 100000 - 100000000000 ), these_arguments_have_values_that_need_to_break_because_they_are_too_long2="akshfdlakjsdfad" + "asdfasdfa", diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap index 3fbc4be1a5..2adff74060 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap @@ -38,6 +38,9 @@ class Test((Aaaa)): class Test(aaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccccc + dddddddddddddddddddddd + eeeeeeeee, ffffffffffffffffff, gggggggggggggggggg): pass +class Test(aaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbb * cccccccccccccccccccccccc + dddddddddddddddddddddd + eeeeeeeee, ffffffffffffffffff, gggggggggggggggggg): + pass + class Test(Aaaa): # trailing comment pass ``` @@ -88,6 +91,17 @@ class Test( pass +class Test( + aaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbb * cccccccccccccccccccccccc + + dddddddddddddddddddddd + + eeeeeeeee, + ffffffffffffffffff, + gggggggggggggggggg, +): + pass + + class Test(Aaaa): # trailing comment pass ``` 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 3eb2c3ee7a..8f54c3461d 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 @@ -114,8 +114,7 @@ with a: # should remove brackets # WithItem allow the `aa + bb` content expression to be wrapped with ( ( - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ) as c, ): ... From f7969cf23c596da9d3c6ea4ee4ed556728d5828f Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 29 Jun 2023 09:19:11 +0200 Subject: [PATCH 281/447] ecosystem: Run `git` command with no human interaction flag (#5435) --- scripts/check_ecosystem.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/check_ecosystem.py b/scripts/check_ecosystem.py index c8c1916299..6667313962 100755 --- a/scripts/check_ecosystem.py +++ b/scripts/check_ecosystem.py @@ -69,7 +69,10 @@ class Repository(NamedTuple): ], ) - process = await create_subprocess_exec(*git_command) + process = await create_subprocess_exec( + *git_command, + env={"GIT_TERMINAL_PROMPT": "0"}, + ) status_code = await process.wait() From ae25638b0b9e4d49fdf70befba68d1fa2a28fcc5 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 30 Jun 2023 08:32:50 +0200 Subject: [PATCH 282/447] Update Black tests (#5438) --- .../ruff_dev/src/check_formatter_stability.rs | 7 +- .../fixtures/black/conditional_expression.py | 67 ++ .../black/conditional_expression.py.expect | 90 ++ .../black/miscellaneous/blackd_diff.py | 6 + .../black/miscellaneous/blackd_diff.py.expect | 6 + .../black/miscellaneous/debug_visitor.py | 32 + .../miscellaneous/debug_visitor.py.expect | 32 + .../black/miscellaneous/decorators.py | 150 +++ .../black/miscellaneous/decorators.py.expect | 29 + .../docstring_no_string_normalization.py | 123 ++ ...ocstring_no_string_normalization.py.expect | 123 ++ ...cstring_preview_no_string_normalization.py | 10 + ..._preview_no_string_normalization.py.expect | 10 + .../black/miscellaneous/force_py36.py | 3 + .../black/miscellaneous/force_py36.py.expect | 12 + .../fixtures/black/miscellaneous/force_pyi.py | 30 + .../black/miscellaneous/force_pyi.py.expect | 32 + .../black/miscellaneous/linelength6.py | 5 + .../black/miscellaneous/linelength6.py.expect | 5 + .../long_strings_flag_disabled.py | 292 +++++ .../long_strings_flag_disabled.py.expect | 292 +++++ .../miscellaneous/missing_final_newline.py | 3 + .../missing_final_newline.py.expect | 3 + .../black/miscellaneous/power_op_newline.py | 1 + .../miscellaneous/power_op_newline.py.expect | 6 + .../black/miscellaneous/string_quotes.py | 57 + .../miscellaneous/string_quotes.py.expect | 52 + .../py_310/parenthesized_context_managers.py | 21 + .../parenthesized_context_managers.py.expect | 21 + .../black/py_310/pattern_matching_complex.py | 144 +++ .../py_310/pattern_matching_complex.py.expect | 144 +++ .../black/py_310/pattern_matching_extras.py | 119 ++ .../py_310/pattern_matching_extras.py.expect | 119 ++ .../black/py_310/pattern_matching_generic.py | 107 ++ .../py_310/pattern_matching_generic.py.expect | 107 ++ .../black/py_310/pattern_matching_simple.py | 92 ++ .../py_310/pattern_matching_simple.py.expect | 92 ++ .../black/py_310/pattern_matching_style.py | 53 + .../py_310/pattern_matching_style.py.expect | 35 + .../fixtures/black/py_310/pep_572_py310.py | 15 + .../black/py_310/pep_572_py310.py.expect | 15 + .../py_310/remove_newline_after_match.py | 19 + .../remove_newline_after_match.py.expect | 13 + .../black/py_310/starred_for_target.py | 27 + .../black/py_310/starred_for_target.py.expect | 27 + .../test/fixtures/black/py_311/pep_654.py | 53 + .../fixtures/black/py_311/pep_654.py.expect | 53 + .../fixtures/black/py_311/pep_654_style.py | 55 + .../black/py_311/pep_654_style.py.expect | 53 + .../fixtures/black/py_36/numeric_literals.py | 20 + .../black/py_36/numeric_literals.py.expect | 20 + .../numeric_literals_skip_underscores.py | 10 + ...umeric_literals_skip_underscores.py.expect | 10 + .../test/fixtures/black/py_37/python37.py | 30 + .../fixtures/black/py_37/python37.py.expect | 30 + .../test/fixtures/black/py_38/pep_570.py | 44 + .../fixtures/black/py_38/pep_570.py.expect | 44 + .../test/fixtures/black/py_38/pep_572.py | 47 + .../fixtures/black/py_38/pep_572.py.expect | 47 + .../test/fixtures/black/py_38/python38.py | 19 + .../fixtures/black/py_38/python38.py.expect | 21 + .../test/fixtures/black/py_39/pep_572_py39.py | 7 + .../black/py_39/pep_572_py39.py.expect | 7 + .../test/fixtures/black/py_39/python39.py | 13 + .../fixtures/black/py_39/python39.py.expect | 20 + .../black/py_39/remove_with_brackets.py | 54 + .../py_39/remove_with_brackets.py.expect | 63 + .../black/simple_cases/comments.py.expect | 2 +- .../black/simple_cases/expression.py.expect | 3 +- .../black/simple_cases/fmtonoff.py.expect | 1 - .../black/simple_cases/fmtpass_imports.py | 19 + .../simple_cases/fmtpass_imports.py.expect | 19 + .../fixtures/black/simple_cases/fstring.py | 2 + .../black/simple_cases/fstring.py.expect | 2 + .../fixtures/black/simple_cases/function.py | 2 +- .../fixtures/black/simple_cases/pep_604.py | 6 + .../black/simple_cases/pep_604.py.expect | 14 + .../fixtures/black/simple_cases/slices.py | 32 - .../black/simple_cases/slices.py.expect | 28 - .../black/simple_cases/whitespace.py.expect | 1 + .../type_comment_syntax_error.py | 3 + .../type_comment_syntax_error.py.expect | 5 + .../test/fixtures/import_black_tests.py | 113 ++ .../ruff_python_formatter/tests/fixtures.rs | 29 +- ...mpatibility@conditional_expression.py.snap | 334 ++++++ ...ibility@miscellaneous__blackd_diff.py.snap | 55 + ...ility@miscellaneous__debug_visitor.py.snap | 167 +++ ...tibility@miscellaneous__decorators.py.snap | 576 +++++++++ ..._docstring_no_string_normalization.py.snap | 556 +++++++++ ...ng_preview_no_string_normalization.py.snap | 68 ++ ...atibility@miscellaneous__force_pyi.py.snap | 220 ++++ ...aneous__long_strings_flag_disabled.py.snap | 1052 +++++++++++++++++ ...ty@miscellaneous__power_op_newline.py.snap | 44 + ...ility@miscellaneous__string_quotes.py.snap | 244 ++++ ...y@py_310__pattern_matching_complex.py.snap | 549 +++++++++ ...ty@py_310__pattern_matching_extras.py.snap | 443 +++++++ ...y@py_310__pattern_matching_generic.py.snap | 425 +++++++ ...ty@py_310__pattern_matching_simple.py.snap | 343 ++++++ ...ity@py_310__pattern_matching_style.py.snap | 186 +++ ...ompatibility@py_310__pep_572_py310.py.snap | 96 ++ ...py_310__remove_newline_after_match.py.snap | 76 ++ ...ibility@py_310__starred_for_target.py.snap | 136 +++ ...lack_compatibility@py_311__pep_654.py.snap | 240 ++++ ...ompatibility@py_311__pep_654_style.py.snap | 243 ++++ ...patibility@py_36__numeric_literals.py.snap | 118 ++ ..._numeric_literals_skip_underscores.py.snap | 72 ++ ...lack_compatibility@py_37__python37.py.snap | 144 +++ ...black_compatibility@py_38__pep_570.py.snap | 174 +++ ...black_compatibility@py_38__pep_572.py.snap | 247 ++++ ...lack_compatibility@py_38__python38.py.snap | 113 ++ ..._compatibility@py_39__pep_572_py39.py.snap | 60 + ...lack_compatibility@py_39__python39.py.snap | 94 ++ ...bility@py_39__remove_with_brackets.py.snap | 216 ++++ ...tribute_access_on_number_literals.py.snap} | 0 ...bility@simple_cases__bracketmatch.py.snap} | 0 ...ple_cases__class_methods_new_line.py.snap} | 0 ...ibility@simple_cases__collections.py.snap} | 0 ...es__comment_after_escaped_newline.py.snap} | 0 ...patibility@simple_cases__comments.py.snap} | 10 +- ...atibility@simple_cases__comments2.py.snap} | 0 ...atibility@simple_cases__comments3.py.snap} | 0 ...atibility@simple_cases__comments4.py.snap} | 0 ...atibility@simple_cases__comments5.py.snap} | 0 ...atibility@simple_cases__comments6.py.snap} | 0 ...atibility@simple_cases__comments9.py.snap} | 0 ...ases__comments_non_breaking_space.py.snap} | 0 ...ibility@simple_cases__composition.py.snap} | 0 ...es__composition_no_trailing_comma.py.snap} | 0 ...atibility@simple_cases__docstring.py.snap} | 0 ...y@simple_cases__docstring_preview.py.snap} | 0 ...ibility@simple_cases__empty_lines.py.snap} | 0 ...tibility@simple_cases__expression.py.snap} | 40 +- ...patibility@simple_cases__fmtonoff.py.snap} | 22 +- ...atibility@simple_cases__fmtonoff2.py.snap} | 0 ...atibility@simple_cases__fmtonoff3.py.snap} | 0 ...atibility@simple_cases__fmtonoff4.py.snap} | 0 ...atibility@simple_cases__fmtonoff5.py.snap} | 0 ...lity@simple_cases__fmtpass_imports.py.snap | 114 ++ ...mpatibility@simple_cases__fmtskip.py.snap} | 0 ...patibility@simple_cases__fmtskip2.py.snap} | 0 ...patibility@simple_cases__fmtskip3.py.snap} | 0 ...patibility@simple_cases__fmtskip5.py.snap} | 0 ...patibility@simple_cases__fmtskip7.py.snap} | 0 ...patibility@simple_cases__fmtskip8.py.snap} | 0 ...mpatibility@simple_cases__fstring.py.snap} | 10 +- ...patibility@simple_cases__function.py.snap} | 22 +- ...atibility@simple_cases__function2.py.snap} | 0 ...le_cases__function_trailing_comma.py.snap} | 0 ...lity@simple_cases__import_spacing.py.snap} | 0 ...mple_cases__one_element_subscript.py.snap} | 0 ...ty@simple_cases__power_op_spacing.py.snap} | 0 ...simple_cases__remove_await_parens.py.snap} | 0 ...imple_cases__remove_except_parens.py.snap} | 0 ...simple_cases__remove_for_brackets.py.snap} | 0 ...ove_newline_after_code_block_open.py.snap} | 0 ...ility@simple_cases__remove_parens.py.snap} | 0 ...cases__return_annotation_brackets.py.snap} | 0 ..._cases__skip_magic_trailing_comma.py.snap} | 0 ...ompatibility@simple_cases__slices.py.snap} | 114 +- ...ity@simple_cases__string_prefixes.py.snap} | 0 ...mpatibility@simple_cases__torture.py.snap} | 0 ...__trailing_comma_optional_parens1.py.snap} | 0 ...__trailing_comma_optional_parens2.py.snap} | 0 ..._trailing_commas_in_leading_parts.py.snap} | 0 ...ibility@simple_cases__tupleassign.py.snap} | 0 ...atibility@simple_cases__whitespace.py.snap | 31 + 166 files changed, 11066 insertions(+), 237 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_py36.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_py36.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/linelength6.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/linelength6.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/missing_final_newline.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/missing_final_newline.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_310/parenthesized_context_managers.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_310/parenthesized_context_managers.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/pep_604.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/pep_604.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/type_comments/type_comment_syntax_error.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/type_comments/type_comment_syntax_error.py.expect create mode 100755 crates/ruff_python_formatter/resources/test/fixtures/import_black_tests.py create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@conditional_expression.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__blackd_diff.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__decorators.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_no_string_normalization.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_preview_no_string_normalization.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__force_pyi.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__long_strings_flag_disabled.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__power_op_newline.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__string_quotes.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_complex.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_extras.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_generic.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_simple.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_style.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pep_572_py310.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__remove_newline_after_match.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__starred_for_target.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654_style.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_36__numeric_literals.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_36__numeric_literals_skip_underscores.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_37__python37.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_570.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_572.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__python38.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__pep_572_py39.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__python39.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__remove_with_brackets.py.snap rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@attribute_access_on_number_literals.py.snap => black_compatibility@simple_cases__attribute_access_on_number_literals.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@bracketmatch.py.snap => black_compatibility@simple_cases__bracketmatch.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@class_methods_new_line.py.snap => black_compatibility@simple_cases__class_methods_new_line.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@collections.py.snap => black_compatibility@simple_cases__collections.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@comment_after_escaped_newline.py.snap => black_compatibility@simple_cases__comment_after_escaped_newline.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@comments.py.snap => black_compatibility@simple_cases__comments.py.snap} (97%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@comments2.py.snap => black_compatibility@simple_cases__comments2.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@comments3.py.snap => black_compatibility@simple_cases__comments3.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@comments4.py.snap => black_compatibility@simple_cases__comments4.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@comments5.py.snap => black_compatibility@simple_cases__comments5.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@comments6.py.snap => black_compatibility@simple_cases__comments6.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@comments9.py.snap => black_compatibility@simple_cases__comments9.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@comments_non_breaking_space.py.snap => black_compatibility@simple_cases__comments_non_breaking_space.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@composition.py.snap => black_compatibility@simple_cases__composition.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@composition_no_trailing_comma.py.snap => black_compatibility@simple_cases__composition_no_trailing_comma.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@docstring.py.snap => black_compatibility@simple_cases__docstring.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@docstring_preview.py.snap => black_compatibility@simple_cases__docstring_preview.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@empty_lines.py.snap => black_compatibility@simple_cases__empty_lines.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@expression.py.snap => black_compatibility@simple_cases__expression.py.snap} (99%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@fmtonoff.py.snap => black_compatibility@simple_cases__fmtonoff.py.snap} (99%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@fmtonoff2.py.snap => black_compatibility@simple_cases__fmtonoff2.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@fmtonoff3.py.snap => black_compatibility@simple_cases__fmtonoff3.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@fmtonoff4.py.snap => black_compatibility@simple_cases__fmtonoff4.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@fmtonoff5.py.snap => black_compatibility@simple_cases__fmtonoff5.py.snap} (100%) create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtpass_imports.py.snap rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@fmtskip.py.snap => black_compatibility@simple_cases__fmtskip.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@fmtskip2.py.snap => black_compatibility@simple_cases__fmtskip2.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@fmtskip3.py.snap => black_compatibility@simple_cases__fmtskip3.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@fmtskip5.py.snap => black_compatibility@simple_cases__fmtskip5.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@fmtskip7.py.snap => black_compatibility@simple_cases__fmtskip7.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@fmtskip8.py.snap => black_compatibility@simple_cases__fmtskip8.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@fstring.py.snap => black_compatibility@simple_cases__fstring.py.snap} (85%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@function.py.snap => black_compatibility@simple_cases__function.py.snap} (97%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@function2.py.snap => black_compatibility@simple_cases__function2.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@function_trailing_comma.py.snap => black_compatibility@simple_cases__function_trailing_comma.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@import_spacing.py.snap => black_compatibility@simple_cases__import_spacing.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@one_element_subscript.py.snap => black_compatibility@simple_cases__one_element_subscript.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@power_op_spacing.py.snap => black_compatibility@simple_cases__power_op_spacing.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@remove_await_parens.py.snap => black_compatibility@simple_cases__remove_await_parens.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@remove_except_parens.py.snap => black_compatibility@simple_cases__remove_except_parens.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@remove_for_brackets.py.snap => black_compatibility@simple_cases__remove_for_brackets.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@remove_newline_after_code_block_open.py.snap => black_compatibility@simple_cases__remove_newline_after_code_block_open.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@remove_parens.py.snap => black_compatibility@simple_cases__remove_parens.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@return_annotation_brackets.py.snap => black_compatibility@simple_cases__return_annotation_brackets.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@skip_magic_trailing_comma.py.snap => black_compatibility@simple_cases__skip_magic_trailing_comma.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@slices.py.snap => black_compatibility@simple_cases__slices.py.snap} (80%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@string_prefixes.py.snap => black_compatibility@simple_cases__string_prefixes.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@torture.py.snap => black_compatibility@simple_cases__torture.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@trailing_comma_optional_parens1.py.snap => black_compatibility@simple_cases__trailing_comma_optional_parens1.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@trailing_comma_optional_parens2.py.snap => black_compatibility@simple_cases__trailing_comma_optional_parens2.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@trailing_commas_in_leading_parts.py.snap => black_compatibility@simple_cases__trailing_commas_in_leading_parts.py.snap} (100%) rename crates/ruff_python_formatter/tests/snapshots/{black_compatibility@tupleassign.py.snap => black_compatibility@simple_cases__tupleassign.py.snap} (100%) create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__whitespace.py.snap diff --git a/crates/ruff_dev/src/check_formatter_stability.rs b/crates/ruff_dev/src/check_formatter_stability.rs index 4f07db548f..a499296fe5 100644 --- a/crates/ruff_dev/src/check_formatter_stability.rs +++ b/crates/ruff_dev/src/check_formatter_stability.rs @@ -4,7 +4,7 @@ //! checking entire repositories. #![allow(clippy::print_stdout)] -use anyhow::Context; +use anyhow::{bail, Context}; use clap::Parser; use log::debug; use ruff::resolver::python_files_in_path; @@ -107,7 +107,10 @@ pub(crate) fn check_repo(args: &Args) -> anyhow::Result { ]) .unwrap(); let (paths, _resolver) = python_files_in_path(&cli.files, &pyproject_config, &overrides)?; - assert!(!paths.is_empty(), "no python files in {:?}", cli.files); + + if paths.is_empty() { + bail!("no python files in {:?}", cli.files) + } let mut formatted_counter = 0; let errors = paths diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py b/crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py new file mode 100644 index 0000000000..bbe56623c6 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py @@ -0,0 +1,67 @@ +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=a + if foo + else b, + baz="hello, this is a another value", +) + +imploding_line = ( + 1 + if 1 + 1 == 2 + else 0 +) + +exploding_line = "hello this is a slightly long string" if some_long_value_name_foo_bar_baz else "this one is a little shorter" + +positional_argument_test(some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz) + +def weird_default_argument(x=some_long_value_name_foo_bar_baz + if SOME_CONSTANT + else some_fallback_value_foo_bar_baz): + pass + +nested = "hello this is a slightly long string" if (some_long_value_name_foo_bar_baz if + nesting_test_expressions else some_fallback_value_foo_bar_baz) \ + else "this one is a little shorter" + +generator_expression = ( + some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz for some_boolean_variable in some_iterable +) + + +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( + sql + for sql in ( + "LIMIT %d" % limit if limit else None, + ("OFFSET %d" % offset) if offset else None, + ) + if sql + ) + + +def something(): + clone._iterable_class = ( + NamedValuesListIterable + if named + else FlatValuesListIterable + if flat + else ValuesListIterable + ) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py.expect new file mode 100644 index 0000000000..122ea7860d --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py.expect @@ -0,0 +1,90 @@ +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ), + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ), + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=a if foo else b, + baz="hello, this is a another value", +) + +imploding_line = 1 if 1 + 1 == 2 else 0 + +exploding_line = ( + "hello this is a slightly long string" + if some_long_value_name_foo_bar_baz + else "this one is a little shorter" +) + +positional_argument_test( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz +) + + +def weird_default_argument( + x=( + some_long_value_name_foo_bar_baz + if SOME_CONSTANT + else some_fallback_value_foo_bar_baz + ), +): + pass + + +nested = ( + "hello this is a slightly long string" + if ( + some_long_value_name_foo_bar_baz + if nesting_test_expressions + else some_fallback_value_foo_bar_baz + ) + else "this one is a little shorter" +) + +generator_expression = ( + ( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ) + for some_boolean_variable in some_iterable +) + + +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( + sql + for sql in ( + "LIMIT %d" % limit if limit else None, + ("OFFSET %d" % offset) if offset else None, + ) + if sql + ) + + +def something(): + clone._iterable_class = ( + NamedValuesListIterable + if named + else FlatValuesListIterable if flat else ValuesListIterable + ) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py new file mode 100644 index 0000000000..c5278325db --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py @@ -0,0 +1,6 @@ +def abc (): + return ["hello", "world", + "!"] + +print( "Incorrect formatting" +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py.expect new file mode 100644 index 0000000000..c5278325db --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py.expect @@ -0,0 +1,6 @@ +def abc (): + return ["hello", "world", + "!"] + +print( "Incorrect formatting" +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py new file mode 100644 index 0000000000..d1d1ba1216 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py @@ -0,0 +1,32 @@ +@dataclass +class DebugVisitor(Visitor[T]): + tree_depth: int = 0 + + def visit_default(self, node: LN) -> Iterator[T]: + indent = ' ' * (2 * self.tree_depth) + if isinstance(node, Node): + _type = type_repr(node.type) + out(f'{indent}{_type}', fg='yellow') + self.tree_depth += 1 + for child in node.children: + yield from self.visit(child) + + self.tree_depth -= 1 + out(f'{indent}/{_type}', fg='yellow', bold=False) + else: + _type = token.tok_name.get(node.type, str(node.type)) + out(f'{indent}{_type}', fg='blue', nl=False) + if node.prefix: + # We don't have to handle prefixes for `Node` objects since + # that delegates to the first child anyway. + out(f' {node.prefix!r}', fg='green', bold=False, nl=False) + out(f' {node.value!r}', fg='blue', bold=False) + + @classmethod + def show(cls, code: str) -> None: + """Pretty-prints a given string of `code`. + + Convenience method for debugging. + """ + v: DebugVisitor[None] = DebugVisitor() + list(v.visit(lib2to3_parse(code))) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py.expect new file mode 100644 index 0000000000..d1d1ba1216 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py.expect @@ -0,0 +1,32 @@ +@dataclass +class DebugVisitor(Visitor[T]): + tree_depth: int = 0 + + def visit_default(self, node: LN) -> Iterator[T]: + indent = ' ' * (2 * self.tree_depth) + if isinstance(node, Node): + _type = type_repr(node.type) + out(f'{indent}{_type}', fg='yellow') + self.tree_depth += 1 + for child in node.children: + yield from self.visit(child) + + self.tree_depth -= 1 + out(f'{indent}/{_type}', fg='yellow', bold=False) + else: + _type = token.tok_name.get(node.type, str(node.type)) + out(f'{indent}{_type}', fg='blue', nl=False) + if node.prefix: + # We don't have to handle prefixes for `Node` objects since + # that delegates to the first child anyway. + out(f' {node.prefix!r}', fg='green', bold=False, nl=False) + out(f' {node.value!r}', fg='blue', bold=False) + + @classmethod + def show(cls, code: str) -> None: + """Pretty-prints a given string of `code`. + + Convenience method for debugging. + """ + v: DebugVisitor[None] = DebugVisitor() + list(v.visit(lib2to3_parse(code))) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py new file mode 100644 index 0000000000..46e37f69ed --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py @@ -0,0 +1,150 @@ +# This file doesn't use the standard decomposition. +# Decorator syntax test cases are separated by double # comments. +# Those before the 'output' comment are valid under the old syntax. +# Those after the 'ouput' comment require PEP614 relaxed syntax. +# Do not remove the double # separator before the first test case, it allows +# the comment before the test case to be ignored. + +## + +@decorator +def f(): + ... + +## + +@decorator() +def f(): + ... + +## + +@decorator(arg) +def f(): + ... + +## + +@decorator(kwarg=0) +def f(): + ... + +## + +@decorator(*args) +def f(): + ... + +## + +@decorator(**kwargs) +def f(): + ... + +## + +@decorator(*args, **kwargs) +def f(): + ... + +## + +@decorator(*args, **kwargs,) +def f(): + ... + +## + +@dotted.decorator +def f(): + ... + +## + +@dotted.decorator(arg) +def f(): + ... + +## + +@dotted.decorator(kwarg=0) +def f(): + ... + +## + +@dotted.decorator(*args) +def f(): + ... + +## + +@dotted.decorator(**kwargs) +def f(): + ... + +## + +@dotted.decorator(*args, **kwargs) +def f(): + ... + +## + +@dotted.decorator(*args, **kwargs,) +def f(): + ... + +## + +@double.dotted.decorator +def f(): + ... + +## + +@double.dotted.decorator(arg) +def f(): + ... + +## + +@double.dotted.decorator(kwarg=0) +def f(): + ... + +## + +@double.dotted.decorator(*args) +def f(): + ... + +## + +@double.dotted.decorator(**kwargs) +def f(): + ... + +## + +@double.dotted.decorator(*args, **kwargs) +def f(): + ... + +## + +@double.dotted.decorator(*args, **kwargs,) +def f(): + ... + +## + +@_(sequence["decorator"]) +def f(): + ... + +## + +@eval("sequence['decorator']") +def f(): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py.expect new file mode 100644 index 0000000000..df17e1e749 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py.expect @@ -0,0 +1,29 @@ +## + +@decorator()() +def f(): + ... + +## + +@(decorator) +def f(): + ... + +## + +@sequence["decorator"] +def f(): + ... + +## + +@decorator[List[str]] +def f(): + ... + +## + +@var := decorator +def f(): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py new file mode 100644 index 0000000000..3116529c65 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py @@ -0,0 +1,123 @@ +class ALonelyClass: + ''' + A multiline class docstring. + ''' + def AnEquallyLonelyMethod(self): + ''' + A multiline method docstring''' + pass + + +def one_function(): + '''This is a docstring with a single line of text.''' + pass + + +def shockingly_the_quotes_are_normalized(): + '''This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. + ''' + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not +make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it! + + """ + pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): ''' +"hey yah"''' + + +def shockingly_the_quotes_are_normalized_v2(): + ''' + Docstring Docstring Docstring + ''' + pass + + +def backslash_space(): + '\ ' + + +def multiline_backslash_1(): + ''' + hey\there\ + \ ''' + + +def multiline_backslash_2(): + ''' + hey there \ ''' + + +def multiline_backslash_3(): + ''' + already escaped \\ ''' diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py.expect new file mode 100644 index 0000000000..8aefa4b2c2 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py.expect @@ -0,0 +1,123 @@ +class ALonelyClass: + ''' + A multiline class docstring. + ''' + + def AnEquallyLonelyMethod(self): + ''' + A multiline method docstring''' + pass + + +def one_function(): + '''This is a docstring with a single line of text.''' + pass + + +def shockingly_the_quotes_are_normalized(): + '''This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. + ''' + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not + make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it!""" + pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): + ''' + "hey yah"''' + + +def shockingly_the_quotes_are_normalized_v2(): + ''' + Docstring Docstring Docstring + ''' + pass + + +def backslash_space(): + '\ ' + + +def multiline_backslash_1(): + ''' + hey\there\ + \ ''' + + +def multiline_backslash_2(): + ''' + hey there \ ''' + + +def multiline_backslash_3(): + ''' + already escaped \\''' diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py new file mode 100644 index 0000000000..338cc01d33 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py @@ -0,0 +1,10 @@ +def do_not_touch_this_prefix(): + R"""There was a bug where docstring prefixes would be normalized even with -S.""" + + +def do_not_touch_this_prefix2(): + FR'There was a bug where docstring prefixes would be normalized even with -S.' + + +def do_not_touch_this_prefix3(): + u'''There was a bug where docstring prefixes would be normalized even with -S.''' diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py.expect new file mode 100644 index 0000000000..338cc01d33 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py.expect @@ -0,0 +1,10 @@ +def do_not_touch_this_prefix(): + R"""There was a bug where docstring prefixes would be normalized even with -S.""" + + +def do_not_touch_this_prefix2(): + FR'There was a bug where docstring prefixes would be normalized even with -S.' + + +def do_not_touch_this_prefix3(): + u'''There was a bug where docstring prefixes would be normalized even with -S.''' diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_py36.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_py36.py new file mode 100644 index 0000000000..106e97214d --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_py36.py @@ -0,0 +1,3 @@ +# The input source must not contain any Py36-specific syntax (e.g. argument type +# annotations, trailing comma after *rest) or this test becomes invalid. +def long_function_name(argument_one, argument_two, argument_three, argument_four, argument_five, argument_six, *rest): ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_py36.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_py36.py.expect new file mode 100644 index 0000000000..bb26932707 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_py36.py.expect @@ -0,0 +1,12 @@ +# The input source must not contain any Py36-specific syntax (e.g. argument type +# annotations, trailing comma after *rest) or this test becomes invalid. +def long_function_name( + argument_one, + argument_two, + argument_three, + argument_four, + argument_five, + argument_six, + *rest, +): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py new file mode 100644 index 0000000000..9c8c40cc96 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py @@ -0,0 +1,30 @@ +from typing import Union + +@bird +def zoo(): ... + +class A: ... +@bar +class B: + def BMethod(self) -> None: ... + @overload + def BMethod(self, arg : List[str]) -> None: ... + +class C: ... +@hmm +class D: ... +class E: ... + +@baz +def foo() -> None: + ... + +class F (A , C): ... +def spam() -> None: ... + +@overload +def spam(arg: str) -> str: ... + +var : int = 1 + +def eggs() -> Union[str, int]: ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py.expect new file mode 100644 index 0000000000..4349ba0a53 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py.expect @@ -0,0 +1,32 @@ +from typing import Union + +@bird +def zoo(): ... + +class A: ... + +@bar +class B: + def BMethod(self) -> None: ... + @overload + def BMethod(self, arg: List[str]) -> None: ... + +class C: ... + +@hmm +class D: ... + +class E: ... + +@baz +def foo() -> None: ... + +class F(A, C): ... + +def spam() -> None: ... +@overload +def spam(arg: str) -> str: ... + +var: int = 1 + +def eggs() -> Union[str, int]: ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/linelength6.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/linelength6.py new file mode 100644 index 0000000000..4fb342726f --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/linelength6.py @@ -0,0 +1,5 @@ +# Regression test for #3427, which reproes only with line length <= 6 +def f(): + """ + x + """ diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/linelength6.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/linelength6.py.expect new file mode 100644 index 0000000000..4fb342726f --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/linelength6.py.expect @@ -0,0 +1,5 @@ +# Regression test for #3427, which reproes only with line length <= 6 +def f(): + """ + x + """ diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py new file mode 100644 index 0000000000..db3954e3ab --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py @@ -0,0 +1,292 @@ +x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +y = "Short string" + +print( + "This is a really long string inside of a print statement with extra arguments attached at the end of it.", + x, + y, + z, +) + +print( + "This is a really long string inside of a print statement with no extra arguments attached at the end of it." +) + +D1 = { + "The First": "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + "The Second": "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D2 = { + 1.0: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + 2.0: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D3 = { + x: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + y: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D4 = { + "A long and ridiculous {}".format( + string_key + ): "This is a really really really long string that has to go i,side of a dictionary. It is soooo bad.", + some_func( + "calling", "some", "stuff" + ): "This is a really really really long string that has to go inside of a dictionary. It is {soooo} bad (#{x}).".format( + sooo="soooo", x=2 + ), + "A %s %s" + % ( + "formatted", + "string", + ): "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." + % ("soooo", 2), +} + +func_with_keywords( + my_arg, + my_kwarg="Long keyword strings also need to be wrapped, but they will probably need to be handled a little bit differently.", +) + +bad_split1 = ( + "But what should happen when code has already been formatted but in the wrong way? Like" + " with a space at the end instead of the beginning. Or what about when it is split too soon?" +) + +bad_split2 = ( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." +) + +bad_split3 = ( + "What if we have inline comments on " # First Comment + "each line of a bad split? In that " # Second Comment + "case, we should just leave it alone." # Third Comment +) + +bad_split_func1( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split.", + xxx, + yyy, + zzz, +) + +bad_split_func2( + xxx, + yyy, + zzz, + long_string_kwarg="But what should happen when code has already been formatted but in the wrong way? Like " + "with a space at the end instead of the beginning. Or what about when it is split too " + "soon?", +) + +bad_split_func3( + ( + "But what should happen when code has already " + r"been formatted but in the wrong way? Like " + "with a space at the end instead of the " + r"beginning. Or what about when it is split too " + r"soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." + ), + xxx, + yyy, + zzz, +) + +raw_string = r"This is a long raw string. When re-formatting this string, black needs to make sure it prepends the 'r' onto the new string." + +fmt_string1 = "We also need to be sure to preserve any and all {} which may or may not be attached to the string in question.".format( + "method calls" +) + +fmt_string2 = "But what about when the string is {} but {}".format( + "short", + "the method call is really really really really really really really really long?", +) + +old_fmt_string1 = ( + "While we are on the topic of %s, we should also note that old-style formatting must also be preserved, since some %s still uses it." + % ("formatting", "code") +) + +old_fmt_string2 = "This is a %s %s %s %s" % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", +) + +old_fmt_string3 = ( + "Whereas only the strings after the percent sign were long in the last example, this example uses a long initial string as well. This is another %s %s %s %s" + % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", + ) +) + +fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." + +fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." + +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. + +arg_comment_string = print( + "Long lines with inline comments which are apart of (and not the only member of) an argument list should have their comments appended to the reformatted string's enclosing left parentheses.", # This comment stays on the bottom. + "Arg #2", + "Arg #3", + "Arg #4", + "Arg #5", +) + +pragma_comment_string1 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa: E501 + +pragma_comment_string2 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa + +"""This is a really really really long triple quote string and it should not be touched.""" + +triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception." + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format( + "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string %s." + % "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic %s %s." + % ("string", "formatting") +) + +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." +) + +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. But then what happens when the final string is also supppppperrrrr long?! Well then that second (realllllllly long) string should be split too.", + "and a second argument", + and_a_third, +) + +return "A really really really really really really really really really really really really really long {} {}".format( + "return", "value" +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma 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 +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " 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 +) + +func_with_bad_parens_that_wont_fit_in_one_line( + ("short string that should have parens stripped"), x, y, z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, y, ("short string that should have parens stripped"), z +) + +func_with_bad_parens( + ("short string that should have parens stripped"), + x, + y, + z, +) + +func_with_bad_parens( + x, + y, + ("short string that should have parens stripped"), + z, +) + +annotated_variable: Final = ( + "This is a large " + + STRING + + " that has been " + + CONCATENATED + + "using the '+' operator." +) +annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: Literal[ + "fakse_literal" +] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." + +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" +backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\" + +short_string = "Hi" " there." + +func_call(short_string=("Hi" " there.")) + +raw_strings = r"Don't" " get" r" merged" " unless they are all raw." + + +def foo(): + yield "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + + +x = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four." + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # noqa + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # pylint: disable=some-pylint-check + " of it." +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py.expect new file mode 100644 index 0000000000..db3954e3ab --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py.expect @@ -0,0 +1,292 @@ +x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +y = "Short string" + +print( + "This is a really long string inside of a print statement with extra arguments attached at the end of it.", + x, + y, + z, +) + +print( + "This is a really long string inside of a print statement with no extra arguments attached at the end of it." +) + +D1 = { + "The First": "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + "The Second": "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D2 = { + 1.0: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + 2.0: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D3 = { + x: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + y: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D4 = { + "A long and ridiculous {}".format( + string_key + ): "This is a really really really long string that has to go i,side of a dictionary. It is soooo bad.", + some_func( + "calling", "some", "stuff" + ): "This is a really really really long string that has to go inside of a dictionary. It is {soooo} bad (#{x}).".format( + sooo="soooo", x=2 + ), + "A %s %s" + % ( + "formatted", + "string", + ): "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." + % ("soooo", 2), +} + +func_with_keywords( + my_arg, + my_kwarg="Long keyword strings also need to be wrapped, but they will probably need to be handled a little bit differently.", +) + +bad_split1 = ( + "But what should happen when code has already been formatted but in the wrong way? Like" + " with a space at the end instead of the beginning. Or what about when it is split too soon?" +) + +bad_split2 = ( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." +) + +bad_split3 = ( + "What if we have inline comments on " # First Comment + "each line of a bad split? In that " # Second Comment + "case, we should just leave it alone." # Third Comment +) + +bad_split_func1( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split.", + xxx, + yyy, + zzz, +) + +bad_split_func2( + xxx, + yyy, + zzz, + long_string_kwarg="But what should happen when code has already been formatted but in the wrong way? Like " + "with a space at the end instead of the beginning. Or what about when it is split too " + "soon?", +) + +bad_split_func3( + ( + "But what should happen when code has already " + r"been formatted but in the wrong way? Like " + "with a space at the end instead of the " + r"beginning. Or what about when it is split too " + r"soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." + ), + xxx, + yyy, + zzz, +) + +raw_string = r"This is a long raw string. When re-formatting this string, black needs to make sure it prepends the 'r' onto the new string." + +fmt_string1 = "We also need to be sure to preserve any and all {} which may or may not be attached to the string in question.".format( + "method calls" +) + +fmt_string2 = "But what about when the string is {} but {}".format( + "short", + "the method call is really really really really really really really really long?", +) + +old_fmt_string1 = ( + "While we are on the topic of %s, we should also note that old-style formatting must also be preserved, since some %s still uses it." + % ("formatting", "code") +) + +old_fmt_string2 = "This is a %s %s %s %s" % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", +) + +old_fmt_string3 = ( + "Whereas only the strings after the percent sign were long in the last example, this example uses a long initial string as well. This is another %s %s %s %s" + % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", + ) +) + +fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." + +fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." + +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. + +arg_comment_string = print( + "Long lines with inline comments which are apart of (and not the only member of) an argument list should have their comments appended to the reformatted string's enclosing left parentheses.", # This comment stays on the bottom. + "Arg #2", + "Arg #3", + "Arg #4", + "Arg #5", +) + +pragma_comment_string1 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa: E501 + +pragma_comment_string2 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa + +"""This is a really really really long triple quote string and it should not be touched.""" + +triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception." + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format( + "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string %s." + % "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic %s %s." + % ("string", "formatting") +) + +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." +) + +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. But then what happens when the final string is also supppppperrrrr long?! Well then that second (realllllllly long) string should be split too.", + "and a second argument", + and_a_third, +) + +return "A really really really really really really really really really really really really really long {} {}".format( + "return", "value" +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma 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 +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " 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 +) + +func_with_bad_parens_that_wont_fit_in_one_line( + ("short string that should have parens stripped"), x, y, z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, y, ("short string that should have parens stripped"), z +) + +func_with_bad_parens( + ("short string that should have parens stripped"), + x, + y, + z, +) + +func_with_bad_parens( + x, + y, + ("short string that should have parens stripped"), + z, +) + +annotated_variable: Final = ( + "This is a large " + + STRING + + " that has been " + + CONCATENATED + + "using the '+' operator." +) +annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: Literal[ + "fakse_literal" +] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." + +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" +backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\" + +short_string = "Hi" " there." + +func_call(short_string=("Hi" " there.")) + +raw_strings = r"Don't" " get" r" merged" " unless they are all raw." + + +def foo(): + yield "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + + +x = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four." + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # noqa + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # pylint: disable=some-pylint-check + " of it." +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/missing_final_newline.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/missing_final_newline.py new file mode 100644 index 0000000000..763909fe59 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/missing_final_newline.py @@ -0,0 +1,3 @@ +# A comment-only file, with no final EOL character +# This triggers https://bugs.python.org/issue2142 +# This is the line without the EOL character diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/missing_final_newline.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/missing_final_newline.py.expect new file mode 100644 index 0000000000..763909fe59 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/missing_final_newline.py.expect @@ -0,0 +1,3 @@ +# A comment-only file, with no final EOL character +# This triggers https://bugs.python.org/issue2142 +# This is the line without the EOL character diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py new file mode 100644 index 0000000000..930e29ab56 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py @@ -0,0 +1 @@ +importA;()<<0**0# diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py.expect new file mode 100644 index 0000000000..32e89db2df --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py.expect @@ -0,0 +1,6 @@ +importA +( + () + << 0 + ** 0 +) # diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py new file mode 100644 index 0000000000..86c68e531a --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py @@ -0,0 +1,57 @@ +'''''' +'\'' +'"' +"'" +"\"" +"Hello" +"Don't do that" +'Here is a "' +'What\'s the deal here?' +"What's the deal \"here\"?" +"And \"here\"?" +"""Strings with "" in them""" +'''Strings with "" in them''' +'''Here's a "''' +'''Here's a " ''' +'''Just a normal triple +quote''' +f"just a normal {f} string" +f'''This is a triple-quoted {f}-string''' +f'MOAR {" ".join([])}' +f"MOAR {' '.join([])}" +r"raw string ftw" +r'Date d\'expiration:(.*)' +r'Tricky "quote' +r'Not-so-tricky \"quote' +rf'{yay}' +'\n\ +The \"quick\"\n\ +brown fox\n\ +jumps over\n\ +the \'lazy\' dog.\n\ +' +re.compile(r'[\\"]') +"x = ''; y = \"\"" +"x = '''; y = \"\"" +"x = ''''; y = \"\"" +"x = '' ''; y = \"\"" +"x = ''; y = \"\"\"" +"x = '''; y = \"\"\"\"" +"x = ''''; y = \"\"\"\"\"" +"x = '' ''; y = \"\"\"\"\"" +'unnecessary \"\"escaping' +"unnecessary \'\'escaping" +'\\""' +"\\''" +'Lots of \\\\\\\\\'quotes\'' +f'{y * " "} \'{z}\'' +f'{{y * " "}} \'{z}\'' +f'\'{z}\' {y * " "}' +f'{y * x} \'{z}\'' +'\'{z}\' {y * " "}' +'{y * x} \'{z}\'' + +# We must bail out if changing the quotes would introduce backslashes in f-string +# expressions. xref: https://github.com/psf/black/issues/2348 +f"\"{b}\"{' ' * (long-len(b)+1)}: \"{sts}\",\n" +f"\"{a}\"{'hello' * b}\"{c}\"" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py.expect new file mode 100644 index 0000000000..dce6105acf --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py.expect @@ -0,0 +1,52 @@ +"""""" +"'" +'"' +"'" +'"' +"Hello" +"Don't do that" +'Here is a "' +"What's the deal here?" +'What\'s the deal "here"?' +'And "here"?' +"""Strings with "" in them""" +"""Strings with "" in them""" +'''Here's a "''' +"""Here's a " """ +"""Just a normal triple +quote""" +f"just a normal {f} string" +f"""This is a triple-quoted {f}-string""" +f'MOAR {" ".join([])}' +f"MOAR {' '.join([])}" +r"raw string ftw" +r"Date d\'expiration:(.*)" +r'Tricky "quote' +r"Not-so-tricky \"quote" +rf"{yay}" +"\nThe \"quick\"\nbrown fox\njumps over\nthe 'lazy' dog.\n" +re.compile(r'[\\"]') +"x = ''; y = \"\"" +"x = '''; y = \"\"" +"x = ''''; y = \"\"" +"x = '' ''; y = \"\"" +'x = \'\'; y = """' +'x = \'\'\'; y = """"' +'x = \'\'\'\'; y = """""' +'x = \'\' \'\'; y = """""' +'unnecessary ""escaping' +"unnecessary ''escaping" +'\\""' +"\\''" +"Lots of \\\\\\\\'quotes'" +f'{y * " "} \'{z}\'' +f"{{y * \" \"}} '{z}'" +f'\'{z}\' {y * " "}' +f"{y * x} '{z}'" +"'{z}' {y * \" \"}" +"{y * x} '{z}'" + +# We must bail out if changing the quotes would introduce backslashes in f-string +# expressions. xref: https://github.com/psf/black/issues/2348 +f"\"{b}\"{' ' * (long-len(b)+1)}: \"{sts}\",\n" +f"\"{a}\"{'hello' * b}\"{c}\"" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/parenthesized_context_managers.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/parenthesized_context_managers.py new file mode 100644 index 0000000000..ccf1f94883 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/parenthesized_context_managers.py @@ -0,0 +1,21 @@ +with (CtxManager() as example): + ... + +with (CtxManager1(), CtxManager2()): + ... + +with (CtxManager1() as example, CtxManager2()): + ... + +with (CtxManager1(), CtxManager2() as example): + ... + +with (CtxManager1() as example1, CtxManager2() as example2): + ... + +with ( + CtxManager1() as example1, + CtxManager2() as example2, + CtxManager3() as example3, +): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/parenthesized_context_managers.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/parenthesized_context_managers.py.expect new file mode 100644 index 0000000000..dfae92c596 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/parenthesized_context_managers.py.expect @@ -0,0 +1,21 @@ +with CtxManager() as example: + ... + +with CtxManager1(), CtxManager2(): + ... + +with CtxManager1() as example, CtxManager2(): + ... + +with CtxManager1(), CtxManager2() as example: + ... + +with CtxManager1() as example1, CtxManager2() as example2: + ... + +with ( + CtxManager1() as example1, + CtxManager2() as example2, + CtxManager3() as example3, +): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py new file mode 100644 index 0000000000..97ee194fd3 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py @@ -0,0 +1,144 @@ +# Cases sampled from Lib/test/test_patma.py + +# case black_test_patma_098 +match x: + case -0j: + y = 0 +# case black_test_patma_142 +match x: + case bytes(z): + y = 0 +# case black_test_patma_073 +match x: + case 0 if 0: + y = 0 + case 0 if 1: + y = 1 +# case black_test_patma_006 +match 3: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_049 +match x: + case [0, 1] | [1, 0]: + y = 0 +# case black_check_sequence_then_mapping +match x: + case [*_]: + return "seq" + case {}: + return "map" +# case black_test_patma_035 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | True} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 +# case black_test_patma_107 +match x: + case 0.25 + 1.75j: + y = 0 +# case black_test_patma_097 +match x: + case -0j: + y = 0 +# case black_test_patma_007 +match 4: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_154 +match x: + case 0 if x: + y = 0 +# case black_test_patma_134 +match x: + case {1: 0}: + y = 0 + case {0: 0}: + y = 1 + case {**z}: + y = 2 +# case black_test_patma_185 +match Seq(): + case [*_]: + y = 0 +# case black_test_patma_063 +match x: + case 1: + y = 0 + case 1: + y = 1 +# case black_test_patma_248 +match x: + case {"foo": bar}: + y = bar +# case black_test_patma_019 +match (0, 1, 2): + case [0, 1, *x, 2]: + y = 0 +# case black_test_patma_052 +match x: + case [0]: + y = 0 + case [1, 0] if (x := x[:0]): + y = 1 + case [1, 0]: + y = 2 +# case black_test_patma_191 +match w: + case [x, y, *_]: + z = 0 +# case black_test_patma_110 +match x: + case -0.25 - 1.75j: + y = 0 +# case black_test_patma_151 +match (x,): + case [y]: + z = 0 +# case black_test_patma_114 +match x: + case A.B.C.D: + y = 0 +# case black_test_patma_232 +match x: + case None: + y = 0 +# case black_test_patma_058 +match x: + case 0: + y = 0 +# case black_test_patma_233 +match x: + case False: + y = 0 +# case black_test_patma_078 +match x: + case []: + y = 0 + case [""]: + y = 1 + case "": + y = 2 +# case black_test_patma_156 +match x: + case z: + y = 0 +# case black_test_patma_189 +match w: + case [x, y, *rest]: + z = 0 +# case black_test_patma_042 +match x: + case (0 as z) | (1 as z) | (2 as z) if z == x % 2: + y = 0 +# case black_test_patma_034 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | False} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py.expect new file mode 100644 index 0000000000..97ee194fd3 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py.expect @@ -0,0 +1,144 @@ +# Cases sampled from Lib/test/test_patma.py + +# case black_test_patma_098 +match x: + case -0j: + y = 0 +# case black_test_patma_142 +match x: + case bytes(z): + y = 0 +# case black_test_patma_073 +match x: + case 0 if 0: + y = 0 + case 0 if 1: + y = 1 +# case black_test_patma_006 +match 3: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_049 +match x: + case [0, 1] | [1, 0]: + y = 0 +# case black_check_sequence_then_mapping +match x: + case [*_]: + return "seq" + case {}: + return "map" +# case black_test_patma_035 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | True} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 +# case black_test_patma_107 +match x: + case 0.25 + 1.75j: + y = 0 +# case black_test_patma_097 +match x: + case -0j: + y = 0 +# case black_test_patma_007 +match 4: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_154 +match x: + case 0 if x: + y = 0 +# case black_test_patma_134 +match x: + case {1: 0}: + y = 0 + case {0: 0}: + y = 1 + case {**z}: + y = 2 +# case black_test_patma_185 +match Seq(): + case [*_]: + y = 0 +# case black_test_patma_063 +match x: + case 1: + y = 0 + case 1: + y = 1 +# case black_test_patma_248 +match x: + case {"foo": bar}: + y = bar +# case black_test_patma_019 +match (0, 1, 2): + case [0, 1, *x, 2]: + y = 0 +# case black_test_patma_052 +match x: + case [0]: + y = 0 + case [1, 0] if (x := x[:0]): + y = 1 + case [1, 0]: + y = 2 +# case black_test_patma_191 +match w: + case [x, y, *_]: + z = 0 +# case black_test_patma_110 +match x: + case -0.25 - 1.75j: + y = 0 +# case black_test_patma_151 +match (x,): + case [y]: + z = 0 +# case black_test_patma_114 +match x: + case A.B.C.D: + y = 0 +# case black_test_patma_232 +match x: + case None: + y = 0 +# case black_test_patma_058 +match x: + case 0: + y = 0 +# case black_test_patma_233 +match x: + case False: + y = 0 +# case black_test_patma_078 +match x: + case []: + y = 0 + case [""]: + y = 1 + case "": + y = 2 +# case black_test_patma_156 +match x: + case z: + y = 0 +# case black_test_patma_189 +match w: + case [x, y, *rest]: + z = 0 +# case black_test_patma_042 +match x: + case (0 as z) | (1 as z) | (2 as z) if z == x % 2: + y = 0 +# case black_test_patma_034 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | False} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py new file mode 100644 index 0000000000..0242d264e5 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py @@ -0,0 +1,119 @@ +import match + +match something: + case [a as b]: + print(b) + case [a as b, c, d, e as f]: + print(f) + case Point(a as b): + print(b) + case Point(int() as x, int() as y): + print(x, y) + + +match = 1 +case: int = re.match(something) + +match re.match(case): + case type("match", match): + pass + case match: + pass + + +def func(match: case, case: match) -> case: + match Something(): + case func(match, case): + ... + case another: + ... + + +match maybe, multiple: + case perhaps, 5: + pass + case perhaps, 6,: + pass + + +match more := (than, one), indeed,: + case _, (5, 6): + pass + case [[5], (6)], [7],: + pass + case _: + pass + + +match a, *b, c: + case [*_]: + assert "seq" == _ + case {}: + assert "map" == b + + +match match( + case, + match( + match, case, match, looooooooooooooooooooooooooooooooooooong, match, case, match + ), + case, +): + case case( + match=case, + case=re.match( + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong + ), + ): + pass + + case [a as match]: + pass + + case case: + pass + + +match match: + case case: + pass + + +match a, *b(), c: + case d, *f, g: + pass + + +match something: + case { + "key": key as key_1, + "password": PASS.ONE | PASS.TWO | PASS.THREE as password, + }: + pass + case {"maybe": something(complicated as this) as that}: + pass + + +match something: + case 1 as a: + pass + + case 2 as b, 3 as c: + pass + + case 4 as d, (5 as e), (6 | 7 as g), *h: + pass + + +match bar1: + case Foo(aa=Callable() as aa, bb=int()): + print(bar1.aa, bar1.bb) + case _: + print("no match", "\n") + + +match bar1: + case Foo( + normal=x, perhaps=[list, {"x": d, "y": 1.0}] as y, otherwise=something, q=t as u + ): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py.expect new file mode 100644 index 0000000000..0242d264e5 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py.expect @@ -0,0 +1,119 @@ +import match + +match something: + case [a as b]: + print(b) + case [a as b, c, d, e as f]: + print(f) + case Point(a as b): + print(b) + case Point(int() as x, int() as y): + print(x, y) + + +match = 1 +case: int = re.match(something) + +match re.match(case): + case type("match", match): + pass + case match: + pass + + +def func(match: case, case: match) -> case: + match Something(): + case func(match, case): + ... + case another: + ... + + +match maybe, multiple: + case perhaps, 5: + pass + case perhaps, 6,: + pass + + +match more := (than, one), indeed,: + case _, (5, 6): + pass + case [[5], (6)], [7],: + pass + case _: + pass + + +match a, *b, c: + case [*_]: + assert "seq" == _ + case {}: + assert "map" == b + + +match match( + case, + match( + match, case, match, looooooooooooooooooooooooooooooooooooong, match, case, match + ), + case, +): + case case( + match=case, + case=re.match( + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong + ), + ): + pass + + case [a as match]: + pass + + case case: + pass + + +match match: + case case: + pass + + +match a, *b(), c: + case d, *f, g: + pass + + +match something: + case { + "key": key as key_1, + "password": PASS.ONE | PASS.TWO | PASS.THREE as password, + }: + pass + case {"maybe": something(complicated as this) as that}: + pass + + +match something: + case 1 as a: + pass + + case 2 as b, 3 as c: + pass + + case 4 as d, (5 as e), (6 | 7 as g), *h: + pass + + +match bar1: + case Foo(aa=Callable() as aa, bb=int()): + print(bar1.aa, bar1.bb) + case _: + print("no match", "\n") + + +match bar1: + case Foo( + normal=x, perhaps=[list, {"x": d, "y": 1.0}] as y, otherwise=something, q=t as u + ): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py new file mode 100644 index 0000000000..00a0e4a677 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py @@ -0,0 +1,107 @@ +re.match() +match = a +with match() as match: + match = f"{match}" + +re.match() +match = a +with match() as match: + match = f"{match}" + + +def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: + if not target_versions: + # No target_version specified, so try all grammars. + return [ + # Python 3.7+ + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords, + # Python 3.0-3.6 + pygram.python_grammar_no_print_statement_no_exec_statement, + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + match match: + case case: + match match: + case case: + pass + + if all(version.is_python2() for version in target_versions): + # Python 2-only code, so try Python 2 grammars. + return [ + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + re.match() + match = a + with match() as match: + match = f"{match}" + + def test_patma_139(self): + x = False + match x: + case bool(z): + y = 0 + self.assertIs(x, False) + self.assertEqual(y, 0) + self.assertIs(z, x) + + # Python 3-compatible code, so only try Python 3 grammar. + grammars = [] + if supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.10+ + grammars.append(pygram.python_grammar_soft_keywords) + # If we have to parse both, try to parse async as a keyword first + if not supports_feature( + target_versions, Feature.ASYNC_IDENTIFIERS + ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.7-3.9 + grammars.append( + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords + ) + if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS): + # Python 3.0-3.6 + grammars.append(pygram.python_grammar_no_print_statement_no_exec_statement) + + def test_patma_155(self): + x = 0 + y = None + match x: + case 1e1000: + y = 0 + self.assertEqual(x, 0) + self.assertIs(y, None) + + x = range(3) + match x: + case [y, case as x, z]: + w = 0 + + # At least one of the above branches must have been taken, because every Python + # version has exactly one of the two 'ASYNC_*' flags + return grammars + + +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"): + src_txt += "\n" + + grammars = get_grammars(set(target_versions)) + + +re.match() +match = a +with match() as match: + match = f"{match}" + +re.match() +match = a +with match() as match: + match = f"{match}" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py.expect new file mode 100644 index 0000000000..00a0e4a677 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py.expect @@ -0,0 +1,107 @@ +re.match() +match = a +with match() as match: + match = f"{match}" + +re.match() +match = a +with match() as match: + match = f"{match}" + + +def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: + if not target_versions: + # No target_version specified, so try all grammars. + return [ + # Python 3.7+ + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords, + # Python 3.0-3.6 + pygram.python_grammar_no_print_statement_no_exec_statement, + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + match match: + case case: + match match: + case case: + pass + + if all(version.is_python2() for version in target_versions): + # Python 2-only code, so try Python 2 grammars. + return [ + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + re.match() + match = a + with match() as match: + match = f"{match}" + + def test_patma_139(self): + x = False + match x: + case bool(z): + y = 0 + self.assertIs(x, False) + self.assertEqual(y, 0) + self.assertIs(z, x) + + # Python 3-compatible code, so only try Python 3 grammar. + grammars = [] + if supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.10+ + grammars.append(pygram.python_grammar_soft_keywords) + # If we have to parse both, try to parse async as a keyword first + if not supports_feature( + target_versions, Feature.ASYNC_IDENTIFIERS + ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.7-3.9 + grammars.append( + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords + ) + if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS): + # Python 3.0-3.6 + grammars.append(pygram.python_grammar_no_print_statement_no_exec_statement) + + def test_patma_155(self): + x = 0 + y = None + match x: + case 1e1000: + y = 0 + self.assertEqual(x, 0) + self.assertIs(y, None) + + x = range(3) + match x: + case [y, case as x, z]: + w = 0 + + # At least one of the above branches must have been taken, because every Python + # version has exactly one of the two 'ASYNC_*' flags + return grammars + + +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"): + src_txt += "\n" + + grammars = get_grammars(set(target_versions)) + + +re.match() +match = a +with match() as match: + match = f"{match}" + +re.match() +match = a +with match() as match: + match = f"{match}" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py new file mode 100644 index 0000000000..5ed62415a4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py @@ -0,0 +1,92 @@ +# Cases sampled from PEP 636 examples + +match command.split(): + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case [action]: + ... # interpret single-verb action + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case ["quit"]: + print("Goodbye!") + quit_game() + case ["look"]: + current_room.describe() + case ["get", obj]: + character.get(obj, current_room) + case ["go", direction]: + current_room = current_room.neighbor(direction) + # The rest of your commands go here + +match command.split(): + case ["drop", *objects]: + for obj in objects: + character.drop(obj, current_room) + # The rest of your commands go here + +match command.split(): + case ["quit"]: + pass + case ["go", direction]: + print("Going:", direction) + case ["drop", *objects]: + print("Dropping: ", *objects) + case _: + print(f"Sorry, I couldn't understand {command!r}") + +match command.split(): + case ["north"] | ["go", "north"]: + current_room = current_room.neighbor("north") + case ["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"]: + ... # Code for picking up the given object + +match command.split(): + case ["go", ("north" | "south" | "east" | "west")]: + current_room = current_room.neighbor(...) + # how do I know which direction to go? + +match command.split(): + case ["go", ("north" | "south" | "east" | "west") as direction]: + current_room = current_room.neighbor(direction) + +match command.split(): + case ["go", direction] if direction in current_room.exits: + current_room = current_room.neighbor(direction) + case ["go", _]: + print("Sorry, you can't go that way") + +match event.get(): + case Click(position=(x, y)): + handle_click_at(x, y) + case KeyPress(key_name="Q") | Quit(): + game.quit() + case KeyPress(key_name="up arrow"): + game.go_north() + case KeyPress(): + pass # Ignore other keystrokes + case other_event: + raise ValueError(f"Unrecognized event: {other_event}") + +match event.get(): + case Click((x, y), button=Button.LEFT): # This is a left click + handle_click_at(x, y) + case Click(): + pass # ignore other clicks + + +def where_is(point): + 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") diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py.expect new file mode 100644 index 0000000000..5ed62415a4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py.expect @@ -0,0 +1,92 @@ +# Cases sampled from PEP 636 examples + +match command.split(): + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case [action]: + ... # interpret single-verb action + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case ["quit"]: + print("Goodbye!") + quit_game() + case ["look"]: + current_room.describe() + case ["get", obj]: + character.get(obj, current_room) + case ["go", direction]: + current_room = current_room.neighbor(direction) + # The rest of your commands go here + +match command.split(): + case ["drop", *objects]: + for obj in objects: + character.drop(obj, current_room) + # The rest of your commands go here + +match command.split(): + case ["quit"]: + pass + case ["go", direction]: + print("Going:", direction) + case ["drop", *objects]: + print("Dropping: ", *objects) + case _: + print(f"Sorry, I couldn't understand {command!r}") + +match command.split(): + case ["north"] | ["go", "north"]: + current_room = current_room.neighbor("north") + case ["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"]: + ... # Code for picking up the given object + +match command.split(): + case ["go", ("north" | "south" | "east" | "west")]: + current_room = current_room.neighbor(...) + # how do I know which direction to go? + +match command.split(): + case ["go", ("north" | "south" | "east" | "west") as direction]: + current_room = current_room.neighbor(direction) + +match command.split(): + case ["go", direction] if direction in current_room.exits: + current_room = current_room.neighbor(direction) + case ["go", _]: + print("Sorry, you can't go that way") + +match event.get(): + case Click(position=(x, y)): + handle_click_at(x, y) + case KeyPress(key_name="Q") | Quit(): + game.quit() + case KeyPress(key_name="up arrow"): + game.go_north() + case KeyPress(): + pass # Ignore other keystrokes + case other_event: + raise ValueError(f"Unrecognized event: {other_event}") + +match event.get(): + case Click((x, y), button=Button.LEFT): # This is a left click + handle_click_at(x, y) + case Click(): + pass # ignore other clicks + + +def where_is(point): + 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") diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py new file mode 100644 index 0000000000..e17f1cdbf6 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py @@ -0,0 +1,53 @@ +match something: + case b(): print(1+1) + case c( + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=- 1 + ): print(1) + case c( + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1, + ): print(2) + case a: pass + +match( + arg # comment +) + +match( +) + +match( + + +) + +case( + arg # comment +) + +case( +) + +case( + + +) + + +re.match( + something # fast +) +re.match( + + + +) +match match( + + +): + case case( + arg, # comment + ): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py.expect new file mode 100644 index 0000000000..d81fa59e33 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py.expect @@ -0,0 +1,35 @@ +match something: + case b(): + print(1 + 1) + case c( + very_complex=True, perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1 + ): + print(1) + case c( + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1, + ): + print(2) + case a: + pass + +match(arg) # comment + +match() + +match() + +case(arg) # comment + +case() + +case() + + +re.match(something) # fast +re.match() +match match(): + case case( + arg, # comment + ): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py new file mode 100644 index 0000000000..cb82b2d23f --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py @@ -0,0 +1,15 @@ +# Unparenthesized walruses are now allowed in indices since Python 3.10. +x[a:=0] +x[a:=0, b:=1] +x[5, b:=0] + +# Walruses are allowed inside generator expressions on function calls since 3.10. +if any(match := pattern_error.match(s) for s in buffer): + if match.group(2) == data_not_available: + # Error OK to ignore. + pass + +f(a := b + c for c in range(10)) +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) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py.expect new file mode 100644 index 0000000000..cb82b2d23f --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py.expect @@ -0,0 +1,15 @@ +# Unparenthesized walruses are now allowed in indices since Python 3.10. +x[a:=0] +x[a:=0, b:=1] +x[5, b:=0] + +# Walruses are allowed inside generator expressions on function calls since 3.10. +if any(match := pattern_error.match(s) for s in buffer): + if match.group(2) == data_not_available: + # Error OK to ignore. + pass + +f(a := b + c for c in range(10)) +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) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py new file mode 100644 index 0000000000..629b645fce --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py @@ -0,0 +1,19 @@ +def http_status(status): + + match status: + + case 400: + + return "Bad request" + + case 401: + + return "Unauthorized" + + case 403: + + return "Forbidden" + + case 404: + + return "Not found" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py.expect new file mode 100644 index 0000000000..735169ef52 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py.expect @@ -0,0 +1,13 @@ +def http_status(status): + match status: + case 400: + return "Bad request" + + case 401: + return "Unauthorized" + + case 403: + return "Forbidden" + + case 404: + return "Not found" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py new file mode 100644 index 0000000000..8fc8e059ed --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py @@ -0,0 +1,27 @@ +for x in *a, *b: + print(x) + +for x in a, b, *c: + print(x) + +for x in *a, b, c: + print(x) + +for x in *a, b, *c: + print(x) + +async for x in *a, *b: + print(x) + +async for x in *a, b, *c: + print(x) + +async for x in a, b, *c: + print(x) + +async for x in ( + *loooooooooooooooooooooong, + very, + *loooooooooooooooooooooooooooooooooooooooooooooooong, +): + print(x) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py.expect new file mode 100644 index 0000000000..8fc8e059ed --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py.expect @@ -0,0 +1,27 @@ +for x in *a, *b: + print(x) + +for x in a, b, *c: + print(x) + +for x in *a, b, c: + print(x) + +for x in *a, b, *c: + print(x) + +async for x in *a, *b: + print(x) + +async for x in *a, b, *c: + print(x) + +async for x in a, b, *c: + print(x) + +async for x in ( + *loooooooooooooooooooooong, + very, + *loooooooooooooooooooooooooooooooooooooooooooooooong, +): + print(x) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py new file mode 100644 index 0000000000..387c0816f4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py @@ -0,0 +1,53 @@ +try: + raise OSError("blah") +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except* Exception: + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except* TypeError as e: + tes = e + raise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except* (TypeError, ValueError) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except* OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py.expect new file mode 100644 index 0000000000..387c0816f4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py.expect @@ -0,0 +1,53 @@ +try: + raise OSError("blah") +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except* Exception: + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except* TypeError as e: + tes = e + raise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except* (TypeError, ValueError) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except* OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py new file mode 100644 index 0000000000..1c5918d17d --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py @@ -0,0 +1,55 @@ +try: + raise OSError("blah") +except * ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except *ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except *(Exception): + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except \ + *TypeError as e: + tes = e + raise + except * ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except *(TypeError, ValueError, *OTHER_EXCEPTIONS) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except\ + * OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py.expect new file mode 100644 index 0000000000..c0d06dbfe1 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py.expect @@ -0,0 +1,53 @@ +try: + raise OSError("blah") +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except* Exception: + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except* TypeError as e: + tes = e + raise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except* (TypeError, ValueError, *OTHER_EXCEPTIONS) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except* OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py new file mode 100644 index 0000000000..6da4ba68d6 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3.6 + +x = 123456789 +x = 123456 +x = .1 +x = 1. +x = 1E+1 +x = 1E-1 +x = 1.000_000_01 +x = 123456789.123456789 +x = 123456789.123456789E123456789 +x = 123456789E123456789 +x = 123456789J +x = 123456789.123456789J +x = 0XB1ACC +x = 0B1011 +x = 0O777 +x = 0.000000006 +x = 10000 +x = 133333 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py.expect new file mode 100644 index 0000000000..e263924b4e --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py.expect @@ -0,0 +1,20 @@ +#!/usr/bin/env python3.6 + +x = 123456789 +x = 123456 +x = 0.1 +x = 1.0 +x = 1e1 +x = 1e-1 +x = 1.000_000_01 +x = 123456789.123456789 +x = 123456789.123456789e123456789 +x = 123456789e123456789 +x = 123456789j +x = 123456789.123456789j +x = 0xB1ACC +x = 0b1011 +x = 0o777 +x = 0.000000006 +x = 10000 +x = 133333 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py new file mode 100644 index 0000000000..d77116a832 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3.6 + +x = 123456789 +x = 1_2_3_4_5_6_7 +x = 1E+1 +x = 0xb1acc +x = 0.00_00_006 +x = 12_34_567J +x = .1_2 +x = 1_2. diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py.expect new file mode 100644 index 0000000000..a81ada11e5 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py.expect @@ -0,0 +1,10 @@ +#!/usr/bin/env python3.6 + +x = 123456789 +x = 1_2_3_4_5_6_7 +x = 1e1 +x = 0xB1ACC +x = 0.00_00_006 +x = 12_34_567j +x = 0.1_2 +x = 1_2.0 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py new file mode 100644 index 0000000000..01fd7eede3 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3.7 + + +def f(): + return (i * 2 async for i in arange(42)) + + +def g(): + return ( + something_long * something_long + async for something_long in async_generator(with_an_argument) + ) + + +async def func(): + if test: + out_batched = [ + i + async for i in aitertools._async_map( + self.async_inc, arange(8), batch_size=3 + ) + ] + + +def awaited_generator_value(n): + return (await awaitable for awaitable in awaitable_list) + + +def make_arange(n): + return (i * 2 for i in range(n) if await wrap(i)) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py.expect new file mode 100644 index 0000000000..01fd7eede3 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py.expect @@ -0,0 +1,30 @@ +#!/usr/bin/env python3.7 + + +def f(): + return (i * 2 async for i in arange(42)) + + +def g(): + return ( + something_long * something_long + async for something_long in async_generator(with_an_argument) + ) + + +async def func(): + if test: + out_batched = [ + i + async for i in aitertools._async_map( + self.async_inc, arange(8), batch_size=3 + ) + ] + + +def awaited_generator_value(n): + return (await awaitable for awaitable in awaitable_list) + + +def make_arange(n): + return (i * 2 for i in range(n) if await wrap(i)) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py new file mode 100644 index 0000000000..ca8f7ab1d9 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py @@ -0,0 +1,44 @@ +def positional_only_arg(a, /): + pass + + +def all_markers(a, b, /, c, d, *, e, f): + pass + + +def all_markers_with_args_and_kwargs( + a_long_one, + b_long_one, + /, + c_long_one, + d_long_one, + *args, + e_long_one, + f_long_one, + **kwargs, +): + pass + + +def all_markers_with_defaults(a, b=1, /, c=2, d=3, *, e=4, f=5): + pass + + +def long_one_with_long_parameter_names( + but_all_of_them, + are_positional_only, + arguments_mmmmkay, + so_this_is_only_valid_after, + three_point_eight, + /, +): + pass + + +lambda a, /: a + +lambda a, b, /, c, d, *, e, f: a + +lambda a, b, /, c, d, *args, e, f, **kwargs: args + +lambda a, b=1, /, c=2, d=3, *, e=4, f=5: 1 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py.expect new file mode 100644 index 0000000000..ca8f7ab1d9 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py.expect @@ -0,0 +1,44 @@ +def positional_only_arg(a, /): + pass + + +def all_markers(a, b, /, c, d, *, e, f): + pass + + +def all_markers_with_args_and_kwargs( + a_long_one, + b_long_one, + /, + c_long_one, + d_long_one, + *args, + e_long_one, + f_long_one, + **kwargs, +): + pass + + +def all_markers_with_defaults(a, b=1, /, c=2, d=3, *, e=4, f=5): + pass + + +def long_one_with_long_parameter_names( + but_all_of_them, + are_positional_only, + arguments_mmmmkay, + so_this_is_only_valid_after, + three_point_eight, + /, +): + pass + + +lambda a, /: a + +lambda a, b, /, c, d, *, e, f: a + +lambda a, b, /, c, d, *args, e, f, **kwargs: args + +lambda a, b=1, /, c=2, d=3, *, e=4, f=5: 1 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py new file mode 100644 index 0000000000..d41805f1cb --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py @@ -0,0 +1,47 @@ +(a := 1) +(a := a) +if (match := pattern.search(data)) is None: + pass +if match := pattern.search(data): + pass +[y := f(x), y**2, y**3] +filtered_data = [y for x in data if (y := f(x)) is None] +(y := f(x)) +y0 = (y1 := f(x)) +foo(x=(y := f(x))) + + +def foo(answer=(p := 42)): + pass + + +def foo(answer: (p := 42) = 5): + pass + + +lambda: (x := 1) +(x := lambda: 1) +(x := lambda: (y := 1)) +lambda line: (m := re.match(pattern, line)) and m.group(1) +x = (y := 0) +(z := (y := (x := 0))) +(info := (name, phone, *rest)) +(x := 1, 2) +(total := total + tax) +len(lines := f.readlines()) +foo(x := 3, cat="vector") +foo(cat=(category := "vector")) +if any(len(longline := l) >= 100 for l in lines): + print(longline) +if env_base := os.environ.get("PYTHONUSERBASE", None): + return env_base +if self._is_special and (ans := self._check_nans(context=context)): + return ans +foo(b := 2, a=1) +foo((b := 2), a=1) +foo(c=(b := 2), a=1) + +while x := f(x): + pass +while x := f(x): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py.expect new file mode 100644 index 0000000000..d41805f1cb --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py.expect @@ -0,0 +1,47 @@ +(a := 1) +(a := a) +if (match := pattern.search(data)) is None: + pass +if match := pattern.search(data): + pass +[y := f(x), y**2, y**3] +filtered_data = [y for x in data if (y := f(x)) is None] +(y := f(x)) +y0 = (y1 := f(x)) +foo(x=(y := f(x))) + + +def foo(answer=(p := 42)): + pass + + +def foo(answer: (p := 42) = 5): + pass + + +lambda: (x := 1) +(x := lambda: 1) +(x := lambda: (y := 1)) +lambda line: (m := re.match(pattern, line)) and m.group(1) +x = (y := 0) +(z := (y := (x := 0))) +(info := (name, phone, *rest)) +(x := 1, 2) +(total := total + tax) +len(lines := f.readlines()) +foo(x := 3, cat="vector") +foo(cat=(category := "vector")) +if any(len(longline := l) >= 100 for l in lines): + print(longline) +if env_base := os.environ.get("PYTHONUSERBASE", None): + return env_base +if self._is_special and (ans := self._check_nans(context=context)): + return ans +foo(b := 2, a=1) +foo((b := 2), a=1) +foo(c=(b := 2), a=1) + +while x := f(x): + pass +while x := f(x): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py new file mode 100644 index 0000000000..391b52f8ce --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3.8 + + +def starred_return(): + my_list = ["value2", "value3"] + return "value1", *my_list + + +def starred_yield(): + my_list = ["value2", "value3"] + yield "value1", *my_list + + +# all right hand side expressions allowed in regular assignments are now also allowed in +# annotated assignments +a : Tuple[ str, int] = "1", 2 +a: Tuple[int , ... ] = b, *c, d +def t(): + a : str = yield "a" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py.expect new file mode 100644 index 0000000000..5df012410a --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py.expect @@ -0,0 +1,21 @@ +#!/usr/bin/env python3.8 + + +def starred_return(): + my_list = ["value2", "value3"] + return "value1", *my_list + + +def starred_yield(): + my_list = ["value2", "value3"] + yield "value1", *my_list + + +# all right hand side expressions allowed in regular assignments are now also allowed in +# annotated assignments +a: Tuple[str, int] = "1", 2 +a: Tuple[int, ...] = b, *c, d + + +def t(): + a: str = yield "a" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py new file mode 100644 index 0000000000..b8b081b8c4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py @@ -0,0 +1,7 @@ +# Unparenthesized walruses are now allowed in set literals & set comprehensions +# since Python 3.9 +{x := 1, 2, 3} +{x4 := x**5 for x in range(7)} +# We better not remove the parentheses here (since it's a 3.10 feature) +x[(a := 1)] +x[(a := 1), (b := 3)] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py.expect new file mode 100644 index 0000000000..b8b081b8c4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py.expect @@ -0,0 +1,7 @@ +# Unparenthesized walruses are now allowed in set literals & set comprehensions +# since Python 3.9 +{x := 1, 2, 3} +{x4 := x**5 for x in range(7)} +# We better not remove the parentheses here (since it's a 3.10 feature) +x[(a := 1)] +x[(a := 1), (b := 3)] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py new file mode 100644 index 0000000000..227faca09a --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3.9 + +@relaxed_decorator[0] +def f(): + ... + +@relaxed_decorator[extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length] +def f(): + ... + +@extremely_long_variable_name_that_doesnt_fit := complex.expression(with_long="arguments_value_that_wont_fit_at_the_end_of_the_line") +def f(): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py.expect new file mode 100644 index 0000000000..4af4beebb2 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py.expect @@ -0,0 +1,20 @@ +#!/usr/bin/env python3.9 + + +@relaxed_decorator[0] +def f(): + ... + + +@relaxed_decorator[ + extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length +] +def f(): + ... + + +@extremely_long_variable_name_that_doesnt_fit := complex.expression( + with_long="arguments_value_that_wont_fit_at_the_end_of_the_line" +) +def f(): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py new file mode 100644 index 0000000000..9634bab444 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py @@ -0,0 +1,54 @@ +with (open("bla.txt")): + pass + +with (open("bla.txt")), (open("bla.txt")): + pass + +with (open("bla.txt") as f): + pass + +# Remove brackets within alias expression +with (open("bla.txt")) as f: + pass + +# Remove brackets around one-line context managers +with (open("bla.txt") as f, (open("x"))): + pass + +with ((open("bla.txt")) as f, open("x")): + pass + +with (CtxManager1() as example1, CtxManager2() as example2): + ... + +# Brackets remain when using magic comma +with (CtxManager1() as example1, CtxManager2() as example2,): + ... + +# Brackets remain for multi-line context managers +with (CtxManager1() as example1, CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2): + ... + +# Don't touch assignment expressions +with (y := open("./test.py")) as f: + pass + +# Deeply nested examples +# N.B. Multiple brackets are only possible +# around the context manager itself. +# Only one brackets is allowed around the +# alias expression or comma-delimited context managers. +with (((open("bla.txt")))): + pass + +with (((open("bla.txt")))), (((open("bla.txt")))): + pass + +with (((open("bla.txt")))) as f: + pass + +with ((((open("bla.txt")))) as f): + pass + +with ((((CtxManager1()))) as example1, (((CtxManager2()))) as example2): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py.expect new file mode 100644 index 0000000000..e70d01b18d --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py.expect @@ -0,0 +1,63 @@ +with open("bla.txt"): + pass + +with open("bla.txt"), open("bla.txt"): + pass + +with open("bla.txt") as f: + pass + +# Remove brackets within alias expression +with open("bla.txt") as f: + pass + +# Remove brackets around one-line context managers +with open("bla.txt") as f, open("x"): + pass + +with open("bla.txt") as f, open("x"): + pass + +with CtxManager1() as example1, CtxManager2() as example2: + ... + +# Brackets remain when using magic comma +with ( + CtxManager1() as example1, + CtxManager2() as example2, +): + ... + +# Brackets remain for multi-line context managers +with ( + CtxManager1() as example1, + CtxManager2() as example2, + CtxManager2() as example2, + CtxManager2() as example2, + CtxManager2() as example2, +): + ... + +# Don't touch assignment expressions +with (y := open("./test.py")) as f: + pass + +# Deeply nested examples +# N.B. Multiple brackets are only possible +# around the context manager itself. +# Only one brackets is allowed around the +# alias expression or comma-delimited context managers. +with open("bla.txt"): + pass + +with open("bla.txt"), open("bla.txt"): + pass + +with open("bla.txt") as f: + pass + +with open("bla.txt") as f: + pass + +with CtxManager1() as example1, CtxManager2() as example2: + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments.py.expect index 4a94e7ad93..c34daaf6f0 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments.py.expect +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments.py.expect @@ -93,4 +93,4 @@ async def wat(): # Some closing comments. # Maybe Vim or Emacs directives for formatting. -# Who knows. \ No newline at end of file +# Who knows. diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/expression.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/expression.py.expect index 08be5ea501..16160f98f3 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/expression.py.expect +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/expression.py.expect @@ -1,3 +1,4 @@ +... "some_string" b"\\xa3" Name @@ -114,7 +115,7 @@ call( arg, another, kwarg="hey", - **kwargs, + **kwargs ) # note: no trailing comma pre-3.6 call(*gidgets[:2]) call(a, *gidgets[:2]) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff.py.expect index 8631f8eaaa..2534e5cc1d 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff.py.expect +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff.py.expect @@ -5,7 +5,6 @@ import sys from third_party import X, Y, Z from library import some_connection, some_decorator - # fmt: off from third_party import (X, Y, Z) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py new file mode 100644 index 0000000000..8b3c0bc662 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py @@ -0,0 +1,19 @@ +# 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/resources/test/fixtures/black/simple_cases/fmtpass_imports.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py.expect new file mode 100644 index 0000000000..8b3c0bc662 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py.expect @@ -0,0 +1,19 @@ +# 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/resources/test/fixtures/black/simple_cases/fstring.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py index b778ec2879..190cd6294a 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py @@ -7,3 +7,5 @@ f"{f'''{'nested'} inner'''} outer" f"\"{f'{nested} inner'}\" outer" f"space between opening braces: { {a for a in (1, 2, 3)}}" f'Hello \'{tricky + "example"}\'' +f"Tried directories {str(rootdirs)} \ +but none started with prefix {parentdir_prefix}" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py.expect index 0c6538c5bf..488d786e39 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py.expect +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py.expect @@ -7,3 +7,5 @@ f"{f'''{'nested'} inner'''} outer" f"\"{f'{nested} inner'}\" outer" f"space between opening braces: { {a for a in (1, 2, 3)}}" f'Hello \'{tricky + "example"}\'' +f"Tried directories {str(rootdirs)} \ +but none started with prefix {parentdir_prefix}" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function.py index bc41e08a16..e94c5c5ace 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function.py @@ -34,7 +34,7 @@ def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r'' def spaces_types(a: int = 1, b: tuple = (), c: list = [], d: dict = {}, e: bool = True, f: int = -1, g: int = 1 if False else 2, h: str = "", i: str = r''): ... def spaces2(result= _core.Value(None)): assert fut is self._read_fut, (fut, self._read_fut) - + # EMPTY LINE WITH WHITESPACE (this comment will be removed) def example(session): result = session.query(models.Customer.id).filter( models.Customer.account_id == account_id, diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/pep_604.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/pep_604.py new file mode 100644 index 0000000000..2ff5ca4829 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/pep_604.py @@ -0,0 +1,6 @@ +def some_very_long_name_function() -> my_module.Asdf | my_module.AnotherType | my_module.YetAnotherType | None: + pass + + +def some_very_long_name_function() -> my_module.Asdf | my_module.AnotherType | my_module.YetAnotherType | my_module.EvenMoreType | None: + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/pep_604.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/pep_604.py.expect new file mode 100644 index 0000000000..1629cc693b --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/pep_604.py.expect @@ -0,0 +1,14 @@ +def some_very_long_name_function() -> ( + my_module.Asdf | my_module.AnotherType | my_module.YetAnotherType | None +): + pass + + +def some_very_long_name_function() -> ( + my_module.Asdf + | my_module.AnotherType + | my_module.YetAnotherType + | my_module.EvenMoreType + | None +): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py index 63885bb872..165117cdcb 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py @@ -29,35 +29,3 @@ ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] ham[lower + offset : upper + offset] - -slice[::, ::] -slice[ - # A - : - # B - : - # C -] -slice[ - # A - 1: - # B - 2: - # C - 3 -] - -slice[ - # A - 1 - + 2 : - # B - 3 : - # C - 4 -] -x[ - 1: # A - 2: # B - 3 # C -] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py.expect index 61218829e4..165117cdcb 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py.expect +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py.expect @@ -29,31 +29,3 @@ ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] ham[lower + offset : upper + offset] - -slice[::, ::] -slice[ - # A - : - # B - : - # C -] -slice[ - # A - 1: - # B - 2: - # C - 3 -] - -slice[ - # A - 1 - + 2 : - # B - 3 : - # C - 4 -] -x[1:2:3] # A # B # C diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/whitespace.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/whitespace.py.expect index e69de29bb2..8b13789179 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/whitespace.py.expect +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/whitespace.py.expect @@ -0,0 +1 @@ + diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/type_comments/type_comment_syntax_error.py b/crates/ruff_python_formatter/resources/test/fixtures/black/type_comments/type_comment_syntax_error.py new file mode 100644 index 0000000000..70e2fbe328 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/type_comments/type_comment_syntax_error.py @@ -0,0 +1,3 @@ +def foo( + # type: Foo + x): pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/type_comments/type_comment_syntax_error.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/type_comments/type_comment_syntax_error.py.expect new file mode 100644 index 0000000000..764f7cedd8 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/type_comments/type_comment_syntax_error.py.expect @@ -0,0 +1,5 @@ +def foo( + # type: Foo + x, +): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/import_black_tests.py b/crates/ruff_python_formatter/resources/test/fixtures/import_black_tests.py new file mode 100755 index 0000000000..6cad807da4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/import_black_tests.py @@ -0,0 +1,113 @@ +#!/usr/bin/python3 + +from __future__ import annotations + +import argparse +from pathlib import Path + + +def import_fixture(fixture: Path, fixture_set: str): + """ + Imports a single fixture by writing the input and expected output to the black directory. + """ + + output_directory = Path(__file__).parent.joinpath("black").joinpath(fixture_set) + output_directory.mkdir(parents=True, exist_ok=True) + + fixture_path = output_directory.joinpath(fixture.name) + expect_path = fixture_path.with_suffix(".py.expect") + + with ( + fixture.open("r") as black_file, + fixture_path.open("w") as fixture_file, + expect_path.open("w") as expect_file + ): + lines = iter(black_file) + expected = [] + input = [] + + for line in lines: + if line.rstrip() == "# output": + expected = list(lines) + break + else: + input.append(line) + + if not expected: + # If there's no output marker, tread the whole file as already pre-formatted + expected = input + + fixture_file.write("".join(input).strip() + "\n") + expect_file.write("".join(expected).strip() + "\n") + + +# The name of the folders in the `data` for which the tests should be imported +FIXTURE_SETS = [ + "py_36", + "py_37", + "py_38", + "py_39", + "py_310", + "py_311", + "simple_cases", + "miscellaneous", + ".", + "type_comments" +] + +# Tests that ruff doesn't fully support yet and, therefore, should not be imported +IGNORE_LIST = [ + "pep_572_remove_parens.py", # Reformatting bugs + "pep_646.py", # Rust Python parser bug + + # Contain syntax errors + "async_as_identifier.py", + "invalid_header.py", + "pattern_matching_invalid.py", + + # Python 2 + "python2_detection.py" +] + + +def import_fixtures(black_dir: str): + """Imports all the black fixtures""" + + test_directory = Path(black_dir, "tests/data") + + if not test_directory.exists(): + print( + "Black directory does not contain a 'tests/data' directory. Does the directory point to a full black " + "checkout (git clone https://github.com/psf/black.git)?") + return + + for fixture_set in FIXTURE_SETS: + fixture_directory = test_directory.joinpath(fixture_set) + fixtures = fixture_directory.glob("*.py") + + if not fixtures: + print(f"Fixture set '{fixture_set}' contains no python files") + return + + for fixture in fixtures: + if fixture.name in IGNORE_LIST: + print(f"Ignoring fixture '{fixture}") + continue + + print(f"Importing fixture '{fixture}") + import_fixture(fixture, fixture_set) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description="Imports the test suite from black.", + epilog="import_black_tests.py " + ) + + parser.add_argument("black_dir", type=Path) + + args = parser.parse_args() + + black_dir = args.black_dir + + import_fixtures(black_dir) diff --git a/crates/ruff_python_formatter/tests/fixtures.rs b/crates/ruff_python_formatter/tests/fixtures.rs index 43a94051e7..d312ff7de6 100644 --- a/crates/ruff_python_formatter/tests/fixtures.rs +++ b/crates/ruff_python_formatter/tests/fixtures.rs @@ -12,7 +12,12 @@ fn black_compatibility() { let content = fs::read_to_string(input_path).unwrap(); let options = PyFormatOptions::default(); - let printed = format_module(&content, options.clone()).expect("Formatting to succeed"); + let printed = format_module(&content, options.clone()).unwrap_or_else(|err| { + panic!( + "Formatting of {} to succeed but encountered error {err}", + input_path.display() + ) + }); let expected_path = input_path.with_extension("py.expect"); let expected_output = fs::read_to_string(&expected_path) @@ -20,7 +25,7 @@ fn black_compatibility() { let formatted_code = printed.as_code(); - ensure_stability_when_formatting_twice(formatted_code, options); + ensure_stability_when_formatting_twice(formatted_code, options, input_path); if formatted_code == expected_output { // Black and Ruff formatting matches. Delete any existing snapshot files because the Black output @@ -95,7 +100,7 @@ fn format() { let printed = format_module(&content, options.clone()).expect("Formatting to succeed"); let formatted_code = printed.as_code(); - ensure_stability_when_formatting_twice(formatted_code, options); + ensure_stability_when_formatting_twice(formatted_code, options, input_path); let mut snapshot = format!("## Input\n{}", CodeFrame::new("py", &content)); @@ -112,7 +117,7 @@ fn format() { format_module(&content, options.clone()).expect("Formatting to succeed"); let formatted_code = printed.as_code(); - ensure_stability_when_formatting_twice(formatted_code, options.clone()); + ensure_stability_when_formatting_twice(formatted_code, options.clone(), input_path); writeln!( snapshot, @@ -128,7 +133,7 @@ fn format() { let printed = format_module(&content, options.clone()).expect("Formatting to succeed"); let formatted_code = printed.as_code(); - ensure_stability_when_formatting_twice(formatted_code, options); + ensure_stability_when_formatting_twice(formatted_code, options, input_path); writeln!( snapshot, @@ -151,13 +156,18 @@ fn format() { } /// Format another time and make sure that there are no changes anymore -fn ensure_stability_when_formatting_twice(formatted_code: &str, options: PyFormatOptions) { +fn ensure_stability_when_formatting_twice( + formatted_code: &str, + options: PyFormatOptions, + input_path: &Path, +) { let reformatted = match format_module(formatted_code, options) { Ok(reformatted) => reformatted, Err(err) => { panic!( - "Expected formatted code to be valid syntax: {err}:\ + "Expected formatted code of {} to be valid syntax: {err}:\ \n---\n{formatted_code}---\n", + input_path.display() ); } }; @@ -168,7 +178,7 @@ fn ensure_stability_when_formatting_twice(formatted_code: &str, options: PyForma .header("Formatted once", "Formatted twice") .to_string(); panic!( - r#"Reformatting the formatted code a second time resulted in formatting changes. + r#"Reformatting the formatted code of {} a second time resulted in formatting changes. --- {diff}--- @@ -179,7 +189,8 @@ Formatted once: Formatted twice: --- {}---"#, - reformatted.as_code() + input_path.display(), + reformatted.as_code(), ); } } 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 new file mode 100644 index 0000000000..5ab4cf643f --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@conditional_expression.py.snap @@ -0,0 +1,334 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py +--- +## Input + +```py +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=a + if foo + else b, + baz="hello, this is a another value", +) + +imploding_line = ( + 1 + if 1 + 1 == 2 + else 0 +) + +exploding_line = "hello this is a slightly long string" if some_long_value_name_foo_bar_baz else "this one is a little shorter" + +positional_argument_test(some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz) + +def weird_default_argument(x=some_long_value_name_foo_bar_baz + if SOME_CONSTANT + else some_fallback_value_foo_bar_baz): + pass + +nested = "hello this is a slightly long string" if (some_long_value_name_foo_bar_baz if + nesting_test_expressions else some_fallback_value_foo_bar_baz) \ + else "this one is a little shorter" + +generator_expression = ( + some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz for some_boolean_variable in some_iterable +) + + +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( + sql + for sql in ( + "LIMIT %d" % limit if limit else None, + ("OFFSET %d" % offset) if offset else None, + ) + if sql + ) + + +def something(): + clone._iterable_class = ( + NamedValuesListIterable + if named + else FlatValuesListIterable + if flat + else ValuesListIterable + ) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,90 +1,48 @@ + long_kwargs_single_line = my_function( + foo="test, this is a sample value", +- bar=( +- some_long_value_name_foo_bar_baz +- if some_boolean_variable +- else some_fallback_value_foo_bar_baz +- ), ++ bar=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + baz="hello, this is a another value", + ) + + multiline_kwargs_indented = my_function( + foo="test, this is a sample value", +- bar=( +- some_long_value_name_foo_bar_baz +- if some_boolean_variable +- else some_fallback_value_foo_bar_baz +- ), ++ bar=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + baz="hello, this is a another value", + ) + + imploding_kwargs = my_function( + foo="test, this is a sample value", +- bar=a if foo else b, ++ bar=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + baz="hello, this is a another value", + ) + +-imploding_line = 1 if 1 + 1 == 2 else 0 ++imploding_line = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + +-exploding_line = ( +- "hello this is a slightly long string" +- if some_long_value_name_foo_bar_baz +- else "this one is a little shorter" +-) ++exploding_line = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + + positional_argument_test( +- some_long_value_name_foo_bar_baz +- if some_boolean_variable +- else some_fallback_value_foo_bar_baz ++ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + ) + + + def weird_default_argument( +- x=( +- some_long_value_name_foo_bar_baz +- if SOME_CONSTANT +- else some_fallback_value_foo_bar_baz +- ), ++ x=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + ): + pass + + +-nested = ( +- "hello this is a slightly long string" +- if ( +- some_long_value_name_foo_bar_baz +- if nesting_test_expressions +- else some_fallback_value_foo_bar_baz +- ) +- else "this one is a little shorter" +-) ++nested = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + +-generator_expression = ( +- ( +- some_long_value_name_foo_bar_baz +- if some_boolean_variable +- else some_fallback_value_foo_bar_baz +- ) +- for some_boolean_variable in some_iterable +-) ++generator_expression = (i for i 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( +- 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 [])) + + + def something(): + clone._iterable_class = ( +- NamedValuesListIterable +- if named +- else FlatValuesListIterable if flat else ValuesListIterable ++ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + ) +``` + +## Ruff Output + +```py +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + baz="hello, this is a another value", +) + +imploding_line = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + +exploding_line = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + +positional_argument_test( + NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false +) + + +def weird_default_argument( + x=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false +): + pass + + +nested = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + +generator_expression = (i for i 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 [])) + + +def something(): + clone._iterable_class = ( + NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + ) +``` + +## Black Output + +```py +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ), + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ), + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=a if foo else b, + baz="hello, this is a another value", +) + +imploding_line = 1 if 1 + 1 == 2 else 0 + +exploding_line = ( + "hello this is a slightly long string" + if some_long_value_name_foo_bar_baz + else "this one is a little shorter" +) + +positional_argument_test( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz +) + + +def weird_default_argument( + x=( + some_long_value_name_foo_bar_baz + if SOME_CONSTANT + else some_fallback_value_foo_bar_baz + ), +): + pass + + +nested = ( + "hello this is a slightly long string" + if ( + some_long_value_name_foo_bar_baz + if nesting_test_expressions + else some_fallback_value_foo_bar_baz + ) + else "this one is a little shorter" +) + +generator_expression = ( + ( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ) + for some_boolean_variable in some_iterable +) + + +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( + sql + for sql in ( + "LIMIT %d" % limit if limit else None, + ("OFFSET %d" % offset) if offset else None, + ) + if sql + ) + + +def something(): + clone._iterable_class = ( + NamedValuesListIterable + if named + else FlatValuesListIterable if flat else ValuesListIterable + ) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__blackd_diff.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__blackd_diff.py.snap new file mode 100644 index 0000000000..835d6e4d60 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__blackd_diff.py.snap @@ -0,0 +1,55 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py +--- +## Input + +```py +def abc (): + return ["hello", "world", + "!"] + +print( "Incorrect formatting" +) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,6 +1,5 @@ +-def abc (): +- return ["hello", "world", +- "!"] ++def abc(): ++ return ["hello", "world", "!"] + +-print( "Incorrect formatting" +-) ++ ++print("Incorrect formatting") +``` + +## Ruff Output + +```py +def abc(): + return ["hello", "world", "!"] + + +print("Incorrect formatting") +``` + +## Black Output + +```py +def abc (): + return ["hello", "world", + "!"] + +print( "Incorrect formatting" +) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap new file mode 100644 index 0000000000..0e00e30129 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap @@ -0,0 +1,167 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py +--- +## Input + +```py +@dataclass +class DebugVisitor(Visitor[T]): + tree_depth: int = 0 + + def visit_default(self, node: LN) -> Iterator[T]: + indent = ' ' * (2 * self.tree_depth) + if isinstance(node, Node): + _type = type_repr(node.type) + out(f'{indent}{_type}', fg='yellow') + self.tree_depth += 1 + for child in node.children: + yield from self.visit(child) + + self.tree_depth -= 1 + out(f'{indent}/{_type}', fg='yellow', bold=False) + else: + _type = token.tok_name.get(node.type, str(node.type)) + out(f'{indent}{_type}', fg='blue', nl=False) + if node.prefix: + # We don't have to handle prefixes for `Node` objects since + # that delegates to the first child anyway. + out(f' {node.prefix!r}', fg='green', bold=False, nl=False) + out(f' {node.value!r}', fg='blue', bold=False) + + @classmethod + def show(cls, code: str) -> None: + """Pretty-prints a given string of `code`. + + Convenience method for debugging. + """ + v: DebugVisitor[None] = DebugVisitor() + list(v.visit(lib2to3_parse(code))) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,26 +1,26 @@ + @dataclass + class DebugVisitor(Visitor[T]): +- tree_depth: int = 0 ++ NOT_YET_IMPLEMENTED_StmtAnnAssign + + def visit_default(self, node: LN) -> Iterator[T]: +- indent = ' ' * (2 * self.tree_depth) ++ indent = " " * (2 * self.tree_depth) + if isinstance(node, Node): + _type = type_repr(node.type) +- out(f'{indent}{_type}', fg='yellow') +- self.tree_depth += 1 ++ out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="yellow") ++ NOT_YET_IMPLEMENTED_StmtAugAssign + for child in node.children: +- yield from self.visit(child) ++ NOT_YET_IMPLEMENTED_ExprYieldFrom + +- self.tree_depth -= 1 +- out(f'{indent}/{_type}', fg='yellow', bold=False) ++ NOT_YET_IMPLEMENTED_StmtAugAssign ++ out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="yellow", bold=False) + else: + _type = token.tok_name.get(node.type, str(node.type)) +- out(f'{indent}{_type}', fg='blue', nl=False) ++ out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="blue", nl=False) + if node.prefix: + # We don't have to handle prefixes for `Node` objects since + # that delegates to the first child anyway. +- out(f' {node.prefix!r}', fg='green', bold=False, nl=False) +- out(f' {node.value!r}', fg='blue', bold=False) ++ out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="green", bold=False, nl=False) ++ out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="blue", bold=False) + + @classmethod + def show(cls, code: str) -> None: +@@ -28,5 +28,5 @@ + + Convenience method for debugging. + """ +- v: DebugVisitor[None] = DebugVisitor() ++ NOT_YET_IMPLEMENTED_StmtAnnAssign + list(v.visit(lib2to3_parse(code))) +``` + +## Ruff Output + +```py +@dataclass +class DebugVisitor(Visitor[T]): + NOT_YET_IMPLEMENTED_StmtAnnAssign + + def visit_default(self, node: LN) -> Iterator[T]: + indent = " " * (2 * self.tree_depth) + if isinstance(node, Node): + _type = type_repr(node.type) + out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="yellow") + NOT_YET_IMPLEMENTED_StmtAugAssign + for child in node.children: + NOT_YET_IMPLEMENTED_ExprYieldFrom + + NOT_YET_IMPLEMENTED_StmtAugAssign + out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="yellow", bold=False) + else: + _type = token.tok_name.get(node.type, str(node.type)) + out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="blue", nl=False) + if node.prefix: + # We don't have to handle prefixes for `Node` objects since + # that delegates to the first child anyway. + out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="green", bold=False, nl=False) + out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="blue", bold=False) + + @classmethod + def show(cls, code: str) -> None: + """Pretty-prints a given string of `code`. + + Convenience method for debugging. + """ + NOT_YET_IMPLEMENTED_StmtAnnAssign + list(v.visit(lib2to3_parse(code))) +``` + +## Black Output + +```py +@dataclass +class DebugVisitor(Visitor[T]): + tree_depth: int = 0 + + def visit_default(self, node: LN) -> Iterator[T]: + indent = ' ' * (2 * self.tree_depth) + if isinstance(node, Node): + _type = type_repr(node.type) + out(f'{indent}{_type}', fg='yellow') + self.tree_depth += 1 + for child in node.children: + yield from self.visit(child) + + self.tree_depth -= 1 + out(f'{indent}/{_type}', fg='yellow', bold=False) + else: + _type = token.tok_name.get(node.type, str(node.type)) + out(f'{indent}{_type}', fg='blue', nl=False) + if node.prefix: + # We don't have to handle prefixes for `Node` objects since + # that delegates to the first child anyway. + out(f' {node.prefix!r}', fg='green', bold=False, nl=False) + out(f' {node.value!r}', fg='blue', bold=False) + + @classmethod + def show(cls, code: str) -> None: + """Pretty-prints a given string of `code`. + + Convenience method for debugging. + """ + v: DebugVisitor[None] = DebugVisitor() + list(v.visit(lib2to3_parse(code))) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__decorators.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__decorators.py.snap new file mode 100644 index 0000000000..020bb3c340 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__decorators.py.snap @@ -0,0 +1,576 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py +--- +## Input + +```py +# This file doesn't use the standard decomposition. +# Decorator syntax test cases are separated by double # comments. +# Those before the 'output' comment are valid under the old syntax. +# Those after the 'ouput' comment require PEP614 relaxed syntax. +# Do not remove the double # separator before the first test case, it allows +# the comment before the test case to be ignored. + +## + +@decorator +def f(): + ... + +## + +@decorator() +def f(): + ... + +## + +@decorator(arg) +def f(): + ... + +## + +@decorator(kwarg=0) +def f(): + ... + +## + +@decorator(*args) +def f(): + ... + +## + +@decorator(**kwargs) +def f(): + ... + +## + +@decorator(*args, **kwargs) +def f(): + ... + +## + +@decorator(*args, **kwargs,) +def f(): + ... + +## + +@dotted.decorator +def f(): + ... + +## + +@dotted.decorator(arg) +def f(): + ... + +## + +@dotted.decorator(kwarg=0) +def f(): + ... + +## + +@dotted.decorator(*args) +def f(): + ... + +## + +@dotted.decorator(**kwargs) +def f(): + ... + +## + +@dotted.decorator(*args, **kwargs) +def f(): + ... + +## + +@dotted.decorator(*args, **kwargs,) +def f(): + ... + +## + +@double.dotted.decorator +def f(): + ... + +## + +@double.dotted.decorator(arg) +def f(): + ... + +## + +@double.dotted.decorator(kwarg=0) +def f(): + ... + +## + +@double.dotted.decorator(*args) +def f(): + ... + +## + +@double.dotted.decorator(**kwargs) +def f(): + ... + +## + +@double.dotted.decorator(*args, **kwargs) +def f(): + ... + +## + +@double.dotted.decorator(*args, **kwargs,) +def f(): + ... + +## + +@_(sequence["decorator"]) +def f(): + ... + +## + +@eval("sequence['decorator']") +def f(): + ... +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,29 +1,182 @@ ++# This file doesn't use the standard decomposition. ++# Decorator syntax test cases are separated by double # comments. ++# Those before the 'output' comment are valid under the old syntax. ++# Those after the 'ouput' comment require PEP614 relaxed syntax. ++# Do not remove the double # separator before the first test case, it allows ++# the comment before the test case to be ignored. ++ ++## ++ ++@decorator ++def f(): ++ ... ++ ++ ++## ++ ++@decorator() ++def f(): ++ ... ++ ++ ++## ++ ++@decorator(arg) ++def f(): ++ ... ++ ++ ++## ++ ++@decorator(kwarg=0) ++def f(): ++ ... ++ ++ ++## ++ ++@decorator(*NOT_YET_IMPLEMENTED_ExprStarred) ++def f(): ++ ... ++ ++ + ## + +-@decorator()() ++@decorator(**kwargs) + def f(): + ... + ++ + ## + +-@(decorator) ++@decorator(*NOT_YET_IMPLEMENTED_ExprStarred, **kwargs) + def f(): + ... + ++ + ## + +-@sequence["decorator"] ++@decorator( ++ *NOT_YET_IMPLEMENTED_ExprStarred, ++ **kwargs, ++) + def f(): + ... + ++ + ## + +-@decorator[List[str]] ++@dotted.decorator + def f(): + ... + ++ + ## + +-@var := decorator ++@dotted.decorator(arg) ++def f(): ++ ... ++ ++ ++## ++ ++@dotted.decorator(kwarg=0) ++def f(): ++ ... ++ ++ ++## ++ ++@dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred) ++def f(): ++ ... ++ ++ ++## ++ ++@dotted.decorator(**kwargs) ++def f(): ++ ... ++ ++ ++## ++ ++@dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred, **kwargs) ++def f(): ++ ... ++ ++ ++## ++ ++@dotted.decorator( ++ *NOT_YET_IMPLEMENTED_ExprStarred, ++ **kwargs, ++) ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator(arg) ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator(kwarg=0) ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred) ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator(**kwargs) ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred, **kwargs) ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator( ++ *NOT_YET_IMPLEMENTED_ExprStarred, ++ **kwargs, ++) ++def f(): ++ ... ++ ++ ++## ++ ++@_(sequence["decorator"]) ++def f(): ++ ... ++ ++ ++## ++ ++@eval("sequence['decorator']") + def f(): + ... +``` + +## Ruff Output + +```py +# This file doesn't use the standard decomposition. +# Decorator syntax test cases are separated by double # comments. +# Those before the 'output' comment are valid under the old syntax. +# Those after the 'ouput' comment require PEP614 relaxed syntax. +# Do not remove the double # separator before the first test case, it allows +# the comment before the test case to be ignored. + +## + +@decorator +def f(): + ... + + +## + +@decorator() +def f(): + ... + + +## + +@decorator(arg) +def f(): + ... + + +## + +@decorator(kwarg=0) +def f(): + ... + + +## + +@decorator(*NOT_YET_IMPLEMENTED_ExprStarred) +def f(): + ... + + +## + +@decorator(**kwargs) +def f(): + ... + + +## + +@decorator(*NOT_YET_IMPLEMENTED_ExprStarred, **kwargs) +def f(): + ... + + +## + +@decorator( + *NOT_YET_IMPLEMENTED_ExprStarred, + **kwargs, +) +def f(): + ... + + +## + +@dotted.decorator +def f(): + ... + + +## + +@dotted.decorator(arg) +def f(): + ... + + +## + +@dotted.decorator(kwarg=0) +def f(): + ... + + +## + +@dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred) +def f(): + ... + + +## + +@dotted.decorator(**kwargs) +def f(): + ... + + +## + +@dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred, **kwargs) +def f(): + ... + + +## + +@dotted.decorator( + *NOT_YET_IMPLEMENTED_ExprStarred, + **kwargs, +) +def f(): + ... + + +## + +@double.dotted.decorator +def f(): + ... + + +## + +@double.dotted.decorator(arg) +def f(): + ... + + +## + +@double.dotted.decorator(kwarg=0) +def f(): + ... + + +## + +@double.dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred) +def f(): + ... + + +## + +@double.dotted.decorator(**kwargs) +def f(): + ... + + +## + +@double.dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred, **kwargs) +def f(): + ... + + +## + +@double.dotted.decorator( + *NOT_YET_IMPLEMENTED_ExprStarred, + **kwargs, +) +def f(): + ... + + +## + +@_(sequence["decorator"]) +def f(): + ... + + +## + +@eval("sequence['decorator']") +def f(): + ... +``` + +## Black Output + +```py +## + +@decorator()() +def f(): + ... + +## + +@(decorator) +def f(): + ... + +## + +@sequence["decorator"] +def f(): + ... + +## + +@decorator[List[str]] +def f(): + ... + +## + +@var := decorator +def f(): + ... +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_no_string_normalization.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_no_string_normalization.py.snap new file mode 100644 index 0000000000..80c7ba60ec --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_no_string_normalization.py.snap @@ -0,0 +1,556 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py +--- +## Input + +```py +class ALonelyClass: + ''' + A multiline class docstring. + ''' + def AnEquallyLonelyMethod(self): + ''' + A multiline method docstring''' + pass + + +def one_function(): + '''This is a docstring with a single line of text.''' + pass + + +def shockingly_the_quotes_are_normalized(): + '''This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. + ''' + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not +make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it! + + """ + pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): ''' +"hey yah"''' + + +def shockingly_the_quotes_are_normalized_v2(): + ''' + Docstring Docstring Docstring + ''' + pass + + +def backslash_space(): + '\ ' + + +def multiline_backslash_1(): + ''' + hey\there\ + \ ''' + + +def multiline_backslash_2(): + ''' + hey there \ ''' + + +def multiline_backslash_3(): + ''' + already escaped \\ ''' +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,73 +1,75 @@ + class ALonelyClass: +- ''' ++ """ + A multiline class docstring. +- ''' ++ """ + + def AnEquallyLonelyMethod(self): +- ''' +- A multiline method docstring''' ++ """ ++ A multiline method docstring""" + pass + + + def one_function(): +- '''This is a docstring with a single line of text.''' ++ """This is a docstring with a single line of text.""" + pass + + + def shockingly_the_quotes_are_normalized(): +- '''This is a multiline docstring. ++ """This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. +- ''' ++ """ + pass + + + def foo(): +- """This is a docstring with +- some lines of text here +- """ ++ """This is a docstring with ++ some lines of text here ++ """ + return + + + def baz(): + '''"This" is a string with some +- embedded "quotes"''' ++ embedded "quotes"''' + return + + + def poit(): + """ +- Lorem ipsum dolor sit amet. ++ Lorem ipsum dolor sit amet. + +- Consectetur adipiscing elit: +- - sed do eiusmod tempor incididunt ut labore +- - dolore magna aliqua +- - enim ad minim veniam +- - quis nostrud exercitation ullamco laboris nisi +- - aliquip ex ea commodo consequat +- """ ++ Consectetur adipiscing elit: ++ - sed do eiusmod tempor incididunt ut labore ++ - dolore magna aliqua ++ - enim ad minim veniam ++ - quis nostrud exercitation ullamco laboris nisi ++ - aliquip ex ea commodo consequat ++ """ + pass + + + def under_indent(): + """ +- These lines are indented in a way that does not +- make sense. +- """ ++ These lines are indented in a way that does not ++make sense. ++ """ + pass + + + def over_indent(): + """ +- This has a shallow indent +- - But some lines are deeper +- - And the closing quote is too deep ++ This has a shallow indent ++ - But some lines are deeper ++ - And the closing quote is too deep + """ + pass + + + def single_line(): +- """But with a newline after it!""" ++ """But with a newline after it! ++ ++ """ + pass + + +@@ -83,41 +85,41 @@ + + def and_that(): + """ +- "hey yah" """ ++ "hey yah" """ + + + def and_this(): +- ''' +- "hey yah"''' ++ ''' ++ "hey yah"''' + + + def believe_it_or_not_this_is_in_the_py_stdlib(): +- ''' +- "hey yah"''' ++ ''' ++"hey yah"''' + + + def shockingly_the_quotes_are_normalized_v2(): +- ''' ++ """ + Docstring Docstring Docstring +- ''' ++ """ + pass + + + def backslash_space(): +- '\ ' ++ "\ " + + + def multiline_backslash_1(): +- ''' ++ """ + hey\there\ +- \ ''' ++ \ """ + + + def multiline_backslash_2(): +- ''' +- hey there \ ''' ++ """ ++ hey there \ """ + + + def multiline_backslash_3(): +- ''' +- already escaped \\''' ++ """ ++ already escaped \\ """ +``` + +## Ruff Output + +```py +class ALonelyClass: + """ + A multiline class docstring. + """ + + def AnEquallyLonelyMethod(self): + """ + A multiline method docstring""" + pass + + +def one_function(): + """This is a docstring with a single line of text.""" + pass + + +def shockingly_the_quotes_are_normalized(): + """This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. + """ + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not +make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it! + + """ + pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): + ''' +"hey yah"''' + + +def shockingly_the_quotes_are_normalized_v2(): + """ + Docstring Docstring Docstring + """ + pass + + +def backslash_space(): + "\ " + + +def multiline_backslash_1(): + """ + hey\there\ + \ """ + + +def multiline_backslash_2(): + """ + hey there \ """ + + +def multiline_backslash_3(): + """ + already escaped \\ """ +``` + +## Black Output + +```py +class ALonelyClass: + ''' + A multiline class docstring. + ''' + + def AnEquallyLonelyMethod(self): + ''' + A multiline method docstring''' + pass + + +def one_function(): + '''This is a docstring with a single line of text.''' + pass + + +def shockingly_the_quotes_are_normalized(): + '''This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. + ''' + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not + make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it!""" + pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): + ''' + "hey yah"''' + + +def shockingly_the_quotes_are_normalized_v2(): + ''' + Docstring Docstring Docstring + ''' + pass + + +def backslash_space(): + '\ ' + + +def multiline_backslash_1(): + ''' + hey\there\ + \ ''' + + +def multiline_backslash_2(): + ''' + hey there \ ''' + + +def multiline_backslash_3(): + ''' + already escaped \\''' +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_preview_no_string_normalization.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_preview_no_string_normalization.py.snap new file mode 100644 index 0000000000..52233c973b --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_preview_no_string_normalization.py.snap @@ -0,0 +1,68 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py +--- +## Input + +```py +def do_not_touch_this_prefix(): + R"""There was a bug where docstring prefixes would be normalized even with -S.""" + + +def do_not_touch_this_prefix2(): + FR'There was a bug where docstring prefixes would be normalized even with -S.' + + +def do_not_touch_this_prefix3(): + u'''There was a bug where docstring prefixes would be normalized even with -S.''' +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -3,8 +3,8 @@ + + + def do_not_touch_this_prefix2(): +- FR'There was a bug where docstring prefixes would be normalized even with -S.' ++ NOT_YET_IMPLEMENTED_ExprJoinedStr + + + def do_not_touch_this_prefix3(): +- u'''There was a bug where docstring prefixes would be normalized even with -S.''' ++ """There was a bug where docstring prefixes would be normalized even with -S.""" +``` + +## Ruff Output + +```py +def do_not_touch_this_prefix(): + R"""There was a bug where docstring prefixes would be normalized even with -S.""" + + +def do_not_touch_this_prefix2(): + NOT_YET_IMPLEMENTED_ExprJoinedStr + + +def do_not_touch_this_prefix3(): + """There was a bug where docstring prefixes would be normalized even with -S.""" +``` + +## Black Output + +```py +def do_not_touch_this_prefix(): + R"""There was a bug where docstring prefixes would be normalized even with -S.""" + + +def do_not_touch_this_prefix2(): + FR'There was a bug where docstring prefixes would be normalized even with -S.' + + +def do_not_touch_this_prefix3(): + u'''There was a bug where docstring prefixes would be normalized even with -S.''' +``` + + 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 new file mode 100644 index 0000000000..52fbac942f --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__force_pyi.py.snap @@ -0,0 +1,220 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py +--- +## Input + +```py +from typing import Union + +@bird +def zoo(): ... + +class A: ... +@bar +class B: + def BMethod(self) -> None: ... + @overload + def BMethod(self, arg : List[str]) -> None: ... + +class C: ... +@hmm +class D: ... +class E: ... + +@baz +def foo() -> None: + ... + +class F (A , C): ... +def spam() -> None: ... + +@overload +def spam(arg: str) -> str: ... + +var : int = 1 + +def eggs() -> Union[str, int]: ... +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,32 +1,58 @@ +-from typing import Union ++NOT_YET_IMPLEMENTED_StmtImportFrom ++ + + @bird +-def zoo(): ... ++def zoo(): ++ ... ++ ++ ++class A: ++ ... + +-class A: ... + + @bar + class B: +- def BMethod(self) -> None: ... ++ def BMethod(self) -> None: ++ ... ++ + @overload +- def BMethod(self, arg: List[str]) -> None: ... ++ def BMethod(self, arg: List[str]) -> None: ++ ... + +-class C: ... + ++class C: ++ ... ++ ++ + @hmm +-class D: ... ++class D: ++ ... ++ ++ ++class E: ++ ... + +-class E: ... + + @baz +-def foo() -> None: ... ++def foo() -> None: ++ ... ++ ++ ++class F(A, C): ++ ... + +-class F(A, C): ... + +-def spam() -> None: ... ++def spam() -> None: ++ ... ++ ++ + @overload +-def spam(arg: str) -> str: ... ++def spam(arg: str) -> str: ++ ... ++ ++ ++NOT_YET_IMPLEMENTED_StmtAnnAssign + +-var: int = 1 + +-def eggs() -> Union[str, int]: ... ++def eggs() -> Union[str, int]: ++ ... +``` + +## Ruff Output + +```py +NOT_YET_IMPLEMENTED_StmtImportFrom + + +@bird +def zoo(): + ... + + +class A: + ... + + +@bar +class B: + def BMethod(self) -> None: + ... + + @overload + def BMethod(self, arg: List[str]) -> None: + ... + + +class C: + ... + + +@hmm +class D: + ... + + +class E: + ... + + +@baz +def foo() -> None: + ... + + +class F(A, C): + ... + + +def spam() -> None: + ... + + +@overload +def spam(arg: str) -> str: + ... + + +NOT_YET_IMPLEMENTED_StmtAnnAssign + + +def eggs() -> Union[str, int]: + ... +``` + +## Black Output + +```py +from typing import Union + +@bird +def zoo(): ... + +class A: ... + +@bar +class B: + def BMethod(self) -> None: ... + @overload + def BMethod(self, arg: List[str]) -> None: ... + +class C: ... + +@hmm +class D: ... + +class E: ... + +@baz +def foo() -> None: ... + +class F(A, C): ... + +def spam() -> None: ... +@overload +def spam(arg: str) -> str: ... + +var: int = 1 + +def eggs() -> Union[str, int]: ... +``` + + 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 new file mode 100644 index 0000000000..e969e1e7a7 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__long_strings_flag_disabled.py.snap @@ -0,0 +1,1052 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py +--- +## Input + +```py +x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +y = "Short string" + +print( + "This is a really long string inside of a print statement with extra arguments attached at the end of it.", + x, + y, + z, +) + +print( + "This is a really long string inside of a print statement with no extra arguments attached at the end of it." +) + +D1 = { + "The First": "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + "The Second": "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D2 = { + 1.0: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + 2.0: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D3 = { + x: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + y: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D4 = { + "A long and ridiculous {}".format( + string_key + ): "This is a really really really long string that has to go i,side of a dictionary. It is soooo bad.", + some_func( + "calling", "some", "stuff" + ): "This is a really really really long string that has to go inside of a dictionary. It is {soooo} bad (#{x}).".format( + sooo="soooo", x=2 + ), + "A %s %s" + % ( + "formatted", + "string", + ): "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." + % ("soooo", 2), +} + +func_with_keywords( + my_arg, + my_kwarg="Long keyword strings also need to be wrapped, but they will probably need to be handled a little bit differently.", +) + +bad_split1 = ( + "But what should happen when code has already been formatted but in the wrong way? Like" + " with a space at the end instead of the beginning. Or what about when it is split too soon?" +) + +bad_split2 = ( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." +) + +bad_split3 = ( + "What if we have inline comments on " # First Comment + "each line of a bad split? In that " # Second Comment + "case, we should just leave it alone." # Third Comment +) + +bad_split_func1( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split.", + xxx, + yyy, + zzz, +) + +bad_split_func2( + xxx, + yyy, + zzz, + long_string_kwarg="But what should happen when code has already been formatted but in the wrong way? Like " + "with a space at the end instead of the beginning. Or what about when it is split too " + "soon?", +) + +bad_split_func3( + ( + "But what should happen when code has already " + r"been formatted but in the wrong way? Like " + "with a space at the end instead of the " + r"beginning. Or what about when it is split too " + r"soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." + ), + xxx, + yyy, + zzz, +) + +raw_string = r"This is a long raw string. When re-formatting this string, black needs to make sure it prepends the 'r' onto the new string." + +fmt_string1 = "We also need to be sure to preserve any and all {} which may or may not be attached to the string in question.".format( + "method calls" +) + +fmt_string2 = "But what about when the string is {} but {}".format( + "short", + "the method call is really really really really really really really really long?", +) + +old_fmt_string1 = ( + "While we are on the topic of %s, we should also note that old-style formatting must also be preserved, since some %s still uses it." + % ("formatting", "code") +) + +old_fmt_string2 = "This is a %s %s %s %s" % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", +) + +old_fmt_string3 = ( + "Whereas only the strings after the percent sign were long in the last example, this example uses a long initial string as well. This is another %s %s %s %s" + % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", + ) +) + +fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." + +fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." + +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. + +arg_comment_string = print( + "Long lines with inline comments which are apart of (and not the only member of) an argument list should have their comments appended to the reformatted string's enclosing left parentheses.", # This comment stays on the bottom. + "Arg #2", + "Arg #3", + "Arg #4", + "Arg #5", +) + +pragma_comment_string1 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa: E501 + +pragma_comment_string2 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa + +"""This is a really really really long triple quote string and it should not be touched.""" + +triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception." + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format( + "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string %s." + % "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic %s %s." + % ("string", "formatting") +) + +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." +) + +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. But then what happens when the final string is also supppppperrrrr long?! Well then that second (realllllllly long) string should be split too.", + "and a second argument", + and_a_third, +) + +return "A really really really really really really really really really really really really really long {} {}".format( + "return", "value" +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma 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 +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " 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 +) + +func_with_bad_parens_that_wont_fit_in_one_line( + ("short string that should have parens stripped"), x, y, z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, y, ("short string that should have parens stripped"), z +) + +func_with_bad_parens( + ("short string that should have parens stripped"), + x, + y, + z, +) + +func_with_bad_parens( + x, + y, + ("short string that should have parens stripped"), + z, +) + +annotated_variable: Final = ( + "This is a large " + + STRING + + " that has been " + + CONCATENATED + + "using the '+' operator." +) +annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: Literal[ + "fakse_literal" +] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." + +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" +backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\" + +short_string = "Hi" " there." + +func_call(short_string=("Hi" " there.")) + +raw_strings = r"Don't" " get" r" merged" " unless they are all raw." + + +def foo(): + yield "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + + +x = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four." + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # noqa + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # pylint: disable=some-pylint-check + " of it." +) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,6 +1,6 @@ + x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +-x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." ++NOT_YET_IMPLEMENTED_StmtAugAssign + + 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 + "each line of a bad split? In that " # Second Comment +- "case, we should just leave it alone." # Third Comment +-) ++ "case, we should just leave it alone." ++) # Third Comment + + bad_split_func1( + "But what should happen when code has already " +@@ -96,15 +96,13 @@ + ) + + bad_split_func3( +- ( +- "But what should happen when code has already " +- r"been formatted but in the wrong way? Like " +- "with a space at the end instead of the " +- r"beginning. Or what about when it is split too " +- r"soon? In the case of a split that is too " +- "short, black will try to honer the custom " +- "split." +- ), ++ "But what should happen when code has already " ++ r"been formatted but in the wrong way? Like " ++ "with a space at the end instead of the " ++ r"beginning. Or what about when it is split too " ++ r"soon? In the case of a split that is too " ++ "short, black will try to honer the custom " ++ "split.", + xxx, + yyy, + zzz, +@@ -143,9 +141,9 @@ + ) + ) + +-fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." ++fstring = NOT_YET_IMPLEMENTED_ExprJoinedStr + +-fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." ++fstring_with_no_fexprs = NOT_YET_IMPLEMENTED_ExprJoinedStr + + 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 @@ + + triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" + +-assert ( +- some_type_of_boolean_expression +-), "Followed by a really really really long string that is used to provide context to the AssertionError exception." ++NOT_YET_IMPLEMENTED_StmtAssert + +-assert ( +- some_type_of_boolean_expression +-), "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format( +- "formatting" +-) ++NOT_YET_IMPLEMENTED_StmtAssert + +-assert some_type_of_boolean_expression, ( +- "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string %s." +- % "formatting" +-) ++NOT_YET_IMPLEMENTED_StmtAssert + +-assert some_type_of_boolean_expression, ( +- "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic %s %s." +- % ("string", "formatting") +-) ++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( +@@ -212,29 +198,25 @@ + ) + + func_with_bad_comma( +- ( +- "This is a really long string argument to a function that has a trailing comma" +- " 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.", + ) + + 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 ++ "This is a really long string argument to a function that has a trailing comma" ++ " which should NOT be there.", # comment after comma + ) + + func_with_bad_parens_that_wont_fit_in_one_line( +- ("short string that should have parens stripped"), x, y, z ++ "short string that should have parens stripped", x, y, z + ) + + func_with_bad_parens_that_wont_fit_in_one_line( +- x, y, ("short string that should have parens stripped"), z ++ x, y, "short string that should have parens stripped", z + ) + + func_with_bad_parens( +- ("short string that should have parens stripped"), ++ "short string that should have parens stripped", + x, + y, + z, +@@ -243,21 +225,13 @@ + func_with_bad_parens( + x, + y, +- ("short string that should have parens stripped"), ++ "short string that should have parens stripped", + z, + ) + +-annotated_variable: Final = ( +- "This is a large " +- + STRING +- + " that has been " +- + CONCATENATED +- + "using the '+' operator." +-) +-annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +-annotated_variable: Literal[ +- "fakse_literal" +-] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." ++NOT_YET_IMPLEMENTED_StmtAnnAssign ++NOT_YET_IMPLEMENTED_StmtAnnAssign ++NOT_YET_IMPLEMENTED_StmtAnnAssign + + backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" + backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" +@@ -271,10 +245,10 @@ + + + def foo(): +- yield "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." ++ NOT_YET_IMPLEMENTED_ExprYield + + +-x = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four." ++x = NOT_YET_IMPLEMENTED_ExprJoinedStr + + long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore +``` + +## Ruff Output + +```py +x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +NOT_YET_IMPLEMENTED_StmtAugAssign + +y = "Short string" + +print( + "This is a really long string inside of a print statement with extra arguments attached at the end of it.", + x, + y, + z, +) + +print( + "This is a really long string inside of a print statement with no extra arguments attached at the end of it.", +) + +D1 = { + "The First": "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + "The Second": "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D2 = { + 1.0: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + 2.0: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D3 = { + x: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + y: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D4 = { + "A long and ridiculous {}".format( + string_key + ): "This is a really really really long string that has to go i,side of a dictionary. It is soooo bad.", + some_func( + "calling", "some", "stuff" + ): "This is a really really really long string that has to go inside of a dictionary. It is {soooo} bad (#{x}).".format( + sooo="soooo", x=2 + ), + "A %s %s" + % ( + "formatted", + "string", + ): "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." + % ("soooo", 2), +} + +func_with_keywords( + my_arg, + my_kwarg="Long keyword strings also need to be wrapped, but they will probably need to be handled a little bit differently.", +) + +bad_split1 = ( + "But what should happen when code has already been formatted but in the wrong way? Like" + " with a space at the end instead of the beginning. Or what about when it is split too soon?" +) + +bad_split2 = ( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." +) + +bad_split3 = ( + "What if we have inline comments on " # First Comment + "each line of a bad split? In that " # Second Comment + "case, we should just leave it alone." +) # Third Comment + +bad_split_func1( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split.", + xxx, + yyy, + zzz, +) + +bad_split_func2( + xxx, + yyy, + zzz, + long_string_kwarg="But what should happen when code has already been formatted but in the wrong way? Like " + "with a space at the end instead of the beginning. Or what about when it is split too " + "soon?", +) + +bad_split_func3( + "But what should happen when code has already " + r"been formatted but in the wrong way? Like " + "with a space at the end instead of the " + r"beginning. Or what about when it is split too " + r"soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split.", + xxx, + yyy, + zzz, +) + +raw_string = r"This is a long raw string. When re-formatting this string, black needs to make sure it prepends the 'r' onto the new string." + +fmt_string1 = "We also need to be sure to preserve any and all {} which may or may not be attached to the string in question.".format( + "method calls" +) + +fmt_string2 = "But what about when the string is {} but {}".format( + "short", + "the method call is really really really really really really really really long?", +) + +old_fmt_string1 = ( + "While we are on the topic of %s, we should also note that old-style formatting must also be preserved, since some %s still uses it." + % ("formatting", "code") +) + +old_fmt_string2 = "This is a %s %s %s %s" % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", +) + +old_fmt_string3 = ( + "Whereas only the strings after the percent sign were long in the last example, this example uses a long initial string as well. This is another %s %s %s %s" + % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", + ) +) + +fstring = NOT_YET_IMPLEMENTED_ExprJoinedStr + +fstring_with_no_fexprs = NOT_YET_IMPLEMENTED_ExprJoinedStr + +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. + +arg_comment_string = print( + "Long lines with inline comments which are apart of (and not the only member of) an argument list should have their comments appended to the reformatted string's enclosing left parentheses.", # This comment stays on the bottom. + "Arg #2", + "Arg #3", + "Arg #4", + "Arg #5", +) + +pragma_comment_string1 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa: E501 + +pragma_comment_string2 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa + +"""This is a really really really long triple quote string and it should not be touched.""" + +triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" + +NOT_YET_IMPLEMENTED_StmtAssert + +NOT_YET_IMPLEMENTED_StmtAssert + +NOT_YET_IMPLEMENTED_StmtAssert + +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.", +) + +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. But then what happens when the final string is also supppppperrrrr long?! Well then that second (realllllllly long) string should be split too.", + "and a second argument", + and_a_third, +) + +return "A really really really really really really really really really really really really really long {} {}".format( + "return", "value" +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma 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 +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma" + " 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 +) + +func_with_bad_parens_that_wont_fit_in_one_line( + "short string that should have parens stripped", x, y, z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, y, "short string that should have parens stripped", z +) + +func_with_bad_parens( + "short string that should have parens stripped", + x, + y, + z, +) + +func_with_bad_parens( + x, + y, + "short string that should have parens stripped", + z, +) + +NOT_YET_IMPLEMENTED_StmtAnnAssign +NOT_YET_IMPLEMENTED_StmtAnnAssign +NOT_YET_IMPLEMENTED_StmtAnnAssign + +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" +backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\" + +short_string = "Hi" " there." + +func_call(short_string=("Hi" " there.")) + +raw_strings = r"Don't" " get" r" merged" " unless they are all raw." + + +def foo(): + NOT_YET_IMPLEMENTED_ExprYield + + +x = NOT_YET_IMPLEMENTED_ExprJoinedStr + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # noqa + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # pylint: disable=some-pylint-check + " of it." +) +``` + +## Black Output + +```py +x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +y = "Short string" + +print( + "This is a really long string inside of a print statement with extra arguments attached at the end of it.", + x, + y, + z, +) + +print( + "This is a really long string inside of a print statement with no extra arguments attached at the end of it." +) + +D1 = { + "The First": "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + "The Second": "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D2 = { + 1.0: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + 2.0: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D3 = { + x: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + y: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D4 = { + "A long and ridiculous {}".format( + string_key + ): "This is a really really really long string that has to go i,side of a dictionary. It is soooo bad.", + some_func( + "calling", "some", "stuff" + ): "This is a really really really long string that has to go inside of a dictionary. It is {soooo} bad (#{x}).".format( + sooo="soooo", x=2 + ), + "A %s %s" + % ( + "formatted", + "string", + ): "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." + % ("soooo", 2), +} + +func_with_keywords( + my_arg, + my_kwarg="Long keyword strings also need to be wrapped, but they will probably need to be handled a little bit differently.", +) + +bad_split1 = ( + "But what should happen when code has already been formatted but in the wrong way? Like" + " with a space at the end instead of the beginning. Or what about when it is split too soon?" +) + +bad_split2 = ( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." +) + +bad_split3 = ( + "What if we have inline comments on " # First Comment + "each line of a bad split? In that " # Second Comment + "case, we should just leave it alone." # Third Comment +) + +bad_split_func1( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split.", + xxx, + yyy, + zzz, +) + +bad_split_func2( + xxx, + yyy, + zzz, + long_string_kwarg="But what should happen when code has already been formatted but in the wrong way? Like " + "with a space at the end instead of the beginning. Or what about when it is split too " + "soon?", +) + +bad_split_func3( + ( + "But what should happen when code has already " + r"been formatted but in the wrong way? Like " + "with a space at the end instead of the " + r"beginning. Or what about when it is split too " + r"soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." + ), + xxx, + yyy, + zzz, +) + +raw_string = r"This is a long raw string. When re-formatting this string, black needs to make sure it prepends the 'r' onto the new string." + +fmt_string1 = "We also need to be sure to preserve any and all {} which may or may not be attached to the string in question.".format( + "method calls" +) + +fmt_string2 = "But what about when the string is {} but {}".format( + "short", + "the method call is really really really really really really really really long?", +) + +old_fmt_string1 = ( + "While we are on the topic of %s, we should also note that old-style formatting must also be preserved, since some %s still uses it." + % ("formatting", "code") +) + +old_fmt_string2 = "This is a %s %s %s %s" % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", +) + +old_fmt_string3 = ( + "Whereas only the strings after the percent sign were long in the last example, this example uses a long initial string as well. This is another %s %s %s %s" + % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", + ) +) + +fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." + +fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." + +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. + +arg_comment_string = print( + "Long lines with inline comments which are apart of (and not the only member of) an argument list should have their comments appended to the reformatted string's enclosing left parentheses.", # This comment stays on the bottom. + "Arg #2", + "Arg #3", + "Arg #4", + "Arg #5", +) + +pragma_comment_string1 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa: E501 + +pragma_comment_string2 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa + +"""This is a really really really long triple quote string and it should not be touched.""" + +triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception." + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format( + "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string %s." + % "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic %s %s." + % ("string", "formatting") +) + +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." +) + +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. But then what happens when the final string is also supppppperrrrr long?! Well then that second (realllllllly long) string should be split too.", + "and a second argument", + and_a_third, +) + +return "A really really really really really really really really really really really really really long {} {}".format( + "return", "value" +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma 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 +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " 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 +) + +func_with_bad_parens_that_wont_fit_in_one_line( + ("short string that should have parens stripped"), x, y, z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, y, ("short string that should have parens stripped"), z +) + +func_with_bad_parens( + ("short string that should have parens stripped"), + x, + y, + z, +) + +func_with_bad_parens( + x, + y, + ("short string that should have parens stripped"), + z, +) + +annotated_variable: Final = ( + "This is a large " + + STRING + + " that has been " + + CONCATENATED + + "using the '+' operator." +) +annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: Literal[ + "fakse_literal" +] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." + +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" +backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\" + +short_string = "Hi" " there." + +func_call(short_string=("Hi" " there.")) + +raw_strings = r"Don't" " get" r" merged" " unless they are all raw." + + +def foo(): + yield "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + + +x = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four." + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # noqa + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # pylint: disable=some-pylint-check + " of it." +) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__power_op_newline.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__power_op_newline.py.snap new file mode 100644 index 0000000000..991e030ac9 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__power_op_newline.py.snap @@ -0,0 +1,44 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py +--- +## Input + +```py +importA;()<<0**0# +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,6 +1,2 @@ + importA +-( +- () +- << 0 +- ** 0 +-) # ++() << 0**0 # +``` + +## Ruff Output + +```py +importA +() << 0**0 # +``` + +## Black Output + +```py +importA +( + () + << 0 + ** 0 +) # +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__string_quotes.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__string_quotes.py.snap new file mode 100644 index 0000000000..9fa5227c0a --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__string_quotes.py.snap @@ -0,0 +1,244 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py +--- +## Input + +```py +'''''' +'\'' +'"' +"'" +"\"" +"Hello" +"Don't do that" +'Here is a "' +'What\'s the deal here?' +"What's the deal \"here\"?" +"And \"here\"?" +"""Strings with "" in them""" +'''Strings with "" in them''' +'''Here's a "''' +'''Here's a " ''' +'''Just a normal triple +quote''' +f"just a normal {f} string" +f'''This is a triple-quoted {f}-string''' +f'MOAR {" ".join([])}' +f"MOAR {' '.join([])}" +r"raw string ftw" +r'Date d\'expiration:(.*)' +r'Tricky "quote' +r'Not-so-tricky \"quote' +rf'{yay}' +'\n\ +The \"quick\"\n\ +brown fox\n\ +jumps over\n\ +the \'lazy\' dog.\n\ +' +re.compile(r'[\\"]') +"x = ''; y = \"\"" +"x = '''; y = \"\"" +"x = ''''; y = \"\"" +"x = '' ''; y = \"\"" +"x = ''; y = \"\"\"" +"x = '''; y = \"\"\"\"" +"x = ''''; y = \"\"\"\"\"" +"x = '' ''; y = \"\"\"\"\"" +'unnecessary \"\"escaping' +"unnecessary \'\'escaping" +'\\""' +"\\''" +'Lots of \\\\\\\\\'quotes\'' +f'{y * " "} \'{z}\'' +f'{{y * " "}} \'{z}\'' +f'\'{z}\' {y * " "}' +f'{y * x} \'{z}\'' +'\'{z}\' {y * " "}' +'{y * x} \'{z}\'' + +# We must bail out if changing the quotes would introduce backslashes in f-string +# expressions. xref: https://github.com/psf/black/issues/2348 +f"\"{b}\"{' ' * (long-len(b)+1)}: \"{sts}\",\n" +f"\"{a}\"{'hello' * b}\"{c}\"" +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -15,16 +15,21 @@ + """Here's a " """ + """Just a normal triple + quote""" +-f"just a normal {f} string" +-f"""This is a triple-quoted {f}-string""" +-f'MOAR {" ".join([])}' +-f"MOAR {' '.join([])}" ++NOT_YET_IMPLEMENTED_ExprJoinedStr ++NOT_YET_IMPLEMENTED_ExprJoinedStr ++NOT_YET_IMPLEMENTED_ExprJoinedStr ++NOT_YET_IMPLEMENTED_ExprJoinedStr + r"raw string ftw" +-r"Date d\'expiration:(.*)" ++r"Date d'expiration:(.*)" + r'Tricky "quote' +-r"Not-so-tricky \"quote" +-rf"{yay}" +-"\nThe \"quick\"\nbrown fox\njumps over\nthe 'lazy' dog.\n" ++r'Not-so-tricky "quote' ++NOT_YET_IMPLEMENTED_ExprJoinedStr ++"\n\ ++The \"quick\"\n\ ++brown fox\n\ ++jumps over\n\ ++the 'lazy' dog.\n\ ++" + re.compile(r'[\\"]') + "x = ''; y = \"\"" + "x = '''; y = \"\"" +@@ -39,14 +44,14 @@ + '\\""' + "\\''" + "Lots of \\\\\\\\'quotes'" +-f'{y * " "} \'{z}\'' +-f"{{y * \" \"}} '{z}'" +-f'\'{z}\' {y * " "}' +-f"{y * x} '{z}'" ++NOT_YET_IMPLEMENTED_ExprJoinedStr ++NOT_YET_IMPLEMENTED_ExprJoinedStr ++NOT_YET_IMPLEMENTED_ExprJoinedStr ++NOT_YET_IMPLEMENTED_ExprJoinedStr + "'{z}' {y * \" \"}" + "{y * x} '{z}'" + + # We must bail out if changing the quotes would introduce backslashes in f-string + # expressions. xref: https://github.com/psf/black/issues/2348 +-f"\"{b}\"{' ' * (long-len(b)+1)}: \"{sts}\",\n" +-f"\"{a}\"{'hello' * b}\"{c}\"" ++NOT_YET_IMPLEMENTED_ExprJoinedStr ++NOT_YET_IMPLEMENTED_ExprJoinedStr +``` + +## Ruff Output + +```py +"""""" +"'" +'"' +"'" +'"' +"Hello" +"Don't do that" +'Here is a "' +"What's the deal here?" +'What\'s the deal "here"?' +'And "here"?' +"""Strings with "" in them""" +"""Strings with "" in them""" +'''Here's a "''' +"""Here's a " """ +"""Just a normal triple +quote""" +NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr +r"raw string ftw" +r"Date d'expiration:(.*)" +r'Tricky "quote' +r'Not-so-tricky "quote' +NOT_YET_IMPLEMENTED_ExprJoinedStr +"\n\ +The \"quick\"\n\ +brown fox\n\ +jumps over\n\ +the 'lazy' dog.\n\ +" +re.compile(r'[\\"]') +"x = ''; y = \"\"" +"x = '''; y = \"\"" +"x = ''''; y = \"\"" +"x = '' ''; y = \"\"" +'x = \'\'; y = """' +'x = \'\'\'; y = """"' +'x = \'\'\'\'; y = """""' +'x = \'\' \'\'; y = """""' +'unnecessary ""escaping' +"unnecessary ''escaping" +'\\""' +"\\''" +"Lots of \\\\\\\\'quotes'" +NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr +"'{z}' {y * \" \"}" +"{y * x} '{z}'" + +# We must bail out if changing the quotes would introduce backslashes in f-string +# expressions. xref: https://github.com/psf/black/issues/2348 +NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr +``` + +## Black Output + +```py +"""""" +"'" +'"' +"'" +'"' +"Hello" +"Don't do that" +'Here is a "' +"What's the deal here?" +'What\'s the deal "here"?' +'And "here"?' +"""Strings with "" in them""" +"""Strings with "" in them""" +'''Here's a "''' +"""Here's a " """ +"""Just a normal triple +quote""" +f"just a normal {f} string" +f"""This is a triple-quoted {f}-string""" +f'MOAR {" ".join([])}' +f"MOAR {' '.join([])}" +r"raw string ftw" +r"Date d\'expiration:(.*)" +r'Tricky "quote' +r"Not-so-tricky \"quote" +rf"{yay}" +"\nThe \"quick\"\nbrown fox\njumps over\nthe 'lazy' dog.\n" +re.compile(r'[\\"]') +"x = ''; y = \"\"" +"x = '''; y = \"\"" +"x = ''''; y = \"\"" +"x = '' ''; y = \"\"" +'x = \'\'; y = """' +'x = \'\'\'; y = """"' +'x = \'\'\'\'; y = """""' +'x = \'\' \'\'; y = """""' +'unnecessary ""escaping' +"unnecessary ''escaping" +'\\""' +"\\''" +"Lots of \\\\\\\\'quotes'" +f'{y * " "} \'{z}\'' +f"{{y * \" \"}} '{z}'" +f'\'{z}\' {y * " "}' +f"{y * x} '{z}'" +"'{z}' {y * \" \"}" +"{y * x} '{z}'" + +# We must bail out if changing the quotes would introduce backslashes in f-string +# expressions. xref: https://github.com/psf/black/issues/2348 +f"\"{b}\"{' ' * (long-len(b)+1)}: \"{sts}\",\n" +f"\"{a}\"{'hello' * b}\"{c}\"" +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_complex.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_complex.py.snap new file mode 100644 index 0000000000..ce5a833fd2 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_complex.py.snap @@ -0,0 +1,549 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py +--- +## Input + +```py +# Cases sampled from Lib/test/test_patma.py + +# case black_test_patma_098 +match x: + case -0j: + y = 0 +# case black_test_patma_142 +match x: + case bytes(z): + y = 0 +# case black_test_patma_073 +match x: + case 0 if 0: + y = 0 + case 0 if 1: + y = 1 +# case black_test_patma_006 +match 3: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_049 +match x: + case [0, 1] | [1, 0]: + y = 0 +# case black_check_sequence_then_mapping +match x: + case [*_]: + return "seq" + case {}: + return "map" +# case black_test_patma_035 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | True} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 +# case black_test_patma_107 +match x: + case 0.25 + 1.75j: + y = 0 +# case black_test_patma_097 +match x: + case -0j: + y = 0 +# case black_test_patma_007 +match 4: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_154 +match x: + case 0 if x: + y = 0 +# case black_test_patma_134 +match x: + case {1: 0}: + y = 0 + case {0: 0}: + y = 1 + case {**z}: + y = 2 +# case black_test_patma_185 +match Seq(): + case [*_]: + y = 0 +# case black_test_patma_063 +match x: + case 1: + y = 0 + case 1: + y = 1 +# case black_test_patma_248 +match x: + case {"foo": bar}: + y = bar +# case black_test_patma_019 +match (0, 1, 2): + case [0, 1, *x, 2]: + y = 0 +# case black_test_patma_052 +match x: + case [0]: + y = 0 + case [1, 0] if (x := x[:0]): + y = 1 + case [1, 0]: + y = 2 +# case black_test_patma_191 +match w: + case [x, y, *_]: + z = 0 +# case black_test_patma_110 +match x: + case -0.25 - 1.75j: + y = 0 +# case black_test_patma_151 +match (x,): + case [y]: + z = 0 +# case black_test_patma_114 +match x: + case A.B.C.D: + y = 0 +# case black_test_patma_232 +match x: + case None: + y = 0 +# case black_test_patma_058 +match x: + case 0: + y = 0 +# case black_test_patma_233 +match x: + case False: + y = 0 +# case black_test_patma_078 +match x: + case []: + y = 0 + case [""]: + y = 1 + case "": + y = 2 +# case black_test_patma_156 +match x: + case z: + y = 0 +# case black_test_patma_189 +match w: + case [x, y, *rest]: + z = 0 +# case black_test_patma_042 +match x: + case (0 as z) | (1 as z) | (2 as z) if z == x % 2: + y = 0 +# case black_test_patma_034 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | False} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,144 +1,60 @@ + # Cases sampled from Lib/test/test_patma.py + + # case black_test_patma_098 +-match x: +- case -0j: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_142 +-match x: +- case bytes(z): +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_073 +-match x: +- case 0 if 0: +- y = 0 +- case 0 if 1: +- y = 1 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_006 +-match 3: +- case 0 | 1 | 2 | 3: +- x = True ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_049 +-match x: +- case [0, 1] | [1, 0]: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_check_sequence_then_mapping +-match x: +- case [*_]: +- return "seq" +- case {}: +- return "map" ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_035 +-match x: +- case {0: [1, 2, {}]}: +- y = 0 +- case {0: [1, 2, {}] | True} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: +- y = 1 +- case []: +- y = 2 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_107 +-match x: +- case 0.25 + 1.75j: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_097 +-match x: +- case -0j: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_007 +-match 4: +- case 0 | 1 | 2 | 3: +- x = True ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_154 +-match x: +- case 0 if x: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_134 +-match x: +- case {1: 0}: +- y = 0 +- case {0: 0}: +- y = 1 +- case {**z}: +- y = 2 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_185 +-match Seq(): +- case [*_]: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_063 +-match x: +- case 1: +- y = 0 +- case 1: +- y = 1 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_248 +-match x: +- case {"foo": bar}: +- y = bar ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_019 +-match (0, 1, 2): +- case [0, 1, *x, 2]: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_052 +-match x: +- case [0]: +- y = 0 +- case [1, 0] if (x := x[:0]): +- y = 1 +- case [1, 0]: +- y = 2 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_191 +-match w: +- case [x, y, *_]: +- z = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_110 +-match x: +- case -0.25 - 1.75j: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_151 +-match (x,): +- case [y]: +- z = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_114 +-match x: +- case A.B.C.D: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_232 +-match x: +- case None: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_058 +-match x: +- case 0: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_233 +-match x: +- case False: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_078 +-match x: +- case []: +- y = 0 +- case [""]: +- y = 1 +- case "": +- y = 2 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_156 +-match x: +- case z: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_189 +-match w: +- case [x, y, *rest]: +- z = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_042 +-match x: +- case (0 as z) | (1 as z) | (2 as z) if z == x % 2: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_034 +-match x: +- case {0: [1, 2, {}]}: +- y = 0 +- case {0: [1, 2, {}] | False} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: +- y = 1 +- case []: +- y = 2 ++NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Ruff Output + +```py +# Cases sampled from Lib/test/test_patma.py + +# case black_test_patma_098 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_142 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_073 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_006 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_049 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_check_sequence_then_mapping +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_035 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_107 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_097 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_007 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_154 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_134 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_185 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_063 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_248 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_019 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_052 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_191 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_110 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_151 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_114 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_232 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_058 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_233 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_078 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_156 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_189 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_042 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_034 +NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Black Output + +```py +# Cases sampled from Lib/test/test_patma.py + +# case black_test_patma_098 +match x: + case -0j: + y = 0 +# case black_test_patma_142 +match x: + case bytes(z): + y = 0 +# case black_test_patma_073 +match x: + case 0 if 0: + y = 0 + case 0 if 1: + y = 1 +# case black_test_patma_006 +match 3: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_049 +match x: + case [0, 1] | [1, 0]: + y = 0 +# case black_check_sequence_then_mapping +match x: + case [*_]: + return "seq" + case {}: + return "map" +# case black_test_patma_035 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | True} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 +# case black_test_patma_107 +match x: + case 0.25 + 1.75j: + y = 0 +# case black_test_patma_097 +match x: + case -0j: + y = 0 +# case black_test_patma_007 +match 4: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_154 +match x: + case 0 if x: + y = 0 +# case black_test_patma_134 +match x: + case {1: 0}: + y = 0 + case {0: 0}: + y = 1 + case {**z}: + y = 2 +# case black_test_patma_185 +match Seq(): + case [*_]: + y = 0 +# case black_test_patma_063 +match x: + case 1: + y = 0 + case 1: + y = 1 +# case black_test_patma_248 +match x: + case {"foo": bar}: + y = bar +# case black_test_patma_019 +match (0, 1, 2): + case [0, 1, *x, 2]: + y = 0 +# case black_test_patma_052 +match x: + case [0]: + y = 0 + case [1, 0] if (x := x[:0]): + y = 1 + case [1, 0]: + y = 2 +# case black_test_patma_191 +match w: + case [x, y, *_]: + z = 0 +# case black_test_patma_110 +match x: + case -0.25 - 1.75j: + y = 0 +# case black_test_patma_151 +match (x,): + case [y]: + z = 0 +# case black_test_patma_114 +match x: + case A.B.C.D: + y = 0 +# case black_test_patma_232 +match x: + case None: + y = 0 +# case black_test_patma_058 +match x: + case 0: + y = 0 +# case black_test_patma_233 +match x: + case False: + y = 0 +# case black_test_patma_078 +match x: + case []: + y = 0 + case [""]: + y = 1 + case "": + y = 2 +# case black_test_patma_156 +match x: + case z: + y = 0 +# case black_test_patma_189 +match w: + case [x, y, *rest]: + z = 0 +# case black_test_patma_042 +match x: + case (0 as z) | (1 as z) | (2 as z) if z == x % 2: + y = 0 +# case black_test_patma_034 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | False} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 +``` + + 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 new file mode 100644 index 0000000000..9afb2c247d --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_extras.py.snap @@ -0,0 +1,443 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py +--- +## Input + +```py +import match + +match something: + case [a as b]: + print(b) + case [a as b, c, d, e as f]: + print(f) + case Point(a as b): + print(b) + case Point(int() as x, int() as y): + print(x, y) + + +match = 1 +case: int = re.match(something) + +match re.match(case): + case type("match", match): + pass + case match: + pass + + +def func(match: case, case: match) -> case: + match Something(): + case func(match, case): + ... + case another: + ... + + +match maybe, multiple: + case perhaps, 5: + pass + case perhaps, 6,: + pass + + +match more := (than, one), indeed,: + case _, (5, 6): + pass + case [[5], (6)], [7],: + pass + case _: + pass + + +match a, *b, c: + case [*_]: + assert "seq" == _ + case {}: + assert "map" == b + + +match match( + case, + match( + match, case, match, looooooooooooooooooooooooooooooooooooong, match, case, match + ), + case, +): + case case( + match=case, + case=re.match( + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong + ), + ): + pass + + case [a as match]: + pass + + case case: + pass + + +match match: + case case: + pass + + +match a, *b(), c: + case d, *f, g: + pass + + +match something: + case { + "key": key as key_1, + "password": PASS.ONE | PASS.TWO | PASS.THREE as password, + }: + pass + case {"maybe": something(complicated as this) as that}: + pass + + +match something: + case 1 as a: + pass + + case 2 as b, 3 as c: + pass + + case 4 as d, (5 as e), (6 | 7 as g), *h: + pass + + +match bar1: + case Foo(aa=Callable() as aa, bb=int()): + print(bar1.aa, bar1.bb) + case _: + print("no match", "\n") + + +match bar1: + case Foo( + normal=x, perhaps=[list, {"x": d, "y": 1.0}] as y, otherwise=something, q=t as u + ): + pass +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,119 +1,43 @@ +-import match ++NOT_YET_IMPLEMENTED_StmtImport + +-match something: +- case [a as b]: +- print(b) +- case [a as b, c, d, e as f]: +- print(f) +- case Point(a as b): +- print(b) +- case Point(int() as x, int() as y): +- print(x, y) ++NOT_YET_IMPLEMENTED_StmtMatch + + + match = 1 +-case: int = re.match(something) ++NOT_YET_IMPLEMENTED_StmtAnnAssign + +-match re.match(case): +- case type("match", match): +- pass +- case match: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + + + def func(match: case, case: match) -> case: +- match Something(): +- case func(match, case): +- ... +- case another: +- ... ++ NOT_YET_IMPLEMENTED_StmtMatch + + +-match maybe, multiple: +- case perhaps, 5: +- pass +- case perhaps, 6,: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + + +-match more := (than, one), indeed,: +- case _, (5, 6): +- pass +- case [[5], (6)], [7],: +- pass +- case _: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + + +-match a, *b, c: +- case [*_]: +- assert "seq" == _ +- case {}: +- assert "map" == b ++NOT_YET_IMPLEMENTED_StmtMatch + + +-match match( +- case, +- match( +- match, case, match, looooooooooooooooooooooooooooooooooooong, match, case, match +- ), +- case, +-): +- case case( +- match=case, +- case=re.match( +- loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong +- ), +- ): +- pass +- +- case [a as match]: +- pass +- +- case case: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + + +-match match: +- case case: +- pass +- +- +-match a, *b(), c: +- case d, *f, g: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + + +-match something: +- case { +- "key": key as key_1, +- "password": PASS.ONE | PASS.TWO | PASS.THREE as password, +- }: +- 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 + +- 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 +- ): +- pass ++NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Ruff Output + +```py +NOT_YET_IMPLEMENTED_StmtImport + +NOT_YET_IMPLEMENTED_StmtMatch + + +match = 1 +NOT_YET_IMPLEMENTED_StmtAnnAssign + +NOT_YET_IMPLEMENTED_StmtMatch + + +def func(match: case, case: match) -> case: + NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Black Output + +```py +import match + +match something: + case [a as b]: + print(b) + case [a as b, c, d, e as f]: + print(f) + case Point(a as b): + print(b) + case Point(int() as x, int() as y): + print(x, y) + + +match = 1 +case: int = re.match(something) + +match re.match(case): + case type("match", match): + pass + case match: + pass + + +def func(match: case, case: match) -> case: + match Something(): + case func(match, case): + ... + case another: + ... + + +match maybe, multiple: + case perhaps, 5: + pass + case perhaps, 6,: + pass + + +match more := (than, one), indeed,: + case _, (5, 6): + pass + case [[5], (6)], [7],: + pass + case _: + pass + + +match a, *b, c: + case [*_]: + assert "seq" == _ + case {}: + assert "map" == b + + +match match( + case, + match( + match, case, match, looooooooooooooooooooooooooooooooooooong, match, case, match + ), + case, +): + case case( + match=case, + case=re.match( + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong + ), + ): + pass + + case [a as match]: + pass + + case case: + pass + + +match match: + case case: + pass + + +match a, *b(), c: + case d, *f, g: + pass + + +match something: + case { + "key": key as key_1, + "password": PASS.ONE | PASS.TWO | PASS.THREE as password, + }: + pass + case {"maybe": something(complicated as this) as that}: + pass + + +match something: + case 1 as a: + pass + + case 2 as b, 3 as c: + pass + + case 4 as d, (5 as e), (6 | 7 as g), *h: + pass + + +match bar1: + case Foo(aa=Callable() as aa, bb=int()): + print(bar1.aa, bar1.bb) + case _: + print("no match", "\n") + + +match bar1: + case Foo( + normal=x, perhaps=[list, {"x": d, "y": 1.0}] as y, otherwise=something, q=t as u + ): + pass +``` + + 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 new file mode 100644 index 0000000000..e1aef4ad1b --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_generic.py.snap @@ -0,0 +1,425 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py +--- +## Input + +```py +re.match() +match = a +with match() as match: + match = f"{match}" + +re.match() +match = a +with match() as match: + match = f"{match}" + + +def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: + if not target_versions: + # No target_version specified, so try all grammars. + return [ + # Python 3.7+ + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords, + # Python 3.0-3.6 + pygram.python_grammar_no_print_statement_no_exec_statement, + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + match match: + case case: + match match: + case case: + pass + + if all(version.is_python2() for version in target_versions): + # Python 2-only code, so try Python 2 grammars. + return [ + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + re.match() + match = a + with match() as match: + match = f"{match}" + + def test_patma_139(self): + x = False + match x: + case bool(z): + y = 0 + self.assertIs(x, False) + self.assertEqual(y, 0) + self.assertIs(z, x) + + # Python 3-compatible code, so only try Python 3 grammar. + grammars = [] + if supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.10+ + grammars.append(pygram.python_grammar_soft_keywords) + # If we have to parse both, try to parse async as a keyword first + if not supports_feature( + target_versions, Feature.ASYNC_IDENTIFIERS + ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.7-3.9 + grammars.append( + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords + ) + if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS): + # Python 3.0-3.6 + grammars.append(pygram.python_grammar_no_print_statement_no_exec_statement) + + def test_patma_155(self): + x = 0 + y = None + match x: + case 1e1000: + y = 0 + self.assertEqual(x, 0) + self.assertIs(y, None) + + x = range(3) + match x: + case [y, case as x, z]: + w = 0 + + # At least one of the above branches must have been taken, because every Python + # version has exactly one of the two 'ASYNC_*' flags + return grammars + + +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"): + src_txt += "\n" + + grammars = get_grammars(set(target_versions)) + + +re.match() +match = a +with match() as match: + match = f"{match}" + +re.match() +match = a +with match() as match: + match = f"{match}" +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,12 +1,12 @@ + re.match() + match = a + with match() as match: +- match = f"{match}" ++ match = NOT_YET_IMPLEMENTED_ExprJoinedStr + + re.match() + match = a + with match() as match: +- match = f"{match}" ++ match = NOT_YET_IMPLEMENTED_ExprJoinedStr + + + def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: +@@ -23,13 +23,9 @@ + pygram.python_grammar, + ] + +- match match: +- case case: +- match match: +- case case: +- pass ++ NOT_YET_IMPLEMENTED_StmtMatch + +- if all(version.is_python2() for version in target_versions): ++ if all((i for i in [])): + # Python 2-only code, so try Python 2 grammars. + return [ + # Python 2.7 with future print_function import +@@ -41,13 +37,11 @@ + re.match() + match = a + with match() as match: +- match = f"{match}" ++ match = NOT_YET_IMPLEMENTED_ExprJoinedStr + + def test_patma_139(self): + x = False +- match x: +- case bool(z): +- y = 0 ++ NOT_YET_IMPLEMENTED_StmtMatch + self.assertIs(x, False) + self.assertEqual(y, 0) + self.assertIs(z, x) +@@ -72,16 +66,12 @@ + def test_patma_155(self): + x = 0 + y = None +- match x: +- case 1e1000: +- y = 0 ++ NOT_YET_IMPLEMENTED_StmtMatch + self.assertEqual(x, 0) + self.assertIs(y, None) + + x = range(3) +- match x: +- case [y, case as x, z]: +- w = 0 ++ NOT_YET_IMPLEMENTED_StmtMatch + + # 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 @@ + 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"): +- src_txt += "\n" ++ NOT_YET_IMPLEMENTED_StmtAugAssign + + grammars = get_grammars(set(target_versions)) + +@@ -99,9 +89,9 @@ + re.match() + match = a + with match() as match: +- match = f"{match}" ++ match = NOT_YET_IMPLEMENTED_ExprJoinedStr + + re.match() + match = a + with match() as match: +- match = f"{match}" ++ match = NOT_YET_IMPLEMENTED_ExprJoinedStr +``` + +## Ruff Output + +```py +re.match() +match = a +with match() as match: + match = NOT_YET_IMPLEMENTED_ExprJoinedStr + +re.match() +match = a +with match() as match: + match = NOT_YET_IMPLEMENTED_ExprJoinedStr + + +def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: + if not target_versions: + # No target_version specified, so try all grammars. + return [ + # Python 3.7+ + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords, + # Python 3.0-3.6 + pygram.python_grammar_no_print_statement_no_exec_statement, + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + NOT_YET_IMPLEMENTED_StmtMatch + + if all((i for i in [])): + # Python 2-only code, so try Python 2 grammars. + return [ + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + re.match() + match = a + with match() as match: + match = NOT_YET_IMPLEMENTED_ExprJoinedStr + + def test_patma_139(self): + x = False + NOT_YET_IMPLEMENTED_StmtMatch + self.assertIs(x, False) + self.assertEqual(y, 0) + self.assertIs(z, x) + + # Python 3-compatible code, so only try Python 3 grammar. + grammars = [] + if supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.10+ + grammars.append(pygram.python_grammar_soft_keywords) + # If we have to parse both, try to parse async as a keyword first + if not supports_feature( + target_versions, Feature.ASYNC_IDENTIFIERS + ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.7-3.9 + grammars.append( + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords + ) + if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS): + # Python 3.0-3.6 + grammars.append(pygram.python_grammar_no_print_statement_no_exec_statement) + + def test_patma_155(self): + x = 0 + y = None + NOT_YET_IMPLEMENTED_StmtMatch + self.assertEqual(x, 0) + self.assertIs(y, None) + + x = range(3) + NOT_YET_IMPLEMENTED_StmtMatch + + # At least one of the above branches must have been taken, because every Python + # version has exactly one of the two 'ASYNC_*' flags + return grammars + + +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"): + NOT_YET_IMPLEMENTED_StmtAugAssign + + grammars = get_grammars(set(target_versions)) + + +re.match() +match = a +with match() as match: + match = NOT_YET_IMPLEMENTED_ExprJoinedStr + +re.match() +match = a +with match() as match: + match = NOT_YET_IMPLEMENTED_ExprJoinedStr +``` + +## Black Output + +```py +re.match() +match = a +with match() as match: + match = f"{match}" + +re.match() +match = a +with match() as match: + match = f"{match}" + + +def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: + if not target_versions: + # No target_version specified, so try all grammars. + return [ + # Python 3.7+ + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords, + # Python 3.0-3.6 + pygram.python_grammar_no_print_statement_no_exec_statement, + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + match match: + case case: + match match: + case case: + pass + + if all(version.is_python2() for version in target_versions): + # Python 2-only code, so try Python 2 grammars. + return [ + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + re.match() + match = a + with match() as match: + match = f"{match}" + + def test_patma_139(self): + x = False + match x: + case bool(z): + y = 0 + self.assertIs(x, False) + self.assertEqual(y, 0) + self.assertIs(z, x) + + # Python 3-compatible code, so only try Python 3 grammar. + grammars = [] + if supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.10+ + grammars.append(pygram.python_grammar_soft_keywords) + # If we have to parse both, try to parse async as a keyword first + if not supports_feature( + target_versions, Feature.ASYNC_IDENTIFIERS + ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.7-3.9 + grammars.append( + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords + ) + if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS): + # Python 3.0-3.6 + grammars.append(pygram.python_grammar_no_print_statement_no_exec_statement) + + def test_patma_155(self): + x = 0 + y = None + match x: + case 1e1000: + y = 0 + self.assertEqual(x, 0) + self.assertIs(y, None) + + x = range(3) + match x: + case [y, case as x, z]: + w = 0 + + # At least one of the above branches must have been taken, because every Python + # version has exactly one of the two 'ASYNC_*' flags + return grammars + + +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"): + src_txt += "\n" + + grammars = get_grammars(set(target_versions)) + + +re.match() +match = a +with match() as match: + match = f"{match}" + +re.match() +match = a +with match() as match: + match = f"{match}" +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_simple.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_simple.py.snap new file mode 100644 index 0000000000..a165da5f84 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_simple.py.snap @@ -0,0 +1,343 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py +--- +## Input + +```py +# Cases sampled from PEP 636 examples + +match command.split(): + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case [action]: + ... # interpret single-verb action + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case ["quit"]: + print("Goodbye!") + quit_game() + case ["look"]: + current_room.describe() + case ["get", obj]: + character.get(obj, current_room) + case ["go", direction]: + current_room = current_room.neighbor(direction) + # The rest of your commands go here + +match command.split(): + case ["drop", *objects]: + for obj in objects: + character.drop(obj, current_room) + # The rest of your commands go here + +match command.split(): + case ["quit"]: + pass + case ["go", direction]: + print("Going:", direction) + case ["drop", *objects]: + print("Dropping: ", *objects) + case _: + print(f"Sorry, I couldn't understand {command!r}") + +match command.split(): + case ["north"] | ["go", "north"]: + current_room = current_room.neighbor("north") + case ["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"]: + ... # Code for picking up the given object + +match command.split(): + case ["go", ("north" | "south" | "east" | "west")]: + current_room = current_room.neighbor(...) + # how do I know which direction to go? + +match command.split(): + case ["go", ("north" | "south" | "east" | "west") as direction]: + current_room = current_room.neighbor(direction) + +match command.split(): + case ["go", direction] if direction in current_room.exits: + current_room = current_room.neighbor(direction) + case ["go", _]: + print("Sorry, you can't go that way") + +match event.get(): + case Click(position=(x, y)): + handle_click_at(x, y) + case KeyPress(key_name="Q") | Quit(): + game.quit() + case KeyPress(key_name="up arrow"): + game.go_north() + case KeyPress(): + pass # Ignore other keystrokes + case other_event: + raise ValueError(f"Unrecognized event: {other_event}") + +match event.get(): + case Click((x, y), button=Button.LEFT): # This is a left click + handle_click_at(x, y) + case Click(): + pass # ignore other clicks + + +def where_is(point): + 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") +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,92 +1,27 @@ + # Cases sampled from PEP 636 examples + +-match command.split(): +- case [action, obj]: +- ... # interpret action, obj ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case [action]: +- ... # interpret single-verb action +- case [action, obj]: +- ... # interpret action, obj ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["quit"]: +- print("Goodbye!") +- quit_game() +- case ["look"]: +- current_room.describe() +- case ["get", obj]: +- character.get(obj, current_room) +- case ["go", direction]: +- current_room = current_room.neighbor(direction) +- # The rest of your commands go here ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["drop", *objects]: +- for obj in objects: +- character.drop(obj, current_room) +- # The rest of your commands go here ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["quit"]: +- pass +- case ["go", direction]: +- print("Going:", direction) +- case ["drop", *objects]: +- print("Dropping: ", *objects) +- case _: +- print(f"Sorry, I couldn't understand {command!r}") ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["north"] | ["go", "north"]: +- current_room = current_room.neighbor("north") +- case ["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"]: +- ... # Code for picking up the given object ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["go", ("north" | "south" | "east" | "west")]: +- current_room = current_room.neighbor(...) +- # how do I know which direction to go? ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["go", ("north" | "south" | "east" | "west") as direction]: +- current_room = current_room.neighbor(direction) ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["go", direction] if direction in current_room.exits: +- current_room = current_room.neighbor(direction) +- case ["go", _]: +- print("Sorry, you can't go that way") ++NOT_YET_IMPLEMENTED_StmtMatch + +-match event.get(): +- case Click(position=(x, y)): +- handle_click_at(x, y) +- case KeyPress(key_name="Q") | Quit(): +- game.quit() +- case KeyPress(key_name="up arrow"): +- game.go_north() +- case KeyPress(): +- pass # Ignore other keystrokes +- case other_event: +- raise ValueError(f"Unrecognized event: {other_event}") ++NOT_YET_IMPLEMENTED_StmtMatch + +-match event.get(): +- case Click((x, y), button=Button.LEFT): # This is a left click +- handle_click_at(x, y) +- case Click(): +- pass # ignore other clicks ++NOT_YET_IMPLEMENTED_StmtMatch + + + def where_is(point): +- 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") ++ NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Ruff Output + +```py +# Cases sampled from PEP 636 examples + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + + +def where_is(point): + NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Black Output + +```py +# Cases sampled from PEP 636 examples + +match command.split(): + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case [action]: + ... # interpret single-verb action + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case ["quit"]: + print("Goodbye!") + quit_game() + case ["look"]: + current_room.describe() + case ["get", obj]: + character.get(obj, current_room) + case ["go", direction]: + current_room = current_room.neighbor(direction) + # The rest of your commands go here + +match command.split(): + case ["drop", *objects]: + for obj in objects: + character.drop(obj, current_room) + # The rest of your commands go here + +match command.split(): + case ["quit"]: + pass + case ["go", direction]: + print("Going:", direction) + case ["drop", *objects]: + print("Dropping: ", *objects) + case _: + print(f"Sorry, I couldn't understand {command!r}") + +match command.split(): + case ["north"] | ["go", "north"]: + current_room = current_room.neighbor("north") + case ["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"]: + ... # Code for picking up the given object + +match command.split(): + case ["go", ("north" | "south" | "east" | "west")]: + current_room = current_room.neighbor(...) + # how do I know which direction to go? + +match command.split(): + case ["go", ("north" | "south" | "east" | "west") as direction]: + current_room = current_room.neighbor(direction) + +match command.split(): + case ["go", direction] if direction in current_room.exits: + current_room = current_room.neighbor(direction) + case ["go", _]: + print("Sorry, you can't go that way") + +match event.get(): + case Click(position=(x, y)): + handle_click_at(x, y) + case KeyPress(key_name="Q") | Quit(): + game.quit() + case KeyPress(key_name="up arrow"): + game.go_north() + case KeyPress(): + pass # Ignore other keystrokes + case other_event: + raise ValueError(f"Unrecognized event: {other_event}") + +match event.get(): + case Click((x, y), button=Button.LEFT): # This is a left click + handle_click_at(x, y) + case Click(): + pass # ignore other clicks + + +def where_is(point): + 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") +``` + + 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 new file mode 100644 index 0000000000..6a8135be42 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_style.py.snap @@ -0,0 +1,186 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py +--- +## Input + +```py +match something: + case b(): print(1+1) + case c( + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=- 1 + ): print(1) + case c( + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1, + ): print(2) + case a: pass + +match( + arg # comment +) + +match( +) + +match( + + +) + +case( + arg # comment +) + +case( +) + +case( + + +) + + +re.match( + something # fast +) +re.match( + + + +) +match match( + + +): + case case( + arg, # comment + ): + pass +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,35 +1,24 @@ +-match something: +- case b(): +- print(1 + 1) +- case c( +- very_complex=True, perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1 +- ): +- print(1) +- case c( +- very_complex=True, +- perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1, +- ): +- print(2) +- case a: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + +-match(arg) # comment ++match( ++ arg, # comment ++) + + match() + + match() + +-case(arg) # comment ++case( ++ arg, # comment ++) + + case() + + case() + + +-re.match(something) # fast ++re.match( ++ something, # fast ++) + re.match() +-match match(): +- case case( +- arg, # comment +- ): +- pass ++NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Ruff Output + +```py +NOT_YET_IMPLEMENTED_StmtMatch + +match( + arg, # comment +) + +match() + +match() + +case( + arg, # comment +) + +case() + +case() + + +re.match( + something, # fast +) +re.match() +NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Black Output + +```py +match something: + case b(): + print(1 + 1) + case c( + very_complex=True, perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1 + ): + print(1) + case c( + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1, + ): + print(2) + case a: + pass + +match(arg) # comment + +match() + +match() + +case(arg) # comment + +case() + +case() + + +re.match(something) # fast +re.match() +match match(): + case case( + arg, # comment + ): + pass +``` + + 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 new file mode 100644 index 0000000000..64f4277f8b --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pep_572_py310.py.snap @@ -0,0 +1,96 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py +--- +## Input + +```py +# Unparenthesized walruses are now allowed in indices since Python 3.10. +x[a:=0] +x[a:=0, b:=1] +x[5, b:=0] + +# Walruses are allowed inside generator expressions on function calls since 3.10. +if any(match := pattern_error.match(s) for s in buffer): + if match.group(2) == data_not_available: + # Error OK to ignore. + pass + +f(a := b + c for c in range(10)) +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) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,15 +1,15 @@ + # Unparenthesized walruses are now allowed in indices since Python 3.10. +-x[a:=0] +-x[a:=0, b:=1] +-x[5, b:=0] ++x[NOT_YET_IMPLEMENTED_ExprNamedExpr] ++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(match := pattern_error.match(s) for s in buffer): ++if any((i for i in [])): + if match.group(2) == data_not_available: + # Error OK to ignore. + pass + +-f(a := b + c for c in range(10)) +-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) +``` + +## Ruff Output + +```py +# Unparenthesized walruses are now allowed in indices since Python 3.10. +x[NOT_YET_IMPLEMENTED_ExprNamedExpr] +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 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) +``` + +## Black Output + +```py +# Unparenthesized walruses are now allowed in indices since Python 3.10. +x[a:=0] +x[a:=0, b:=1] +x[5, b:=0] + +# Walruses are allowed inside generator expressions on function calls since 3.10. +if any(match := pattern_error.match(s) for s in buffer): + if match.group(2) == data_not_available: + # Error OK to ignore. + pass + +f(a := b + c for c in range(10)) +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) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__remove_newline_after_match.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__remove_newline_after_match.py.snap new file mode 100644 index 0000000000..6099825e88 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__remove_newline_after_match.py.snap @@ -0,0 +1,76 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py +--- +## Input + +```py +def http_status(status): + + match status: + + case 400: + + return "Bad request" + + case 401: + + return "Unauthorized" + + case 403: + + return "Forbidden" + + case 404: + + return "Not found" +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,13 +1,2 @@ + def http_status(status): +- match status: +- case 400: +- return "Bad request" +- +- case 401: +- return "Unauthorized" +- +- case 403: +- return "Forbidden" +- +- case 404: +- return "Not found" ++ NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Ruff Output + +```py +def http_status(status): + NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Black Output + +```py +def http_status(status): + match status: + case 400: + return "Bad request" + + case 401: + return "Unauthorized" + + case 403: + return "Forbidden" + + case 404: + return "Not found" +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__starred_for_target.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__starred_for_target.py.snap new file mode 100644 index 0000000000..da4a8b4b11 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__starred_for_target.py.snap @@ -0,0 +1,136 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py +--- +## Input + +```py +for x in *a, *b: + print(x) + +for x in a, b, *c: + print(x) + +for x in *a, b, c: + print(x) + +for x in *a, b, *c: + print(x) + +async for x in *a, *b: + print(x) + +async for x in *a, b, *c: + print(x) + +async for x in a, b, *c: + print(x) + +async for x in ( + *loooooooooooooooooooooong, + very, + *loooooooooooooooooooooooooooooooooooooooooooooooong, +): + print(x) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,27 +1,19 @@ +-for x in *a, *b: ++for x in *NOT_YET_IMPLEMENTED_ExprStarred, *NOT_YET_IMPLEMENTED_ExprStarred: + print(x) + +-for x in a, b, *c: ++for x in a, b, *NOT_YET_IMPLEMENTED_ExprStarred: + print(x) + +-for x in *a, b, c: ++for x in *NOT_YET_IMPLEMENTED_ExprStarred, b, c: + print(x) + +-for x in *a, b, *c: ++for x in *NOT_YET_IMPLEMENTED_ExprStarred, b, *NOT_YET_IMPLEMENTED_ExprStarred: + print(x) + +-async for x in *a, *b: +- print(x) ++NOT_YET_IMPLEMENTED_StmtAsyncFor + +-async for x in *a, b, *c: +- print(x) ++NOT_YET_IMPLEMENTED_StmtAsyncFor + +-async for x in a, b, *c: +- print(x) ++NOT_YET_IMPLEMENTED_StmtAsyncFor + +-async for x in ( +- *loooooooooooooooooooooong, +- very, +- *loooooooooooooooooooooooooooooooooooooooooooooooong, +-): +- print(x) ++NOT_YET_IMPLEMENTED_StmtAsyncFor +``` + +## Ruff Output + +```py +for x in *NOT_YET_IMPLEMENTED_ExprStarred, *NOT_YET_IMPLEMENTED_ExprStarred: + print(x) + +for x in a, b, *NOT_YET_IMPLEMENTED_ExprStarred: + print(x) + +for x in *NOT_YET_IMPLEMENTED_ExprStarred, b, c: + print(x) + +for x in *NOT_YET_IMPLEMENTED_ExprStarred, b, *NOT_YET_IMPLEMENTED_ExprStarred: + print(x) + +NOT_YET_IMPLEMENTED_StmtAsyncFor + +NOT_YET_IMPLEMENTED_StmtAsyncFor + +NOT_YET_IMPLEMENTED_StmtAsyncFor + +NOT_YET_IMPLEMENTED_StmtAsyncFor +``` + +## Black Output + +```py +for x in *a, *b: + print(x) + +for x in a, b, *c: + print(x) + +for x in *a, b, c: + print(x) + +for x in *a, b, *c: + print(x) + +async for x in *a, *b: + print(x) + +async for x in *a, b, *c: + print(x) + +async for x in a, b, *c: + print(x) + +async for x in ( + *loooooooooooooooooooooong, + very, + *loooooooooooooooooooooooooooooooooooooooooooooooong, +): + print(x) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654.py.snap new file mode 100644 index 0000000000..b7995833b5 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654.py.snap @@ -0,0 +1,240 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py +--- +## Input + +```py +try: + raise OSError("blah") +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except* Exception: + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except* TypeError as e: + tes = e + raise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except* (TypeError, ValueError) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except* OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,5 +1,5 @@ + try: +- raise OSError("blah") ++ NOT_YET_IMPLEMENTED_StmtRaise + except* ExceptionGroup as e: + pass + +@@ -14,10 +14,10 @@ + + try: + try: +- raise ValueError(42) ++ NOT_YET_IMPLEMENTED_StmtRaise + except: + try: +- raise TypeError(int) ++ NOT_YET_IMPLEMENTED_StmtRaise + except* Exception: + pass + 1 / 0 +@@ -26,10 +26,10 @@ + + try: + try: +- raise FalsyEG("eg", [TypeError(1), ValueError(2)]) ++ NOT_YET_IMPLEMENTED_StmtRaise + except* TypeError as e: + tes = e +- raise ++ NOT_YET_IMPLEMENTED_StmtRaise + except* ValueError as e: + ves = e + pass +@@ -38,16 +38,16 @@ + + try: + try: +- raise orig ++ NOT_YET_IMPLEMENTED_StmtRaise + except* (TypeError, ValueError) as e: +- raise SyntaxError(3) from e ++ NOT_YET_IMPLEMENTED_StmtRaise + except BaseException as e: + exc = e + + try: + try: +- raise orig ++ NOT_YET_IMPLEMENTED_StmtRaise + except* OSError as e: +- raise TypeError(3) from e ++ NOT_YET_IMPLEMENTED_StmtRaise + except ExceptionGroup as e: + exc = e +``` + +## Ruff Output + +```py +try: + NOT_YET_IMPLEMENTED_StmtRaise +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + NOT_YET_IMPLEMENTED_StmtRaise + except: + try: + NOT_YET_IMPLEMENTED_StmtRaise + except* Exception: + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + NOT_YET_IMPLEMENTED_StmtRaise + except* TypeError as e: + tes = e + NOT_YET_IMPLEMENTED_StmtRaise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + NOT_YET_IMPLEMENTED_StmtRaise + except* (TypeError, ValueError) as e: + NOT_YET_IMPLEMENTED_StmtRaise +except BaseException as e: + exc = e + +try: + try: + NOT_YET_IMPLEMENTED_StmtRaise + except* OSError as e: + NOT_YET_IMPLEMENTED_StmtRaise +except ExceptionGroup as e: + exc = e +``` + +## Black Output + +```py +try: + raise OSError("blah") +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except* Exception: + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except* TypeError as e: + tes = e + raise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except* (TypeError, ValueError) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except* OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654_style.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654_style.py.snap new file mode 100644 index 0000000000..19568a1176 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654_style.py.snap @@ -0,0 +1,243 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py +--- +## Input + +```py +try: + raise OSError("blah") +except * ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except *ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except *(Exception): + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except \ + *TypeError as e: + tes = e + raise + except * ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except *(TypeError, ValueError, *OTHER_EXCEPTIONS) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except\ + * OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,5 +1,5 @@ + try: +- raise OSError("blah") ++ NOT_YET_IMPLEMENTED_StmtRaise + except* ExceptionGroup as e: + pass + +@@ -14,10 +14,10 @@ + + try: + try: +- raise ValueError(42) ++ NOT_YET_IMPLEMENTED_StmtRaise + except: + try: +- raise TypeError(int) ++ NOT_YET_IMPLEMENTED_StmtRaise + except* Exception: + pass + 1 / 0 +@@ -26,10 +26,10 @@ + + try: + try: +- raise FalsyEG("eg", [TypeError(1), ValueError(2)]) ++ NOT_YET_IMPLEMENTED_StmtRaise + except* TypeError as e: + tes = e +- raise ++ NOT_YET_IMPLEMENTED_StmtRaise + except* ValueError as e: + ves = e + pass +@@ -38,16 +38,16 @@ + + try: + try: +- raise orig +- except* (TypeError, ValueError, *OTHER_EXCEPTIONS) as e: +- raise SyntaxError(3) from e ++ NOT_YET_IMPLEMENTED_StmtRaise ++ except* (TypeError, ValueError, *NOT_YET_IMPLEMENTED_ExprStarred) as e: ++ NOT_YET_IMPLEMENTED_StmtRaise + except BaseException as e: + exc = e + + try: + try: +- raise orig ++ NOT_YET_IMPLEMENTED_StmtRaise + except* OSError as e: +- raise TypeError(3) from e ++ NOT_YET_IMPLEMENTED_StmtRaise + except ExceptionGroup as e: + exc = e +``` + +## Ruff Output + +```py +try: + NOT_YET_IMPLEMENTED_StmtRaise +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + NOT_YET_IMPLEMENTED_StmtRaise + except: + try: + NOT_YET_IMPLEMENTED_StmtRaise + except* Exception: + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + NOT_YET_IMPLEMENTED_StmtRaise + except* TypeError as e: + tes = e + NOT_YET_IMPLEMENTED_StmtRaise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + NOT_YET_IMPLEMENTED_StmtRaise + except* (TypeError, ValueError, *NOT_YET_IMPLEMENTED_ExprStarred) as e: + NOT_YET_IMPLEMENTED_StmtRaise +except BaseException as e: + exc = e + +try: + try: + NOT_YET_IMPLEMENTED_StmtRaise + except* OSError as e: + NOT_YET_IMPLEMENTED_StmtRaise +except ExceptionGroup as e: + exc = e +``` + +## Black Output + +```py +try: + raise OSError("blah") +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except* Exception: + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except* TypeError as e: + tes = e + raise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except* (TypeError, ValueError, *OTHER_EXCEPTIONS) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except* OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_36__numeric_literals.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_36__numeric_literals.py.snap new file mode 100644 index 0000000000..4135458654 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_36__numeric_literals.py.snap @@ -0,0 +1,118 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py +--- +## Input + +```py +#!/usr/bin/env python3.6 + +x = 123456789 +x = 123456 +x = .1 +x = 1. +x = 1E+1 +x = 1E-1 +x = 1.000_000_01 +x = 123456789.123456789 +x = 123456789.123456789E123456789 +x = 123456789E123456789 +x = 123456789J +x = 123456789.123456789J +x = 0XB1ACC +x = 0B1011 +x = 0O777 +x = 0.000000006 +x = 10000 +x = 133333 +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -2,19 +2,19 @@ + + x = 123456789 + x = 123456 +-x = 0.1 +-x = 1.0 +-x = 1e1 +-x = 1e-1 ++x = .1 ++x = 1. ++x = 1E+1 ++x = 1E-1 + x = 1.000_000_01 + x = 123456789.123456789 +-x = 123456789.123456789e123456789 +-x = 123456789e123456789 +-x = 123456789j +-x = 123456789.123456789j +-x = 0xB1ACC +-x = 0b1011 +-x = 0o777 ++x = 123456789.123456789E123456789 ++x = 123456789E123456789 ++x = 123456789J ++x = 123456789.123456789J ++x = 0XB1ACC ++x = 0B1011 ++x = 0O777 + x = 0.000000006 + x = 10000 + x = 133333 +``` + +## Ruff Output + +```py +#!/usr/bin/env python3.6 + +x = 123456789 +x = 123456 +x = .1 +x = 1. +x = 1E+1 +x = 1E-1 +x = 1.000_000_01 +x = 123456789.123456789 +x = 123456789.123456789E123456789 +x = 123456789E123456789 +x = 123456789J +x = 123456789.123456789J +x = 0XB1ACC +x = 0B1011 +x = 0O777 +x = 0.000000006 +x = 10000 +x = 133333 +``` + +## Black Output + +```py +#!/usr/bin/env python3.6 + +x = 123456789 +x = 123456 +x = 0.1 +x = 1.0 +x = 1e1 +x = 1e-1 +x = 1.000_000_01 +x = 123456789.123456789 +x = 123456789.123456789e123456789 +x = 123456789e123456789 +x = 123456789j +x = 123456789.123456789j +x = 0xB1ACC +x = 0b1011 +x = 0o777 +x = 0.000000006 +x = 10000 +x = 133333 +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_36__numeric_literals_skip_underscores.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_36__numeric_literals_skip_underscores.py.snap new file mode 100644 index 0000000000..69d74787f3 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_36__numeric_literals_skip_underscores.py.snap @@ -0,0 +1,72 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py +--- +## Input + +```py +#!/usr/bin/env python3.6 + +x = 123456789 +x = 1_2_3_4_5_6_7 +x = 1E+1 +x = 0xb1acc +x = 0.00_00_006 +x = 12_34_567J +x = .1_2 +x = 1_2. +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -2,9 +2,9 @@ + + x = 123456789 + x = 1_2_3_4_5_6_7 +-x = 1e1 +-x = 0xB1ACC ++x = 1E+1 ++x = 0xb1acc + x = 0.00_00_006 +-x = 12_34_567j +-x = 0.1_2 +-x = 1_2.0 ++x = 12_34_567J ++x = .1_2 ++x = 1_2. +``` + +## Ruff Output + +```py +#!/usr/bin/env python3.6 + +x = 123456789 +x = 1_2_3_4_5_6_7 +x = 1E+1 +x = 0xb1acc +x = 0.00_00_006 +x = 12_34_567J +x = .1_2 +x = 1_2. +``` + +## Black Output + +```py +#!/usr/bin/env python3.6 + +x = 123456789 +x = 1_2_3_4_5_6_7 +x = 1e1 +x = 0xB1ACC +x = 0.00_00_006 +x = 12_34_567j +x = 0.1_2 +x = 1_2.0 +``` + + 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 new file mode 100644 index 0000000000..47302f6806 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_37__python37.py.snap @@ -0,0 +1,144 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py +--- +## Input + +```py +#!/usr/bin/env python3.7 + + +def f(): + return (i * 2 async for i in arange(42)) + + +def g(): + return ( + something_long * something_long + async for something_long in async_generator(with_an_argument) + ) + + +async def func(): + if test: + out_batched = [ + i + async for i in aitertools._async_map( + self.async_inc, arange(8), batch_size=3 + ) + ] + + +def awaited_generator_value(n): + return (await awaitable for awaitable in awaitable_list) + + +def make_arange(n): + return (i * 2 for i in range(n) if await wrap(i)) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -2,29 +2,21 @@ + + + def f(): +- return (i * 2 async for i in arange(42)) ++ return (i for i in []) + + + def g(): +- return ( +- something_long * something_long +- async for something_long in async_generator(with_an_argument) +- ) ++ return (i for i in []) + + + async def func(): + if test: +- out_batched = [ +- i +- async for i in aitertools._async_map( +- self.async_inc, arange(8), batch_size=3 +- ) +- ] ++ out_batched = [i for i in []] + + + def awaited_generator_value(n): +- return (await awaitable for awaitable in awaitable_list) ++ return (i for i in []) + + + def make_arange(n): +- return (i * 2 for i in range(n) if await wrap(i)) ++ return (i for i in []) +``` + +## Ruff Output + +```py +#!/usr/bin/env python3.7 + + +def f(): + return (i for i in []) + + +def g(): + return (i for i in []) + + +async def func(): + if test: + out_batched = [i for i in []] + + +def awaited_generator_value(n): + return (i for i in []) + + +def make_arange(n): + return (i for i in []) +``` + +## Black Output + +```py +#!/usr/bin/env python3.7 + + +def f(): + return (i * 2 async for i in arange(42)) + + +def g(): + return ( + something_long * something_long + async for something_long in async_generator(with_an_argument) + ) + + +async def func(): + if test: + out_batched = [ + i + async for i in aitertools._async_map( + self.async_inc, arange(8), batch_size=3 + ) + ] + + +def awaited_generator_value(n): + return (await awaitable for awaitable in awaitable_list) + + +def make_arange(n): + return (i * 2 for i in range(n) if await wrap(i)) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_570.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_570.py.snap new file mode 100644 index 0000000000..fb9745c292 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_570.py.snap @@ -0,0 +1,174 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py +--- +## Input + +```py +def positional_only_arg(a, /): + pass + + +def all_markers(a, b, /, c, d, *, e, f): + pass + + +def all_markers_with_args_and_kwargs( + a_long_one, + b_long_one, + /, + c_long_one, + d_long_one, + *args, + e_long_one, + f_long_one, + **kwargs, +): + pass + + +def all_markers_with_defaults(a, b=1, /, c=2, d=3, *, e=4, f=5): + pass + + +def long_one_with_long_parameter_names( + but_all_of_them, + are_positional_only, + arguments_mmmmkay, + so_this_is_only_valid_after, + three_point_eight, + /, +): + pass + + +lambda a, /: a + +lambda a, b, /, c, d, *, e, f: a + +lambda a, b, /, c, d, *args, e, f, **kwargs: args + +lambda a, b=1, /, c=2, d=3, *, e=4, f=5: 1 +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -35,10 +35,10 @@ + pass + + +-lambda a, /: a ++lambda x: True + +-lambda a, b, /, c, d, *, e, f: a ++lambda x: True + +-lambda a, b, /, c, d, *args, e, f, **kwargs: args ++lambda x: True + +-lambda a, b=1, /, c=2, d=3, *, e=4, f=5: 1 ++lambda x: True +``` + +## Ruff Output + +```py +def positional_only_arg(a, /): + pass + + +def all_markers(a, b, /, c, d, *, e, f): + pass + + +def all_markers_with_args_and_kwargs( + a_long_one, + b_long_one, + /, + c_long_one, + d_long_one, + *args, + e_long_one, + f_long_one, + **kwargs, +): + pass + + +def all_markers_with_defaults(a, b=1, /, c=2, d=3, *, e=4, f=5): + pass + + +def long_one_with_long_parameter_names( + but_all_of_them, + are_positional_only, + arguments_mmmmkay, + so_this_is_only_valid_after, + three_point_eight, + /, +): + pass + + +lambda x: True + +lambda x: True + +lambda x: True + +lambda x: True +``` + +## Black Output + +```py +def positional_only_arg(a, /): + pass + + +def all_markers(a, b, /, c, d, *, e, f): + pass + + +def all_markers_with_args_and_kwargs( + a_long_one, + b_long_one, + /, + c_long_one, + d_long_one, + *args, + e_long_one, + f_long_one, + **kwargs, +): + pass + + +def all_markers_with_defaults(a, b=1, /, c=2, d=3, *, e=4, f=5): + pass + + +def long_one_with_long_parameter_names( + but_all_of_them, + are_positional_only, + arguments_mmmmkay, + so_this_is_only_valid_after, + three_point_eight, + /, +): + pass + + +lambda a, /: a + +lambda a, b, /, c, d, *, e, f: a + +lambda a, b, /, c, d, *args, e, f, **kwargs: args + +lambda a, b=1, /, c=2, d=3, *, e=4, f=5: 1 +``` + + 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 new file mode 100644 index 0000000000..5e1fa92d6b --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_572.py.snap @@ -0,0 +1,247 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py +--- +## Input + +```py +(a := 1) +(a := a) +if (match := pattern.search(data)) is None: + pass +if match := pattern.search(data): + pass +[y := f(x), y**2, y**3] +filtered_data = [y for x in data if (y := f(x)) is None] +(y := f(x)) +y0 = (y1 := f(x)) +foo(x=(y := f(x))) + + +def foo(answer=(p := 42)): + pass + + +def foo(answer: (p := 42) = 5): + pass + + +lambda: (x := 1) +(x := lambda: 1) +(x := lambda: (y := 1)) +lambda line: (m := re.match(pattern, line)) and m.group(1) +x = (y := 0) +(z := (y := (x := 0))) +(info := (name, phone, *rest)) +(x := 1, 2) +(total := total + tax) +len(lines := f.readlines()) +foo(x := 3, cat="vector") +foo(cat=(category := "vector")) +if any(len(longline := l) >= 100 for l in lines): + print(longline) +if env_base := os.environ.get("PYTHONUSERBASE", None): + return env_base +if self._is_special and (ans := self._check_nans(context=context)): + return ans +foo(b := 2, a=1) +foo((b := 2), a=1) +foo(c=(b := 2), a=1) + +while x := f(x): + pass +while x := f(x): + pass +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,47 +1,47 @@ +-(a := 1) +-(a := a) +-if (match := pattern.search(data)) is None: ++(NOT_YET_IMPLEMENTED_ExprNamedExpr) ++(NOT_YET_IMPLEMENTED_ExprNamedExpr) ++if (NOT_YET_IMPLEMENTED_ExprNamedExpr) is None: + pass +-if match := pattern.search(data): ++if NOT_YET_IMPLEMENTED_ExprNamedExpr: + pass +-[y := f(x), y**2, y**3] +-filtered_data = [y for x in data if (y := f(x)) is None] +-(y := 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 []] ++(NOT_YET_IMPLEMENTED_ExprNamedExpr) ++y0 = NOT_YET_IMPLEMENTED_ExprNamedExpr ++foo(x=(NOT_YET_IMPLEMENTED_ExprNamedExpr)) + + +-def foo(answer=(p := 42)): ++def foo(answer=(NOT_YET_IMPLEMENTED_ExprNamedExpr)): + pass + + +-def foo(answer: (p := 42) = 5): ++def foo(answer: (NOT_YET_IMPLEMENTED_ExprNamedExpr) = 5): + pass + + +-lambda: (x := 1) +-(x := lambda: 1) +-(x := lambda: (y := 1)) +-lambda line: (m := re.match(pattern, line)) and m.group(1) +-x = (y := 0) +-(z := (y := (x := 0))) +-(info := (name, phone, *rest)) +-(x := 1, 2) +-(total := total + tax) +-len(lines := f.readlines()) +-foo(x := 3, cat="vector") +-foo(cat=(category := "vector")) +-if any(len(longline := l) >= 100 for l in lines): ++lambda x: True ++(NOT_YET_IMPLEMENTED_ExprNamedExpr) ++(NOT_YET_IMPLEMENTED_ExprNamedExpr) ++lambda x: True ++x = NOT_YET_IMPLEMENTED_ExprNamedExpr ++(NOT_YET_IMPLEMENTED_ExprNamedExpr) ++(NOT_YET_IMPLEMENTED_ExprNamedExpr) ++(NOT_YET_IMPLEMENTED_ExprNamedExpr, 2) ++(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 [])): + print(longline) +-if env_base := os.environ.get("PYTHONUSERBASE", None): ++if NOT_YET_IMPLEMENTED_ExprNamedExpr: + return env_base +-if self._is_special and (ans := self._check_nans(context=context)): ++if self._is_special and (NOT_YET_IMPLEMENTED_ExprNamedExpr): + return ans +-foo(b := 2, a=1) +-foo((b := 2), a=1) +-foo(c=(b := 2), a=1) ++foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, a=1) ++foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, a=1) ++foo(c=(NOT_YET_IMPLEMENTED_ExprNamedExpr), a=1) + +-while x := f(x): ++while NOT_YET_IMPLEMENTED_ExprNamedExpr: + pass +-while x := f(x): ++while NOT_YET_IMPLEMENTED_ExprNamedExpr: + pass +``` + +## Ruff Output + +```py +(NOT_YET_IMPLEMENTED_ExprNamedExpr) +(NOT_YET_IMPLEMENTED_ExprNamedExpr) +if (NOT_YET_IMPLEMENTED_ExprNamedExpr) is None: + pass +if NOT_YET_IMPLEMENTED_ExprNamedExpr: + pass +[NOT_YET_IMPLEMENTED_ExprNamedExpr, y**2, y**3] +filtered_data = [i for i in []] +(NOT_YET_IMPLEMENTED_ExprNamedExpr) +y0 = NOT_YET_IMPLEMENTED_ExprNamedExpr +foo(x=(NOT_YET_IMPLEMENTED_ExprNamedExpr)) + + +def foo(answer=(NOT_YET_IMPLEMENTED_ExprNamedExpr)): + pass + + +def foo(answer: (NOT_YET_IMPLEMENTED_ExprNamedExpr) = 5): + pass + + +lambda x: True +(NOT_YET_IMPLEMENTED_ExprNamedExpr) +(NOT_YET_IMPLEMENTED_ExprNamedExpr) +lambda x: True +x = NOT_YET_IMPLEMENTED_ExprNamedExpr +(NOT_YET_IMPLEMENTED_ExprNamedExpr) +(NOT_YET_IMPLEMENTED_ExprNamedExpr) +(NOT_YET_IMPLEMENTED_ExprNamedExpr, 2) +(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 [])): + print(longline) +if NOT_YET_IMPLEMENTED_ExprNamedExpr: + return env_base +if self._is_special and (NOT_YET_IMPLEMENTED_ExprNamedExpr): + return ans +foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, a=1) +foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, a=1) +foo(c=(NOT_YET_IMPLEMENTED_ExprNamedExpr), a=1) + +while NOT_YET_IMPLEMENTED_ExprNamedExpr: + pass +while NOT_YET_IMPLEMENTED_ExprNamedExpr: + pass +``` + +## Black Output + +```py +(a := 1) +(a := a) +if (match := pattern.search(data)) is None: + pass +if match := pattern.search(data): + pass +[y := f(x), y**2, y**3] +filtered_data = [y for x in data if (y := f(x)) is None] +(y := f(x)) +y0 = (y1 := f(x)) +foo(x=(y := f(x))) + + +def foo(answer=(p := 42)): + pass + + +def foo(answer: (p := 42) = 5): + pass + + +lambda: (x := 1) +(x := lambda: 1) +(x := lambda: (y := 1)) +lambda line: (m := re.match(pattern, line)) and m.group(1) +x = (y := 0) +(z := (y := (x := 0))) +(info := (name, phone, *rest)) +(x := 1, 2) +(total := total + tax) +len(lines := f.readlines()) +foo(x := 3, cat="vector") +foo(cat=(category := "vector")) +if any(len(longline := l) >= 100 for l in lines): + print(longline) +if env_base := os.environ.get("PYTHONUSERBASE", None): + return env_base +if self._is_special and (ans := self._check_nans(context=context)): + return ans +foo(b := 2, a=1) +foo((b := 2), a=1) +foo(c=(b := 2), a=1) + +while x := f(x): + pass +while x := f(x): + pass +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__python38.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__python38.py.snap new file mode 100644 index 0000000000..4f4ceeb54b --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__python38.py.snap @@ -0,0 +1,113 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py +--- +## Input + +```py +#!/usr/bin/env python3.8 + + +def starred_return(): + my_list = ["value2", "value3"] + return "value1", *my_list + + +def starred_yield(): + my_list = ["value2", "value3"] + yield "value1", *my_list + + +# all right hand side expressions allowed in regular assignments are now also allowed in +# annotated assignments +a : Tuple[ str, int] = "1", 2 +a: Tuple[int , ... ] = b, *c, d +def t(): + a : str = yield "a" +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -3,19 +3,19 @@ + + def starred_return(): + my_list = ["value2", "value3"] +- return "value1", *my_list ++ return "value1", *NOT_YET_IMPLEMENTED_ExprStarred + + + def starred_yield(): + my_list = ["value2", "value3"] +- yield "value1", *my_list ++ NOT_YET_IMPLEMENTED_ExprYield + + + # all right hand side expressions allowed in regular assignments are now also allowed in + # annotated assignments +-a: Tuple[str, int] = "1", 2 +-a: Tuple[int, ...] = b, *c, d ++NOT_YET_IMPLEMENTED_StmtAnnAssign ++NOT_YET_IMPLEMENTED_StmtAnnAssign + + + def t(): +- a: str = yield "a" ++ NOT_YET_IMPLEMENTED_StmtAnnAssign +``` + +## Ruff Output + +```py +#!/usr/bin/env python3.8 + + +def starred_return(): + my_list = ["value2", "value3"] + return "value1", *NOT_YET_IMPLEMENTED_ExprStarred + + +def starred_yield(): + my_list = ["value2", "value3"] + NOT_YET_IMPLEMENTED_ExprYield + + +# all right hand side expressions allowed in regular assignments are now also allowed in +# annotated assignments +NOT_YET_IMPLEMENTED_StmtAnnAssign +NOT_YET_IMPLEMENTED_StmtAnnAssign + + +def t(): + NOT_YET_IMPLEMENTED_StmtAnnAssign +``` + +## Black Output + +```py +#!/usr/bin/env python3.8 + + +def starred_return(): + my_list = ["value2", "value3"] + return "value1", *my_list + + +def starred_yield(): + my_list = ["value2", "value3"] + yield "value1", *my_list + + +# all right hand side expressions allowed in regular assignments are now also allowed in +# annotated assignments +a: Tuple[str, int] = "1", 2 +a: Tuple[int, ...] = b, *c, d + + +def t(): + a: str = yield "a" +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__pep_572_py39.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__pep_572_py39.py.snap new file mode 100644 index 0000000000..ec43406b54 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__pep_572_py39.py.snap @@ -0,0 +1,60 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py +--- +## Input + +```py +# Unparenthesized walruses are now allowed in set literals & set comprehensions +# since Python 3.9 +{x := 1, 2, 3} +{x4 := x**5 for x in range(7)} +# We better not remove the parentheses here (since it's a 3.10 feature) +x[(a := 1)] +x[(a := 1), (b := 3)] +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,7 +1,7 @@ + # Unparenthesized walruses are now allowed in set literals & set comprehensions + # since Python 3.9 +-{x := 1, 2, 3} +-{x4 := x**5 for x in range(7)} ++{NOT_YET_IMPLEMENTED_ExprNamedExpr, 2, 3} ++{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} + # We better not remove the parentheses here (since it's a 3.10 feature) +-x[(a := 1)] +-x[(a := 1), (b := 3)] ++x[(NOT_YET_IMPLEMENTED_ExprNamedExpr)] ++x[((NOT_YET_IMPLEMENTED_ExprNamedExpr), (NOT_YET_IMPLEMENTED_ExprNamedExpr))] +``` + +## Ruff Output + +```py +# Unparenthesized walruses are now allowed in set literals & set comprehensions +# since Python 3.9 +{NOT_YET_IMPLEMENTED_ExprNamedExpr, 2, 3} +{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} +# We better not remove the parentheses here (since it's a 3.10 feature) +x[(NOT_YET_IMPLEMENTED_ExprNamedExpr)] +x[((NOT_YET_IMPLEMENTED_ExprNamedExpr), (NOT_YET_IMPLEMENTED_ExprNamedExpr))] +``` + +## Black Output + +```py +# Unparenthesized walruses are now allowed in set literals & set comprehensions +# since Python 3.9 +{x := 1, 2, 3} +{x4 := x**5 for x in range(7)} +# We better not remove the parentheses here (since it's a 3.10 feature) +x[(a := 1)] +x[(a := 1), (b := 3)] +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__python39.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__python39.py.snap new file mode 100644 index 0000000000..86ad5e3c92 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__python39.py.snap @@ -0,0 +1,94 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py +--- +## Input + +```py +#!/usr/bin/env python3.9 + +@relaxed_decorator[0] +def f(): + ... + +@relaxed_decorator[extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length] +def f(): + ... + +@extremely_long_variable_name_that_doesnt_fit := complex.expression(with_long="arguments_value_that_wont_fit_at_the_end_of_the_line") +def f(): + ... +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,6 +1,5 @@ + #!/usr/bin/env python3.9 + +- + @relaxed_decorator[0] + def f(): + ... +@@ -13,8 +12,6 @@ + ... + + +-@extremely_long_variable_name_that_doesnt_fit := complex.expression( +- with_long="arguments_value_that_wont_fit_at_the_end_of_the_line" +-) ++@NOT_YET_IMPLEMENTED_ExprNamedExpr + def f(): + ... +``` + +## Ruff Output + +```py +#!/usr/bin/env python3.9 + +@relaxed_decorator[0] +def f(): + ... + + +@relaxed_decorator[ + extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length +] +def f(): + ... + + +@NOT_YET_IMPLEMENTED_ExprNamedExpr +def f(): + ... +``` + +## Black Output + +```py +#!/usr/bin/env python3.9 + + +@relaxed_decorator[0] +def f(): + ... + + +@relaxed_decorator[ + extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length +] +def f(): + ... + + +@extremely_long_variable_name_that_doesnt_fit := complex.expression( + with_long="arguments_value_that_wont_fit_at_the_end_of_the_line" +) +def f(): + ... +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__remove_with_brackets.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__remove_with_brackets.py.snap new file mode 100644 index 0000000000..8766515712 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__remove_with_brackets.py.snap @@ -0,0 +1,216 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py +--- +## Input + +```py +with (open("bla.txt")): + pass + +with (open("bla.txt")), (open("bla.txt")): + pass + +with (open("bla.txt") as f): + pass + +# Remove brackets within alias expression +with (open("bla.txt")) as f: + pass + +# Remove brackets around one-line context managers +with (open("bla.txt") as f, (open("x"))): + pass + +with ((open("bla.txt")) as f, open("x")): + pass + +with (CtxManager1() as example1, CtxManager2() as example2): + ... + +# Brackets remain when using magic comma +with (CtxManager1() as example1, CtxManager2() as example2,): + ... + +# Brackets remain for multi-line context managers +with (CtxManager1() as example1, CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2): + ... + +# Don't touch assignment expressions +with (y := open("./test.py")) as f: + pass + +# Deeply nested examples +# N.B. Multiple brackets are only possible +# around the context manager itself. +# Only one brackets is allowed around the +# alias expression or comma-delimited context managers. +with (((open("bla.txt")))): + pass + +with (((open("bla.txt")))), (((open("bla.txt")))): + pass + +with (((open("bla.txt")))) as f: + pass + +with ((((open("bla.txt")))) as f): + pass + +with ((((CtxManager1()))) as example1, (((CtxManager2()))) as example2): + ... +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -39,7 +39,7 @@ + ... + + # Don't touch assignment expressions +-with (y := open("./test.py")) as f: ++with NOT_YET_IMPLEMENTED_ExprNamedExpr as f: + pass + + # Deeply nested examples +``` + +## Ruff Output + +```py +with open("bla.txt"): + pass + +with open("bla.txt"), open("bla.txt"): + pass + +with open("bla.txt") as f: + pass + +# Remove brackets within alias expression +with open("bla.txt") as f: + pass + +# Remove brackets around one-line context managers +with open("bla.txt") as f, open("x"): + pass + +with open("bla.txt") as f, open("x"): + pass + +with CtxManager1() as example1, CtxManager2() as example2: + ... + +# Brackets remain when using magic comma +with ( + CtxManager1() as example1, + CtxManager2() as example2, +): + ... + +# Brackets remain for multi-line context managers +with ( + CtxManager1() as example1, + CtxManager2() as example2, + CtxManager2() as example2, + CtxManager2() as example2, + CtxManager2() as example2, +): + ... + +# Don't touch assignment expressions +with NOT_YET_IMPLEMENTED_ExprNamedExpr as f: + pass + +# Deeply nested examples +# N.B. Multiple brackets are only possible +# around the context manager itself. +# Only one brackets is allowed around the +# alias expression or comma-delimited context managers. +with open("bla.txt"): + pass + +with open("bla.txt"), open("bla.txt"): + pass + +with open("bla.txt") as f: + pass + +with open("bla.txt") as f: + pass + +with CtxManager1() as example1, CtxManager2() as example2: + ... +``` + +## Black Output + +```py +with open("bla.txt"): + pass + +with open("bla.txt"), open("bla.txt"): + pass + +with open("bla.txt") as f: + pass + +# Remove brackets within alias expression +with open("bla.txt") as f: + pass + +# Remove brackets around one-line context managers +with open("bla.txt") as f, open("x"): + pass + +with open("bla.txt") as f, open("x"): + pass + +with CtxManager1() as example1, CtxManager2() as example2: + ... + +# Brackets remain when using magic comma +with ( + CtxManager1() as example1, + CtxManager2() as example2, +): + ... + +# Brackets remain for multi-line context managers +with ( + CtxManager1() as example1, + CtxManager2() as example2, + CtxManager2() as example2, + CtxManager2() as example2, + CtxManager2() as example2, +): + ... + +# Don't touch assignment expressions +with (y := open("./test.py")) as f: + pass + +# Deeply nested examples +# N.B. Multiple brackets are only possible +# around the context manager itself. +# Only one brackets is allowed around the +# alias expression or comma-delimited context managers. +with open("bla.txt"): + pass + +with open("bla.txt"), open("bla.txt"): + pass + +with open("bla.txt") as f: + pass + +with open("bla.txt") as f: + pass + +with CtxManager1() as example1, CtxManager2() as example2: + ... +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@attribute_access_on_number_literals.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__attribute_access_on_number_literals.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@attribute_access_on_number_literals.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__attribute_access_on_number_literals.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@bracketmatch.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__bracketmatch.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@bracketmatch.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__bracketmatch.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@class_methods_new_line.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__class_methods_new_line.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@class_methods_new_line.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__class_methods_new_line.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@collections.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__collections.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@collections.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__collections.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comment_after_escaped_newline.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comment_after_escaped_newline.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@comment_after_escaped_newline.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comment_after_escaped_newline.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments.py.snap similarity index 97% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments.py.snap index f5c40ca6bc..d78df8d356 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments.py.snap @@ -140,13 +140,6 @@ async def wat(): if inner_imports.are_evil(): # Explains why we have this if. -@@ -93,4 +93,4 @@ - - # Some closing comments. - # Maybe Vim or Emacs directives for formatting. --# Who knows. -\ No newline at end of file -+# Who knows. ``` ## Ruff Output @@ -348,6 +341,7 @@ async def wat(): # Some closing comments. # Maybe Vim or Emacs directives for formatting. -# Who knows.``` +# Who knows. +``` diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments2.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments2.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments2.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments3.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments3.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments3.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments3.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments4.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments4.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments4.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments4.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments5.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments5.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments5.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments5.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments6.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments6.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments6.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments6.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments9.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments9.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments9.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments9.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments_non_breaking_space.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments_non_breaking_space.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments_non_breaking_space.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments_non_breaking_space.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition_no_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition_no_trailing_comma.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition_no_trailing_comma.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition_no_trailing_comma.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@docstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@docstring.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@docstring_preview.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring_preview.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@docstring_preview.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring_preview.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@empty_lines.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__empty_lines.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@empty_lines.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__empty_lines.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__expression.py.snap similarity index 99% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__expression.py.snap index a2b53747c5..9d7a94ba9a 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__expression.py.snap @@ -266,15 +266,15 @@ last_call() ```diff --- Black +++ Ruff -@@ -1,5 +1,6 @@ -+... +@@ -1,6 +1,6 @@ + ... "some_string" -b"\\xa3" +b"NOT_YET_IMPLEMENTED_BYTE_STRING" Name None True -@@ -23,40 +24,46 @@ +@@ -24,40 +24,46 @@ 1 >> v2 1 % finished 1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8 @@ -346,7 +346,7 @@ last_call() () (1,) (1, 2) -@@ -68,40 +75,37 @@ +@@ -69,40 +75,37 @@ 2, 3, ] @@ -405,9 +405,12 @@ last_call() Python3 > Python2 > COBOL Life is Life call() -@@ -116,8 +120,8 @@ +@@ -115,10 +118,10 @@ + arg, + another, kwarg="hey", - **kwargs, +- **kwargs ++ **kwargs, ) # note: no trailing comma pre-3.6 -call(*gidgets[:2]) -call(a, *gidgets[:2]) @@ -416,7 +419,7 @@ last_call() call(**self.screen_kwargs) call(b, **self.screen_kwargs) lukasz.langa.pl -@@ -130,34 +134,28 @@ +@@ -131,34 +134,28 @@ tuple[str, ...] tuple[str, int, float, dict[str, int]] tuple[ @@ -424,9 +427,6 @@ last_call() - int, - float, - dict[str, int], --] --very_long_variable_name_filters: t.List[ -- t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], + ( + str, + int, @@ -434,6 +434,9 @@ last_call() + dict[str, int], + ) ] +-very_long_variable_name_filters: t.List[ +- t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], +-] -xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore - sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) -) @@ -464,7 +467,7 @@ last_call() numpy[0, :] numpy[:, i] numpy[0, :2] -@@ -171,20 +169,27 @@ +@@ -172,20 +169,27 @@ numpy[1 : c + 1, c] numpy[-(c + 1) :, d] numpy[:, l[-2]] @@ -500,7 +503,7 @@ last_call() { "id": "1", "type": "type", -@@ -199,32 +204,22 @@ +@@ -200,32 +204,22 @@ c = 1 d = (1,) + a + (2,) e = (1,).count(1) @@ -513,7 +516,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( @@ -521,7 +524,7 @@ last_call() - ) - .order_by(models.Customer.id.asc()) - .all() --) + ) -result = ( - session.query(models.Customer.id) - .filter( @@ -543,7 +546,7 @@ last_call() Ø = set() authors.łukasz.say_thanks() mapping = { -@@ -236,29 +231,27 @@ +@@ -237,29 +231,27 @@ def gen(): @@ -584,7 +587,7 @@ last_call() ... for i in call(): ... -@@ -327,13 +320,18 @@ +@@ -328,13 +320,18 @@ ): return True if ( @@ -606,7 +609,7 @@ last_call() ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n ): return True -@@ -341,7 +339,8 @@ +@@ -342,7 +339,8 @@ ~aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e @@ -995,6 +998,7 @@ last_call() ## Black Output ```py +... "some_string" b"\\xa3" Name @@ -1111,7 +1115,7 @@ call( arg, another, kwarg="hey", - **kwargs, + **kwargs ) # note: no trailing comma pre-3.6 call(*gidgets[:2]) call(a, *gidgets[:2]) diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap similarity index 99% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap index e4f11e148d..4d40d39ee7 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap @@ -198,18 +198,17 @@ d={'a':1, ```diff --- Black +++ Ruff -@@ -1,16 +1,14 @@ +@@ -1,15 +1,14 @@ #!/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 +NOT_YET_IMPLEMENTED_StmtImportFrom # fmt: off -from third_party import (X, @@ -221,7 +220,7 @@ d={'a':1, # Comment 1 # Comment 2 -@@ -18,30 +16,54 @@ +@@ -17,30 +16,54 @@ # fmt: off def func_no_args(): @@ -297,7 +296,7 @@ d={'a':1, def spaces_types( -@@ -51,7 +73,7 @@ +@@ -50,7 +73,7 @@ d: dict = {}, e: bool = True, f: int = -1, @@ -306,7 +305,7 @@ d={'a':1, h: str = "", i: str = r"", ): -@@ -64,55 +86,54 @@ +@@ -63,55 +86,54 @@ something = { # fmt: off @@ -381,7 +380,7 @@ d={'a':1, # fmt: on -@@ -133,10 +154,10 @@ +@@ -132,10 +154,10 @@ """Another known limitation.""" # fmt: on # fmt: off @@ -396,7 +395,7 @@ d={'a':1, # fmt: on # fmt: off # ...but comments still get reformatted even though they should not be -@@ -151,12 +172,10 @@ +@@ -150,12 +172,10 @@ ast_args.kw_defaults, parameters, implicit_default=True, @@ -411,7 +410,7 @@ d={'a':1, # fmt: on _type_comment_re = re.compile( r""" -@@ -179,7 +198,7 @@ +@@ -178,7 +198,7 @@ $ """, # fmt: off @@ -420,7 +419,7 @@ d={'a':1, # fmt: on ) -@@ -217,8 +236,7 @@ +@@ -216,8 +236,7 @@ xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5, ) # fmt: off @@ -691,7 +690,6 @@ import sys from third_party import X, Y, Z from library import some_connection, some_decorator - # fmt: off from third_party import (X, Y, Z) diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff2.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff2.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff2.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff3.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff3.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff3.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff3.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff4.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff4.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff4.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff4.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff5.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff5.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff5.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff5.py.snap 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 new file mode 100644 index 0000000000..5a0bfd673f --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtpass_imports.py.snap @@ -0,0 +1,114 @@ +--- +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@fmtskip.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip2.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip2.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip2.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip3.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip3.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip3.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip3.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip5.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip5.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip5.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip5.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip7.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip7.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip7.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip7.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip8.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip8.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fstring.py.snap similarity index 85% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fstring.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fstring.py.snap index 852778fb08..aa8a125ac7 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fstring.py.snap @@ -14,6 +14,8 @@ f"{f'''{'nested'} inner'''} outer" f"\"{f'{nested} inner'}\" outer" f"space between opening braces: { {a for a in (1, 2, 3)}}" f'Hello \'{tricky + "example"}\'' +f"Tried directories {str(rootdirs)} \ +but none started with prefix {parentdir_prefix}" ``` ## Black Differences @@ -21,7 +23,7 @@ f'Hello \'{tricky + "example"}\'' ```diff --- Black +++ Ruff -@@ -1,9 +1,9 @@ +@@ -1,11 +1,10 @@ -f"f-string without formatted values is just a string" -f"{{NOT a formatted value}}" -f'{{NOT \'a\' "formatted" "value"}}' @@ -31,6 +33,9 @@ f'Hello \'{tricky + "example"}\'' -f"\"{f'{nested} inner'}\" outer" -f"space between opening braces: { {a for a in (1, 2, 3)}}" -f'Hello \'{tricky + "example"}\'' +-f"Tried directories {str(rootdirs)} \ +-but none started with prefix {parentdir_prefix}" ++NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr @@ -54,6 +59,7 @@ NOT_YET_IMPLEMENTED_ExprJoinedStr NOT_YET_IMPLEMENTED_ExprJoinedStr NOT_YET_IMPLEMENTED_ExprJoinedStr NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr ``` ## Black Output @@ -68,6 +74,8 @@ f"{f'''{'nested'} inner'''} outer" f"\"{f'{nested} inner'}\" outer" f"space between opening braces: { {a for a in (1, 2, 3)}}" f'Hello \'{tricky + "example"}\'' +f"Tried directories {str(rootdirs)} \ +but none started with prefix {parentdir_prefix}" ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap similarity index 97% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@function.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap index 960b474e66..eed13bbae7 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap @@ -41,7 +41,7 @@ def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r'' def spaces_types(a: int = 1, b: tuple = (), c: list = [], d: dict = {}, e: bool = True, f: int = -1, g: int = 1 if False else 2, h: str = "", i: str = r''): ... def spaces2(result= _core.Value(None)): assert fut is self._read_fut, (fut, self._read_fut) - + # EMPTY LINE WITH WHITESPACE (this comment will be removed) def example(session): result = session.query(models.Customer.id).filter( models.Customer.account_id == account_id, @@ -111,14 +111,14 @@ def __await__(): return (yield) #!/usr/bin/env python3 -import asyncio -import sys -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImport - +- -from third_party import X, Y, Z -+NOT_YET_IMPLEMENTED_StmtImportFrom ++NOT_YET_IMPLEMENTED_StmtImport ++NOT_YET_IMPLEMENTED_StmtImport -from library import some_connection, some_decorator -- ++NOT_YET_IMPLEMENTED_StmtImportFrom + -f"trigger 3.6 mode" +NOT_YET_IMPLEMENTED_StmtImportFrom +NOT_YET_IMPLEMENTED_ExprJoinedStr @@ -170,12 +170,13 @@ def __await__(): return (yield) h: str = "", i: str = r"", ): -@@ -64,19 +73,16 @@ +@@ -64,19 +73,17 @@ def spaces2(result=_core.Value(None)): - assert fut is self._read_fut, (fut, self._read_fut) + NOT_YET_IMPLEMENTED_StmtAssert ++ # EMPTY LINE WITH WHITESPACE (this comment will be removed) def example(session): @@ -197,7 +198,7 @@ def __await__(): return (yield) def long_lines(): -@@ -87,7 +93,7 @@ +@@ -87,7 +94,7 @@ ast_args.kw_defaults, parameters, implicit_default=True, @@ -206,7 +207,7 @@ def __await__(): return (yield) ) typedargslist.extend( gen_annotated_params( -@@ -96,7 +102,7 @@ +@@ -96,7 +103,7 @@ parameters, implicit_default=True, # trailing standalone comment @@ -215,7 +216,7 @@ def __await__(): return (yield) ) _type_comment_re = re.compile( r""" -@@ -135,14 +141,8 @@ +@@ -135,14 +142,8 @@ a, **kwargs, ) -> A: @@ -313,6 +314,7 @@ def spaces_types( def spaces2(result=_core.Value(None)): NOT_YET_IMPLEMENTED_StmtAssert + # EMPTY LINE WITH WHITESPACE (this comment will be removed) def example(session): diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function2.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@function2.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function2.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function_trailing_comma.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@function_trailing_comma.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function_trailing_comma.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@import_spacing.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__import_spacing.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@import_spacing.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__import_spacing.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@one_element_subscript.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__one_element_subscript.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@one_element_subscript.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__one_element_subscript.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@power_op_spacing.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__power_op_spacing.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@power_op_spacing.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__power_op_spacing.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_await_parens.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_await_parens.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_await_parens.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_await_parens.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_except_parens.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_except_parens.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_except_parens.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_except_parens.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_for_brackets.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_for_brackets.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_for_brackets.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_for_brackets.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@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 similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_newline_after_code_block_open.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_newline_after_code_block_open.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_parens.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_parens.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_parens.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_parens.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@return_annotation_brackets.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__return_annotation_brackets.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@return_annotation_brackets.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__return_annotation_brackets.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@skip_magic_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__skip_magic_trailing_comma.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@skip_magic_trailing_comma.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__skip_magic_trailing_comma.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@slices.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__slices.py.snap similarity index 80% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@slices.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__slices.py.snap index 50113ee3a3..bf646fcda1 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@slices.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__slices.py.snap @@ -36,38 +36,6 @@ ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] ham[lower + offset : upper + offset] - -slice[::, ::] -slice[ - # A - : - # B - : - # C -] -slice[ - # A - 1: - # B - 2: - # C - 3 -] - -slice[ - # A - 1 - + 2 : - # B - 3 : - # C - 4 -] -x[ - 1: # A - 2: # B - 3 # C -] ``` ## Black Differences @@ -75,7 +43,7 @@ x[ ```diff --- Black +++ Ruff -@@ -4,30 +4,30 @@ +@@ -4,28 +4,28 @@ slice[d::d] slice[0] slice[-1] @@ -113,27 +81,6 @@ x[ -ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] +ham[ : upper_fn(x) : step_fn(x)], ham[ :: step_fn(x)] ham[lower + offset : upper + offset] - - slice[::, ::] -@@ -49,11 +49,14 @@ - - slice[ - # A -- 1 -- + 2 : -+ 1 + 2 : - # B -- 3 : -+ 3 : - # C - 4 - ] --x[1:2:3] # A # B # C -+x[ -+ 1: # A -+ 2: # B -+ 3 # C -+] ``` ## Ruff Output @@ -170,37 +117,6 @@ ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] ham[ : upper_fn(x) : step_fn(x)], ham[ :: step_fn(x)] ham[lower + offset : upper + offset] - -slice[::, ::] -slice[ - # A - : - # B - : - # C -] -slice[ - # A - 1: - # B - 2: - # C - 3 -] - -slice[ - # A - 1 + 2 : - # B - 3 : - # C - 4 -] -x[ - 1: # A - 2: # B - 3 # C -] ``` ## Black Output @@ -237,34 +153,6 @@ ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] ham[lower + offset : upper + offset] - -slice[::, ::] -slice[ - # A - : - # B - : - # C -] -slice[ - # A - 1: - # B - 2: - # C - 3 -] - -slice[ - # A - 1 - + 2 : - # B - 3 : - # C - 4 -] -x[1:2:3] # A # B # C ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@string_prefixes.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__string_prefixes.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@string_prefixes.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__string_prefixes.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@torture.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__torture.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@torture.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__torture.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens1.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens1.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens1.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens1.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens2.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens2.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens2.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_commas_in_leading_parts.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_commas_in_leading_parts.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_commas_in_leading_parts.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_commas_in_leading_parts.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@tupleassign.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__tupleassign.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@tupleassign.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__tupleassign.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__whitespace.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__whitespace.py.snap new file mode 100644 index 0000000000..70636c22b7 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__whitespace.py.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/whitespace.py +--- +## Input + +```py + +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1 +0,0 @@ +- +``` + +## Ruff Output + +```py +``` + +## Black Output + +```py + +``` + + From 9c2a75284b9c9b7ee5da5ec2d9d5d87b90da0c1d Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 30 Jun 2023 09:52:14 +0200 Subject: [PATCH 283/447] Preserve parentheses around left side of binary expression ## Summary This PR fixes an issue where the binary expression formatting removed parentheses around the left hand side of an expression. ## Test Plan I added a new regression test and re-ran the ecosystem check. It brings down the `check-formatter-stability` output from a 3.4MB file down to 900KB. --- .../test/fixtures/ruff/expression/binary.py | 6 ++++++ .../src/expression/expr_bin_op.rs | 19 +++++++++++++++---- ...atibility@simple_cases__expression.py.snap | 17 ++++------------- .../format@expression__binary.py.snap | 12 ++++++++++++ .../format@expression__unary.py.snap | 6 ++++-- 5 files changed, 41 insertions(+), 19 deletions(-) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py index cbd93631ae..30cf4c4465 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py @@ -205,3 +205,9 @@ if ( # Unstable formatting in https://github.com/realtyem/synapse-unraid/blob/unraid_develop/synapse/handlers/presence.py for user_id in set(target_user_ids) - {u.user_id for u in updates}: updates.append(UserPresenceState.default(user_id)) + +# Keeps parenthesized left hand sides +( + log(self.price / self.strike) + + (self.risk_free - self.div_cont + 0.5 * (self.sigma**2)) * self.exp_time +) / self.sigmaT diff --git a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs index 6e2dc5226f..859e03ce4f 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs @@ -1,12 +1,14 @@ use crate::comments::{trailing_comments, trailing_node_comments, Comments}; use crate::expression::binary_like::{BinaryLayout, FormatBinaryLike}; use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parenthesize, + default_expression_needs_parentheses, is_expression_parenthesized, NeedsParentheses, + Parenthesize, }; use crate::expression::Parentheses; use crate::prelude::*; use crate::FormatNodeRule; use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions}; +use ruff_python_ast::node::AstNode; use rustpython_parser::ast::{ Constant, Expr, ExprAttribute, ExprBinOp, ExprConstant, ExprUnaryOp, Operator, UnaryOp, }; @@ -44,9 +46,18 @@ impl<'ast> FormatBinaryLike<'ast> for ExprBinOp { fn fmt_default(&self, f: &mut PyFormatter<'ast, '_>) -> FormatResult<()> { let comments = f.context().comments().clone(); - let format_inner = format_with(|f| { - let binary_chain: SmallVec<[&ExprBinOp; 4]> = - iter::successors(Some(self), |parent| parent.left.as_bin_op_expr()).collect(); + let format_inner = format_with(|f: &mut PyFormatter| { + let source = f.context().contents(); + let binary_chain: SmallVec<[&ExprBinOp; 4]> = iter::successors(Some(self), |parent| { + parent.left.as_bin_op_expr().and_then(|bin_expression| { + if is_expression_parenthesized(bin_expression.as_any_node_ref(), source) { + None + } else { + Some(bin_expression) + } + }) + }) + .collect(); // SAFETY: `binary_chain` is guaranteed not to be empty because it always contains the current expression. let left_most = binary_chain.last().unwrap(); 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 9d7a94ba9a..86ac68c37e 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 @@ -274,20 +274,11 @@ last_call() Name None True -@@ -24,40 +24,46 @@ - 1 >> v2 - 1 % finished - 1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8 --((1 + v2) - (v3 * 4)) ^ (((5**v6) / 7) // 8) -+(1 + v2 - (v3 * 4)) ^ (5**v6 / 7 // 8) - not great - ~great - +value +@@ -31,33 +31,39 @@ -1 ~int and not v1 ^ 123 + v2 | True --(~int) and (not ((v1 ^ (123 + v2)) | True)) + (~int) and (not ((v1 ^ (123 + v2)) | True)) -+(really ** -(confusing ** ~(operator**-precedence))) -+(~int) and (not (v1 ^ (123 + v2) | True)) ++really ** -confusing ** ~operator**-precedence flags & ~select.EPOLLIN and waiters.write_task is not None -lambda arg: None @@ -650,13 +641,13 @@ v1 << 2 1 >> v2 1 % finished 1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8 -(1 + v2 - (v3 * 4)) ^ (5**v6 / 7 // 8) +((1 + v2) - (v3 * 4)) ^ (((5**v6) / 7) // 8) not great ~great +value -1 ~int and not v1 ^ 123 + v2 | True -(~int) and (not (v1 ^ (123 + v2) | True)) +(~int) and (not ((v1 ^ (123 + v2)) | True)) +really ** -confusing ** ~operator**-precedence flags & ~select.EPOLLIN and waiters.write_task is not None lambda x: True 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 670762dfe9..8b6f78bd10 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 @@ -211,6 +211,12 @@ if ( # Unstable formatting in https://github.com/realtyem/synapse-unraid/blob/unraid_develop/synapse/handlers/presence.py for user_id in set(target_user_ids) - {u.user_id for u in updates}: updates.append(UserPresenceState.default(user_id)) + +# Keeps parenthesized left hand sides +( + log(self.price / self.strike) + + (self.risk_free - self.div_cont + 0.5 * (self.sigma**2)) * self.exp_time +) / self.sigmaT ``` ## Output @@ -468,6 +474,12 @@ for user_id in set( target_user_ids ) - {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set}: updates.append(UserPresenceState.default(user_id)) + +# Keeps parenthesized left hand sides +( + log(self.price / self.strike) + + (self.risk_free - self.div_cont + 0.5 * (self.sigma**2)) * self.exp_time +) / self.sigmaT ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap index b31b763052..c6f1534b31 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap @@ -257,8 +257,10 @@ if aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & ( pass if ( - not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + not ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ) & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ): pass From dc65007fe9ea9c65fee94d21a956c61bd1410d4d Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 30 Jun 2023 10:05:25 +0200 Subject: [PATCH 284/447] Use rayon to parallelize the stability check ## Summary This PR uses rayon to parallelize the stability check by scheduling each project as its own task. ## Test Plan I ran the ecosystem check. It now makes use of all cores (except at the end, there are some large projects). ## Performance The check now completes in minutes where it took about 30 minutes before. --- Cargo.lock | 1 + crates/ruff_dev/Cargo.toml | 1 + .../ruff_dev/src/check_formatter_stability.rs | 357 ++++++++++++------ 3 files changed, 252 insertions(+), 107 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c0a31643e0..9e7fbc656e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1983,6 +1983,7 @@ dependencies = [ "log", "once_cell", "pretty_assertions", + "rayon", "regex", "ruff", "ruff_cli", diff --git a/crates/ruff_dev/Cargo.toml b/crates/ruff_dev/Cargo.toml index cd84e8368e..07be9e5d72 100644 --- a/crates/ruff_dev/Cargo.toml +++ b/crates/ruff_dev/Cargo.toml @@ -26,6 +26,7 @@ log = { workspace = true } once_cell = { workspace = true } pretty_assertions = { version = "1.3.0" } regex = { workspace = true } +rayon = "1.7.0" rustpython-format = { workspace = true } rustpython-parser = { workspace = true } schemars = { workspace = true } diff --git a/crates/ruff_dev/src/check_formatter_stability.rs b/crates/ruff_dev/src/check_formatter_stability.rs index a499296fe5..95a37e6838 100644 --- a/crates/ruff_dev/src/check_formatter_stability.rs +++ b/crates/ruff_dev/src/check_formatter_stability.rs @@ -2,23 +2,27 @@ //! known as formatter stability or formatter idempotency, and that the formatter prints //! syntactically valid code. As our test cases cover only a limited amount of code, this allows //! checking entire repositories. -#![allow(clippy::print_stdout)] + +use std::fmt::{Display, Formatter}; +use std::io::stdout; +use std::io::Write; +use std::panic::catch_unwind; +use std::path::{Path, PathBuf}; +use std::process::ExitCode; +use std::sync::mpsc::channel; +use std::time::{Duration, Instant}; +use std::{fmt, fs, iter}; use anyhow::{bail, Context}; use clap::Parser; use log::debug; +use similar::{ChangeTag, TextDiff}; + use ruff::resolver::python_files_in_path; use ruff::settings::types::{FilePattern, FilePatternSet}; use ruff_cli::args::CheckArgs; use ruff_cli::resolve::resolve; use ruff_python_formatter::{format_module, PyFormatOptions}; -use similar::{ChangeTag, TextDiff}; -use std::io::Write; -use std::panic::catch_unwind; -use std::path::{Path, PathBuf}; -use std::process::ExitCode; -use std::time::Instant; -use std::{fs, io, iter}; /// Control the verbosity of the output #[derive(Copy, Clone, PartialEq, Eq, clap::ValueEnum, Default)] @@ -58,24 +62,16 @@ struct WrapperArgs { pub(crate) fn main(args: &Args) -> anyhow::Result { let all_success = if args.multi_project { - let mut all_success = true; - for base_dir in &args.files { - for dir in base_dir.read_dir()? { - let dir = dir?; - println!("Starting {}", dir.path().display()); - let success = check_repo(&Args { - files: vec![dir.path().clone()], - ..*args - }); - println!("Finished {}: {:?}", dir.path().display(), success); - if !matches!(success, Ok(true)) { - all_success = false; - } - } - } - all_success + check_multi_project(args) } else { - check_repo(args)? + let result = check_repo(args)?; + + #[allow(clippy::print_stdout)] + { + print!("{}", result.display(args.format)); + } + + result.is_success() }; if all_success { Ok(ExitCode::SUCCESS) @@ -84,8 +80,92 @@ pub(crate) fn main(args: &Args) -> anyhow::Result { } } +enum Message { + Start { + path: PathBuf, + }, + Failed { + path: PathBuf, + error: anyhow::Error, + }, + Finished { + path: PathBuf, + result: CheckRepoResult, + }, +} + +fn check_multi_project(args: &Args) -> bool { + let mut all_success = true; + let mut total_errors = 0; + let mut total_files = 0; + let start = Instant::now(); + + rayon::scope(|scope| { + let (sender, receiver) = channel(); + + for base_dir in &args.files { + for dir in base_dir.read_dir().unwrap() { + let path = dir.unwrap().path().clone(); + + let sender = sender.clone(); + + scope.spawn(move |_| { + sender.send(Message::Start { path: path.clone() }).unwrap(); + + match check_repo(&Args { + files: vec![path.clone()], + ..*args + }) { + Ok(result) => sender.send(Message::Finished { result, path }), + Err(error) => sender.send(Message::Failed { error, path }), + } + .unwrap(); + }); + } + } + + scope.spawn(|_| { + let mut stdout = stdout().lock(); + + for message in receiver { + match message { + Message::Start { path } => { + writeln!(stdout, "Starting {}", path.display()).unwrap(); + } + Message::Finished { path, result } => { + total_errors += result.diagnostics.len(); + total_files += result.file_count; + writeln!( + stdout, + "Finished {}\n{}\n", + path.display(), + result.display(args.format) + ) + .unwrap(); + all_success = all_success && result.is_success(); + } + Message::Failed { path, error } => { + writeln!(stdout, "Failed {}: {}", path.display(), error).unwrap(); + all_success = false; + } + } + } + }); + }); + + let duration = start.elapsed(); + + #[allow(clippy::print_stdout)] + { + println!("{total_errors} stability errors in {total_files} files"); + println!("Finished in {}s", duration.as_secs_f32()); + } + + all_success +} + /// Returns whether the check was successful -pub(crate) fn check_repo(args: &Args) -> anyhow::Result { +fn check_repo(args: &Args) -> anyhow::Result { let start = Instant::now(); // Find files to check (or in this case, format twice). Adapted from ruff_cli @@ -113,7 +193,7 @@ pub(crate) fn check_repo(args: &Args) -> anyhow::Result { } let mut formatted_counter = 0; - let errors = paths + let errors: Vec<_> = paths .into_iter() .map(|dir_entry| { // Doesn't make sense to recover here in this test script @@ -130,8 +210,14 @@ pub(crate) fn check_repo(args: &Args) -> anyhow::Result { let result = match catch_unwind(|| check_file(&file)) { Ok(result) => result, Err(panic) => { - if let Ok(message) = panic.downcast::() { - Err(FormatterStabilityError::Panic { message: *message }) + if let Some(message) = panic.downcast_ref::() { + Err(FormatterStabilityError::Panic { + message: message.clone(), + }) + } else if let Some(&message) = panic.downcast_ref::<&str>() { + Err(FormatterStabilityError::Panic { + message: message.to_string(), + }) } else { Err(FormatterStabilityError::Panic { // This should not happen, but it can @@ -144,94 +230,27 @@ pub(crate) fn check_repo(args: &Args) -> anyhow::Result { }) // We only care about the errors .filter_map(|(result, file)| match result { - Err(err) => Some((err, file)), + Err(error) => Some(Diagnostic { file, error }), Ok(()) => None, - }); + }) + .collect(); - let mut any_errors = false; - - // Don't collect the iterator so we already see errors while it's still processing - for (error, file) in errors { - any_errors = true; - match error { - FormatterStabilityError::Unstable { - formatted, - reformatted, - } => { - println!("Unstable formatting {}", file.display()); - match args.format { - Format::Minimal => {} - Format::Default => { - diff_show_only_changes( - io::stdout().lock().by_ref(), - &formatted, - &reformatted, - )?; - } - Format::Full => { - let diff = TextDiff::from_lines(&formatted, &reformatted) - .unified_diff() - .header("Formatted once", "Formatted twice") - .to_string(); - println!( - r#"Reformatting the formatted code a second time resulted in formatting changes. ---- -{diff}--- - -Formatted once: ---- -{formatted}--- - -Formatted twice: ---- -{reformatted}---"#, - ); - } - } - } - FormatterStabilityError::InvalidSyntax { err, formatted } => { - println!( - "Formatter generated invalid syntax {}: {}", - file.display(), - err - ); - if args.format == Format::Full { - println!("---\n{formatted}\n---\n"); - } - } - FormatterStabilityError::Panic { message } => { - println!("Panic {}: {}", file.display(), message); - } - FormatterStabilityError::Other(err) => { - println!("Uncategorized error {}: {}", file.display(), err); - } - } - - if args.exit_first_error { - return Ok(false); - } - } let duration = start.elapsed(); - println!( - "Formatting {} files twice took {:.2}s", - formatted_counter, - duration.as_secs_f32() - ); - if any_errors { - Ok(false) - } else { - Ok(true) - } + Ok(CheckRepoResult { + duration, + file_count: formatted_counter, + diagnostics: errors, + }) } /// A compact diff that only shows a header and changes, but nothing unchanged. This makes viewing /// multiple errors easier. fn diff_show_only_changes( - writer: &mut impl Write, + writer: &mut Formatter, formatted: &str, reformatted: &str, -) -> io::Result<()> { +) -> fmt::Result { for changes in TextDiff::from_lines(formatted, reformatted) .unified_diff() .iter_hunks() @@ -245,12 +264,136 @@ fn diff_show_only_changes( writeln!(writer, "{}", changes.header())?; } write!(writer, "{}", change.tag())?; - writer.write_all(change.value().as_bytes())?; + writer.write_str(change.value())?; } } Ok(()) } +struct CheckRepoResult { + duration: Duration, + file_count: usize, + diagnostics: Vec, +} + +impl CheckRepoResult { + fn display(&self, format: Format) -> DisplayCheckRepoResult { + DisplayCheckRepoResult { + result: self, + format, + } + } + + fn is_success(&self) -> bool { + self.diagnostics.is_empty() + } +} + +struct DisplayCheckRepoResult<'a> { + result: &'a CheckRepoResult, + format: Format, +} + +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 { + write!(f, "{}", diagnostic.display(self.format))?; + } + + writeln!( + f, + "Formatting {} files twice took {:.2}s", + file_count, + duration.as_secs_f32() + ) + } +} + +#[derive(Debug)] +struct Diagnostic { + file: PathBuf, + error: FormatterStabilityError, +} + +impl Diagnostic { + fn display(&self, format: Format) -> DisplayDiagnostic { + DisplayDiagnostic { + diagnostic: self, + format, + } + } +} + +struct DisplayDiagnostic<'a> { + format: Format, + diagnostic: &'a Diagnostic, +} + +impl Display for DisplayDiagnostic<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let Diagnostic { file, error } = &self.diagnostic; + + match error { + FormatterStabilityError::Unstable { + formatted, + reformatted, + } => { + writeln!(f, "Unstable formatting {}", file.display())?; + match self.format { + Format::Minimal => {} + Format::Default => { + diff_show_only_changes(f, formatted, reformatted)?; + } + Format::Full => { + let diff = TextDiff::from_lines(formatted.as_str(), reformatted.as_str()) + .unified_diff() + .header("Formatted once", "Formatted twice") + .to_string(); + writeln!( + f, + r#"Reformatting the formatted code a second time resulted in formatting changes. +--- +{diff}--- + +Formatted once: +--- +{formatted}--- + +Formatted twice: +--- +{reformatted}---\n"#, + )?; + } + } + } + FormatterStabilityError::InvalidSyntax { err, formatted } => { + writeln!( + f, + "Formatter generated invalid syntax {}: {}", + file.display(), + err + )?; + if self.format == Format::Full { + writeln!(f, "---\n{formatted}\n---\n")?; + } + } + FormatterStabilityError::Panic { message } => { + writeln!(f, "Panic {}: {}", file.display(), message)?; + } + FormatterStabilityError::Other(err) => { + writeln!(f, "Uncategorized error {}: {}", file.display(), err)?; + } + } + Ok(()) + } +} + #[derive(Debug)] enum FormatterStabilityError { /// First and second pass of the formatter are different From f9129e435a5402b7bf2ec3e91d7c27d6145666c0 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 30 Jun 2023 10:13:23 +0200 Subject: [PATCH 285/447] Normalize '\r' in string literals to '\n' ## Summary This PR normalizes line endings inside of strings to `\n` as required by the printer. ## Test Plan I added a new test using `\r\n` and ran the ecosystem check. There are no remaining end of line panics. https://gist.github.com/MichaReiser/8f36b1391ca7b48475b3a4f592d74ff4 --- .../ruff/carriage_return/.editorconfig | 2 + .../ruff/carriage_return/.gitattributes | 1 + .../fixtures/ruff/carriage_return/string.py | 6 ++ .../src/expression/string.rs | 99 +++++++++++-------- .../format@carriage_return__string.py.snap | 26 +++++ 5 files changed, 93 insertions(+), 41 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.editorconfig create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.gitattributes create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/string.py create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@carriage_return__string.py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.editorconfig b/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.editorconfig new file mode 100644 index 0000000000..cafa748cf3 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.editorconfig @@ -0,0 +1,2 @@ +[*.py] +end_of_line = crlf diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.gitattributes b/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.gitattributes new file mode 100644 index 0000000000..0c42f3cc29 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=crlf diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/string.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/string.py new file mode 100644 index 0000000000..45f9dacc38 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/string.py @@ -0,0 +1,6 @@ +'This string will not include \ +backslashes or newline characters.' + +"""Multiline +String \" +""" diff --git a/crates/ruff_python_formatter/src/expression/string.rs b/crates/ruff_python_formatter/src/expression/string.rs index 0362aff005..7b8369d088 100644 --- a/crates/ruff_python_formatter/src/expression/string.rs +++ b/crates/ruff_python_formatter/src/expression/string.rs @@ -199,12 +199,11 @@ impl Format> for FormatStringPart { let raw_content_range = relative_raw_content_range + self.part_range.start(); let raw_content = &string_content[relative_raw_content_range]; - let (preferred_quotes, contains_newlines) = - preferred_quotes(raw_content, quotes, f.options().quote_style()); + let preferred_quotes = preferred_quotes(raw_content, quotes, f.options().quote_style()); write!(f, [prefix, preferred_quotes])?; - let normalized = normalize_quotes(raw_content, preferred_quotes); + let (normalized, contains_newlines) = normalize_string(raw_content, preferred_quotes); match normalized { Cow::Borrowed(_) => { @@ -294,9 +293,7 @@ fn preferred_quotes( input: &str, quotes: StringQuotes, configured_style: QuoteStyle, -) -> (StringQuotes, ContainsNewlines) { - let mut contains_newlines = ContainsNewlines::No; - +) -> StringQuotes { let preferred_style = if quotes.triple { // True if the string contains a triple quote sequence of the configured quote style. let mut uses_triple_quotes = false; @@ -305,7 +302,6 @@ fn preferred_quotes( while let Some(c) = chars.next() { let configured_quote_char = configured_style.as_char(); match c { - '\n' | '\r' => contains_newlines = ContainsNewlines::Yes, '\\' => { if matches!(chars.peek(), Some('"' | '\\')) { chars.next(); @@ -358,10 +354,6 @@ fn preferred_quotes( double_quotes += 1; } - '\n' | '\r' => { - contains_newlines = ContainsNewlines::Yes; - } - _ => continue, } } @@ -384,13 +376,10 @@ fn preferred_quotes( } }; - ( - StringQuotes { - triple: quotes.triple, - style: preferred_style, - }, - contains_newlines, - ) + StringQuotes { + triple: quotes.triple, + style: preferred_style, + } } #[derive(Copy, Clone, Debug)] @@ -435,30 +424,56 @@ impl Format> for StringQuotes { /// Adds the necessary quote escapes and removes unnecessary escape sequences when quoting `input` /// with the provided `style`. -fn normalize_quotes(input: &str, quotes: StringQuotes) -> Cow { - if quotes.triple { - Cow::Borrowed(input) - } else { - // The normalized string if `input` is not yet normalized. - // `output` must remain empty if `input` is already normalized. - let mut output = String::new(); - // Tracks the last index of `input` that has been written to `output`. - // If `last_index` is `0` at the end, then the input is already normalized and can be returned as is. - let mut last_index = 0; +/// +/// Returns the normalized string and whether it contains new lines. +fn normalize_string(input: &str, quotes: StringQuotes) -> (Cow, ContainsNewlines) { + // The normalized string if `input` is not yet normalized. + // `output` must remain empty if `input` is already normalized. + let mut output = String::new(); + // Tracks the last index of `input` that has been written to `output`. + // If `last_index` is `0` at the end, then the input is already normalized and can be returned as is. + let mut last_index = 0; - let style = quotes.style; - let preferred_quote = style.as_char(); - let opposite_quote = style.invert().as_char(); + let mut newlines = ContainsNewlines::No; - let mut chars = input.char_indices(); + let style = quotes.style; + let preferred_quote = style.as_char(); + let opposite_quote = style.invert().as_char(); - while let Some((index, c)) = chars.next() { + let mut chars = input.char_indices(); + + while let Some((index, c)) = chars.next() { + if c == '\r' { + output.push_str(&input[last_index..index]); + + // Skip over the '\r' character, keep the `\n` + if input.as_bytes().get(index + 1).copied() == Some(b'\n') { + chars.next(); + } + // Replace the `\r` with a `\n` + else { + output.push('\n'); + } + + last_index = index + '\r'.len_utf8(); + newlines = ContainsNewlines::Yes; + } else if c == '\n' { + newlines = ContainsNewlines::Yes; + } else if !quotes.triple { if c == '\\' { - if let Some((_, next)) = chars.next() { + if let Some(next) = input.as_bytes().get(index + 1).copied().map(char::from) { + #[allow(clippy::if_same_then_else)] if next == opposite_quote { // Remove the escape by ending before the backslash and starting again with the quote + chars.next(); output.push_str(&input[last_index..index]); last_index = index + '\\'.len_utf8(); + } else if next == preferred_quote { + // Quote is already escaped, skip over it. + chars.next(); + } else if next == '\\' { + // Skip over escaped backslashes + chars.next(); } } } else if c == preferred_quote { @@ -469,12 +484,14 @@ fn normalize_quotes(input: &str, quotes: StringQuotes) -> Cow { last_index = index + preferred_quote.len_utf8(); } } - - if last_index == 0 { - Cow::Borrowed(input) - } else { - output.push_str(&input[last_index..]); - Cow::Owned(output) - } } + + let normalized = if last_index == 0 { + Cow::Borrowed(input) + } else { + output.push_str(&input[last_index..]); + Cow::Owned(output) + }; + + (normalized, newlines) } diff --git a/crates/ruff_python_formatter/tests/snapshots/format@carriage_return__string.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@carriage_return__string.py.snap new file mode 100644 index 0000000000..522538378a --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@carriage_return__string.py.snap @@ -0,0 +1,26 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/string.py +--- +## Input +```py +'This string will not include \ +backslashes or newline characters.' + +"""Multiline +String \" +""" +``` + +## Output +```py +"This string will not include \ +backslashes or newline characters." + +"""Multiline +String \" +""" +``` + + + From f0ec9ecd67e1423acd9319107bb866c43667b3c5 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 30 Jun 2023 11:49:00 +0200 Subject: [PATCH 286/447] Show `BestFitting` mode if it isn't `FirstLine` (#5452) --- crates/ruff_formatter/src/format_element/document.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_formatter/src/format_element/document.rs b/crates/ruff_formatter/src/format_element/document.rs index 9799511fc5..68baa0558e 100644 --- a/crates/ruff_formatter/src/format_element/document.rs +++ b/crates/ruff_formatter/src/format_element/document.rs @@ -296,7 +296,7 @@ impl Format> for &[FormatElement] { FormatElement::Line(LineMode::Hard), ])?; - if *mode != BestFittingMode::AllLines { + if *mode != BestFittingMode::FirstLine { write!( f, [ From af7051b976e9d4a266de0d600f2f06908f5d75f6 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 2 Jul 2023 20:18:33 -0400 Subject: [PATCH 287/447] 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 288/447] 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 289/447] [`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 290/447] [`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 291/447] [`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 292/447] 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 293/447] [`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 294/447] [`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 295/447] 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 296/447] 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 297/447] 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 298/447] 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 299/447] 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 300/447] 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 301/447] 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 302/447] 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 303/447] 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 304/447] 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 305/447] 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 306/447] 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 307/447] 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 308/447] 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 309/447] 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 310/447] 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 311/447] 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 312/447] 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 313/447] 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" + ] } ] }, From c395e44bd761b34cab03e4d811af332ee0a2d49a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 4 Jul 2023 14:21:05 -0400 Subject: [PATCH 314/447] Avoid PERF rules for iteration-dependent assignments (#5508) ## Summary We need to avoid raising "rewrite as a comprehension" violations in cases like: ```python d = defaultdict(list) for i in [1, 2, 3]: d[i].append(i**2) ``` Closes https://github.com/astral-sh/ruff/issues/5494. Closes https://github.com/astral-sh/ruff/issues/5500. --- .../test/fixtures/perflint/PERF401.py | 7 ++++ .../test/fixtures/perflint/PERF402.py | 7 ++++ .../rules/manual_list_comprehension.rs | 30 ++++++++++------ .../rules/perflint/rules/manual_list_copy.rs | 36 ++++++++++++------- 4 files changed, 57 insertions(+), 23 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/perflint/PERF401.py b/crates/ruff/resources/test/fixtures/perflint/PERF401.py index beb3d4546c..12b084e2f6 100644 --- a/crates/ruff/resources/test/fixtures/perflint/PERF401.py +++ b/crates/ruff/resources/test/fixtures/perflint/PERF401.py @@ -30,3 +30,10 @@ def f(): result = [] for i in items: result.append(i) # OK + + +def f(): + items = [1, 2, 3, 4] + result = {} + for i in items: + result[i].append(i) # OK diff --git a/crates/ruff/resources/test/fixtures/perflint/PERF402.py b/crates/ruff/resources/test/fixtures/perflint/PERF402.py index 4db9a3dc52..55f3e08cbc 100644 --- a/crates/ruff/resources/test/fixtures/perflint/PERF402.py +++ b/crates/ruff/resources/test/fixtures/perflint/PERF402.py @@ -17,3 +17,10 @@ def f(): result = [] for i in items: result.append(i * i) # OK + + +def f(): + items = [1, 2, 3, 4] + result = {} + for i in items: + result[i].append(i * i) # OK 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 eb6e56735b..7e9607d591 100644 --- a/crates/ruff/src/rules/perflint/rules/manual_list_comprehension.rs +++ b/crates/ruff/src/rules/perflint/rules/manual_list_comprehension.rs @@ -2,6 +2,7 @@ use rustpython_parser::ast::{self, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::any_over_expr; use crate::checkers::ast::Checker; @@ -52,6 +53,10 @@ impl Violation for ManualListComprehension { /// PERF401 pub(crate) fn manual_list_comprehension(checker: &mut Checker, target: &Expr, body: &[Stmt]) { + let Expr::Name(ast::ExprName { id, .. }) = target else { + return; + }; + let (stmt, conditional) = match body { // ```python // for x in y: @@ -99,22 +104,27 @@ pub(crate) fn manual_list_comprehension(checker: &mut Checker, target: &Expr, bo // 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) - }) { + if arg.as_name_expr().map_or(false, |arg| arg.id == *id) { return; } } - let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() else { + let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func.as_ref() else { return; }; - if attr.as_str() == "append" { - checker - .diagnostics - .push(Diagnostic::new(ManualListComprehension, *range)); + if attr.as_str() != "append" { + return; } + + // Avoid, e.g., `for x in y: filtered[x].append(x * x)`. + if any_over_expr(value, &|expr| { + expr.as_name_expr().map_or(false, |expr| expr.id == *id) + }) { + return; + } + + 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 13554488bd..3752c4eb1c 100644 --- a/crates/ruff/src/rules/perflint/rules/manual_list_copy.rs +++ b/crates/ruff/src/rules/perflint/rules/manual_list_copy.rs @@ -2,6 +2,7 @@ use rustpython_parser::ast::{self, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::any_over_expr; use crate::checkers::ast::Checker; @@ -45,6 +46,10 @@ impl Violation for ManualListCopy { /// PERF402 pub(crate) fn manual_list_copy(checker: &mut Checker, target: &Expr, body: &[Stmt]) { + let Expr::Name(ast::ExprName { id, .. }) = target else { + return; + }; + let [stmt] = body else { return; }; @@ -72,21 +77,26 @@ pub(crate) fn manual_list_copy(checker: &mut Checker, target: &Expr, body: &[Stm }; // 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) + if !arg.as_name_expr().map_or(false, |arg| arg.id == *id) { + return; + } + + let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func.as_ref() else { + return; + }; + + if !matches!(attr.as_str(), "append" | "insert") { + return; + } + + // Avoid, e.g., `for x in y: filtered[x].append(x * x)`. + if any_over_expr(value, &|expr| { + expr.as_name_expr().map_or(false, |expr| expr.id == *id) }) { 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)); - } + checker + .diagnostics + .push(Diagnostic::new(ManualListCopy, *range)); } From 0e67757edbbf28c3a49dcc92c5844c1f432d1b51 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Tue, 4 Jul 2023 19:49:43 +0100 Subject: [PATCH 315/447] [`pylint`] Implement Pylint `typevar-name-mismatch` (`C0132`) (#5501) ## Summary Implement Pylint `typevar-name-mismatch` (`C0132`) as `type-param-name-mismatch` (`PLC0132`). Includes documentation. Related to #970. The Pylint implementation checks only `TypeVar`, but this PR checks `TypeVarTuple`, `ParamSpec`, and `NewType` as well. This seems to better represent the Pylint rule's [intended behaviour](https://github.com/pylint-dev/pylint/issues/5224). Full disclosure: I am not a fan of the translated name and think it should probably be different. ## Test Plan `cargo test` --- .../pylint/type_param_name_mismatch.py | 56 ++++++ crates/ruff/src/checkers/ast/mod.rs | 3 + crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/pylint/mod.rs | 1 + crates/ruff/src/rules/pylint/rules/mod.rs | 2 + .../pylint/rules/type_param_name_mismatch.rs | 166 ++++++++++++++++++ ...__PLC0132_type_param_name_mismatch.py.snap | 76 ++++++++ ruff.schema.json | 3 + 8 files changed, 308 insertions(+) create mode 100644 crates/ruff/resources/test/fixtures/pylint/type_param_name_mismatch.py create mode 100644 crates/ruff/src/rules/pylint/rules/type_param_name_mismatch.rs create mode 100644 crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0132_type_param_name_mismatch.py.snap diff --git a/crates/ruff/resources/test/fixtures/pylint/type_param_name_mismatch.py b/crates/ruff/resources/test/fixtures/pylint/type_param_name_mismatch.py new file mode 100644 index 0000000000..267ae9a38b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pylint/type_param_name_mismatch.py @@ -0,0 +1,56 @@ +from typing import TypeVar, ParamSpec, NewType, TypeVarTuple + +# Errors. + +X = TypeVar("T") +X = TypeVar(name="T") + +Y = ParamSpec("T") +Y = ParamSpec(name="T") + +Z = NewType("T", int) +Z = NewType(name="T", tp=int) + +Ws = TypeVarTuple("Ts") +Ws = TypeVarTuple(name="Ts") + +# Non-errors. + +T = TypeVar("T") +T = TypeVar(name="T") + +T = ParamSpec("T") +T = ParamSpec(name="T") + +T = NewType("T", int) +T = NewType(name="T", tp=int) + +Ts = TypeVarTuple("Ts") +Ts = TypeVarTuple(name="Ts") + +# Errors, but not covered by this rule. + +# Non-string literal name. +T = TypeVar(some_str) +T = TypeVar(name=some_str) +T = TypeVar(1) +T = TypeVar(name=1) +T = ParamSpec(some_str) +T = ParamSpec(name=some_str) +T = ParamSpec(1) +T = ParamSpec(name=1) +T = NewType(some_str, int) +T = NewType(name=some_str, tp=int) +T = NewType(1, int) +T = NewType(name=1, tp=int) +Ts = TypeVarTuple(some_str) +Ts = TypeVarTuple(name=some_str) +Ts = TypeVarTuple(1) +Ts = TypeVarTuple(name=1) + +# No names provided. +T = TypeVar() +T = ParamSpec() +T = NewType() +T = NewType(tp=int) +Ts = TypeVarTuple() diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 67752031eb..1ed61170dd 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1655,6 +1655,9 @@ where self.diagnostics.push(diagnostic); } } + if self.settings.rules.enabled(Rule::TypeParamNameMismatch) { + pylint::rules::type_param_name_mismatch(self, value, targets); + } if self.is_stub { if self.any_enabled(&[ Rule::UnprefixedTypeParam, diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 5e526f599d..f39106a745 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -156,6 +156,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pyflakes, "901") => (RuleGroup::Unspecified, rules::pyflakes::rules::RaiseNotImplemented), // pylint + (Pylint, "C0132") => (RuleGroup::Unspecified, rules::pylint::rules::TypeParamNameMismatch), (Pylint, "C0205") => (RuleGroup::Unspecified, rules::pylint::rules::SingleStringSlots), (Pylint, "C0414") => (RuleGroup::Unspecified, rules::pylint::rules::UselessImportAlias), (Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString), diff --git a/crates/ruff/src/rules/pylint/mod.rs b/crates/ruff/src/rules/pylint/mod.rs index 4f5a5b2f6a..c1bb1ae591 100644 --- a/crates/ruff/src/rules/pylint/mod.rs +++ b/crates/ruff/src/rules/pylint/mod.rs @@ -85,6 +85,7 @@ mod tests { Path::new("too_many_return_statements.py") )] #[test_case(Rule::TooManyStatements, Path::new("too_many_statements.py"))] + #[test_case(Rule::TypeParamNameMismatch, Path::new("type_param_name_mismatch.py"))] #[test_case( Rule::UnexpectedSpecialMethodSignature, Path::new("unexpected_special_method_signature.py") diff --git a/crates/ruff/src/rules/pylint/rules/mod.rs b/crates/ruff/src/rules/pylint/rules/mod.rs index 84251fb061..431c3fd170 100644 --- a/crates/ruff/src/rules/pylint/rules/mod.rs +++ b/crates/ruff/src/rules/pylint/rules/mod.rs @@ -37,6 +37,7 @@ pub(crate) use too_many_arguments::*; pub(crate) use too_many_branches::*; pub(crate) use too_many_return_statements::*; pub(crate) use too_many_statements::*; +pub(crate) use type_param_name_mismatch::*; pub(crate) use unexpected_special_method_signature::*; pub(crate) use unnecessary_direct_lambda_call::*; pub(crate) use useless_else_on_loop::*; @@ -84,6 +85,7 @@ mod too_many_arguments; mod too_many_branches; mod too_many_return_statements; mod too_many_statements; +mod type_param_name_mismatch; mod unexpected_special_method_signature; mod unnecessary_direct_lambda_call; mod useless_else_on_loop; diff --git a/crates/ruff/src/rules/pylint/rules/type_param_name_mismatch.rs b/crates/ruff/src/rules/pylint/rules/type_param_name_mismatch.rs new file mode 100644 index 0000000000..c7020475ff --- /dev/null +++ b/crates/ruff/src/rules/pylint/rules/type_param_name_mismatch.rs @@ -0,0 +1,166 @@ +use std::fmt; + +use rustpython_parser::ast::{self, Constant, Expr, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for `TypeVar`, `TypeVarTuple`, `ParamSpec`, and `NewType` +/// definitions in which the name of the type parameter does not match the name +/// of the variable to which it is assigned. +/// +/// ## Why is this bad? +/// When defining a `TypeVar` or a related type parameter, Python allows you to +/// provide a name for the type parameter. According to [PEP 484], the name +/// provided to the `TypeVar` constructor must be equal to the name of the +/// variable to which it is assigned. +/// +/// ## Example +/// ```python +/// from typing import TypeVar +/// +/// T = TypeVar("U") +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import TypeVar +/// +/// T = TypeVar("T") +/// ``` +/// +/// ## References +/// - [Python documentation: `typing` — Support for type hints](https://docs.python.org/3/library/typing.html) +/// - [PEP 484 – Type Hints: Generics](https://peps.python.org/pep-0484/#generics) +/// +/// [PEP 484]:https://peps.python.org/pep-0484/#generics +#[violation] +pub struct TypeParamNameMismatch { + kind: VarKind, + var_name: String, + param_name: String, +} + +impl Violation for TypeParamNameMismatch { + #[derive_message_formats] + fn message(&self) -> String { + let TypeParamNameMismatch { + kind, + var_name, + param_name, + } = self; + format!("`{kind}` name `{param_name}` does not match assigned variable name `{var_name}`") + } +} + +/// PLC0132 +pub(crate) fn type_param_name_mismatch(checker: &mut Checker, value: &Expr, targets: &[Expr]) { + let [target] = targets else { + return; + }; + + let Expr::Name(ast::ExprName { id: var_name, .. }) = &target else { + return; + }; + + let Some(param_name) = param_name(value) else { + return; + }; + + if var_name == param_name { + return; + } + + let Expr::Call(ast::ExprCall { func, .. }) = value else { + return; + }; + + 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 if checker + .semantic() + .match_typing_call_path(&call_path, "NewType") + { + Some(VarKind::NewType) + } else { + None + } + }) + else { + return; + }; + + checker.diagnostics.push(Diagnostic::new( + TypeParamNameMismatch { + kind, + var_name: var_name.to_string(), + param_name: param_name.to_string(), + }, + value.range(), + )); +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum VarKind { + TypeVar, + ParamSpec, + TypeVarTuple, + NewType, +} + +impl fmt::Display for VarKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + VarKind::TypeVar => fmt.write_str("TypeVar"), + VarKind::ParamSpec => fmt.write_str("ParamSpec"), + VarKind::TypeVarTuple => fmt.write_str("TypeVarTuple"), + VarKind::NewType => fmt.write_str("NewType"), + } + } +} + +/// Returns the value of the `name` parameter to, e.g., a `TypeVar` constructor. +fn param_name(value: &Expr) -> Option<&str> { + // Handle both `TypeVar("T")` and `TypeVar(name="T")`. + let call = value.as_call_expr()?; + let name_param = call + .keywords + .iter() + .find(|keyword| { + keyword + .arg + .as_ref() + .map_or(false, |keyword| keyword.as_str() == "name") + }) + .map(|keyword| &keyword.value) + .or_else(|| call.args.get(0))?; + if let Expr::Constant(ast::ExprConstant { + value: Constant::Str(name), + .. + }) = &name_param + { + Some(name) + } else { + None + } +} diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0132_type_param_name_mismatch.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0132_type_param_name_mismatch.py.snap new file mode 100644 index 0000000000..44084245c2 --- /dev/null +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0132_type_param_name_mismatch.py.snap @@ -0,0 +1,76 @@ +--- +source: crates/ruff/src/rules/pylint/mod.rs +--- +type_param_name_mismatch.py:5:5: PLC0132 `TypeVar` name `T` does not match assigned variable name `X` + | +3 | # Errors. +4 | +5 | X = TypeVar("T") + | ^^^^^^^^^^^^ PLC0132 +6 | X = TypeVar(name="T") + | + +type_param_name_mismatch.py:6:5: PLC0132 `TypeVar` name `T` does not match assigned variable name `X` + | +5 | X = TypeVar("T") +6 | X = TypeVar(name="T") + | ^^^^^^^^^^^^^^^^^ PLC0132 +7 | +8 | Y = ParamSpec("T") + | + +type_param_name_mismatch.py:8:5: PLC0132 `ParamSpec` name `T` does not match assigned variable name `Y` + | +6 | X = TypeVar(name="T") +7 | +8 | Y = ParamSpec("T") + | ^^^^^^^^^^^^^^ PLC0132 +9 | Y = ParamSpec(name="T") + | + +type_param_name_mismatch.py:9:5: PLC0132 `ParamSpec` name `T` does not match assigned variable name `Y` + | + 8 | Y = ParamSpec("T") + 9 | Y = ParamSpec(name="T") + | ^^^^^^^^^^^^^^^^^^^ PLC0132 +10 | +11 | Z = NewType("T", int) + | + +type_param_name_mismatch.py:11:5: PLC0132 `NewType` name `T` does not match assigned variable name `Z` + | + 9 | Y = ParamSpec(name="T") +10 | +11 | Z = NewType("T", int) + | ^^^^^^^^^^^^^^^^^ PLC0132 +12 | Z = NewType(name="T", tp=int) + | + +type_param_name_mismatch.py:12:5: PLC0132 `NewType` name `T` does not match assigned variable name `Z` + | +11 | Z = NewType("T", int) +12 | Z = NewType(name="T", tp=int) + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0132 +13 | +14 | Ws = TypeVarTuple("Ts") + | + +type_param_name_mismatch.py:14:6: PLC0132 `TypeVarTuple` name `Ts` does not match assigned variable name `Ws` + | +12 | Z = NewType(name="T", tp=int) +13 | +14 | Ws = TypeVarTuple("Ts") + | ^^^^^^^^^^^^^^^^^^ PLC0132 +15 | Ws = TypeVarTuple(name="Ts") + | + +type_param_name_mismatch.py:15:6: PLC0132 `TypeVarTuple` name `Ts` does not match assigned variable name `Ws` + | +14 | Ws = TypeVarTuple("Ts") +15 | Ws = TypeVarTuple(name="Ts") + | ^^^^^^^^^^^^^^^^^^^^^^^ PLC0132 +16 | +17 | # Non-errors. + | + + diff --git a/ruff.schema.json b/ruff.schema.json index 064c0f7ec9..3b2252d987 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2121,6 +2121,9 @@ "PL", "PLC", "PLC0", + "PLC01", + "PLC013", + "PLC0132", "PLC02", "PLC020", "PLC0205", From 0a2620164350d8bd88d0ec1d1558c52fd8df4889 Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 4 Jul 2023 21:22:00 +0200 Subject: [PATCH 316/447] Merge clippy and clippy (wasm) jobs on CI (#5447) ## Summary The clippy wasm job rarely fails if regular clippy doesn't, wasm clippy still compiles a lot of native dependencies for the proc macro and we have less CI jobs overall, so i think this an improvement to our CI. ```shell $ CARGO_TARGET_DIR=target-wasm cargo clippy -p ruff_wasm --target wasm32-unknown-unknown --all-features -j 2 -- -D warnings $ du -sh target-wasm/* 12K target-wasm/CACHEDIR.TAG 582M target-wasm/debug 268M target-wasm/wasm32-unknown-unknown ``` ## Test plan n/a --- .github/workflows/ci.yaml | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a7a5543ce0..54db12d04d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -31,17 +31,6 @@ jobs: cargo-clippy: name: "cargo clippy" runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: "Install Rust toolchain" - run: | - rustup component add clippy - - uses: Swatinem/rust-cache@v2 - - run: cargo clippy --workspace --all-targets --all-features -- -D warnings - - cargo-clippy-wasm: - name: "cargo clippy (wasm)" - runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: "Install Rust toolchain" @@ -49,7 +38,10 @@ jobs: rustup component add clippy rustup target add wasm32-unknown-unknown - uses: Swatinem/rust-cache@v2 - - run: cargo clippy -p ruff_wasm --target wasm32-unknown-unknown --all-features -- -D warnings + - name: "Clippy" + run: cargo clippy --workspace --all-targets --all-features -- -D warnings + - name: "Clippy (wasm)" + run: cargo clippy -p ruff_wasm --target wasm32-unknown-unknown --all-features -- -D warnings cargo-test: strategy: From 952c62310238d8bd7bb9cb42099d7ea785c0baef Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 4 Jul 2023 15:23:05 -0400 Subject: [PATCH 317/447] Avoid returning first-match for rule prefixes (#5511) Closes #5495, but there's a TODO here to improve this further. The current `from_code` implementation feels really indirect. --- crates/ruff/src/registry.rs | 10 +++++++++- crates/ruff_cli/src/commands/rule.rs | 1 - 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index d53cd6ac7e..d1d9014bdf 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -19,7 +19,13 @@ 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.rules().next().unwrap()) + let rule = prefix.rules().next().unwrap(); + // TODO(charlie): Add a method to return an individual code, rather than matching on the + // prefix. + if rule.noqa_code().to_string() != format!("{}{}", linter.common_prefix(), code) { + return Err(FromCodeError::Prefix); + } + Ok(rule) } } @@ -27,6 +33,8 @@ impl Rule { pub enum FromCodeError { #[error("unknown rule code")] Unknown, + #[error("expected a rule code (like `SIM101`), not a prefix (like `SIM` or `SIM1`)")] + Prefix, } #[derive(EnumIter, Debug, PartialEq, Eq, Clone, Hash, RuleNamespace)] diff --git a/crates/ruff_cli/src/commands/rule.rs b/crates/ruff_cli/src/commands/rule.rs index ddb8e4ff21..9cd41e7bb2 100644 --- a/crates/ruff_cli/src/commands/rule.rs +++ b/crates/ruff_cli/src/commands/rule.rs @@ -31,7 +31,6 @@ pub(crate) fn rule(rule: Rule, format: HelpFormat) -> Result<()> { output.push('\n'); output.push('\n'); - let (linter, _) = Linter::parse_code(&rule.noqa_code().to_string()).unwrap(); output.push_str(&format!("Derived from the **{}** linter.", linter.name())); output.push('\n'); output.push('\n'); From d7214e77e69c731e74f4e7cfe8d063aab9f8ebf8 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 4 Jul 2023 22:45:38 +0300 Subject: [PATCH 318/447] Add `ruff rule --all` subcommand (with JSON output) (#5059) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This adds a `ruff rule --all` switch that prints out a human-readable Markdown or a machine-readable JSON document of the lint rules known to Ruff. I needed a machine-readable document of the rules [for a project](https://github.com/astral-sh/ruff/discussions/5078), and figured it could be useful for other people – or tooling! – to be able to interrogate Ruff about its arcane knowledge. The JSON output is an array of the same objects printed by `ruff rule --format=json`. ## Test Plan I ran `ruff rule --all --format=json`. I think more might be needed, but maybe a snapshot test is overkill? --- crates/ruff_cli/src/args.rs | 12 ++- crates/ruff_cli/src/commands/rule.rs | 140 +++++++++++++++++---------- crates/ruff_cli/src/lib.rs | 9 +- docs/configuration.md | 2 +- 4 files changed, 106 insertions(+), 57 deletions(-) diff --git a/crates/ruff_cli/src/args.rs b/crates/ruff_cli/src/args.rs index 0199d2f552..1050aafcbf 100644 --- a/crates/ruff_cli/src/args.rs +++ b/crates/ruff_cli/src/args.rs @@ -35,11 +35,17 @@ pub struct Args { pub enum Command { /// Run Ruff on the given files or directories (default). Check(CheckArgs), - /// Explain a rule. + /// Explain a rule (or all rules). #[clap(alias = "--explain")] + #[command(group = clap::ArgGroup::new("selector").multiple(false).required(true))] Rule { - #[arg(value_parser=Rule::from_code)] - rule: Rule, + /// Rule to explain + #[arg(value_parser=Rule::from_code, group = "selector")] + rule: Option, + + /// Explain all rules + #[arg(long, conflicts_with = "rule", group = "selector")] + all: bool, /// Output format #[arg(long, value_enum, default_value = "text")] diff --git a/crates/ruff_cli/src/commands/rule.rs b/crates/ruff_cli/src/commands/rule.rs index 9cd41e7bb2..a16ec4622b 100644 --- a/crates/ruff_cli/src/commands/rule.rs +++ b/crates/ruff_cli/src/commands/rule.rs @@ -1,7 +1,9 @@ use std::io::{self, BufWriter, Write}; use anyhow::Result; -use serde::Serialize; +use serde::ser::SerializeSeq; +use serde::{Serialize, Serializer}; +use strum::IntoEnumIterator; use ruff::registry::{Linter, Rule, RuleNamespace}; use ruff_diagnostics::AutofixKind; @@ -11,72 +13,106 @@ use crate::args::HelpFormat; #[derive(Serialize)] struct Explanation<'a> { name: &'a str, - code: &'a str, + code: String, linter: &'a str, summary: &'a str, message_formats: &'a [&'a str], - autofix: &'a str, + autofix: String, explanation: Option<&'a str>, + nursery: bool, +} + +impl<'a> Explanation<'a> { + fn from_rule(rule: &'a Rule) -> Self { + let code = rule.noqa_code().to_string(); + let (linter, _) = Linter::parse_code(&code).unwrap(); + let autofix = rule.autofixable().to_string(); + Self { + name: rule.as_ref(), + code, + linter: linter.name(), + summary: rule.message_formats()[0], + message_formats: rule.message_formats(), + autofix, + explanation: rule.explanation(), + nursery: rule.is_nursery(), + } + } +} + +fn format_rule_text(rule: Rule) -> String { + let mut output = String::new(); + output.push_str(&format!("# {} ({})", rule.as_ref(), rule.noqa_code())); + output.push('\n'); + output.push('\n'); + + let (linter, _) = Linter::parse_code(&rule.noqa_code().to_string()).unwrap(); + output.push_str(&format!("Derived from the **{}** linter.", linter.name())); + output.push('\n'); + output.push('\n'); + + let autofix = rule.autofixable(); + if matches!(autofix, AutofixKind::Always | AutofixKind::Sometimes) { + output.push_str(&autofix.to_string()); + output.push('\n'); + output.push('\n'); + } + + if rule.is_nursery() { + output.push_str(&format!( + r#"This rule is part of the **nursery**, a collection of newer lints that are +still under development. As such, it must be enabled by explicitly selecting +{}."#, + rule.noqa_code() + )); + output.push('\n'); + output.push('\n'); + } + + if let Some(explanation) = rule.explanation() { + output.push_str(explanation.trim()); + } else { + output.push_str("Message formats:"); + for format in rule.message_formats() { + output.push('\n'); + output.push_str(&format!("* {format}")); + } + } + output } /// Explain a `Rule` to the user. pub(crate) fn rule(rule: Rule, format: HelpFormat) -> Result<()> { - let (linter, _) = Linter::parse_code(&rule.noqa_code().to_string()).unwrap(); let mut stdout = BufWriter::new(io::stdout().lock()); - let mut output = String::new(); - match format { HelpFormat::Text => { - output.push_str(&format!("# {} ({})", rule.as_ref(), rule.noqa_code())); - output.push('\n'); - output.push('\n'); + writeln!(stdout, "{}", format_rule_text(rule))?; + } + HelpFormat::Json => { + serde_json::to_writer_pretty(stdout, &Explanation::from_rule(&rule))?; + } + }; + Ok(()) +} - output.push_str(&format!("Derived from the **{}** linter.", linter.name())); - output.push('\n'); - output.push('\n'); - - let autofix = rule.autofixable(); - if matches!(autofix, AutofixKind::Always | AutofixKind::Sometimes) { - output.push_str(&autofix.to_string()); - output.push('\n'); - output.push('\n'); - } - - if rule.is_nursery() { - output.push_str(&format!( - r#"This rule is part of the **nursery**, a collection of newer lints that are -still under development. As such, it must be enabled by explicitly selecting -{}."#, - rule.noqa_code() - )); - output.push('\n'); - output.push('\n'); - } - - if let Some(explanation) = rule.explanation() { - output.push_str(explanation.trim()); - } else { - output.push_str("Message formats:"); - for format in rule.message_formats() { - output.push('\n'); - output.push_str(&format!("* {format}")); - } +/// Explain all rules to the user. +pub(crate) fn rules(format: HelpFormat) -> Result<()> { + let mut stdout = BufWriter::new(io::stdout().lock()); + match format { + HelpFormat::Text => { + for rule in Rule::iter() { + writeln!(stdout, "{}", format_rule_text(rule))?; + writeln!(stdout)?; } } HelpFormat::Json => { - output.push_str(&serde_json::to_string_pretty(&Explanation { - name: rule.as_ref(), - code: &rule.noqa_code().to_string(), - linter: linter.name(), - summary: rule.message_formats()[0], - message_formats: rule.message_formats(), - autofix: &rule.autofixable().to_string(), - explanation: rule.explanation(), - })?); + let mut serializer = serde_json::Serializer::pretty(stdout); + let mut seq = serializer.serialize_seq(None)?; + for rule in Rule::iter() { + seq.serialize_element(&Explanation::from_rule(&rule))?; + } + seq.end()?; } - }; - - writeln!(stdout, "{output}")?; - + } Ok(()) } diff --git a/crates/ruff_cli/src/lib.rs b/crates/ruff_cli/src/lib.rs index 90d705b286..edfb43f5de 100644 --- a/crates/ruff_cli/src/lib.rs +++ b/crates/ruff_cli/src/lib.rs @@ -134,7 +134,14 @@ quoting the executed command, along with the relevant file contents and `pyproje set_up_logging(&log_level)?; match command { - Command::Rule { rule, format } => commands::rule::rule(rule, format)?, + Command::Rule { rule, all, format } => { + if all { + commands::rule::rules(format)?; + } + if let Some(rule) = rule { + commands::rule::rule(rule, format)?; + } + } Command::Config { option } => return Ok(commands::config::config(option.as_deref())), Command::Linter { format } => commands::linter::linter(format)?, Command::Clean => commands::clean::clean(log_level)?, diff --git a/docs/configuration.md b/docs/configuration.md index a64bef7a50..3575253ed7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -161,7 +161,7 @@ Usage: ruff [OPTIONS] Commands: check Run Ruff on the given files or directories (default) - rule Explain a rule + rule Explain a rule (or all rules) config List or describe the available configuration options linter List all supported upstream linters clean Clear any caches in the current directory and any subdirectories From 485d997d359b3fdbaef992e634557fd953a24f75 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 4 Jul 2023 16:02:57 -0400 Subject: [PATCH 319/447] Tweak prefix match to use .all_rules() (#5512) ## Summary No behavior change, but I think this is a little cleaner. --- crates/ruff/src/codes.rs | 12 ++++++++++++ crates/ruff/src/registry.rs | 14 ++++---------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index f39106a745..5932150ab7 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -14,6 +14,18 @@ use crate::rules; #[derive(PartialEq, Eq, PartialOrd, Ord)] pub struct NoqaCode(&'static str, &'static str); +impl NoqaCode { + /// Return the prefix for the [`NoqaCode`], e.g., `SIM` for `SIM101`. + pub fn prefix(&self) -> &str { + self.0 + } + + /// Return the suffix for the [`NoqaCode`], e.g., `101` for `SIM101`. + pub fn suffix(&self) -> &str { + self.1 + } +} + impl std::fmt::Debug for NoqaCode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(self, f) diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index d1d9014bdf..29d1da806f 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -18,14 +18,10 @@ pub trait AsRule { 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)?; - let rule = prefix.rules().next().unwrap(); - // TODO(charlie): Add a method to return an individual code, rather than matching on the - // prefix. - if rule.noqa_code().to_string() != format!("{}{}", linter.common_prefix(), code) { - return Err(FromCodeError::Prefix); - } - Ok(rule) + linter + .all_rules() + .find(|rule| rule.noqa_code().suffix() == code) + .ok_or(FromCodeError::Unknown) } } @@ -33,8 +29,6 @@ impl Rule { pub enum FromCodeError { #[error("unknown rule code")] Unknown, - #[error("expected a rule code (like `SIM101`), not a prefix (like `SIM` or `SIM1`)")] - Prefix, } #[derive(EnumIter, Debug, PartialEq, Eq, Clone, Hash, RuleNamespace)] From da1c320bfa0fdae38c0c51c8fe91781fcdcd8728 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 4 Jul 2023 16:25:16 -0400 Subject: [PATCH 320/447] Add .ipynb_checkpoints, .pyenv, .pytest_cache, and .vscode to default excludes (#5513) ## Summary VS Code extensions are [recommended](https://code.visualstudio.com/docs/python/settings-reference#_linting-settings) to exclude `.vscode` and `site-packages`. Black also now omits `.vscode`, `.pytest_cache`, and `.ipynb_checkpoints` by default. Omitting `.pyenv` is similar to omitting virtual environments, but really only matters in the context of VS Code (see: https://github.com/astral-sh/ruff/discussions/5509). Closes: #5510. --- BREAKING_CHANGES.md | 36 ++++++++++++++++++++++++++++ crates/ruff/src/settings/defaults.rs | 4 ++++ 2 files changed, 40 insertions(+) diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index 6f1c0ae85f..cc69ea3483 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -1,5 +1,41 @@ # Breaking Changes +## 0.0.277 + +### `.ipynb_checkpoints`, `.pyenv`, `.pytest_cache`, and `.vscode` are now excluded by default ([#5513](https://github.com/astral-sh/ruff/pull/5513)) + +Ruff maintains a list of default exclusions, which now consists of the following patterns: + +- `.bzr` +- `.direnv` +- `.eggs` +- `.git` +- `.git-rewrite` +- `.hg` +- `.ipynb_checkpoints` +- `.mypy_cache` +- `.nox` +- `.pants.d` +- `.pyenv` +- `.pytest_cache` +- `.pytype` +- `.ruff_cache` +- `.svn` +- `.tox` +- `.venv` +- `.vscode` +- `__pypackages__` +- `_build` +- `buck-out` +- `build` +- `dist` +- `node_modules` +- `venv` + +Previously, the `.ipynb_checkpoints`, `.pyenv`, `.pytest_cache`, and `.vscode` directories were not +excluded by default. This change brings Ruff's default exclusions in line with other tools like +Black. + ## 0.0.276 ### The `keep-runtime-typing` setting has been reinstated ([#5470](https://github.com/astral-sh/ruff/pull/5470)) diff --git a/crates/ruff/src/settings/defaults.rs b/crates/ruff/src/settings/defaults.rs index 987148705d..0893fa7543 100644 --- a/crates/ruff/src/settings/defaults.rs +++ b/crates/ruff/src/settings/defaults.rs @@ -39,14 +39,18 @@ pub static EXCLUDE: Lazy> = Lazy::new(|| { FilePattern::Builtin(".git"), FilePattern::Builtin(".git-rewrite"), FilePattern::Builtin(".hg"), + FilePattern::Builtin(".ipynb_checkpoints"), FilePattern::Builtin(".mypy_cache"), FilePattern::Builtin(".nox"), FilePattern::Builtin(".pants.d"), + FilePattern::Builtin(".pyenv"), + FilePattern::Builtin(".pytest_cache"), FilePattern::Builtin(".pytype"), FilePattern::Builtin(".ruff_cache"), FilePattern::Builtin(".svn"), FilePattern::Builtin(".tox"), FilePattern::Builtin(".venv"), + FilePattern::Builtin(".vscode"), FilePattern::Builtin("__pypackages__"), FilePattern::Builtin("_build"), FilePattern::Builtin("buck-out"), From 324455f580813a7c7721dc1d7ef1ee7721ae3e76 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 4 Jul 2023 17:31:32 -0400 Subject: [PATCH 321/447] Bump version to 0.0.277 (#5515) --- 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 efc811ad0e..b949e7d4b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -746,7 +746,7 @@ dependencies = [ [[package]] name = "flake8-to-ruff" -version = "0.0.276" +version = "0.0.277" dependencies = [ "anyhow", "clap", @@ -1829,7 +1829,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.0.276" +version = "0.0.277" dependencies = [ "annotate-snippets 0.9.1", "anyhow", @@ -1927,7 +1927,7 @@ dependencies = [ [[package]] name = "ruff_cli" -version = "0.0.276" +version = "0.0.277" dependencies = [ "annotate-snippets 0.9.1", "anyhow", diff --git a/README.md b/README.md index 1a73e65b38..4069782b61 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.276 + rev: v0.0.277 hooks: - id: ruff ``` diff --git a/crates/flake8_to_ruff/Cargo.toml b/crates/flake8_to_ruff/Cargo.toml index c411d33fe6..1324d76596 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.276" +version = "0.0.277" description = """ Convert Flake8 configuration files to Ruff configuration files. """ diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index c7a401b79c..45f4b18c04 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.0.276" +version = "0.0.277" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_cli/Cargo.toml b/crates/ruff_cli/Cargo.toml index 212c59de10..ffa12956f7 100644 --- a/crates/ruff_cli/Cargo.toml +++ b/crates/ruff_cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_cli" -version = "0.0.276" +version = "0.0.277" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/docs/tutorial.md b/docs/tutorial.md index 452bb2583e..404ddc26a9 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.276 + rev: v0.0.277 hooks: - id: ruff ``` diff --git a/docs/usage.md b/docs/usage.md index 4d2f32448f..1a84c8eddd 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.276 + rev: v0.0.277 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.276 + rev: v0.0.277 hooks: - id: ruff args: [ --fix, --exit-non-zero-on-fix ] diff --git a/pyproject.toml b/pyproject.toml index b860e31629..789be5574e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.0.276" +version = "0.0.277" 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 26a268a3ecf703d4bcaa7804eaf054c295d0accf Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 4 Jul 2023 20:25:54 -0400 Subject: [PATCH 322/447] Refactor the `unnecessary-map` (`C417`) implementation (#5518) ## Summary No behavioral changes. Just refactors + adding a test for a false positive, which I'll fix in a downstream PR. --- .../fixtures/flake8_comprehensions/C417.py | 8 +- .../src/rules/flake8_comprehensions/fixes.rs | 20 +- .../rules/unnecessary_map.rs | 200 ++++++++---------- ...8_comprehensions__tests__C417_C417.py.snap | 41 ++-- 4 files changed, 135 insertions(+), 134 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_comprehensions/C417.py b/crates/ruff/resources/test/fixtures/flake8_comprehensions/C417.py index e0353b49c3..5cf7548cc3 100644 --- a/crates/ruff/resources/test/fixtures/flake8_comprehensions/C417.py +++ b/crates/ruff/resources/test/fixtures/flake8_comprehensions/C417.py @@ -25,10 +25,12 @@ map(lambda x=2, y=1: x + y, nums, nums) set(map(lambda x, y: x, nums, nums)) -def myfunc(arg1: int, arg2: int = 4): +def func(arg1: int, arg2: int = 4): return 2 * arg1 + arg2 -list(map(myfunc, nums)) +# Non-error: `func` is not a lambda. +list(map(func, nums)) -[x for x in nums] +# False positive: need to preserve the late-binding of `x`. +callbacks = map(lambda x: lambda: x, range(4)) diff --git a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs index 1d4db80bfd..1017bea5ac 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs @@ -14,6 +14,7 @@ use ruff_diagnostics::{Edit, Fix}; use ruff_python_ast::source_code::{Locator, Stylist}; use crate::autofix::codemods::CodegenStylist; +use crate::rules::flake8_comprehensions::rules::ObjectType; use crate::{ checkers::ast::Checker, cst::matchers::{ @@ -888,7 +889,7 @@ pub(crate) fn fix_unnecessary_map( stylist: &Stylist, expr: &rustpython_parser::ast::Expr, parent: Option<&rustpython_parser::ast::Expr>, - kind: &str, + object_type: ObjectType, ) -> Result { let module_text = locator.slice(expr.range()); let mut tree = match_expression(module_text)?; @@ -948,8 +949,8 @@ pub(crate) fn fix_unnecessary_map( whitespace_after_in: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(" ")), }); - match kind { - "generator" => { + match object_type { + ObjectType::Generator => { tree = Expression::GeneratorExp(Box::new(GeneratorExp { elt: func_body.body.clone(), for_in: compfor, @@ -957,7 +958,7 @@ pub(crate) fn fix_unnecessary_map( rpar: vec![RightParen::default()], })); } - "list" => { + ObjectType::List => { tree = Expression::ListComp(Box::new(ListComp { elt: func_body.body.clone(), for_in: compfor, @@ -967,7 +968,7 @@ pub(crate) fn fix_unnecessary_map( rpar: vec![], })); } - "set" => { + ObjectType::Set => { tree = Expression::SetComp(Box::new(SetComp { elt: func_body.body.clone(), for_in: compfor, @@ -977,7 +978,7 @@ pub(crate) fn fix_unnecessary_map( rbrace: RightCurlyBrace::default(), })); } - "dict" => { + ObjectType::Dict => { let (key, value) = if let Expression::Tuple(tuple) = func_body.body.as_ref() { if tuple.elements.len() != 2 { bail!("Expected two elements") @@ -1009,17 +1010,14 @@ pub(crate) fn fix_unnecessary_map( ), })); } - _ => { - bail!("Expected generator, list, set or dict"); - } } let mut content = tree.codegen_stylist(stylist); // If the expression is embedded in an f-string, surround it with spaces to avoid // syntax errors. - if kind == "set" || kind == "dict" { - if let Some(rustpython_parser::ast::Expr::FormattedValue(_)) = parent { + if matches!(object_type, ObjectType::Set | ObjectType::Dict) { + if parent.map_or(false, rustpython_parser::ast::Expr::is_formatted_value_expr) { content = format!(" {content} "); } } 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 9de5aeec53..4f29021ff9 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs @@ -1,8 +1,9 @@ -use ruff_text_size::TextRange; +use std::fmt; + use rustpython_parser::ast::{self, Expr, Ranged}; -use ruff_diagnostics::Diagnostic; use ruff_diagnostics::{AutofixKind, Violation}; +use ruff_diagnostics::{Diagnostic, Fix}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; @@ -40,7 +41,7 @@ use super::helpers; /// `{v: v ** 2 for v in values}`. #[violation] pub struct UnnecessaryMap { - obj_type: String, + object_type: ObjectType, } impl Violation for UnnecessaryMap { @@ -48,21 +49,13 @@ impl Violation for UnnecessaryMap { #[derive_message_formats] fn message(&self) -> String { - let UnnecessaryMap { obj_type } = self; - if obj_type == "generator" { - format!("Unnecessary `map` usage (rewrite using a generator expression)") - } else { - format!("Unnecessary `map` usage (rewrite using a `{obj_type}` comprehension)") - } + let UnnecessaryMap { object_type } = self; + format!("Unnecessary `map` usage (rewrite using a {object_type})") } fn autofix_title(&self) -> Option { - let UnnecessaryMap { obj_type } = self; - Some(if obj_type == "generator" { - format!("Replace `map` using a generator expression") - } else { - format!("Replace `map` using a `{obj_type}` comprehension") - }) + let UnnecessaryMap { object_type } = self; + Some(format!("Replace `map` with a {object_type}")) } } @@ -74,116 +67,109 @@ pub(crate) fn unnecessary_map( func: &Expr, args: &[Expr], ) { - fn create_diagnostic(kind: &str, location: TextRange) -> Diagnostic { - Diagnostic::new( - UnnecessaryMap { - obj_type: kind.to_string(), - }, - location, - ) - } - let Some(id) = helpers::expr_name(func) else { return; }; - match id { - "map" => { - if !checker.semantic().is_builtin(id) { - return; - } - // Exclude the parent if already matched by other arms - if let Some(Expr::Call(ast::ExprCall { func: f, .. })) = parent { - if let Some(id_parent) = helpers::expr_name(f) { - if id_parent == "dict" || id_parent == "set" || id_parent == "list" { + let object_type = match id { + "map" => ObjectType::Generator, + "list" => ObjectType::List, + "set" => ObjectType::Set, + "dict" => ObjectType::Dict, + _ => return, + }; + + if !checker.semantic().is_builtin(id) { + return; + } + + match object_type { + ObjectType::Generator => { + // Exclude the parent if already matched by other arms. + if let Some(Expr::Call(ast::ExprCall { func, .. })) = parent { + if let Some(name) = helpers::expr_name(func) { + if matches!(name, "list" | "set" | "dict") { return; } } }; - if args.len() == 2 && matches!(&args[0], Expr::Lambda(_)) { - let mut diagnostic = create_diagnostic("generator", expr.range()); - if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { - fixes::fix_unnecessary_map( - checker.locator, - checker.stylist, - expr, - parent, - "generator", - ) - }); - } - checker.diagnostics.push(diagnostic); + // Only flag, e.g., `map(lambda x: x + 1, iterable)`. + if !matches!(args, [Expr::Lambda(_), _]) { + return; } } - "list" | "set" => { - if !checker.semantic().is_builtin(id) { + ObjectType::List | ObjectType::Set => { + // Only flag, e.g., `list(map(lambda x: x + 1, iterable))`. + let [Expr::Call(ast::ExprCall { func, args, .. })] = args else { + return; + }; + + if args.len() != 2 { return; } - if let Some(Expr::Call(ast::ExprCall { func, args, .. })) = args.first() { - if args.len() != 2 { - 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()) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { - fixes::fix_unnecessary_map( - checker.locator, - checker.stylist, - expr, - parent, - id, - ) - }); - } - checker.diagnostics.push(diagnostic); - } - } - } - "dict" => { - if !checker.semantic().is_builtin(id) { + let Some(argument) = helpers::first_argument_with_matching_function("map", func, args) + else { + return; + }; + + if !argument.is_lambda_expr() { return; } + } + ObjectType::Dict => { + // Only flag, e.g., `dict(map(lambda v: (v, v ** 2), values))`. + let [Expr::Call(ast::ExprCall { func, args, .. })] = args else { + return; + }; - 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 { - return; - }; - if let Expr::Lambda(ast::ExprLambda { body, .. }) = argument { - if matches!(body.as_ref(), Expr::Tuple(ast::ExprTuple { elts, .. }) | Expr::List(ast::ExprList { elts, .. } ) if elts.len() == 2) - { - let mut diagnostic = create_diagnostic(id, expr.range()); - if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { - fixes::fix_unnecessary_map( - checker.locator, - checker.stylist, - expr, - parent, - id, - ) - }); - } - checker.diagnostics.push(diagnostic); - } - } - } + let Some(argument) = helpers::first_argument_with_matching_function("map", func, args) + else { + return; + }; + + let Expr::Lambda(ast::ExprLambda { body, .. }) = argument else { + return; + }; + + let (Expr::Tuple(ast::ExprTuple { elts, .. }) | Expr::List(ast::ExprList { elts, .. })) = + body.as_ref() + else { + return; + }; + + if elts.len() != 2 { + return; } } - _ => (), + } + + let mut diagnostic = Diagnostic::new(UnnecessaryMap { object_type }, expr.range()); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.try_set_fix(|| { + fixes::fix_unnecessary_map(checker.locator, checker.stylist, expr, parent, object_type) + .map(Fix::suggested) + }); + } + checker.diagnostics.push(diagnostic); +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(crate) enum ObjectType { + Generator, + List, + Set, + Dict, +} + +impl fmt::Display for ObjectType { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + ObjectType::Generator => fmt.write_str("generator expression"), + ObjectType::List => fmt.write_str("`list` comprehension"), + ObjectType::Set => fmt.write_str("`set` comprehension"), + ObjectType::Dict => fmt.write_str("`dict` comprehension"), + } } } diff --git a/crates/ruff/src/rules/flake8_comprehensions/snapshots/ruff__rules__flake8_comprehensions__tests__C417_C417.py.snap b/crates/ruff/src/rules/flake8_comprehensions/snapshots/ruff__rules__flake8_comprehensions__tests__C417_C417.py.snap index b27c132244..318af2d9d2 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/snapshots/ruff__rules__flake8_comprehensions__tests__C417_C417.py.snap +++ b/crates/ruff/src/rules/flake8_comprehensions/snapshots/ruff__rules__flake8_comprehensions__tests__C417_C417.py.snap @@ -10,7 +10,7 @@ C417.py:3:1: C417 [*] Unnecessary `map` usage (rewrite using a generator express 4 | map(lambda x: str(x), nums) 5 | list(map(lambda x: x * 2, nums)) | - = help: Replace `map` using a generator expression + = help: Replace `map` with a generator expression ℹ Suggested fix 1 1 | # Errors. @@ -30,7 +30,7 @@ C417.py:4:1: C417 [*] Unnecessary `map` usage (rewrite using a generator express 5 | list(map(lambda x: x * 2, nums)) 6 | set(map(lambda x: x % 2 == 0, nums)) | - = help: Replace `map` using a generator expression + = help: Replace `map` with a generator expression ℹ Suggested fix 1 1 | # Errors. @@ -51,7 +51,7 @@ C417.py:5:1: C417 [*] Unnecessary `map` usage (rewrite using a `list` comprehens 6 | set(map(lambda x: x % 2 == 0, nums)) 7 | dict(map(lambda v: (v, v**2), nums)) | - = help: Replace `map` using a `list` comprehension + = help: Replace `map` with a `list` comprehension ℹ Suggested fix 2 2 | nums = [1, 2, 3] @@ -72,7 +72,7 @@ C417.py:6:1: C417 [*] Unnecessary `map` usage (rewrite using a `set` comprehensi 7 | dict(map(lambda v: (v, v**2), nums)) 8 | map(lambda: "const", nums) | - = help: Replace `map` using a `set` comprehension + = help: Replace `map` with a `set` comprehension ℹ Suggested fix 3 3 | map(lambda x: x + 1, nums) @@ -93,7 +93,7 @@ C417.py:7:1: C417 [*] Unnecessary `map` usage (rewrite using a `dict` comprehens 8 | map(lambda: "const", nums) 9 | map(lambda _: 3.0, nums) | - = help: Replace `map` using a `dict` comprehension + = help: Replace `map` with a `dict` comprehension ℹ Suggested fix 4 4 | map(lambda x: str(x), nums) @@ -114,7 +114,7 @@ C417.py:8:1: C417 [*] Unnecessary `map` usage (rewrite using a generator express 9 | map(lambda _: 3.0, nums) 10 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123))) | - = help: Replace `map` using a generator expression + = help: Replace `map` with a generator expression ℹ Suggested fix 5 5 | list(map(lambda x: x * 2, nums)) @@ -135,7 +135,7 @@ C417.py:9:1: C417 [*] Unnecessary `map` usage (rewrite using a generator express 10 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123))) 11 | all(map(lambda v: isinstance(v, dict), nums)) | - = help: Replace `map` using a generator expression + = help: Replace `map` with a generator expression ℹ Suggested fix 6 6 | set(map(lambda x: x % 2 == 0, nums)) @@ -156,7 +156,7 @@ C417.py:10:13: C417 [*] Unnecessary `map` usage (rewrite using a generator expre 11 | all(map(lambda v: isinstance(v, dict), nums)) 12 | filter(func, map(lambda v: v, nums)) | - = help: Replace `map` using a generator expression + = help: Replace `map` with a generator expression ℹ Suggested fix 7 7 | dict(map(lambda v: (v, v**2), nums)) @@ -176,7 +176,7 @@ C417.py:11:5: C417 [*] Unnecessary `map` usage (rewrite using a generator expres | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417 12 | filter(func, map(lambda v: v, nums)) | - = help: Replace `map` using a generator expression + = help: Replace `map` with a generator expression ℹ Suggested fix 8 8 | map(lambda: "const", nums) @@ -197,7 +197,7 @@ C417.py:12:14: C417 [*] Unnecessary `map` usage (rewrite using a generator expre 13 | 14 | # When inside f-string, then the fix should be surrounded by whitespace | - = help: Replace `map` using a generator expression + = help: Replace `map` with a generator expression ℹ Suggested fix 9 9 | map(lambda _: 3.0, nums) @@ -216,7 +216,7 @@ C417.py:15:8: C417 [*] Unnecessary `map` usage (rewrite using a `set` comprehens | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417 16 | _ = f"{dict(map(lambda v: (v, v**2), nums))}" | - = help: Replace `map` using a `set` comprehension + = help: Replace `map` with a `set` comprehension ℹ Suggested fix 12 12 | filter(func, map(lambda v: v, nums)) @@ -237,7 +237,7 @@ C417.py:16:8: C417 [*] Unnecessary `map` usage (rewrite using a `dict` comprehen 17 | 18 | # Error, but unfixable. | - = help: Replace `map` using a `dict` comprehension + = help: Replace `map` with a `dict` comprehension ℹ Suggested fix 13 13 | @@ -258,6 +258,21 @@ C417.py:21:1: C417 Unnecessary `map` usage (rewrite using a generator expression 22 | 23 | # False negatives. | - = help: Replace `map` using a generator expression + = help: Replace `map` with a generator expression + +C417.py:36:13: C417 [*] Unnecessary `map` usage (rewrite using a generator expression) + | +35 | # False positive: need to preserve the late-binding of `x`. +36 | callbacks = map(lambda x: lambda: x, range(4)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417 + | + = help: Replace `map` with a generator expression + +ℹ Suggested fix +33 33 | list(map(func, nums)) +34 34 | +35 35 | # False positive: need to preserve the late-binding of `x`. +36 |-callbacks = map(lambda x: lambda: x, range(4)) + 36 |+callbacks = (lambda: x for x in range(4)) From 5100c562737878698d076c255d1503555a0de406 Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Tue, 4 Jul 2023 20:57:26 -0500 Subject: [PATCH 323/447] Add rule documentation template to `scripts/add_rule.py` (#5519) --- scripts/add_rule.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/add_rule.py b/scripts/add_rule.py index 1fb271aa85..3e4c105822 100755 --- a/scripts/add_rule.py +++ b/scripts/add_rule.py @@ -98,6 +98,17 @@ use ruff_macros::{{derive_message_formats, violation}}; use crate::checkers::ast::Checker; +/// ## What it does +/// +/// ## Why is this bad? +/// +/// ## Example +/// ```python +/// ``` +/// +/// Use instead: +/// ```python +/// ``` #[violation] pub struct {name}; From 634ed8975c66f7145dd745c53b7fc31f2e22599a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 4 Jul 2023 22:06:21 -0400 Subject: [PATCH 324/447] Add pip to the ecosystem-ci check (#5521) --- scripts/check_ecosystem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/check_ecosystem.py b/scripts/check_ecosystem.py index 6667313962..8c4023ce9b 100755 --- a/scripts/check_ecosystem.py +++ b/scripts/check_ecosystem.py @@ -89,6 +89,7 @@ REPOSITORIES: list[Repository] = [ Repository("pypa", "build", "main"), Repository("pypa", "cibuildwheel", "main"), Repository("pypa", "setuptools", "main"), + Repository("pypa", "pip", "main"), Repository("python", "mypy", "master"), Repository("DisnakeDev", "disnake", "master"), Repository("scikit-build", "scikit-build", "main"), From 0726dc25c2fe892cd5e5fdd03847f3494706bc26 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 4 Jul 2023 22:09:50 -0400 Subject: [PATCH 325/447] Add some additional users to the README (#5522) --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4069782b61..f333191ea2 100644 --- a/README.md +++ b/README.md @@ -347,6 +347,7 @@ Ruff is released under the MIT license. Ruff is used by a number of major open-source projects and companies, including: - Amazon ([AWS SAM](https://github.com/aws/serverless-application-model)) +- Anthropic ([Python SDK](https://github.com/anthropics/anthropic-sdk-python)) - [Apache Airflow](https://github.com/apache/airflow) - AstraZeneca ([Magnus](https://github.com/AstraZeneca/magnus-core)) - Benchling ([Refac](https://github.com/benchling/refac)) @@ -356,6 +357,7 @@ Ruff is used by a number of major open-source projects and companies, including: - [DVC](https://github.com/iterative/dvc) - [Dagger](https://github.com/dagger/dagger) - [Dagster](https://github.com/dagster-io/dagster) +- Databricks ([MLflow](https://github.com/mlflow/mlflow)) - [FastAPI](https://github.com/tiangolo/fastapi) - [Gradio](https://github.com/gradio-app/gradio) - [Great Expectations](https://github.com/great-expectations/great_expectations) @@ -369,13 +371,14 @@ Ruff is used by a number of major open-source projects and companies, including: - [LangChain](https://github.com/hwchase17/langchain) - [LlamaIndex](https://github.com/jerryjliu/llama_index) - Matrix ([Synapse](https://github.com/matrix-org/synapse)) -- Meltano ([Meltano CLI](https://github.com/meltano/meltano), [Singer SDK](https://github.com/meltano/sdk)) -- Modern Treasury ([Python SDK](https://github.com/Modern-Treasury/modern-treasury-python-sdk)) -- Mozilla ([Firefox](https://github.com/mozilla/gecko-dev)) - [MegaLinter](https://github.com/oxsecurity/megalinter) +- Meltano ([Meltano CLI](https://github.com/meltano/meltano), [Singer SDK](https://github.com/meltano/sdk)) - Microsoft ([Semantic Kernel](https://github.com/microsoft/semantic-kernel), [ONNX Runtime](https://github.com/microsoft/onnxruntime), [LightGBM](https://github.com/microsoft/LightGBM)) +- Modern Treasury ([Python SDK](https://github.com/Modern-Treasury/modern-treasury-python-sdk)) +- Mozilla ([Firefox](https://github.com/mozilla/gecko-dev)) +- [Mypy](https://github.com/python/mypy) - Netflix ([Dispatch](https://github.com/Netflix/dispatch)) - [Neon](https://github.com/neondatabase/neon) - [ONNX](https://github.com/onnx/onnx) @@ -411,6 +414,7 @@ Ruff is used by a number of major open-source projects and companies, including: - [featuretools](https://github.com/alteryx/featuretools) - [meson-python](https://github.com/mesonbuild/meson-python) - [nox](https://github.com/wntrblm/nox) +- [pip](https://github.com/pypa/pip) ### Show Your Support From dd60a3865c3067f22e692104249a941fee8431e9 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 4 Jul 2023 22:11:29 -0400 Subject: [PATCH 326/447] Avoid triggering `unnecessary-map` (`C417`) for late-bound lambdas (#5520) Closes https://github.com/astral-sh/ruff/issues/5502. --- .../fixtures/flake8_comprehensions/C417.py | 7 +- .../rules/unnecessary_map.rs | 100 +++++++++++++++++- ...8_comprehensions__tests__C417_C417.py.snap | 18 ++-- 3 files changed, 110 insertions(+), 15 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_comprehensions/C417.py b/crates/ruff/resources/test/fixtures/flake8_comprehensions/C417.py index 5cf7548cc3..7077acfad6 100644 --- a/crates/ruff/resources/test/fixtures/flake8_comprehensions/C417.py +++ b/crates/ruff/resources/test/fixtures/flake8_comprehensions/C417.py @@ -32,5 +32,8 @@ def func(arg1: int, arg2: int = 4): # Non-error: `func` is not a lambda. list(map(func, nums)) -# False positive: need to preserve the late-binding of `x`. -callbacks = map(lambda x: lambda: x, range(4)) +# False positive: need to preserve the late-binding of `x` in the inner lambda. +map(lambda x: lambda: x, range(4)) + +# Error: the `x` is overridden by the inner lambda. +map(lambda x: lambda x: x, range(4)) 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 4f29021ff9..0505d08028 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs @@ -1,10 +1,13 @@ use std::fmt; -use rustpython_parser::ast::{self, Expr, Ranged}; +use rustpython_parser::ast::{self, Arguments, Expr, ExprContext, Ranged, Stmt}; use ruff_diagnostics::{AutofixKind, Violation}; use ruff_diagnostics::{Diagnostic, Fix}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::includes_arg_name; +use ruff_python_ast::visitor; +use ruff_python_ast::visitor::Visitor; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -95,7 +98,11 @@ pub(crate) fn unnecessary_map( }; // Only flag, e.g., `map(lambda x: x + 1, iterable)`. - if !matches!(args, [Expr::Lambda(_), _]) { + let [Expr::Lambda(ast::ExprLambda { args, body, .. }), _] = args else { + return; + }; + + if late_binding(args, body) { return; } } @@ -114,7 +121,11 @@ pub(crate) fn unnecessary_map( return; }; - if !argument.is_lambda_expr() { + let Expr::Lambda(ast::ExprLambda { args, body, .. }) = argument else { + return; + }; + + if late_binding(args, body) { return; } } @@ -129,7 +140,7 @@ pub(crate) fn unnecessary_map( return; }; - let Expr::Lambda(ast::ExprLambda { body, .. }) = argument else { + let Expr::Lambda(ast::ExprLambda { args, body, .. }) = argument else { return; }; @@ -142,6 +153,10 @@ pub(crate) fn unnecessary_map( if elts.len() != 2 { return; } + + if late_binding(args, body) { + return; + } } } @@ -173,3 +188,80 @@ impl fmt::Display for ObjectType { } } } + +/// Returns `true` if the lambda defined by the given arguments and body contains any names that +/// are late-bound within nested lambdas. +/// +/// For example, given: +/// +/// ```python +/// map(lambda x: lambda: x, range(4)) # (0, 1, 2, 3) +/// ``` +/// +/// The `x` in the inner lambda is "late-bound". Specifically, rewriting the above as: +/// +/// ```python +/// (lambda: x for x in range(4)) # (3, 3, 3, 3) +/// ``` +/// +/// Would yield an incorrect result, as the `x` in the inner lambda would be bound to the last +/// value of `x` in the comprehension. +fn late_binding(args: &Arguments, body: &Expr) -> bool { + let mut visitor = LateBindingVisitor::new(args); + visitor.visit_expr(body); + visitor.late_bound +} + +#[derive(Debug)] +struct LateBindingVisitor<'a> { + /// The arguments to the current lambda. + args: &'a Arguments, + /// The arguments to any lambdas within the current lambda body. + lambdas: Vec<&'a Arguments>, + /// Whether any names within the current lambda body are late-bound within nested lambdas. + late_bound: bool, +} + +impl<'a> LateBindingVisitor<'a> { + fn new(args: &'a Arguments) -> Self { + Self { + args, + lambdas: Vec::new(), + late_bound: false, + } + } +} + +impl<'a> Visitor<'a> for LateBindingVisitor<'a> { + fn visit_stmt(&mut self, _stmt: &'a Stmt) {} + + fn visit_expr(&mut self, expr: &'a Expr) { + match expr { + Expr::Lambda(ast::ExprLambda { args, .. }) => { + self.lambdas.push(args); + visitor::walk_expr(self, expr); + self.lambdas.pop(); + } + Expr::Name(ast::ExprName { + id, + ctx: ExprContext::Load, + .. + }) => { + // If we're within a nested lambda... + if !self.lambdas.is_empty() { + // If the name is defined in the current lambda... + if includes_arg_name(id, self.args) { + // And isn't overridden by any nested lambdas... + if !self.lambdas.iter().any(|args| includes_arg_name(id, args)) { + // Then it's late-bound. + self.late_bound = true; + } + } + } + } + _ => visitor::walk_expr(self, expr), + } + } + + fn visit_body(&mut self, _body: &'a [Stmt]) {} +} diff --git a/crates/ruff/src/rules/flake8_comprehensions/snapshots/ruff__rules__flake8_comprehensions__tests__C417_C417.py.snap b/crates/ruff/src/rules/flake8_comprehensions/snapshots/ruff__rules__flake8_comprehensions__tests__C417_C417.py.snap index 318af2d9d2..acb6700d3f 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/snapshots/ruff__rules__flake8_comprehensions__tests__C417_C417.py.snap +++ b/crates/ruff/src/rules/flake8_comprehensions/snapshots/ruff__rules__flake8_comprehensions__tests__C417_C417.py.snap @@ -260,19 +260,19 @@ C417.py:21:1: C417 Unnecessary `map` usage (rewrite using a generator expression | = help: Replace `map` with a generator expression -C417.py:36:13: C417 [*] Unnecessary `map` usage (rewrite using a generator expression) +C417.py:39:1: C417 [*] Unnecessary `map` usage (rewrite using a generator expression) | -35 | # False positive: need to preserve the late-binding of `x`. -36 | callbacks = map(lambda x: lambda: x, range(4)) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417 +38 | # Error: the `x` is overridden by the inner lambda. +39 | map(lambda x: lambda x: x, range(4)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417 | = help: Replace `map` with a generator expression ℹ Suggested fix -33 33 | list(map(func, nums)) -34 34 | -35 35 | # False positive: need to preserve the late-binding of `x`. -36 |-callbacks = map(lambda x: lambda: x, range(4)) - 36 |+callbacks = (lambda: x for x in range(4)) +36 36 | map(lambda x: lambda: x, range(4)) +37 37 | +38 38 | # Error: the `x` is overridden by the inner lambda. +39 |-map(lambda x: lambda x: x, range(4)) + 39 |+(lambda x: x for x in range(4)) From 6fd71e6f53ace6328a77193a91fa5f859ef2153f Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Wed, 5 Jul 2023 09:48:59 +0530 Subject: [PATCH 327/447] Avoid triggering DTZ001-006 when using `.astimezone()` (#5524) ## Summary Avoid triggering DTZ001-006 when using `.astimezone()` ## Test Plan Added test cases to call `.astimezone()` on DTZ001-006 fixes: #5516 --- .../test/fixtures/flake8_datetimez/DTZ001.py | 3 +++ .../test/fixtures/flake8_datetimez/DTZ002.py | 3 +++ .../test/fixtures/flake8_datetimez/DTZ003.py | 3 +++ .../test/fixtures/flake8_datetimez/DTZ004.py | 3 +++ .../test/fixtures/flake8_datetimez/DTZ005.py | 3 +++ .../test/fixtures/flake8_datetimez/DTZ006.py | 3 +++ .../rules/call_datetime_fromtimestamp.rs | 6 ++++++ .../rules/call_datetime_now_without_tzinfo.rs | 6 ++++++ .../rules/call_datetime_today.rs | 16 ++++++++++++---- .../rules/call_datetime_utcfromtimestamp.rs | 16 ++++++++++++---- .../rules/call_datetime_utcnow.rs | 16 ++++++++++++---- .../rules/call_datetime_without_tzinfo.rs | 6 ++++++ .../src/rules/flake8_datetimez/rules/helpers.rs | 11 +++++++++++ .../ruff/src/rules/flake8_datetimez/rules/mod.rs | 1 + ...lake8_datetimez__tests__DTZ001_DTZ001.py.snap | 2 ++ ...lake8_datetimez__tests__DTZ002_DTZ002.py.snap | 12 +++++++----- ...lake8_datetimez__tests__DTZ003_DTZ003.py.snap | 12 +++++++----- ...lake8_datetimez__tests__DTZ004_DTZ004.py.snap | 12 +++++++----- ...lake8_datetimez__tests__DTZ005_DTZ005.py.snap | 2 ++ ...lake8_datetimez__tests__DTZ006_DTZ006.py.snap | 2 ++ 20 files changed, 111 insertions(+), 27 deletions(-) create mode 100644 crates/ruff/src/rules/flake8_datetimez/rules/helpers.rs diff --git a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ001.py b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ001.py index 29533cba75..0974f807ef 100644 --- a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ001.py +++ b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ001.py @@ -19,3 +19,6 @@ from datetime import datetime # no args unqualified datetime(2000, 1, 1, 0, 0, 0) + +# uses `astimezone` method +datetime(2000, 1, 1, 0, 0, 0).astimezone() diff --git a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ002.py b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ002.py index 7f6231deff..9d124091bb 100644 --- a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ002.py +++ b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ002.py @@ -7,3 +7,6 @@ from datetime import datetime # unqualified datetime.today() + +# uses `astimezone` method +datetime.today().astimezone() diff --git a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ003.py b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ003.py index 4c463802e7..646bfa32b3 100644 --- a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ003.py +++ b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ003.py @@ -7,3 +7,6 @@ from datetime import datetime # unqualified datetime.utcnow() + +# uses `astimezone` method +datetime.utcnow().astimezone() diff --git a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ004.py b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ004.py index b4b535f57f..ae1658a688 100644 --- a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ004.py +++ b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ004.py @@ -7,3 +7,6 @@ from datetime import datetime # unqualified datetime.utcfromtimestamp(1234) + +# uses `astimezone` method +datetime.utcfromtimestamp(1234).astimezone() diff --git a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ005.py b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ005.py index 41f1a72a38..5d6f8596b6 100644 --- a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ005.py +++ b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ005.py @@ -16,3 +16,6 @@ from datetime import datetime # no args unqualified datetime.now() + +# uses `astimezone` method +datetime.now().astimezone() diff --git a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ006.py b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ006.py index b6f80613e2..5e9b217611 100644 --- a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ006.py +++ b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ006.py @@ -16,3 +16,6 @@ from datetime import datetime # no args unqualified datetime.fromtimestamp(1234) + +# uses `astimezone` method +datetime.fromtimestamp(1234).astimezone() diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs index 0832acc59d..5ce71fa558 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs @@ -7,6 +7,8 @@ use ruff_python_ast::helpers::{has_non_none_keyword, is_const_none}; use crate::checkers::ast::Checker; +use super::helpers; + #[violation] pub struct CallDatetimeFromtimestamp; @@ -40,6 +42,10 @@ pub(crate) fn call_datetime_fromtimestamp( return; } + if helpers::parent_expr_is_astimezone(checker) { + return; + } + // no args / no args unqualified if args.len() < 2 && keywords.is_empty() { checker diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs index 54491cc3cb..478fc06c10 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs @@ -7,6 +7,8 @@ use ruff_python_ast::helpers::{has_non_none_keyword, is_const_none}; use crate::checkers::ast::Checker; +use super::helpers; + #[violation] pub struct CallDatetimeNowWithoutTzinfo; @@ -35,6 +37,10 @@ pub(crate) fn call_datetime_now_without_tzinfo( return; } + if helpers::parent_expr_is_astimezone(checker) { + return; + } + // no args / no args unqualified if args.is_empty() && keywords.is_empty() { checker diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_today.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_today.rs index 4ca31f75e5..0738b1ae70 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_today.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_today.rs @@ -6,6 +6,8 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +use super::helpers; + #[violation] pub struct CallDatetimeToday; @@ -26,15 +28,21 @@ impl Violation for CallDatetimeToday { /// It uses the system local timezone. /// Use `datetime.datetime.now(tz=)` instead. pub(crate) fn call_datetime_today(checker: &mut Checker, func: &Expr, location: TextRange) { - if checker + if !checker .semantic() .resolve_call_path(func) .map_or(false, |call_path| { matches!(call_path.as_slice(), ["datetime", "datetime", "today"]) }) { - checker - .diagnostics - .push(Diagnostic::new(CallDatetimeToday, location)); + return; } + + if helpers::parent_expr_is_astimezone(checker) { + return; + } + + checker + .diagnostics + .push(Diagnostic::new(CallDatetimeToday, location)); } diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs index 075e5d7352..c63c79483f 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs @@ -6,6 +6,8 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +use super::helpers; + #[violation] pub struct CallDatetimeUtcfromtimestamp; @@ -33,7 +35,7 @@ pub(crate) fn call_datetime_utcfromtimestamp( func: &Expr, location: TextRange, ) { - if checker + if !checker .semantic() .resolve_call_path(func) .map_or(false, |call_path| { @@ -43,8 +45,14 @@ pub(crate) fn call_datetime_utcfromtimestamp( ) }) { - checker - .diagnostics - .push(Diagnostic::new(CallDatetimeUtcfromtimestamp, location)); + return; } + + if helpers::parent_expr_is_astimezone(checker) { + return; + } + + checker + .diagnostics + .push(Diagnostic::new(CallDatetimeUtcfromtimestamp, location)); } diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs index b6abbb4722..1c92b1ea66 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs @@ -6,6 +6,8 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +use super::helpers; + #[violation] pub struct CallDatetimeUtcnow; @@ -28,15 +30,21 @@ impl Violation for CallDatetimeUtcnow { /// UTC. As such, the recommended way to create an object representing the /// current time in UTC is by calling `datetime.now(timezone.utc)`. pub(crate) fn call_datetime_utcnow(checker: &mut Checker, func: &Expr, location: TextRange) { - if checker + if !checker .semantic() .resolve_call_path(func) .map_or(false, |call_path| { matches!(call_path.as_slice(), ["datetime", "datetime", "utcnow"]) }) { - checker - .diagnostics - .push(Diagnostic::new(CallDatetimeUtcnow, location)); + return; } + + if helpers::parent_expr_is_astimezone(checker) { + return; + } + + checker + .diagnostics + .push(Diagnostic::new(CallDatetimeUtcnow, location)); } diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs index 86de1e3ea2..cf153a5c7d 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs @@ -7,6 +7,8 @@ use ruff_python_ast::helpers::{has_non_none_keyword, is_const_none}; use crate::checkers::ast::Checker; +use super::helpers; + #[violation] pub struct CallDatetimeWithoutTzinfo; @@ -34,6 +36,10 @@ pub(crate) fn call_datetime_without_tzinfo( return; } + if helpers::parent_expr_is_astimezone(checker) { + return; + } + // No positional arg: keyword is missing or constant None. if args.len() < 8 && !has_non_none_keyword(keywords, "tzinfo") { checker diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/helpers.rs b/crates/ruff/src/rules/flake8_datetimez/rules/helpers.rs new file mode 100644 index 0000000000..bd237f9da1 --- /dev/null +++ b/crates/ruff/src/rules/flake8_datetimez/rules/helpers.rs @@ -0,0 +1,11 @@ +use rustpython_parser::ast::{Expr, ExprAttribute}; + +use crate::checkers::ast::Checker; + +/// Check if the parent expression is a call to `astimezone`. This assumes that +/// the current expression is a `datetime.datetime` object. +pub(crate) fn parent_expr_is_astimezone(checker: &Checker) -> bool { + checker.semantic().expr_parent().map_or(false, |parent| { + matches!(parent, Expr::Attribute(ExprAttribute { attr, .. }) if attr.as_str() == "astimezone") + }) +} diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/mod.rs b/crates/ruff/src/rules/flake8_datetimez/rules/mod.rs index 53f734ee94..87646ca775 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/mod.rs @@ -17,3 +17,4 @@ mod call_datetime_today; mod call_datetime_utcfromtimestamp; mod call_datetime_utcnow; mod call_datetime_without_tzinfo; +mod helpers; diff --git a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ001_DTZ001.py.snap b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ001_DTZ001.py.snap index c6b3004ade..70434e4d3e 100644 --- a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ001_DTZ001.py.snap +++ b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ001_DTZ001.py.snap @@ -42,6 +42,8 @@ DTZ001.py:21:1: DTZ001 The use of `datetime.datetime()` without `tzinfo` argumen 20 | # no args unqualified 21 | datetime(2000, 1, 1, 0, 0, 0) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DTZ001 +22 | +23 | # uses `astimezone` method | diff --git a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ002_DTZ002.py.snap b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ002_DTZ002.py.snap index 699e908355..8ea66ef3ee 100644 --- a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ002_DTZ002.py.snap +++ b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ002_DTZ002.py.snap @@ -11,10 +11,12 @@ DTZ002.py:4:1: DTZ002 The use of `datetime.datetime.today()` is not allowed, use | DTZ002.py:9:1: DTZ002 The use of `datetime.datetime.today()` is not allowed, use `datetime.datetime.now(tz=)` instead - | -8 | # unqualified -9 | datetime.today() - | ^^^^^^^^^^^^^^^^ DTZ002 - | + | + 8 | # unqualified + 9 | datetime.today() + | ^^^^^^^^^^^^^^^^ DTZ002 +10 | +11 | # uses `astimezone` method + | diff --git a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ003_DTZ003.py.snap b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ003_DTZ003.py.snap index acf0408afd..3bf5215e39 100644 --- a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ003_DTZ003.py.snap +++ b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ003_DTZ003.py.snap @@ -11,10 +11,12 @@ DTZ003.py:4:1: DTZ003 The use of `datetime.datetime.utcnow()` is not allowed, us | DTZ003.py:9:1: DTZ003 The use of `datetime.datetime.utcnow()` is not allowed, use `datetime.datetime.now(tz=)` instead - | -8 | # unqualified -9 | datetime.utcnow() - | ^^^^^^^^^^^^^^^^^ DTZ003 - | + | + 8 | # unqualified + 9 | datetime.utcnow() + | ^^^^^^^^^^^^^^^^^ DTZ003 +10 | +11 | # uses `astimezone` method + | diff --git a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ004_DTZ004.py.snap b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ004_DTZ004.py.snap index 20cb555d19..95f2f2e68a 100644 --- a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ004_DTZ004.py.snap +++ b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ004_DTZ004.py.snap @@ -11,10 +11,12 @@ DTZ004.py:4:1: DTZ004 The use of `datetime.datetime.utcfromtimestamp()` is not a | DTZ004.py:9:1: DTZ004 The use of `datetime.datetime.utcfromtimestamp()` is not allowed, use `datetime.datetime.fromtimestamp(ts, tz=)` instead - | -8 | # unqualified -9 | datetime.utcfromtimestamp(1234) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DTZ004 - | + | + 8 | # unqualified + 9 | datetime.utcfromtimestamp(1234) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DTZ004 +10 | +11 | # uses `astimezone` method + | diff --git a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ005_DTZ005.py.snap b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ005_DTZ005.py.snap index b6b7d88ba3..e037abffa6 100644 --- a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ005_DTZ005.py.snap +++ b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ005_DTZ005.py.snap @@ -42,6 +42,8 @@ DTZ005.py:18:1: DTZ005 The use of `datetime.datetime.now()` without `tz` argumen 17 | # no args unqualified 18 | datetime.now() | ^^^^^^^^^^^^^^ DTZ005 +19 | +20 | # uses `astimezone` method | diff --git a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ006_DTZ006.py.snap b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ006_DTZ006.py.snap index 65b7579403..d5cba0cd7c 100644 --- a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ006_DTZ006.py.snap +++ b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ006_DTZ006.py.snap @@ -42,6 +42,8 @@ DTZ006.py:18:1: DTZ006 The use of `datetime.datetime.fromtimestamp()` without `t 17 | # no args unqualified 18 | datetime.fromtimestamp(1234) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DTZ006 +19 | +20 | # uses `astimezone` method | From 9a8e5f7877c422f037e5ccc89375d7c0cb2a0012 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Jul 2023 12:34:15 -0400 Subject: [PATCH 328/447] Run `cargo update` (#5534) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ```console ❯ cargo update Updating crates.io index Updating git repository `https://github.com/charliermarsh/LibCST` Updating git repository `https://github.com/astral-sh/RustPython-Parser.git` Updating git repository `https://github.com/youknowone/unicode_names2.git` Updating bitflags v2.3.2 -> v2.3.3 Updating bstr v1.5.0 -> v1.6.0 Updating clap v4.3.8 -> v4.3.11 Updating clap_builder v4.3.8 -> v4.3.11 Updating clap_complete v4.3.1 -> v4.3.2 Updating colored v2.0.0 -> v2.0.4 Removing hermit-abi v0.2.6 Removing hermit-abi v0.3.1 Adding hermit-abi v0.3.2 Updating is-terminal v0.4.7 -> v0.4.8 Updating itoa v1.0.6 -> v1.0.8 Adding linux-raw-sys v0.4.3 Updating num_cpus v1.15.0 -> v1.16.0 Updating paste v1.0.12 -> v1.0.13 Updating pin-project-lite v0.2.9 -> v0.2.10 Updating quote v1.0.28 -> v1.0.29 Updating regex v1.8.4 -> v1.9.0 Updating regex-automata v0.1.10 -> v0.3.0 Updating regex-syntax v0.7.2 -> v0.7.3 Removing rustix v0.37.20 Adding rustix v0.37.23 Adding rustix v0.38.3 Updating rustversion v1.0.12 -> v1.0.13 Updating ryu v1.0.13 -> v1.0.14 Updating serde v1.0.164 -> v1.0.166 Updating serde_derive v1.0.164 -> v1.0.166 Updating serde_json v1.0.99 -> v1.0.100 Updating syn v2.0.22 -> v2.0.23 Updating thiserror v1.0.40 -> v1.0.41 Updating thiserror-impl v1.0.40 -> v1.0.41 Updating unicode-ident v1.0.9 -> v1.0.10 Updating uuid v1.3.4 -> v1.4.0 Updating windows-targets v0.48.0 -> v0.48.1 ``` --- Cargo.lock | 232 ++++++++++++++++++------------------- crates/ruff_cli/Cargo.toml | 1 - 2 files changed, 112 insertions(+), 121 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b949e7d4b8..245e470209 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,17 +148,6 @@ dependencies = [ "wait-timeout", ] -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -188,18 +177,17 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbe3c979c178231552ecba20214a8272df4e09f232a87aef4320cf06539aded" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" [[package]] name = "bstr" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a246e68bb43f6cd9db24bea052a53e40405417c5fb372e3d1a8a7f770a564ef5" +checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" dependencies = [ "memchr", - "once_cell", "regex-automata", "serde", ] @@ -291,9 +279,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.8" +version = "4.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9394150f5b4273a1763355bd1c2ec54cc5a2593f790587bcd6b2c947cfa9211" +checksum = "1640e5cc7fb47dbb8338fd471b105e7ed6c3cb2aeb00c2e067127ffd3764a05d" dependencies = [ "clap_builder", "clap_derive", @@ -302,22 +290,21 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.8" +version = "4.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a78fbdd3cc2914ddf37ba444114bc7765bbdcb55ec9cbe6fa054f0137400717" +checksum = "98c59138d527eeaf9b53f35a77fcc1fad9d883116070c63d5de1c7dc7b00c72b" dependencies = [ "anstream", "anstyle", - "bitflags 1.3.2", "clap_lex", "strsim", ] [[package]] name = "clap_complete" -version = "4.3.1" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6b5c519bab3ea61843a7923d074b04245624bb84a64a8c150f5deb014e388b" +checksum = "5fc443334c81a804575546c5a8a79b4913b50e28d69232903604cada1de817ce" dependencies = [ "clap", ] @@ -363,7 +350,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.23", ] [[package]] @@ -393,13 +380,13 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "colored" -version = "2.0.0" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" dependencies = [ - "atty", + "is-terminal", "lazy_static", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -577,7 +564,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.22", + "syn 2.0.23", ] [[package]] @@ -588,7 +575,7 @@ checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" dependencies = [ "darling_core", "quote", - "syn 2.0.22", + "syn 2.0.23", ] [[package]] @@ -853,27 +840,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" [[package]] name = "hex" @@ -1030,7 +999,7 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.1", + "hermit-abi", "libc", "windows-sys 0.48.0", ] @@ -1050,13 +1019,12 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb" dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", - "rustix", + "hermit-abi", + "rustix 0.38.3", "windows-sys 0.48.0", ] @@ -1071,9 +1039,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" [[package]] name = "js-sys" @@ -1198,6 +1166,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +[[package]] +name = "linux-raw-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" + [[package]] name = "log" version = "0.4.19" @@ -1351,11 +1325,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi", "libc", ] @@ -1397,9 +1371,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35" [[package]] name = "path-absolutize" @@ -1526,7 +1500,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.23", ] [[package]] @@ -1540,9 +1514,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" [[package]] name = "plotters" @@ -1694,9 +1668,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.28" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" dependencies = [ "proc-macro2", ] @@ -1769,26 +1743,32 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.4" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +checksum = "89089e897c013b3deb627116ae56a6955a72b8bed395c9526af31c9fe528b484" +dependencies = [ + "aho-corasick 1.0.2", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa250384981ea14565685dea16a9ccc4d1c541a13f82b9c168572264d1df8c56" dependencies = [ "aho-corasick 1.0.2", "memchr", "regex-syntax", ] -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" - [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846" [[package]] name = "result-like" @@ -1833,7 +1813,7 @@ version = "0.0.277" dependencies = [ "annotate-snippets 0.9.1", "anyhow", - "bitflags 2.3.2", + "bitflags 2.3.3", "chrono", "clap", "colored", @@ -1933,9 +1913,8 @@ dependencies = [ "anyhow", "argfile", "assert_cmd", - "atty", "bincode", - "bitflags 2.3.2", + "bitflags 2.3.3", "cachedir", "chrono", "clap", @@ -2042,7 +2021,7 @@ dependencies = [ "proc-macro2", "quote", "ruff_textwrap", - "syn 2.0.22", + "syn 2.0.23", ] [[package]] @@ -2050,7 +2029,7 @@ name = "ruff_python_ast" version = "0.0.0" dependencies = [ "anyhow", - "bitflags 2.3.2", + "bitflags 2.3.3", "insta", "is-macro", "itertools", @@ -2074,7 +2053,7 @@ name = "ruff_python_formatter" version = "0.0.0" dependencies = [ "anyhow", - "bitflags 2.3.2", + "bitflags 2.3.3", "clap", "countme", "insta", @@ -2107,7 +2086,7 @@ dependencies = [ name = "ruff_python_semantic" version = "0.0.0" dependencies = [ - "bitflags 2.3.2", + "bitflags 2.3.3", "is-macro", "nohash-hasher", "num-traits", @@ -2194,15 +2173,28 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.37.20" +version = "0.37.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac5ffa1efe7548069688cd7028f32591853cd7b5b756d41bcffd2353e4fc75b4" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys 0.4.3", "windows-sys 0.48.0", ] @@ -2244,7 +2236,7 @@ name = "rustpython-format" version = "0.2.0" source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0" dependencies = [ - "bitflags 2.3.2", + "bitflags 2.3.3", "itertools", "num-bigint", "num-traits", @@ -2298,15 +2290,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" +checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f" [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" [[package]] name = "same-file" @@ -2371,9 +2363,9 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.164" +version = "1.0.166" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8" dependencies = [ "serde_derive", ] @@ -2391,13 +2383,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.164" +version = "1.0.166" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.23", ] [[package]] @@ -2413,9 +2405,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" +checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" dependencies = [ "itoa", "ryu", @@ -2456,7 +2448,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.23", ] [[package]] @@ -2539,9 +2531,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.22" +version = "2.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616" +checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" dependencies = [ "proc-macro2", "quote", @@ -2567,7 +2559,7 @@ dependencies = [ "cfg-if", "fastrand", "redox_syscall 0.3.5", - "rustix", + "rustix 0.37.23", "windows-sys 0.48.0", ] @@ -2636,22 +2628,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "c16a64ba9387ef3fdae4f9c1a7f07a0997fce91985c0336f1ddc1822b3b37802" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "d14928354b01c4d6a4f0e549069adef399a284e7995c7ccca94e8a07a5346c59" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.23", ] [[package]] @@ -2811,7 +2803,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.23", ] [[package]] @@ -2901,9 +2893,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" +checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" [[package]] name = "unicode-normalization" @@ -2970,9 +2962,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.3.4" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81" +checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be" [[package]] name = "version_check" @@ -3032,7 +3024,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.23", "wasm-bindgen-shared", ] @@ -3066,7 +3058,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.23", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3177,7 +3169,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.1", ] [[package]] @@ -3195,7 +3187,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.1", ] [[package]] @@ -3215,9 +3207,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" dependencies = [ "windows_aarch64_gnullvm 0.48.0", "windows_aarch64_msvc 0.48.0", diff --git a/crates/ruff_cli/Cargo.toml b/crates/ruff_cli/Cargo.toml index ffa12956f7..86f37c1a0a 100644 --- a/crates/ruff_cli/Cargo.toml +++ b/crates/ruff_cli/Cargo.toml @@ -34,7 +34,6 @@ ruff_textwrap = { path = "../ruff_textwrap" } annotate-snippets = { version = "0.9.1", features = ["color"] } anyhow = { workspace = true } argfile = { version = "0.1.5" } -atty = { version = "0.2.14" } bincode = { version = "1.3.3" } bitflags = { workspace = true } cachedir = { version = "0.3.0" } From 9478454b9691abdcdf8fce20354c86dc03ad7752 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Wed, 5 Jul 2023 19:53:41 +0100 Subject: [PATCH 329/447] [`pylint`] Implement Pylint `typevar-double-variance` (`C0131`) (#5517) ## Summary Implement Pylint `typevar-double-variance` (`C0131`) as `type-bivariance` (`PLC0131`). Includes documentation. Related to #970. Renamed the rule to be more clear (it's not immediately obvious what 'double' means, IMO). The Pylint implementation checks only `TypeVar`, but this PR checks `ParamSpec` as well. ## Test Plan Added tests. `cargo test` --- .../test/fixtures/pylint/type_bivariance.py | 37 +++++ crates/ruff/src/checkers/ast/mod.rs | 3 + crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/pylint/helpers.rs | 26 ++- crates/ruff/src/rules/pylint/mod.rs | 1 + crates/ruff/src/rules/pylint/rules/mod.rs | 2 + .../src/rules/pylint/rules/type_bivariance.rs | 153 ++++++++++++++++++ .../pylint/rules/type_param_name_mismatch.rs | 39 +---- ...nt__tests__PLC0131_type_bivariance.py.snap | 40 +++++ ruff.schema.json | 1 + 10 files changed, 270 insertions(+), 33 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/pylint/type_bivariance.py create mode 100644 crates/ruff/src/rules/pylint/rules/type_bivariance.rs create mode 100644 crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0131_type_bivariance.py.snap diff --git a/crates/ruff/resources/test/fixtures/pylint/type_bivariance.py b/crates/ruff/resources/test/fixtures/pylint/type_bivariance.py new file mode 100644 index 0000000000..6084d97232 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pylint/type_bivariance.py @@ -0,0 +1,37 @@ +from typing import ParamSpec, TypeVar + +# Errors. + +T = TypeVar("T", covariant=True, contravariant=True) +T = TypeVar(name="T", covariant=True, contravariant=True) + +T = ParamSpec("T", covariant=True, contravariant=True) +T = ParamSpec(name="T", covariant=True, contravariant=True) + +# Non-errors. + +T = TypeVar("T") +T = TypeVar("T", covariant=False) +T = TypeVar("T", contravariant=False) +T = TypeVar("T", covariant=False, contravariant=False) +T = TypeVar("T", covariant=True) +T = TypeVar("T", covariant=True, contravariant=False) +T = TypeVar(name="T", covariant=True, contravariant=False) +T = TypeVar(name="T", covariant=True) +T = TypeVar("T", contravariant=True) +T = TypeVar("T", covariant=False, contravariant=True) +T = TypeVar(name="T", covariant=False, contravariant=True) +T = TypeVar(name="T", contravariant=True) + +T = ParamSpec("T") +T = ParamSpec("T", covariant=False) +T = ParamSpec("T", contravariant=False) +T = ParamSpec("T", covariant=False, contravariant=False) +T = ParamSpec("T", covariant=True) +T = ParamSpec("T", covariant=True, contravariant=False) +T = ParamSpec(name="T", covariant=True, contravariant=False) +T = ParamSpec(name="T", covariant=True) +T = ParamSpec("T", contravariant=True) +T = ParamSpec("T", covariant=False, contravariant=True) +T = ParamSpec(name="T", covariant=False, contravariant=True) +T = ParamSpec(name="T", contravariant=True) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 1ed61170dd..a321aebbc6 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1658,6 +1658,9 @@ where if self.settings.rules.enabled(Rule::TypeParamNameMismatch) { pylint::rules::type_param_name_mismatch(self, value, targets); } + if self.settings.rules.enabled(Rule::TypeBivariance) { + pylint::rules::type_bivariance(self, value); + } if self.is_stub { if self.any_enabled(&[ Rule::UnprefixedTypeParam, diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 5932150ab7..280007374b 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -168,6 +168,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pyflakes, "901") => (RuleGroup::Unspecified, rules::pyflakes::rules::RaiseNotImplemented), // pylint + (Pylint, "C0131") => (RuleGroup::Unspecified, rules::pylint::rules::TypeBivariance), (Pylint, "C0132") => (RuleGroup::Unspecified, rules::pylint::rules::TypeParamNameMismatch), (Pylint, "C0205") => (RuleGroup::Unspecified, rules::pylint::rules::SingleStringSlots), (Pylint, "C0414") => (RuleGroup::Unspecified, rules::pylint::rules::UselessImportAlias), diff --git a/crates/ruff/src/rules/pylint/helpers.rs b/crates/ruff/src/rules/pylint/helpers.rs index 51d9360558..02bbd98207 100644 --- a/crates/ruff/src/rules/pylint/helpers.rs +++ b/crates/ruff/src/rules/pylint/helpers.rs @@ -1,13 +1,37 @@ use std::fmt; use rustpython_parser::ast; -use rustpython_parser::ast::CmpOp; +use rustpython_parser::ast::{CmpOp, Constant, Expr, Keyword}; use ruff_python_semantic::analyze::function_type; use ruff_python_semantic::{ScopeKind, SemanticModel}; use crate::settings::Settings; +/// Returns the value of the `name` parameter to, e.g., a `TypeVar` constructor. +pub(super) fn type_param_name<'a>(args: &'a [Expr], keywords: &'a [Keyword]) -> Option<&'a str> { + // Handle both `TypeVar("T")` and `TypeVar(name="T")`. + let name_param = keywords + .iter() + .find(|keyword| { + keyword + .arg + .as_ref() + .map_or(false, |keyword| keyword.as_str() == "name") + }) + .map(|keyword| &keyword.value) + .or_else(|| args.get(0))?; + if let Expr::Constant(ast::ExprConstant { + value: Constant::Str(name), + .. + }) = &name_param + { + Some(name) + } else { + None + } +} + pub(super) fn in_dunder_init(semantic: &SemanticModel, settings: &Settings) -> bool { let scope = semantic.scope(); let (ScopeKind::Function(ast::StmtFunctionDef { diff --git a/crates/ruff/src/rules/pylint/mod.rs b/crates/ruff/src/rules/pylint/mod.rs index c1bb1ae591..9129aeb506 100644 --- a/crates/ruff/src/rules/pylint/mod.rs +++ b/crates/ruff/src/rules/pylint/mod.rs @@ -85,6 +85,7 @@ mod tests { Path::new("too_many_return_statements.py") )] #[test_case(Rule::TooManyStatements, Path::new("too_many_statements.py"))] + #[test_case(Rule::TypeBivariance, Path::new("type_bivariance.py"))] #[test_case(Rule::TypeParamNameMismatch, Path::new("type_param_name_mismatch.py"))] #[test_case( Rule::UnexpectedSpecialMethodSignature, diff --git a/crates/ruff/src/rules/pylint/rules/mod.rs b/crates/ruff/src/rules/pylint/rules/mod.rs index 431c3fd170..ad2b6d0d6c 100644 --- a/crates/ruff/src/rules/pylint/rules/mod.rs +++ b/crates/ruff/src/rules/pylint/rules/mod.rs @@ -37,6 +37,7 @@ pub(crate) use too_many_arguments::*; pub(crate) use too_many_branches::*; pub(crate) use too_many_return_statements::*; pub(crate) use too_many_statements::*; +pub(crate) use type_bivariance::*; pub(crate) use type_param_name_mismatch::*; pub(crate) use unexpected_special_method_signature::*; pub(crate) use unnecessary_direct_lambda_call::*; @@ -85,6 +86,7 @@ mod too_many_arguments; mod too_many_branches; mod too_many_return_statements; mod too_many_statements; +mod type_bivariance; mod type_param_name_mismatch; mod unexpected_special_method_signature; mod unnecessary_direct_lambda_call; diff --git a/crates/ruff/src/rules/pylint/rules/type_bivariance.rs b/crates/ruff/src/rules/pylint/rules/type_bivariance.rs new file mode 100644 index 0000000000..724584a962 --- /dev/null +++ b/crates/ruff/src/rules/pylint/rules/type_bivariance.rs @@ -0,0 +1,153 @@ +use std::fmt; + +use rustpython_parser::ast::{self, Expr, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_true; + +use crate::checkers::ast::Checker; +use crate::rules::pylint::helpers::type_param_name; + +/// ## What it does +/// Checks for `TypeVar` and `ParamSpec` definitions in which the type is +/// both covariant and contravariant. +/// +/// ## Why is this bad? +/// By default, Python's generic types are invariant, but can be marked as +/// either covariant or contravariant via the `covariant` and `contravariant` +/// keyword arguments. While the API does allow you to mark a type as both +/// covariant and contravariant, this is not supported by the type system, +/// and should be avoided. +/// +/// Instead, change the variance of the type to be either covariant, +/// contravariant, or invariant. If you want to describe both covariance and +/// contravariance, consider using two separate type parameters. +/// +/// For context: an "invariant" generic type only accepts values that exactly +/// match the type parameter; for example, `list[Dog]` accepts only `list[Dog]`, +/// not `list[Animal]` (superclass) or `list[Bulldog]` (subclass). This is +/// the default behavior for Python's generic types. +/// +/// A "covariant" generic type accepts subclasses of the type parameter; for +/// example, `Sequence[Animal]` accepts `Sequence[Dog]`. A "contravariant" +/// generic type accepts superclasses of the type parameter; for example, +/// `Callable[Dog]` accepts `Callable[Animal]`. +/// +/// ## Example +/// ```python +/// from typing import TypeVar +/// +/// T = TypeVar("T", covariant=True, contravariant=True) +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import TypeVar +/// +/// T_co = TypeVar("T_co", covariant=True) +/// T_contra = TypeVar("T_contra", contravariant=True) +/// ``` +/// +/// ## References +/// - [Python documentation: `typing` — Support for type hints](https://docs.python.org/3/library/typing.html) +/// - [PEP 483 – The Theory of Type Hints: Covariance and Contravariance](https://peps.python.org/pep-0483/#covariance-and-contravariance) +/// - [PEP 484 – Type Hints: Covariance and contravariance](https://peps.python.org/pep-0484/#covariance-and-contravariance) +#[violation] +pub struct TypeBivariance { + kind: VarKind, + param_name: Option, +} + +impl Violation for TypeBivariance { + #[derive_message_formats] + fn message(&self) -> String { + let TypeBivariance { kind, param_name } = self; + match param_name { + None => format!("`{kind}` cannot be both covariant and contravariant"), + Some(param_name) => { + format!("`{kind}` \"{param_name}\" cannot be both covariant and contravariant",) + } + } + } +} + +/// PLC0131 +pub(crate) fn type_bivariance(checker: &mut Checker, value: &Expr) { + let Expr::Call(ast::ExprCall { func,args, keywords, .. }) = value else { + return; + }; + + let Some(covariant) = keywords + .iter() + .find(|keyword| { + keyword + .arg + .as_ref() + .map_or(false, |keyword| keyword.as_str() == "covariant") + }) + .map(|keyword| &keyword.value) + else { + return; + }; + + let Some(contravariant) = keywords + .iter() + .find(|keyword| { + keyword + .arg + .as_ref() + .map_or(false, |keyword| keyword.as_str() == "contravariant") + }) + .map(|keyword| &keyword.value) + else { + return; + }; + + if is_const_true(covariant) && is_const_true(contravariant) { + 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 { + None + } + }) + else { + return; + }; + + checker.diagnostics.push(Diagnostic::new( + TypeBivariance { + kind, + param_name: type_param_name(args, keywords).map(ToString::to_string), + }, + func.range(), + )); + } +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum VarKind { + TypeVar, + ParamSpec, +} + +impl fmt::Display for VarKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + VarKind::TypeVar => fmt.write_str("TypeVar"), + VarKind::ParamSpec => fmt.write_str("ParamSpec"), + } + } +} diff --git a/crates/ruff/src/rules/pylint/rules/type_param_name_mismatch.rs b/crates/ruff/src/rules/pylint/rules/type_param_name_mismatch.rs index c7020475ff..88e058312b 100644 --- a/crates/ruff/src/rules/pylint/rules/type_param_name_mismatch.rs +++ b/crates/ruff/src/rules/pylint/rules/type_param_name_mismatch.rs @@ -1,11 +1,12 @@ use std::fmt; -use rustpython_parser::ast::{self, Constant, Expr, Ranged}; +use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +use crate::rules::pylint::helpers::type_param_name; /// ## What it does /// Checks for `TypeVar`, `TypeVarTuple`, `ParamSpec`, and `NewType` @@ -66,7 +67,11 @@ pub(crate) fn type_param_name_mismatch(checker: &mut Checker, value: &Expr, targ return; }; - let Some(param_name) = param_name(value) else { + let Expr::Call(ast::ExprCall { func, args, keywords, .. }) = value else { + return; + }; + + let Some(param_name) = type_param_name(args, keywords) else { return; }; @@ -74,10 +79,6 @@ pub(crate) fn type_param_name_mismatch(checker: &mut Checker, value: &Expr, targ return; } - let Expr::Call(ast::ExprCall { func, .. }) = value else { - return; - }; - let Some(kind) = checker .semantic() .resolve_call_path(func) @@ -138,29 +139,3 @@ impl fmt::Display for VarKind { } } } - -/// Returns the value of the `name` parameter to, e.g., a `TypeVar` constructor. -fn param_name(value: &Expr) -> Option<&str> { - // Handle both `TypeVar("T")` and `TypeVar(name="T")`. - let call = value.as_call_expr()?; - let name_param = call - .keywords - .iter() - .find(|keyword| { - keyword - .arg - .as_ref() - .map_or(false, |keyword| keyword.as_str() == "name") - }) - .map(|keyword| &keyword.value) - .or_else(|| call.args.get(0))?; - if let Expr::Constant(ast::ExprConstant { - value: Constant::Str(name), - .. - }) = &name_param - { - Some(name) - } else { - None - } -} diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0131_type_bivariance.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0131_type_bivariance.py.snap new file mode 100644 index 0000000000..01107aac91 --- /dev/null +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0131_type_bivariance.py.snap @@ -0,0 +1,40 @@ +--- +source: crates/ruff/src/rules/pylint/mod.rs +--- +type_bivariance.py:5:5: PLC0131 `TypeVar` "T" cannot be both covariant and contravariant + | +3 | # Errors. +4 | +5 | T = TypeVar("T", covariant=True, contravariant=True) + | ^^^^^^^ PLC0131 +6 | T = TypeVar(name="T", covariant=True, contravariant=True) + | + +type_bivariance.py:6:5: PLC0131 `TypeVar` "T" cannot be both covariant and contravariant + | +5 | T = TypeVar("T", covariant=True, contravariant=True) +6 | T = TypeVar(name="T", covariant=True, contravariant=True) + | ^^^^^^^ PLC0131 +7 | +8 | T = ParamSpec("T", covariant=True, contravariant=True) + | + +type_bivariance.py:8:5: PLC0131 `ParamSpec` "T" cannot be both covariant and contravariant + | +6 | T = TypeVar(name="T", covariant=True, contravariant=True) +7 | +8 | T = ParamSpec("T", covariant=True, contravariant=True) + | ^^^^^^^^^ PLC0131 +9 | T = ParamSpec(name="T", covariant=True, contravariant=True) + | + +type_bivariance.py:9:5: PLC0131 `ParamSpec` "T" cannot be both covariant and contravariant + | + 8 | T = ParamSpec("T", covariant=True, contravariant=True) + 9 | T = ParamSpec(name="T", covariant=True, contravariant=True) + | ^^^^^^^^^ PLC0131 +10 | +11 | # Non-errors. + | + + diff --git a/ruff.schema.json b/ruff.schema.json index 3b2252d987..13181e83d7 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2123,6 +2123,7 @@ "PLC0", "PLC01", "PLC013", + "PLC0131", "PLC0132", "PLC02", "PLC020", From 9e1039f823243dd790cf35cd7afd53762dc00a25 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Jul 2023 15:19:14 -0400 Subject: [PATCH 330/447] Enable attribute lookups via semantic model (#5536) ## Summary This PR enables us to resolve attribute accesses within files, at least for static and class methods. For example, we can now detect that this is a function access (and avoid a false-positive): ```python class Class: @staticmethod def error(): return ValueError("Something") # OK raise Class.error() ``` Closes #5487. Closes #5416. --- .../test/fixtures/flake8_raise/RSE102.py | 20 +++++++++++ crates/ruff/src/checkers/ast/mod.rs | 36 +++++++++---------- crates/ruff/src/renamer.rs | 4 +-- .../unnecessary_paren_on_raise_exception.rs | 12 +++++++ ...ry-paren-on-raise-exception_RSE102.py.snap | 6 ++-- crates/ruff_python_semantic/src/binding.rs | 14 ++++---- crates/ruff_python_semantic/src/model.rs | 28 ++++++++++++++- 7 files changed, 89 insertions(+), 31 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_raise/RSE102.py b/crates/ruff/resources/test/fixtures/flake8_raise/RSE102.py index aa80fa5145..ce75d7ab7c 100644 --- a/crates/ruff/resources/test/fixtures/flake8_raise/RSE102.py +++ b/crates/ruff/resources/test/fixtures/flake8_raise/RSE102.py @@ -29,6 +29,26 @@ raise TypeError( # Hello, world! ) +# OK raise AssertionError +# OK raise AttributeError("test message") + + +def return_error(): + return ValueError("Something") + + +# OK +raise return_error() + + +class Class: + @staticmethod + def error(): + return ValueError("Something") + + +# OK +raise Class.error() diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index a321aebbc6..7767129c5f 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1781,7 +1781,6 @@ where match stmt { Stmt::FunctionDef(ast::StmtFunctionDef { body, - name, args, decorator_list, returns, @@ -1789,7 +1788,6 @@ where }) | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { body, - name, args, decorator_list, returns, @@ -1848,13 +1846,6 @@ where }; } - self.add_binding( - name, - stmt.identifier(), - BindingKind::FunctionDefinition, - BindingFlags::empty(), - ); - let definition = docstrings::extraction::extract_definition( ExtractionTarget::Function, stmt, @@ -2064,19 +2055,28 @@ where // Post-visit. match stmt { - Stmt::FunctionDef(_) | Stmt::AsyncFunctionDef(_) => { - self.deferred.scopes.push(self.semantic.scope_id); - self.semantic.pop_scope(); - self.semantic.pop_definition(); - } - Stmt::ClassDef(ast::StmtClassDef { name, .. }) => { - self.deferred.scopes.push(self.semantic.scope_id); + Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) + | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { name, .. }) => { + let scope_id = self.semantic.scope_id; + self.deferred.scopes.push(scope_id); self.semantic.pop_scope(); self.semantic.pop_definition(); self.add_binding( name, stmt.identifier(), - BindingKind::ClassDefinition, + BindingKind::FunctionDefinition(scope_id), + BindingFlags::empty(), + ); + } + Stmt::ClassDef(ast::StmtClassDef { name, .. }) => { + let scope_id = self.semantic.scope_id; + self.deferred.scopes.push(scope_id); + self.semantic.pop_scope(); + self.semantic.pop_definition(); + self.add_binding( + name, + stmt.identifier(), + BindingKind::ClassDefinition(scope_id), BindingFlags::empty(), ); } @@ -3887,7 +3887,7 @@ where } // Store the existing binding, if any. - let existing_id = self.semantic.lookup(name); + let existing_id = self.semantic.lookup_symbol(name); // Add the bound exception name to the scope. let binding_id = self.add_binding( diff --git a/crates/ruff/src/renamer.rs b/crates/ruff/src/renamer.rs index a7beaafc5e..14aec66ef7 100644 --- a/crates/ruff/src/renamer.rs +++ b/crates/ruff/src/renamer.rs @@ -248,8 +248,8 @@ impl Renamer { | BindingKind::LoopVar | BindingKind::Global | BindingKind::Nonlocal(_) - | BindingKind::ClassDefinition - | BindingKind::FunctionDefinition + | BindingKind::ClassDefinition(_) + | BindingKind::FunctionDefinition(_) | BindingKind::Deletion | BindingKind::UnboundException(_) => { Some(Edit::range_replacement(target.to_string(), binding.range)) diff --git a/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs b/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs index ee3b0d7e69..b9fbec4697 100644 --- a/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs +++ b/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs @@ -2,6 +2,7 @@ use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; + use ruff_python_ast::helpers::match_parens; use crate::checkers::ast::Checker; @@ -53,6 +54,17 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr: }) = expr { if args.is_empty() && keywords.is_empty() { + // `raise func()` still requires parentheses; only `raise Class()` does not. + if checker + .semantic() + .lookup_attribute(func) + .map_or(false, |id| { + checker.semantic().binding(id).kind.is_function_definition() + }) + { + return; + } + let range = match_parens(func.end(), checker.locator) .expect("Expected call to include parentheses"); let mut diagnostic = Diagnostic::new(UnnecessaryParenOnRaiseException, range); diff --git a/crates/ruff/src/rules/flake8_raise/snapshots/ruff__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap b/crates/ruff/src/rules/flake8_raise/snapshots/ruff__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap index 9d84196033..4f905dd654 100644 --- a/crates/ruff/src/rules/flake8_raise/snapshots/ruff__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap +++ b/crates/ruff/src/rules/flake8_raise/snapshots/ruff__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap @@ -118,7 +118,7 @@ RSE102.py:28:16: RSE102 [*] Unnecessary parentheses on raised exception 30 | | ) | |_^ RSE102 31 | -32 | raise AssertionError +32 | # OK | = help: Remove unnecessary parentheses @@ -131,7 +131,7 @@ RSE102.py:28:16: RSE102 [*] Unnecessary parentheses on raised exception 30 |-) 28 |+raise TypeError 31 29 | -32 30 | raise AssertionError -33 31 | +32 30 | # OK +33 31 | raise AssertionError diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index 00d138494f..862be2b617 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -126,11 +126,11 @@ impl<'a> Binding<'a> { } matches!( existing.kind, - BindingKind::ClassDefinition - | BindingKind::FunctionDefinition - | BindingKind::Import(..) - | BindingKind::FromImport(..) - | BindingKind::SubmoduleImport(..) + BindingKind::ClassDefinition(_) + | BindingKind::FunctionDefinition(_) + | BindingKind::Import(_) + | BindingKind::FromImport(_) + | BindingKind::SubmoduleImport(_) ) } @@ -372,14 +372,14 @@ pub enum BindingKind<'a> { /// class Foo: /// ... /// ``` - ClassDefinition, + ClassDefinition(ScopeId), /// A binding for a function, like `foo` in: /// ```python /// def foo(): /// ... /// ``` - FunctionDefinition, + FunctionDefinition(ScopeId), /// A binding for an `__all__` export, like `__all__` in: /// ```python diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 619020ac12..6773c973f2 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -414,7 +414,7 @@ impl<'a> SemanticModel<'a> { /// Lookup a symbol in the current scope. This is a carbon copy of [`Self::resolve_read`], but /// doesn't add any read references to the resolved symbol. - pub fn lookup(&mut self, symbol: &str) -> Option { + pub fn lookup_symbol(&self, symbol: &str) -> Option { if self.in_forward_reference() { if let Some(binding_id) = self.scopes.global().get(symbol) { if !self.bindings[binding_id].is_unbound() { @@ -456,6 +456,32 @@ impl<'a> SemanticModel<'a> { None } + /// Lookup a qualified attribute in the current scope. + /// + /// For example, given `["Class", "method"`], resolve the `BindingKind::ClassDefinition` + /// associated with `Class`, then the `BindingKind::FunctionDefinition` associated with + /// `Class#method`. + pub fn lookup_attribute(&'a self, value: &'a Expr) -> Option { + let call_path = collect_call_path(value)?; + + // Find the symbol in the current scope. + let (symbol, attribute) = call_path.split_first()?; + let mut binding_id = self.lookup_symbol(symbol)?; + + // Recursively resolve class attributes, e.g., `foo.bar.baz` in. + let mut tail = attribute; + while let Some((symbol, rest)) = tail.split_first() { + // Find the next symbol in the class scope. + let BindingKind::ClassDefinition(scope_id) = self.binding(binding_id).kind else { + return None; + }; + binding_id = self.scopes[scope_id].get(symbol)?; + tail = rest; + } + + Some(binding_id) + } + /// Given a `BindingId`, return the `BindingId` of the submodule import that it aliases. fn resolve_submodule( &self, From c5bfd1e87763ca140986ecb11b68fb46eb86ba04 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Jul 2023 15:19:24 -0400 Subject: [PATCH 331/447] Allow descriptor instantiations in dataclass fields (#5537) ## Summary Per the Python documentation, dataclasses are allowed to instantiate descriptors, like so: ```python class IntConversionDescriptor: def __init__(self, *, default): self._default = default def __set_name__(self, owner, name): self._name = "_" + name def __get__(self, obj, type): if obj is None: return self._default return getattr(obj, self._name, self._default) def __set__(self, obj, value): setattr(obj, self._name, int(value)) @dataclass class InventoryItem: quantity_on_hand: IntConversionDescriptor = IntConversionDescriptor(default=100) ``` Closes https://github.com/astral-sh/ruff/issues/4451. --- .../resources/test/fixtures/ruff/RUF009.py | 28 ++++++++++- .../function_call_in_dataclass_default.rs | 3 +- crates/ruff/src/rules/ruff/rules/helpers.rs | 21 +++++++- ..._rules__ruff__tests__RUF009_RUF009.py.snap | 48 +++++++++---------- 4 files changed, 72 insertions(+), 28 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF009.py b/crates/ruff/resources/test/fixtures/ruff/RUF009.py index 3ba1aad6be..f1cc836ffc 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF009.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF009.py @@ -6,6 +6,7 @@ from fractions import Fraction from pathlib import Path from typing import ClassVar, NamedTuple + def default_function() -> list[int]: return [] @@ -25,12 +26,13 @@ class A: fine_timedelta: datetime.timedelta = datetime.timedelta(hours=7) fine_tuple: tuple[int] = tuple([1]) fine_regex: re.Pattern = re.compile(r".*") - fine_float: float = float('-inf') + fine_float: float = float("-inf") fine_int: int = int(12) fine_complex: complex = complex(1, 2) fine_str: str = str("foo") fine_bool: bool = bool("foo") - fine_fraction: Fraction = Fraction(1,2) + fine_fraction: Fraction = Fraction(1, 2) + DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES = ImmutableType(40) DEFAULT_A_FOR_ALL_DATACLASSES = A([1, 2, 3]) @@ -45,3 +47,25 @@ class B: okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES fine_dataclass_function: list[int] = field(default_factory=list) + + +class IntConversionDescriptor: + def __init__(self, *, default): + self._default = default + + def __set_name__(self, owner, name): + self._name = "_" + name + + def __get__(self, obj, type): + if obj is None: + return self._default + + return getattr(obj, self._name, self._default) + + def __set__(self, obj, value): + setattr(obj, self._name, int(value)) + + +@dataclass +class InventoryItem: + quantity_on_hand: IntConversionDescriptor = IntConversionDescriptor(default=100) diff --git a/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs b/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs index 93961572ae..3b52cac788 100644 --- a/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs +++ b/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs @@ -8,7 +8,7 @@ use ruff_python_semantic::analyze::typing::is_immutable_func; use crate::checkers::ast::Checker; use crate::rules::ruff::rules::helpers::{ - is_class_var_annotation, is_dataclass, is_dataclass_field, + is_class_var_annotation, is_dataclass, is_dataclass_field, is_descriptor_class, }; /// ## What it does @@ -98,6 +98,7 @@ pub(crate) fn function_call_in_dataclass_default( if !is_class_var_annotation(annotation, checker.semantic()) && !is_immutable_func(func, checker.semantic(), &extend_immutable_calls) && !is_dataclass_field(func, checker.semantic()) + && !is_descriptor_class(func, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( FunctionCallInDataclassDefaultArgument { diff --git a/crates/ruff/src/rules/ruff/rules/helpers.rs b/crates/ruff/src/rules/ruff/rules/helpers.rs index b70c6918e1..8ba7e01624 100644 --- a/crates/ruff/src/rules/ruff/rules/helpers.rs +++ b/crates/ruff/src/rules/ruff/rules/helpers.rs @@ -1,7 +1,7 @@ use rustpython_parser::ast::{self, Expr}; use ruff_python_ast::helpers::map_callable; -use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::{BindingKind, SemanticModel}; /// Return `true` if the given [`Expr`] is a special class attribute, like `__slots__`. /// @@ -64,3 +64,22 @@ pub(super) fn is_pydantic_model(class_def: &ast::StmtClassDef, semantic: &Semant }) }) } + +/// Returns `true` if the given function is an instantiation of a class that implements the +/// descriptor protocol. +/// +/// See: +pub(super) fn is_descriptor_class(func: &Expr, semantic: &SemanticModel) -> bool { + semantic.lookup_attribute(func).map_or(false, |id| { + let BindingKind::ClassDefinition(scope_id) = semantic.binding(id).kind else { + return false; + }; + + // Look for `__get__`, `__set__`, and `__delete__` methods. + ["__get__", "__set__", "__delete__"].iter().any(|method| { + semantic.scopes[scope_id].get(method).map_or(false, |id| { + semantic.binding(id).kind.is_function_definition() + }) + }) + }) +} diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF009_RUF009.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF009_RUF009.py.snap index ea6bb93008..64571bbe2d 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF009_RUF009.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF009_RUF009.py.snap @@ -1,44 +1,44 @@ --- source: crates/ruff/src/rules/ruff/mod.rs --- -RUF009.py:19:41: RUF009 Do not perform function call `default_function` in dataclass defaults +RUF009.py:20:41: RUF009 Do not perform function call `default_function` in dataclass defaults | -17 | @dataclass() -18 | class A: -19 | hidden_mutable_default: list[int] = default_function() +18 | @dataclass() +19 | class A: +20 | hidden_mutable_default: list[int] = default_function() | ^^^^^^^^^^^^^^^^^^ RUF009 -20 | class_variable: typing.ClassVar[list[int]] = default_function() -21 | another_class_var: ClassVar[list[int]] = default_function() +21 | class_variable: typing.ClassVar[list[int]] = default_function() +22 | another_class_var: ClassVar[list[int]] = default_function() | -RUF009.py:41:41: RUF009 Do not perform function call `default_function` in dataclass defaults +RUF009.py:43:41: RUF009 Do not perform function call `default_function` in dataclass defaults | -39 | @dataclass -40 | class B: -41 | hidden_mutable_default: list[int] = default_function() +41 | @dataclass +42 | class B: +43 | hidden_mutable_default: list[int] = default_function() | ^^^^^^^^^^^^^^^^^^ RUF009 -42 | another_dataclass: A = A() -43 | not_optimal: ImmutableType = ImmutableType(20) +44 | another_dataclass: A = A() +45 | not_optimal: ImmutableType = ImmutableType(20) | -RUF009.py:42:28: RUF009 Do not perform function call `A` in dataclass defaults +RUF009.py:44:28: RUF009 Do not perform function call `A` in dataclass defaults | -40 | class B: -41 | hidden_mutable_default: list[int] = default_function() -42 | another_dataclass: A = A() +42 | class B: +43 | hidden_mutable_default: list[int] = default_function() +44 | another_dataclass: A = A() | ^^^ RUF009 -43 | not_optimal: ImmutableType = ImmutableType(20) -44 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES +45 | not_optimal: ImmutableType = ImmutableType(20) +46 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES | -RUF009.py:43:34: RUF009 Do not perform function call `ImmutableType` in dataclass defaults +RUF009.py:45:34: RUF009 Do not perform function call `ImmutableType` in dataclass defaults | -41 | hidden_mutable_default: list[int] = default_function() -42 | another_dataclass: A = A() -43 | not_optimal: ImmutableType = ImmutableType(20) +43 | hidden_mutable_default: list[int] = default_function() +44 | another_dataclass: A = A() +45 | not_optimal: ImmutableType = ImmutableType(20) | ^^^^^^^^^^^^^^^^^ RUF009 -44 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES -45 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES +46 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES +47 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES | From 5a74a8e5a1420b750c7b7960ce71964ee624f802 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Jul 2023 15:22:22 -0400 Subject: [PATCH 332/447] Avoid syntax errors when rewriting str(dict) in f-strings (#5538) Closes https://github.com/astral-sh/ruff/issues/5530. --- .../ruff/resources/test/fixtures/ruff/RUF010.py | 4 ++++ .../src/rules/pylint/rules/type_bivariance.rs | 8 +++++++- .../pylint/rules/type_param_name_mismatch.rs | 8 +++++++- .../rules/explicit_f_string_type_conversion.rs | 15 ++++++++++++++- ...uff__rules__ruff__tests__RUF010_RUF010.py.snap | 2 ++ 5 files changed, 34 insertions(+), 3 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF010.py b/crates/ruff/resources/test/fixtures/ruff/RUF010.py index 77e459c214..031e08412f 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF010.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF010.py @@ -34,3 +34,7 @@ f"{ascii(bla)}" # OK " intermediary content " f" that flows {repr(obj)} of type {type(obj)}.{additional_message}" # RUF010 ) + + +# OK +f"{str({})}" diff --git a/crates/ruff/src/rules/pylint/rules/type_bivariance.rs b/crates/ruff/src/rules/pylint/rules/type_bivariance.rs index 724584a962..b8f3c98faa 100644 --- a/crates/ruff/src/rules/pylint/rules/type_bivariance.rs +++ b/crates/ruff/src/rules/pylint/rules/type_bivariance.rs @@ -74,7 +74,13 @@ impl Violation for TypeBivariance { /// PLC0131 pub(crate) fn type_bivariance(checker: &mut Checker, value: &Expr) { - let Expr::Call(ast::ExprCall { func,args, keywords, .. }) = value else { + let Expr::Call(ast::ExprCall { + func, + args, + keywords, + .. + }) = value + else { return; }; diff --git a/crates/ruff/src/rules/pylint/rules/type_param_name_mismatch.rs b/crates/ruff/src/rules/pylint/rules/type_param_name_mismatch.rs index 88e058312b..e7bbc7ae04 100644 --- a/crates/ruff/src/rules/pylint/rules/type_param_name_mismatch.rs +++ b/crates/ruff/src/rules/pylint/rules/type_param_name_mismatch.rs @@ -67,7 +67,13 @@ pub(crate) fn type_param_name_mismatch(checker: &mut Checker, value: &Expr, targ return; }; - let Expr::Call(ast::ExprCall { func, args, keywords, .. }) = value else { + let Expr::Call(ast::ExprCall { + func, + args, + keywords, + .. + }) = value + else { return; }; 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 419fd6df41..408c4ffbcb 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 @@ -88,7 +88,20 @@ pub(crate) fn explicit_f_string_type_conversion( }; // Can't be a conversion otherwise. - if args.len() != 1 || !keywords.is_empty() { + if !keywords.is_empty() { + continue; + } + + // Can't be a conversion otherwise. + let [arg] = args.as_slice() else { + continue; + }; + + // Avoid attempting to rewrite, e.g., `f"{str({})}"`; the curly braces are problematic. + if matches!( + arg, + Expr::Dict(_) | Expr::Set(_) | Expr::DictComp(_) | Expr::SetComp(_) + ) { continue; } diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap index ffbb12608b..da5f4232c5 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap @@ -243,5 +243,7 @@ RUF010.py:35:20: RUF010 [*] Use explicit conversion flag 35 |- f" that flows {repr(obj)} of type {type(obj)}.{additional_message}" # RUF010 35 |+ f" that flows {obj!r} of type {type(obj)}.{additional_message}" # RUF010 36 36 | ) +37 37 | +38 38 | From 6f548d9872f203e32b50d6a8f6cd137842ecd4fc Mon Sep 17 00:00:00 2001 From: qdegraaf <34540841+qdegraaf@users.noreply.github.com> Date: Wed, 5 Jul 2023 22:10:53 +0200 Subject: [PATCH 333/447] `[isort]` Add `--case-sensitive` flag (#5539) ## Summary Adds a `--case-sensitive` setting/flag to isort (default: `false`) which, when set to `true` sorts imports case sensitively instead of case insensitively. Tests and Docs can be improved, can do that if the general idea of the implementation is in order. First `isort` edit so any and all feedback is welcomed even more than usual. ## Test Plan Added a fixture with an assortment of imports in various cases. ## Issue links Closes: https://github.com/astral-sh/ruff/issues/5514 --- .../test/fixtures/isort/case_sensitive.py | 9 ++++ crates/ruff/src/rules/isort/mod.rs | 30 ++++++++++++- crates/ruff/src/rules/isort/order.rs | 9 +++- .../src/rules/isort/rules/organize_imports.rs | 1 + crates/ruff/src/rules/isort/settings.rs | 13 ++++++ ...sts__case_sensitive_case_sensitive.py.snap | 33 ++++++++++++++ crates/ruff/src/rules/isort/sorting.rs | 44 ++++++++++++++----- ruff.schema.json | 7 +++ 8 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/isort/case_sensitive.py create mode 100644 crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__case_sensitive_case_sensitive.py.snap diff --git a/crates/ruff/resources/test/fixtures/isort/case_sensitive.py b/crates/ruff/resources/test/fixtures/isort/case_sensitive.py new file mode 100644 index 0000000000..6f500358ee --- /dev/null +++ b/crates/ruff/resources/test/fixtures/isort/case_sensitive.py @@ -0,0 +1,9 @@ +import A +import B +import b +import C +import d +import E +import f +from g import a, B, c +from h import A, b, C diff --git a/crates/ruff/src/rules/isort/mod.rs b/crates/ruff/src/rules/isort/mod.rs index 251b69f6e4..f156451770 100644 --- a/crates/ruff/src/rules/isort/mod.rs +++ b/crates/ruff/src/rules/isort/mod.rs @@ -74,6 +74,7 @@ pub(crate) fn format_imports( combine_as_imports: bool, force_single_line: bool, force_sort_within_sections: bool, + case_sensitive: bool, force_wrap_aliases: bool, force_to_top: &BTreeSet, known_modules: &KnownModules, @@ -114,6 +115,7 @@ pub(crate) fn format_imports( src, package, force_sort_within_sections, + case_sensitive, force_wrap_aliases, force_to_top, known_modules, @@ -171,6 +173,7 @@ fn format_import_block( src: &[PathBuf], package: Option<&Path>, force_sort_within_sections: bool, + case_sensitive: bool, force_wrap_aliases: bool, force_to_top: &BTreeSet, known_modules: &KnownModules, @@ -206,6 +209,7 @@ fn format_import_block( let imports = order_imports( import_block, order_by_type, + case_sensitive, relative_imports_order, classes, constants, @@ -222,7 +226,13 @@ fn format_import_block( .collect::>(); if force_sort_within_sections { imports.sort_by(|import1, import2| { - cmp_either_import(import1, import2, relative_imports_order, force_to_top) + cmp_either_import( + import1, + import2, + relative_imports_order, + force_to_top, + case_sensitive, + ) }); }; imports @@ -449,6 +459,24 @@ mod tests { Ok(()) } + #[test_case(Path::new("case_sensitive.py"))] + fn case_sensitive(path: &Path) -> Result<()> { + let snapshot = format!("case_sensitive_{}", path.to_string_lossy()); + let diagnostics = test_path( + Path::new("isort").join(path).as_path(), + &Settings { + isort: super::settings::Settings { + case_sensitive: true, + ..super::settings::Settings::default() + }, + src: vec![test_resource_path("fixtures/isort")], + ..Settings::for_rule(Rule::UnsortedImports) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } + #[test_case(Path::new("force_to_top.py"))] fn force_to_top(path: &Path) -> Result<()> { let snapshot = format!("force_to_top_{}", path.to_string_lossy()); diff --git a/crates/ruff/src/rules/isort/order.rs b/crates/ruff/src/rules/isort/order.rs index 1867ebf0de..66509a797b 100644 --- a/crates/ruff/src/rules/isort/order.rs +++ b/crates/ruff/src/rules/isort/order.rs @@ -9,9 +9,11 @@ use super::settings::RelativeImportsOrder; use super::sorting::{cmp_import_from, cmp_members, cmp_modules}; use super::types::{AliasData, CommentSet, ImportBlock, OrderedImportBlock}; +#[allow(clippy::too_many_arguments)] pub(crate) fn order_imports<'a>( block: ImportBlock<'a>, order_by_type: bool, + case_sensitive: bool, relative_imports_order: RelativeImportsOrder, classes: &'a BTreeSet, constants: &'a BTreeSet, @@ -25,7 +27,9 @@ pub(crate) fn order_imports<'a>( block .import .into_iter() - .sorted_by(|(alias1, _), (alias2, _)| cmp_modules(alias1, alias2, force_to_top)), + .sorted_by(|(alias1, _), (alias2, _)| { + cmp_modules(alias1, alias2, force_to_top, case_sensitive) + }), ); // Sort `Stmt::ImportFrom`. @@ -70,6 +74,7 @@ pub(crate) fn order_imports<'a>( constants, variables, force_to_top, + case_sensitive, ) }) .collect::>(), @@ -83,6 +88,7 @@ pub(crate) fn order_imports<'a>( import_from2, relative_imports_order, force_to_top, + case_sensitive, ) .then_with(|| match (aliases1.first(), aliases2.first()) { (None, None) => Ordering::Equal, @@ -96,6 +102,7 @@ pub(crate) fn order_imports<'a>( constants, variables, force_to_top, + case_sensitive, ), }) }, diff --git a/crates/ruff/src/rules/isort/rules/organize_imports.rs b/crates/ruff/src/rules/isort/rules/organize_imports.rs index d7a3dbe549..89832f9431 100644 --- a/crates/ruff/src/rules/isort/rules/organize_imports.rs +++ b/crates/ruff/src/rules/isort/rules/organize_imports.rs @@ -127,6 +127,7 @@ pub(crate) fn organize_imports( settings.isort.combine_as_imports, settings.isort.force_single_line, settings.isort.force_sort_within_sections, + settings.isort.case_sensitive, settings.isort.force_wrap_aliases, &settings.isort.force_to_top, &settings.isort.known_modules, diff --git a/crates/ruff/src/rules/isort/settings.rs b/crates/ruff/src/rules/isort/settings.rs index 262829b73d..f01aa4afd1 100644 --- a/crates/ruff/src/rules/isort/settings.rs +++ b/crates/ruff/src/rules/isort/settings.rs @@ -127,6 +127,15 @@ pub struct Options { /// imports (like `from itertools import groupby`). Instead, sort the /// imports by module, independent of import style. pub force_sort_within_sections: Option, + #[option( + default = r#"false"#, + value_type = "bool", + example = r#" + case-sensitive = true + "# + )] + /// Sort imports taking into account case sensitivity. + pub case_sensitive: Option, #[option( default = r#"[]"#, value_type = "list[str]", @@ -303,6 +312,7 @@ pub struct Settings { pub combine_as_imports: bool, pub force_single_line: bool, pub force_sort_within_sections: bool, + pub case_sensitive: bool, pub force_wrap_aliases: bool, pub force_to_top: BTreeSet, pub known_modules: KnownModules, @@ -327,6 +337,7 @@ impl Default for Settings { combine_as_imports: false, force_single_line: false, force_sort_within_sections: false, + case_sensitive: false, force_wrap_aliases: false, force_to_top: BTreeSet::new(), known_modules: KnownModules::default(), @@ -429,6 +440,7 @@ impl From for Settings { combine_as_imports: options.combine_as_imports.unwrap_or(false), force_single_line: options.force_single_line.unwrap_or(false), force_sort_within_sections: options.force_sort_within_sections.unwrap_or(false), + case_sensitive: options.case_sensitive.unwrap_or(false), force_wrap_aliases: options.force_wrap_aliases.unwrap_or(false), force_to_top: BTreeSet::from_iter(options.force_to_top.unwrap_or_default()), known_modules: KnownModules::new( @@ -468,6 +480,7 @@ impl From for Options { ), force_single_line: Some(settings.force_single_line), force_sort_within_sections: Some(settings.force_sort_within_sections), + case_sensitive: Some(settings.case_sensitive), force_wrap_aliases: Some(settings.force_wrap_aliases), force_to_top: Some(settings.force_to_top.into_iter().collect()), known_first_party: Some( diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__case_sensitive_case_sensitive.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__case_sensitive_case_sensitive.py.snap new file mode 100644 index 0000000000..c6fe266802 --- /dev/null +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__case_sensitive_case_sensitive.py.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff/src/rules/isort/mod.rs +--- +case_sensitive.py:1:1: I001 [*] Import block is un-sorted or un-formatted + | +1 | / import A +2 | | import B +3 | | import b +4 | | import C +5 | | import d +6 | | import E +7 | | import f +8 | | from g import a, B, c +9 | | from h import A, b, C + | + = help: Organize imports + +ℹ Fix +1 1 | import A +2 2 | import B + 3 |+import C + 4 |+import E +3 5 | import b +4 |-import C +5 6 | import d +6 |-import E +7 7 | import f +8 |-from g import a, B, c +9 |-from h import A, b, C + 8 |+from g import B, a, c + 9 |+from h import A, C, b + + diff --git a/crates/ruff/src/rules/isort/sorting.rs b/crates/ruff/src/rules/isort/sorting.rs index ec8e15af4e..75bb9a2653 100644 --- a/crates/ruff/src/rules/isort/sorting.rs +++ b/crates/ruff/src/rules/isort/sorting.rs @@ -56,10 +56,17 @@ pub(crate) fn cmp_modules( alias1: &AliasData, alias2: &AliasData, force_to_top: &BTreeSet, + case_sensitive: bool, ) -> Ordering { cmp_force_to_top(alias1.name, alias2.name, force_to_top) - .then_with(|| natord::compare_ignore_case(alias1.name, alias2.name)) - .then_with(|| natord::compare(alias1.name, alias2.name)) + .then_with(|| { + if case_sensitive { + natord::compare(alias1.name, alias2.name) + } else { + natord::compare_ignore_case(alias1.name, alias2.name) + .then_with(|| natord::compare(alias1.name, alias2.name)) + } + }) .then_with(|| match (alias1.asname, alias2.asname) { (None, None) => Ordering::Equal, (None, Some(_)) => Ordering::Less, @@ -69,6 +76,7 @@ pub(crate) fn cmp_modules( } /// Compare two member imports within `Stmt::ImportFrom` blocks. +#[allow(clippy::too_many_arguments)] pub(crate) fn cmp_members( alias1: &AliasData, alias2: &AliasData, @@ -77,6 +85,7 @@ pub(crate) fn cmp_members( constants: &BTreeSet, variables: &BTreeSet, force_to_top: &BTreeSet, + case_sensitive: bool, ) -> Ordering { match (alias1.name == "*", alias2.name == "*") { (true, false) => Ordering::Less, @@ -85,9 +94,9 @@ pub(crate) fn cmp_members( if order_by_type { prefix(alias1.name, classes, constants, variables) .cmp(&prefix(alias2.name, classes, constants, variables)) - .then_with(|| cmp_modules(alias1, alias2, force_to_top)) + .then_with(|| cmp_modules(alias1, alias2, force_to_top, case_sensitive)) } else { - cmp_modules(alias1, alias2, force_to_top) + cmp_modules(alias1, alias2, force_to_top, case_sensitive) } } } @@ -116,6 +125,7 @@ pub(crate) fn cmp_import_from( import_from2: &ImportFromData, relative_imports_order: RelativeImportsOrder, force_to_top: &BTreeSet, + case_sensitive: bool, ) -> Ordering { cmp_levels( import_from1.level, @@ -133,8 +143,13 @@ pub(crate) fn cmp_import_from( (None, None) => Ordering::Equal, (None, Some(_)) => Ordering::Less, (Some(_), None) => Ordering::Greater, - (Some(module1), Some(module2)) => natord::compare_ignore_case(module1, module2) - .then_with(|| natord::compare(module1, module2)), + (Some(module1), Some(module2)) => { + if case_sensitive { + natord::compare(module1, module2) + } else { + natord::compare_ignore_case(module1, module2) + } + } }) } @@ -143,9 +158,14 @@ fn cmp_import_import_from( import: &AliasData, import_from: &ImportFromData, force_to_top: &BTreeSet, + case_sensitive: bool, ) -> Ordering { cmp_force_to_top(import.name, &import_from.module_name(), force_to_top).then_with(|| { - natord::compare_ignore_case(import.name, import_from.module.unwrap_or_default()) + if case_sensitive { + natord::compare(import.name, import_from.module.unwrap_or_default()) + } else { + natord::compare_ignore_case(import.name, import_from.module.unwrap_or_default()) + } }) } @@ -156,20 +176,24 @@ pub(crate) fn cmp_either_import( b: &EitherImport, relative_imports_order: RelativeImportsOrder, force_to_top: &BTreeSet, + case_sensitive: bool, ) -> Ordering { match (a, b) { - (Import((alias1, _)), Import((alias2, _))) => cmp_modules(alias1, alias2, force_to_top), + (Import((alias1, _)), Import((alias2, _))) => { + cmp_modules(alias1, alias2, force_to_top, case_sensitive) + } (ImportFrom((import_from, ..)), Import((alias, _))) => { - cmp_import_import_from(alias, import_from, force_to_top).reverse() + cmp_import_import_from(alias, import_from, force_to_top, case_sensitive).reverse() } (Import((alias, _)), ImportFrom((import_from, ..))) => { - cmp_import_import_from(alias, import_from, force_to_top) + cmp_import_import_from(alias, import_from, force_to_top, case_sensitive) } (ImportFrom((import_from1, ..)), ImportFrom((import_from2, ..))) => cmp_import_from( import_from1, import_from2, relative_imports_order, force_to_top, + case_sensitive, ), } } diff --git a/ruff.schema.json b/ruff.schema.json index 13181e83d7..47e0a9fde0 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1135,6 +1135,13 @@ "IsortOptions": { "type": "object", "properties": { + "case-sensitive": { + "description": "Sort imports taking into account case sensitivity.", + "type": [ + "boolean", + "null" + ] + }, "classes": { "description": "An override list of tokens to always recognize as a Class for `order-by-type` regardless of casing.", "type": [ From 1a2e4447990dc3e08b9c5268ffe4346d842f1bc5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Jul 2023 16:36:26 -0400 Subject: [PATCH 334/447] Use Insiders version of `mkdocs-material` (#5540) ## Summary This PR migrates our `mkdocs-material` version to [Insiders](https://squidfunk.github.io/mkdocs-material/insiders/), which we can access now that we're sponsors. We can't allow public access to the Insiders version, so we instead have a private fork, which contains a deploy key that I've added as a read-only Actions secret in this repo. (That is: the deploy key only lets you read that one repo, and do nothing else.) In general, non-Astral contributors can use the non-insiders version, and everything is expected to "work", but without the insiders features (they're intended to be ignored). See: https://squidfunk.github.io/mkdocs-material/insiders/#compatibility. --- .github/workflows/ci.yaml | 6 +++++- .github/workflows/docs.yaml | 7 +++++-- docs/requirements-insiders.txt | 4 ++++ docs/requirements.txt | 6 +++--- 4 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 docs/requirements-insiders.txt diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 54db12d04d..9346c527a5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -255,11 +255,15 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 + - name: "Add SSH key" + uses: webfactory/ssh-agent@v0.7.0 + with: + ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }} - name: "Install Rust toolchain" run: rustup show - uses: Swatinem/rust-cache@v2 - name: "Install dependencies" - run: pip install -r docs/requirements.txt + run: pip install -r docs/requirements-insiders.txt - name: "Update README File" run: python scripts/transform_readme.py --target mkdocs - name: "Generate docs" diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index cb8f646df5..27f3bacd7d 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -13,12 +13,15 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 + - name: "Add SSH key" + uses: webfactory/ssh-agent@v0.7.0 + with: + ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }} - name: "Install Rust toolchain" run: rustup show - uses: Swatinem/rust-cache@v2 - name: "Install dependencies" - run: | - pip install -r docs/requirements.txt + run: pip install -r docs/requirements-insiders.txt - name: "Copy README File" run: | python scripts/transform_readme.py --target mkdocs diff --git a/docs/requirements-insiders.txt b/docs/requirements-insiders.txt new file mode 100644 index 0000000000..20e4c11ad7 --- /dev/null +++ b/docs/requirements-insiders.txt @@ -0,0 +1,4 @@ +PyYAML==6.0 +black==23.3.0 +mkdocs==1.4.3 +git+ssh://git@github.com/astral-sh/mkdocs-material-insiders.git diff --git a/docs/requirements.txt b/docs/requirements.txt index 647559a59e..db22a4e7cb 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ -mkdocs~=1.4.2 -mkdocs-material~=9.0.6 -PyYAML~=6.0 +PyYAML==6.0 black==23.3.0 +mkdocs==1.4.3 +mkdocs-material==9.1.18 From a0c0b74b6d1000ad7161c40244a48ecbd97fe53f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Jul 2023 17:37:32 -0400 Subject: [PATCH 335/447] Use structs for noqa `Directive` variants (#5533) ## Summary No behavioral changes, just clearer (IMO) and with better documentation. --- crates/ruff/src/checkers/noqa.rs | 52 +++--- crates/ruff/src/noqa.rs | 287 ++++++++++++++++++------------- 2 files changed, 202 insertions(+), 137 deletions(-) diff --git a/crates/ruff/src/checkers/noqa.rs b/crates/ruff/src/checkers/noqa.rs index 52d47182b5..9cb887cd41 100644 --- a/crates/ruff/src/checkers/noqa.rs +++ b/crates/ruff/src/checkers/noqa.rs @@ -7,7 +7,7 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_python_ast::source_code::Locator; use crate::noqa; -use crate::noqa::{Directive, FileExemption, NoqaDirectives, NoqaMapping}; +use crate::noqa::{All, Codes, Directive, FileExemption, NoqaDirectives, NoqaMapping}; use crate::registry::{AsRule, Rule}; use crate::rule_redirects::get_redirect_target; use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA}; @@ -22,7 +22,7 @@ pub(crate) fn check_noqa( settings: &Settings, ) -> Vec { // Identify any codes that are globally exempted (within the current file). - let exemption = noqa::file_exemption(locator.contents(), comment_ranges); + let exemption = FileExemption::extract(locator.contents(), comment_ranges); // Extract all `noqa` directives. let mut noqa_directives = NoqaDirectives::from_commented_ranges(comment_ranges, locator); @@ -70,7 +70,7 @@ pub(crate) fn check_noqa( ignored_diagnostics.push(index); true } - Directive::Codes(.., codes, _) => { + Directive::Codes(Codes { codes, .. }) => { if noqa::includes(diagnostic.kind.rule(), codes) { directive_line .matches @@ -95,23 +95,32 @@ pub(crate) fn check_noqa( if analyze_directives && settings.rules.enabled(Rule::UnusedNOQA) { for line in noqa_directives.lines() { match &line.directive { - Directive::All(leading_spaces, noqa_range, trailing_spaces) => { + Directive::All(All { + leading_space_len, + noqa_range, + trailing_space_len, + }) => { if line.matches.is_empty() { let mut diagnostic = Diagnostic::new(UnusedNOQA { codes: None }, *noqa_range); if settings.rules.should_fix(diagnostic.kind.rule()) { #[allow(deprecated)] diagnostic.set_fix_from_edit(delete_noqa( - *leading_spaces, + *leading_space_len, *noqa_range, - *trailing_spaces, + *trailing_space_len, locator, )); } diagnostics.push(diagnostic); } } - Directive::Codes(leading_spaces, range, codes, trailing_spaces) => { + Directive::Codes(Codes { + leading_space_len, + noqa_range, + codes, + trailing_space_len, + }) => { let mut disabled_codes = vec![]; let mut unknown_codes = vec![]; let mut unmatched_codes = vec![]; @@ -166,22 +175,22 @@ pub(crate) fn check_noqa( .collect(), }), }, - *range, + *noqa_range, ); if settings.rules.should_fix(diagnostic.kind.rule()) { if valid_codes.is_empty() { #[allow(deprecated)] diagnostic.set_fix_from_edit(delete_noqa( - *leading_spaces, - *range, - *trailing_spaces, + *leading_space_len, + *noqa_range, + *trailing_space_len, locator, )); } else { #[allow(deprecated)] diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( format!("# noqa: {}", valid_codes.join(", ")), - *range, + *noqa_range, ))); } } @@ -199,9 +208,9 @@ pub(crate) fn check_noqa( /// Generate a [`Edit`] to delete a `noqa` directive. fn delete_noqa( - leading_spaces: TextSize, + leading_space_len: TextSize, noqa_range: TextRange, - trailing_spaces: TextSize, + trailing_space_len: TextSize, locator: &Locator, ) -> Edit { let line_range = locator.line_range(noqa_range.start()); @@ -209,27 +218,28 @@ fn delete_noqa( // Ex) `# noqa` if line_range == TextRange::new( - noqa_range.start() - leading_spaces, - noqa_range.end() + trailing_spaces, + noqa_range.start() - leading_space_len, + noqa_range.end() + trailing_space_len, ) { let full_line_end = locator.full_line_end(line_range.end()); Edit::deletion(line_range.start(), full_line_end) } // Ex) `x = 1 # noqa` - else if noqa_range.end() + trailing_spaces == line_range.end() { - Edit::deletion(noqa_range.start() - leading_spaces, line_range.end()) + else if noqa_range.end() + trailing_space_len == line_range.end() { + Edit::deletion(noqa_range.start() - leading_space_len, line_range.end()) } // Ex) `x = 1 # noqa # type: ignore` - else if locator.contents()[usize::from(noqa_range.end() + trailing_spaces)..].starts_with('#') + else if locator.contents()[usize::from(noqa_range.end() + trailing_space_len)..] + .starts_with('#') { - Edit::deletion(noqa_range.start(), noqa_range.end() + trailing_spaces) + Edit::deletion(noqa_range.start(), noqa_range.end() + trailing_space_len) } // Ex) `x = 1 # noqa here` else { Edit::deletion( noqa_range.start() + "# ".text_len(), - noqa_range.end() + trailing_spaces, + noqa_range.end() + trailing_space_len, ) } } diff --git a/crates/ruff/src/noqa.rs b/crates/ruff/src/noqa.rs index 1d8aa49dae..3d1422f675 100644 --- a/crates/ruff/src/noqa.rs +++ b/crates/ruff/src/noqa.rs @@ -25,99 +25,94 @@ static NOQA_LINE_REGEX: Lazy = Lazy::new(|| { .unwrap() }); +/// A directive to ignore a set of rules for a given line of Python source code (e.g., +/// `# noqa: F401, F841`). #[derive(Debug)] pub(crate) enum Directive<'a> { + /// No `noqa` directive was found. None, - // (leading spaces, noqa_range, trailing_spaces) - All(TextSize, TextRange, TextSize), - // (leading spaces, start_offset, end_offset, codes, trailing_spaces) - Codes(TextSize, TextRange, Vec<&'a str>, TextSize), + /// The `noqa` directive ignores all rules (e.g., `# noqa`). + All(All), + /// The `noqa` directive ignores specific rules (e.g., `# noqa: F401, F841`). + Codes(Codes<'a>), } -/// Extract the noqa `Directive` from a line of Python source code. -pub(crate) fn extract_noqa_directive<'a>(range: TextRange, locator: &'a Locator) -> Directive<'a> { - let text = &locator.contents()[range]; - match NOQA_LINE_REGEX.captures(text) { - Some(caps) => match ( - caps.name("leading_spaces"), - caps.name("noqa"), - caps.name("codes"), - caps.name("trailing_spaces"), - ) { - (Some(leading_spaces), Some(noqa), Some(codes), Some(trailing_spaces)) => { - let codes = codes - .as_str() - .split(|c: char| c.is_whitespace() || c == ',') - .map(str::trim) - .filter(|code| !code.is_empty()) - .collect_vec(); - let start = range.start() + TextSize::try_from(noqa.start()).unwrap(); - if codes.is_empty() { - #[allow(deprecated)] - let line = locator.compute_line_index(start); - warn!("Expected rule codes on `noqa` directive: \"{line}\""); +impl<'a> Directive<'a> { + /// Extract the noqa `Directive` from a line of Python source code. + pub(crate) fn extract(range: TextRange, locator: &'a Locator) -> Self { + let text = &locator.contents()[range]; + match NOQA_LINE_REGEX.captures(text) { + Some(caps) => match ( + caps.name("leading_spaces"), + caps.name("noqa"), + caps.name("codes"), + caps.name("trailing_spaces"), + ) { + (Some(leading_spaces), Some(noqa), Some(codes), Some(trailing_spaces)) => { + let codes = codes + .as_str() + .split(|c: char| c.is_whitespace() || c == ',') + .map(str::trim) + .filter(|code| !code.is_empty()) + .collect_vec(); + let start = range.start() + TextSize::try_from(noqa.start()).unwrap(); + if codes.is_empty() { + #[allow(deprecated)] + let line = locator.compute_line_index(start); + warn!("Expected rule codes on `noqa` directive: \"{line}\""); + } + + let leading_space_len = leading_spaces.as_str().text_len(); + let noqa_range = TextRange::at(start, noqa.as_str().text_len()); + let trailing_space_len = trailing_spaces.as_str().text_len(); + + Self::Codes(Codes { + leading_space_len, + noqa_range, + trailing_space_len, + codes, + }) } - Directive::Codes( - leading_spaces.as_str().text_len(), - TextRange::at(start, noqa.as_str().text_len()), - codes, - trailing_spaces.as_str().text_len(), - ) - } - - (Some(leading_spaces), Some(noqa), None, Some(trailing_spaces)) => Directive::All( - leading_spaces.as_str().text_len(), - TextRange::at( - range.start() + TextSize::try_from(noqa.start()).unwrap(), - noqa.as_str().text_len(), - ), - trailing_spaces.as_str().text_len(), - ), - _ => Directive::None, - }, - None => Directive::None, - } -} - -enum ParsedExemption<'a> { - None, - All, - Codes(Vec<&'a str>), -} - -/// Return a [`ParsedExemption`] for a given comment line. -fn parse_file_exemption(line: &str) -> ParsedExemption { - let line = line.trim_whitespace_start(); - - if line.starts_with("# flake8: noqa") - || line.starts_with("# flake8: NOQA") - || line.starts_with("# flake8: NoQA") - { - return ParsedExemption::All; - } - - if let Some(remainder) = line - .strip_prefix("# ruff: noqa") - .or_else(|| line.strip_prefix("# ruff: NOQA")) - .or_else(|| line.strip_prefix("# ruff: NoQA")) - { - if remainder.is_empty() { - return ParsedExemption::All; - } else if let Some(codes) = remainder.strip_prefix(':') { - let codes = codes - .split(|c: char| c.is_whitespace() || c == ',') - .map(str::trim) - .filter(|code| !code.is_empty()) - .collect_vec(); - if codes.is_empty() { - warn!("Expected rule codes on `noqa` directive: \"{line}\""); - } - return ParsedExemption::Codes(codes); + (Some(leading_spaces), Some(noqa), None, Some(trailing_spaces)) => { + let leading_space_len = leading_spaces.as_str().text_len(); + let noqa_range = TextRange::at( + range.start() + TextSize::try_from(noqa.start()).unwrap(), + noqa.as_str().text_len(), + ); + let trailing_space_len = trailing_spaces.as_str().text_len(); + Self::All(All { + leading_space_len, + noqa_range, + trailing_space_len, + }) + } + _ => Self::None, + }, + None => Self::None, } - warn!("Unexpected suffix on `noqa` directive: \"{line}\""); } +} - ParsedExemption::None +#[derive(Debug)] +pub(crate) struct All { + /// The length of the leading whitespace before the `noqa` directive. + pub(crate) leading_space_len: TextSize, + /// The range of the `noqa` directive. + pub(crate) noqa_range: TextRange, + /// The length of the trailing whitespace after the `noqa` directive. + pub(crate) trailing_space_len: TextSize, +} + +#[derive(Debug)] +pub(crate) struct Codes<'a> { + /// The length of the leading whitespace before the `noqa` directive. + pub(crate) leading_space_len: TextSize, + /// The range of the `noqa` directive. + pub(crate) noqa_range: TextRange, + /// The length of the trailing whitespace after the `noqa` directive. + pub(crate) trailing_space_len: TextSize, + /// The codes that are ignored by the `noqa` directive. + pub(crate) codes: Vec<&'a str>, } /// Returns `true` if the string list of `codes` includes `code` (or an alias @@ -138,47 +133,105 @@ pub(crate) fn rule_is_ignored( ) -> bool { let offset = noqa_line_for.resolve(offset); let line_range = locator.line_range(offset); - match extract_noqa_directive(line_range, locator) { + match Directive::extract(line_range, locator) { Directive::None => false, Directive::All(..) => true, - Directive::Codes(.., codes, _) => includes(code, &codes), + Directive::Codes(Codes { codes, .. }) => includes(code, &codes), } } +/// The file-level exemptions extracted from a given Python file. +#[derive(Debug)] pub(crate) enum FileExemption { + /// No file-level exemption. None, + /// The file is exempt from all rules. All, + /// The file is exempt from the given rules. Codes(Vec), } -/// Extract the [`FileExemption`] for a given Python source file, enumerating any rules that are -/// globally ignored within the file. -pub(crate) fn file_exemption(contents: &str, comment_ranges: &[TextRange]) -> FileExemption { - let mut exempt_codes: Vec = vec![]; +impl FileExemption { + /// Extract the [`FileExemption`] for a given Python source file, enumerating any rules that are + /// globally ignored within the file. + pub(crate) fn extract(contents: &str, comment_ranges: &[TextRange]) -> Self { + let mut exempt_codes: Vec = vec![]; - for range in comment_ranges { - match parse_file_exemption(&contents[*range]) { - ParsedExemption::All => { - return FileExemption::All; + for range in comment_ranges { + match ParsedFileExemption::extract(&contents[*range]) { + ParsedFileExemption::All => { + return Self::All; + } + ParsedFileExemption::Codes(codes) => { + exempt_codes.extend(codes.into_iter().filter_map(|code| { + if let Ok(rule) = Rule::from_code(get_redirect_target(code).unwrap_or(code)) + { + Some(rule.noqa_code()) + } else { + warn!("Invalid code provided to `# ruff: noqa`: {}", code); + None + } + })); + } + ParsedFileExemption::None => {} } - ParsedExemption::Codes(codes) => { - exempt_codes.extend(codes.into_iter().filter_map(|code| { - if let Ok(rule) = Rule::from_code(get_redirect_target(code).unwrap_or(code)) { - Some(rule.noqa_code()) - } else { - warn!("Invalid code provided to `# ruff: noqa`: {}", code); - None - } - })); - } - ParsedExemption::None => {} + } + + if exempt_codes.is_empty() { + Self::None + } else { + Self::Codes(exempt_codes) } } +} - if exempt_codes.is_empty() { - FileExemption::None - } else { - FileExemption::Codes(exempt_codes) +/// An individual file-level exemption (e.g., `# ruff: noqa` or `# ruff: noqa: F401, F841`). Like +/// [`FileExemption`], but only for a single line, as opposed to an aggregated set of exemptions +/// across a source file. +#[derive(Debug)] +enum ParsedFileExemption<'a> { + /// No file-level exemption was found. + None, + /// The file-level exemption ignores all rules (e.g., `# ruff: noqa`). + All, + /// The file-level exemption ignores specific rules (e.g., `# ruff: noqa: F401, F841`). + Codes(Vec<&'a str>), +} + +impl<'a> ParsedFileExemption<'a> { + /// Return a [`ParsedFileExemption`] for a given comment line. + fn extract(line: &'a str) -> Self { + let line = line.trim_whitespace_start(); + + if line.starts_with("# flake8: noqa") + || line.starts_with("# flake8: NOQA") + || line.starts_with("# flake8: NoQA") + { + return Self::All; + } + + if let Some(remainder) = line + .strip_prefix("# ruff: noqa") + .or_else(|| line.strip_prefix("# ruff: NOQA")) + .or_else(|| line.strip_prefix("# ruff: NoQA")) + { + if remainder.is_empty() { + return Self::All; + } else if let Some(codes) = remainder.strip_prefix(':') { + let codes = codes + .split(|c: char| c.is_whitespace() || c == ',') + .map(str::trim) + .filter(|code| !code.is_empty()) + .collect_vec(); + if codes.is_empty() { + warn!("Expected rule codes on `noqa` directive: \"{line}\""); + } + return Self::Codes(codes); + } + warn!("Unexpected suffix on `noqa` directive: \"{line}\""); + } + + Self::None } } @@ -215,7 +268,7 @@ fn add_noqa_inner( // Whether the file is exempted from all checks. // Codes that are globally exempted (within the current file). - let exemption = file_exemption(locator.contents(), commented_ranges); + let exemption = FileExemption::extract(locator.contents(), commented_ranges); let directives = NoqaDirectives::from_commented_ranges(commented_ranges, locator); // Mark any non-ignored diagnostics. @@ -243,7 +296,7 @@ fn add_noqa_inner( Directive::All(..) => { continue; } - Directive::Codes(.., codes, _) => { + Directive::Codes(Codes { codes, .. }) => { if includes(diagnostic.kind.rule(), codes) { continue; } @@ -261,7 +314,7 @@ fn add_noqa_inner( Directive::All(..) => { continue; } - Directive::Codes(.., codes, _) => { + Directive::Codes(Codes { codes, .. }) => { let rule = diagnostic.kind.rule(); if !includes(rule, codes) { matches_by_line @@ -311,7 +364,9 @@ fn add_noqa_inner( Some(Directive::All(..)) => { // Does not get inserted into the map. } - Some(Directive::Codes(_, noqa_range, existing, _)) => { + Some(Directive::Codes(Codes { + noqa_range, codes, .. + })) => { // Reconstruct the line based on the preserved rule codes. // This enables us to tally the number of edits. let output_start = output.len(); @@ -331,8 +386,8 @@ fn add_noqa_inner( &mut output, rules .iter() - .map(|r| r.noqa_code().to_string()) - .chain(existing.iter().map(ToString::to_string)) + .map(|rule| rule.noqa_code().to_string()) + .chain(codes.iter().map(ToString::to_string)) .sorted_unstable(), ); @@ -386,7 +441,7 @@ impl<'a> NoqaDirectives<'a> { for comment_range in comment_ranges { let line_range = locator.line_range(comment_range.start()); - let directive = match extract_noqa_directive(line_range, locator) { + let directive = match Directive::extract(line_range, locator) { Directive::None => { continue; } From cdb9fda3b8f91451113ad1028edeb4619fad3b55 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Jul 2023 17:49:07 -0400 Subject: [PATCH 336/447] Add debug-based snapshot tests for noqa directive parsing (#5535) ## Summary Better tests, helpful for future refactors. --- crates/ruff/src/noqa.rs | 129 ++++++++++++++++-- .../ruff__noqa__tests__noqa_all.snap | 11 ++ ...oqa__tests__noqa_all_case_insensitive.snap | 11 ++ ...ff__noqa__tests__noqa_all_multi_space.snap | 5 + .../ruff__noqa__tests__noqa_all_no_space.snap | 5 + .../ruff__noqa__tests__noqa_code.snap | 14 ++ ...qa__tests__noqa_code_case_insensitive.snap | 14 ++ ...f__noqa__tests__noqa_code_multi_space.snap | 5 + ...ruff__noqa__tests__noqa_code_no_space.snap | 5 + .../ruff__noqa__tests__noqa_codes.snap | 15 ++ ...a__tests__noqa_codes_case_insensitive.snap | 15 ++ ...__noqa__tests__noqa_codes_multi_space.snap | 5 + ...uff__noqa__tests__noqa_codes_no_space.snap | 5 + ...ruff__noqa__tests__noqa_leading_space.snap | 14 ++ ...uff__noqa__tests__noqa_trailing_space.snap | 14 ++ 15 files changed, 252 insertions(+), 15 deletions(-) create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_case_insensitive.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_multi_space.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_no_space.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_case_insensitive.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_multi_space.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_no_space.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_case_insensitive.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_multi_space.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_no_space.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_leading_space.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_trailing_space.snap diff --git a/crates/ruff/src/noqa.rs b/crates/ruff/src/noqa.rs index 3d1422f675..64f953811c 100644 --- a/crates/ruff/src/noqa.rs +++ b/crates/ruff/src/noqa.rs @@ -20,7 +20,7 @@ use crate::rule_redirects::get_redirect_target; static NOQA_LINE_REGEX: Lazy = Lazy::new(|| { Regex::new( - r"(?P\s*)(?P(?i:# noqa)(?::\s?(?P(?:[A-Z]+[0-9]+)(?:[,\s]+[A-Z]+[0-9]+)*))?)(?P\s*)", + r"(?P\s*)(?P(?i:# noqa)(?::\s?(?P[A-Z]+[0-9]+(?:[,\s]+[A-Z]+[0-9]+)*))?)(?P\s*)", ) .unwrap() }); @@ -566,28 +566,127 @@ impl FromIterator for NoqaMapping { #[cfg(test)] mod tests { + use insta::assert_debug_snapshot; use ruff_text_size::{TextRange, TextSize}; use ruff_diagnostics::Diagnostic; use ruff_python_ast::source_code::Locator; use ruff_python_whitespace::LineEnding; - use crate::noqa::{add_noqa_inner, NoqaMapping, NOQA_LINE_REGEX}; + use crate::noqa::{add_noqa_inner, Directive, NoqaMapping}; use crate::rules::pycodestyle::rules::AmbiguousVariableName; - use crate::rules::pyflakes; + use crate::rules::pyflakes::rules::UnusedVariable; #[test] - fn regex() { - assert!(NOQA_LINE_REGEX.is_match("# noqa")); - assert!(NOQA_LINE_REGEX.is_match("# NoQA")); + fn noqa_all() { + let source = "# noqa"; + let range = TextRange::new(TextSize::from(0), TextSize::from(6)); + let locator = Locator::new(source); + assert_debug_snapshot!(Directive::extract(range, &locator)); + } - assert!(NOQA_LINE_REGEX.is_match("# noqa: F401")); - assert!(NOQA_LINE_REGEX.is_match("# NoQA: F401")); - assert!(NOQA_LINE_REGEX.is_match("# noqa: F401, E501")); + #[test] + fn noqa_code() { + let source = "# noqa: F401"; + let range = TextRange::new(TextSize::from(0), TextSize::from(12)); + let locator = Locator::new(source); + assert_debug_snapshot!(Directive::extract(range, &locator)); + } - assert!(NOQA_LINE_REGEX.is_match("# noqa:F401")); - assert!(NOQA_LINE_REGEX.is_match("# NoQA:F401")); - assert!(NOQA_LINE_REGEX.is_match("# noqa:F401, E501")); + #[test] + fn noqa_codes() { + let source = "# noqa: F401, F841"; + let range = TextRange::new(TextSize::from(0), TextSize::from(18)); + let locator = Locator::new(source); + assert_debug_snapshot!(Directive::extract(range, &locator)); + } + + #[test] + fn noqa_all_case_insensitive() { + let source = "# NOQA"; + let range = TextRange::new(TextSize::from(0), TextSize::from(6)); + let locator = Locator::new(source); + assert_debug_snapshot!(Directive::extract(range, &locator)); + } + + #[test] + fn noqa_code_case_insensitive() { + let source = "# NOQA: F401"; + let range = TextRange::new(TextSize::from(0), TextSize::from(12)); + let locator = Locator::new(source); + assert_debug_snapshot!(Directive::extract(range, &locator)); + } + + #[test] + fn noqa_codes_case_insensitive() { + let source = "# NOQA: F401, F841"; + let range = TextRange::new(TextSize::from(0), TextSize::from(18)); + let locator = Locator::new(source); + assert_debug_snapshot!(Directive::extract(range, &locator)); + } + + #[test] + fn noqa_leading_space() { + let source = "# # noqa: F401"; + let range = TextRange::new(TextSize::from(0), TextSize::from(16)); + let locator = Locator::new(source); + assert_debug_snapshot!(Directive::extract(range, &locator)); + } + + #[test] + fn noqa_trailing_space() { + let source = "# noqa: F401 #"; + let range = TextRange::new(TextSize::from(0), TextSize::from(16)); + let locator = Locator::new(source); + assert_debug_snapshot!(Directive::extract(range, &locator)); + } + + #[test] + fn noqa_all_no_space() { + let source = "#noqa"; + let range = TextRange::new(TextSize::from(0), TextSize::from(5)); + let locator = Locator::new(source); + assert_debug_snapshot!(Directive::extract(range, &locator)); + } + + #[test] + fn noqa_code_no_space() { + let source = "#noqa:F401"; + let range = TextRange::new(TextSize::from(0), TextSize::from(10)); + let locator = Locator::new(source); + assert_debug_snapshot!(Directive::extract(range, &locator)); + } + + #[test] + fn noqa_codes_no_space() { + let source = "#noqa:F401,F841"; + let range = TextRange::new(TextSize::from(0), TextSize::from(15)); + let locator = Locator::new(source); + assert_debug_snapshot!(Directive::extract(range, &locator)); + } + + #[test] + fn noqa_all_multi_space() { + let source = "# noqa"; + let range = TextRange::new(TextSize::from(0), TextSize::from(7)); + let locator = Locator::new(source); + assert_debug_snapshot!(Directive::extract(range, &locator)); + } + + #[test] + fn noqa_code_multi_space() { + let source = "# noqa: F401"; + let range = TextRange::new(TextSize::from(0), TextSize::from(13)); + let locator = Locator::new(source); + assert_debug_snapshot!(Directive::extract(range, &locator)); + } + + #[test] + fn noqa_codes_multi_space() { + let source = "# noqa: F401, F841"; + let range = TextRange::new(TextSize::from(0), TextSize::from(20)); + let locator = Locator::new(source); + assert_debug_snapshot!(Directive::extract(range, &locator)); } #[test] @@ -605,7 +704,7 @@ mod tests { assert_eq!(output, format!("{contents}")); let diagnostics = [Diagnostic::new( - pyflakes::rules::UnusedVariable { + UnusedVariable { name: "x".to_string(), }, TextRange::new(TextSize::from(0), TextSize::from(0)), @@ -629,7 +728,7 @@ mod tests { TextRange::new(TextSize::from(0), TextSize::from(0)), ), Diagnostic::new( - pyflakes::rules::UnusedVariable { + UnusedVariable { name: "x".to_string(), }, TextRange::new(TextSize::from(0), TextSize::from(0)), @@ -653,7 +752,7 @@ mod tests { TextRange::new(TextSize::from(0), TextSize::from(0)), ), Diagnostic::new( - pyflakes::rules::UnusedVariable { + UnusedVariable { name: "x".to_string(), }, TextRange::new(TextSize::from(0), TextSize::from(0)), diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all.snap new file mode 100644 index 0000000000..d87458d852 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::extract(range, &locator)" +--- +All( + All { + leading_space_len: 0, + noqa_range: 0..6, + trailing_space_len: 0, + }, +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_case_insensitive.snap new file mode 100644 index 0000000000..d87458d852 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_case_insensitive.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::extract(range, &locator)" +--- +All( + All { + leading_space_len: 0, + noqa_range: 0..6, + trailing_space_len: 0, + }, +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_multi_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_multi_space.snap new file mode 100644 index 0000000000..57be5a0532 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_multi_space.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::extract(range, &locator)" +--- +None diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_no_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_no_space.snap new file mode 100644 index 0000000000..57be5a0532 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_no_space.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::extract(range, &locator)" +--- +None diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code.snap new file mode 100644 index 0000000000..4620f00ce0 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code.snap @@ -0,0 +1,14 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::extract(range, &locator)" +--- +Codes( + Codes { + leading_space_len: 0, + noqa_range: 0..12, + trailing_space_len: 0, + codes: [ + "F401", + ], + }, +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_case_insensitive.snap new file mode 100644 index 0000000000..4620f00ce0 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_case_insensitive.snap @@ -0,0 +1,14 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::extract(range, &locator)" +--- +Codes( + Codes { + leading_space_len: 0, + noqa_range: 0..12, + trailing_space_len: 0, + codes: [ + "F401", + ], + }, +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_multi_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_multi_space.snap new file mode 100644 index 0000000000..57be5a0532 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_multi_space.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::extract(range, &locator)" +--- +None diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_no_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_no_space.snap new file mode 100644 index 0000000000..57be5a0532 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_no_space.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::extract(range, &locator)" +--- +None diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes.snap new file mode 100644 index 0000000000..4b2854c1f6 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes.snap @@ -0,0 +1,15 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::extract(range, &locator)" +--- +Codes( + Codes { + leading_space_len: 0, + noqa_range: 0..18, + trailing_space_len: 0, + codes: [ + "F401", + "F841", + ], + }, +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_case_insensitive.snap new file mode 100644 index 0000000000..4b2854c1f6 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_case_insensitive.snap @@ -0,0 +1,15 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::extract(range, &locator)" +--- +Codes( + Codes { + leading_space_len: 0, + noqa_range: 0..18, + trailing_space_len: 0, + codes: [ + "F401", + "F841", + ], + }, +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_multi_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_multi_space.snap new file mode 100644 index 0000000000..57be5a0532 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_multi_space.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::extract(range, &locator)" +--- +None diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_no_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_no_space.snap new file mode 100644 index 0000000000..57be5a0532 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_no_space.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::extract(range, &locator)" +--- +None diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_leading_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_leading_space.snap new file mode 100644 index 0000000000..cef106a235 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_leading_space.snap @@ -0,0 +1,14 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::extract(range, &locator)" +--- +Codes( + Codes { + leading_space_len: 3, + noqa_range: 4..16, + trailing_space_len: 0, + codes: [ + "F401", + ], + }, +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_trailing_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_trailing_space.snap new file mode 100644 index 0000000000..54e666a3c4 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_trailing_space.snap @@ -0,0 +1,14 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::extract(range, &locator)" +--- +Codes( + Codes { + leading_space_len: 0, + noqa_range: 0..12, + trailing_space_len: 3, + codes: [ + "F401", + ], + }, +) From ea270da2892849aceb1fb05cbdf08a2c67cebd77 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Jul 2023 18:06:01 -0400 Subject: [PATCH 337/447] Move some MkDocs responsibilities around (#5542) ## Summary Note that I've also changed from `mkdocs serve` to `mkdocs serve -f mkdocs.generated.yml` to be clearer that this is a generated file. --- .github/workflows/ci.yaml | 2 +- .github/workflows/docs.yaml | 2 +- .gitignore | 3 +-- CONTRIBUTING.md | 2 +- README.md | 3 ++- .../integrations/analytics/fathom.html | 1 + mkdocs.template.yml | 5 +++- scripts/generate_mkdocs.py | 26 ++++++------------- 8 files changed, 19 insertions(+), 25 deletions(-) create mode 100644 docs/.overrides/partials/integrations/analytics/fathom.html diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9346c527a5..58969ed587 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -271,4 +271,4 @@ jobs: - name: "Check docs formatting" run: python scripts/check_docs_formatted.py - name: "Build docs" - run: mkdocs build --strict + run: mkdocs build --strict -f mkdocs.generated.yml diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 27f3bacd7d..56774ee991 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -26,7 +26,7 @@ jobs: run: | python scripts/transform_readme.py --target mkdocs python scripts/generate_mkdocs.py - mkdocs build --strict + mkdocs build --strict -f mkdocs.generated.yml - name: "Deploy to Cloudflare Pages" if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }} uses: cloudflare/wrangler-action@2.0.0 diff --git a/.gitignore b/.gitignore index 57a5b8f8ce..5bfce3449a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,7 @@ # Benchmarking cpython (CONTRIBUTING.md) crates/ruff/resources/test/cpython # generate_mkdocs.py -mkdocs.yml -.overrides +mkdocs.generated.yml # check_ecosystem.py ruff-old github_search*.jsonl diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 153b43fa39..efd43b4c7e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -256,7 +256,7 @@ To preview any changes to the documentation locally: 1. Run the development server with: ```shell - mkdocs serve + mkdocs serve -f mkdocs.generated.yml ``` The documentation should then be available locally at diff --git a/README.md b/README.md index f333191ea2..b488d61eac 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ An extremely fast Python linter, written in Rust. - ⚖️ [Near-parity](https://beta.ruff.rs/docs/faq/#how-does-ruff-compare-to-flake8) with the built-in Flake8 rule set - 🔌 Native re-implementations of dozens of Flake8 plugins, like flake8-bugbear -- ⌨️ First-party editor integrations for [VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://github.com/astral-sh/ruff-lsp) +- ⌨️ First-party [editor integrations](https://beta.ruff.rs/docs/editor-integrations/) for + [VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://github.com/astral-sh/ruff-lsp) - 🌎 Monorepo-friendly, with [hierarchical and cascading configuration](https://beta.ruff.rs/docs/configuration/#pyprojecttoml-discovery) Ruff aims to be orders of magnitude faster than alternative tools while integrating more diff --git a/docs/.overrides/partials/integrations/analytics/fathom.html b/docs/.overrides/partials/integrations/analytics/fathom.html new file mode 100644 index 0000000000..340d60d816 --- /dev/null +++ b/docs/.overrides/partials/integrations/analytics/fathom.html @@ -0,0 +1 @@ + diff --git a/mkdocs.template.yml b/mkdocs.template.yml index ec7dab6ced..761c36be23 100644 --- a/mkdocs.template.yml +++ b/mkdocs.template.yml @@ -23,7 +23,7 @@ theme: toggle: icon: material/weather-night name: Switch to light mode - custom_dir: .overrides + custom_dir: docs/.overrides repo_url: https://github.com/astral-sh/ruff repo_name: ruff site_author: charliermarsh @@ -50,3 +50,6 @@ plugins: - search extra_css: - stylesheets/extra.css +extra: + analytics: + provider: fathom diff --git a/scripts/generate_mkdocs.py b/scripts/generate_mkdocs.py index 8d0b92b882..f6954b153a 100644 --- a/scripts/generate_mkdocs.py +++ b/scripts/generate_mkdocs.py @@ -32,10 +32,6 @@ SECTIONS: list[Section] = [ Section("Contributing", "contributing.md", generated=True), ] -FATHOM_SCRIPT: str = ( - '" -) LINK_REWRITES: dict[str, str] = { "https://beta.ruff.rs/docs/": "index.md", @@ -45,7 +41,6 @@ LINK_REWRITES: dict[str, str] = { ), "https://beta.ruff.rs/docs/contributing/": "contributing.md", "https://beta.ruff.rs/docs/editor-integrations/": "editor-integrations.md", - "https://beta.ruff.rs/docs/faq/": "faq.md", "https://beta.ruff.rs/docs/faq/#how-does-ruff-compare-to-flake8": ( "faq.md#how-does-ruff-compare-to-flake8" ), @@ -53,7 +48,6 @@ LINK_REWRITES: dict[str, str] = { "https://beta.ruff.rs/docs/rules/": "rules.md", "https://beta.ruff.rs/docs/rules/#error-e": "rules.md#error-e", "https://beta.ruff.rs/docs/settings/": "settings.md", - "https://beta.ruff.rs/docs/usage/": "usage.md", } @@ -92,7 +86,13 @@ def main() -> None: # Rewrite links to the documentation. for src, dst in LINK_REWRITES.items(): - content = content.replace(f"({src})", f"({dst})") + before = content + after = content.replace(f"({src})", f"({dst})") + if before == after: + msg = f"Unexpected link rewrite in README.md: {src}" + raise ValueError(msg) + content = after + if m := re.search(r"\(https://beta.ruff.rs/docs/.*\)", content): msg = f"Unexpected absolute link to documentation: {m.group(0)}" raise ValueError(msg) @@ -140,18 +140,8 @@ def main() -> None: with Path("mkdocs.template.yml").open(encoding="utf8") as fp: config = yaml.safe_load(fp) config["nav"] = [{section.title: section.filename} for section in SECTIONS] - config["extra"] = {"analytics": {"provider": "fathom"}} - Path(".overrides/partials/integrations/analytics").mkdir( - parents=True, - exist_ok=True, - ) - with Path(".overrides/partials/integrations/analytics/fathom.html").open( - "w+", - ) as fp: - fp.write(FATHOM_SCRIPT) - - with Path("mkdocs.yml").open("w+") as fp: + with Path("mkdocs.generated.yml").open("w+") as fp: yaml.safe_dump(config, fp) From d097b49371fda053fe4d2d9cf33e8297987e70b2 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Jul 2023 18:22:21 -0400 Subject: [PATCH 338/447] Remove `Directive::None` variant (#5543) ## Summary This is creating some weird, impossible states. Make impossible states unrepresentable! --- crates/ruff/src/checkers/noqa.rs | 10 +- crates/ruff/src/noqa.rs | 95 ++++++++----------- .../ruff__noqa__tests__noqa_all.snap | 14 +-- ...oqa__tests__noqa_all_case_insensitive.snap | 14 +-- .../ruff__noqa__tests__noqa_code.snap | 20 ++-- ...qa__tests__noqa_code_case_insensitive.snap | 20 ++-- .../ruff__noqa__tests__noqa_codes.snap | 22 +++-- ...a__tests__noqa_codes_case_insensitive.snap | 22 +++-- ...ruff__noqa__tests__noqa_leading_space.snap | 20 ++-- ...uff__noqa__tests__noqa_trailing_space.snap | 20 ++-- 10 files changed, 130 insertions(+), 127 deletions(-) diff --git a/crates/ruff/src/checkers/noqa.rs b/crates/ruff/src/checkers/noqa.rs index 9cb887cd41..83b90cd2c8 100644 --- a/crates/ruff/src/checkers/noqa.rs +++ b/crates/ruff/src/checkers/noqa.rs @@ -22,7 +22,7 @@ pub(crate) fn check_noqa( settings: &Settings, ) -> Vec { // Identify any codes that are globally exempted (within the current file). - let exemption = FileExemption::extract(locator.contents(), comment_ranges); + let exemption = FileExemption::try_extract(locator.contents(), comment_ranges); // Extract all `noqa` directives. let mut noqa_directives = NoqaDirectives::from_commented_ranges(comment_ranges, locator); @@ -37,19 +37,19 @@ pub(crate) fn check_noqa( } match &exemption { - FileExemption::All => { + Some(FileExemption::All) => { // If the file is exempted, ignore all diagnostics. ignored_diagnostics.push(index); continue; } - FileExemption::Codes(codes) => { + Some(FileExemption::Codes(codes)) => { // If the diagnostic is ignored by a global exemption, ignore it. if codes.contains(&diagnostic.kind.rule().noqa_code()) { ignored_diagnostics.push(index); continue; } } - FileExemption::None => {} + None => {} } let noqa_offsets = diagnostic @@ -81,7 +81,6 @@ pub(crate) fn check_noqa( false } } - Directive::None => unreachable!(), }; if suppressed { @@ -197,7 +196,6 @@ pub(crate) fn check_noqa( diagnostics.push(diagnostic); } } - Directive::None => {} } } } diff --git a/crates/ruff/src/noqa.rs b/crates/ruff/src/noqa.rs index 64f953811c..83f724348b 100644 --- a/crates/ruff/src/noqa.rs +++ b/crates/ruff/src/noqa.rs @@ -29,8 +29,6 @@ static NOQA_LINE_REGEX: Lazy = Lazy::new(|| { /// `# noqa: F401, F841`). #[derive(Debug)] pub(crate) enum Directive<'a> { - /// No `noqa` directive was found. - None, /// The `noqa` directive ignores all rules (e.g., `# noqa`). All(All), /// The `noqa` directive ignores specific rules (e.g., `# noqa: F401, F841`). @@ -39,7 +37,7 @@ pub(crate) enum Directive<'a> { impl<'a> Directive<'a> { /// Extract the noqa `Directive` from a line of Python source code. - pub(crate) fn extract(range: TextRange, locator: &'a Locator) -> Self { + pub(crate) fn try_extract(range: TextRange, locator: &'a Locator) -> Option { let text = &locator.contents()[range]; match NOQA_LINE_REGEX.captures(text) { Some(caps) => match ( @@ -66,12 +64,12 @@ impl<'a> Directive<'a> { let noqa_range = TextRange::at(start, noqa.as_str().text_len()); let trailing_space_len = trailing_spaces.as_str().text_len(); - Self::Codes(Codes { + Some(Self::Codes(Codes { leading_space_len, noqa_range, trailing_space_len, codes, - }) + })) } (Some(leading_spaces), Some(noqa), None, Some(trailing_spaces)) => { let leading_space_len = leading_spaces.as_str().text_len(); @@ -80,15 +78,15 @@ impl<'a> Directive<'a> { noqa.as_str().text_len(), ); let trailing_space_len = trailing_spaces.as_str().text_len(); - Self::All(All { + Some(Self::All(All { leading_space_len, noqa_range, trailing_space_len, - }) + })) } - _ => Self::None, + _ => None, }, - None => Self::None, + None => None, } } } @@ -133,18 +131,16 @@ pub(crate) fn rule_is_ignored( ) -> bool { let offset = noqa_line_for.resolve(offset); let line_range = locator.line_range(offset); - match Directive::extract(line_range, locator) { - Directive::None => false, - Directive::All(..) => true, - Directive::Codes(Codes { codes, .. }) => includes(code, &codes), + match Directive::try_extract(line_range, locator) { + Some(Directive::All(..)) => true, + Some(Directive::Codes(Codes { codes, .. })) => includes(code, &codes), + None => false, } } /// The file-level exemptions extracted from a given Python file. #[derive(Debug)] pub(crate) enum FileExemption { - /// No file-level exemption. - None, /// The file is exempt from all rules. All, /// The file is exempt from the given rules. @@ -154,13 +150,13 @@ pub(crate) enum FileExemption { impl FileExemption { /// Extract the [`FileExemption`] for a given Python source file, enumerating any rules that are /// globally ignored within the file. - pub(crate) fn extract(contents: &str, comment_ranges: &[TextRange]) -> Self { + pub(crate) fn try_extract(contents: &str, comment_ranges: &[TextRange]) -> Option { let mut exempt_codes: Vec = vec![]; for range in comment_ranges { match ParsedFileExemption::extract(&contents[*range]) { ParsedFileExemption::All => { - return Self::All; + return Some(Self::All); } ParsedFileExemption::Codes(codes) => { exempt_codes.extend(codes.into_iter().filter_map(|code| { @@ -178,9 +174,9 @@ impl FileExemption { } if exempt_codes.is_empty() { - Self::None + None } else { - Self::Codes(exempt_codes) + Some(Self::Codes(exempt_codes)) } } } @@ -268,23 +264,23 @@ fn add_noqa_inner( // Whether the file is exempted from all checks. // Codes that are globally exempted (within the current file). - let exemption = FileExemption::extract(locator.contents(), commented_ranges); + let exemption = FileExemption::try_extract(locator.contents(), commented_ranges); let directives = NoqaDirectives::from_commented_ranges(commented_ranges, locator); // Mark any non-ignored diagnostics. for diagnostic in diagnostics { match &exemption { - FileExemption::All => { + Some(FileExemption::All) => { // If the file is exempted, don't add any noqa directives. continue; } - FileExemption::Codes(codes) => { + Some(FileExemption::Codes(codes)) => { // If the diagnostic is ignored by a global exemption, don't add a noqa directive. if codes.contains(&diagnostic.kind.rule().noqa_code()) { continue; } } - FileExemption::None => {} + None => {} } // Is the violation ignored by a `noqa` directive on the parent line? @@ -301,14 +297,13 @@ fn add_noqa_inner( continue; } } - Directive::None => {} } } } let noqa_offset = noqa_line_for.resolve(diagnostic.start()); - // Or ignored by the directive itself + // Or ignored by the directive itself? if let Some(directive_line) = directives.find_line_with_directive(noqa_offset) { match &directive_line.directive { Directive::All(..) => { @@ -327,7 +322,6 @@ fn add_noqa_inner( } continue; } - Directive::None => {} } } @@ -349,7 +343,7 @@ fn add_noqa_inner( let line = locator.full_line(offset); match directive { - None | Some(Directive::None) => { + None => { // Add existing content. output.push_str(line.trim_end()); @@ -441,19 +435,14 @@ impl<'a> NoqaDirectives<'a> { for comment_range in comment_ranges { let line_range = locator.line_range(comment_range.start()); - let directive = match Directive::extract(line_range, locator) { - Directive::None => { - continue; - } - directive @ (Directive::All(..) | Directive::Codes(..)) => directive, + if let Some(directive) = Directive::try_extract(line_range, locator) { + // noqa comments are guaranteed to be single line. + directives.push(NoqaDirectiveLine { + range: line_range, + directive, + matches: Vec::new(), + }); }; - - // noqa comments are guaranteed to be single line. - directives.push(NoqaDirectiveLine { - range: line_range, - directive, - matches: Vec::new(), - }); } // Extend a mapping at the end of the file to also include the EOF token. @@ -582,7 +571,7 @@ mod tests { let source = "# noqa"; let range = TextRange::new(TextSize::from(0), TextSize::from(6)); let locator = Locator::new(source); - assert_debug_snapshot!(Directive::extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(range, &locator)); } #[test] @@ -590,7 +579,7 @@ mod tests { let source = "# noqa: F401"; let range = TextRange::new(TextSize::from(0), TextSize::from(12)); let locator = Locator::new(source); - assert_debug_snapshot!(Directive::extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(range, &locator)); } #[test] @@ -598,7 +587,7 @@ mod tests { let source = "# noqa: F401, F841"; let range = TextRange::new(TextSize::from(0), TextSize::from(18)); let locator = Locator::new(source); - assert_debug_snapshot!(Directive::extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(range, &locator)); } #[test] @@ -606,7 +595,7 @@ mod tests { let source = "# NOQA"; let range = TextRange::new(TextSize::from(0), TextSize::from(6)); let locator = Locator::new(source); - assert_debug_snapshot!(Directive::extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(range, &locator)); } #[test] @@ -614,7 +603,7 @@ mod tests { let source = "# NOQA: F401"; let range = TextRange::new(TextSize::from(0), TextSize::from(12)); let locator = Locator::new(source); - assert_debug_snapshot!(Directive::extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(range, &locator)); } #[test] @@ -622,7 +611,7 @@ mod tests { let source = "# NOQA: F401, F841"; let range = TextRange::new(TextSize::from(0), TextSize::from(18)); let locator = Locator::new(source); - assert_debug_snapshot!(Directive::extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(range, &locator)); } #[test] @@ -630,7 +619,7 @@ mod tests { let source = "# # noqa: F401"; let range = TextRange::new(TextSize::from(0), TextSize::from(16)); let locator = Locator::new(source); - assert_debug_snapshot!(Directive::extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(range, &locator)); } #[test] @@ -638,7 +627,7 @@ mod tests { let source = "# noqa: F401 #"; let range = TextRange::new(TextSize::from(0), TextSize::from(16)); let locator = Locator::new(source); - assert_debug_snapshot!(Directive::extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(range, &locator)); } #[test] @@ -646,7 +635,7 @@ mod tests { let source = "#noqa"; let range = TextRange::new(TextSize::from(0), TextSize::from(5)); let locator = Locator::new(source); - assert_debug_snapshot!(Directive::extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(range, &locator)); } #[test] @@ -654,7 +643,7 @@ mod tests { let source = "#noqa:F401"; let range = TextRange::new(TextSize::from(0), TextSize::from(10)); let locator = Locator::new(source); - assert_debug_snapshot!(Directive::extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(range, &locator)); } #[test] @@ -662,7 +651,7 @@ mod tests { let source = "#noqa:F401,F841"; let range = TextRange::new(TextSize::from(0), TextSize::from(15)); let locator = Locator::new(source); - assert_debug_snapshot!(Directive::extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(range, &locator)); } #[test] @@ -670,7 +659,7 @@ mod tests { let source = "# noqa"; let range = TextRange::new(TextSize::from(0), TextSize::from(7)); let locator = Locator::new(source); - assert_debug_snapshot!(Directive::extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(range, &locator)); } #[test] @@ -678,7 +667,7 @@ mod tests { let source = "# noqa: F401"; let range = TextRange::new(TextSize::from(0), TextSize::from(13)); let locator = Locator::new(source); - assert_debug_snapshot!(Directive::extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(range, &locator)); } #[test] @@ -686,7 +675,7 @@ mod tests { let source = "# noqa: F401, F841"; let range = TextRange::new(TextSize::from(0), TextSize::from(20)); let locator = Locator::new(source); - assert_debug_snapshot!(Directive::extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(range, &locator)); } #[test] diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all.snap index d87458d852..260721baf2 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all.snap @@ -2,10 +2,12 @@ source: crates/ruff/src/noqa.rs expression: "Directive::extract(range, &locator)" --- -All( - All { - leading_space_len: 0, - noqa_range: 0..6, - trailing_space_len: 0, - }, +Some( + All( + All { + leading_space_len: 0, + noqa_range: 0..6, + trailing_space_len: 0, + }, + ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_case_insensitive.snap index d87458d852..260721baf2 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_case_insensitive.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_case_insensitive.snap @@ -2,10 +2,12 @@ source: crates/ruff/src/noqa.rs expression: "Directive::extract(range, &locator)" --- -All( - All { - leading_space_len: 0, - noqa_range: 0..6, - trailing_space_len: 0, - }, +Some( + All( + All { + leading_space_len: 0, + noqa_range: 0..6, + trailing_space_len: 0, + }, + ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code.snap index 4620f00ce0..bfb5b2f11e 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code.snap @@ -2,13 +2,15 @@ source: crates/ruff/src/noqa.rs expression: "Directive::extract(range, &locator)" --- -Codes( - Codes { - leading_space_len: 0, - noqa_range: 0..12, - trailing_space_len: 0, - codes: [ - "F401", - ], - }, +Some( + Codes( + Codes { + leading_space_len: 0, + noqa_range: 0..12, + trailing_space_len: 0, + codes: [ + "F401", + ], + }, + ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_case_insensitive.snap index 4620f00ce0..bfb5b2f11e 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_case_insensitive.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_case_insensitive.snap @@ -2,13 +2,15 @@ source: crates/ruff/src/noqa.rs expression: "Directive::extract(range, &locator)" --- -Codes( - Codes { - leading_space_len: 0, - noqa_range: 0..12, - trailing_space_len: 0, - codes: [ - "F401", - ], - }, +Some( + Codes( + Codes { + leading_space_len: 0, + noqa_range: 0..12, + trailing_space_len: 0, + codes: [ + "F401", + ], + }, + ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes.snap index 4b2854c1f6..aecdd27050 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes.snap @@ -2,14 +2,16 @@ source: crates/ruff/src/noqa.rs expression: "Directive::extract(range, &locator)" --- -Codes( - Codes { - leading_space_len: 0, - noqa_range: 0..18, - trailing_space_len: 0, - codes: [ - "F401", - "F841", - ], - }, +Some( + Codes( + Codes { + leading_space_len: 0, + noqa_range: 0..18, + trailing_space_len: 0, + codes: [ + "F401", + "F841", + ], + }, + ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_case_insensitive.snap index 4b2854c1f6..aecdd27050 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_case_insensitive.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_case_insensitive.snap @@ -2,14 +2,16 @@ source: crates/ruff/src/noqa.rs expression: "Directive::extract(range, &locator)" --- -Codes( - Codes { - leading_space_len: 0, - noqa_range: 0..18, - trailing_space_len: 0, - codes: [ - "F401", - "F841", - ], - }, +Some( + Codes( + Codes { + leading_space_len: 0, + noqa_range: 0..18, + trailing_space_len: 0, + codes: [ + "F401", + "F841", + ], + }, + ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_leading_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_leading_space.snap index cef106a235..27602af1d0 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_leading_space.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_leading_space.snap @@ -2,13 +2,15 @@ source: crates/ruff/src/noqa.rs expression: "Directive::extract(range, &locator)" --- -Codes( - Codes { - leading_space_len: 3, - noqa_range: 4..16, - trailing_space_len: 0, - codes: [ - "F401", - ], - }, +Some( + Codes( + Codes { + leading_space_len: 3, + noqa_range: 4..16, + trailing_space_len: 0, + codes: [ + "F401", + ], + }, + ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_trailing_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_trailing_space.snap index 54e666a3c4..bc8c548b14 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_trailing_space.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_trailing_space.snap @@ -2,13 +2,15 @@ source: crates/ruff/src/noqa.rs expression: "Directive::extract(range, &locator)" --- -Codes( - Codes { - leading_space_len: 0, - noqa_range: 0..12, - trailing_space_len: 3, - codes: [ - "F401", - ], - }, +Some( + Codes( + Codes { + leading_space_len: 0, + noqa_range: 0..12, + trailing_space_len: 3, + codes: [ + "F401", + ], + }, + ), ) From c9e02c52a852370bd9e88a9ffb74e8b5dced03b5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Jul 2023 18:40:21 -0400 Subject: [PATCH 339/447] Add separate configuration for MkDocs Insiders plugins (#5544) ## Summary This PR adds a separate configuration file to enable us to turn on [Insiders-only plugins](https://squidfunk.github.io/mkdocs-material/insiders/getting-started/#built-in-plugins). I've turned on the `typeset` plugin which ensures that the settings on the left-hand navigation pane render as code: Screen Shot 2023-07-05 at 6 27 20 PM --- .github/workflows/ci.yaml | 2 +- .github/workflows/docs.yaml | 2 +- CONTRIBUTING.md | 4 ++++ crates/ruff_dev/src/generate_options.rs | 2 +- mkdocs.insiders.yml | 4 ++++ mkdocs.template.yml | 1 + 6 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 mkdocs.insiders.yml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 58969ed587..211c90d17b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -271,4 +271,4 @@ jobs: - name: "Check docs formatting" run: python scripts/check_docs_formatted.py - name: "Build docs" - run: mkdocs build --strict -f mkdocs.generated.yml + run: mkdocs build --strict -f mkdocs.insiders.yml diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 56774ee991..986ba0fa4c 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -26,7 +26,7 @@ jobs: run: | python scripts/transform_readme.py --target mkdocs python scripts/generate_mkdocs.py - mkdocs build --strict -f mkdocs.generated.yml + mkdocs build --strict -f mkdocs.insiders.yml - name: "Deploy to Cloudflare Pages" if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }} uses: cloudflare/wrangler-action@2.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index efd43b4c7e..8c51d5f6b3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -256,7 +256,11 @@ To preview any changes to the documentation locally: 1. Run the development server with: ```shell + # For contributors. mkdocs serve -f mkdocs.generated.yml + + # For members of the Astral org, which has access to MkDocs Insiders via sponsorship. + mkdocs serve -f mkdocs.insiders.yml ``` The documentation should then be available locally at diff --git a/crates/ruff_dev/src/generate_options.rs b/crates/ruff_dev/src/generate_options.rs index 4e9c5fe681..7737f2097f 100644 --- a/crates/ruff_dev/src/generate_options.rs +++ b/crates/ruff_dev/src/generate_options.rs @@ -58,7 +58,7 @@ pub(crate) fn generate() -> String { let OptionEntry::Group(fields) = entry else { continue; }; - output.push_str(&format!("### `{group_name}`\n")); + 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 { diff --git a/mkdocs.insiders.yml b/mkdocs.insiders.yml new file mode 100644 index 0000000000..7435eb6a01 --- /dev/null +++ b/mkdocs.insiders.yml @@ -0,0 +1,4 @@ +INHERIT: mkdocs.generated.yml +plugins: + - search + - typeset diff --git a/mkdocs.template.yml b/mkdocs.template.yml index 761c36be23..8f4ec08a09 100644 --- a/mkdocs.template.yml +++ b/mkdocs.template.yml @@ -5,6 +5,7 @@ theme: favicon: assets/ruff-favicon.png features: - navigation.instant + - navigation.instant.prefetch - navigation.tracking - content.code.annotate - toc.integrate From e4596ebc35353007615cde420d5b5f88f3713c2c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Jul 2023 19:03:06 -0400 Subject: [PATCH 340/447] Remove leading and trailing space length from `Directive` (#5545) ## Summary We only need this in one place (when removing the directive), and it simplifies a lot of details to just compute it there. --- crates/ruff/src/checkers/noqa.rs | 75 +++++------ crates/ruff/src/noqa.rs | 118 +++++++----------- .../ruff__noqa__tests__noqa_all.snap | 6 +- ...oqa__tests__noqa_all_case_insensitive.snap | 6 +- .../ruff__noqa__tests__noqa_code.snap | 6 +- ...qa__tests__noqa_code_case_insensitive.snap | 6 +- .../ruff__noqa__tests__noqa_codes.snap | 6 +- ...a__tests__noqa_codes_case_insensitive.snap | 6 +- ...ruff__noqa__tests__noqa_leading_space.snap | 6 +- ...uff__noqa__tests__noqa_trailing_space.snap | 6 +- 10 files changed, 89 insertions(+), 152 deletions(-) diff --git a/crates/ruff/src/checkers/noqa.rs b/crates/ruff/src/checkers/noqa.rs index 83b90cd2c8..a11592adda 100644 --- a/crates/ruff/src/checkers/noqa.rs +++ b/crates/ruff/src/checkers/noqa.rs @@ -94,32 +94,17 @@ pub(crate) fn check_noqa( if analyze_directives && settings.rules.enabled(Rule::UnusedNOQA) { for line in noqa_directives.lines() { match &line.directive { - Directive::All(All { - leading_space_len, - noqa_range, - trailing_space_len, - }) => { + Directive::All(All { range }) => { if line.matches.is_empty() { - let mut diagnostic = - Diagnostic::new(UnusedNOQA { codes: None }, *noqa_range); + let mut diagnostic = Diagnostic::new(UnusedNOQA { codes: None }, *range); if settings.rules.should_fix(diagnostic.kind.rule()) { #[allow(deprecated)] - diagnostic.set_fix_from_edit(delete_noqa( - *leading_space_len, - *noqa_range, - *trailing_space_len, - locator, - )); + diagnostic.set_fix_from_edit(delete_noqa(*range, locator)); } diagnostics.push(diagnostic); } } - Directive::Codes(Codes { - leading_space_len, - noqa_range, - codes, - trailing_space_len, - }) => { + Directive::Codes(Codes { range, codes }) => { let mut disabled_codes = vec![]; let mut unknown_codes = vec![]; let mut unmatched_codes = vec![]; @@ -174,22 +159,17 @@ pub(crate) fn check_noqa( .collect(), }), }, - *noqa_range, + *range, ); if settings.rules.should_fix(diagnostic.kind.rule()) { if valid_codes.is_empty() { #[allow(deprecated)] - diagnostic.set_fix_from_edit(delete_noqa( - *leading_space_len, - *noqa_range, - *trailing_space_len, - locator, - )); + diagnostic.set_fix_from_edit(delete_noqa(*range, locator)); } else { #[allow(deprecated)] diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( format!("# noqa: {}", valid_codes.join(", ")), - *noqa_range, + *range, ))); } } @@ -205,39 +185,46 @@ pub(crate) fn check_noqa( } /// Generate a [`Edit`] to delete a `noqa` directive. -fn delete_noqa( - leading_space_len: TextSize, - noqa_range: TextRange, - trailing_space_len: TextSize, - locator: &Locator, -) -> Edit { - let line_range = locator.line_range(noqa_range.start()); +fn delete_noqa(range: TextRange, locator: &Locator) -> Edit { + let line_range = locator.line_range(range.start()); + + // Compute the leading space. + let prefix = locator.slice(TextRange::new(line_range.start(), range.start())); + let leading_space = prefix + .rfind(|c: char| !c.is_whitespace()) + .map_or(prefix.len(), |i| prefix.len() - i - 1); + let leading_space_len = TextSize::try_from(leading_space).unwrap(); + + // Compute the trailing space. + let suffix = locator.slice(TextRange::new(range.end(), line_range.end())); + let trailing_space = suffix + .find(|c: char| !c.is_whitespace()) + .map_or(suffix.len(), |i| i); + let trailing_space_len = TextSize::try_from(trailing_space).unwrap(); // Ex) `# noqa` if line_range == TextRange::new( - noqa_range.start() - leading_space_len, - noqa_range.end() + trailing_space_len, + range.start() - leading_space_len, + range.end() + trailing_space_len, ) { let full_line_end = locator.full_line_end(line_range.end()); Edit::deletion(line_range.start(), full_line_end) } // Ex) `x = 1 # noqa` - else if noqa_range.end() + trailing_space_len == line_range.end() { - Edit::deletion(noqa_range.start() - leading_space_len, line_range.end()) + else if range.end() + trailing_space_len == line_range.end() { + Edit::deletion(range.start() - leading_space_len, line_range.end()) } // Ex) `x = 1 # noqa # type: ignore` - else if locator.contents()[usize::from(noqa_range.end() + trailing_space_len)..] - .starts_with('#') - { - Edit::deletion(noqa_range.start(), noqa_range.end() + trailing_space_len) + else if locator.contents()[usize::from(range.end() + trailing_space_len)..].starts_with('#') { + Edit::deletion(range.start(), range.end() + trailing_space_len) } // Ex) `x = 1 # noqa here` else { Edit::deletion( - noqa_range.start() + "# ".text_len(), - noqa_range.end() + trailing_space_len, + range.start() + "# ".text_len(), + range.end() + trailing_space_len, ) } } diff --git a/crates/ruff/src/noqa.rs b/crates/ruff/src/noqa.rs index 83f724348b..d139e00f79 100644 --- a/crates/ruff/src/noqa.rs +++ b/crates/ruff/src/noqa.rs @@ -19,10 +19,8 @@ use crate::registry::{AsRule, Rule, RuleSet}; use crate::rule_redirects::get_redirect_target; static NOQA_LINE_REGEX: Lazy = Lazy::new(|| { - Regex::new( - r"(?P\s*)(?P(?i:# noqa)(?::\s?(?P[A-Z]+[0-9]+(?:[,\s]+[A-Z]+[0-9]+)*))?)(?P\s*)", - ) - .unwrap() + Regex::new(r"(?P(?i:# noqa)(?::\s?(?P[A-Z]+[0-9]+(?:[,\s]+[A-Z]+[0-9]+)*))?)") + .unwrap() }); /// A directive to ignore a set of rules for a given line of Python source code (e.g., @@ -39,76 +37,47 @@ impl<'a> Directive<'a> { /// Extract the noqa `Directive` from a line of Python source code. pub(crate) fn try_extract(range: TextRange, locator: &'a Locator) -> Option { let text = &locator.contents()[range]; - match NOQA_LINE_REGEX.captures(text) { - Some(caps) => match ( - caps.name("leading_spaces"), - caps.name("noqa"), - caps.name("codes"), - caps.name("trailing_spaces"), - ) { - (Some(leading_spaces), Some(noqa), Some(codes), Some(trailing_spaces)) => { - let codes = codes - .as_str() - .split(|c: char| c.is_whitespace() || c == ',') - .map(str::trim) - .filter(|code| !code.is_empty()) - .collect_vec(); - let start = range.start() + TextSize::try_from(noqa.start()).unwrap(); - if codes.is_empty() { - #[allow(deprecated)] - let line = locator.compute_line_index(start); - warn!("Expected rule codes on `noqa` directive: \"{line}\""); - } - - let leading_space_len = leading_spaces.as_str().text_len(); - let noqa_range = TextRange::at(start, noqa.as_str().text_len()); - let trailing_space_len = trailing_spaces.as_str().text_len(); - - Some(Self::Codes(Codes { - leading_space_len, - noqa_range, - trailing_space_len, - codes, - })) + let caps = NOQA_LINE_REGEX.captures(text)?; + match (caps.name("noqa"), caps.name("codes")) { + (Some(noqa), Some(codes)) => { + let codes = codes + .as_str() + .split(|c: char| c.is_whitespace() || c == ',') + .map(str::trim) + .filter(|code| !code.is_empty()) + .collect_vec(); + let start = range.start() + TextSize::try_from(noqa.start()).unwrap(); + if codes.is_empty() { + #[allow(deprecated)] + let line = locator.compute_line_index(start); + warn!("Expected rule codes on `noqa` directive: \"{line}\""); } - (Some(leading_spaces), Some(noqa), None, Some(trailing_spaces)) => { - let leading_space_len = leading_spaces.as_str().text_len(); - let noqa_range = TextRange::at( - range.start() + TextSize::try_from(noqa.start()).unwrap(), - noqa.as_str().text_len(), - ); - let trailing_space_len = trailing_spaces.as_str().text_len(); - Some(Self::All(All { - leading_space_len, - noqa_range, - trailing_space_len, - })) - } - _ => None, - }, - None => None, + + let range = TextRange::at(start, noqa.as_str().text_len()); + Some(Self::Codes(Codes { range, codes })) + } + (Some(noqa), None) => { + let range = TextRange::at( + range.start() + TextSize::try_from(noqa.start()).unwrap(), + noqa.as_str().text_len(), + ); + Some(Self::All(All { range })) + } + _ => None, } } } #[derive(Debug)] pub(crate) struct All { - /// The length of the leading whitespace before the `noqa` directive. - pub(crate) leading_space_len: TextSize, /// The range of the `noqa` directive. - pub(crate) noqa_range: TextRange, - /// The length of the trailing whitespace after the `noqa` directive. - pub(crate) trailing_space_len: TextSize, + pub(crate) range: TextRange, } #[derive(Debug)] pub(crate) struct Codes<'a> { - /// The length of the leading whitespace before the `noqa` directive. - pub(crate) leading_space_len: TextSize, /// The range of the `noqa` directive. - pub(crate) noqa_range: TextRange, - /// The length of the trailing whitespace after the `noqa` directive. - pub(crate) trailing_space_len: TextSize, + pub(crate) range: TextRange, /// The codes that are ignored by the `noqa` directive. pub(crate) codes: Vec<&'a str>, } @@ -132,8 +101,8 @@ pub(crate) fn rule_is_ignored( let offset = noqa_line_for.resolve(offset); let line_range = locator.line_range(offset); match Directive::try_extract(line_range, locator) { - Some(Directive::All(..)) => true, - Some(Directive::Codes(Codes { codes, .. })) => includes(code, &codes), + Some(Directive::All(_)) => true, + Some(Directive::Codes(Codes { codes, range: _ })) => includes(code, &codes), None => false, } } @@ -289,10 +258,10 @@ fn add_noqa_inner( directives.find_line_with_directive(noqa_line_for.resolve(parent)) { match &directive_line.directive { - Directive::All(..) => { + Directive::All(_) => { continue; } - Directive::Codes(Codes { codes, .. }) => { + Directive::Codes(Codes { codes, range: _ }) => { if includes(diagnostic.kind.rule(), codes) { continue; } @@ -306,10 +275,10 @@ fn add_noqa_inner( // Or ignored by the directive itself? if let Some(directive_line) = directives.find_line_with_directive(noqa_offset) { match &directive_line.directive { - Directive::All(..) => { + Directive::All(_) => { continue; } - Directive::Codes(Codes { codes, .. }) => { + Directive::Codes(Codes { codes, range: _ }) => { let rule = diagnostic.kind.rule(); if !includes(rule, codes) { matches_by_line @@ -355,12 +324,10 @@ fn add_noqa_inner( output.push_str(&line_ending); count += 1; } - Some(Directive::All(..)) => { + Some(Directive::All(_)) => { // Does not get inserted into the map. } - Some(Directive::Codes(Codes { - noqa_range, codes, .. - })) => { + Some(Directive::Codes(Codes { range, codes })) => { // Reconstruct the line based on the preserved rule codes. // This enables us to tally the number of edits. let output_start = output.len(); @@ -368,7 +335,7 @@ fn add_noqa_inner( // Add existing content. output.push_str( locator - .slice(TextRange::new(offset, noqa_range.start())) + .slice(TextRange::new(offset, range.start())) .trim_end(), ); @@ -433,12 +400,11 @@ impl<'a> NoqaDirectives<'a> { ) -> Self { let mut directives = Vec::new(); - for comment_range in comment_ranges { - let line_range = locator.line_range(comment_range.start()); - if let Some(directive) = Directive::try_extract(line_range, locator) { + for range in comment_ranges { + if let Some(directive) = Directive::try_extract(*range, locator) { // noqa comments are guaranteed to be single line. directives.push(NoqaDirectiveLine { - range: line_range, + range: locator.line_range(range.start()), directive, matches: Vec::new(), }); diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all.snap index 260721baf2..6d5ffa6fd8 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all.snap @@ -1,13 +1,11 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::extract(range, &locator)" +expression: "Directive::try_extract(range, &locator)" --- Some( All( All { - leading_space_len: 0, - noqa_range: 0..6, - trailing_space_len: 0, + range: 0..6, }, ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_case_insensitive.snap index 260721baf2..6d5ffa6fd8 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_case_insensitive.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_case_insensitive.snap @@ -1,13 +1,11 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::extract(range, &locator)" +expression: "Directive::try_extract(range, &locator)" --- Some( All( All { - leading_space_len: 0, - noqa_range: 0..6, - trailing_space_len: 0, + range: 0..6, }, ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code.snap index bfb5b2f11e..fd0bc5502d 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code.snap @@ -1,13 +1,11 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::extract(range, &locator)" +expression: "Directive::try_extract(range, &locator)" --- Some( Codes( Codes { - leading_space_len: 0, - noqa_range: 0..12, - trailing_space_len: 0, + range: 0..12, codes: [ "F401", ], diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_case_insensitive.snap index bfb5b2f11e..fd0bc5502d 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_case_insensitive.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_case_insensitive.snap @@ -1,13 +1,11 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::extract(range, &locator)" +expression: "Directive::try_extract(range, &locator)" --- Some( Codes( Codes { - leading_space_len: 0, - noqa_range: 0..12, - trailing_space_len: 0, + range: 0..12, codes: [ "F401", ], diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes.snap index aecdd27050..61c78604a1 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes.snap @@ -1,13 +1,11 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::extract(range, &locator)" +expression: "Directive::try_extract(range, &locator)" --- Some( Codes( Codes { - leading_space_len: 0, - noqa_range: 0..18, - trailing_space_len: 0, + range: 0..18, codes: [ "F401", "F841", diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_case_insensitive.snap index aecdd27050..61c78604a1 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_case_insensitive.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_case_insensitive.snap @@ -1,13 +1,11 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::extract(range, &locator)" +expression: "Directive::try_extract(range, &locator)" --- Some( Codes( Codes { - leading_space_len: 0, - noqa_range: 0..18, - trailing_space_len: 0, + range: 0..18, codes: [ "F401", "F841", diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_leading_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_leading_space.snap index 27602af1d0..85081b1a53 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_leading_space.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_leading_space.snap @@ -1,13 +1,11 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::extract(range, &locator)" +expression: "Directive::try_extract(range, &locator)" --- Some( Codes( Codes { - leading_space_len: 3, - noqa_range: 4..16, - trailing_space_len: 0, + range: 4..16, codes: [ "F401", ], diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_trailing_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_trailing_space.snap index bc8c548b14..fd0bc5502d 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_trailing_space.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_trailing_space.snap @@ -1,13 +1,11 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::extract(range, &locator)" +expression: "Directive::try_extract(range, &locator)" --- Some( Codes( Codes { - leading_space_len: 0, - noqa_range: 0..12, - trailing_space_len: 3, + range: 0..12, codes: [ "F401", ], From 23363cafd18bd0a214eaf73a6845933968dfe676 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Jul 2023 19:13:41 -0400 Subject: [PATCH 341/447] Move `Directive` fields behind accessor methods (#5546) --- crates/ruff/src/checkers/noqa.rs | 29 ++++++++++++++++------------- crates/ruff/src/noqa.rs | 27 +++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/crates/ruff/src/checkers/noqa.rs b/crates/ruff/src/checkers/noqa.rs index a11592adda..2733ca0605 100644 --- a/crates/ruff/src/checkers/noqa.rs +++ b/crates/ruff/src/checkers/noqa.rs @@ -2,12 +2,13 @@ use itertools::Itertools; use ruff_text_size::{TextLen, TextRange, TextSize}; +use rustpython_parser::ast::Ranged; use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_python_ast::source_code::Locator; use crate::noqa; -use crate::noqa::{All, Codes, Directive, FileExemption, NoqaDirectives, NoqaMapping}; +use crate::noqa::{Directive, FileExemption, NoqaDirectives, NoqaMapping}; use crate::registry::{AsRule, Rule}; use crate::rule_redirects::get_redirect_target; use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA}; @@ -63,15 +64,15 @@ pub(crate) fn check_noqa( if let Some(directive_line) = noqa_directives.find_line_with_directive_mut(noqa_offset) { let suppressed = match &directive_line.directive { - Directive::All(..) => { + Directive::All(_) => { directive_line .matches .push(diagnostic.kind.rule().noqa_code()); ignored_diagnostics.push(index); true } - Directive::Codes(Codes { codes, .. }) => { - if noqa::includes(diagnostic.kind.rule(), codes) { + Directive::Codes(directive) => { + if noqa::includes(diagnostic.kind.rule(), directive.codes()) { directive_line .matches .push(diagnostic.kind.rule().noqa_code()); @@ -94,30 +95,31 @@ pub(crate) fn check_noqa( if analyze_directives && settings.rules.enabled(Rule::UnusedNOQA) { for line in noqa_directives.lines() { match &line.directive { - Directive::All(All { range }) => { + Directive::All(directive) => { if line.matches.is_empty() { - let mut diagnostic = Diagnostic::new(UnusedNOQA { codes: None }, *range); + let mut diagnostic = + Diagnostic::new(UnusedNOQA { codes: None }, directive.range()); if settings.rules.should_fix(diagnostic.kind.rule()) { #[allow(deprecated)] - diagnostic.set_fix_from_edit(delete_noqa(*range, locator)); + diagnostic.set_fix_from_edit(delete_noqa(directive.range(), locator)); } diagnostics.push(diagnostic); } } - Directive::Codes(Codes { range, codes }) => { + Directive::Codes(directive) => { let mut disabled_codes = vec![]; let mut unknown_codes = vec![]; let mut unmatched_codes = vec![]; let mut valid_codes = vec![]; let mut self_ignore = false; - for code in codes { + for code in directive.codes() { let code = get_redirect_target(code).unwrap_or(code); if Rule::UnusedNOQA.noqa_code() == code { self_ignore = true; break; } - if line.matches.iter().any(|m| *m == code) + if line.matches.iter().any(|match_| *match_ == code) || settings.external.contains(code) { valid_codes.push(code); @@ -159,17 +161,18 @@ pub(crate) fn check_noqa( .collect(), }), }, - *range, + directive.range(), ); if settings.rules.should_fix(diagnostic.kind.rule()) { if valid_codes.is_empty() { #[allow(deprecated)] - diagnostic.set_fix_from_edit(delete_noqa(*range, locator)); + diagnostic + .set_fix_from_edit(delete_noqa(directive.range(), locator)); } else { #[allow(deprecated)] diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( format!("# noqa: {}", valid_codes.join(", ")), - *range, + directive.range(), ))); } } diff --git a/crates/ruff/src/noqa.rs b/crates/ruff/src/noqa.rs index d139e00f79..e861bfd469 100644 --- a/crates/ruff/src/noqa.rs +++ b/crates/ruff/src/noqa.rs @@ -9,6 +9,7 @@ use log::warn; use once_cell::sync::Lazy; use regex::Regex; use ruff_text_size::{TextLen, TextRange, TextSize}; +use rustpython_parser::ast::Ranged; use ruff_diagnostics::Diagnostic; use ruff_python_ast::source_code::Locator; @@ -70,16 +71,34 @@ impl<'a> Directive<'a> { #[derive(Debug)] pub(crate) struct All { + range: TextRange, +} + +impl Ranged for All { /// The range of the `noqa` directive. - pub(crate) range: TextRange, + fn range(&self) -> TextRange { + self.range + } } #[derive(Debug)] pub(crate) struct Codes<'a> { - /// The range of the `noqa` directive. - pub(crate) range: TextRange, + range: TextRange, + codes: Vec<&'a str>, +} + +impl Codes<'_> { /// The codes that are ignored by the `noqa` directive. - pub(crate) codes: Vec<&'a str>, + pub(crate) fn codes(&self) -> &[&str] { + &self.codes + } +} + +impl Ranged for Codes<'_> { + /// The range of the `noqa` directive. + fn range(&self) -> TextRange { + self.range + } } /// Returns `true` if the string list of `codes` includes `code` (or an alias From 5dff3195d4ecfec54bf4bf5eeee91e0d32086d88 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Jul 2023 19:21:42 -0400 Subject: [PATCH 342/447] Refactor tokens-based rules to take an `&mut Vec` (#5525) --- crates/ruff/src/autofix/edits.rs | 14 +-- crates/ruff/src/checkers/tokens.rs | 86 +++++++------------ .../eradicate/rules/commented_out_code.rs | 7 +- .../flake8_commas/rules/trailing_commas.rs | 7 +- .../src/rules/flake8_fixme/rules/todos.rs | 29 ++++--- .../rules/implicit.rs | 11 +-- .../flake8_pyi/rules/type_comment_in_stub.rs | 10 +-- .../rules/flake8_quotes/rules/from_tokens.rs | 9 +- .../src/rules/flake8_todos/rules/todos.rs | 13 ++- .../pycodestyle/rules/compound_statements.rs | 7 +- .../rules/invalid_escape_sequence.rs | 21 +++-- crates/ruff/src/rules/pyflakes/fixes.rs | 10 +-- .../pylint/rules/bad_string_format_type.rs | 6 +- .../pylint/rules/invalid_string_characters.rs | 9 +- .../pyupgrade/rules/extraneous_parentheses.rs | 9 +- .../rules/printf_string_formatting.rs | 2 +- .../pyupgrade/rules/redundant_open_modes.rs | 8 +- .../ruff/rules/ambiguous_unicode_character.rs | 9 +- 18 files changed, 113 insertions(+), 154 deletions(-) diff --git a/crates/ruff/src/autofix/edits.rs b/crates/ruff/src/autofix/edits.rs index aeb9c8114a..25272394c4 100644 --- a/crates/ruff/src/autofix/edits.rs +++ b/crates/ruff/src/autofix/edits.rs @@ -2,7 +2,7 @@ use anyhow::{bail, Result}; use ruff_text_size::{TextLen, TextRange, TextSize}; use rustpython_parser::ast::{self, ExceptHandler, Expr, Keyword, Ranged, Stmt}; -use rustpython_parser::{lexer, Mode, Tok}; +use rustpython_parser::{lexer, Mode}; use ruff_diagnostics::Edit; use ruff_python_ast::helpers; @@ -98,7 +98,7 @@ pub(crate) fn remove_argument( // Case 1: there is only one argument. let mut count = 0u32; for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, call_at).flatten() { - if matches!(tok, Tok::Lpar) { + if tok.is_lpar() { if count == 0 { fix_start = Some(if remove_parentheses { range.start() @@ -109,7 +109,7 @@ pub(crate) fn remove_argument( count = count.saturating_add(1); } - if matches!(tok, Tok::Rpar) { + if tok.is_rpar() { count = count.saturating_sub(1); if count == 0 { fix_end = Some(if remove_parentheses { @@ -131,11 +131,11 @@ pub(crate) fn remove_argument( let mut seen_comma = false; for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, call_at).flatten() { if seen_comma { - if matches!(tok, Tok::NonLogicalNewline) { + if tok.is_non_logical_newline() { // Also delete any non-logical newlines after the comma. continue; } - fix_end = Some(if matches!(tok, Tok::Newline) { + fix_end = Some(if tok.is_newline() { range.end() } else { range.start() @@ -145,7 +145,7 @@ pub(crate) fn remove_argument( if range.start() == expr_range.start() { fix_start = Some(range.start()); } - if fix_start.is_some() && matches!(tok, Tok::Comma) { + if fix_start.is_some() && tok.is_comma() { seen_comma = true; } } @@ -157,7 +157,7 @@ pub(crate) fn remove_argument( fix_end = Some(expr_range.end()); break; } - if matches!(tok, Tok::Comma) { + if tok.is_comma() { fix_start = Some(range.start()); } } diff --git a/crates/ruff/src/checkers/tokens.rs b/crates/ruff/src/checkers/tokens.rs index 812bf455a2..b7883b4045 100644 --- a/crates/ruff/src/checkers/tokens.rs +++ b/crates/ruff/src/checkers/tokens.rs @@ -3,6 +3,9 @@ use rustpython_parser::lexer::LexResult; use rustpython_parser::Tok; +use ruff_diagnostics::Diagnostic; +use ruff_python_ast::source_code::{Indexer, Locator}; + use crate::directives::TodoComment; use crate::lex::docstring_detection::StateMachine; use crate::registry::{AsRule, Rule}; @@ -12,8 +15,6 @@ use crate::rules::{ flake8_todos, pycodestyle, pylint, pyupgrade, ruff, }; use crate::settings::Settings; -use ruff_diagnostics::Diagnostic; -use ruff_python_ast::source_code::{Indexer, Locator}; pub(crate) fn check_tokens( locator: &Locator, @@ -88,10 +89,11 @@ pub(crate) fn check_tokens( }; if matches!(tok, Tok::String { .. } | Tok::Comment(_)) { - diagnostics.extend(ruff::rules::ambiguous_unicode_character( + ruff::rules::ambiguous_unicode_character( + &mut diagnostics, locator, range, - if matches!(tok, Tok::String { .. }) { + if tok.is_string() { if is_docstring { Context::Docstring } else { @@ -101,93 +103,77 @@ pub(crate) fn check_tokens( Context::Comment }, settings, - )); + ); } } } // ERA001 if enforce_commented_out_code { - diagnostics.extend(eradicate::rules::commented_out_code( - locator, indexer, settings, - )); + eradicate::rules::commented_out_code(&mut diagnostics, locator, indexer, settings); } // W605 if enforce_invalid_escape_sequence { for (tok, range) in tokens.iter().flatten() { - if matches!(tok, Tok::String { .. }) { - diagnostics.extend(pycodestyle::rules::invalid_escape_sequence( + if tok.is_string() { + pycodestyle::rules::invalid_escape_sequence( + &mut diagnostics, locator, *range, settings.rules.should_fix(Rule::InvalidEscapeSequence), - )); + ); } } } // PLE2510, PLE2512, PLE2513 if enforce_invalid_string_character { for (tok, range) in tokens.iter().flatten() { - if matches!(tok, Tok::String { .. }) { - diagnostics.extend( - pylint::rules::invalid_string_characters(locator, *range) - .into_iter() - .filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())), - ); + if tok.is_string() { + pylint::rules::invalid_string_characters(&mut diagnostics, *range, locator); } } } // E701, E702, E703 if enforce_compound_statements { - diagnostics.extend( - pycodestyle::rules::compound_statements(tokens, locator, indexer, settings) - .into_iter() - .filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())), + pycodestyle::rules::compound_statements( + &mut diagnostics, + tokens, + locator, + indexer, + settings, ); } // Q001, Q002, Q003 if enforce_quotes { - diagnostics.extend( - flake8_quotes::rules::from_tokens(tokens, locator, settings) - .into_iter() - .filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())), - ); + flake8_quotes::rules::from_tokens(&mut diagnostics, tokens, locator, settings); } // ISC001, ISC002 if enforce_implicit_string_concatenation { - diagnostics.extend( - flake8_implicit_str_concat::rules::implicit( - tokens, - &settings.flake8_implicit_str_concat, - locator, - ) - .into_iter() - .filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())), + flake8_implicit_str_concat::rules::implicit( + &mut diagnostics, + tokens, + &settings.flake8_implicit_str_concat, + locator, ); } // COM812, COM818, COM819 if enforce_trailing_comma { - diagnostics.extend( - flake8_commas::rules::trailing_commas(tokens, locator, settings) - .into_iter() - .filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())), - ); + flake8_commas::rules::trailing_commas(&mut diagnostics, tokens, locator, settings); } // UP034 if enforce_extraneous_parenthesis { - diagnostics.extend( - pyupgrade::rules::extraneous_parentheses(tokens, locator, settings).into_iter(), - ); + pyupgrade::rules::extraneous_parentheses(&mut diagnostics, tokens, locator, settings); } // PYI033 if enforce_type_comment_in_stub && is_stub { - diagnostics.extend(flake8_pyi::rules::type_comment_in_stub(locator, indexer)); + flake8_pyi::rules::type_comment_in_stub(&mut diagnostics, locator, indexer); } // TD001, TD002, TD003, TD004, TD005, TD006, TD007 @@ -203,18 +189,12 @@ pub(crate) fn check_tokens( }) .collect(); - diagnostics.extend( - flake8_todos::rules::todos(&todo_comments, locator, indexer, settings) - .into_iter() - .filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())), - ); + flake8_todos::rules::todos(&mut diagnostics, &todo_comments, locator, indexer, settings); - diagnostics.extend( - flake8_fixme::rules::todos(&todo_comments) - .into_iter() - .filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())), - ); + flake8_fixme::rules::todos(&mut diagnostics, &todo_comments); } + diagnostics.retain(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())); + diagnostics } diff --git a/crates/ruff/src/rules/eradicate/rules/commented_out_code.rs b/crates/ruff/src/rules/eradicate/rules/commented_out_code.rs index 7864eb99bf..15e23ab6f1 100644 --- a/crates/ruff/src/rules/eradicate/rules/commented_out_code.rs +++ b/crates/ruff/src/rules/eradicate/rules/commented_out_code.rs @@ -48,12 +48,11 @@ fn is_standalone_comment(line: &str) -> bool { /// ERA001 pub(crate) fn commented_out_code( + diagnostics: &mut Vec, locator: &Locator, indexer: &Indexer, settings: &Settings, -) -> Vec { - let mut diagnostics = vec![]; - +) { for range in indexer.comment_ranges() { let line = locator.full_lines(*range); @@ -69,6 +68,4 @@ pub(crate) fn commented_out_code( diagnostics.push(diagnostic); } } - - diagnostics } diff --git a/crates/ruff/src/rules/flake8_commas/rules/trailing_commas.rs b/crates/ruff/src/rules/flake8_commas/rules/trailing_commas.rs index 979bfd80c2..1335ff3bb4 100644 --- a/crates/ruff/src/rules/flake8_commas/rules/trailing_commas.rs +++ b/crates/ruff/src/rules/flake8_commas/rules/trailing_commas.rs @@ -222,12 +222,11 @@ impl AlwaysAutofixableViolation for ProhibitedTrailingComma { /// COM812, COM818, COM819 pub(crate) fn trailing_commas( + diagnostics: &mut Vec, tokens: &[LexResult], locator: &Locator, settings: &Settings, -) -> Vec { - let mut diagnostics = vec![]; - +) { let tokens = tokens .iter() .flatten() @@ -387,6 +386,4 @@ pub(crate) fn trailing_commas( stack.pop(); } } - - diagnostics } diff --git a/crates/ruff/src/rules/flake8_fixme/rules/todos.rs b/crates/ruff/src/rules/flake8_fixme/rules/todos.rs index 495fccc030..d43d20b623 100644 --- a/crates/ruff/src/rules/flake8_fixme/rules/todos.rs +++ b/crates/ruff/src/rules/flake8_fixme/rules/todos.rs @@ -39,18 +39,19 @@ impl Violation for LineContainsHack { } } -pub(crate) fn todos(directive_ranges: &[TodoComment]) -> Vec { - directive_ranges - .iter() - .map(|TodoComment { directive, .. }| match directive.kind { - // FIX001 - TodoDirectiveKind::Fixme => Diagnostic::new(LineContainsFixme, directive.range), - // FIX002 - TodoDirectiveKind::Hack => Diagnostic::new(LineContainsHack, directive.range), - // FIX003 - TodoDirectiveKind::Todo => Diagnostic::new(LineContainsTodo, directive.range), - // FIX004 - TodoDirectiveKind::Xxx => Diagnostic::new(LineContainsXxx, directive.range), - }) - .collect::>() +pub(crate) fn todos(diagnostics: &mut Vec, directive_ranges: &[TodoComment]) { + diagnostics.extend( + directive_ranges + .iter() + .map(|TodoComment { directive, .. }| match directive.kind { + // FIX001 + TodoDirectiveKind::Fixme => Diagnostic::new(LineContainsFixme, directive.range), + // FIX002 + TodoDirectiveKind::Hack => Diagnostic::new(LineContainsHack, directive.range), + // FIX003 + TodoDirectiveKind::Todo => Diagnostic::new(LineContainsTodo, directive.range), + // FIX004 + TodoDirectiveKind::Xxx => Diagnostic::new(LineContainsXxx, directive.range), + }), + ); } diff --git a/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs b/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs index 69def84c39..227d889d02 100644 --- a/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs +++ b/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs @@ -1,7 +1,6 @@ use itertools::Itertools; use ruff_text_size::TextRange; use rustpython_parser::lexer::LexResult; -use rustpython_parser::Tok; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -91,21 +90,20 @@ impl Violation for MultiLineImplicitStringConcatenation { /// ISC001, ISC002 pub(crate) fn implicit( + diagnostics: &mut Vec, tokens: &[LexResult], settings: &Settings, locator: &Locator, -) -> Vec { - let mut diagnostics = vec![]; +) { for ((a_tok, a_range), (b_tok, b_range)) in tokens .iter() .flatten() .filter(|(tok, _)| { - !matches!(tok, Tok::Comment(..)) - && (settings.allow_multiline || !matches!(tok, Tok::NonLogicalNewline)) + !tok.is_comment() && (settings.allow_multiline || !tok.is_non_logical_newline()) }) .tuple_windows() { - if matches!(a_tok, Tok::String { .. }) && matches!(b_tok, Tok::String { .. }) { + if a_tok.is_string() && b_tok.is_string() { if locator.contains_line_break(TextRange::new(a_range.end(), b_range.start())) { diagnostics.push(Diagnostic::new( MultiLineImplicitStringConcatenation, @@ -125,7 +123,6 @@ pub(crate) fn implicit( }; }; } - diagnostics } fn concatenate_strings(a_range: TextRange, b_range: TextRange, locator: &Locator) -> Option { diff --git a/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs index 9d5dc3d1df..d7f8fda8ce 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs @@ -34,9 +34,11 @@ impl Violation for TypeCommentInStub { } /// PYI033 -pub(crate) fn type_comment_in_stub(locator: &Locator, indexer: &Indexer) -> Vec { - let mut diagnostics = vec![]; - +pub(crate) fn type_comment_in_stub( + diagnostics: &mut Vec, + locator: &Locator, + indexer: &Indexer, +) { for range in indexer.comment_ranges() { let comment = locator.slice(*range); @@ -44,8 +46,6 @@ pub(crate) fn type_comment_in_stub(locator: &Locator, indexer: &Indexer) -> Vec< diagnostics.push(Diagnostic::new(TypeCommentInStub, *range)); } } - - diagnostics } static TYPE_COMMENT_REGEX: Lazy = 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 710dec32cc..db40775805 100644 --- a/crates/ruff/src/rules/flake8_quotes/rules/from_tokens.rs +++ b/crates/ruff/src/rules/flake8_quotes/rules/from_tokens.rs @@ -464,12 +464,11 @@ fn strings(locator: &Locator, sequence: &[TextRange], settings: &Settings) -> Ve /// Generate `flake8-quote` diagnostics from a token stream. pub(crate) fn from_tokens( + diagnostics: &mut Vec, lxr: &[LexResult], locator: &Locator, settings: &Settings, -) -> Vec { - let mut diagnostics = vec![]; - +) { // Keep track of sequences of strings, which represent implicit string // concatenation, and should thus be handled as a single unit. let mut sequence = vec![]; @@ -488,7 +487,7 @@ pub(crate) fn from_tokens( diagnostics.push(diagnostic); } } else { - if matches!(tok, Tok::String { .. }) { + if tok.is_string() { // If this is a string, add it to the sequence. sequence.push(range); } else if !matches!(tok, Tok::Comment(..) | Tok::NonLogicalNewline) { @@ -506,6 +505,4 @@ pub(crate) fn from_tokens( diagnostics.extend(strings(locator, &sequence, settings)); sequence.clear(); } - - diagnostics } diff --git a/crates/ruff/src/rules/flake8_todos/rules/todos.rs b/crates/ruff/src/rules/flake8_todos/rules/todos.rs index 20072b0ded..fc94be27eb 100644 --- a/crates/ruff/src/rules/flake8_todos/rules/todos.rs +++ b/crates/ruff/src/rules/flake8_todos/rules/todos.rs @@ -235,13 +235,12 @@ static ISSUE_LINK_REGEX_SET: Lazy = Lazy::new(|| { }); pub(crate) fn todos( + diagnostics: &mut Vec, todo_comments: &[TodoComment], locator: &Locator, indexer: &Indexer, settings: &Settings, -) -> Vec { - let mut diagnostics: Vec = vec![]; - +) { for todo_comment in todo_comments { let TodoComment { directive, @@ -256,8 +255,8 @@ pub(crate) fn todos( continue; } - directive_errors(directive, &mut diagnostics, settings); - static_errors(&mut diagnostics, content, range, directive); + directive_errors(diagnostics, directive, settings); + static_errors(diagnostics, content, range, directive); let mut has_issue_link = false; let mut curr_range = range; @@ -297,14 +296,12 @@ pub(crate) fn todos( diagnostics.push(Diagnostic::new(MissingTodoLink, directive.range)); } } - - diagnostics } /// Check that the directive itself is valid. This function modifies `diagnostics` in-place. fn directive_errors( - directive: &TodoDirective, diagnostics: &mut Vec, + directive: &TodoDirective, settings: &Settings, ) { if directive.content == "TODO" { diff --git a/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs b/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs index 4192e84db3..b3fd153481 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs @@ -100,13 +100,12 @@ impl AlwaysAutofixableViolation for UselessSemicolon { /// E701, E702, E703 pub(crate) fn compound_statements( + diagnostics: &mut Vec, lxr: &[LexResult], locator: &Locator, indexer: &Indexer, settings: &Settings, -) -> Vec { - let mut diagnostics = vec![]; - +) { // Track the last seen instance of a variety of tokens. let mut colon = None; let mut semi = None; @@ -311,6 +310,4 @@ pub(crate) fn compound_statements( _ => {} }; } - - diagnostics } diff --git a/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs b/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs index f58bd5855d..fab07187d7 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs @@ -40,25 +40,24 @@ impl AlwaysAutofixableViolation for InvalidEscapeSequence { /// W605 pub(crate) fn invalid_escape_sequence( + diagnostics: &mut Vec, locator: &Locator, range: TextRange, autofix: bool, -) -> Vec { - let mut diagnostics = vec![]; - +) { let text = locator.slice(range); // Determine whether the string is single- or triple-quoted. let Some(leading_quote) = leading_quote(text) else { - return diagnostics; + return; }; let Some(trailing_quote) = trailing_quote(text) else { - return diagnostics; + return; }; let body = &text[leading_quote.len()..text.len() - trailing_quote.len()]; if leading_quote.contains(['r', 'R']) { - return diagnostics; + return; } let start_offset = range.start() + TextSize::try_from(leading_quote.len()).unwrap(); @@ -67,6 +66,7 @@ pub(crate) fn invalid_escape_sequence( let mut contains_valid_escape_sequence = false; + let mut invalid_escape_sequence = Vec::new(); while let Some((i, c)) = chars_iter.next() { if c != '\\' { continue; @@ -122,14 +122,13 @@ pub(crate) fn invalid_escape_sequence( let location = start_offset + TextSize::try_from(i).unwrap(); let range = TextRange::at(location, next_char.text_len() + TextSize::from(1)); - let diagnostic = Diagnostic::new(InvalidEscapeSequence(*next_char), range); - diagnostics.push(diagnostic); + invalid_escape_sequence.push(Diagnostic::new(InvalidEscapeSequence(*next_char), range)); } if autofix { if contains_valid_escape_sequence { // Escape with backslash. - for diagnostic in &mut diagnostics { + for diagnostic in &mut invalid_escape_sequence { diagnostic.set_fix(Fix::automatic(Edit::insertion( r"\".to_string(), diagnostic.range().start() + TextSize::from(1), @@ -137,7 +136,7 @@ pub(crate) fn invalid_escape_sequence( } } else { // Turn into raw string. - for diagnostic in &mut diagnostics { + for diagnostic in &mut invalid_escape_sequence { // If necessary, add a space between any leading keyword (`return`, `yield`, // `assert`, etc.) and the string. For example, `return"foo"` is valid, but // `returnr"foo"` is not. @@ -159,5 +158,5 @@ pub(crate) fn invalid_escape_sequence( } } - diagnostics + diagnostics.extend(invalid_escape_sequence); } diff --git a/crates/ruff/src/rules/pyflakes/fixes.rs b/crates/ruff/src/rules/pyflakes/fixes.rs index 49796e9bd4..694e03bf87 100644 --- a/crates/ruff/src/rules/pyflakes/fixes.rs +++ b/crates/ruff/src/rules/pyflakes/fixes.rs @@ -1,7 +1,7 @@ use anyhow::{bail, Ok, Result}; use ruff_text_size::TextRange; use rustpython_parser::ast::{ExceptHandler, Expr, Ranged}; -use rustpython_parser::{lexer, Mode, Tok}; +use rustpython_parser::{lexer, Mode}; use ruff_diagnostics::Edit; use ruff_python_ast::source_code::{Locator, Stylist}; @@ -10,7 +10,7 @@ use crate::autofix::codemods::CodegenStylist; use crate::cst::matchers::{match_call_mut, match_dict, match_expression}; /// Generate a [`Edit`] to remove unused keys from format dict. -pub(crate) fn remove_unused_format_arguments_from_dict( +pub(super) fn remove_unused_format_arguments_from_dict( unused_arguments: &[usize], stmt: &Expr, locator: &Locator, @@ -35,7 +35,7 @@ pub(crate) fn remove_unused_format_arguments_from_dict( } /// Generate a [`Edit`] to remove unused keyword arguments from a `format` call. -pub(crate) fn remove_unused_keyword_arguments_from_format_call( +pub(super) fn remove_unused_keyword_arguments_from_format_call( unused_arguments: &[usize], location: TextRange, locator: &Locator, @@ -102,10 +102,10 @@ pub(crate) fn remove_exception_handler_assignment( for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, except_handler.start()).flatten() { - if matches!(tok, Tok::As) { + if tok.is_as() { fix_start = prev; } - if matches!(tok, Tok::Colon) { + if tok.is_colon() { fix_end = Some(range.start()); break; } diff --git a/crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs b/crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs index 137b346cda..9eb1ec8865 100644 --- a/crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs +++ b/crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs @@ -4,7 +4,7 @@ use ruff_text_size::TextRange; use rustc_hash::FxHashMap; use rustpython_format::cformat::{CFormatPart, CFormatSpec, CFormatStrOrBytes, CFormatString}; use rustpython_parser::ast::{self, Constant, Expr, Ranged}; -use rustpython_parser::{lexer, Mode, Tok}; +use rustpython_parser::{lexer, Mode}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -205,9 +205,9 @@ pub(crate) fn bad_string_format_type(checker: &mut Checker, expr: &Expr, right: let content = checker.locator.slice(expr.range()); let mut strings: Vec = vec![]; for (tok, range) in lexer::lex_starts_at(content, Mode::Module, expr.start()).flatten() { - if matches!(tok, Tok::String { .. }) { + if tok.is_string() { strings.push(range); - } else if matches!(tok, Tok::Percent) { + } else if tok.is_percent() { // Break as soon as we find the modulo symbol. break; } diff --git a/crates/ruff/src/rules/pylint/rules/invalid_string_characters.rs b/crates/ruff/src/rules/pylint/rules/invalid_string_characters.rs index 3b5b28fcf3..5bccae0694 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_string_characters.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_string_characters.rs @@ -171,8 +171,11 @@ impl AlwaysAutofixableViolation for InvalidCharacterZeroWidthSpace { } /// PLE2510, PLE2512, PLE2513, PLE2514, PLE2515 -pub(crate) fn invalid_string_characters(locator: &Locator, range: TextRange) -> Vec { - let mut diagnostics = Vec::new(); +pub(crate) fn invalid_string_characters( + diagnostics: &mut Vec, + range: TextRange, + locator: &Locator, +) { let text = locator.slice(range); for (column, match_) in text.match_indices(&['\x08', '\x1A', '\x1B', '\0', '\u{200b}']) { @@ -195,6 +198,4 @@ pub(crate) fn invalid_string_characters(locator: &Locator, range: TextRange) -> Edit::range_replacement(replacement.to_string(), range), ))); } - - diagnostics } diff --git a/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs b/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs index f5d67ae166..185932b8eb 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs @@ -134,21 +134,21 @@ fn match_extraneous_parentheses(tokens: &[LexResult], mut i: usize) -> Option<(u /// UP034 pub(crate) fn extraneous_parentheses( + diagnostics: &mut Vec, tokens: &[LexResult], locator: &Locator, settings: &Settings, -) -> Vec { - let mut diagnostics = vec![]; +) { let mut i = 0; while i < tokens.len() { if matches!(tokens[i], Ok((Tok::Lpar, _))) { if let Some((start, end)) = match_extraneous_parentheses(tokens, i) { i = end + 1; let Ok((_, start_range)) = &tokens[start] else { - return diagnostics; + return; }; let Ok((.., end_range)) = &tokens[end] else { - return diagnostics; + return; }; let mut diagnostic = Diagnostic::new( ExtraneousParentheses, @@ -171,5 +171,4 @@ pub(crate) fn extraneous_parentheses( i += 1; } } - diagnostics } diff --git a/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs b/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs index 48ede85924..4477aa3cb6 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs @@ -344,7 +344,7 @@ pub(crate) fn printf_string_formatting( ) .flatten() { - if matches!(tok, Tok::String { .. }) { + if tok.is_string() { strings.push(range); } else if matches!(tok, Tok::Rpar) { // If we hit a right paren, we have to preserve it. diff --git a/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs b/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs index 60e6f431b4..47fa960d18 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use anyhow::{anyhow, Result}; use ruff_text_size::TextSize; use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged}; -use rustpython_parser::{lexer, Mode, Tok}; +use rustpython_parser::{lexer, Mode}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -168,15 +168,15 @@ fn create_remove_param_fix(locator: &Locator, expr: &Expr, mode_param: &Expr) -> fix_end = Some(range.end()); break; } - if delete_first_arg && matches!(tok, Tok::Name { .. }) { + if delete_first_arg && tok.is_name() { fix_end = Some(range.start()); break; } - if matches!(tok, Tok::Lpar) { + if tok.is_lpar() { is_first_arg = true; fix_start = Some(range.end()); } - if matches!(tok, Tok::Comma) { + if tok.is_comma() { is_first_arg = false; if !delete_first_arg { fix_start = Some(range.start()); diff --git a/crates/ruff/src/rules/ruff/rules/ambiguous_unicode_character.rs b/crates/ruff/src/rules/ruff/rules/ambiguous_unicode_character.rs index c89f132dd4..132814af1b 100644 --- a/crates/ruff/src/rules/ruff/rules/ambiguous_unicode_character.rs +++ b/crates/ruff/src/rules/ruff/rules/ambiguous_unicode_character.rs @@ -159,18 +159,17 @@ impl AlwaysAutofixableViolation for AmbiguousUnicodeCharacterComment { } pub(crate) fn ambiguous_unicode_character( + diagnostics: &mut Vec, locator: &Locator, range: TextRange, context: Context, settings: &Settings, -) -> Vec { - let mut diagnostics = vec![]; - +) { let text = locator.slice(range); // Most of the time, we don't need to check for ambiguous unicode characters at all. if text.is_ascii() { - return diagnostics; + return; } // Iterate over the "words" in the text. @@ -232,8 +231,6 @@ pub(crate) fn ambiguous_unicode_character( } word_candidates.clear(); } - - diagnostics } bitflags! { From ba7041b6bfcede014a25cec61ad1ea9bbce2808f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Jul 2023 19:33:57 -0400 Subject: [PATCH 343/447] Remove Directive's dependency on Locator (#5547) ## Summary It's a bit simpler to let the API just take the text itself, plus an offset (to make the returned `TextRange` absolute, rather than relative). --- crates/ruff/src/noqa.rs | 81 ++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 54 deletions(-) diff --git a/crates/ruff/src/noqa.rs b/crates/ruff/src/noqa.rs index e861bfd469..c937bbc898 100644 --- a/crates/ruff/src/noqa.rs +++ b/crates/ruff/src/noqa.rs @@ -1,6 +1,7 @@ use std::collections::BTreeMap; use std::fmt::{Display, Write}; use std::fs; +use std::ops::Add; use std::path::Path; use anyhow::Result; @@ -36,8 +37,7 @@ pub(crate) enum Directive<'a> { impl<'a> Directive<'a> { /// Extract the noqa `Directive` from a line of Python source code. - pub(crate) fn try_extract(range: TextRange, locator: &'a Locator) -> Option { - let text = &locator.contents()[range]; + pub(crate) fn try_extract(text: &'a str, offset: TextSize) -> Option { let caps = NOQA_LINE_REGEX.captures(text)?; match (caps.name("noqa"), caps.name("codes")) { (Some(noqa), Some(codes)) => { @@ -47,19 +47,18 @@ impl<'a> Directive<'a> { .map(str::trim) .filter(|code| !code.is_empty()) .collect_vec(); - let start = range.start() + TextSize::try_from(noqa.start()).unwrap(); if codes.is_empty() { - #[allow(deprecated)] - let line = locator.compute_line_index(start); - warn!("Expected rule codes on `noqa` directive: \"{line}\""); + warn!("Expected rule codes on `noqa` directive: \"{text}\""); } - - let range = TextRange::at(start, noqa.as_str().text_len()); + let range = TextRange::at( + TextSize::try_from(noqa.start()).unwrap().add(offset), + noqa.as_str().text_len(), + ); Some(Self::Codes(Codes { range, codes })) } (Some(noqa), None) => { let range = TextRange::at( - range.start() + TextSize::try_from(noqa.start()).unwrap(), + TextSize::try_from(noqa.start()).unwrap().add(offset), noqa.as_str().text_len(), ); Some(Self::All(All { range })) @@ -119,7 +118,7 @@ pub(crate) fn rule_is_ignored( ) -> bool { let offset = noqa_line_for.resolve(offset); let line_range = locator.line_range(offset); - match Directive::try_extract(line_range, locator) { + match Directive::try_extract(locator.slice(line_range), line_range.start()) { Some(Directive::All(_)) => true, Some(Directive::Codes(Codes { codes, range: _ })) => includes(code, &codes), None => false, @@ -401,9 +400,11 @@ fn push_codes(str: &mut String, codes: impl Iterator) { #[derive(Debug)] pub(crate) struct NoqaDirectiveLine<'a> { - // The range of the text line for which the noqa directive applies. + /// The range of the text line for which the noqa directive applies. pub(crate) range: TextRange, + /// The noqa directive. pub(crate) directive: Directive<'a>, + /// The codes that are ignored by the directive. pub(crate) matches: Vec, } @@ -420,7 +421,7 @@ impl<'a> NoqaDirectives<'a> { let mut directives = Vec::new(); for range in comment_ranges { - if let Some(directive) = Directive::try_extract(*range, locator) { + if let Some(directive) = Directive::try_extract(locator.slice(*range), range.start()) { // noqa comments are guaranteed to be single line. directives.push(NoqaDirectiveLine { range: locator.line_range(range.start()), @@ -554,113 +555,85 @@ mod tests { #[test] fn noqa_all() { let source = "# noqa"; - let range = TextRange::new(TextSize::from(0), TextSize::from(6)); - let locator = Locator::new(source); - assert_debug_snapshot!(Directive::try_extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_code() { let source = "# noqa: F401"; - let range = TextRange::new(TextSize::from(0), TextSize::from(12)); - let locator = Locator::new(source); - assert_debug_snapshot!(Directive::try_extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_codes() { let source = "# noqa: F401, F841"; - let range = TextRange::new(TextSize::from(0), TextSize::from(18)); - let locator = Locator::new(source); - assert_debug_snapshot!(Directive::try_extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_all_case_insensitive() { let source = "# NOQA"; - let range = TextRange::new(TextSize::from(0), TextSize::from(6)); - let locator = Locator::new(source); - assert_debug_snapshot!(Directive::try_extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_code_case_insensitive() { let source = "# NOQA: F401"; - let range = TextRange::new(TextSize::from(0), TextSize::from(12)); - let locator = Locator::new(source); - assert_debug_snapshot!(Directive::try_extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_codes_case_insensitive() { let source = "# NOQA: F401, F841"; - let range = TextRange::new(TextSize::from(0), TextSize::from(18)); - let locator = Locator::new(source); - assert_debug_snapshot!(Directive::try_extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_leading_space() { let source = "# # noqa: F401"; - let range = TextRange::new(TextSize::from(0), TextSize::from(16)); - let locator = Locator::new(source); - assert_debug_snapshot!(Directive::try_extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_trailing_space() { let source = "# noqa: F401 #"; - let range = TextRange::new(TextSize::from(0), TextSize::from(16)); - let locator = Locator::new(source); - assert_debug_snapshot!(Directive::try_extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_all_no_space() { let source = "#noqa"; - let range = TextRange::new(TextSize::from(0), TextSize::from(5)); - let locator = Locator::new(source); - assert_debug_snapshot!(Directive::try_extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_code_no_space() { let source = "#noqa:F401"; - let range = TextRange::new(TextSize::from(0), TextSize::from(10)); - let locator = Locator::new(source); - assert_debug_snapshot!(Directive::try_extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_codes_no_space() { let source = "#noqa:F401,F841"; - let range = TextRange::new(TextSize::from(0), TextSize::from(15)); - let locator = Locator::new(source); - assert_debug_snapshot!(Directive::try_extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_all_multi_space() { let source = "# noqa"; - let range = TextRange::new(TextSize::from(0), TextSize::from(7)); - let locator = Locator::new(source); - assert_debug_snapshot!(Directive::try_extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_code_multi_space() { let source = "# noqa: F401"; - let range = TextRange::new(TextSize::from(0), TextSize::from(13)); - let locator = Locator::new(source); - assert_debug_snapshot!(Directive::try_extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_codes_multi_space() { let source = "# noqa: F401, F841"; - let range = TextRange::new(TextSize::from(0), TextSize::from(20)); - let locator = Locator::new(source); - assert_debug_snapshot!(Directive::try_extract(range, &locator)); + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] From bf02c77fd7038e4f59ef6df11f0bd03eba766d61 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Jul 2023 19:42:21 -0400 Subject: [PATCH 344/447] Replace stat mapping with match statement (#5548) --- .../rules/bad_file_permissions.rs | 156 +++++++++--------- 1 file changed, 75 insertions(+), 81 deletions(-) diff --git a/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs b/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs index 076c97cea3..1344db788c 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs @@ -1,12 +1,11 @@ use num_traits::ToPrimitive; -use once_cell::sync::Lazy; -use rustc_hash::FxHashMap; use rustpython_parser::ast::{self, Constant, Expr, Keyword, Operator, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::compose_call_path; +use ruff_python_ast::call_path::CallPath; use ruff_python_ast::helpers::SimpleCallArgs; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; @@ -23,83 +22,6 @@ impl Violation for BadFilePermissions { } } -const WRITE_WORLD: u16 = 0o2; -const EXECUTE_GROUP: u16 = 0o10; - -static PYSTAT_MAPPING: Lazy> = Lazy::new(|| { - FxHashMap::from_iter([ - ("stat.ST_MODE", 0o0), - ("stat.S_IFDOOR", 0o0), - ("stat.S_IFPORT", 0o0), - ("stat.ST_INO", 0o1), - ("stat.S_IXOTH", 0o1), - ("stat.UF_NODUMP", 0o1), - ("stat.ST_DEV", 0o2), - ("stat.S_IWOTH", 0o2), - ("stat.UF_IMMUTABLE", 0o2), - ("stat.ST_NLINK", 0o3), - ("stat.ST_UID", 0o4), - ("stat.S_IROTH", 0o4), - ("stat.UF_APPEND", 0o4), - ("stat.ST_GID", 0o5), - ("stat.ST_SIZE", 0o6), - ("stat.ST_ATIME", 0o7), - ("stat.S_IRWXO", 0o7), - ("stat.ST_MTIME", 0o10), - ("stat.S_IXGRP", 0o10), - ("stat.UF_OPAQUE", 0o10), - ("stat.ST_CTIME", 0o11), - ("stat.S_IWGRP", 0o20), - ("stat.UF_NOUNLINK", 0o20), - ("stat.S_IRGRP", 0o40), - ("stat.UF_COMPRESSED", 0o40), - ("stat.S_IRWXG", 0o70), - ("stat.S_IEXEC", 0o100), - ("stat.S_IXUSR", 0o100), - ("stat.S_IWRITE", 0o200), - ("stat.S_IWUSR", 0o200), - ("stat.S_IREAD", 0o400), - ("stat.S_IRUSR", 0o400), - ("stat.S_IRWXU", 0o700), - ("stat.S_ISVTX", 0o1000), - ("stat.S_ISGID", 0o2000), - ("stat.S_ENFMT", 0o2000), - ("stat.S_ISUID", 0o4000), - ]) -}); - -fn get_int_value(expr: &Expr) -> Option { - match expr { - Expr::Constant(ast::ExprConstant { - value: Constant::Int(value), - .. - }) => value.to_u16(), - Expr::Attribute(_) => { - compose_call_path(expr).and_then(|path| PYSTAT_MAPPING.get(path.as_str()).copied()) - } - Expr::BinOp(ast::ExprBinOp { - left, - op, - right, - range: _, - }) => { - if let (Some(left_value), Some(right_value)) = - (get_int_value(left), get_int_value(right)) - { - match op { - Operator::BitAnd => Some(left_value & right_value), - Operator::BitOr => Some(left_value | right_value), - Operator::BitXor => Some(left_value ^ right_value), - _ => None, - } - } else { - None - } - } - _ => None, - } -} - /// S103 pub(crate) fn bad_file_permissions( checker: &mut Checker, @@ -116,7 +38,7 @@ pub(crate) fn bad_file_permissions( { let call_args = SimpleCallArgs::new(args, keywords); if let Some(mode_arg) = call_args.argument("mode", 1) { - if let Some(int_value) = get_int_value(mode_arg) { + if let Some(int_value) = int_value(mode_arg, checker.semantic()) { if (int_value & WRITE_WORLD > 0) || (int_value & EXECUTE_GROUP > 0) { checker.diagnostics.push(Diagnostic::new( BadFilePermissions { mask: int_value }, @@ -127,3 +49,75 @@ pub(crate) fn bad_file_permissions( } } } + +const WRITE_WORLD: u16 = 0o2; +const EXECUTE_GROUP: u16 = 0o10; + +fn py_stat(call_path: &CallPath) -> Option { + match call_path.as_slice() { + ["stat", "ST_MODE"] => Some(0o0), + ["stat", "S_IFDOOR"] => Some(0o0), + ["stat", "S_IFPORT"] => Some(0o0), + ["stat", "ST_INO"] => Some(0o1), + ["stat", "S_IXOTH"] => Some(0o1), + ["stat", "UF_NODUMP"] => Some(0o1), + ["stat", "ST_DEV"] => Some(0o2), + ["stat", "S_IWOTH"] => Some(0o2), + ["stat", "UF_IMMUTABLE"] => Some(0o2), + ["stat", "ST_NLINK"] => Some(0o3), + ["stat", "ST_UID"] => Some(0o4), + ["stat", "S_IROTH"] => Some(0o4), + ["stat", "UF_APPEND"] => Some(0o4), + ["stat", "ST_GID"] => Some(0o5), + ["stat", "ST_SIZE"] => Some(0o6), + ["stat", "ST_ATIME"] => Some(0o7), + ["stat", "S_IRWXO"] => Some(0o7), + ["stat", "ST_MTIME"] => Some(0o10), + ["stat", "S_IXGRP"] => Some(0o10), + ["stat", "UF_OPAQUE"] => Some(0o10), + ["stat", "ST_CTIME"] => Some(0o11), + ["stat", "S_IWGRP"] => Some(0o20), + ["stat", "UF_NOUNLINK"] => Some(0o20), + ["stat", "S_IRGRP"] => Some(0o40), + ["stat", "UF_COMPRESSED"] => Some(0o40), + ["stat", "S_IRWXG"] => Some(0o70), + ["stat", "S_IEXEC"] => Some(0o100), + ["stat", "S_IXUSR"] => Some(0o100), + ["stat", "S_IWRITE"] => Some(0o200), + ["stat", "S_IWUSR"] => Some(0o200), + ["stat", "S_IREAD"] => Some(0o400), + ["stat", "S_IRUSR"] => Some(0o400), + ["stat", "S_IRWXU"] => Some(0o700), + ["stat", "S_ISVTX"] => Some(0o1000), + ["stat", "S_ISGID"] => Some(0o2000), + ["stat", "S_ENFMT"] => Some(0o2000), + ["stat", "S_ISUID"] => Some(0o4000), + _ => None, + } +} + +fn int_value(expr: &Expr, model: &SemanticModel) -> Option { + match expr { + Expr::Constant(ast::ExprConstant { + value: Constant::Int(value), + .. + }) => value.to_u16(), + Expr::Attribute(_) => model.resolve_call_path(expr).as_ref().and_then(py_stat), + Expr::BinOp(ast::ExprBinOp { + left, + op, + right, + range: _, + }) => { + let left_value = int_value(left, model)?; + let right_value = int_value(right, model)?; + match op { + Operator::BitAnd => Some(left_value & right_value), + Operator::BitOr => Some(left_value | right_value), + Operator::BitXor => Some(left_value ^ right_value), + _ => None, + } + } + _ => None, + } +} From b56b8915cade6245af78083ec3d5d45a740e9e90 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 6 Jul 2023 01:46:49 -0400 Subject: [PATCH 345/447] Allow MkDocs job to run on forks (#5553) Conditionally check whether the secret is available -- see: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsif. --- .github/workflows/ci.yaml | 9 ++++++++- .github/workflows/docs.yaml | 8 +++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 211c90d17b..5e6c402adf 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -252,18 +252,25 @@ jobs: docs: name: "mkdocs" runs-on: ubuntu-latest + env: + MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }} steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 - name: "Add SSH key" + if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} uses: webfactory/ssh-agent@v0.7.0 with: ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }} - name: "Install Rust toolchain" run: rustup show - uses: Swatinem/rust-cache@v2 - - name: "Install dependencies" + - name: "Install Insiders dependencies" + if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} run: pip install -r docs/requirements-insiders.txt + - name: "Install dependencies" + if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }} + run: pip install -r docs/requirements.txt - name: "Update README File" run: python scripts/transform_readme.py --target mkdocs - name: "Generate docs" diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 986ba0fa4c..b3df05f255 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -10,18 +10,24 @@ jobs: runs-on: ubuntu-latest env: CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }} + MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }} steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 - name: "Add SSH key" + if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} uses: webfactory/ssh-agent@v0.7.0 with: ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }} - name: "Install Rust toolchain" run: rustup show - uses: Swatinem/rust-cache@v2 - - name: "Install dependencies" + - name: "Install Insiders dependencies" + if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} run: pip install -r docs/requirements-insiders.txt + - name: "Install dependencies" + if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }} + run: pip install -r docs/requirements.txt - name: "Copy README File" run: | python scripts/transform_readme.py --target mkdocs From 25981420c4518bb1afe70db5acbaa892eedce6ac Mon Sep 17 00:00:00 2001 From: Kar Petrosyan <92274156+karosis88@users.noreply.github.com> Date: Thu, 6 Jul 2023 09:52:28 -0400 Subject: [PATCH 346/447] Add httpx into the `Who's Using Ruff?` section (#5560) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b488d61eac..41a0a36f85 100644 --- a/README.md +++ b/README.md @@ -362,6 +362,7 @@ Ruff is used by a number of major open-source projects and companies, including: - [FastAPI](https://github.com/tiangolo/fastapi) - [Gradio](https://github.com/gradio-app/gradio) - [Great Expectations](https://github.com/great-expectations/great_expectations) +- [HTTPX](https://github.com/encode/httpx) - Hugging Face ([Transformers](https://github.com/huggingface/transformers), [Datasets](https://github.com/huggingface/datasets), [Diffusers](https://github.com/huggingface/diffusers)) From 8184235f93a6404e08e56e7ba7b5467112f7f330 Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 6 Jul 2023 16:07:47 +0200 Subject: [PATCH 347/447] Try statements have a body: Fix formatter instability (#5558) ## Summary The following code was previously leading to unstable formatting: ```python try: try: pass finally: print(1) # issue7208 except A: pass ``` The comment would be formatted as a trailing comment of `try` which is unstable as an end-of-line comment gets two extra whitespaces. This was originally found in https://github.com/python/cpython/blob/99b00efd5edfd5b26bf9e2a35cbfc96277fdcbb1/Lib/getpass.py#L68-L91 ## Test Plan I added a regression test --- crates/ruff_python_ast/src/node.rs | 2 + .../test/fixtures/ruff/statement/try.py | 10 ++ .../src/comments/placement.rs | 152 +++++++++--------- 3 files changed, 88 insertions(+), 76 deletions(-) diff --git a/crates/ruff_python_ast/src/node.rs b/crates/ruff_python_ast/src/node.rs index cc752a7a1f..ccbc44c723 100644 --- a/crates/ruff_python_ast/src/node.rs +++ b/crates/ruff_python_ast/src/node.rs @@ -4281,6 +4281,8 @@ impl AnyNodeRef<'_> { | AnyNodeRef::StmtFunctionDef(_) | AnyNodeRef::StmtAsyncFunctionDef(_) | AnyNodeRef::StmtClassDef(_) + | AnyNodeRef::StmtTry(_) + | AnyNodeRef::StmtTryStar(_) ) } } diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py index 11d07349bc..3ec6c3ca50 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py @@ -89,3 +89,13 @@ else: # before finally finally: ... + +# try and try star are statements with body +# Minimized from https://github.com/python/cpython/blob/99b00efd5edfd5b26bf9e2a35cbfc96277fdcbb1/Lib/getpass.py#L68-L91 +try: + try: + pass + finally: + print(1) # issue7208 +except A: + pass diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 2692b0e27d..44ac6b5195 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -335,89 +335,89 @@ fn handle_in_between_bodies_end_of_line_comment<'a>( } // The comment must be between two statements... - if let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) - { - // ...and the following statement must be the first statement in an alternate body of the parent... - if !is_first_statement_in_enclosing_alternate_body(following, comment.enclosing_node()) { - // ```python - // if test: - // a - // # comment - // b - // ``` - return CommentPlacement::Default(comment); - } + let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) + else { + return CommentPlacement::Default(comment); + }; - if locator.contains_line_break(TextRange::new(preceding.end(), comment.slice().start())) { - // The `elif` or except handlers have their own body to which we can attach the trailing comment + // ...and the following statement must be the first statement in an alternate body of the parent... + if !is_first_statement_in_enclosing_alternate_body(following, comment.enclosing_node()) { + // ```python + // if test: + // a + // # comment + // b + // ``` + return CommentPlacement::Default(comment); + } + + if locator.contains_line_break(TextRange::new(preceding.end(), comment.slice().start())) { + // The `elif` or except handlers have their own body to which we can attach the trailing comment + // ```python + // if test: + // a + // elif c: # comment + // b + // ``` + if following.is_except_handler() { + return CommentPlacement::trailing(following, comment); + } else if following.is_stmt_if() { + // We have to exclude for following if statements that are not elif by checking the + // indentation // ```python - // if test: - // a - // elif c: # comment - // b - // ``` - if following.is_except_handler() { - return CommentPlacement::trailing(following, comment); - } else if following.is_stmt_if() { - // We have to exclude for following if statements that are not elif by checking the - // indentation - // ```python - // if True: - // pass - // else: # Comment - // if False: - // pass - // pass - // ``` - let base_if_indent = - whitespace::indentation_at_offset(locator, following.range().start()); - let maybe_elif_indent = whitespace::indentation_at_offset( - locator, - comment.enclosing_node().range().start(), - ); - if base_if_indent == maybe_elif_indent { - return CommentPlacement::trailing(following, comment); - } - } - // There are no bodies for the "else" branch and other bodies that are represented as a `Vec`. - // This means, there's no good place to attach the comments to. - // Make this a dangling comments and manually format the comment in - // in the enclosing node's formatting logic. For `try`, it's the formatters responsibility - // to correctly identify the comments for the `finally` and `orelse` block by looking - // at the comment's range. - // - // ```python - // while x == y: + // if True: + // pass + // else: # Comment + // if False: + // pass // pass - // else: # trailing - // print("nooop") // ``` - CommentPlacement::dangling(comment.enclosing_node(), comment) - } else { - // Trailing comment of the preceding statement - // ```python - // while test: - // a # comment - // else: - // b - // ``` - if preceding.is_node_with_body() { - // We can't set this as a trailing comment of the function declaration because it - // will then move behind the function block instead of sticking with the pass - // ```python - // if True: - // def f(): - // pass # a - // else: - // pass - // ``` - CommentPlacement::Default(comment) - } else { - CommentPlacement::trailing(preceding, comment) + let base_if_indent = + whitespace::indentation_at_offset(locator, following.range().start()); + let maybe_elif_indent = whitespace::indentation_at_offset( + locator, + comment.enclosing_node().range().start(), + ); + if base_if_indent == maybe_elif_indent { + return CommentPlacement::trailing(following, comment); } } + // There are no bodies for the "else" branch and other bodies that are represented as a `Vec`. + // This means, there's no good place to attach the comments to. + // Make this a dangling comments and manually format the comment in + // in the enclosing node's formatting logic. For `try`, it's the formatters responsibility + // to correctly identify the comments for the `finally` and `orelse` block by looking + // at the comment's range. + // + // ```python + // while x == y: + // pass + // else: # trailing + // print("nooop") + // ``` + CommentPlacement::dangling(comment.enclosing_node(), comment) } else { - CommentPlacement::Default(comment) + // Trailing comment of the preceding statement + // ```python + // while test: + // a # comment + // else: + // b + // ``` + if preceding.is_node_with_body() { + // We can't set this as a trailing comment of the function declaration because it + // will then move behind the function block instead of sticking with the pass + // ```python + // if True: + // def f(): + // pass # a + // else: + // pass + // ``` + CommentPlacement::Default(comment) + } else { + CommentPlacement::trailing(preceding, comment) + } } } From 528bf2df3adb731c1d1e8021ed931e8a2c152037 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 6 Jul 2023 11:02:46 -0400 Subject: [PATCH 348/447] Use non-Insiders MkDocs for building in forks (#5562) --- .github/workflows/ci.yaml | 6 +++++- .github/workflows/docs.yaml | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5e6c402adf..23d2595b6d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -277,5 +277,9 @@ jobs: run: python scripts/generate_mkdocs.py - name: "Check docs formatting" run: python scripts/check_docs_formatted.py - - name: "Build docs" + - name: "Build Insiders docs" + if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} run: mkdocs build --strict -f mkdocs.insiders.yml + - name: "Build docs" + if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }} + run: mkdocs build --strict -f mkdocs.generated.yml diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index b3df05f255..21f17c1f89 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -32,7 +32,12 @@ jobs: run: | python scripts/transform_readme.py --target mkdocs python scripts/generate_mkdocs.py - mkdocs build --strict -f mkdocs.insiders.yml + - name: "Build Insiders docs" + if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} + run: mkdocs build --strict -f mkdocs.insiders.yml + - name: "Build docs" + if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }} + run: mkdocs build --strict -f mkdocs.generated.yml - name: "Deploy to Cloudflare Pages" if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }} uses: cloudflare/wrangler-action@2.0.0 From 9713ee4b809c7a118bacf4bf7fc7b54000c739a2 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 6 Jul 2023 11:15:46 -0400 Subject: [PATCH 349/447] Remove `ParsedFileExemption::None` (#5555) ## Summary This is more aligned with the other enums in this module. Should've been changed in a previous refactor, just an oversight. --- crates/ruff/src/noqa.rs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/crates/ruff/src/noqa.rs b/crates/ruff/src/noqa.rs index c937bbc898..3868151455 100644 --- a/crates/ruff/src/noqa.rs +++ b/crates/ruff/src/noqa.rs @@ -141,11 +141,11 @@ impl FileExemption { let mut exempt_codes: Vec = vec![]; for range in comment_ranges { - match ParsedFileExemption::extract(&contents[*range]) { - ParsedFileExemption::All => { + match ParsedFileExemption::try_extract(&contents[*range]) { + Some(ParsedFileExemption::All) => { return Some(Self::All); } - ParsedFileExemption::Codes(codes) => { + Some(ParsedFileExemption::Codes(codes)) => { exempt_codes.extend(codes.into_iter().filter_map(|code| { if let Ok(rule) = Rule::from_code(get_redirect_target(code).unwrap_or(code)) { @@ -156,7 +156,7 @@ impl FileExemption { } })); } - ParsedFileExemption::None => {} + None => {} } } @@ -173,8 +173,6 @@ impl FileExemption { /// across a source file. #[derive(Debug)] enum ParsedFileExemption<'a> { - /// No file-level exemption was found. - None, /// The file-level exemption ignores all rules (e.g., `# ruff: noqa`). All, /// The file-level exemption ignores specific rules (e.g., `# ruff: noqa: F401, F841`). @@ -183,14 +181,14 @@ enum ParsedFileExemption<'a> { impl<'a> ParsedFileExemption<'a> { /// Return a [`ParsedFileExemption`] for a given comment line. - fn extract(line: &'a str) -> Self { + fn try_extract(line: &'a str) -> Option { let line = line.trim_whitespace_start(); if line.starts_with("# flake8: noqa") || line.starts_with("# flake8: NOQA") || line.starts_with("# flake8: NoQA") { - return Self::All; + return Some(Self::All); } if let Some(remainder) = line @@ -199,7 +197,7 @@ impl<'a> ParsedFileExemption<'a> { .or_else(|| line.strip_prefix("# ruff: NoQA")) { if remainder.is_empty() { - return Self::All; + return Some(Self::All); } else if let Some(codes) = remainder.strip_prefix(':') { let codes = codes .split(|c: char| c.is_whitespace() || c == ',') @@ -209,12 +207,12 @@ impl<'a> ParsedFileExemption<'a> { if codes.is_empty() { warn!("Expected rule codes on `noqa` directive: \"{line}\""); } - return Self::Codes(codes); + return Some(Self::Codes(codes)); } warn!("Unexpected suffix on `noqa` directive: \"{line}\""); } - Self::None + None } } From 87ca6171cf830e47822c621597db9fe4637e3d9c Mon Sep 17 00:00:00 2001 From: Simon Brugman Date: Thu, 6 Jul 2023 17:55:27 +0200 Subject: [PATCH 350/447] docs: add user (#5563) ## Summary Adding two repositories at ING Bank using ruff. Demonstrates corporate/industry adoption, e.g. similar to AstraZeneca. ## Test Plan Note that the tests failing seems unrelated. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 41a0a36f85..d154cebbbe 100644 --- a/README.md +++ b/README.md @@ -368,6 +368,7 @@ Ruff is used by a number of major open-source projects and companies, including: [Diffusers](https://github.com/huggingface/diffusers)) - [Hatch](https://github.com/pypa/hatch) - [Home Assistant](https://github.com/home-assistant/core) +- ING Bank ([popmon](https://github.com/ing-bank/popmon), [probatus](https://github.com/ing-bank/probatus)) - [Ibis](https://github.com/ibis-project/ibis) - [Jupyter](https://github.com/jupyter-server/jupyter_server) - [LangChain](https://github.com/hwchase17/langchain) From cc822082a70a935e6f47bd4ced226403570fd389 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 6 Jul 2023 12:03:10 -0400 Subject: [PATCH 351/447] Refactor `noqa` directive parsing away from regex-based implementation (#5554) ## Summary I'll write up a more detailed description tomorrow, but in short, this PR removes our regex-based implementation in favor of "manual" parsing. I tried a couple different implementations. In the benchmarks below: - `Directive/Regex` is our implementation on `main`. - `Directive/Find` just uses `text.find("noqa")`, which is insufficient, since it doesn't cover case-insensitive variants like `NOQA`, and doesn't handle multiple `noqa` matches in a single like, like ` # Here's a noqa comment # noqa: F401`. But it's kind of a baseline. - `Directive/Memchr` uses three `memchr` iterative finders (one for `noqa`, `NOQA`, and `NoQA`). - `Directive/AhoCorasick` is roughly the variant checked-in here. The raw results: ``` Directive/Regex/# noqa: F401 time: [273.69 ns 274.71 ns 276.03 ns] change: [+1.4467% +1.8979% +2.4243%] (p = 0.00 < 0.05) Performance has regressed. Found 15 outliers among 100 measurements (15.00%) 3 (3.00%) low mild 8 (8.00%) high mild 4 (4.00%) high severe Directive/Find/# noqa: F401 time: [66.972 ns 67.048 ns 67.132 ns] change: [+2.8292% +2.9377% +3.0540%] (p = 0.00 < 0.05) Performance has regressed. Found 15 outliers among 100 measurements (15.00%) 1 (1.00%) low severe 3 (3.00%) low mild 8 (8.00%) high mild 3 (3.00%) high severe Directive/AhoCorasick/# noqa: F401 time: [76.922 ns 77.189 ns 77.536 ns] change: [+0.4265% +0.6862% +0.9871%] (p = 0.00 < 0.05) Change within noise threshold. Found 8 outliers among 100 measurements (8.00%) 1 (1.00%) low mild 3 (3.00%) high mild 4 (4.00%) high severe Directive/Memchr/# noqa: F401 time: [62.627 ns 62.654 ns 62.679 ns] change: [-0.1780% -0.0887% -0.0120%] (p = 0.03 < 0.05) Change within noise threshold. Found 11 outliers among 100 measurements (11.00%) 1 (1.00%) low severe 5 (5.00%) low mild 3 (3.00%) high mild 2 (2.00%) high severe Directive/Regex/# noqa: F401, F841 time: [321.83 ns 322.39 ns 322.93 ns] change: [+8602.4% +8623.5% +8644.5%] (p = 0.00 < 0.05) Performance has regressed. Found 5 outliers among 100 measurements (5.00%) 1 (1.00%) low severe 2 (2.00%) low mild 1 (1.00%) high mild 1 (1.00%) high severe Directive/Find/# noqa: F401, F841 time: [78.618 ns 78.758 ns 78.896 ns] change: [+1.6909% +1.8771% +2.0628%] (p = 0.00 < 0.05) Performance has regressed. Found 3 outliers among 100 measurements (3.00%) 3 (3.00%) high mild Directive/AhoCorasick/# noqa: F401, F841 time: [87.739 ns 88.057 ns 88.468 ns] change: [+0.1843% +0.4685% +0.7854%] (p = 0.00 < 0.05) Change within noise threshold. Found 11 outliers among 100 measurements (11.00%) 5 (5.00%) low mild 3 (3.00%) high mild 3 (3.00%) high severe Directive/Memchr/# noqa: F401, F841 time: [80.674 ns 80.774 ns 80.860 ns] change: [-0.7343% -0.5633% -0.4031%] (p = 0.00 < 0.05) Change within noise threshold. Found 14 outliers among 100 measurements (14.00%) 4 (4.00%) low severe 9 (9.00%) low mild 1 (1.00%) high mild Directive/Regex/# noqa time: [194.86 ns 195.93 ns 196.97 ns] change: [+11973% +12039% +12103%] (p = 0.00 < 0.05) Performance has regressed. Found 6 outliers among 100 measurements (6.00%) 5 (5.00%) low mild 1 (1.00%) high mild Directive/Find/# noqa time: [25.327 ns 25.354 ns 25.383 ns] change: [+3.8524% +4.0267% +4.1845%] (p = 0.00 < 0.05) Performance has regressed. Found 9 outliers among 100 measurements (9.00%) 6 (6.00%) high mild 3 (3.00%) high severe Directive/AhoCorasick/# noqa time: [34.267 ns 34.368 ns 34.481 ns] change: [+0.5646% +0.8505% +1.1281%] (p = 0.00 < 0.05) Change within noise threshold. Found 5 outliers among 100 measurements (5.00%) 5 (5.00%) high mild Directive/Memchr/# noqa time: [21.770 ns 21.818 ns 21.874 ns] change: [-0.0990% +0.1464% +0.4046%] (p = 0.26 > 0.05) No change in performance detected. Found 10 outliers among 100 measurements (10.00%) 4 (4.00%) low mild 4 (4.00%) high mild 2 (2.00%) high severe Directive/Regex/# type: ignore # noqa: E501 time: [278.76 ns 279.69 ns 280.72 ns] change: [+7449.4% +7469.8% +7490.5%] (p = 0.00 < 0.05) Performance has regressed. Found 3 outliers among 100 measurements (3.00%) 1 (1.00%) low mild 1 (1.00%) high mild 1 (1.00%) high severe Directive/Find/# type: ignore # noqa: E501 time: [67.791 ns 67.976 ns 68.184 ns] change: [+2.8321% +3.1735% +3.5418%] (p = 0.00 < 0.05) Performance has regressed. Found 6 outliers among 100 measurements (6.00%) 5 (5.00%) high mild 1 (1.00%) high severe Directive/AhoCorasick/# type: ignore # noqa: E501 time: [75.908 ns 76.055 ns 76.210 ns] change: [+0.9269% +1.1427% +1.3955%] (p = 0.00 < 0.05) Change within noise threshold. Found 1 outliers among 100 measurements (1.00%) 1 (1.00%) high severe Directive/Memchr/# type: ignore # noqa: E501 time: [72.549 ns 72.723 ns 72.957 ns] change: [+1.5881% +1.9660% +2.3974%] (p = 0.00 < 0.05) Performance has regressed. Found 15 outliers among 100 measurements (15.00%) 10 (10.00%) high mild 5 (5.00%) high severe Directive/Regex/# type: ignore # nosec time: [66.967 ns 67.075 ns 67.207 ns] change: [+1713.0% +1715.8% +1718.9%] (p = 0.00 < 0.05) Performance has regressed. Found 10 outliers among 100 measurements (10.00%) 1 (1.00%) low severe 3 (3.00%) low mild 2 (2.00%) high mild 4 (4.00%) high severe Directive/Find/# type: ignore # nosec time: [18.505 ns 18.548 ns 18.597 ns] change: [+1.3520% +1.6976% +2.0333%] (p = 0.00 < 0.05) Performance has regressed. Found 4 outliers among 100 measurements (4.00%) 4 (4.00%) high mild Directive/AhoCorasick/# type: ignore # nosec time: [16.162 ns 16.206 ns 16.252 ns] change: [+1.2919% +1.5587% +1.8430%] (p = 0.00 < 0.05) Performance has regressed. Found 4 outliers among 100 measurements (4.00%) 3 (3.00%) high mild 1 (1.00%) high severe Directive/Memchr/# type: ignore # nosec time: [39.192 ns 39.233 ns 39.276 ns] change: [+0.5164% +0.7456% +0.9790%] (p = 0.00 < 0.05) Change within noise threshold. Found 13 outliers among 100 measurements (13.00%) 2 (2.00%) low severe 4 (4.00%) low mild 3 (3.00%) high mild 4 (4.00%) high severe Directive/Regex/# some very long comment that # is interspersed with characters but # no directive time: [81.460 ns 81.578 ns 81.703 ns] change: [+2093.3% +2098.8% +2104.2%] (p = 0.00 < 0.05) Performance has regressed. Found 4 outliers among 100 measurements (4.00%) 2 (2.00%) low mild 2 (2.00%) high mild Directive/Find/# some very long comment that # is interspersed with characters but # no directive time: [26.284 ns 26.331 ns 26.387 ns] change: [+0.7554% +1.1027% +1.3832%] (p = 0.00 < 0.05) Change within noise threshold. Found 6 outliers among 100 measurements (6.00%) 5 (5.00%) high mild 1 (1.00%) high severe Directive/AhoCorasick/# some very long comment that # is interspersed with characters but # no direc... time: [28.643 ns 28.714 ns 28.787 ns] change: [+1.3774% +1.6780% +2.0028%] (p = 0.00 < 0.05) Performance has regressed. Found 2 outliers among 100 measurements (2.00%) 2 (2.00%) high mild Directive/Memchr/# some very long comment that # is interspersed with characters but # no directive time: [55.766 ns 55.831 ns 55.897 ns] change: [+1.5802% +1.7476% +1.9021%] (p = 0.00 < 0.05) Performance has regressed. Found 2 outliers among 100 measurements (2.00%) 2 (2.00%) low mild ``` While memchr is faster than aho-corasick in some of the common cases (like `# noqa: F401`), the latter is way, way faster when there _isn't_ a match (like 2x faster -- see the last two cases). Since most comments _aren't_ `noqa` comments, this felt like the right tradeoff. Note that all implementations are significantly faster than the regex version. (I know I originally reported a 10x speedup, but I ended up improving the regex version a bit in some prior PRs, so it got unintentionally faster via some refactors.) There's also one behavior change in here, which is that we now allow variable spaces, e.g., `#noqa` or `# noqa`. Previously, we required exactly one space. This thus closes #5177. --- Cargo.lock | 1 + crates/ruff/Cargo.toml | 1 + crates/ruff/src/noqa.rs | 176 +++++++++++++++--- ...noqa__tests__noqa_all_leading_comment.snap | 11 ++ ...ff__noqa__tests__noqa_all_multi_space.snap | 10 +- .../ruff__noqa__tests__noqa_all_no_space.snap | 10 +- ...oqa__tests__noqa_all_trailing_comment.snap | 11 ++ ...oqa__tests__noqa_code_leading_comment.snap | 14 ++ ...f__noqa__tests__noqa_code_multi_space.snap | 13 +- ...ruff__noqa__tests__noqa_code_no_space.snap | 13 +- ...qa__tests__noqa_code_trailing_comment.snap | 14 ++ ...qa__tests__noqa_codes_leading_comment.snap | 15 ++ ...__noqa__tests__noqa_codes_multi_space.snap | 14 +- ...uff__noqa__tests__noqa_codes_no_space.snap | 14 +- ...a__tests__noqa_codes_trailing_comment.snap | 15 ++ ...ruff__noqa__tests__noqa_invalid_codes.snap | 14 ++ foo.py | 1 + scripts/check_ecosystem.py | 7 +- scripts/pyproject.toml | 3 - 19 files changed, 310 insertions(+), 47 deletions(-) create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_leading_comment.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_trailing_comment.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_leading_comment.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_trailing_comment.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_leading_comment.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_trailing_comment.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__noqa_invalid_codes.snap create mode 100644 foo.py diff --git a/Cargo.lock b/Cargo.lock index 245e470209..db8cdef966 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1811,6 +1811,7 @@ dependencies = [ name = "ruff" version = "0.0.277" dependencies = [ + "aho-corasick 1.0.2", "annotate-snippets 0.9.1", "anyhow", "bitflags 2.3.3", diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 45f4b18c04..7503cdddd5 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -27,6 +27,7 @@ ruff_rustpython = { path = "../ruff_rustpython" } ruff_text_size = { workspace = true } ruff_textwrap = { path = "../ruff_textwrap" } +aho-corasick = { version = "1.0.2" } annotate-snippets = { version = "0.9.1", features = ["color"] } anyhow = { workspace = true } bitflags = { workspace = true } diff --git a/crates/ruff/src/noqa.rs b/crates/ruff/src/noqa.rs index 3868151455..176dd6d345 100644 --- a/crates/ruff/src/noqa.rs +++ b/crates/ruff/src/noqa.rs @@ -4,11 +4,11 @@ use std::fs; use std::ops::Add; use std::path::Path; +use aho_corasick::AhoCorasick; use anyhow::Result; use itertools::Itertools; use log::warn; use once_cell::sync::Lazy; -use regex::Regex; use ruff_text_size::{TextLen, TextRange, TextSize}; use rustpython_parser::ast::Ranged; @@ -20,8 +20,10 @@ use crate::codes::NoqaCode; use crate::registry::{AsRule, Rule, RuleSet}; use crate::rule_redirects::get_redirect_target; -static NOQA_LINE_REGEX: Lazy = Lazy::new(|| { - Regex::new(r"(?P(?i:# noqa)(?::\s?(?P[A-Z]+[0-9]+(?:[,\s]+[A-Z]+[0-9]+)*))?)") +static NOQA_MATCHER: Lazy = Lazy::new(|| { + AhoCorasick::builder() + .ascii_case_insensitive(true) + .build(["noqa"]) .unwrap() }); @@ -38,32 +40,104 @@ pub(crate) enum Directive<'a> { impl<'a> Directive<'a> { /// Extract the noqa `Directive` from a line of Python source code. pub(crate) fn try_extract(text: &'a str, offset: TextSize) -> Option { - let caps = NOQA_LINE_REGEX.captures(text)?; - match (caps.name("noqa"), caps.name("codes")) { - (Some(noqa), Some(codes)) => { - let codes = codes - .as_str() - .split(|c: char| c.is_whitespace() || c == ',') - .map(str::trim) - .filter(|code| !code.is_empty()) - .collect_vec(); - if codes.is_empty() { - warn!("Expected rule codes on `noqa` directive: \"{text}\""); - } - let range = TextRange::at( - TextSize::try_from(noqa.start()).unwrap().add(offset), - noqa.as_str().text_len(), - ); - Some(Self::Codes(Codes { range, codes })) + for mat in NOQA_MATCHER.find_iter(text) { + let noqa_literal_start = mat.start(); + + // Determine the start of the comment. + let mut comment_start = noqa_literal_start; + + // Trim any whitespace between the `#` character and the `noqa` literal. + comment_start = text[..comment_start].trim_end().len(); + + // The next character has to be the `#` character. + if text[..comment_start] + .chars() + .last() + .map_or(false, |c| c != '#') + { + continue; } - (Some(noqa), None) => { - let range = TextRange::at( - TextSize::try_from(noqa.start()).unwrap().add(offset), - noqa.as_str().text_len(), - ); - Some(Self::All(All { range })) - } - _ => None, + comment_start -= '#'.len_utf8(); + + // If the next character is `:`, then it's a list of codes. Otherwise, it's a directive + // to ignore all rules. + let noqa_literal_end = mat.end(); + return Some( + if text[noqa_literal_end..] + .chars() + .next() + .map_or(false, |c| c == ':') + { + // E.g., `# noqa: F401, F841`. + let mut codes_start = noqa_literal_end; + + // Skip the `:` character. + codes_start += ':'.len_utf8(); + + // Skip any whitespace between the `:` and the codes. + codes_start += text[codes_start..] + .find(|c: char| !c.is_whitespace()) + .unwrap_or(0); + + // Extract the comma-separated list of codes. + let mut codes = vec![]; + let mut codes_end = codes_start; + let mut leading_space = 0; + while let Some(code) = Directive::lex_code(&text[codes_end + leading_space..]) { + codes.push(code); + codes_end += leading_space; + codes_end += code.len(); + + // Codes can be comma- or whitespace-delimited. Compute the length of the + // delimiter, but only add it in the next iteration, once we find the next + // code. + if let Some(space_between) = + text[codes_end..].find(|c: char| !(c.is_whitespace() || c == ',')) + { + leading_space = space_between; + } else { + break; + } + } + + let range = TextRange::new( + TextSize::try_from(comment_start).unwrap(), + TextSize::try_from(codes_end).unwrap(), + ); + + Self::Codes(Codes { + range: range.add(offset), + codes, + }) + } else { + // E.g., `# noqa`. + let range = TextRange::new( + TextSize::try_from(comment_start).unwrap(), + TextSize::try_from(noqa_literal_end).unwrap(), + ); + Self::All(All { + range: range.add(offset), + }) + }, + ); + } + + None + } + + /// Lex an individual rule code (e.g., `F401`). + fn lex_code(text: &str) -> Option<&str> { + // Extract, e.g., the `F` in `F401`. + let prefix = text.chars().take_while(char::is_ascii_uppercase).count(); + // Extract, e.g., the `401` in `F401`. + let suffix = text[prefix..] + .chars() + .take_while(char::is_ascii_digit) + .count(); + if prefix > 0 && suffix > 0 { + Some(&text[..prefix + suffix]) + } else { + None } } } @@ -488,7 +562,7 @@ impl NoqaMapping { } /// Returns the re-mapped position or `position` if no mapping exists. - pub fn resolve(&self, offset: TextSize) -> TextSize { + pub(crate) fn resolve(&self, offset: TextSize) -> TextSize { let index = self.ranges.binary_search_by(|range| { if range.end() < offset { std::cmp::Ordering::Less @@ -506,7 +580,7 @@ impl NoqaMapping { } } - pub fn push_mapping(&mut self, range: TextRange) { + pub(crate) fn push_mapping(&mut self, range: TextRange) { if let Some(last_range) = self.ranges.last_mut() { // Strictly sorted insertion if last_range.end() <= range.start() { @@ -634,6 +708,48 @@ mod tests { assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } + #[test] + fn noqa_all_leading_comment() { + let source = "# Some comment describing the noqa # noqa"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_code_leading_comment() { + let source = "# Some comment describing the noqa # noqa: F401"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_codes_leading_comment() { + let source = "# Some comment describing the noqa # noqa: F401, F841"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_all_trailing_comment() { + let source = "# noqa # Some comment describing the noqa"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_code_trailing_comment() { + let source = "# noqa: F401 # Some comment describing the noqa"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_codes_trailing_comment() { + let source = "# noqa: F401, F841 # Some comment describing the noqa"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_invalid_codes() { + let source = "# noqa: F401, unused-import, some other code"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + #[test] fn modification() { let contents = "x = 1"; diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_leading_comment.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_leading_comment.snap new file mode 100644 index 0000000000..e05a6cbb9d --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_leading_comment.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Some( + All( + All { + range: 35..41, + }, + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_multi_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_multi_space.snap index 57be5a0532..a7a81d2752 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_multi_space.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_multi_space.snap @@ -1,5 +1,11 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::extract(range, &locator)" +expression: "Directive::try_extract(source, TextSize::default())" --- -None +Some( + All( + All { + range: 0..7, + }, + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_no_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_no_space.snap index 57be5a0532..a6504f2ee0 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_no_space.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_no_space.snap @@ -1,5 +1,11 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::extract(range, &locator)" +expression: "Directive::try_extract(source, TextSize::default())" --- -None +Some( + All( + All { + range: 0..5, + }, + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_trailing_comment.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_trailing_comment.snap new file mode 100644 index 0000000000..528d99278f --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_trailing_comment.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Some( + All( + All { + range: 0..6, + }, + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_leading_comment.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_leading_comment.snap new file mode 100644 index 0000000000..1132885227 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_leading_comment.snap @@ -0,0 +1,14 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Some( + Codes( + Codes { + range: 35..47, + codes: [ + "F401", + ], + }, + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_multi_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_multi_space.snap index 57be5a0532..e08e9a849a 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_multi_space.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_multi_space.snap @@ -1,5 +1,14 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::extract(range, &locator)" +expression: "Directive::try_extract(source, TextSize::default())" --- -None +Some( + Codes( + Codes { + range: 0..13, + codes: [ + "F401", + ], + }, + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_no_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_no_space.snap index 57be5a0532..7c07294cc7 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_no_space.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_no_space.snap @@ -1,5 +1,14 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::extract(range, &locator)" +expression: "Directive::try_extract(source, TextSize::default())" --- -None +Some( + Codes( + Codes { + range: 0..10, + codes: [ + "F401", + ], + }, + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_trailing_comment.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_trailing_comment.snap new file mode 100644 index 0000000000..ff3293f0af --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_trailing_comment.snap @@ -0,0 +1,14 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Some( + Codes( + Codes { + range: 0..12, + codes: [ + "F401", + ], + }, + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_leading_comment.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_leading_comment.snap new file mode 100644 index 0000000000..d771ab548e --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_leading_comment.snap @@ -0,0 +1,15 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Some( + Codes( + Codes { + range: 35..53, + codes: [ + "F401", + "F841", + ], + }, + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_multi_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_multi_space.snap index 57be5a0532..e76c0a28fd 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_multi_space.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_multi_space.snap @@ -1,5 +1,15 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::extract(range, &locator)" +expression: "Directive::try_extract(source, TextSize::default())" --- -None +Some( + Codes( + Codes { + range: 0..20, + codes: [ + "F401", + "F841", + ], + }, + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_no_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_no_space.snap index 57be5a0532..6c16281542 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_no_space.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_no_space.snap @@ -1,5 +1,15 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::extract(range, &locator)" +expression: "Directive::try_extract(source, TextSize::default())" --- -None +Some( + Codes( + Codes { + range: 0..15, + codes: [ + "F401", + "F841", + ], + }, + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_trailing_comment.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_trailing_comment.snap new file mode 100644 index 0000000000..da2e044c73 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_trailing_comment.snap @@ -0,0 +1,15 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Some( + Codes( + Codes { + range: 0..18, + codes: [ + "F401", + "F841", + ], + }, + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_invalid_codes.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_invalid_codes.snap new file mode 100644 index 0000000000..ff3293f0af --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_invalid_codes.snap @@ -0,0 +1,14 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Some( + Codes( + Codes { + range: 0..12, + codes: [ + "F401", + ], + }, + ), +) diff --git a/foo.py b/foo.py new file mode 100644 index 0000000000..4ff7cee301 --- /dev/null +++ b/foo.py @@ -0,0 +1 @@ +import os # noqa diff --git a/scripts/check_ecosystem.py b/scripts/check_ecosystem.py index 8c4023ce9b..a9904b305a 100755 --- a/scripts/check_ecosystem.py +++ b/scripts/check_ecosystem.py @@ -20,7 +20,7 @@ from asyncio.subprocess import PIPE, create_subprocess_exec from contextlib import asynccontextmanager, nullcontext from pathlib import Path from signal import SIGINT, SIGTERM -from typing import TYPE_CHECKING, NamedTuple, Self +from typing import TYPE_CHECKING, NamedTuple, Self, TypeVar if TYPE_CHECKING: from collections.abc import AsyncIterator, Iterator, Sequence @@ -272,6 +272,9 @@ def read_projects_jsonl(projects_jsonl: Path) -> dict[tuple[str, str], Repositor return repositories +T = TypeVar("T") + + async def main( *, ruff1: Path, @@ -291,7 +294,7 @@ async def main( # Otherwise doing 3k repositories can take >8GB RAM semaphore = asyncio.Semaphore(50) - async def limited_parallelism(coroutine): # noqa: ANN + async def limited_parallelism(coroutine: T) -> T: async with semaphore: return await coroutine diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml index f912d35e1d..bdb5a08d5e 100644 --- a/scripts/pyproject.toml +++ b/scripts/pyproject.toml @@ -23,6 +23,3 @@ ignore = [ [tool.ruff.isort] required-imports = ["from __future__ import annotations"] - -[tool.ruff.pydocstyle] -convention = "pep257" From 3650aaa8b306078b76043df9ad725e703705cc4c Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Thu, 6 Jul 2023 18:46:16 +0100 Subject: [PATCH 352/447] Add documentation to the `S1XX` rules (#5479) ## Summary Add documentation to the `S1XX` rules (the `flake8-bandit` ['misc tests'](https://bandit.readthedocs.io/en/latest/plugins/index.html#plugin-id-groupings) rule group). ## Test Plan `python scripts/check_docs_formatted.py && mkdocs serve` --- .../rules/bad_file_permissions.rs | 27 ++++++++++++++- .../rules/flake8_bandit/rules/exec_used.rs | 15 ++++++++ .../rules/hardcoded_bind_all_interfaces.rs | 21 ++++++++++++ .../rules/hardcoded_password_default.rs | 33 +++++++++++++++++- .../rules/hardcoded_password_func_arg.rs | 29 +++++++++++++++- .../rules/hardcoded_password_string.rs | 26 ++++++++++++++ .../rules/hardcoded_tmp_directory.rs | 29 ++++++++++++++++ .../rules/request_without_timeout.rs | 25 ++++++++++++++ .../rules/try_except_continue.rs | 34 +++++++++++++++++++ .../flake8_bandit/rules/try_except_pass.rs | 30 ++++++++++++++++ 10 files changed, 266 insertions(+), 3 deletions(-) diff --git a/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs b/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs index 1344db788c..8cad152de7 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs @@ -9,6 +9,31 @@ use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for files with overly permissive permissions. +/// +/// ## Why is this bad? +/// Overly permissive file permissions may allow unintended access and +/// arbitrary code execution. +/// +/// ## Example +/// ```python +/// import os +/// +/// os.chmod("/etc/secrets.txt", 0o666) # rw-rw-rw- +/// ``` +/// +/// Use instead: +/// ```python +/// import os +/// +/// os.chmod("/etc/secrets.txt", 0o600) # rw------- +/// ``` +/// +/// ## References +/// - [Python documentation: `os.chmod`](https://docs.python.org/3/library/os.html#os.chmod) +/// - [Python documentation: `stat`](https://docs.python.org/3/library/stat.html) +/// - [Common Weakness Enumeration: CWE-732](https://cwe.mitre.org/data/definitions/732.html) #[violation] pub struct BadFilePermissions { mask: u16, @@ -18,7 +43,7 @@ impl Violation for BadFilePermissions { #[derive_message_formats] fn message(&self) -> String { let BadFilePermissions { mask } = self; - format!("`os.chmod` setting a permissive mask `{mask:#o}` on file or directory",) + format!("`os.chmod` setting a permissive mask `{mask:#o}` on file or directory") } } 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 d2dfb83fb5..af0caabc1d 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/exec_used.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/exec_used.rs @@ -5,6 +5,21 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for uses of the builtin `exec` function. +/// +/// ## Why is this bad? +/// The `exec()` function is insecure as it allows for arbitrary code +/// execution. +/// +/// ## Example +/// ```python +/// exec("print('Hello World')") +/// ``` +/// +/// ## References +/// - [Python documentation: `exec`](https://docs.python.org/3/library/functions.html#exec) +/// - [Common Weakness Enumeration: CWE-78](https://cwe.mitre.org/data/definitions/78.html) #[violation] pub struct ExecBuiltin; diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs index 86f68e10b8..ac8090395b 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs @@ -3,6 +3,27 @@ use ruff_text_size::TextRange; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +/// ## What it does +/// Checks for hardcoded bindings to all network interfaces (`0.0.0.0`). +/// +/// ## Why is this bad? +/// Binding to all network interfaces is insecure as it allows access from +/// unintended interfaces, which may be poorly secured or unauthorized. +/// +/// Instead, bind to specific interfaces. +/// +/// ## Example +/// ```python +/// ALLOWED_HOSTS = ["0.0.0.0"] +/// ``` +/// +/// Use instead: +/// ```python +/// ALLOWED_HOSTS = ["127.0.0.1", "localhost"] +/// ``` +/// +/// ## References +/// - [Common Weakness Enumeration: CWE-200](https://cwe.mitre.org/data/definitions/200.html) #[violation] pub struct HardcodedBindAllInterfaces; 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 50be800d9e..7883d346ac 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,11 +1,42 @@ 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}; +use crate::checkers::ast::Checker; + use super::super::helpers::{matches_password_name, string_literal}; +/// ## What it does +/// Checks for potential uses of hardcoded passwords in function argument +/// defaults. +/// +/// ## Why is this bad? +/// Including a hardcoded password in source code is a security risk, as an +/// attacker could discover the password and use it to gain unauthorized +/// access. +/// +/// Instead, store passwords and other secrets in configuration files, +/// environment variables, or other sources that are excluded from version +/// control. +/// +/// ## Example +/// ```python +/// def connect_to_server(password="hunter2"): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// import os +/// +/// +/// def connect_to_server(password=os.environ["PASSWORD"]): +/// ... +/// ``` +/// +/// ## References +/// - [Common Weakness Enumeration: CWE-259](https://cwe.mitre.org/data/definitions/259.html) #[violation] pub struct HardcodedPasswordDefault { name: String, diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_func_arg.rs b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_func_arg.rs index 1874d0e26d..5449b51171 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_func_arg.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_func_arg.rs @@ -1,11 +1,38 @@ use rustpython_parser::ast::{Keyword, Ranged}; -use crate::checkers::ast::Checker; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use crate::checkers::ast::Checker; + use super::super::helpers::{matches_password_name, string_literal}; +/// ## What it does +/// Checks for potential uses of hardcoded passwords in function calls. +/// +/// ## Why is this bad? +/// Including a hardcoded password in source code is a security risk, as an +/// attacker could discover the password and use it to gain unauthorized +/// access. +/// +/// Instead, store passwords and other secrets in configuration files, +/// environment variables, or other sources that are excluded from version +/// control. +/// +/// ## Example +/// ```python +/// connect_to_server(password="hunter2") +/// ``` +/// +/// Use instead: +/// ```python +/// import os +/// +/// connect_to_server(password=os.environ["PASSWORD"]) +/// ``` +/// +/// ## References +/// - [Common Weakness Enumeration: CWE-259](https://cwe.mitre.org/data/definitions/259.html) #[violation] pub struct HardcodedPasswordFuncArg { name: String, diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_string.rs b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_string.rs index e8d09125e5..c48036517f 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_string.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_string.rs @@ -7,6 +7,32 @@ use crate::checkers::ast::Checker; use super::super::helpers::{matches_password_name, string_literal}; +/// ## What it does +/// Checks for potential uses of hardcoded passwords in strings. +/// +/// ## Why is this bad? +/// Including a hardcoded password in source code is a security risk, as an +/// attacker could discover the password and use it to gain unauthorized +/// access. +/// +/// Instead, store passwords and other secrets in configuration files, +/// environment variables, or other sources that are excluded from version +/// control. +/// +/// ## Example +/// ```python +/// SECRET_KEY = "hunter2" +/// ``` +/// +/// Use instead: +/// ```python +/// import os +/// +/// SECRET_KEY = os.environ["SECRET_KEY"] +/// ``` +/// +/// ## References +/// - [Common Weakness Enumeration: CWE-259](https://cwe.mitre.org/data/definitions/259.html) #[violation] pub struct HardcodedPasswordString { name: String, diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs index 8c27763f80..2fae004907 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs @@ -3,6 +3,35 @@ use rustpython_parser::ast::{Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +/// ## What it does +/// Checks for the use of hardcoded temporary file or directory paths. +/// +/// ## Why is this bad? +/// The use of hardcoded paths for temporary files can be insecure. If an +/// attacker discovers the location of a hardcoded path, they can replace the +/// contents of the file or directory with a malicious payload. +/// +/// Other programs may also read or write contents to these hardcoded paths, +/// causing unexpected behavior. +/// +/// ## Example +/// ```python +/// with open("/tmp/foo.txt", "w") as file: +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// import tempfile +/// +/// with tempfile.NamedTemporaryFile() as file: +/// ... +/// ``` +/// +/// ## References +/// - [Common Weakness Enumeration: CWE-377](https://cwe.mitre.org/data/definitions/377.html) +/// - [Common Weakness Enumeration: CWE-379](https://cwe.mitre.org/data/definitions/379.html) +/// - [Python documentation: `tempfile`](https://docs.python.org/3/library/tempfile.html) #[violation] pub struct HardcodedTempFile { string: String, 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 08736d5fc9..e62e92687c 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 @@ -6,6 +6,31 @@ use ruff_python_ast::helpers::{is_const_none, SimpleCallArgs}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for uses of the Python `requests` module that omit the `timeout` +/// parameter. +/// +/// ## Why is this bad? +/// The `timeout` parameter is used to set the maximum time to wait for a +/// response from the server. By omitting the `timeout` parameter, the program +/// may hang indefinitely while awaiting a response. +/// +/// ## Example +/// ```python +/// import requests +/// +/// requests.get("https://www.example.com/") +/// ``` +/// +/// Use instead: +/// ```python +/// import requests +/// +/// requests.get("https://www.example.com/", timeout=10) +/// ``` +/// +/// ## References +/// - [Requests documentation: Timeouts](https://requests.readthedocs.io/en/latest/user/advanced/#timeouts) #[violation] pub struct RequestWithoutTimeout { implicit: bool, diff --git a/crates/ruff/src/rules/flake8_bandit/rules/try_except_continue.rs b/crates/ruff/src/rules/flake8_bandit/rules/try_except_continue.rs index 5a4e4faec4..82cc379cfb 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/try_except_continue.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/try_except_continue.rs @@ -6,6 +6,40 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::rules::flake8_bandit::helpers::is_untyped_exception; +/// ## What it does +/// Checks for uses of the `try`-`except`-`continue` pattern. +/// +/// ## Why is this bad? +/// The `try`-`except`-`continue` pattern suppresses all exceptions. +/// Suppressing exceptions may hide errors that could otherwise reveal +/// unexpected behavior, security vulnerabilities, or malicious activity. +/// Instead, consider logging the exception. +/// +/// ## Example +/// ```python +/// import logging +/// +/// while predicate: +/// try: +/// ... +/// except Exception: +/// continue +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// while predicate: +/// try: +/// ... +/// except Exception as exc: +/// logging.exception("Error occurred") +/// ``` +/// +/// ## References +/// - [Common Weakness Enumeration: CWE-703](https://cwe.mitre.org/data/definitions/703.html) +/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) #[violation] pub struct TryExceptContinue; diff --git a/crates/ruff/src/rules/flake8_bandit/rules/try_except_pass.rs b/crates/ruff/src/rules/flake8_bandit/rules/try_except_pass.rs index ee4bd533d3..ba4e1d713d 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/try_except_pass.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/try_except_pass.rs @@ -6,6 +6,36 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::rules::flake8_bandit::helpers::is_untyped_exception; +/// ## What it does +/// Checks for uses of the `try`-`except`-`pass` pattern. +/// +/// ## Why is this bad? +/// The `try`-`except`-`pass` pattern suppresses all exceptions. Suppressing +/// exceptions may hide errors that could otherwise reveal unexpected behavior, +/// security vulnerabilities, or malicious activity. Instead, consider logging +/// the exception. +/// +/// ## Example +/// ```python +/// try: +/// ... +/// except Exception: +/// pass +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// try: +/// ... +/// except Exception as exc: +/// logging.exception("Exception occurred") +/// ``` +/// +/// ## References +/// - [Common Weakness Enumeration: CWE-703](https://cwe.mitre.org/data/definitions/703.html) +/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) #[violation] pub struct TryExceptPass; From 5e5a96ca281995c1b85dad02a175df0f41fa836b Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 6 Jul 2023 20:23:53 +0200 Subject: [PATCH 353/447] Fix formatter `StmtTry` test (#5568) For some reason this didn't turn up on CI before CC @michareiser this is the fix for the error you had --- .../snapshots/format@statement__try.py.snap | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__try.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__try.py.snap index 588c6e5b50..eb3717b299 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__try.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__try.py.snap @@ -95,6 +95,16 @@ else: # before finally finally: ... + +# try and try star are statements with body +# Minimized from https://github.com/python/cpython/blob/99b00efd5edfd5b26bf9e2a35cbfc96277fdcbb1/Lib/getpass.py#L68-L91 +try: + try: + pass + finally: + print(1) # issue7208 +except A: + pass ``` ## Output @@ -200,6 +210,16 @@ else: # before finally finally: ... + +# try and try star are statements with body +# Minimized from https://github.com/python/cpython/blob/99b00efd5edfd5b26bf9e2a35cbfc96277fdcbb1/Lib/getpass.py#L68-L91 +try: + try: + pass + finally: + print(1) # issue7208 +except A: + pass ``` From edfe76d673b01118fa97255fc0d3fe315e042c6a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 6 Jul 2023 17:46:17 -0400 Subject: [PATCH 354/447] Remove checked-in scratch file (#5573) --- foo.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 foo.py diff --git a/foo.py b/foo.py deleted file mode 100644 index 4ff7cee301..0000000000 --- a/foo.py +++ /dev/null @@ -1 +0,0 @@ -import os # noqa From 5908b391023e6f4778013c5cdc03eb10ad2ed5c1 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Fri, 7 Jul 2023 01:37:41 +0100 Subject: [PATCH 355/447] Support globbing in `isort` options (#5473) ## Summary Support glob patterns in `isort` options. Closes #5420. ## Test Plan Added test. `cargo test` --- crates/ruff/src/rules/isort/categorize.rs | 92 ++++++----- crates/ruff/src/rules/isort/mod.rs | 44 ++++- crates/ruff/src/rules/isort/settings.rs | 152 ++++++++++++++++-- ...kage_first_and_third_party_imports.py.snap | 33 ++++ crates/ruff/src/settings/mod.rs | 3 +- 5 files changed, 260 insertions(+), 64 deletions(-) create mode 100644 crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__glob_1_separate_subpackage_first_and_third_party_imports.py.snap diff --git a/crates/ruff/src/rules/isort/categorize.rs b/crates/ruff/src/rules/isort/categorize.rs index c9ec34447a..3ed3d8d1a3 100644 --- a/crates/ruff/src/rules/isort/categorize.rs +++ b/crates/ruff/src/rules/isort/categorize.rs @@ -1,9 +1,10 @@ use std::collections::BTreeMap; +use std::hash::BuildHasherDefault; use std::path::{Path, PathBuf}; use std::{fs, iter}; use log::debug; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use serde::{Deserialize, Serialize}; use strum_macros::EnumIter; @@ -210,20 +211,20 @@ pub(crate) fn categorize_imports<'a>( #[derive(Debug, Default, CacheKey)] pub struct KnownModules { /// A map of known modules to their section. - known: FxHashMap, + known: Vec<(glob::Pattern, ImportSection)>, /// Whether any of the known modules are submodules (e.g., `foo.bar`, as opposed to `foo`). has_submodules: bool, } impl KnownModules { pub(crate) fn new( - first_party: Vec, - third_party: Vec, - local_folder: Vec, - standard_library: Vec, - user_defined: FxHashMap>, + first_party: Vec, + third_party: Vec, + local_folder: Vec, + standard_library: Vec, + user_defined: FxHashMap>, ) -> Self { - let modules = user_defined + let known: Vec<(glob::Pattern, ImportSection)> = user_defined .into_iter() .flat_map(|(section, modules)| { modules @@ -249,19 +250,23 @@ impl KnownModules { standard_library .into_iter() .map(|module| (module, ImportSection::Known(ImportType::StandardLibrary))), - ); + ) + .collect(); - let mut known = FxHashMap::with_capacity_and_hasher( - modules.size_hint().0, - std::hash::BuildHasherDefault::default(), - ); - modules.for_each(|(module, section)| { - if known.insert(module, section).is_some() { - warn_user_once!("One or more modules are part of multiple import sections."); + // Warn in the case of duplicate modules. + let mut seen = + FxHashSet::with_capacity_and_hasher(known.len(), BuildHasherDefault::default()); + for (module, _) in &known { + if !seen.insert(module) { + warn_user_once!("One or more modules are part of multiple import sections, including: `{module}`"); + break; } - }); + } - let has_submodules = known.keys().any(|module| module.contains('.')); + // Check if any of the known modules are submodules. + let has_submodules = known + .iter() + .any(|(module, _)| module.as_str().contains('.')); Self { known, @@ -296,31 +301,37 @@ impl KnownModules { } fn categorize_submodule(&self, submodule: &str) -> Option<(&ImportSection, Reason)> { - if let Some(section) = self.known.get(submodule) { - let reason = match section { - ImportSection::UserDefined(_) => Reason::UserDefinedSection, - ImportSection::Known(ImportType::FirstParty) => Reason::KnownFirstParty, - ImportSection::Known(ImportType::ThirdParty) => Reason::KnownThirdParty, - ImportSection::Known(ImportType::LocalFolder) => Reason::KnownLocalFolder, - ImportSection::Known(ImportType::StandardLibrary) => Reason::ExtraStandardLibrary, - ImportSection::Known(ImportType::Future) => { - unreachable!("__future__ imports are not known") - } - }; - Some((section, reason)) - } else { - None - } + let section = self.known.iter().find_map(|(pattern, section)| { + if pattern.matches(submodule) { + Some(section) + } else { + None + } + })?; + let reason = match section { + ImportSection::UserDefined(_) => Reason::UserDefinedSection, + ImportSection::Known(ImportType::FirstParty) => Reason::KnownFirstParty, + ImportSection::Known(ImportType::ThirdParty) => Reason::KnownThirdParty, + ImportSection::Known(ImportType::LocalFolder) => Reason::KnownLocalFolder, + ImportSection::Known(ImportType::StandardLibrary) => Reason::ExtraStandardLibrary, + ImportSection::Known(ImportType::Future) => { + unreachable!("__future__ imports are not known") + } + }; + Some((section, reason)) } /// Return the list of modules that are known to be of a given type. - pub(crate) fn modules_for_known_type(&self, import_type: ImportType) -> Vec { + pub(crate) fn modules_for_known_type( + &self, + import_type: ImportType, + ) -> impl Iterator { self.known .iter() - .filter_map(|(module, known_section)| { + .filter_map(move |(module, known_section)| { if let ImportSection::Known(section) = known_section { if *section == import_type { - Some(module.clone()) + Some(module) } else { None } @@ -328,18 +339,17 @@ impl KnownModules { None } }) - .collect() } /// Return the list of user-defined modules, indexed by section. - pub(crate) fn user_defined(&self) -> FxHashMap> { - let mut user_defined: FxHashMap> = FxHashMap::default(); + pub(crate) fn user_defined(&self) -> FxHashMap<&str, Vec<&glob::Pattern>> { + let mut user_defined: FxHashMap<&str, Vec<&glob::Pattern>> = FxHashMap::default(); for (module, section) in &self.known { if let ImportSection::UserDefined(section_name) = section { user_defined - .entry(section_name.clone()) + .entry(section_name.as_str()) .or_default() - .push(module.clone()); + .push(module); } } user_defined diff --git a/crates/ruff/src/rules/isort/mod.rs b/crates/ruff/src/rules/isort/mod.rs index f156451770..bcc2f1d9a9 100644 --- a/crates/ruff/src/rules/isort/mod.rs +++ b/crates/ruff/src/rules/isort/mod.rs @@ -369,6 +369,10 @@ mod tests { Ok(()) } + fn pattern(pattern: &str) -> glob::Pattern { + glob::Pattern::new(pattern).unwrap() + } + #[test_case(Path::new("separate_subpackage_first_and_third_party_imports.py"))] fn separate_modules(path: &Path) -> Result<()> { let snapshot = format!("1_{}", path.to_string_lossy()); @@ -377,8 +381,32 @@ mod tests { &Settings { isort: super::settings::Settings { known_modules: KnownModules::new( - vec!["foo.bar".to_string(), "baz".to_string()], - vec!["foo".to_string(), "__future__".to_string()], + vec![pattern("foo.bar"), pattern("baz")], + vec![pattern("foo"), pattern("__future__")], + vec![], + vec![], + FxHashMap::default(), + ), + ..super::settings::Settings::default() + }, + src: vec![test_resource_path("fixtures/isort")], + ..Settings::for_rule(Rule::UnsortedImports) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } + + #[test_case(Path::new("separate_subpackage_first_and_third_party_imports.py"))] + fn separate_modules_glob(path: &Path) -> Result<()> { + let snapshot = format!("glob_1_{}", path.to_string_lossy()); + let diagnostics = test_path( + Path::new("isort").join(path).as_path(), + &Settings { + isort: super::settings::Settings { + known_modules: KnownModules::new( + vec![pattern("foo.*"), pattern("baz")], + vec![pattern("foo"), pattern("__future__")], vec![], vec![], FxHashMap::default(), @@ -401,8 +429,8 @@ mod tests { &Settings { isort: super::settings::Settings { known_modules: KnownModules::new( - vec!["foo".to_string()], - vec!["foo.bar".to_string()], + vec![pattern("foo")], + vec![pattern("foo.bar")], vec![], vec![], FxHashMap::default(), @@ -445,7 +473,7 @@ mod tests { known_modules: KnownModules::new( vec![], vec![], - vec!["ruff".to_string()], + vec![pattern("ruff")], vec![], FxHashMap::default(), ), @@ -961,7 +989,7 @@ mod tests { vec![], vec![], vec![], - FxHashMap::from_iter([("django".to_string(), vec!["django".to_string()])]), + FxHashMap::from_iter([("django".to_string(), vec![pattern("django")])]), ), section_order: vec![ ImportSection::Known(ImportType::Future), @@ -990,11 +1018,11 @@ mod tests { src: vec![test_resource_path("fixtures/isort")], isort: super::settings::Settings { known_modules: KnownModules::new( - vec!["library".to_string()], + vec![pattern("library")], vec![], vec![], vec![], - FxHashMap::from_iter([("django".to_string(), vec!["django".to_string()])]), + FxHashMap::from_iter([("django".to_string(), vec![pattern("django")])]), ), section_order: vec![ ImportSection::Known(ImportType::Future), diff --git a/crates/ruff/src/rules/isort/settings.rs b/crates/ruff/src/rules/isort/settings.rs index f01aa4afd1..8e9648df1b 100644 --- a/crates/ruff/src/rules/isort/settings.rs +++ b/crates/ruff/src/rules/isort/settings.rs @@ -1,6 +1,8 @@ //! Settings for the `isort` plugin. use std::collections::BTreeSet; +use std::error::Error; +use std::fmt; use std::hash::BuildHasherDefault; use rustc_hash::{FxHashMap, FxHashSet}; @@ -11,6 +13,7 @@ use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions}; use crate::rules::isort::categorize::KnownModules; use crate::rules::isort::ImportType; +use crate::settings::types::IdentifierPattern; use crate::warn_user_once; use super::categorize::ImportSection; @@ -154,6 +157,9 @@ pub struct Options { )] /// A list of modules to consider first-party, regardless of whether they /// can be identified as such via introspection of the local filesystem. + /// + /// Supports glob patterns. For more information on the glob syntax, refer + /// to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax). pub known_first_party: Option>, #[option( default = r#"[]"#, @@ -164,6 +170,9 @@ pub struct Options { )] /// A list of modules to consider third-party, regardless of whether they /// can be identified as such via introspection of the local filesystem. + /// + /// Supports glob patterns. For more information on the glob syntax, refer + /// to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax). pub known_third_party: Option>, #[option( default = r#"[]"#, @@ -174,6 +183,9 @@ pub struct Options { )] /// A list of modules to consider being a local folder. /// Generally, this is reserved for relative imports (`from . import module`). + /// + /// Supports glob patterns. For more information on the glob syntax, refer + /// to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax). pub known_local_folder: Option>, #[option( default = r#"[]"#, @@ -184,6 +196,9 @@ pub struct Options { )] /// A list of modules to consider standard-library, in addition to those /// known to Ruff in advance. + /// + /// Supports glob patterns. For more information on the glob syntax, refer + /// to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax). pub extra_standard_library: Option>, #[option( default = r#"furthest-to-closest"#, @@ -357,21 +372,63 @@ impl Default for Settings { } } -impl From for Settings { - fn from(options: Options) -> Self { +impl TryFrom for Settings { + type Error = SettingsError; + + fn try_from(options: Options) -> Result { // Extract any configuration options that deal with user-defined sections. let mut section_order: Vec<_> = options .section_order .unwrap_or_else(|| ImportType::iter().map(ImportSection::Known).collect()); - let known_first_party = options.known_first_party.unwrap_or_default(); - let known_third_party = options.known_third_party.unwrap_or_default(); - let known_local_folder = options.known_local_folder.unwrap_or_default(); - let extra_standard_library = options.extra_standard_library.unwrap_or_default(); + let known_first_party = options + .known_first_party + .map(|names| { + names + .into_iter() + .map(|name| IdentifierPattern::new(&name)) + .collect() + }) + .transpose() + .map_err(SettingsError::InvalidKnownFirstParty)? + .unwrap_or_default(); + let known_third_party = options + .known_third_party + .map(|names| { + names + .into_iter() + .map(|name| IdentifierPattern::new(&name)) + .collect() + }) + .transpose() + .map_err(SettingsError::InvalidKnownThirdParty)? + .unwrap_or_default(); + let known_local_folder = options + .known_local_folder + .map(|names| { + names + .into_iter() + .map(|name| IdentifierPattern::new(&name)) + .collect() + }) + .transpose() + .map_err(SettingsError::InvalidKnownLocalFolder)? + .unwrap_or_default(); + let extra_standard_library = options + .extra_standard_library + .map(|names| { + names + .into_iter() + .map(|name| IdentifierPattern::new(&name)) + .collect() + }) + .transpose() + .map_err(SettingsError::InvalidExtraStandardLibrary)? + .unwrap_or_default(); let no_lines_before = options.no_lines_before.unwrap_or_default(); let sections = options.sections.unwrap_or_default(); // Verify that `sections` doesn't contain any built-in sections. - let sections: FxHashMap> = sections + let sections: FxHashMap> = sections .into_iter() .filter_map(|(section, modules)| match section { ImportSection::Known(section) => { @@ -380,7 +437,17 @@ impl From for Settings { } ImportSection::UserDefined(section) => Some((section, modules)), }) - .collect(); + .map(|(section, modules)| { + let modules = modules + .into_iter() + .map(|module| { + IdentifierPattern::new(&module) + .map_err(SettingsError::InvalidUserDefinedSection) + }) + .collect::, Self::Error>>()?; + Ok((section, modules)) + }) + .collect::>()?; // Verify that `section_order` doesn't contain any duplicates. let mut seen = @@ -435,7 +502,7 @@ impl From for Settings { } } - Self { + Ok(Self { required_imports: BTreeSet::from_iter(options.required_imports.unwrap_or_default()), combine_as_imports: options.combine_as_imports.unwrap_or(false), force_single_line: options.force_single_line.unwrap_or(false), @@ -464,6 +531,50 @@ impl From for Settings { lines_between_types: options.lines_between_types.unwrap_or_default(), forced_separate: Vec::from_iter(options.forced_separate.unwrap_or_default()), section_order, + }) + } +} + +/// Error returned by the [`TryFrom`] implementation of [`Settings`]. +#[derive(Debug)] +pub enum SettingsError { + InvalidKnownFirstParty(glob::PatternError), + InvalidKnownThirdParty(glob::PatternError), + InvalidKnownLocalFolder(glob::PatternError), + InvalidExtraStandardLibrary(glob::PatternError), + InvalidUserDefinedSection(glob::PatternError), +} + +impl fmt::Display for SettingsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SettingsError::InvalidKnownThirdParty(err) => { + write!(f, "invalid known third-party pattern: {err}") + } + SettingsError::InvalidKnownFirstParty(err) => { + write!(f, "invalid known first-party pattern: {err}") + } + SettingsError::InvalidKnownLocalFolder(err) => { + write!(f, "invalid known local folder pattern: {err}") + } + SettingsError::InvalidExtraStandardLibrary(err) => { + write!(f, "invalid extra standard library pattern: {err}") + } + SettingsError::InvalidUserDefinedSection(err) => { + write!(f, "invalid user-defined section pattern: {err}") + } + } + } +} + +impl Error for SettingsError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + SettingsError::InvalidKnownThirdParty(err) => Some(err), + SettingsError::InvalidKnownFirstParty(err) => Some(err), + SettingsError::InvalidKnownLocalFolder(err) => Some(err), + SettingsError::InvalidExtraStandardLibrary(err) => Some(err), + SettingsError::InvalidUserDefinedSection(err) => Some(err), } } } @@ -476,7 +587,9 @@ impl From for Options { extra_standard_library: Some( settings .known_modules - .modules_for_known_type(ImportType::StandardLibrary), + .modules_for_known_type(ImportType::StandardLibrary) + .map(ToString::to_string) + .collect(), ), force_single_line: Some(settings.force_single_line), force_sort_within_sections: Some(settings.force_sort_within_sections), @@ -486,17 +599,23 @@ impl From for Options { known_first_party: Some( settings .known_modules - .modules_for_known_type(ImportType::FirstParty), + .modules_for_known_type(ImportType::FirstParty) + .map(ToString::to_string) + .collect(), ), known_third_party: Some( settings .known_modules - .modules_for_known_type(ImportType::ThirdParty), + .modules_for_known_type(ImportType::ThirdParty) + .map(ToString::to_string) + .collect(), ), known_local_folder: Some( settings .known_modules - .modules_for_known_type(ImportType::LocalFolder), + .modules_for_known_type(ImportType::LocalFolder) + .map(ToString::to_string) + .collect(), ), order_by_type: Some(settings.order_by_type), relative_imports_order: Some(settings.relative_imports_order), @@ -515,7 +634,12 @@ impl From for Options { .known_modules .user_defined() .into_iter() - .map(|(section, modules)| (ImportSection::UserDefined(section), modules)) + .map(|(section, modules)| { + ( + ImportSection::UserDefined(section.to_string()), + modules.into_iter().map(ToString::to_string).collect(), + ) + }) .collect(), ), } diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__glob_1_separate_subpackage_first_and_third_party_imports.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__glob_1_separate_subpackage_first_and_third_party_imports.py.snap new file mode 100644 index 0000000000..41e6eb47f6 --- /dev/null +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__glob_1_separate_subpackage_first_and_third_party_imports.py.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff/src/rules/isort/mod.rs +--- +separate_subpackage_first_and_third_party_imports.py:1:1: I001 [*] Import block is un-sorted or un-formatted + | +1 | / import sys +2 | | import baz +3 | | from foo import bar, baz +4 | | from foo.bar import blah, blub +5 | | from foo.bar.baz import something +6 | | import foo +7 | | import foo.bar +8 | | import foo.bar.baz + | + = help: Organize imports + +ℹ Fix +1 1 | import sys + 2 |+ + 3 |+import foo + 4 |+from foo import bar, baz + 5 |+ +2 6 | import baz +3 |-from foo import bar, baz + 7 |+import foo.bar + 8 |+import foo.bar.baz +4 9 | from foo.bar import blah, blub +5 10 | from foo.bar.baz import something +6 |-import foo +7 |-import foo.bar +8 |-import foo.bar.baz + + diff --git a/crates/ruff/src/settings/mod.rs b/crates/ruff/src/settings/mod.rs index f00a4833b2..b398292771 100644 --- a/crates/ruff/src/settings/mod.rs +++ b/crates/ruff/src/settings/mod.rs @@ -258,7 +258,8 @@ impl Settings { .unwrap_or_default(), isort: config .isort - .map(isort::settings::Settings::from) + .map(isort::settings::Settings::try_from) + .transpose()? .unwrap_or_default(), mccabe: config .mccabe From cd4718988a87080c3f0bde92b96228b07236198c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 6 Jul 2023 22:38:39 -0400 Subject: [PATCH 356/447] Update JSON schema (#5576) Confused as to how this got merged, but... oh well. --- ruff.schema.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ruff.schema.json b/ruff.schema.json index 47e0a9fde0..1a688b4386 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1170,7 +1170,7 @@ } }, "extra-standard-library": { - "description": "A list of modules to consider standard-library, in addition to those known to Ruff in advance.", + "description": "A list of modules to consider standard-library, in addition to those known to Ruff in advance.\n\nSupports glob patterns. For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).", "type": [ "array", "null" @@ -1221,7 +1221,7 @@ } }, "known-first-party": { - "description": "A list of modules to consider first-party, regardless of whether they can be identified as such via introspection of the local filesystem.", + "description": "A list of modules to consider first-party, regardless of whether they can be identified as such via introspection of the local filesystem.\n\nSupports glob patterns. For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).", "type": [ "array", "null" @@ -1231,7 +1231,7 @@ } }, "known-local-folder": { - "description": "A list of modules to consider being a local folder. Generally, this is reserved for relative imports (`from . import module`).", + "description": "A list of modules to consider being a local folder. Generally, this is reserved for relative imports (`from . import module`).\n\nSupports glob patterns. For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).", "type": [ "array", "null" @@ -1241,7 +1241,7 @@ } }, "known-third-party": { - "description": "A list of modules to consider third-party, regardless of whether they can be identified as such via introspection of the local filesystem.", + "description": "A list of modules to consider third-party, regardless of whether they can be identified as such via introspection of the local filesystem.\n\nSupports glob patterns. For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).", "type": [ "array", "null" From b11492e94028554961bfa00e55e826d0ab77da82 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 6 Jul 2023 22:49:19 -0400 Subject: [PATCH 357/447] Fix remaining Copyright rule references (#5577) --- crates/ruff/src/codes.rs | 4 ++-- crates/ruff/src/registry.rs | 4 ++-- crates/ruff/src/rules/flake8_copyright/mod.rs | 2 +- crates/ruff/src/settings/options.rs | 2 +- crates/ruff_macros/src/rule_namespace.rs | 8 ++++---- ruff.schema.json | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 280007374b..bb6081db96 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -389,8 +389,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Simplify, "401") => (RuleGroup::Unspecified, rules::flake8_simplify::rules::IfElseBlockInsteadOfDictGet), (Flake8Simplify, "910") => (RuleGroup::Unspecified, rules::flake8_simplify::rules::DictGetWithNoneDefault), - // copyright - (Copyright, "001") => (RuleGroup::Nursery, rules::flake8_copyright::rules::MissingCopyrightNotice), + // flake8-copyright + (Flake8Copyright, "001") => (RuleGroup::Nursery, rules::flake8_copyright::rules::MissingCopyrightNotice), // pyupgrade (Pyupgrade, "001") => (RuleGroup::Unspecified, rules::pyupgrade::rules::UselessMetaclassType), diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 29d1da806f..4ff731653b 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -82,9 +82,9 @@ pub enum Linter { /// [flake8-commas](https://pypi.org/project/flake8-commas/) #[prefix = "COM"] Flake8Commas, - /// Copyright-related rules + /// [flake8-copyright](https://pypi.org/project/flake8-copyright/) #[prefix = "CPY"] - Copyright, + Flake8Copyright, /// [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/) #[prefix = "C4"] Flake8Comprehensions, diff --git a/crates/ruff/src/rules/flake8_copyright/mod.rs b/crates/ruff/src/rules/flake8_copyright/mod.rs index 576d9daedf..6e2a39c4cb 100644 --- a/crates/ruff/src/rules/flake8_copyright/mod.rs +++ b/crates/ruff/src/rules/flake8_copyright/mod.rs @@ -1,4 +1,4 @@ -//! Rules related to copyright notices. +//! Rules from [flake8-copyright](https://pypi.org/project/flake8-copyright/). pub(crate) mod rules; pub mod settings; diff --git a/crates/ruff/src/settings/options.rs b/crates/ruff/src/settings/options.rs index f003b6bb5a..697b630491 100644 --- a/crates/ruff/src/settings/options.rs +++ b/crates/ruff/src/settings/options.rs @@ -498,7 +498,7 @@ pub struct Options { /// Options for the `flake8-comprehensions` plugin. pub flake8_comprehensions: Option, #[option_group] - /// Options for the `copyright` plugin. + /// Options for the `flake8-copyright` plugin. pub flake8_copyright: Option, #[option_group] /// Options for the `flake8-errmsg` plugin. diff --git a/crates/ruff_macros/src/rule_namespace.rs b/crates/ruff_macros/src/rule_namespace.rs index 811033f8ea..79ae54c19d 100644 --- a/crates/ruff_macros/src/rule_namespace.rs +++ b/crates/ruff_macros/src/rule_namespace.rs @@ -21,9 +21,9 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result "Ruff-specific rules", Self::Numpy => "NumPy-specific rules", Self::Copyright => "Copyright-related rules", ); - let mut url_match_arms = - quote!(Self::Ruff => None, Self::Numpy => None, Self::Copyright => None, ); + let mut name_match_arms = + quote!(Self::Ruff => "Ruff-specific rules", Self::Numpy => "NumPy-specific rules", ); + let mut url_match_arms = quote!(Self::Ruff => None, Self::Numpy => None, ); let mut all_prefixes = HashSet::new(); @@ -69,7 +69,7 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result #name,}); url_match_arms.extend(quote! {Self::#variant_ident => Some(#url),}); diff --git a/ruff.schema.json b/ruff.schema.json index 1a688b4386..e906e89423 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -199,7 +199,7 @@ ] }, "flake8-copyright": { - "description": "Options for the `copyright` plugin.", + "description": "Options for the `flake8-copyright` plugin.", "anyOf": [ { "$ref": "#/definitions/Flake8CopyrightOptions" From bf4b96c5ded81c90ecfba77b94f8eac81f28e010 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 7 Jul 2023 00:21:44 -0400 Subject: [PATCH 358/447] Differentiate between runtime and typing-time annotations (#5575) ## Summary In Python, the annotations on `x` and `y` here have very different treatment: ```python def foo(x: int): y: int ``` The `int` in `x: int` is a runtime-required annotation, because `x` gets added to the function's `__annotations__`. You'll notice, for example, that this fails: ```python from typing import TYPE_CHECKING if TYPE_CHECKING: from foo import Bar def f(x: Bar): ... ``` Because `Bar` is required to be available at runtime, not just at typing time. Meanwhile, this succeeds: ```python from typing import TYPE_CHECKING if TYPE_CHECKING: from foo import Bar def f(): x: Bar = 1 f() ``` (Both cases are fine if you use `from __future__ import annotations`.) Historically, we've tracked those annotations that are _not_ runtime-required via the semantic model's `ANNOTATION` flag. But annotations that _are_ runtime-required have been treated as "type definitions" that aren't annotations. This causes problems for the flake8-future-annotations rules, which try to detect whether adding `from __future__ import annotations` would _allow_ you to rewrite a type annotation. We need to know whether we're in _any_ type annotation, runtime-required or not, since adding `from __future__ import annotations` will convert any runtime-required annotation to a typing-only annotation. This PR adds separate state to track these runtime-required annotations. The changes in the test fixtures are correct -- these were false negatives before. Closes https://github.com/astral-sh/ruff/issues/5574. --- crates/ruff/src/checkers/ast/mod.rs | 20 +++-- ...ture_annotations__tests__edge_case.py.snap | 8 ++ ...02_no_future_import_uses_lowercase.py.snap | 7 ++ ..._fa102_no_future_import_uses_union.py.snap | 14 ++++ ..._no_future_import_uses_union_inner.py.snap | 16 ++++ crates/ruff_python_semantic/src/model.rs | 75 +++++++++++++------ 6 files changed, 113 insertions(+), 27 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 7767129c5f..606d8a2ef8 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1811,7 +1811,7 @@ where { if let Some(expr) = &arg_with_default.def.annotation { if runtime_annotation { - self.visit_type_definition(expr); + self.visit_runtime_annotation(expr); } else { self.visit_annotation(expr); }; @@ -1823,7 +1823,7 @@ where if let Some(arg) = &args.vararg { if let Some(expr) = &arg.annotation { if runtime_annotation { - self.visit_type_definition(expr); + self.visit_runtime_annotation(expr); } else { self.visit_annotation(expr); }; @@ -1832,7 +1832,7 @@ where if let Some(arg) = &args.kwarg { if let Some(expr) = &arg.annotation { if runtime_annotation { - self.visit_type_definition(expr); + self.visit_runtime_annotation(expr); } else { self.visit_annotation(expr); }; @@ -1840,7 +1840,7 @@ where } for expr in returns { if runtime_annotation { - self.visit_type_definition(expr); + self.visit_runtime_annotation(expr); } else { self.visit_annotation(expr); }; @@ -1992,7 +1992,7 @@ where }; if runtime_annotation { - self.visit_type_definition(annotation); + self.visit_runtime_annotation(annotation); } else { self.visit_annotation(annotation); } @@ -2089,7 +2089,7 @@ where fn visit_annotation(&mut self, expr: &'b Expr) { let flags_snapshot = self.semantic.flags; - self.semantic.flags |= SemanticModelFlags::ANNOTATION; + self.semantic.flags |= SemanticModelFlags::TYPING_ONLY_ANNOTATION; self.visit_type_definition(expr); self.semantic.flags = flags_snapshot; } @@ -4125,6 +4125,14 @@ impl<'a> Checker<'a> { self.semantic.flags = snapshot; } + /// Visit an [`Expr`], and treat it as a runtime-required type annotation. + fn visit_runtime_annotation(&mut self, expr: &'a Expr) { + let snapshot = self.semantic.flags; + self.semantic.flags |= SemanticModelFlags::RUNTIME_ANNOTATION; + self.visit_type_definition(expr); + self.semantic.flags = snapshot; + } + /// Visit an [`Expr`], and treat it as a type definition. fn visit_type_definition(&mut self, expr: &'a Expr) { let snapshot = self.semantic.flags; diff --git a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__edge_case.py.snap b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__edge_case.py.snap index fc0cdbf8ad..ed1dc0f3df 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__edge_case.py.snap +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__edge_case.py.snap @@ -1,6 +1,14 @@ --- source: crates/ruff/src/rules/flake8_future_annotations/mod.rs --- +edge_case.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List` + | +5 | def main(_: List[int]) -> None: + | ^^^^ FA100 +6 | a_list: t.List[str] = [] +7 | a_list.append("hello") + | + edge_case.py:6:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List` | 5 | def main(_: List[int]) -> None: diff --git a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_lowercase.py.snap b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_lowercase.py.snap index 5fb0fe7444..14acba04f7 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_lowercase.py.snap +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_lowercase.py.snap @@ -9,4 +9,11 @@ no_future_import_uses_lowercase.py:2:13: FA102 Missing `from __future__ import a 3 | a_list.append("hello") | +no_future_import_uses_lowercase.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection + | +6 | def hello(y: dict[str, int]) -> None: + | ^^^^^^^^^^^^^^ FA102 +7 | del y + | + diff --git a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_union.py.snap b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_union.py.snap index cee83994c0..6bf73cdcf1 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_union.py.snap +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_union.py.snap @@ -17,4 +17,18 @@ no_future_import_uses_union.py:2:13: FA102 Missing `from __future__ import annot 3 | a_list.append("hello") | +no_future_import_uses_union.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union + | +6 | def hello(y: dict[str, int] | None) -> None: + | ^^^^^^^^^^^^^^^^^^^^^ FA102 +7 | del y + | + +no_future_import_uses_union.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection + | +6 | def hello(y: dict[str, int] | None) -> None: + | ^^^^^^^^^^^^^^ FA102 +7 | del y + | + diff --git a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_union_inner.py.snap b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_union_inner.py.snap index d153861855..7f5156463a 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_union_inner.py.snap +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_union_inner.py.snap @@ -17,6 +17,22 @@ no_future_import_uses_union_inner.py:2:18: FA102 Missing `from __future__ import 3 | a_list.append("hello") | +no_future_import_uses_union_inner.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection + | +6 | def hello(y: dict[str | None, int]) -> None: + | ^^^^^^^^^^^^^^^^^^^^^ FA102 +7 | z: tuple[str, str | None, str] = tuple(y) +8 | del z + | + +no_future_import_uses_union_inner.py:6:19: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union + | +6 | def hello(y: dict[str | None, int]) -> None: + | ^^^^^^^^^^ FA102 +7 | z: tuple[str, str | None, str] = tuple(y) +8 | del z + | + no_future_import_uses_union_inner.py:7:8: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection | 6 | def hello(y: dict[str | None, int]) -> None: diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 6773c973f2..38bd3c4783 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -929,7 +929,7 @@ impl<'a> SemanticModel<'a> { /// Return the [`ExecutionContext`] of the current scope. pub const fn execution_context(&self) -> ExecutionContext { if self.in_type_checking_block() - || self.in_annotation() + || self.in_typing_only_annotation() || self.in_complex_string_type_definition() || self.in_simple_string_type_definition() { @@ -974,7 +974,18 @@ impl<'a> SemanticModel<'a> { /// Return `true` if the context is in a type annotation. pub const fn in_annotation(&self) -> bool { - self.flags.contains(SemanticModelFlags::ANNOTATION) + self.in_typing_only_annotation() || self.in_runtime_annotation() + } + + /// Return `true` if the context is in a typing-only type annotation. + pub const fn in_typing_only_annotation(&self) -> bool { + self.flags + .contains(SemanticModelFlags::TYPING_ONLY_ANNOTATION) + } + + /// Return `true` if the context is in a runtime-required type annotation. + pub const fn in_runtime_annotation(&self) -> bool { + self.flags.contains(SemanticModelFlags::RUNTIME_ANNOTATION) } /// Return `true` if the context is in a type definition. @@ -1025,7 +1036,7 @@ impl<'a> SemanticModel<'a> { pub const fn in_forward_reference(&self) -> bool { self.in_simple_string_type_definition() || self.in_complex_string_type_definition() - || (self.in_future_type_definition() && self.in_annotation()) + || (self.in_future_type_definition() && self.in_typing_only_annotation()) } /// Return `true` if the context is in an exception handler. @@ -1147,13 +1158,36 @@ bitflags! { /// Flags indicating the current context of the analysis. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] pub struct SemanticModelFlags: u16 { - /// The context is in a type annotation. + /// The context is in a typing-time-only type annotation. /// /// For example, the context could be visiting `int` in: /// ```python - /// x: int = 1 + /// def foo() -> int: + /// x: int = 1 /// ``` - const ANNOTATION = 1 << 0; + /// + /// In this case, Python doesn't require that the type annotation be evaluated at runtime. + /// + /// If `from __future__ import annotations` is used, all annotations are evaluated at + /// typing time. Otherwise, all function argument annotations are evaluated at runtime, as + /// are any annotated assignments in module or class scopes. + const TYPING_ONLY_ANNOTATION = 1 << 0; + + /// The context is in a runtime type annotation. + /// + /// For example, the context could be visiting `int` in: + /// ```python + /// def foo(x: int) -> int: + /// ... + /// ``` + /// + /// In this case, Python requires that the type annotation be evaluated at runtime, + /// as it needs to be available on the function's `__annotations__` attribute. + /// + /// If `from __future__ import annotations` is used, all annotations are evaluated at + /// typing time. Otherwise, all function argument annotations are evaluated at runtime, as + /// are any annotated assignments in module or class scopes. + const RUNTIME_ANNOTATION = 1 << 1; /// The context is in a type definition. /// @@ -1167,7 +1201,7 @@ bitflags! { /// All type annotations are also type definitions, but the converse is not true. /// In our example, `int` is a type definition but not a type annotation, as it /// doesn't appear in a type annotation context, but rather in a type definition. - const TYPE_DEFINITION = 1 << 1; + const TYPE_DEFINITION = 1 << 2; /// The context is in a (deferred) "simple" string type definition. /// @@ -1178,7 +1212,7 @@ bitflags! { /// /// "Simple" string type definitions are those that consist of a single string literal, /// as opposed to an implicitly concatenated string literal. - const SIMPLE_STRING_TYPE_DEFINITION = 1 << 2; + const SIMPLE_STRING_TYPE_DEFINITION = 1 << 3; /// The context is in a (deferred) "complex" string type definition. /// @@ -1189,7 +1223,7 @@ bitflags! { /// /// "Complex" string type definitions are those that consist of a implicitly concatenated /// string literals. These are uncommon but valid. - const COMPLEX_STRING_TYPE_DEFINITION = 1 << 3; + const COMPLEX_STRING_TYPE_DEFINITION = 1 << 4; /// The context is in a (deferred) `__future__` type definition. /// @@ -1202,7 +1236,7 @@ bitflags! { /// /// `__future__`-style type annotations are only enabled if the `annotations` feature /// is enabled via `from __future__ import annotations`. - const FUTURE_TYPE_DEFINITION = 1 << 4; + const FUTURE_TYPE_DEFINITION = 1 << 5; /// The context is in an exception handler. /// @@ -1213,7 +1247,7 @@ bitflags! { /// except Exception: /// x: int = 1 /// ``` - const EXCEPTION_HANDLER = 1 << 5; + const EXCEPTION_HANDLER = 1 << 6; /// The context is in an f-string. /// @@ -1221,7 +1255,7 @@ bitflags! { /// ```python /// f'{x}' /// ``` - const F_STRING = 1 << 6; + const F_STRING = 1 << 7; /// The context is in a nested f-string. /// @@ -1229,7 +1263,7 @@ bitflags! { /// ```python /// f'{f"{x}"}' /// ``` - const NESTED_F_STRING = 1 << 7; + const NESTED_F_STRING = 1 << 8; /// The context is in a boolean test. /// @@ -1241,7 +1275,7 @@ bitflags! { /// /// The implication is that the actual value returned by the current expression is /// not used, only its truthiness. - const BOOLEAN_TEST = 1 << 8; + const BOOLEAN_TEST = 1 << 9; /// The context is in a `typing::Literal` annotation. /// @@ -1250,7 +1284,7 @@ bitflags! { /// def f(x: Literal["A", "B", "C"]): /// ... /// ``` - const LITERAL = 1 << 9; + const LITERAL = 1 << 10; /// The context is in a subscript expression. /// @@ -1258,7 +1292,7 @@ bitflags! { /// ```python /// x["a"]["b"] /// ``` - const SUBSCRIPT = 1 << 10; + const SUBSCRIPT = 1 << 11; /// The context is in a type-checking block. /// @@ -1270,8 +1304,7 @@ bitflags! { /// if TYPE_CHECKING: /// x: int = 1 /// ``` - const TYPE_CHECKING_BLOCK = 1 << 11; - + const TYPE_CHECKING_BLOCK = 1 << 12; /// The context has traversed past the "top-of-file" import boundary. /// @@ -1284,7 +1317,7 @@ bitflags! { /// /// x: int = 1 /// ``` - const IMPORT_BOUNDARY = 1 << 12; + const IMPORT_BOUNDARY = 1 << 13; /// The context has traversed past the `__future__` import boundary. /// @@ -1299,7 +1332,7 @@ bitflags! { /// /// Python considers it a syntax error to import from `__future__` after /// any other non-`__future__`-importing statements. - const FUTURES_BOUNDARY = 1 << 13; + const FUTURES_BOUNDARY = 1 << 14; /// `__future__`-style type annotations are enabled in this context. /// @@ -1311,7 +1344,7 @@ bitflags! { /// def f(x: int) -> int: /// ... /// ``` - const FUTURE_ANNOTATIONS = 1 << 14; + const FUTURE_ANNOTATIONS = 1 << 15; } } From 40ddc1604cf53d209d55c7fd9ec40a399fc2a53f Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 7 Jul 2023 11:28:25 +0200 Subject: [PATCH 359/447] Introduce `parenthesized` helper (#5565) --- .../src/expression/expr_call.rs | 9 +++-- .../src/expression/expr_dict.rs | 12 ++----- .../src/expression/expr_list.rs | 12 ++----- .../src/expression/expr_set.rs | 21 ++++------- .../src/expression/expr_tuple.rs | 24 +++---------- .../src/expression/parentheses.rs | 36 +++++++++++++++++++ .../src/other/arguments.rs | 10 ++---- 7 files changed, 60 insertions(+), 64 deletions(-) diff --git a/crates/ruff_python_formatter/src/expression/expr_call.rs b/crates/ruff_python_formatter/src/expression/expr_call.rs index 0c9f7f443d..87ed281572 100644 --- a/crates/ruff_python_formatter/src/expression/expr_call.rs +++ b/crates/ruff_python_formatter/src/expression/expr_call.rs @@ -1,10 +1,11 @@ use crate::builders::PyFormatterExtensions; use crate::comments::{dangling_comments, Comments}; use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, + default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, + Parenthesize, }; use crate::{AsFormat, FormatNodeRule, PyFormatter}; -use ruff_formatter::prelude::{format_with, group, soft_block_indent, text}; +use ruff_formatter::prelude::{format_with, group, text}; use ruff_formatter::{write, Buffer, FormatResult}; use rustpython_parser::ast::ExprCall; @@ -56,7 +57,6 @@ impl FormatNodeRule for FormatExprCall { f, [ func.format(), - text("("), // The outer group is for things like // ```python // get_collection( @@ -73,8 +73,7 @@ impl FormatNodeRule for FormatExprCall { // ) // ``` // TODO(konstin): Doesn't work see wrongly formatted test - &group(&soft_block_indent(&group(&all_args))), - text(")") + parenthesized("(", &group(&all_args), ")") ] ) } diff --git a/crates/ruff_python_formatter/src/expression/expr_dict.rs b/crates/ruff_python_formatter/src/expression/expr_dict.rs index a0a4c57a31..6299fc0845 100644 --- a/crates/ruff_python_formatter/src/expression/expr_dict.rs +++ b/crates/ruff_python_formatter/src/expression/expr_dict.rs @@ -1,6 +1,7 @@ use crate::comments::{dangling_node_comments, leading_comments, Comments}; use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, + default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, + Parenthesize, }; use crate::prelude::*; use crate::FormatNodeRule; @@ -86,14 +87,7 @@ impl FormatNodeRule for FormatExprDict { joiner.finish() }); - write!( - f, - [group(&format_args![ - text("{"), - soft_block_indent(&format_pairs), - text("}") - ])] - ) + parenthesized("{", &format_pairs, "}").fmt(f) } fn fmt_dangling_comments(&self, _node: &ExprDict, _f: &mut PyFormatter) -> FormatResult<()> { diff --git a/crates/ruff_python_formatter/src/expression/expr_list.rs b/crates/ruff_python_formatter/src/expression/expr_list.rs index 16896dac16..90d5e244e1 100644 --- a/crates/ruff_python_formatter/src/expression/expr_list.rs +++ b/crates/ruff_python_formatter/src/expression/expr_list.rs @@ -1,6 +1,7 @@ use crate::comments::{dangling_comments, CommentLinePosition, Comments}; use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, + default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, + Parenthesize, }; use crate::prelude::*; use crate::FormatNodeRule; @@ -54,14 +55,7 @@ impl FormatNodeRule for FormatExprList { let items = format_with(|f| f.join_comma_separated().nodes(elts.iter()).finish()); - write!( - f, - [group(&format_args![ - text("["), - soft_block_indent(&items), - text("]") - ])] - ) + parenthesized("[", &items, "]").fmt(f) } fn fmt_dangling_comments(&self, _node: &ExprList, _f: &mut PyFormatter) -> FormatResult<()> { diff --git a/crates/ruff_python_formatter/src/expression/expr_set.rs b/crates/ruff_python_formatter/src/expression/expr_set.rs index 7e379d1cea..c1b1a90091 100644 --- a/crates/ruff_python_formatter/src/expression/expr_set.rs +++ b/crates/ruff_python_formatter/src/expression/expr_set.rs @@ -1,12 +1,11 @@ use crate::comments::Comments; use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, + default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, + Parenthesize, }; -use crate::{FormatNodeRule, FormattedIterExt, PyFormatter}; -use ruff_formatter::prelude::{ - format_with, group, if_group_breaks, soft_block_indent, soft_line_break_or_space, text, -}; -use ruff_formatter::{format_args, write, Buffer, FormatResult}; +use crate::prelude::*; +use crate::FormatNodeRule; +use ruff_formatter::format_args; use rustpython_parser::ast::ExprSet; #[derive(Default)] @@ -23,14 +22,8 @@ impl FormatNodeRule for FormatExprSet { .entries(elts.iter().formatted()) .finish() }); - write!( - f, - [group(&format_args![ - text("{"), - soft_block_indent(&format_args![joined, if_group_breaks(&text(",")),]), - text("}") - ])] - ) + + parenthesized("{", &format_args![joined, if_group_breaks(&text(","))], "}").fmt(f) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_tuple.rs b/crates/ruff_python_formatter/src/expression/expr_tuple.rs index b7783b549d..de97a5bac9 100644 --- a/crates/ruff_python_formatter/src/expression/expr_tuple.rs +++ b/crates/ruff_python_formatter/src/expression/expr_tuple.rs @@ -1,7 +1,8 @@ use crate::builders::optional_parentheses; use crate::comments::{dangling_node_comments, Comments}; use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, + default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, + Parenthesize, }; use crate::prelude::*; use ruff_formatter::{format_args, write, FormatRuleWithOptions}; @@ -69,15 +70,8 @@ impl FormatNodeRule for FormatExprTuple { ) } [single] => { - write!( - f, - [group(&format_args![ - // A single element tuple always needs parentheses and a trailing comma - &text("("), - soft_block_indent(&format_args![single.format(), &text(",")]), - &text(")"), - ])] - ) + // A single element tuple always needs parentheses and a trailing comma + parenthesized("(", &format_args![single.format(), &text(",")], ")").fmt(f) } // If the tuple has parentheses, we generally want to keep them. The exception are for // loops, see `TupleParentheses::StripInsideForLoop` doc comment. @@ -87,15 +81,7 @@ impl FormatNodeRule for FormatExprTuple { elts if is_parenthesized(*range, elts, f) && self.parentheses != TupleParentheses::StripInsideForLoop => { - write!( - f, - [group(&format_args![ - // If there were previously parentheses, keep them - &text("("), - soft_block_indent(&ExprSequence::new(elts)), - &text(")"), - ])] - ) + parenthesized("(", &ExprSequence::new(elts), ")").fmt(f) } elts => optional_parentheses(&ExprSequence::new(elts)).fmt(f), } diff --git a/crates/ruff_python_formatter/src/expression/parentheses.rs b/crates/ruff_python_formatter/src/expression/parentheses.rs index 3df553f126..c73b5773ce 100644 --- a/crates/ruff_python_formatter/src/expression/parentheses.rs +++ b/crates/ruff_python_formatter/src/expression/parentheses.rs @@ -1,5 +1,7 @@ use crate::comments::Comments; +use crate::prelude::*; use crate::trivia::{first_non_trivia_token, first_non_trivia_token_rev, Token, TokenKind}; +use ruff_formatter::{format_args, write, Argument, Arguments}; use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::Ranged; @@ -119,3 +121,37 @@ pub(crate) fn is_expression_parenthesized(expr: AnyNodeRef, contents: &str) -> b }) ) } + +pub(crate) fn parenthesized<'content, 'ast, Content>( + left: &'static str, + content: &'content Content, + right: &'static str, +) -> FormatParenthesized<'content, 'ast> +where + Content: Format>, +{ + FormatParenthesized { + left, + content: Argument::new(content), + right, + } +} + +pub(crate) struct FormatParenthesized<'content, 'ast> { + left: &'static str, + content: Argument<'content, PyFormatContext<'ast>>, + right: &'static str, +} + +impl<'ast> Format> for FormatParenthesized<'_, 'ast> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + write!( + f, + [group(&format_args![ + text(self.left), + &soft_block_indent(&Arguments::from(&self.content)), + text(self.right) + ])] + ) + } +} diff --git a/crates/ruff_python_formatter/src/other/arguments.rs b/crates/ruff_python_formatter/src/other/arguments.rs index 0864249b85..e6d91f7b69 100644 --- a/crates/ruff_python_formatter/src/other/arguments.rs +++ b/crates/ruff_python_formatter/src/other/arguments.rs @@ -10,6 +10,7 @@ use crate::comments::{ CommentLinePosition, SourceComment, }; use crate::context::NodeLevel; +use crate::expression::parentheses::parenthesized; use crate::prelude::*; use crate::trivia::{first_non_trivia_token, SimpleTokenizer, Token, TokenKind}; use crate::FormatNodeRule; @@ -179,14 +180,7 @@ impl FormatNodeRule for FormatArguments { ] )?; } else { - write!( - f, - [group(&format_args!( - text("("), - soft_block_indent(&group(&format_inner)), - text(")") - ))] - )?; + parenthesized("(", &group(&format_inner), ")").fmt(f)?; } f.context_mut().set_node_level(saved_level); From b22e6c3d389441a4d9bf4ef1b330e271fd907c71 Mon Sep 17 00:00:00 2001 From: konsti Date: Fri, 7 Jul 2023 13:30:12 +0200 Subject: [PATCH 360/447] Extend ruff_dev formatter script to compute statistics and format a project (#5492) ## Summary This extends the `ruff_dev` formatter script util. Instead of only doing stability checks, you can now choose different compatible options on the CLI and get statistics. * It adds an option the formats all files that ruff would check to allow looking at an entire black-formatted repository with `git diff` * It computes the [Jaccard index](https://en.wikipedia.org/wiki/Jaccard_index) as a measure of deviation between input and output, which is useful as single number metric for assessing our current deviations from black. * It adds progress bars to both the single projects as well as the multi-project mode. * It adds an option to write the multi-project output to a file Sample usage: ``` $ cargo run --bin ruff_dev -- format-dev --stability-check crates/ruff/resources/test/cpython $ cargo run --bin ruff_dev -- format-dev --stability-check /home/konsti/projects/django Syntax error in /home/konsti/projects/django/tests/test_runner_apps/tagged/tests_syntax_error.py: source contains syntax errors (parser error): BaseError { error: UnrecognizedToken(Name { name: "syntax_error" }, None), offset: 131, source_path: "" } Found 0 stability errors in 2755 files (jaccard index 0.911) in 9.75s $ cargo run --bin ruff_dev -- format-dev --write /home/konsti/projects/django ``` Options: ``` Several utils related to the formatter which can be run on one or more repositories. The selected set of files in a repository is the same as for `ruff check`. * Check formatter stability: Format a repository twice and ensure that it looks that the first and second formatting look the same. * Format: Format the files in a repository to be able to check them with `git diff` * Statistics: The subcommand the Jaccard index between the (assumed to be black formatted) input and the ruff formatted output Usage: ruff_dev format-dev [OPTIONS] [FILES]... Arguments: [FILES]... Like `ruff check`'s files. See `--multi-project` if you want to format an ecosystem checkout Options: --stability-check Check stability We want to ensure that once formatted content stays the same when formatted again, which is known as formatter stability or formatter idempotency, and that the formatter prints syntactically valid code. As our test cases cover only a limited amount of code, this allows checking entire repositories. --write Format the files. Without this flag, the python files are not modified --format Control the verbosity of the output [default: default] Possible values: - minimal: Filenames only - default: Filenames and reduced diff - full: Full diff and invalid code -x, --exit-first-error Print only the first error and exit, `-x` is same as pytest --multi-project Checks each project inside a directory, useful e.g. if you want to check all of the ecosystem checkouts --error-file Write all errors to this file in addition to stdout. Only used in multi-project mode ``` ## Test Plan I ran this on django (2755 files, jaccard index 0.911) and discovered a magic trailing comma problem and that we really needed to implement import formatting. I ran the script on cpython to identify https://github.com/astral-sh/ruff/pull/5558. --- Cargo.lock | 39 +- Cargo.toml | 1 + crates/ruff/Cargo.toml | 2 +- crates/ruff_cli/src/args.rs | 3 +- crates/ruff_dev/Cargo.toml | 4 + .../ruff_dev/src/check_formatter_stability.rs | 472 ------------- crates/ruff_dev/src/format_dev.rs | 628 ++++++++++++++++++ crates/ruff_dev/src/main.rs | 18 +- crates/ruff_python_formatter/Cargo.toml | 1 + crates/ruff_python_formatter/src/lib.rs | 57 +- 10 files changed, 726 insertions(+), 499 deletions(-) delete mode 100644 crates/ruff_dev/src/check_formatter_stability.rs create mode 100644 crates/ruff_dev/src/format_dev.rs diff --git a/Cargo.lock b/Cargo.lock index db8cdef966..2376d4aab3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -404,6 +404,7 @@ dependencies = [ "encode_unicode", "lazy_static", "libc", + "unicode-width", "windows-sys 0.45.0", ] @@ -949,6 +950,19 @@ dependencies = [ "hashbrown 0.14.0", ] +[[package]] +name = "indicatif" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ff8cc23a7393a397ed1d7f56e6365cba772aba9f9912ab968b03043c395d057" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", + "unicode-width", +] + [[package]] name = "inotify" version = "0.9.6" @@ -1333,6 +1347,12 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "once_cell" version = "1.18.0" @@ -1557,6 +1577,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "portable-atomic" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "767eb9f07d4a5ebcb39bbf2d452058a93c011373abf6832e24194a1c3f004794" + [[package]] name = "predicates" version = "3.0.3" @@ -1959,6 +1985,8 @@ version = "0.0.0" dependencies = [ "anyhow", "clap", + "ignore", + "indicatif", "itertools", "libcst", "log", @@ -1969,6 +1997,7 @@ dependencies = [ "ruff", "ruff_cli", "ruff_diagnostics", + "ruff_formatter", "ruff_python_formatter", "ruff_python_stdlib", "ruff_textwrap", @@ -1979,6 +2008,7 @@ dependencies = [ "similar", "strum", "strum_macros", + "tempfile", ] [[package]] @@ -2071,6 +2101,7 @@ dependencies = [ "serde_json", "similar", "smallvec", + "thiserror", ] [[package]] @@ -2629,18 +2660,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.41" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c16a64ba9387ef3fdae4f9c1a7f07a0997fce91985c0336f1ddc1822b3b37802" +checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.41" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d14928354b01c4d6a4f0e549069adef399a284e7995c7ccca94e8a07a5346c59" +checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index d48fedbf29..1ca004af7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ strum = { version = "0.24.1", features = ["strum_macros"] } strum_macros = { version = "0.24.3" } syn = { version = "2.0.15" } test-case = { version = "3.0.0" } +thiserror = { version = "1.0.43" } toml = { version = "0.7.2" } # v0.0.1 diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 7503cdddd5..ad1372686c 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -74,7 +74,7 @@ shellexpand = { workspace = true } smallvec = { workspace = true } strum = { workspace = true } strum_macros = { workspace = true } -thiserror = { version = "1.0.38" } +thiserror = { version = "1.0.43" } toml = { workspace = true } typed-arena = { version = "2.0.2" } unicode-width = { version = "0.1.10" } diff --git a/crates/ruff_cli/src/args.rs b/crates/ruff_cli/src/args.rs index 1050aafcbf..aa299cad60 100644 --- a/crates/ruff_cli/src/args.rs +++ b/crates/ruff_cli/src/args.rs @@ -74,7 +74,8 @@ pub enum Command { }, } -#[derive(Clone, Debug, clap::Args)] +// The `Parser` derive is for ruff_dev, for ruff_cli `Args` would be sufficient +#[derive(Clone, Debug, clap::Parser)] #[allow(clippy::struct_excessive_bools, clippy::module_name_repetitions)] pub struct CheckArgs { /// List of files or directories to check. diff --git a/crates/ruff_dev/Cargo.toml b/crates/ruff_dev/Cargo.toml index 07be9e5d72..36c9c42d59 100644 --- a/crates/ruff_dev/Cargo.toml +++ b/crates/ruff_dev/Cargo.toml @@ -14,12 +14,15 @@ license = { workspace = true } ruff = { path = "../ruff", features = ["schemars"] } ruff_cli = { path = "../ruff_cli" } ruff_diagnostics = { path = "../ruff_diagnostics" } +ruff_formatter = { path = "../ruff_formatter" } ruff_python_formatter = { path = "../ruff_python_formatter" } ruff_python_stdlib = { path = "../ruff_python_stdlib" } ruff_textwrap = { path = "../ruff_textwrap" } anyhow = { workspace = true } clap = { workspace = true } +ignore = { workspace = true } +indicatif = "0.17.5" itertools = { workspace = true } libcst = { workspace = true } log = { workspace = true } @@ -34,3 +37,4 @@ serde_json = { workspace = true } similar = { workspace = true } strum = { workspace = true } strum_macros = { workspace = true } +tempfile = "3.6.0" diff --git a/crates/ruff_dev/src/check_formatter_stability.rs b/crates/ruff_dev/src/check_formatter_stability.rs deleted file mode 100644 index b955a7245a..0000000000 --- a/crates/ruff_dev/src/check_formatter_stability.rs +++ /dev/null @@ -1,472 +0,0 @@ -//! We want to ensure that once formatted content stays the same when formatted again, which is -//! known as formatter stability or formatter idempotency, and that the formatter prints -//! syntactically valid code. As our test cases cover only a limited amount of code, this allows -//! checking entire repositories. - -use std::fmt::{Display, Formatter}; -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; -use std::sync::mpsc::channel; -use std::time::{Duration, Instant}; -use std::{fmt, fs, iter}; - -use anyhow::{bail, Context}; -use clap::Parser; -use log::debug; -use similar::{ChangeTag, TextDiff}; - -use ruff::resolver::python_files_in_path; -use ruff::settings::types::{FilePattern, FilePatternSet}; -use ruff_cli::args::CheckArgs; -use ruff_cli::resolve::resolve; -use ruff_python_formatter::{format_module, PyFormatOptions}; - -/// Control the verbosity of the output -#[derive(Copy, Clone, PartialEq, Eq, clap::ValueEnum, Default)] -pub(crate) enum Format { - /// Filenames only - Minimal, - /// Filenames and reduced diff - #[default] - Default, - /// Full diff and invalid code - Full, -} - -#[derive(clap::Args)] -pub(crate) struct Args { - /// Like `ruff check`'s files - pub(crate) files: Vec, - /// Control the verbosity of the output - #[arg(long, default_value_t, value_enum)] - pub(crate) format: Format, - /// Print only the first error and exit, `-x` is same as pytest - #[arg(long, short = 'x')] - pub(crate) exit_first_error: bool, - /// 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 -/// we want the same behaviour as `ruff_cli` and clap seems to lack a way to parse directly to -/// `Args` instead of a `Parser` -#[derive(Debug, clap::Parser)] -struct WrapperArgs { - #[clap(flatten)] - check_args: CheckArgs, -} - -pub(crate) fn main(args: &Args) -> anyhow::Result { - let all_success = if args.multi_project { - check_multi_project(args) - } else { - let result = check_repo(args)?; - - #[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() - }; - if all_success { - Ok(ExitCode::SUCCESS) - } else { - Ok(ExitCode::FAILURE) - } -} - -enum Message { - Start { - path: PathBuf, - }, - Failed { - path: PathBuf, - error: anyhow::Error, - }, - Finished { - path: PathBuf, - result: CheckRepoResult, - }, -} - -fn check_multi_project(args: &Args) -> bool { - let mut all_success = true; - let mut total_errors = 0; - let mut total_files = 0; - let start = Instant::now(); - - rayon::scope(|scope| { - let (sender, receiver) = channel(); - - for base_dir in &args.files { - for dir in base_dir.read_dir().unwrap() { - let path = dir.unwrap().path().clone(); - - let sender = sender.clone(); - - scope.spawn(move |_| { - sender.send(Message::Start { path: path.clone() }).unwrap(); - - match check_repo(&Args { - files: vec![path.clone()], - error_file: args.error_file.clone(), - ..*args - }) { - Ok(result) => sender.send(Message::Finished { result, path }), - Err(error) => sender.send(Message::Failed { error, path }), - } - .unwrap(); - }); - } - } - - 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 { - Message::Start { path } => { - writeln!(stdout, "Starting {}", path.display()).unwrap(); - } - Message::Finished { path, result } => { - total_errors += result.diagnostics.len(); - total_files += result.file_count; - - writeln!( - stdout, - "Finished {} with {} files in {:.2}s", - path.display(), - 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 } => { - writeln!(stdout, "Failed {}: {}", path.display(), error).unwrap(); - all_success = false; - } - } - } - }); - }); - - let duration = start.elapsed(); - - #[allow(clippy::print_stdout)] - { - println!( - "{total_errors} stability errors in {total_files} files in {}s", - duration.as_secs_f32() - ); - } - - all_success -} - -/// Returns whether the check was successful -fn check_repo(args: &Args) -> anyhow::Result { - let start = Instant::now(); - - // Find files to check (or in this case, format twice). Adapted from ruff_cli - // First argument is ignored - let dummy = PathBuf::from("check"); - let check_args_input = iter::once(&dummy).chain(&args.files); - let check_args: CheckArgs = WrapperArgs::try_parse_from(check_args_input)?.check_args; - let (cli, overrides) = check_args.partition(); - let mut pyproject_config = resolve( - cli.isolated, - cli.config.as_deref(), - &overrides, - cli.stdin_filename.as_deref(), - )?; - // We don't want to format pyproject.toml - pyproject_config.settings.lib.include = FilePatternSet::try_from_vec(vec![ - FilePattern::Builtin("*.py"), - FilePattern::Builtin("*.pyi"), - ]) - .unwrap(); - let (paths, _resolver) = python_files_in_path(&cli.files, &pyproject_config, &overrides)?; - - if paths.is_empty() { - bail!("no python files in {:?}", cli.files) - } - - let mut formatted_counter = 0; - let errors: Vec<_> = paths - .into_iter() - .map(|dir_entry| { - // Doesn't make sense to recover here in this test script - dir_entry.expect("Iterating the files in the repository failed") - }) - .filter(|dir_entry| { - // For some reason it does not filter in the beginning - dir_entry.file_name() != "pyproject.toml" - }) - .map(|dir_entry| { - let file = dir_entry.path().to_path_buf(); - formatted_counter += 1; - // Handle panics (mostly in `debug_assert!`) - let result = match catch_unwind(|| check_file(&file)) { - Ok(result) => result, - Err(panic) => { - if let Some(message) = panic.downcast_ref::() { - Err(FormatterStabilityError::Panic { - message: message.clone(), - }) - } else if let Some(&message) = panic.downcast_ref::<&str>() { - Err(FormatterStabilityError::Panic { - message: message.to_string(), - }) - } else { - Err(FormatterStabilityError::Panic { - // This should not happen, but it can - message: "(Panic didn't set a string message)".to_string(), - }) - } - } - }; - (result, file) - }) - // We only care about the errors - .filter_map(|(result, file)| match result { - Err(error) => Some(Diagnostic { file, error }), - Ok(()) => None, - }) - .collect(); - - let duration = start.elapsed(); - - Ok(CheckRepoResult { - duration, - file_count: formatted_counter, - diagnostics: errors, - }) -} - -/// A compact diff that only shows a header and changes, but nothing unchanged. This makes viewing -/// multiple errors easier. -fn diff_show_only_changes( - writer: &mut Formatter, - formatted: &str, - reformatted: &str, -) -> fmt::Result { - for changes in TextDiff::from_lines(formatted, reformatted) - .unified_diff() - .iter_hunks() - { - for (idx, change) in changes - .iter_changes() - .filter(|change| change.tag() != ChangeTag::Equal) - .enumerate() - { - if idx == 0 { - writeln!(writer, "{}", changes.header())?; - } - write!(writer, "{}", change.tag())?; - writer.write_str(change.value())?; - } - } - Ok(()) -} - -struct CheckRepoResult { - duration: Duration, - file_count: usize, - diagnostics: Vec, -} - -impl CheckRepoResult { - fn display(&self, format: Format) -> DisplayCheckRepoResult { - DisplayCheckRepoResult { - result: self, - format, - } - } - - fn is_success(&self) -> bool { - self.diagnostics.is_empty() - } -} - -struct DisplayCheckRepoResult<'a> { - result: &'a CheckRepoResult, - format: Format, -} - -impl Display for DisplayCheckRepoResult<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - for diagnostic in &self.result.diagnostics { - write!(f, "{}", diagnostic.display(self.format))?; - } - Ok(()) - } -} - -#[derive(Debug)] -struct Diagnostic { - file: PathBuf, - error: FormatterStabilityError, -} - -impl Diagnostic { - fn display(&self, format: Format) -> DisplayDiagnostic { - DisplayDiagnostic { - diagnostic: self, - format, - } - } -} - -struct DisplayDiagnostic<'a> { - format: Format, - diagnostic: &'a Diagnostic, -} - -impl Display for DisplayDiagnostic<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let Diagnostic { file, error } = &self.diagnostic; - - match error { - FormatterStabilityError::Unstable { - formatted, - reformatted, - } => { - writeln!(f, "Unstable formatting {}", file.display())?; - match self.format { - Format::Minimal => {} - Format::Default => { - diff_show_only_changes(f, formatted, reformatted)?; - } - Format::Full => { - let diff = TextDiff::from_lines(formatted.as_str(), reformatted.as_str()) - .unified_diff() - .header("Formatted once", "Formatted twice") - .to_string(); - writeln!( - f, - r#"Reformatting the formatted code a second time resulted in formatting changes. ---- -{diff}--- - -Formatted once: ---- -{formatted}--- - -Formatted twice: ---- -{reformatted}---\n"#, - )?; - } - } - } - FormatterStabilityError::InvalidSyntax { err, formatted } => { - writeln!( - f, - "Formatter generated invalid syntax {}: {}", - file.display(), - err - )?; - if self.format == Format::Full { - writeln!(f, "---\n{formatted}\n---\n")?; - } - } - FormatterStabilityError::Panic { message } => { - writeln!(f, "Panic {}: {}", file.display(), message)?; - } - FormatterStabilityError::Other(err) => { - writeln!(f, "Uncategorized error {}: {}", file.display(), err)?; - } - } - Ok(()) - } -} - -#[derive(Debug)] -enum FormatterStabilityError { - /// First and second pass of the formatter are different - Unstable { - formatted: String, - reformatted: String, - }, - /// The formatter printed invalid code - InvalidSyntax { - err: anyhow::Error, - formatted: String, - }, - /// From `catch_unwind` - Panic { - message: String, - }, - Other(anyhow::Error), -} - -impl From for FormatterStabilityError { - fn from(error: anyhow::Error) -> Self { - Self::Other(error) - } -} - -/// Run the formatter twice on the given file. Does not write back to the file -fn check_file(input_path: &Path) -> Result<(), FormatterStabilityError> { - let content = fs::read_to_string(input_path).context("Failed to read file")?; - let printed = match format_module(&content, PyFormatOptions::default()) { - Ok(printed) => printed, - Err(err) => { - return if err - .to_string() - .starts_with("Source contains syntax errors ") - { - debug!( - "Skipping {} with invalid first pass {}", - input_path.display(), - err - ); - Ok(()) - } else { - Err(err.into()) - }; - } - }; - let formatted = printed.as_code(); - - let reformatted = match format_module(formatted, PyFormatOptions::default()) { - Ok(reformatted) => reformatted, - Err(err) => { - return Err(FormatterStabilityError::InvalidSyntax { - err, - formatted: formatted.to_string(), - }); - } - }; - - if reformatted.as_code() != formatted { - return Err(FormatterStabilityError::Unstable { - formatted: formatted.to_string(), - reformatted: reformatted.into_code(), - }); - } - Ok(()) -} diff --git a/crates/ruff_dev/src/format_dev.rs b/crates/ruff_dev/src/format_dev.rs new file mode 100644 index 0000000000..7695d93e65 --- /dev/null +++ b/crates/ruff_dev/src/format_dev.rs @@ -0,0 +1,628 @@ +use anyhow::{bail, Context}; +use clap::{CommandFactory, FromArgMatches}; +use ignore::DirEntry; +use indicatif::ProgressBar; +use rayon::iter::{IntoParallelIterator, ParallelIterator}; +use ruff::resolver::python_files_in_path; +use ruff::settings::types::{FilePattern, FilePatternSet}; +use ruff_cli::args::CheckArgs; +use ruff_cli::resolve::resolve; +use ruff_formatter::{FormatError, PrintError}; +use ruff_python_formatter::{format_module, FormatModuleError, PyFormatOptions}; +use similar::{ChangeTag, TextDiff}; +use std::fmt::{Display, Formatter}; +use std::fs::File; +use std::io::{BufWriter, Write}; +use std::ops::{Add, AddAssign}; +use std::panic::catch_unwind; +use std::path::{Path, PathBuf}; +use std::process::ExitCode; +use std::sync::mpsc::channel; +use std::time::{Duration, Instant}; +use std::{fmt, fs, io}; +use tempfile::NamedTempFile; + +/// Find files that ruff would check so we can format them. Adapted from `ruff_cli`. +fn ruff_check_paths(dirs: &[PathBuf]) -> anyhow::Result>> { + let args_matches = CheckArgs::command() + .no_binary_name(true) + .get_matches_from(dirs); + let check_args: CheckArgs = CheckArgs::from_arg_matches(&args_matches)?; + let (cli, overrides) = check_args.partition(); + let mut pyproject_config = resolve( + cli.isolated, + cli.config.as_deref(), + &overrides, + cli.stdin_filename.as_deref(), + )?; + // We don't want to format pyproject.toml + pyproject_config.settings.lib.include = FilePatternSet::try_from_vec(vec![ + FilePattern::Builtin("*.py"), + FilePattern::Builtin("*.pyi"), + ]) + .unwrap(); + let (paths, _resolver) = python_files_in_path(&cli.files, &pyproject_config, &overrides)?; + if paths.is_empty() { + bail!("no python files in {:?}", dirs) + } + Ok(paths) +} + +/// Collects statistics over the formatted files, currently only computes the Jaccard index +/// +/// The [Jaccard index](https://en.wikipedia.org/wiki/Jaccard_index) can be defined as +/// ```text +/// J(A, B) = |A∩B| / (|A\B| + |B\A| + |A∩B|) +/// ``` +/// where in our case `A` is the black formatted input, `B` is the ruff formatted output and the +/// intersection are the lines in the diff that didn't change. We don't just track intersection and +/// union so we can make statistics about size changes. If the input is not black formatted, this +/// only becomes a measure for the changes made to the codebase during the initial formatting. +#[derive(Default, Debug, Copy, Clone)] +pub(crate) struct Statistics { + /// The size of `A\B`, the number of lines only in the input, which we assume to be black + /// formatted + black_input: u32, + /// The size of `B\A`, the number of lines only in the formatted output + ruff_output: u32, + /// The number of matching identical lines + intersection: u32, +} + +impl Statistics { + pub(crate) fn from_versions(black: &str, ruff: &str) -> Self { + if black == ruff { + let intersection = u32::try_from(black.lines().count()).unwrap(); + Self { + black_input: 0, + ruff_output: 0, + intersection, + } + } else { + let diff = TextDiff::from_lines(black, ruff); + let mut statistics = Self::default(); + for change in diff.iter_all_changes() { + match change.tag() { + ChangeTag::Delete => statistics.black_input += 1, + ChangeTag::Insert => statistics.ruff_output += 1, + ChangeTag::Equal => statistics.intersection += 1, + } + } + statistics + } + } + + #[allow(clippy::cast_precision_loss)] + pub(crate) fn jaccard_index(&self) -> f32 { + self.intersection as f32 / (self.black_input + self.ruff_output + self.intersection) as f32 + } +} + +impl Add for Statistics { + type Output = Statistics; + + fn add(self, rhs: Statistics) -> Self::Output { + Statistics { + black_input: self.black_input + rhs.black_input, + ruff_output: self.ruff_output + rhs.ruff_output, + intersection: self.intersection + rhs.intersection, + } + } +} + +impl AddAssign for Statistics { + fn add_assign(&mut self, rhs: Statistics) { + *self = *self + rhs; + } +} + +/// Control the verbosity of the output +#[derive(Copy, Clone, PartialEq, Eq, clap::ValueEnum, Default)] +pub(crate) enum Format { + /// Filenames only + Minimal, + /// Filenames and reduced diff + #[default] + Default, + /// Full diff and invalid code + Full, +} + +#[allow(clippy::struct_excessive_bools)] +#[derive(clap::Args)] +pub(crate) struct Args { + /// Like `ruff check`'s files. See `--multi-project` if you want to format an ecosystem + /// checkout. + pub(crate) files: Vec, + /// Check stability + /// + /// We want to ensure that once formatted content stays the same when formatted again, which is + /// known as formatter stability or formatter idempotency, and that the formatter prints + /// syntactically valid code. As our test cases cover only a limited amount of code, this allows + /// checking entire repositories. + #[arg(long)] + pub(crate) stability_check: bool, + /// Format the files. Without this flag, the python files are not modified + #[arg(long)] + pub(crate) write: bool, + /// Control the verbosity of the output + #[arg(long, default_value_t, value_enum)] + pub(crate) format: Format, + /// Print only the first error and exit, `-x` is same as pytest + #[arg(long, short = 'x')] + pub(crate) exit_first_error: bool, + /// Checks each project inside a directory, useful e.g. if you want to check all of the + /// ecosystem checkouts. + #[arg(long)] + pub(crate) multi_project: bool, + /// Write all errors to this file in addition to stdout. Only used in multi-project mode. + #[arg(long)] + pub(crate) error_file: Option, +} + +pub(crate) fn main(args: &Args) -> anyhow::Result { + let all_success = if args.multi_project { + format_dev_multi_project(args) + } else { + let result = format_dev_project(&args.files, args.stability_check, args.write, true)?; + + #[allow(clippy::print_stdout)] + { + print!("{}", result.display(args.format)); + println!( + "Found {} stability errors in {} files (jaccard index {:.3}) in {:.2}s", + result + .diagnostics + .iter() + .filter(|diagnostics| !diagnostics.error.is_success()) + .count(), + result.file_count, + result.statistics.jaccard_index(), + result.duration.as_secs_f32(), + ); + } + + !result.is_success() + }; + if all_success { + Ok(ExitCode::SUCCESS) + } else { + Ok(ExitCode::FAILURE) + } +} + +/// Each `path` is one of the `files` in `Args` +enum Message { + Start { + path: PathBuf, + }, + Failed { + path: PathBuf, + error: anyhow::Error, + }, + Finished { + path: PathBuf, + result: CheckRepoResult, + }, +} + +/// Checks a directory of projects +fn format_dev_multi_project(args: &Args) -> bool { + let mut all_success = true; + let mut total_errors = 0; + let mut total_files = 0; + let start = Instant::now(); + + rayon::scope(|scope| { + let (sender, receiver) = channel(); + + // Workers, to check is subdirectory in parallel + for base_dir in &args.files { + for dir in base_dir.read_dir().unwrap() { + let path = dir.unwrap().path().clone(); + + let sender = sender.clone(); + + scope.spawn(move |_| { + sender.send(Message::Start { path: path.clone() }).unwrap(); + + match format_dev_project( + &[path.clone()], + args.stability_check, + args.write, + false, + ) { + Ok(result) => sender.send(Message::Finished { result, path }), + Err(error) => sender.send(Message::Failed { error, path }), + } + .unwrap(); + }); + } + } + + // Main thread, writing to stdout + scope.spawn(|_| { + let mut error_file = args.error_file.as_ref().map(|error_file| { + BufWriter::new(File::create(error_file).expect("Couldn't open error file")) + }); + + let bar = ProgressBar::new(args.files.len() as u64); + for message in receiver { + match message { + Message::Start { path } => { + bar.println(path.display().to_string()); + } + Message::Finished { path, result } => { + total_errors += result.diagnostics.len(); + total_files += result.file_count; + + bar.println(format!( + "Finished {} with {} files (jaccard index {:.3}) in {:.2}s", + path.display(), + result.file_count, + result.statistics.jaccard_index(), + result.duration.as_secs_f32(), + )); + bar.println(result.display(args.format).to_string().trim_end()); + if let Some(error_file) = &mut error_file { + write!(error_file, "{}", result.display(args.format)).unwrap(); + } + all_success = all_success && !result.is_success(); + bar.inc(1); + } + Message::Failed { path, error } => { + bar.println(format!("Failed {}: {}", path.display(), error)); + all_success = false; + bar.inc(1); + } + } + } + bar.finish(); + }); + }); + + let duration = start.elapsed(); + + #[allow(clippy::print_stdout)] + { + println!( + "{total_errors} stability errors in {total_files} files in {}s", + duration.as_secs_f32() + ); + } + + all_success +} + +fn format_dev_project( + files: &[PathBuf], + stability_check: bool, + write: bool, + progress_bar: bool, +) -> anyhow::Result { + let start = Instant::now(); + + // Find files to check (or in this case, format twice). Adapted from ruff_cli + // First argument is ignored + let paths = ruff_check_paths(files)?; + + let bar = progress_bar.then(|| ProgressBar::new(paths.len() as u64)); + let result_iter = paths + .into_par_iter() + .map(|dir_entry| { + let dir_entry = match dir_entry.context("Iterating the files in the repository failed") + { + Ok(dir_entry) => dir_entry, + Err(err) => return Err(err), + }; + let file = dir_entry.path().to_path_buf(); + // For some reason it does not filter in the beginning + if dir_entry.file_name() == "pyproject.toml" { + return Ok((Ok(Statistics::default()), file)); + } + + let file = dir_entry.path().to_path_buf(); + // Handle panics (mostly in `debug_assert!`) + let result = match catch_unwind(|| format_dev_file(&file, stability_check, write)) { + Ok(result) => result, + Err(panic) => { + if let Some(message) = panic.downcast_ref::() { + Err(CheckFileError::Panic { + message: message.clone(), + }) + } else if let Some(&message) = panic.downcast_ref::<&str>() { + Err(CheckFileError::Panic { + message: message.to_string(), + }) + } else { + Err(CheckFileError::Panic { + // This should not happen, but it can + message: "(Panic didn't set a string message)".to_string(), + }) + } + } + }; + if let Some(bar) = &bar { + bar.inc(1); + } + Ok((result, file)) + }) + .collect::>>()?; + if let Some(bar) = bar { + bar.finish(); + } + + let mut statistics = Statistics::default(); + let mut formatted_counter = 0; + let mut diagnostics = Vec::new(); + for (result, file) in result_iter { + formatted_counter += 1; + match result { + Ok(statistics_file) => statistics += statistics_file, + Err(error) => diagnostics.push(Diagnostic { file, error }), + } + } + + let duration = start.elapsed(); + + Ok(CheckRepoResult { + duration, + file_count: formatted_counter, + diagnostics, + statistics, + }) +} + +/// A compact diff that only shows a header and changes, but nothing unchanged. This makes viewing +/// multiple errors easier. +fn diff_show_only_changes( + writer: &mut Formatter, + formatted: &str, + reformatted: &str, +) -> fmt::Result { + for changes in TextDiff::from_lines(formatted, reformatted) + .unified_diff() + .iter_hunks() + { + for (idx, change) in changes + .iter_changes() + .filter(|change| change.tag() != ChangeTag::Equal) + .enumerate() + { + if idx == 0 { + writeln!(writer, "{}", changes.header())?; + } + write!(writer, "{}", change.tag())?; + writer.write_str(change.value())?; + } + } + Ok(()) +} + +struct CheckRepoResult { + duration: Duration, + file_count: usize, + diagnostics: Vec, + statistics: Statistics, +} + +impl CheckRepoResult { + fn display(&self, format: Format) -> DisplayCheckRepoResult { + DisplayCheckRepoResult { + result: self, + format, + } + } + + /// We also emit diagnostics if the input file was already invalid or the were io errors. This + /// method helps to differentiate + fn is_success(&self) -> bool { + self.diagnostics + .iter() + .all(|diagnostic| diagnostic.error.is_success()) + } +} + +struct DisplayCheckRepoResult<'a> { + result: &'a CheckRepoResult, + format: Format, +} + +impl Display for DisplayCheckRepoResult<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + for diagnostic in &self.result.diagnostics { + write!(f, "{}", diagnostic.display(self.format))?; + } + Ok(()) + } +} + +#[derive(Debug)] +struct Diagnostic { + file: PathBuf, + error: CheckFileError, +} + +impl Diagnostic { + fn display(&self, format: Format) -> DisplayDiagnostic { + DisplayDiagnostic { + diagnostic: self, + format, + } + } +} + +struct DisplayDiagnostic<'a> { + format: Format, + diagnostic: &'a Diagnostic, +} + +impl Display for DisplayDiagnostic<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let Diagnostic { file, error } = &self.diagnostic; + + match error { + CheckFileError::Unstable { + formatted, + reformatted, + } => { + writeln!(f, "Unstable formatting {}", file.display())?; + match self.format { + Format::Minimal => {} + Format::Default => { + diff_show_only_changes(f, formatted, reformatted)?; + } + Format::Full => { + let diff = TextDiff::from_lines(formatted.as_str(), reformatted.as_str()) + .unified_diff() + .header("Formatted once", "Formatted twice") + .to_string(); + writeln!( + f, + r#"Reformatting the formatted code a second time resulted in formatting changes. +--- +{diff}--- + +Formatted once: +--- +{formatted}--- + +Formatted twice: +--- +{reformatted}---\n"#, + )?; + } + } + } + CheckFileError::Panic { message } => { + writeln!(f, "Panic {}: {}", file.display(), message)?; + } + CheckFileError::SyntaxErrorInInput(error) => { + writeln!(f, "Syntax error in {}: {}", file.display(), error)?; + } + CheckFileError::SyntaxErrorInOutput { formatted, error } => { + writeln!( + f, + "Formatter generated invalid syntax {}: {}", + file.display(), + error + )?; + if self.format == Format::Full { + writeln!(f, "---\n{formatted}\n---\n")?; + } + } + CheckFileError::FormatError(error) => { + writeln!(f, "Formatter error for {}: {}", file.display(), error)?; + } + CheckFileError::PrintError(error) => { + writeln!(f, "Printer error for {}: {}", file.display(), error)?; + } + CheckFileError::IoError(error) => { + writeln!(f, "Error reading {}: {}", file.display(), error)?; + } + } + Ok(()) + } +} + +#[derive(Debug)] +enum CheckFileError { + /// First and second pass of the formatter are different + Unstable { + formatted: String, + reformatted: String, + }, + /// The input file was already invalid (not a bug) + SyntaxErrorInInput(FormatModuleError), + /// The formatter introduced a syntax error + SyntaxErrorInOutput { + formatted: String, + error: FormatModuleError, + }, + /// The formatter failed (bug) + FormatError(FormatError), + /// The printer failed (bug) + PrintError(PrintError), + /// Failed to read the file, this sometimes happens e.g. with strange filenames (not a bug) + IoError(io::Error), + /// From `catch_unwind` + Panic { message: String }, +} + +impl CheckFileError { + /// Returns `false` if this is a formatter bug or `true` is if it is something outside of ruff + fn is_success(&self) -> bool { + match self { + CheckFileError::SyntaxErrorInInput(_) | CheckFileError::IoError(_) => true, + CheckFileError::Unstable { .. } + | CheckFileError::SyntaxErrorInOutput { .. } + | CheckFileError::FormatError(_) + | CheckFileError::PrintError(_) + | CheckFileError::Panic { .. } => false, + } + } +} + +impl From for CheckFileError { + fn from(value: io::Error) -> Self { + Self::IoError(value) + } +} + +fn format_dev_file( + input_path: &Path, + stability_check: bool, + write: bool, +) -> Result { + let content = fs::read_to_string(input_path)?; + let printed = match format_module(&content, PyFormatOptions::default()) { + Ok(printed) => printed, + Err(err @ (FormatModuleError::LexError(_) | FormatModuleError::ParseError(_))) => { + return Err(CheckFileError::SyntaxErrorInInput(err)); + } + Err(FormatModuleError::FormatError(err)) => { + return Err(CheckFileError::FormatError(err)); + } + Err(FormatModuleError::PrintError(err)) => { + return Err(CheckFileError::PrintError(err)); + } + }; + let formatted = printed.as_code(); + + if write && content != formatted { + // Simple atomic write. + // The file is in a directory so it must have a parent. Surprisingly `DirEntry` doesn't + // give us access without unwrap + let mut file = NamedTempFile::new_in(input_path.parent().unwrap())?; + file.write_all(formatted.as_bytes())?; + // "If a file exists at the target path, persist will atomically replace it." + file.persist(input_path).map_err(|error| error.error)?; + } + + if stability_check { + let reformatted = match format_module(formatted, PyFormatOptions::default()) { + Ok(reformatted) => reformatted, + Err(err @ (FormatModuleError::LexError(_) | FormatModuleError::ParseError(_))) => { + return Err(CheckFileError::SyntaxErrorInOutput { + formatted: formatted.to_string(), + error: err, + }); + } + Err(FormatModuleError::FormatError(err)) => { + return Err(CheckFileError::FormatError(err)); + } + Err(FormatModuleError::PrintError(err)) => { + return Err(CheckFileError::PrintError(err)); + } + }; + + if reformatted.as_code() != formatted { + return Err(CheckFileError::Unstable { + formatted: formatted.to_string(), + reformatted: reformatted.into_code(), + }); + } + } + + Ok(Statistics::from_versions(&content, formatted)) +} diff --git a/crates/ruff_dev/src/main.rs b/crates/ruff_dev/src/main.rs index 243fceb34b..c981d8c8a2 100644 --- a/crates/ruff_dev/src/main.rs +++ b/crates/ruff_dev/src/main.rs @@ -8,7 +8,7 @@ use ruff::logging::{set_up_logging, LogLevel}; use ruff_cli::check; use std::process::ExitCode; -mod check_formatter_stability; +mod format_dev; mod generate_all; mod generate_cli_help; mod generate_docs; @@ -63,9 +63,15 @@ enum Command { #[clap(long)] repeat: usize, }, - /// Format a repository twice and ensure that it looks that the first and second formatting - /// look the same. Same arguments as `ruff check` - CheckFormatterStability(check_formatter_stability::Args), + /// Several utils related to the formatter which can be run on one or more repositories. The + /// selected set of files in a repository is the same as for `ruff check`. + /// + /// * Check formatter stability: Format a repository twice and ensure that it looks that the + /// first and second formatting look the same. + /// * Format: Format the files in a repository to be able to check them with `git diff` + /// * Statistics: The subcommand the Jaccard index between the (assumed to be black formatted) + /// input and the ruff formatted output + FormatDev(format_dev::Args), } fn main() -> Result { @@ -93,8 +99,8 @@ fn main() -> Result { check(args.clone(), log_level)?; } } - Command::CheckFormatterStability(args) => { - let exit_code = check_formatter_stability::main(&args)?; + Command::FormatDev(args) => { + let exit_code = format_dev::main(&args)?; return Ok(exit_code); } } diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index a096918fa3..c27d92a907 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -27,6 +27,7 @@ rustc-hash = { workspace = true } rustpython-parser = { workspace = true } serde = { workspace = true, optional = true } smallvec = { workspace = true } +thiserror = { workspace = true } [dev-dependencies] ruff_formatter = { path = "../ruff_formatter", features = ["serde"]} diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 4932ca5da0..dcdf449795 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -3,17 +3,23 @@ use crate::comments::{ }; use crate::context::PyFormatContext; pub use crate::options::{MagicTrailingComma, PyFormatOptions, QuoteStyle}; -use anyhow::{anyhow, Context, Result}; -use ruff_formatter::prelude::*; -use ruff_formatter::{format, write}; +use ruff_formatter::format_element::tag; +use ruff_formatter::prelude::{ + dynamic_text, source_position, source_text_slice, text, ContainsNewlines, Formatter, Tag, +}; +use ruff_formatter::{ + format, normalize_newlines, write, Buffer, Format, FormatElement, FormatError, FormatResult, + PrintError, +}; use ruff_formatter::{Formatted, Printed, SourceCode}; use ruff_python_ast::node::{AnyNodeRef, AstNode, NodeKind}; use ruff_python_ast::source_code::{CommentRanges, CommentRangesBuilder, Locator}; use ruff_text_size::{TextLen, TextRange}; use rustpython_parser::ast::{Mod, Ranged}; -use rustpython_parser::lexer::lex; -use rustpython_parser::{parse_tokens, Mode}; +use rustpython_parser::lexer::{lex, LexicalError}; +use rustpython_parser::{parse_tokens, Mode, ParseError}; use std::borrow::Cow; +use thiserror::Error; pub(crate) mod builders; pub mod cli; @@ -84,16 +90,40 @@ where } } -pub fn format_module(contents: &str, options: PyFormatOptions) -> Result { +#[derive(Error, Debug)] +pub enum FormatModuleError { + #[error("source contains syntax errors (lexer error): {0:?}")] + LexError(LexicalError), + #[error("source contains syntax errors (parser error): {0:?}")] + ParseError(ParseError), + #[error(transparent)] + FormatError(#[from] FormatError), + #[error(transparent)] + PrintError(#[from] PrintError), +} + +impl From for FormatModuleError { + fn from(value: LexicalError) -> Self { + Self::LexError(value) + } +} + +impl From for FormatModuleError { + fn from(value: ParseError) -> Self { + Self::ParseError(value) + } +} + +pub fn format_module( + contents: &str, + options: PyFormatOptions, +) -> Result { // Tokenize once let mut tokens = Vec::new(); let mut comment_ranges = CommentRangesBuilder::default(); for result in lex(contents, Mode::Module) { - let (token, range) = match result { - Ok((token, range)) => (token, range), - Err(err) => return Err(anyhow!("Source contains syntax errors {err:?}")), - }; + let (token, range) = result?; comment_ranges.visit_token(&token, range); tokens.push(Ok((token, range))); @@ -102,14 +132,11 @@ pub fn format_module(contents: &str, options: PyFormatOptions) -> Result") - .with_context(|| "Syntax error in input")?; + let python_ast = parse_tokens(tokens, Mode::Module, "")?; let formatted = format_node(&python_ast, &comment_ranges, contents, options)?; - formatted - .print() - .with_context(|| "Failed to print the formatter IR") + Ok(formatted.print()?) } pub fn format_node<'a>( From aaab9f1597abc6abe49ddf0c517ff767f30c0a0f Mon Sep 17 00:00:00 2001 From: Peter Attia Date: Fri, 7 Jul 2023 06:35:50 -0700 Subject: [PATCH 361/447] Bugfix: Remove version numbers from pypi links (#5579) ## Summary There are two pypi links in the documentation that link to specific version numbers of other packages. Removing these versioned links allows users to immediately view the latest version of the package and maintains consistency with the other links. ## Test Plan N/A --- crates/ruff/src/registry.rs | 4 ++-- crates/ruff/src/rules/flake8_logging_format/mod.rs | 2 +- crates/ruff/src/rules/tryceratops/mod.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 4ff731653b..56e30db0ff 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -112,7 +112,7 @@ pub enum Linter { /// [flake8-import-conventions](https://github.com/joaopalmeiro/flake8-import-conventions) #[prefix = "ICN"] Flake8ImportConventions, - /// [flake8-logging-format](https://pypi.org/project/flake8-logging-format/0.9.0/) + /// [flake8-logging-format](https://pypi.org/project/flake8-logging-format/) #[prefix = "G"] Flake8LoggingFormat, /// [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420/) @@ -181,7 +181,7 @@ pub enum Linter { /// [Pylint](https://pypi.org/project/pylint/) #[prefix = "PL"] Pylint, - /// [tryceratops](https://pypi.org/project/tryceratops/1.1.0/) + /// [tryceratops](https://pypi.org/project/tryceratops/) #[prefix = "TRY"] Tryceratops, /// [flynt](https://pypi.org/project/flynt/) diff --git a/crates/ruff/src/rules/flake8_logging_format/mod.rs b/crates/ruff/src/rules/flake8_logging_format/mod.rs index c76235febe..c334117482 100644 --- a/crates/ruff/src/rules/flake8_logging_format/mod.rs +++ b/crates/ruff/src/rules/flake8_logging_format/mod.rs @@ -1,4 +1,4 @@ -//! Rules from [flake8-logging-format](https://pypi.org/project/flake8-logging-format/0.9.0/). +//! Rules from [flake8-logging-format](https://pypi.org/project/flake8-logging-format/). pub(crate) mod rules; pub(crate) mod violations; diff --git a/crates/ruff/src/rules/tryceratops/mod.rs b/crates/ruff/src/rules/tryceratops/mod.rs index bf88fb825b..cd1075ba86 100644 --- a/crates/ruff/src/rules/tryceratops/mod.rs +++ b/crates/ruff/src/rules/tryceratops/mod.rs @@ -1,4 +1,4 @@ -//! Rules from [tryceratops](https://pypi.org/project/tryceratops/1.1.0/). +//! Rules from [tryceratops](https://pypi.org/project/tryceratops/). pub(crate) mod helpers; pub(crate) mod rules; From 072358e26b9812239e70ca3b183fb9c3d2f519d2 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 7 Jul 2023 10:00:44 -0400 Subject: [PATCH 362/447] Use Instagram's LibCST rather than our fork (#5593) ## Summary Historically, we only used a fork to enable building without pyo3. But pyo3 is an optional feature. I may've just not understood how to accomplish this way back when. --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2376d4aab3..bc1fa2a60b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1137,7 +1137,7 @@ checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "libcst" version = "0.1.0" -source = "git+https://github.com/charliermarsh/LibCST?rev=80e4c1399f95e5beb532fdd1e209ad2dbb470438#80e4c1399f95e5beb532fdd1e209ad2dbb470438" +source = "git+https://github.com/Instagram/LibCST.git?rev=3cacca1a1029f05707e50703b49fe3dd860aa839#3cacca1a1029f05707e50703b49fe3dd860aa839" dependencies = [ "chic", "itertools", @@ -1152,7 +1152,7 @@ dependencies = [ [[package]] name = "libcst_derive" version = "0.1.0" -source = "git+https://github.com/charliermarsh/LibCST?rev=80e4c1399f95e5beb532fdd1e209ad2dbb470438#80e4c1399f95e5beb532fdd1e209ad2dbb470438" +source = "git+https://github.com/Instagram/LibCST.git?rev=3cacca1a1029f05707e50703b49fe3dd860aa839#3cacca1a1029f05707e50703b49fe3dd860aa839" dependencies = [ "quote", "syn 1.0.109", diff --git a/Cargo.toml b/Cargo.toml index 1ca004af7b..de63ac83b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,8 +48,8 @@ test-case = { version = "3.0.0" } thiserror = { version = "1.0.43" } toml = { version = "0.7.2" } -# v0.0.1 -libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "80e4c1399f95e5beb532fdd1e209ad2dbb470438" } +# v1.0.1 +libcst = { git = "https://github.com/Instagram/LibCST.git", rev = "3cacca1a1029f05707e50703b49fe3dd860aa839", default-features = false } # Please tag the RustPython version everytime you update its revision here and in fuzz/Cargo.toml # Tagging the version ensures that older ruff versions continue to build from source even when we rebase our RustPython fork. From 5640c310bb9957606ea120b64b2687fd3f33f4a5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 7 Jul 2023 11:41:20 -0400 Subject: [PATCH 363/447] Move file-level rule exemption to lexer-based approach (#5567) ## Summary In addition to `# noqa` codes, we also support file-level exemptions, which look like: - `# flake8: noqa` (ignore all rules in the file, for compatibility) - `# ruff: noqa` (all rules in the file) - `# ruff: noqa: F401` (ignore `F401` in the file, Flake8 doesn't support this) This PR moves that logic to something that looks a lot more like our `# noqa` parser. Performance is actually quite a bit _worse_ than the previous approach (lexing `# flake8: noqa` goes from 2ns to 11ns; lexing `# ruff: noqa: F401, F841` is about the same`; lexing `# type: ignore # noqa: E501` fgoes from 4ns to 6ns), but the numbers are very small so it's... maybe worth it? The primary benefit here is that we now properly support flexible whitespace, like: `#flake8:noqa`. Previously, we required exact string matching, and we also didn't support all case-insensitive variants of `noqa`. --- crates/ruff/src/noqa.rs | 207 +++++++++++++++--- ...ff__noqa__tests__flake8_exemption_all.snap | 7 + ...flake8_exemption_all_case_insensitive.snap | 7 + ..._tests__flake8_exemption_all_no_space.snap | 7 + ...__noqa__tests__flake8_exemption_codes.snap | 7 + ...ruff__noqa__tests__ruff_exemption_all.snap | 7 + ...__ruff_exemption_all_case_insensitive.snap | 7 + ...a__tests__ruff_exemption_all_no_space.snap | 7 + ...ff__noqa__tests__ruff_exemption_codes.snap | 12 + 9 files changed, 234 insertions(+), 34 deletions(-) create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_case_insensitive.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_no_space.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_codes.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_case_insensitive.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_no_space.snap create mode 100644 crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_codes.snap diff --git a/crates/ruff/src/noqa.rs b/crates/ruff/src/noqa.rs index 176dd6d345..a34b73f57c 100644 --- a/crates/ruff/src/noqa.rs +++ b/crates/ruff/src/noqa.rs @@ -14,7 +14,7 @@ use rustpython_parser::ast::Ranged; use ruff_diagnostics::Diagnostic; use ruff_python_ast::source_code::Locator; -use ruff_python_whitespace::{LineEnding, PythonWhitespace}; +use ruff_python_whitespace::LineEnding; use crate::codes::NoqaCode; use crate::registry::{AsRule, Rule, RuleSet}; @@ -83,7 +83,7 @@ impl<'a> Directive<'a> { let mut codes = vec![]; let mut codes_end = codes_start; let mut leading_space = 0; - while let Some(code) = Directive::lex_code(&text[codes_end + leading_space..]) { + while let Some(code) = Self::lex_code(&text[codes_end + leading_space..]) { codes.push(code); codes_end += leading_space; codes_end += code.len(); @@ -126,16 +126,17 @@ impl<'a> Directive<'a> { } /// Lex an individual rule code (e.g., `F401`). - fn lex_code(text: &str) -> Option<&str> { + #[inline] + fn lex_code(line: &str) -> Option<&str> { // Extract, e.g., the `F` in `F401`. - let prefix = text.chars().take_while(char::is_ascii_uppercase).count(); + let prefix = line.chars().take_while(char::is_ascii_uppercase).count(); // Extract, e.g., the `401` in `F401`. - let suffix = text[prefix..] + let suffix = line[prefix..] .chars() .take_while(char::is_ascii_digit) .count(); if prefix > 0 && suffix > 0 { - Some(&text[..prefix + suffix]) + Some(&line[..prefix + suffix]) } else { None } @@ -256,37 +257,126 @@ enum ParsedFileExemption<'a> { impl<'a> ParsedFileExemption<'a> { /// Return a [`ParsedFileExemption`] for a given comment line. fn try_extract(line: &'a str) -> Option { - let line = line.trim_whitespace_start(); + let line = Self::lex_whitespace(line); + let line = Self::lex_char(line, '#')?; + let line = Self::lex_whitespace(line); - if line.starts_with("# flake8: noqa") - || line.starts_with("# flake8: NOQA") - || line.starts_with("# flake8: NoQA") - { - return Some(Self::All); - } + if let Some(line) = Self::lex_flake8(line) { + // Ex) `# flake8: noqa` + let line = Self::lex_whitespace(line); + let line = Self::lex_char(line, ':')?; + let line = Self::lex_whitespace(line); + Self::lex_noqa(line)?; + Some(Self::All) + } else if let Some(line) = Self::lex_ruff(line) { + let line = Self::lex_whitespace(line); + let line = Self::lex_char(line, ':')?; + let line = Self::lex_whitespace(line); + let line = Self::lex_noqa(line)?; - if let Some(remainder) = line - .strip_prefix("# ruff: noqa") - .or_else(|| line.strip_prefix("# ruff: NOQA")) - .or_else(|| line.strip_prefix("# ruff: NoQA")) - { - if remainder.is_empty() { - return Some(Self::All); - } else if let Some(codes) = remainder.strip_prefix(':') { - let codes = codes - .split(|c: char| c.is_whitespace() || c == ',') - .map(str::trim) - .filter(|code| !code.is_empty()) - .collect_vec(); - if codes.is_empty() { - warn!("Expected rule codes on `noqa` directive: \"{line}\""); + if line.is_empty() { + // Ex) `# ruff: noqa` + Some(Self::All) + } else { + // Ex) `# ruff: noqa: F401, F841` + let line = Self::lex_whitespace(line); + let Some(line) = Self::lex_char(line, ':') else { + warn!("Unexpected suffix on `noqa` directive: \"{line}\""); + return None; + }; + let line = Self::lex_whitespace(line); + + // Extract the codes from the line (e.g., `F401, F841`). + let mut codes = vec![]; + let mut line = line; + while let Some(code) = Self::lex_code(line) { + codes.push(code); + line = &line[code.len()..]; + + // Codes can be comma- or whitespace-delimited. + if let Some(rest) = Self::lex_delimiter(line).map(Self::lex_whitespace) { + line = rest; + } else { + break; + } } - return Some(Self::Codes(codes)); - } - warn!("Unexpected suffix on `noqa` directive: \"{line}\""); - } - None + Some(Self::Codes(codes)) + } + } else { + None + } + } + + /// Lex optional leading whitespace. + #[inline] + fn lex_whitespace(line: &str) -> &str { + line.trim_start() + } + + /// Lex a specific character, or return `None` if the character is not the first character in + /// the line. + #[inline] + fn lex_char(line: &str, c: char) -> Option<&str> { + let mut chars = line.chars(); + if chars.next() == Some(c) { + Some(chars.as_str()) + } else { + None + } + } + + /// Lex the "flake8" prefix of a `noqa` directive. + #[inline] + fn lex_flake8(line: &str) -> Option<&str> { + line.strip_prefix("flake8") + } + + /// Lex the "ruff" prefix of a `noqa` directive. + #[inline] + fn lex_ruff(line: &str) -> Option<&str> { + line.strip_prefix("ruff") + } + + /// Lex a `noqa` directive with case-insensitive matching. + #[inline] + fn lex_noqa(line: &str) -> Option<&str> { + match line.as_bytes() { + [b'n' | b'N', b'o' | b'O', b'q' | b'Q', b'a' | b'A', ..] => Some(&line["noqa".len()..]), + _ => None, + } + } + + /// Lex a code delimiter, which can either be a comma or whitespace. + #[inline] + fn lex_delimiter(line: &str) -> Option<&str> { + let mut chars = line.chars(); + if let Some(c) = chars.next() { + if c == ',' || c.is_whitespace() { + Some(chars.as_str()) + } else { + None + } + } else { + None + } + } + + /// Lex an individual rule code (e.g., `F401`). + #[inline] + fn lex_code(line: &str) -> Option<&str> { + // Extract, e.g., the `F` in `F401`. + let prefix = line.chars().take_while(char::is_ascii_uppercase).count(); + // Extract, e.g., the `401` in `F401`. + let suffix = line[prefix..] + .chars() + .take_while(char::is_ascii_digit) + .count(); + if prefix > 0 && suffix > 0 { + Some(&line[..prefix + suffix]) + } else { + None + } } } @@ -620,7 +710,7 @@ mod tests { use ruff_python_ast::source_code::Locator; use ruff_python_whitespace::LineEnding; - use crate::noqa::{add_noqa_inner, Directive, NoqaMapping}; + use crate::noqa::{add_noqa_inner, Directive, NoqaMapping, ParsedFileExemption}; use crate::rules::pycodestyle::rules::AmbiguousVariableName; use crate::rules::pyflakes::rules::UnusedVariable; @@ -750,6 +840,55 @@ mod tests { assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } + #[test] + fn flake8_exemption_all() { + let source = "# flake8: noqa"; + assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); + } + + #[test] + fn ruff_exemption_all() { + let source = "# ruff: noqa"; + assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); + } + + #[test] + fn flake8_exemption_all_no_space() { + let source = "#flake8:noqa"; + assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); + } + + #[test] + fn ruff_exemption_all_no_space() { + let source = "#ruff:noqa"; + assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); + } + + #[test] + fn flake8_exemption_codes() { + // Note: Flake8 doesn't support this; it's treated as a blanket exemption. + let source = "# flake8: noqa: F401, F841"; + assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); + } + + #[test] + fn ruff_exemption_codes() { + let source = "# ruff: noqa: F401, F841"; + assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); + } + + #[test] + fn flake8_exemption_all_case_insensitive() { + let source = "# flake8: NoQa"; + assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); + } + + #[test] + fn ruff_exemption_all_case_insensitive() { + let source = "# ruff: NoQa"; + assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); + } + #[test] fn modification() { let contents = "x = 1"; diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all.snap new file mode 100644 index 0000000000..30642fbd1c --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all.snap @@ -0,0 +1,7 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "ParsedFileExemption::try_extract(source)" +--- +Some( + All, +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_case_insensitive.snap new file mode 100644 index 0000000000..30642fbd1c --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_case_insensitive.snap @@ -0,0 +1,7 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "ParsedFileExemption::try_extract(source)" +--- +Some( + All, +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_no_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_no_space.snap new file mode 100644 index 0000000000..30642fbd1c --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_no_space.snap @@ -0,0 +1,7 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "ParsedFileExemption::try_extract(source)" +--- +Some( + All, +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_codes.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_codes.snap new file mode 100644 index 0000000000..30642fbd1c --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_codes.snap @@ -0,0 +1,7 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "ParsedFileExemption::try_extract(source)" +--- +Some( + All, +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all.snap new file mode 100644 index 0000000000..30642fbd1c --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all.snap @@ -0,0 +1,7 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "ParsedFileExemption::try_extract(source)" +--- +Some( + All, +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_case_insensitive.snap new file mode 100644 index 0000000000..30642fbd1c --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_case_insensitive.snap @@ -0,0 +1,7 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "ParsedFileExemption::try_extract(source)" +--- +Some( + All, +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_no_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_no_space.snap new file mode 100644 index 0000000000..30642fbd1c --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_no_space.snap @@ -0,0 +1,7 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "ParsedFileExemption::try_extract(source)" +--- +Some( + All, +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_codes.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_codes.snap new file mode 100644 index 0000000000..cbf46f63ff --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_codes.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "ParsedFileExemption::try_extract(source)" +--- +Some( + Codes( + [ + "F401", + "F841", + ], + ), +) From 60d318ddcfac26be28d8f6e3448f11326e68009a Mon Sep 17 00:00:00 2001 From: konsti Date: Fri, 7 Jul 2023 18:28:36 +0200 Subject: [PATCH 364/447] Check formatter stability on CI (#5446) Check formatter stability on CI using CPython. This should be merged into the ecosystem checks, but i think this is a good start. --- .github/workflows/ci.yaml | 14 ++++++++++++++ crates/ruff_dev/src/format_dev.rs | 24 +++++++++--------------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 23d2595b6d..d85d5a79d9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -283,3 +283,17 @@ jobs: - name: "Build docs" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }} run: mkdocs build --strict -f mkdocs.generated.yml + + check-formatter-stability: + name: "Check formatter stability" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: "Install Rust toolchain" + run: rustup show + - name: "Cache rust" + uses: Swatinem/rust-cache@v2 + - name: "Clone CPython 3.10" + run: git clone --branch 3.10 --depth 1 https://github.com/python/cpython.git crates/ruff/resources/test/cpython + - name: "Check stability" + run: cargo run --bin ruff_dev -- format-dev --stability-check crates/ruff/resources/test/cpython diff --git a/crates/ruff_dev/src/format_dev.rs b/crates/ruff_dev/src/format_dev.rs index 7695d93e65..d4bf4157be 100644 --- a/crates/ruff_dev/src/format_dev.rs +++ b/crates/ruff_dev/src/format_dev.rs @@ -165,24 +165,21 @@ pub(crate) fn main(args: &Args) -> anyhow::Result { format_dev_multi_project(args) } else { let result = format_dev_project(&args.files, args.stability_check, args.write, true)?; + let error_count = result.error_count(); #[allow(clippy::print_stdout)] { print!("{}", result.display(args.format)); println!( "Found {} stability errors in {} files (jaccard index {:.3}) in {:.2}s", - result - .diagnostics - .iter() - .filter(|diagnostics| !diagnostics.error.is_success()) - .count(), + error_count, result.file_count, result.statistics.jaccard_index(), result.duration.as_secs_f32(), ); } - !result.is_success() + error_count == 0 }; if all_success { Ok(ExitCode::SUCCESS) @@ -208,7 +205,6 @@ enum Message { /// Checks a directory of projects fn format_dev_multi_project(args: &Args) -> bool { - let mut all_success = true; let mut total_errors = 0; let mut total_files = 0; let start = Instant::now(); @@ -253,7 +249,7 @@ fn format_dev_multi_project(args: &Args) -> bool { bar.println(path.display().to_string()); } Message::Finished { path, result } => { - total_errors += result.diagnostics.len(); + total_errors += result.error_count(); total_files += result.file_count; bar.println(format!( @@ -267,12 +263,10 @@ fn format_dev_multi_project(args: &Args) -> bool { if let Some(error_file) = &mut error_file { write!(error_file, "{}", result.display(args.format)).unwrap(); } - all_success = all_success && !result.is_success(); bar.inc(1); } Message::Failed { path, error } => { bar.println(format!("Failed {}: {}", path.display(), error)); - all_success = false; bar.inc(1); } } @@ -291,7 +285,7 @@ fn format_dev_multi_project(args: &Args) -> bool { ); } - all_success + total_errors == 0 } fn format_dev_project( @@ -414,12 +408,12 @@ impl CheckRepoResult { } } - /// We also emit diagnostics if the input file was already invalid or the were io errors. This - /// method helps to differentiate - fn is_success(&self) -> bool { + /// Count the actual errors excluding invalid input files and io errors + fn error_count(&self) -> usize { self.diagnostics .iter() - .all(|diagnostic| diagnostic.error.is_success()) + .filter(|diagnostics| !diagnostics.error.is_success()) + .count() } } From bb7303f86784fe8330f1d670d9ffbdef9a319fd7 Mon Sep 17 00:00:00 2001 From: Zanie Date: Fri, 7 Jul 2023 11:43:10 -0500 Subject: [PATCH 365/447] Implement PYI030: Unnecessary literal union (#5570) Implements PYI030 as part of https://github.com/astral-sh/ruff/issues/848 > Union expressions should never have more than one Literal member, as Literal[1] | Literal[2] is semantically identical to Literal[1, 2]. Note we differ slightly from the flake8-pyi implementation: - We detect cases where there are parentheses or nested unions - We detect cases with mixed `Union` and `|` syntax - We use the same error message for all violations; flake8-pyi has two different messages - We retain the user's quoting style when displaying string literals; flake8-pyi uses single quotes - We warn on duplicates of the same literal `Literal[1] | Literal[1]` --- .../test/fixtures/flake8_pyi/PYI030.py | 24 ++ .../test/fixtures/flake8_pyi/PYI030.pyi | 86 +++++++ crates/ruff/src/checkers/ast/mod.rs | 40 ++- crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/flake8_pyi/mod.rs | 2 + crates/ruff/src/rules/flake8_pyi/rules/mod.rs | 2 + .../rules/unnecessary_literal_union.rs | 120 +++++++++ ...__flake8_pyi__tests__PYI030_PYI030.py.snap | 4 + ..._flake8_pyi__tests__PYI030_PYI030.pyi.snap | 233 ++++++++++++++++++ ruff.schema.json | 1 + 10 files changed, 504 insertions(+), 9 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI030.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI030.pyi create mode 100644 crates/ruff/src/rules/flake8_pyi/rules/unnecessary_literal_union.rs create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI030_PYI030.py.snap create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI030_PYI030.pyi.snap diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI030.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI030.py new file mode 100644 index 0000000000..cc199f1480 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI030.py @@ -0,0 +1,24 @@ +from typing import Literal +# Shouldn't emit for any cases in the non-stub file for compatibility with flake8-pyi. +# Note that this rule could be applied here in the future. + +field1: Literal[1] # OK +field2: Literal[1] | Literal[2] # OK + +def func1(arg1: Literal[1] | Literal[2]): # OK + print(arg1) + + +def func2() -> Literal[1] | Literal[2]: # OK + return "my Literal[1]ing" + + +field3: Literal[1] | Literal[2] | str # OK +field4: str | Literal[1] | Literal[2] # OK +field5: Literal[1] | str | Literal[2] # OK +field6: Literal[1] | bool | Literal[2] | str # OK +field7 = Literal[1] | Literal[2] # OK +field8: Literal[1] | (Literal[2] | str) # OK +field9: Literal[1] | (Literal[2] | str) # OK +field10: (Literal[1] | str) | Literal[2] # OK +field11: dict[Literal[1] | Literal[2], str] # OK diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI030.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI030.pyi new file mode 100644 index 0000000000..e92af925df --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI030.pyi @@ -0,0 +1,86 @@ +import typing +import typing_extensions +from typing import Literal + +# Shouldn't affect non-union field types. +field1: Literal[1] # OK + +# Should emit for duplicate field types. +field2: Literal[1] | Literal[2] # Error + +# Should emit for union types in arguments. +def func1(arg1: Literal[1] | Literal[2]): # Error + print(arg1) + + +# Should emit for unions in return types. +def func2() -> Literal[1] | Literal[2]: # Error + return "my Literal[1]ing" + + +# Should emit in longer unions, even if not directly adjacent. +field3: Literal[1] | Literal[2] | str # Error +field4: str | Literal[1] | Literal[2] # Error +field5: Literal[1] | str | Literal[2] # Error +field6: Literal[1] | bool | Literal[2] | str # Error + +# Should emit for non-type unions. +field7 = Literal[1] | Literal[2] # Error + +# Should emit for parenthesized unions. +field8: Literal[1] | (Literal[2] | str) # Error + +# Should handle user parentheses when fixing. +field9: Literal[1] | (Literal[2] | str) # Error +field10: (Literal[1] | str) | Literal[2] # Error + +# Should emit for union in generic parent type. +field11: dict[Literal[1] | Literal[2], str] # Error + +# Should emit for unions with more than two cases +field12: Literal[1] | Literal[2] | Literal[3] # Error +field13: Literal[1] | Literal[2] | Literal[3] | Literal[4] # Error + +# Should emit for unions with more than two cases, even if not directly adjacent +field14: Literal[1] | Literal[2] | str | Literal[3] # Error + +# Should emit for unions with mixed literal internal types +field15: Literal[1] | Literal["foo"] | Literal[True] # Error + +# Shouldn't emit for duplicate field types with same value; covered by Y016 +field16: Literal[1] | Literal[1] # OK + +# Shouldn't emit if in new parent type +field17: Literal[1] | dict[Literal[2], str] # OK + +# Shouldn't emit if not in a union parent +field18: dict[Literal[1], Literal[2]] # OK + +# Should respect name of literal type used +field19: typing.Literal[1] | typing.Literal[2] # Error + +# Should emit in cases with newlines +field20: typing.Union[ + Literal[ + 1 # test + ], + Literal[2], +] # Error, newline and comment will not be emitted in message + +# Should handle multiple unions with multiple members +field21: Literal[1, 2] | Literal[3, 4] # Error + +# Should emit in cases with `typing.Union` instead of `|` +field22: typing.Union[Literal[1], Literal[2]] # Error + +# Should emit in cases with `typing_extensions.Literal` +field23: typing_extensions.Literal[1] | typing_extensions.Literal[2] # Error + +# Should emit in cases with nested `typing.Union` +field24: typing.Union[Literal[1], typing.Union[Literal[2], str]] # Error + +# Should emit in cases with mixed `typing.Union` and `|` +field25: typing.Union[Literal[1], Literal[2] | str] # Error + +# Should emit only once in cases with multiple nested `typing.Union` +field24: typing.Union[Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]]] # Error diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 606d8a2ef8..be8aa68af6 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -2189,6 +2189,22 @@ where } } + // Ex) Union[...] + if self.enabled(Rule::UnnecessaryLiteralUnion) { + let mut check = true; + + // Avoid duplicate checks if the parent is an `Union[...]` + if let Some(Expr::Subscript(ast::ExprSubscript { value, .. })) = + self.semantic.expr_grandparent() + { + check = !self.semantic.match_typing_expr(value, "Union"); + } + + if check { + flake8_pyi::rules::unnecessary_literal_union(self, expr); + } + } + if self.semantic.match_typing_expr(value, "Literal") { self.semantic.flags |= SemanticModelFlags::LITERAL; } @@ -3136,18 +3152,24 @@ where if self.is_stub { if self.enabled(Rule::DuplicateUnionMember) && self.semantic.in_type_definition() - && self.semantic.expr_parent().map_or(true, |parent| { - !matches!( - parent, - Expr::BinOp(ast::ExprBinOp { - op: Operator::BitOr, - .. - }) - ) - }) + // Avoid duplicate checks if the parent is an `|` + && !matches!( + self.semantic.expr_parent(), + Some(Expr::BinOp(ast::ExprBinOp { op: Operator::BitOr, ..})) + ) { flake8_pyi::rules::duplicate_union_member(self, expr); } + + if self.enabled(Rule::UnnecessaryLiteralUnion) + // Avoid duplicate checks if the parent is an `|` + && !matches!( + self.semantic.expr_parent(), + Some(Expr::BinOp(ast::ExprBinOp { op: Operator::BitOr, ..})) + ) + { + flake8_pyi::rules::unnecessary_literal_union(self, expr); + } } } Expr::UnaryOp(ast::ExprUnaryOp { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index bb6081db96..b73d2caa44 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -630,6 +630,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Pyi, "024") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::CollectionsNamedTuple), (Flake8Pyi, "025") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnaliasedCollectionsAbcSetImport), (Flake8Pyi, "029") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::StrOrReprDefinedInStub), + (Flake8Pyi, "030") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnnecessaryLiteralUnion), (Flake8Pyi, "032") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::AnyEqNeAnnotation), (Flake8Pyi, "033") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::TypeCommentInStub), (Flake8Pyi, "034") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::NonSelfReturnType), diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index 5575a7ced5..dda789c12a 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -52,6 +52,8 @@ mod tests { #[test_case(Rule::UnassignedSpecialVariableInStub, Path::new("PYI035.pyi"))] #[test_case(Rule::StrOrReprDefinedInStub, Path::new("PYI029.py"))] #[test_case(Rule::StrOrReprDefinedInStub, Path::new("PYI029.pyi"))] + #[test_case(Rule::UnnecessaryLiteralUnion, Path::new("PYI030.py"))] + #[test_case(Rule::UnnecessaryLiteralUnion, Path::new("PYI030.pyi"))] #[test_case(Rule::StubBodyMultipleStatements, Path::new("PYI048.py"))] #[test_case(Rule::StubBodyMultipleStatements, Path::new("PYI048.pyi"))] #[test_case(Rule::TSuffixedTypeAlias, Path::new("PYI043.py"))] diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index 612055db37..ce083d31c3 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 stub_body_multiple_statements::*; pub(crate) use type_alias_naming::*; pub(crate) use type_comment_in_stub::*; pub(crate) use unaliased_collections_abc_set_import::*; +pub(crate) use unnecessary_literal_union::*; pub(crate) use unrecognized_platform::*; pub(crate) use unrecognized_version_info::*; @@ -49,5 +50,6 @@ mod stub_body_multiple_statements; mod type_alias_naming; mod type_comment_in_stub; mod unaliased_collections_abc_set_import; +mod unnecessary_literal_union; mod unrecognized_platform; mod unrecognized_version_info; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/unnecessary_literal_union.rs b/crates/ruff/src/rules/flake8_pyi/rules/unnecessary_literal_union.rs new file mode 100644 index 0000000000..6d5735ac32 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/unnecessary_literal_union.rs @@ -0,0 +1,120 @@ +use ruff_python_semantic::SemanticModel; +use rustpython_parser::ast::{self, Expr, Operator, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use smallvec::SmallVec; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for the presence of multiple literal types in a union. +/// +/// ## Why is this bad? +/// Literal types accept multiple arguments and it is clearer to specify them +/// as a single literal. +/// +/// ## Example +/// ```python +/// from typing import Literal +/// +/// field: Literal[1] | Literal[2] +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import Literal +/// +/// field: Literal[1, 2] +/// ``` +#[violation] +pub struct UnnecessaryLiteralUnion { + members: Vec, +} + +impl Violation for UnnecessaryLiteralUnion { + #[derive_message_formats] + fn message(&self) -> String { + format!( + "Multiple literal members in a union. Use a single literal, e.g. `Literal[{}]`", + self.members.join(", ") + ) + } +} + +/// PYI030 +pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Expr) { + let mut literal_exprs = SmallVec::<[&Box; 1]>::new(); + + // Adds a member to `literal_exprs` if it is a `Literal` annotation + let mut collect_literal_expr = |expr: &'a Expr| { + if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr { + if checker.semantic().match_typing_expr(value, "Literal") { + literal_exprs.push(slice); + } + } + }; + + // Traverse the union, collect all literal members + traverse_union(&mut collect_literal_expr, expr, checker.semantic()); + + // Raise a violation if more than one + if literal_exprs.len() > 1 { + let diagnostic = Diagnostic::new( + UnnecessaryLiteralUnion { + members: literal_exprs + .into_iter() + .map(|literal_expr| checker.locator.slice(literal_expr.range()).to_string()) + .collect(), + }, + expr.range(), + ); + + checker.diagnostics.push(diagnostic); + } +} + +/// Traverse a "union" type annotation, calling `func` on each expression in the union. +fn traverse_union<'a, F>(func: &mut F, expr: &'a Expr, semantic: &SemanticModel) +where + F: FnMut(&'a Expr), +{ + // Ex) x | y + if let Expr::BinOp(ast::ExprBinOp { + op: Operator::BitOr, + left, + right, + range: _, + }) = expr + { + // The union data structure usually looks like this: + // a | b | c -> (a | b) | c + // + // However, parenthesized expressions can coerce it into any structure: + // a | (b | c) + // + // So we have to traverse both branches in order (left, then right), to report members + // in the order they appear in the source code. + + // Traverse the left then right arms + traverse_union(func, left, semantic); + traverse_union(func, right, semantic); + return; + } + + // Ex) Union[x, y] + if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr { + if semantic.match_typing_expr(value, "Union") { + if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() { + // Traverse each element of the tuple within the union recursively to handle cases + // such as `Union[..., Union[...]] + elts.iter() + .for_each(|elt| traverse_union(func, elt, semantic)); + return; + } + } + } + + // Otherwise, call the function on expression + func(expr); +} diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI030_PYI030.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI030_PYI030.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI030_PYI030.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__PYI030_PYI030.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI030_PYI030.pyi.snap new file mode 100644 index 0000000000..42a1e51719 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI030_PYI030.pyi.snap @@ -0,0 +1,233 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI030.pyi:9:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | + 8 | # Should emit for duplicate field types. + 9 | field2: Literal[1] | Literal[2] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +10 | +11 | # Should emit for union types in arguments. + | + +PYI030.pyi:12:17: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +11 | # Should emit for union types in arguments. +12 | def func1(arg1: Literal[1] | Literal[2]): # Error + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +13 | print(arg1) + | + +PYI030.pyi:17:16: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +16 | # Should emit for unions in return types. +17 | def func2() -> Literal[1] | Literal[2]: # Error + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +18 | return "my Literal[1]ing" + | + +PYI030.pyi:22:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +21 | # Should emit in longer unions, even if not directly adjacent. +22 | field3: Literal[1] | Literal[2] | str # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +23 | field4: str | Literal[1] | Literal[2] # Error +24 | field5: Literal[1] | str | Literal[2] # Error + | + +PYI030.pyi:23:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +21 | # Should emit in longer unions, even if not directly adjacent. +22 | field3: Literal[1] | Literal[2] | str # Error +23 | field4: str | Literal[1] | Literal[2] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +24 | field5: Literal[1] | str | Literal[2] # Error +25 | field6: Literal[1] | bool | Literal[2] | str # Error + | + +PYI030.pyi:24:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +22 | field3: Literal[1] | Literal[2] | str # Error +23 | field4: str | Literal[1] | Literal[2] # Error +24 | field5: Literal[1] | str | Literal[2] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +25 | field6: Literal[1] | bool | Literal[2] | str # Error + | + +PYI030.pyi:25:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +23 | field4: str | Literal[1] | Literal[2] # Error +24 | field5: Literal[1] | str | Literal[2] # Error +25 | field6: Literal[1] | bool | Literal[2] | str # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +26 | +27 | # Should emit for non-type unions. + | + +PYI030.pyi:28:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +27 | # Should emit for non-type unions. +28 | field7 = Literal[1] | Literal[2] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +29 | +30 | # Should emit for parenthesized unions. + | + +PYI030.pyi:31:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +30 | # Should emit for parenthesized unions. +31 | field8: Literal[1] | (Literal[2] | str) # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +32 | +33 | # Should handle user parentheses when fixing. + | + +PYI030.pyi:34:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +33 | # Should handle user parentheses when fixing. +34 | field9: Literal[1] | (Literal[2] | str) # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +35 | field10: (Literal[1] | str) | Literal[2] # Error + | + +PYI030.pyi:35:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +33 | # Should handle user parentheses when fixing. +34 | field9: Literal[1] | (Literal[2] | str) # Error +35 | field10: (Literal[1] | str) | Literal[2] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +36 | +37 | # Should emit for union in generic parent type. + | + +PYI030.pyi:38:15: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +37 | # Should emit for union in generic parent type. +38 | field11: dict[Literal[1] | Literal[2], str] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +39 | +40 | # Should emit for unions with more than two cases + | + +PYI030.pyi:41:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3]` + | +40 | # Should emit for unions with more than two cases +41 | field12: Literal[1] | Literal[2] | Literal[3] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +42 | field13: Literal[1] | Literal[2] | Literal[3] | Literal[4] # Error + | + +PYI030.pyi:42:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3, 4]` + | +40 | # Should emit for unions with more than two cases +41 | field12: Literal[1] | Literal[2] | Literal[3] # Error +42 | field13: Literal[1] | Literal[2] | Literal[3] | Literal[4] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +43 | +44 | # Should emit for unions with more than two cases, even if not directly adjacent + | + +PYI030.pyi:45:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3]` + | +44 | # Should emit for unions with more than two cases, even if not directly adjacent +45 | field14: Literal[1] | Literal[2] | str | Literal[3] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +46 | +47 | # Should emit for unions with mixed literal internal types + | + +PYI030.pyi:48:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, "foo", True]` + | +47 | # Should emit for unions with mixed literal internal types +48 | field15: Literal[1] | Literal["foo"] | Literal[True] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +49 | +50 | # Shouldn't emit for duplicate field types with same value; covered by Y016 + | + +PYI030.pyi:51:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 1]` + | +50 | # Shouldn't emit for duplicate field types with same value; covered by Y016 +51 | field16: Literal[1] | Literal[1] # OK + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +52 | +53 | # Shouldn't emit if in new parent type + | + +PYI030.pyi:60:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +59 | # Should respect name of literal type used +60 | field19: typing.Literal[1] | typing.Literal[2] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +61 | +62 | # Should emit in cases with newlines + | + +PYI030.pyi:63:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +62 | # Should emit in cases with newlines +63 | field20: typing.Union[ + | __________^ +64 | | Literal[ +65 | | 1 # test +66 | | ], +67 | | Literal[2], +68 | | ] # Error, newline and comment will not be emitted in message + | |_^ PYI030 +69 | +70 | # Should handle multiple unions with multiple members + | + +PYI030.pyi:71:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3, 4]` + | +70 | # Should handle multiple unions with multiple members +71 | field21: Literal[1, 2] | Literal[3, 4] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +72 | +73 | # Should emit in cases with `typing.Union` instead of `|` + | + +PYI030.pyi:74:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +73 | # Should emit in cases with `typing.Union` instead of `|` +74 | field22: typing.Union[Literal[1], Literal[2]] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +75 | +76 | # Should emit in cases with `typing_extensions.Literal` + | + +PYI030.pyi:77:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +76 | # Should emit in cases with `typing_extensions.Literal` +77 | field23: typing_extensions.Literal[1] | typing_extensions.Literal[2] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +78 | +79 | # Should emit in cases with nested `typing.Union` + | + +PYI030.pyi:80:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +79 | # Should emit in cases with nested `typing.Union` +80 | field24: typing.Union[Literal[1], typing.Union[Literal[2], str]] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +81 | +82 | # Should emit in cases with mixed `typing.Union` and `|` + | + +PYI030.pyi:83:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +82 | # Should emit in cases with mixed `typing.Union` and `|` +83 | field25: typing.Union[Literal[1], Literal[2] | str] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +84 | +85 | # Should emit only once in cases with multiple nested `typing.Union` + | + +PYI030.pyi:86:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3, 4]` + | +85 | # Should emit only once in cases with multiple nested `typing.Union` +86 | field24: typing.Union[Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]]] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 + | + + diff --git a/ruff.schema.json b/ruff.schema.json index e906e89423..0948038c0a 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2346,6 +2346,7 @@ "PYI025", "PYI029", "PYI03", + "PYI030", "PYI032", "PYI033", "PYI034", From 0f9d7283e7f31d0212f72b59785f9745caf7cfdb Mon Sep 17 00:00:00 2001 From: konsti Date: Fri, 7 Jul 2023 18:52:13 +0200 Subject: [PATCH 366/447] Add format-dev contributor docs (#5594) ## Summary This adds markdown-level docs for #5492 ## Test Plan n/a --------- Co-authored-by: Micha Reiser --- crates/ruff_python_formatter/README.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/ruff_python_formatter/README.md b/crates/ruff_python_formatter/README.md index b115eb1754..ab7c451e2c 100644 --- a/crates/ruff_python_formatter/README.md +++ b/crates/ruff_python_formatter/README.md @@ -241,18 +241,30 @@ 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 +### Checking entire projects -There are tree common problems with the formatter: The second formatting pass looks different than +It's possible to format an entire project: + +```shell +cargo run --bin ruff_dev -- format-dev --write my_project +``` + +This will format all files that `ruff check` would lint and computes the +[Jaccard index](https://en.wikipedia.org/wiki/Jaccard_index), a measure for how close the original +and formatted versions are. The Jaccard index is 1 if there were no changes at all, while 0 means +every line was changed. If you run this on a black formatted projects, this tells you how similar +the ruff formatter is to black for the given project, with our goal being as close to 1 as possible. + +There are three 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` +of these using the `--stability-check` option in the `format-dev` subcommand: 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 +cargo run --bin ruff_dev -- format-dev --stability-check crates/ruff/resources/test/cpython ``` It is also possible large number of repositories using ruff. This dataset is large (~60GB), so we @@ -261,7 +273,7 @@ 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 +cargo run --bin ruff_dev -- format-dev --stability-check --multi-project target/checkouts ``` ## The orphan rules and trait structure From 0b9af031fb8f51e7f9a72c31366bf5f87b90ca3c Mon Sep 17 00:00:00 2001 From: konsti Date: Fri, 7 Jul 2023 21:11:52 +0200 Subject: [PATCH 367/447] Format ExprIfExp (ternary operator) (#5597) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Format `ExprIfExp`, also known as the ternary operator or inline `if`. It can look like ```python a1 = 1 if True else 2 ``` but also ```python b1 = ( # We return "a" ... "a" # that's our True value # ... if this condition matches ... if True # that's our test # ... otherwise we return "b§ else "b" # that's our False value ) ``` This also fixes a visitor order bug. The jaccard index on django goes from 0.911 to 0.915. ## Test Plan I added fixtures without and with comments in strange places. --- .../ruff_python_ast/src/visitor/preorder.rs | 3 +- .../test/fixtures/ruff/expression/if.py | 33 ++++++ .../src/comments/placement.rs | 85 +++++++++++++- .../src/expression/expr_if_exp.rs | 35 ++++-- ...mpatibility@conditional_expression.py.snap | 99 +++++++--------- ...atibility@simple_cases__expression.py.snap | 111 ++++++------------ ...mpatibility@simple_cases__fmtonoff.py.snap | 48 ++------ ...mpatibility@simple_cases__function.py.snap | 42 +------ .../snapshots/format@expression__if.py.snap | 84 +++++++++++++ 9 files changed, 328 insertions(+), 212 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/if.py create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@expression__if.py.snap diff --git a/crates/ruff_python_ast/src/visitor/preorder.rs b/crates/ruff_python_ast/src/visitor/preorder.rs index 1595ab11ee..4a21e82bad 100644 --- a/crates/ruff_python_ast/src/visitor/preorder.rs +++ b/crates/ruff_python_ast/src/visitor/preorder.rs @@ -476,8 +476,9 @@ where orelse, range: _range, }) => { - visitor.visit_expr(test); + // `body if test else orelse` visitor.visit_expr(body); + visitor.visit_expr(test); visitor.visit_expr(orelse); } diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/if.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/if.py new file mode 100644 index 0000000000..ed8c1ab97b --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/if.py @@ -0,0 +1,33 @@ +a1 = 1 if True else 2 + +a2 = "this is a very long text that will make the group break to check that parentheses are added" if True else 2 + +# These comment should be kept in place +b1 = ( + # We return "a" ... + "a" # that's our True value + # ... if this condition matches ... + if True # that's our test + # ... otherwise we return "b" + else "b" # that's our False value +) + +# These only need to be stable, bonus is we also keep the order +c1 = ( + "a" # 1 + if # 2 + True # 3 + else # 4 + "b" # 5 +) +c2 = ( + "a" # 1 + # 2 + if # 3 + # 4 + True # 5 + # 6 + else # 7 + # 8 + "b" # 9 +) diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 44ac6b5195..207b9a68a8 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -1,8 +1,8 @@ use std::cmp::Ordering; -use ruff_text_size::TextRange; +use ruff_text_size::{TextLen, TextRange}; use rustpython_parser::ast; -use rustpython_parser::ast::{Expr, ExprSlice, Ranged}; +use rustpython_parser::ast::{Expr, ExprIfExp, ExprSlice, Ranged}; use ruff_python_ast::node::{AnyNodeRef, AstNode}; use ruff_python_ast::source_code::Locator; @@ -14,7 +14,9 @@ use crate::expression::expr_slice::{assign_comment_in_slice, ExprSliceCommentSec use crate::other::arguments::{ assign_argument_separator_comment_placement, find_argument_separators, }; -use crate::trivia::{first_non_trivia_token_rev, SimpleTokenizer, Token, TokenKind}; +use crate::trivia::{ + first_non_trivia_token, first_non_trivia_token_rev, SimpleTokenizer, Token, TokenKind, +}; /// Implements the custom comment placement logic. pub(super) fn place_comment<'a>( @@ -37,6 +39,7 @@ pub(super) fn place_comment<'a>( handle_dict_unpacking_comment, handle_slice_comments, handle_attribute_comment, + handle_expr_if_comment, ]; for handler in HANDLERS { comment = match handler(comment, locator) { @@ -1154,6 +1157,82 @@ fn handle_attribute_comment<'a>( } } +/// Assign comments between `if` and `test` and `else` and `orelse` as leading to the respective +/// node. +/// +/// ```python +/// x = ( +/// "a" +/// if # leading comment of `True` +/// True +/// else # leading comment of `"b"` +/// "b" +/// ) +/// ``` +/// +/// This placement ensures comments remain in their previous order. This an edge case that only +/// happens if the comments are in a weird position but it also doesn't hurt handling it. +fn handle_expr_if_comment<'a>( + comment: DecoratedComment<'a>, + locator: &Locator, +) -> CommentPlacement<'a> { + let Some(expr_if) = comment.enclosing_node().expr_if_exp() else { + return CommentPlacement::Default(comment); + }; + let ExprIfExp { + range: _, + test, + body, + orelse, + } = expr_if; + + if comment.line_position().is_own_line() { + return CommentPlacement::Default(comment); + } + + // Find the if and the else + let if_token = + find_only_token_str_in_range(TextRange::new(body.end(), test.start()), locator, "if"); + let else_token = + find_only_token_str_in_range(TextRange::new(test.end(), orelse.start()), locator, "else"); + + // Between `if` and `test` + if if_token.range.start() < comment.slice().start() && comment.slice().start() < test.start() { + return CommentPlacement::leading(test.as_ref().into(), comment); + } + + // Between `else` and `orelse` + if else_token.range.start() < comment.slice().start() + && comment.slice().start() < orelse.start() + { + return CommentPlacement::leading(orelse.as_ref().into(), comment); + } + + CommentPlacement::Default(comment) +} + +/// Looks for a multi char token in the range that contains no other tokens. `SimpleTokenizer` only +/// works with single char tokens so we check that we have the right token by string comparison. +fn find_only_token_str_in_range(range: TextRange, locator: &Locator, token_str: &str) -> Token { + let token = + first_non_trivia_token(range.start(), locator.contents()).expect("Expected a token"); + debug_assert!( + locator.after(token.start()).starts_with(token_str), + "expected a `{token_str}` token", + ); + debug_assert!( + SimpleTokenizer::new( + locator.contents(), + TextRange::new(token.start() + token_str.text_len(), range.end()) + ) + .skip_trivia() + .next() + .is_none(), + "Didn't expect any other token" + ); + token +} + /// Returns `true` if `right` is `Some` and `left` and `right` are referentially equal. fn are_same_optional<'a, T>(left: AnyNodeRef, right: Option) -> bool where diff --git a/crates/ruff_python_formatter/src/expression/expr_if_exp.rs b/crates/ruff_python_formatter/src/expression/expr_if_exp.rs index 980571d853..0a2ffcf584 100644 --- a/crates/ruff_python_formatter/src/expression/expr_if_exp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_if_exp.rs @@ -1,21 +1,42 @@ -use crate::comments::Comments; +use crate::comments::{leading_comments, Comments}; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; -use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{group, soft_line_break_or_space, space, text}; +use ruff_formatter::{format_args, write, Buffer, FormatResult}; use rustpython_parser::ast::ExprIfExp; #[derive(Default)] pub struct FormatExprIfExp; impl FormatNodeRule for FormatExprIfExp { - fn fmt_fields(&self, _item: &ExprIfExp, f: &mut PyFormatter) -> FormatResult<()> { + fn fmt_fields(&self, item: &ExprIfExp, f: &mut PyFormatter) -> FormatResult<()> { + let ExprIfExp { + range: _, + test, + body, + orelse, + } = item; + let comments = f.context().comments().clone(); + // We place `if test` and `else orelse` on a single line, so the `test` and `orelse` leading + // comments go on the line before the `if` or `else` instead of directly ahead `test` or + // `orelse` write!( f, - [not_yet_implemented_custom_text( - "NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false" - )] + [group(&format_args![ + body.format(), + soft_line_break_or_space(), + leading_comments(comments.leading_comments(test.as_ref())), + text("if"), + space(), + test.format(), + soft_line_break_or_space(), + leading_comments(comments.leading_comments(orelse.as_ref())), + text("else"), + space(), + orelse.format() + ])] ) } } 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 cc7b50679b..57ef1898fb 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,50 @@ +@@ -1,20 +1,16 @@ long_kwargs_single_line = my_function( foo="test, this is a sample value", - bar=( @@ -87,7 +87,9 @@ def something(): - if some_boolean_variable - else some_fallback_value_foo_bar_baz - ), -+ bar=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, ++ bar=some_long_value_name_foo_bar_baz ++ if some_boolean_variable ++ else some_fallback_value_foo_bar_baz, baz="hello, this is a another value", ) @@ -98,33 +100,13 @@ def something(): - if some_boolean_variable - else some_fallback_value_foo_bar_baz - ), -+ bar=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, ++ bar=some_long_value_name_foo_bar_baz ++ if some_boolean_variable ++ else some_fallback_value_foo_bar_baz, baz="hello, this is a another value", ) - imploding_kwargs = my_function( - foo="test, this is a sample value", -- bar=a if foo else b, -+ bar=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, - baz="hello, this is a another value", - ) - --imploding_line = 1 if 1 + 1 == 2 else 0 -+imploding_line = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false - --exploding_line = ( -- "hello this is a slightly long string" -- if some_long_value_name_foo_bar_baz -- else "this one is a little shorter" --) -+exploding_line = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false - - positional_argument_test( -- some_long_value_name_foo_bar_baz -- if some_boolean_variable -- else some_fallback_value_foo_bar_baz -+ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false - ) +@@ -40,11 +36,9 @@ def weird_default_argument( @@ -133,21 +115,15 @@ def something(): - if SOME_CONSTANT - else some_fallback_value_foo_bar_baz - ), -+ x=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false ++ x=some_long_value_name_foo_bar_baz ++ if SOME_CONSTANT ++ else some_fallback_value_foo_bar_baz, ): pass - --nested = ( -- "hello this is a slightly long string" -- if ( -- some_long_value_name_foo_bar_baz -- if nesting_test_expressions -- else some_fallback_value_foo_bar_baz -- ) -- else "this one is a little shorter" --) -+nested = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false +@@ -59,26 +53,14 @@ + else "this one is a little shorter" + ) -generator_expression = ( - ( @@ -174,13 +150,6 @@ def something(): ) - def something(): - clone._iterable_class = ( -- NamedValuesListIterable -- if named -- else FlatValuesListIterable if flat else ValuesListIterable -+ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false - ) ``` ## Ruff Output @@ -188,38 +157,58 @@ def something(): ```py long_kwargs_single_line = my_function( foo="test, this is a sample value", - bar=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + bar=some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz, baz="hello, this is a another value", ) multiline_kwargs_indented = my_function( foo="test, this is a sample value", - bar=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + bar=some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz, baz="hello, this is a another value", ) imploding_kwargs = my_function( foo="test, this is a sample value", - bar=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + bar=a if foo else b, baz="hello, this is a another value", ) -imploding_line = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false +imploding_line = 1 if 1 + 1 == 2 else 0 -exploding_line = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false +exploding_line = ( + "hello this is a slightly long string" + if some_long_value_name_foo_bar_baz + else "this one is a little shorter" +) positional_argument_test( - NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz ) def weird_default_argument( - x=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + x=some_long_value_name_foo_bar_baz + if SOME_CONSTANT + else some_fallback_value_foo_bar_baz, ): pass -nested = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false +nested = ( + "hello this is a slightly long string" + if ( + some_long_value_name_foo_bar_baz + if nesting_test_expressions + else some_fallback_value_foo_bar_baz + ) + else "this one is a little shorter" +) generator_expression = (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) @@ -234,7 +223,9 @@ def limit_offset_sql(self, low_mark, high_mark): def something(): clone._iterable_class = ( - NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + NamedValuesListIterable + if named + else FlatValuesListIterable if flat else ValuesListIterable ) ``` 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 729478a04b..b743d9c808 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 @@ -274,7 +274,7 @@ last_call() Name None True -@@ -31,33 +31,39 @@ +@@ -31,18 +31,15 @@ -1 ~int and not v1 ^ 123 + v2 | True (~int) and (not ((v1 ^ (123 + v2)) | True)) @@ -291,16 +291,6 @@ last_call() - "port1": port1_resource, - "port2": port2_resource, -}[port_id] --1 if True else 2 --str or None if True else str or bytes or None --(str or None) if True else (str or bytes or None) --str or None if (1 if True else 2) else str or bytes or None --(str or None) if (1 if True else 2) else (str or bytes or None) --( -- (super_long_variable_name or None) -- if (1 if super_long_test_name else 2) -- else (str or bytes or None) --) +lambda x: True +lambda x: True +lambda x: True @@ -308,25 +298,14 @@ last_call() +lambda x: True +manylambdas = lambda x: True +foo = lambda x: True -+NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -+NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -+NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -+NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -+NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -+(NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false) - {"2.7": dead, "3.7": (long_live or die_hard)} + 1 if True else 2 + str or None if True else str or bytes or None + (str or None) if True else (str or bytes or None) +@@ -57,7 +54,13 @@ {"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} {**a, **b, **c} --{"2.7", "3.6", "3.7", "3.8", "3.9", ("4.0" if gilectomy else "3.10")} + {"2.7", "3.6", "3.7", "3.8", "3.9", ("4.0" if gilectomy else "3.10")} -({"a": "b"}, (True or False), (+value), "string", b"bytes") or None -+{ -+ "2.7", -+ "3.6", -+ "3.7", -+ "3.8", -+ "3.9", -+ (NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false), -+} +( + {"a": "b"}, + (True or False), @@ -337,7 +316,7 @@ last_call() () (1,) (1, 2) -@@ -69,40 +75,37 @@ +@@ -69,40 +72,37 @@ 2, 3, ] @@ -396,7 +375,7 @@ last_call() Python3 > Python2 > COBOL Life is Life call() -@@ -115,10 +118,10 @@ +@@ -115,10 +115,10 @@ arg, another, kwarg="hey", @@ -410,7 +389,7 @@ last_call() call(**self.screen_kwargs) call(b, **self.screen_kwargs) lukasz.langa.pl -@@ -131,34 +134,28 @@ +@@ -131,34 +131,28 @@ tuple[str, ...] tuple[str, int, float, dict[str, int]] tuple[ @@ -458,26 +437,16 @@ last_call() numpy[0, :] numpy[:, i] numpy[0, :2] -@@ -172,20 +169,27 @@ +@@ -172,7 +166,7 @@ numpy[1 : c + 1, c] numpy[-(c + 1) :, d] numpy[:, l[-2]] -numpy[:, ::-1] +numpy[:, :: -1] numpy[np.newaxis, :] --(str or None) if (sys.version_info[0] > (3,)) else (str or bytes or None) -+NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + (str or None) if (sys.version_info[0] > (3,)) else (str or bytes or None) {"2.7": dead, "3.7": long_live or die_hard} --{"2.7", "3.6", "3.7", "3.8", "3.9", "4.0" if gilectomy else "3.10"} -+{ -+ "2.7", -+ "3.6", -+ "3.7", -+ "3.8", -+ "3.9", -+ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, -+} - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C] +@@ -181,11 +175,11 @@ (SomeName) SomeName (Good, Bad, Ugly) @@ -494,7 +463,7 @@ last_call() { "id": "1", "type": "type", -@@ -200,32 +204,22 @@ +@@ -200,32 +194,22 @@ c = 1 d = (1,) + a + (2,) e = (1,).count(1) @@ -537,7 +506,7 @@ last_call() Ø = set() authors.łukasz.say_thanks() mapping = { -@@ -237,29 +231,29 @@ +@@ -237,29 +221,33 @@ def gen(): @@ -564,7 +533,11 @@ last_call() -), "Short message" -assert parens is TooMany +print(*NOT_YET_IMPLEMENTED_ExprStarred) -+print(**NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false) ++print( ++ **{1: 3} ++ if False ++ else {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} ++) +print(*NOT_YET_IMPLEMENTED_ExprStarred) +NOT_YET_IMPLEMENTED_StmtAssert +NOT_YET_IMPLEMENTED_StmtAssert @@ -580,7 +553,7 @@ last_call() ... for i in call(): ... -@@ -328,13 +322,18 @@ +@@ -328,13 +316,18 @@ ): return True if ( @@ -602,7 +575,7 @@ last_call() ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n ): return True -@@ -342,7 +341,8 @@ +@@ -342,7 +335,8 @@ ~aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e @@ -659,23 +632,20 @@ lambda x: True lambda x: True manylambdas = lambda x: True foo = lambda x: True -NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -(NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false) +1 if True else 2 +str or None if True else str or bytes or None +(str or None) if True else (str or bytes or None) +str or None if (1 if True else 2) else str or bytes or None +(str or None) if (1 if True else 2) else (str or bytes or None) +( + (super_long_variable_name or None) + if (1 if super_long_test_name else 2) + else (str or bytes or None) +) {"2.7": dead, "3.7": (long_live or die_hard)} {"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} {**a, **b, **c} -{ - "2.7", - "3.6", - "3.7", - "3.8", - "3.9", - (NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false), -} +{"2.7", "3.6", "3.7", "3.8", "3.9", ("4.0" if gilectomy else "3.10")} ( {"a": "b"}, (True or False), @@ -790,16 +760,9 @@ numpy[-(c + 1) :, d] numpy[:, l[-2]] numpy[:, :: -1] numpy[np.newaxis, :] -NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false +(str or None) if (sys.version_info[0] > (3,)) else (str or bytes or None) {"2.7": dead, "3.7": long_live or die_hard} -{ - "2.7", - "3.6", - "3.7", - "3.8", - "3.9", - NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, -} +{"2.7", "3.6", "3.7", "3.8", "3.9", "4.0" if gilectomy else "3.10"} [1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C] (SomeName) SomeName @@ -861,7 +824,11 @@ async def f(): print(*NOT_YET_IMPLEMENTED_ExprStarred) -print(**NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false) +print( + **{1: 3} + if False + else {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} +) print(*NOT_YET_IMPLEMENTED_ExprStarred) NOT_YET_IMPLEMENTED_StmtAssert NOT_YET_IMPLEMENTED_StmtAssert 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 013a97677b..48167498b5 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 @@ -211,7 +211,7 @@ d={'a':1, # Comment 1 # Comment 2 -@@ -17,30 +16,54 @@ +@@ -17,30 +16,44 @@ # fmt: off def func_no_args(): @@ -268,35 +268,15 @@ d={'a':1, + + # fmt: on --def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): + def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): - offset = attr.ib(default=attr.Factory(lambda: _r.uniform(1, 2))) - assert task._cancel_stack[: len(old_stack)] == old_stack -+def spaces( -+ a=1, -+ b=(), -+ c=[], -+ d={}, -+ e=True, -+ f=-1, -+ g=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, -+ h="", -+ i=r"", -+): + offset = attr.ib(default=attr.Factory(lambda x: True)) + NOT_YET_IMPLEMENTED_StmtAssert def spaces_types( -@@ -50,7 +73,7 @@ - d: dict = {}, - e: bool = True, - f: int = -1, -- g: int = 1 if False else 2, -+ g: int = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, - h: str = "", - i: str = r"", - ): -@@ -63,55 +86,54 @@ +@@ -63,55 +76,54 @@ something = { # fmt: off @@ -371,7 +351,7 @@ d={'a':1, # fmt: on -@@ -132,10 +154,10 @@ +@@ -132,10 +144,10 @@ """Another known limitation.""" # fmt: on # fmt: off @@ -386,7 +366,7 @@ d={'a':1, # fmt: on # fmt: off # ...but comments still get reformatted even though they should not be -@@ -153,9 +175,7 @@ +@@ -153,9 +165,7 @@ ) ) # fmt: off @@ -397,7 +377,7 @@ d={'a':1, # fmt: on _type_comment_re = re.compile( r""" -@@ -178,7 +198,7 @@ +@@ -178,7 +188,7 @@ $ """, # fmt: off @@ -406,7 +386,7 @@ d={'a':1, # fmt: on ) -@@ -216,8 +236,7 @@ +@@ -216,8 +226,7 @@ xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5, ) # fmt: off @@ -476,17 +456,7 @@ def function_signature_stress_test( # fmt: on -def spaces( - a=1, - b=(), - c=[], - d={}, - e=True, - f=-1, - g=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, - h="", - i=r"", -): +def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): offset = attr.ib(default=attr.Factory(lambda x: True)) NOT_YET_IMPLEMENTED_StmtAssert @@ -498,7 +468,7 @@ def spaces_types( d: dict = {}, e: bool = True, f: int = -1, - g: int = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + g: int = 1 if False else 2, h: str = "", i: str = r"", ): 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 3536f09059..2ccef6cead 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 @@ -126,7 +126,7 @@ def __await__(): return (yield) if False: ... for i in range(10): -@@ -41,12 +40,22 @@ +@@ -41,12 +40,12 @@ debug: bool = False, **kwargs, ) -> str: @@ -134,35 +134,15 @@ def __await__(): return (yield) + return text[number : -1] --def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): + def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): - offset = attr.ib(default=attr.Factory(lambda: _r.uniform(10000, 200000))) - assert task._cancel_stack[: len(old_stack)] == old_stack -+def spaces( -+ a=1, -+ b=(), -+ c=[], -+ d={}, -+ e=True, -+ f=-1, -+ g=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, -+ h="", -+ i=r"", -+): + offset = attr.ib(default=attr.Factory(lambda x: True)) + NOT_YET_IMPLEMENTED_StmtAssert def spaces_types( -@@ -56,7 +65,7 @@ - d: dict = {}, - e: bool = True, - f: int = -1, -- g: int = 1 if False else 2, -+ g: int = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, - h: str = "", - i: str = r"", - ): -@@ -64,19 +73,17 @@ +@@ -64,19 +63,17 @@ def spaces2(result=_core.Value(None)): @@ -190,7 +170,7 @@ def __await__(): return (yield) def long_lines(): -@@ -135,14 +142,8 @@ +@@ -135,14 +132,8 @@ a, **kwargs, ) -> A: @@ -257,17 +237,7 @@ def function_signature_stress_test( return text[number : -1] -def spaces( - a=1, - b=(), - c=[], - d={}, - e=True, - f=-1, - g=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, - h="", - i=r"", -): +def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): offset = attr.ib(default=attr.Factory(lambda x: True)) NOT_YET_IMPLEMENTED_StmtAssert @@ -279,7 +249,7 @@ def spaces_types( d: dict = {}, e: bool = True, f: int = -1, - g: int = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + g: int = 1 if False else 2, h: str = "", i: str = r"", ): diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__if.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__if.py.snap new file mode 100644 index 0000000000..3b563f0454 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__if.py.snap @@ -0,0 +1,84 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/if.py +--- +## Input +```py +a1 = 1 if True else 2 + +a2 = "this is a very long text that will make the group break to check that parentheses are added" if True else 2 + +# These comment should be kept in place +b1 = ( + # We return "a" ... + "a" # that's our True value + # ... if this condition matches ... + if True # that's our test + # ... otherwise we return "b" + else "b" # that's our False value +) + +# These only need to be stable, bonus is we also keep the order +c1 = ( + "a" # 1 + if # 2 + True # 3 + else # 4 + "b" # 5 +) +c2 = ( + "a" # 1 + # 2 + if # 3 + # 4 + True # 5 + # 6 + else # 7 + # 8 + "b" # 9 +) +``` + +## Output +```py +a1 = 1 if True else 2 + +a2 = ( + "this is a very long text that will make the group break to check that parentheses are added" + if True + else 2 +) + +# These comment should be kept in place +b1 = ( + # We return "a" ... + "a" # that's our True value + # ... if this condition matches ... + if True # that's our test + # ... otherwise we return "b" + else "b" # that's our False value +) + +# These only need to be stable, bonus is we also keep the order +c1 = ( + "a" # 1 + # 2 + if True # 3 + # 4 + else "b" # 5 +) +c2 = ( + "a" # 1 + # 2 + # 3 + # 4 + if True # 5 + # 6 + # 7 + # 8 + else "b" # 9 +) +``` + + + From efe7c393d1eb6bfa60a9f6c9c8dbb0863858baf9 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Sat, 8 Jul 2023 12:33:18 +0200 Subject: [PATCH 368/447] Fix typos found by codespell (#5607) ## Summary Fix typos found by [codespell](https://github.com/codespell-project/codespell). I have left out `memoize` for now (see #5606). ## Test Plan CI tests. --- Cargo.toml | 2 +- _typos.toml | 1 - crates/ruff/src/rules/ruff/rules/unreachable.rs | 2 +- crates/ruff_formatter/src/format_extensions.rs | 2 +- crates/ruff_formatter/src/printer/mod.rs | 2 +- .../resources/test/fixtures/ruff/expression/dict.py | 2 +- .../resources/test/fixtures/ruff/expression/string.py | 2 +- .../test/fixtures/ruff/skip_magic_trailing_comma.py | 2 +- crates/ruff_python_formatter/src/lib.rs | 2 +- .../tests/snapshots/format@expression__dict.py.snap | 4 ++-- .../tests/snapshots/format@expression__string.py.snap | 6 +++--- .../snapshots/format@skip_magic_trailing_comma.py.snap | 6 +++--- 12 files changed, 16 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index de63ac83b6..bec1ee357d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ toml = { version = "0.7.2" } # v1.0.1 libcst = { git = "https://github.com/Instagram/LibCST.git", rev = "3cacca1a1029f05707e50703b49fe3dd860aa839", default-features = false } -# Please tag the RustPython version everytime you update its revision here and in fuzz/Cargo.toml +# Please tag the RustPython version every time you update its revision here and in fuzz/Cargo.toml # Tagging the version ensures that older ruff versions continue to build from source even when we rebase our RustPython fork. # Current tag: v0.0.7 ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" } diff --git a/_typos.toml b/_typos.toml index cf274a9fcf..6d9da48c39 100644 --- a/_typos.toml +++ b/_typos.toml @@ -2,7 +2,6 @@ extend-exclude = ["resources", "snapshots"] [default.extend-words] -trivias = "trivias" hel = "hel" whos = "whos" spawnve = "spawnve" diff --git a/crates/ruff/src/rules/ruff/rules/unreachable.rs b/crates/ruff/src/rules/ruff/rules/unreachable.rs index 8aede6a613..5c81acd9be 100644 --- a/crates/ruff/src/rules/ruff/rules/unreachable.rs +++ b/crates/ruff/src/rules/ruff/rules/unreachable.rs @@ -693,7 +693,7 @@ impl<'stmt> BasicBlocksBuilder<'stmt> { } } - /// Select the next block from `blocks` unconditonally. + /// Select the next block from `blocks` unconditionally. fn unconditional_next_block(&self, after: Option) -> NextBlock<'static> { if let Some(after) = after { return NextBlock::Always(after); diff --git a/crates/ruff_formatter/src/format_extensions.rs b/crates/ruff_formatter/src/format_extensions.rs index 317d9c1337..cde30df58a 100644 --- a/crates/ruff_formatter/src/format_extensions.rs +++ b/crates/ruff_formatter/src/format_extensions.rs @@ -42,7 +42,7 @@ pub trait MemoizeFormat { /// # fn main() -> FormatResult<()> { /// let normal = MyFormat::new(); /// - /// // Calls `format` for everytime the object gets formatted + /// // Calls `format` every time the object gets formatted /// assert_eq!( /// "Formatted 1 times. Formatted 2 times.", /// format!(SimpleFormatContext::default(), [normal, space(), normal])?.print()?.as_code() diff --git a/crates/ruff_formatter/src/printer/mod.rs b/crates/ruff_formatter/src/printer/mod.rs index df4608f733..25687b7631 100644 --- a/crates/ruff_formatter/src/printer/mod.rs +++ b/crates/ruff_formatter/src/printer/mod.rs @@ -701,7 +701,7 @@ struct PrinterState<'a> { verbatim_markers: Vec, group_modes: GroupModes, // Re-used queue to measure if a group fits. Optimisation to avoid re-allocating a new - // vec everytime a group gets measured + // vec every time a group gets measured fits_stack: Vec, fits_queue: Vec<&'a [FormatElement]>, } diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/dict.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/dict.py index a0631959db..b53ff5c6f3 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/dict.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/dict.py @@ -26,7 +26,7 @@ b } { - **a # comment before preceeding node's comma + **a # comment before preceding node's comma , # before ** # between diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py index f9c4fe8b1d..6767a2463e 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py @@ -92,7 +92,7 @@ if "Let's" "start" "with" "a" "simple" "example" "now repeat after me:" "I am co ( # leading - "a" # trailing part commen + "a" # trailing part comment # leading part comment diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py index 8f4c58789a..1c138f163d 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py @@ -18,5 +18,5 @@ "fifth entry", "sixt entry", "seventh entry", - "eigth entry", + "eighth entry", ) diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index dcdf449795..eebcf05664 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -76,7 +76,7 @@ where /// default implementation formats the dangling comments at the end of the node, which isn't ideal but ensures that /// no comments are dropped. /// - /// A node can have dangling comments if all its children are tokens or if all node childrens are optional. + /// A node can have dangling comments if all its children are tokens or if all node children are optional. fn fmt_dangling_comments(&self, node: &N, f: &mut PyFormatter) -> FormatResult<()> { dangling_node_comments(node).fmt(f) } 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 bfafb3e2e7..043bd0e952 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 @@ -32,7 +32,7 @@ b } { - **a # comment before preceeding node's comma + **a # comment before preceding node's comma , # before ** # between @@ -91,7 +91,7 @@ a = { } { - **a, # comment before preceeding node's comma + **a, # comment before preceding node's comma # before **b, # between } diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap index 7bdc05dbb5..53afe8b2a3 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap @@ -98,7 +98,7 @@ if "Let's" "start" "with" "a" "simple" "example" "now repeat after me:" "I am co ( # leading - "a" # trailing part commen + "a" # trailing part comment # leading part comment @@ -255,7 +255,7 @@ if ( ( # leading - "a" # trailing part commen + "a" # trailing part comment # leading part comment "b" # trailing second part comment # trailing @@ -403,7 +403,7 @@ if ( ( # leading - 'a' # trailing part commen + 'a' # trailing part comment # leading part comment 'b' # trailing second part comment # trailing diff --git a/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap index e035e69a1c..f3613924cc 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap @@ -24,7 +24,7 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic "fifth entry", "sixt entry", "seventh entry", - "eigth entry", + "eighth entry", ) ``` @@ -54,7 +54,7 @@ magic-trailing-comma = Respect "fifth entry", "sixt entry", "seventh entry", - "eigth entry", + "eighth entry", ) ``` @@ -80,7 +80,7 @@ magic-trailing-comma = Ignore "fifth entry", "sixt entry", "seventh entry", - "eigth entry", + "eighth entry", ) ``` From d0dae7e576b20ca93c95371bea7308de6ebeb801 Mon Sep 17 00:00:00 2001 From: konsti Date: Sat, 8 Jul 2023 16:54:49 +0200 Subject: [PATCH 369/447] Fix CI by downgrading to cargo insta 1.29.0 (#5589) Since the (implicit) update to cargo-insta 1.30, CI would pass even when the tests failed. This downgrades to cargo insta 1.29.0 and CI fails again when it should (which i can't show here, because CI needs to pass to merge this PR). I've improved the unreferenced snapshot handling in the process See https://github.com/mitsuhiko/insta/issues/392 --- .github/workflows/ci.yaml | 14 ++++++-------- crates/ruff_python_resolver/src/lib.rs | 25 +++++++++++++++++-------- ruff.schema.json | 1 + 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d85d5a79d9..da672bea87 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -54,21 +54,19 @@ jobs: - name: "Install Rust toolchain" run: rustup show - uses: Swatinem/rust-cache@v2 - - run: cargo install cargo-insta + # cargo insta 1.30.0 fails for some reason (https://github.com/mitsuhiko/insta/issues/392) + - run: cargo install cargo-insta@=1.29.0 - run: pip install black[d]==23.1.0 - name: "Run tests (Ubuntu)" if: ${{ matrix.os == 'ubuntu-latest' }} - run: | - cargo insta test --all --all-features --delete-unreferenced-snapshots - git diff --exit-code + run: cargo insta test --all --all-features --unreferenced reject - name: "Run tests (Windows)" if: ${{ matrix.os == 'windows-latest' }} shell: bash - run: | - cargo insta test --all --all-features - git diff --exit-code + # We can't reject unreferenced snapshots on windows because flake8_executable can't run on windows + run: cargo insta test --all --all-features - run: cargo test --package ruff_cli --test black_compatibility_test -- --ignored - # Skipped as it's currently broken. The resource were moved from the + # TODO: Skipped as it's currently broken. The resource were moved from the # ruff_cli to ruff crate, but this test was not updated. if: false # Check for broken links in the documentation. diff --git a/crates/ruff_python_resolver/src/lib.rs b/crates/ruff_python_resolver/src/lib.rs index c4ef690a39..52be653d6c 100644 --- a/crates/ruff_python_resolver/src/lib.rs +++ b/crates/ruff_python_resolver/src/lib.rs @@ -15,7 +15,6 @@ mod search; #[cfg(test)] mod tests { - use insta::assert_debug_snapshot; use std::fs::{create_dir_all, File}; use std::io::{self, Write}; use std::path::{Path, PathBuf}; @@ -129,6 +128,16 @@ mod tests { env_logger::builder().is_test(true).try_init().ok(); } + macro_rules! assert_debug_snapshot_normalize_paths { + ($value: ident) => {{ + // The debug representation for the backslash are two backslashes (escaping) + let $value = std::format!("{:#?}", $value).replace("\\\\", "/"); + // `insta::assert_snapshot` uses the debug representation of the string, which would + // be a single line containing `\n` + insta::assert_display_snapshot!($value); + }}; + } + #[test] fn partial_stub_file_exists() -> io::Result<()> { setup(); @@ -810,7 +819,7 @@ mod tests { }, ); - assert_debug_snapshot!(result); + assert_debug_snapshot_normalize_paths!(result); } #[test] @@ -831,7 +840,7 @@ mod tests { }, ); - assert_debug_snapshot!(result); + assert_debug_snapshot_normalize_paths!(result); } #[test] @@ -852,7 +861,7 @@ mod tests { }, ); - assert_debug_snapshot!(result); + assert_debug_snapshot_normalize_paths!(result); } #[test] @@ -873,7 +882,7 @@ mod tests { }, ); - assert_debug_snapshot!(result); + assert_debug_snapshot_normalize_paths!(result); } #[test] @@ -894,7 +903,7 @@ mod tests { }, ); - assert_debug_snapshot!(result); + assert_debug_snapshot_normalize_paths!(result); } #[test] @@ -915,7 +924,7 @@ mod tests { }, ); - assert_debug_snapshot!(result); + assert_debug_snapshot_normalize_paths!(result); } #[test] @@ -936,6 +945,6 @@ mod tests { }, ); - assert_debug_snapshot!(result); + assert_debug_snapshot_normalize_paths!(result); } } diff --git a/ruff.schema.json b/ruff.schema.json index 0948038c0a..f283f253b8 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2400,6 +2400,7 @@ "RUF011", "RUF012", "RUF013", + "RUF014", "RUF1", "RUF10", "RUF100", From a1c559eaa4ecf776b6d3a16bf1d7ecbe53867ccd Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 8 Jul 2023 11:05:05 -0400 Subject: [PATCH 370/447] Only run pyproject.toml lint rules when enabled (#5578) ## Summary I was testing some changes on Airflow, and I realized that we _always_ run the `pyproject.toml` validation rules, even if they're not enabled. This PR gates them behind the appropriate enablement flags. ## Test Plan - Ran: `cargo run -p ruff_cli -- check ../airflow -n`. Verified that no RUF200 violations were raised. - Run: `cargo run -p ruff_cli -- check ../airflow -n --select RUF200`. Verified that two RUF200 violations were raised. --- crates/ruff/src/pyproject_toml.rs | 51 ++++++++++++++++++------------ crates/ruff/src/registry.rs | 2 ++ crates/ruff/src/rules/ruff/mod.rs | 5 ++- crates/ruff_cli/src/diagnostics.rs | 17 +++++++--- 4 files changed, 50 insertions(+), 25 deletions(-) diff --git a/crates/ruff/src/pyproject_toml.rs b/crates/ruff/src/pyproject_toml.rs index cabdff1b39..a0cfeb961b 100644 --- a/crates/ruff/src/pyproject_toml.rs +++ b/crates/ruff/src/pyproject_toml.rs @@ -7,7 +7,9 @@ use ruff_diagnostics::Diagnostic; use ruff_python_ast::source_code::SourceFile; use crate::message::Message; +use crate::registry::Rule; use crate::rules::ruff::rules::InvalidPyprojectToml; +use crate::settings::Settings; use crate::IOError; /// Unlike [`pyproject_toml::PyProjectToml`], in our case `build_system` is also optional @@ -20,9 +22,11 @@ struct PyProjectToml { project: Option, } -pub fn lint_pyproject_toml(source_file: SourceFile) -> Result> { +pub fn lint_pyproject_toml(source_file: SourceFile, settings: &Settings) -> Result> { + let mut messages = vec![]; + let err = match toml::from_str::(source_file.source_text()) { - Ok(_) => return Ok(Vec::default()), + Ok(_) => return Ok(messages), Err(err) => err, }; @@ -32,17 +36,20 @@ pub fn lint_pyproject_toml(source_file: SourceFile) -> Result> { None => TextRange::default(), Some(range) => { let Ok(end) = TextSize::try_from(range.end) else { - let diagnostic = Diagnostic::new( - IOError { - message: "pyproject.toml is larger than 4GB".to_string(), - }, - TextRange::default(), - ); - return Ok(vec![Message::from_diagnostic( - diagnostic, - source_file, - TextSize::default(), - )]); + if settings.rules.enabled(Rule::IOError) { + let diagnostic = Diagnostic::new( + IOError { + message: "pyproject.toml is larger than 4GB".to_string(), + }, + TextRange::default(), + ); + messages.push(Message::from_diagnostic( + diagnostic, + source_file, + TextSize::default(), + )); + } + return Ok(messages); }; TextRange::new( // start <= end, so if end < 4GB follows start < 4GB @@ -52,11 +59,15 @@ pub fn lint_pyproject_toml(source_file: SourceFile) -> Result> { } }; - let toml_err = err.message().to_string(); - let diagnostic = Diagnostic::new(InvalidPyprojectToml { message: toml_err }, range); - Ok(vec![Message::from_diagnostic( - diagnostic, - source_file, - TextSize::default(), - )]) + if settings.rules.enabled(Rule::InvalidPyprojectToml) { + let toml_err = err.message().to_string(); + let diagnostic = Diagnostic::new(InvalidPyprojectToml { message: toml_err }, range); + messages.push(Message::from_diagnostic( + diagnostic, + source_file, + TextSize::default(), + )); + } + + Ok(messages) } diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 56e30db0ff..a6ac161b7b 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -252,6 +252,7 @@ pub enum LintSource { Imports, Noqa, Filesystem, + PyprojectToml, } impl Rule { @@ -259,6 +260,7 @@ impl Rule { /// physical lines). pub const fn lint_source(&self) -> LintSource { match self { + Rule::InvalidPyprojectToml => LintSource::PyprojectToml, Rule::UnusedNOQA => LintSource::Noqa, Rule::BlanketNOQA | Rule::BlanketTypeIgnore diff --git a/crates/ruff/src/rules/ruff/mod.rs b/crates/ruff/src/rules/ruff/mod.rs index a110328a24..336a65c7e1 100644 --- a/crates/ruff/src/rules/ruff/mod.rs +++ b/crates/ruff/src/rules/ruff/mod.rs @@ -203,7 +203,10 @@ mod tests { .join("pyproject.toml"); let contents = fs::read_to_string(path)?; let source_file = SourceFileBuilder::new("pyproject.toml", contents).finish(); - let messages = lint_pyproject_toml(source_file)?; + let messages = lint_pyproject_toml( + source_file, + &settings::Settings::for_rule(Rule::InvalidPyprojectToml), + )?; assert_messages!(snapshot, messages); Ok(()) } diff --git a/crates/ruff_cli/src/diagnostics.rs b/crates/ruff_cli/src/diagnostics.rs index fe638ad169..f994b7a055 100644 --- a/crates/ruff_cli/src/diagnostics.rs +++ b/crates/ruff_cli/src/diagnostics.rs @@ -127,11 +127,20 @@ pub(crate) fn lint_path( debug!("Checking: {}", path.display()); - // We have to special case this here since the python tokenizer doesn't work with toml + // We have to special case this here since the Python tokenizer doesn't work with TOML. if is_project_toml(path) { - let contents = std::fs::read_to_string(path)?; - let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish(); - let messages = lint_pyproject_toml(source_file)?; + let messages = if settings + .lib + .rules + .iter_enabled() + .any(|rule_code| rule_code.lint_source().is_pyproject_toml()) + { + let contents = std::fs::read_to_string(path)?; + let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish(); + lint_pyproject_toml(source_file, &settings.lib)? + } else { + vec![] + }; return Ok(Diagnostics { messages, ..Diagnostics::default() From 507961f27d54c84e431bcc7fc0b040b90ff51fa2 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 8 Jul 2023 12:37:55 -0400 Subject: [PATCH 371/447] Emit warnings for invalid `# noqa` directives (#5571) ## Summary This PR adds a `ParseError` type to the `noqa` parsing system to enable us to render useful warnings instead of silently failing when parsing `noqa` codes. For example, given `foo.py`: ```python # ruff: noqa: x # ruff: noqa foo # flake8: noqa: F401 import os # noqa: foo-bar ``` We would now output: ```console warning: Invalid `# noqa` directive on line 2: expected a comma-separated list of codes (e.g., `# noqa: F401, F841`). warning: Invalid `# noqa` directive on line 4: expected `:` followed by a comma-separated list of codes (e.g., `# noqa: F401, F841`). warning: Invalid `# noqa` directive on line 6: Flake8's blanket exemption does not support exempting specific codes. To exempt specific codes, use, e.g., `# ruff: noqa: F401, F841` instead. warning: Invalid `# noqa` directive on line 7: expected a comma-separated list of codes (e.g., `# noqa: F401, F841`). ``` There's one important behavior change here too. Right now, with Flake8, if you do `# flake8: noqa: F401`, Flake8 treats that as equivalent to `# flake8: noqa` -- it turns off _all_ diagnostics in the file, not just `F401`. Historically, we respected this... but, I think it's confusing. So we now raise a warning, and don't respect it at all. This will lead to errors in some projects, but I'd argue that right now, those directives are almost certainly behaving in an unintended way for users anyway. Closes https://github.com/astral-sh/ruff/issues/3339. --- crates/ruff/src/checkers/noqa.rs | 2 +- crates/ruff/src/noqa.rs | 150 +++++++++++++----- ...ff__noqa__tests__flake8_exemption_all.snap | 6 +- ...flake8_exemption_all_case_insensitive.snap | 6 +- ..._tests__flake8_exemption_all_no_space.snap | 6 +- ...__noqa__tests__flake8_exemption_codes.snap | 4 +- .../ruff__noqa__tests__noqa_all.snap | 14 +- ...oqa__tests__noqa_all_case_insensitive.snap | 14 +- ...noqa__tests__noqa_all_leading_comment.snap | 12 +- ...ff__noqa__tests__noqa_all_multi_space.snap | 12 +- .../ruff__noqa__tests__noqa_all_no_space.snap | 12 +- ...oqa__tests__noqa_all_trailing_comment.snap | 12 +- .../ruff__noqa__tests__noqa_code.snap | 20 +-- ...qa__tests__noqa_code_case_insensitive.snap | 20 +-- ...oqa__tests__noqa_code_leading_comment.snap | 18 ++- ...f__noqa__tests__noqa_code_multi_space.snap | 18 ++- ...ruff__noqa__tests__noqa_code_no_space.snap | 18 ++- ...qa__tests__noqa_code_trailing_comment.snap | 18 ++- .../ruff__noqa__tests__noqa_codes.snap | 22 +-- ...a__tests__noqa_codes_case_insensitive.snap | 22 +-- ...qa__tests__noqa_codes_leading_comment.snap | 20 +-- ...__noqa__tests__noqa_codes_multi_space.snap | 20 +-- ...uff__noqa__tests__noqa_codes_no_space.snap | 20 +-- ...a__tests__noqa_codes_trailing_comment.snap | 20 +-- ...ruff__noqa__tests__noqa_invalid_codes.snap | 11 +- ...ruff__noqa__tests__noqa_leading_space.snap | 20 +-- ...uff__noqa__tests__noqa_trailing_space.snap | 20 +-- ...ruff__noqa__tests__ruff_exemption_all.snap | 6 +- ...__ruff_exemption_all_case_insensitive.snap | 6 +- ...a__tests__ruff_exemption_all_no_space.snap | 6 +- ...ff__noqa__tests__ruff_exemption_codes.snap | 14 +- 31 files changed, 346 insertions(+), 223 deletions(-) diff --git a/crates/ruff/src/checkers/noqa.rs b/crates/ruff/src/checkers/noqa.rs index 2733ca0605..16cea1d7cb 100644 --- a/crates/ruff/src/checkers/noqa.rs +++ b/crates/ruff/src/checkers/noqa.rs @@ -23,7 +23,7 @@ pub(crate) fn check_noqa( settings: &Settings, ) -> Vec { // Identify any codes that are globally exempted (within the current file). - let exemption = FileExemption::try_extract(locator.contents(), comment_ranges); + let exemption = FileExemption::try_extract(locator.contents(), comment_ranges, locator); // Extract all `noqa` directives. let mut noqa_directives = NoqaDirectives::from_commented_ranges(comment_ranges, locator); diff --git a/crates/ruff/src/noqa.rs b/crates/ruff/src/noqa.rs index a34b73f57c..4f015ed268 100644 --- a/crates/ruff/src/noqa.rs +++ b/crates/ruff/src/noqa.rs @@ -1,4 +1,5 @@ use std::collections::BTreeMap; +use std::error::Error; use std::fmt::{Display, Write}; use std::fs; use std::ops::Add; @@ -39,7 +40,7 @@ pub(crate) enum Directive<'a> { impl<'a> Directive<'a> { /// Extract the noqa `Directive` from a line of Python source code. - pub(crate) fn try_extract(text: &'a str, offset: TextSize) -> Option { + pub(crate) fn try_extract(text: &'a str, offset: TextSize) -> Result, ParseError> { for mat in NOQA_MATCHER.find_iter(text) { let noqa_literal_start = mat.start(); @@ -62,7 +63,7 @@ impl<'a> Directive<'a> { // If the next character is `:`, then it's a list of codes. Otherwise, it's a directive // to ignore all rules. let noqa_literal_end = mat.end(); - return Some( + return Ok(Some( if text[noqa_literal_end..] .chars() .next() @@ -100,6 +101,11 @@ impl<'a> Directive<'a> { } } + // If we didn't identify any codes, warn. + if codes.is_empty() { + return Err(ParseError::MissingCodes); + } + let range = TextRange::new( TextSize::try_from(comment_start).unwrap(), TextSize::try_from(codes_end).unwrap(), @@ -119,10 +125,10 @@ impl<'a> Directive<'a> { range: range.add(offset), }) }, - ); + )); } - None + Ok(None) } /// Lex an individual rule code (e.g., `F401`). @@ -194,9 +200,9 @@ pub(crate) fn rule_is_ignored( let offset = noqa_line_for.resolve(offset); let line_range = locator.line_range(offset); match Directive::try_extract(locator.slice(line_range), line_range.start()) { - Some(Directive::All(_)) => true, - Some(Directive::Codes(Codes { codes, range: _ })) => includes(code, &codes), - None => false, + Ok(Some(Directive::All(_))) => true, + Ok(Some(Directive::Codes(Codes { codes, range: _ }))) => includes(code, &codes), + _ => false, } } @@ -212,26 +218,37 @@ pub(crate) enum FileExemption { impl FileExemption { /// Extract the [`FileExemption`] for a given Python source file, enumerating any rules that are /// globally ignored within the file. - pub(crate) fn try_extract(contents: &str, comment_ranges: &[TextRange]) -> Option { + pub(crate) fn try_extract( + contents: &str, + comment_ranges: &[TextRange], + locator: &Locator, + ) -> Option { let mut exempt_codes: Vec = vec![]; for range in comment_ranges { match ParsedFileExemption::try_extract(&contents[*range]) { - Some(ParsedFileExemption::All) => { + Err(err) => { + #[allow(deprecated)] + let line = locator.compute_line_index(range.start()); + warn!("Invalid `# noqa` directive on line {line}: {err}"); + } + Ok(Some(ParsedFileExemption::All)) => { return Some(Self::All); } - Some(ParsedFileExemption::Codes(codes)) => { + Ok(Some(ParsedFileExemption::Codes(codes))) => { exempt_codes.extend(codes.into_iter().filter_map(|code| { if let Ok(rule) = Rule::from_code(get_redirect_target(code).unwrap_or(code)) { Some(rule.noqa_code()) } else { - warn!("Invalid code provided to `# ruff: noqa`: {}", code); + #[allow(deprecated)] + let line = locator.compute_line_index(range.start()); + warn!("Invalid code provided to `# ruff: noqa` on line {line}: {code}"); None } })); } - None => {} + Ok(None) => {} } } @@ -256,33 +273,51 @@ enum ParsedFileExemption<'a> { impl<'a> ParsedFileExemption<'a> { /// Return a [`ParsedFileExemption`] for a given comment line. - fn try_extract(line: &'a str) -> Option { + fn try_extract(line: &'a str) -> Result, ParseError> { let line = Self::lex_whitespace(line); - let line = Self::lex_char(line, '#')?; + let Some(line) = Self::lex_char(line, '#') else { + return Ok(None); + }; let line = Self::lex_whitespace(line); if let Some(line) = Self::lex_flake8(line) { // Ex) `# flake8: noqa` let line = Self::lex_whitespace(line); - let line = Self::lex_char(line, ':')?; + let Some(line) = Self::lex_char(line, ':') else { + return Ok(None); + }; let line = Self::lex_whitespace(line); - Self::lex_noqa(line)?; - Some(Self::All) + let Some(line) = Self::lex_noqa(line) else { + return Ok(None); + }; + + // Guard against, e.g., `# flake8: noqa: F401`, which isn't supported by Flake8. + // Flake8 treats this as a blanket exemption, which is almost never the intent. + // Warn, but treat as a blanket exemption. + let line = Self::lex_whitespace(line); + if Self::lex_char(line, ':').is_some() { + return Err(ParseError::UnsupportedCodes); + } + + Ok(Some(Self::All)) } else if let Some(line) = Self::lex_ruff(line) { let line = Self::lex_whitespace(line); - let line = Self::lex_char(line, ':')?; + let Some(line) = Self::lex_char(line, ':') else { + return Ok(None); + }; + let line = Self::lex_whitespace(line); + let Some(line) = Self::lex_noqa(line) else { + return Ok(None); + }; let line = Self::lex_whitespace(line); - let line = Self::lex_noqa(line)?; - if line.is_empty() { + Ok(Some(if line.is_empty() { // Ex) `# ruff: noqa` - Some(Self::All) + Self::All } else { // Ex) `# ruff: noqa: F401, F841` - let line = Self::lex_whitespace(line); let Some(line) = Self::lex_char(line, ':') else { - warn!("Unexpected suffix on `noqa` directive: \"{line}\""); - return None; + return Err(ParseError::InvalidSuffix); }; let line = Self::lex_whitespace(line); @@ -301,10 +336,15 @@ impl<'a> ParsedFileExemption<'a> { } } - Some(Self::Codes(codes)) - } + // If we didn't identify any codes, warn. + if codes.is_empty() { + return Err(ParseError::MissingCodes); + } + + Self::Codes(codes) + })) } else { - None + Ok(None) } } @@ -380,6 +420,34 @@ impl<'a> ParsedFileExemption<'a> { } } +/// The result of an [`Importer::get_or_import_symbol`] call. +#[derive(Debug)] +pub(crate) enum ParseError { + /// The `noqa` directive was missing valid codes (e.g., `# noqa: unused-import` instead of `# noqa: F401`). + MissingCodes, + /// The `noqa` directive used an invalid suffix (e.g., `# noqa; F401` instead of `# noqa: F401`). + InvalidSuffix, + /// The `noqa` directive attempted to exempt specific codes in an unsupported location (e.g., + /// `# flake8: noqa: F401, F841` instead of `# ruff: noqa: F401, F841`). + UnsupportedCodes, +} + +impl Display for ParseError { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ParseError::MissingCodes => fmt.write_str("expected a comma-separated list of codes (e.g., `# noqa: F401, F841`)."), + ParseError::InvalidSuffix => { + fmt.write_str("expected `:` followed by a comma-separated list of codes (e.g., `# noqa: F401, F841`).") + } + ParseError::UnsupportedCodes => { + fmt.write_str("Flake8's blanket exemption does not support exempting specific codes. To exempt specific codes, use, e.g., `# ruff: noqa: F401, F841` instead.") + } + } + } +} + +impl Error for ParseError {} + /// Adds noqa comments to suppress all diagnostics of a file. pub(crate) fn add_noqa( path: &Path, @@ -413,7 +481,7 @@ fn add_noqa_inner( // Whether the file is exempted from all checks. // Codes that are globally exempted (within the current file). - let exemption = FileExemption::try_extract(locator.contents(), commented_ranges); + let exemption = FileExemption::try_extract(locator.contents(), commented_ranges, locator); let directives = NoqaDirectives::from_commented_ranges(commented_ranges, locator); // Mark any non-ignored diagnostics. @@ -583,14 +651,22 @@ impl<'a> NoqaDirectives<'a> { let mut directives = Vec::new(); for range in comment_ranges { - if let Some(directive) = Directive::try_extract(locator.slice(*range), range.start()) { - // noqa comments are guaranteed to be single line. - directives.push(NoqaDirectiveLine { - range: locator.line_range(range.start()), - directive, - matches: Vec::new(), - }); - }; + match Directive::try_extract(locator.slice(*range), range.start()) { + Err(err) => { + #[allow(deprecated)] + let line = locator.compute_line_index(range.start()); + warn!("Invalid `# noqa` directive on line {line}: {err}"); + } + Ok(Some(directive)) => { + // noqa comments are guaranteed to be single line. + directives.push(NoqaDirectiveLine { + range: locator.line_range(range.start()), + directive, + matches: Vec::new(), + }); + } + Ok(None) => {} + } } // Extend a mapping at the end of the file to also include the EOF token. @@ -836,7 +912,7 @@ mod tests { #[test] fn noqa_invalid_codes() { - let source = "# noqa: F401, unused-import, some other code"; + let source = "# noqa: unused-import, F401, some other code"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all.snap index 30642fbd1c..55b08ffd6f 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all.snap @@ -2,6 +2,8 @@ source: crates/ruff/src/noqa.rs expression: "ParsedFileExemption::try_extract(source)" --- -Some( - All, +Ok( + Some( + All, + ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_case_insensitive.snap index 30642fbd1c..55b08ffd6f 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_case_insensitive.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_case_insensitive.snap @@ -2,6 +2,8 @@ source: crates/ruff/src/noqa.rs expression: "ParsedFileExemption::try_extract(source)" --- -Some( - All, +Ok( + Some( + All, + ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_no_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_no_space.snap index 30642fbd1c..55b08ffd6f 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_no_space.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_no_space.snap @@ -2,6 +2,8 @@ source: crates/ruff/src/noqa.rs expression: "ParsedFileExemption::try_extract(source)" --- -Some( - All, +Ok( + Some( + All, + ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_codes.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_codes.snap index 30642fbd1c..9b5fede6df 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_codes.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_codes.snap @@ -2,6 +2,6 @@ source: crates/ruff/src/noqa.rs expression: "ParsedFileExemption::try_extract(source)" --- -Some( - All, +Err( + UnsupportedCodes, ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all.snap index 6d5ffa6fd8..806987e18b 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all.snap @@ -1,11 +1,13 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::try_extract(range, &locator)" +expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - All( - All { - range: 0..6, - }, +Ok( + Some( + All( + All { + range: 0..6, + }, + ), ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_case_insensitive.snap index 6d5ffa6fd8..806987e18b 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_case_insensitive.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_case_insensitive.snap @@ -1,11 +1,13 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::try_extract(range, &locator)" +expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - All( - All { - range: 0..6, - }, +Ok( + Some( + All( + All { + range: 0..6, + }, + ), ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_leading_comment.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_leading_comment.snap index e05a6cbb9d..bd4fea2744 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_leading_comment.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_leading_comment.snap @@ -2,10 +2,12 @@ source: crates/ruff/src/noqa.rs expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - All( - All { - range: 35..41, - }, +Ok( + Some( + All( + All { + range: 35..41, + }, + ), ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_multi_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_multi_space.snap index a7a81d2752..b7fa476cc1 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_multi_space.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_multi_space.snap @@ -2,10 +2,12 @@ source: crates/ruff/src/noqa.rs expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - All( - All { - range: 0..7, - }, +Ok( + Some( + All( + All { + range: 0..7, + }, + ), ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_no_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_no_space.snap index a6504f2ee0..b5dcc889b8 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_no_space.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_no_space.snap @@ -2,10 +2,12 @@ source: crates/ruff/src/noqa.rs expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - All( - All { - range: 0..5, - }, +Ok( + Some( + All( + All { + range: 0..5, + }, + ), ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_trailing_comment.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_trailing_comment.snap index 528d99278f..806987e18b 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_trailing_comment.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_trailing_comment.snap @@ -2,10 +2,12 @@ source: crates/ruff/src/noqa.rs expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - All( - All { - range: 0..6, - }, +Ok( + Some( + All( + All { + range: 0..6, + }, + ), ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code.snap index fd0bc5502d..bee06c0364 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code.snap @@ -1,14 +1,16 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::try_extract(range, &locator)" +expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - Codes( - Codes { - range: 0..12, - codes: [ - "F401", - ], - }, +Ok( + Some( + Codes( + Codes { + range: 0..12, + codes: [ + "F401", + ], + }, + ), ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_case_insensitive.snap index fd0bc5502d..bee06c0364 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_case_insensitive.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_case_insensitive.snap @@ -1,14 +1,16 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::try_extract(range, &locator)" +expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - Codes( - Codes { - range: 0..12, - codes: [ - "F401", - ], - }, +Ok( + Some( + Codes( + Codes { + range: 0..12, + codes: [ + "F401", + ], + }, + ), ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_leading_comment.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_leading_comment.snap index 1132885227..4577119d57 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_leading_comment.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_leading_comment.snap @@ -2,13 +2,15 @@ source: crates/ruff/src/noqa.rs expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - Codes( - Codes { - range: 35..47, - codes: [ - "F401", - ], - }, +Ok( + Some( + Codes( + Codes { + range: 35..47, + codes: [ + "F401", + ], + }, + ), ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_multi_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_multi_space.snap index e08e9a849a..4b6104870f 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_multi_space.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_multi_space.snap @@ -2,13 +2,15 @@ source: crates/ruff/src/noqa.rs expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - Codes( - Codes { - range: 0..13, - codes: [ - "F401", - ], - }, +Ok( + Some( + Codes( + Codes { + range: 0..13, + codes: [ + "F401", + ], + }, + ), ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_no_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_no_space.snap index 7c07294cc7..5f3e8124ff 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_no_space.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_no_space.snap @@ -2,13 +2,15 @@ source: crates/ruff/src/noqa.rs expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - Codes( - Codes { - range: 0..10, - codes: [ - "F401", - ], - }, +Ok( + Some( + Codes( + Codes { + range: 0..10, + codes: [ + "F401", + ], + }, + ), ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_trailing_comment.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_trailing_comment.snap index ff3293f0af..bee06c0364 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_trailing_comment.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_trailing_comment.snap @@ -2,13 +2,15 @@ source: crates/ruff/src/noqa.rs expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - Codes( - Codes { - range: 0..12, - codes: [ - "F401", - ], - }, +Ok( + Some( + Codes( + Codes { + range: 0..12, + codes: [ + "F401", + ], + }, + ), ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes.snap index 61c78604a1..65e429c7af 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes.snap @@ -1,15 +1,17 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::try_extract(range, &locator)" +expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - Codes( - Codes { - range: 0..18, - codes: [ - "F401", - "F841", - ], - }, +Ok( + Some( + Codes( + Codes { + range: 0..18, + codes: [ + "F401", + "F841", + ], + }, + ), ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_case_insensitive.snap index 61c78604a1..65e429c7af 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_case_insensitive.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_case_insensitive.snap @@ -1,15 +1,17 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::try_extract(range, &locator)" +expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - Codes( - Codes { - range: 0..18, - codes: [ - "F401", - "F841", - ], - }, +Ok( + Some( + Codes( + Codes { + range: 0..18, + codes: [ + "F401", + "F841", + ], + }, + ), ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_leading_comment.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_leading_comment.snap index d771ab548e..9f72b14ab0 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_leading_comment.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_leading_comment.snap @@ -2,14 +2,16 @@ source: crates/ruff/src/noqa.rs expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - Codes( - Codes { - range: 35..53, - codes: [ - "F401", - "F841", - ], - }, +Ok( + Some( + Codes( + Codes { + range: 35..53, + codes: [ + "F401", + "F841", + ], + }, + ), ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_multi_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_multi_space.snap index e76c0a28fd..c4e9c8643c 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_multi_space.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_multi_space.snap @@ -2,14 +2,16 @@ source: crates/ruff/src/noqa.rs expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - Codes( - Codes { - range: 0..20, - codes: [ - "F401", - "F841", - ], - }, +Ok( + Some( + Codes( + Codes { + range: 0..20, + codes: [ + "F401", + "F841", + ], + }, + ), ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_no_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_no_space.snap index 6c16281542..4c699e253f 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_no_space.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_no_space.snap @@ -2,14 +2,16 @@ source: crates/ruff/src/noqa.rs expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - Codes( - Codes { - range: 0..15, - codes: [ - "F401", - "F841", - ], - }, +Ok( + Some( + Codes( + Codes { + range: 0..15, + codes: [ + "F401", + "F841", + ], + }, + ), ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_trailing_comment.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_trailing_comment.snap index da2e044c73..65e429c7af 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_trailing_comment.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_trailing_comment.snap @@ -2,14 +2,16 @@ source: crates/ruff/src/noqa.rs expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - Codes( - Codes { - range: 0..18, - codes: [ - "F401", - "F841", - ], - }, +Ok( + Some( + Codes( + Codes { + range: 0..18, + codes: [ + "F401", + "F841", + ], + }, + ), ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_invalid_codes.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_invalid_codes.snap index ff3293f0af..deb7795314 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_invalid_codes.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_invalid_codes.snap @@ -2,13 +2,6 @@ source: crates/ruff/src/noqa.rs expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - Codes( - Codes { - range: 0..12, - codes: [ - "F401", - ], - }, - ), +Err( + MissingCodes, ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_leading_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_leading_space.snap index 85081b1a53..47f626e011 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_leading_space.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_leading_space.snap @@ -1,14 +1,16 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::try_extract(range, &locator)" +expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - Codes( - Codes { - range: 4..16, - codes: [ - "F401", - ], - }, +Ok( + Some( + Codes( + Codes { + range: 4..16, + codes: [ + "F401", + ], + }, + ), ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_trailing_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_trailing_space.snap index fd0bc5502d..bee06c0364 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_trailing_space.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_trailing_space.snap @@ -1,14 +1,16 @@ --- source: crates/ruff/src/noqa.rs -expression: "Directive::try_extract(range, &locator)" +expression: "Directive::try_extract(source, TextSize::default())" --- -Some( - Codes( - Codes { - range: 0..12, - codes: [ - "F401", - ], - }, +Ok( + Some( + Codes( + Codes { + range: 0..12, + codes: [ + "F401", + ], + }, + ), ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all.snap index 30642fbd1c..55b08ffd6f 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all.snap @@ -2,6 +2,8 @@ source: crates/ruff/src/noqa.rs expression: "ParsedFileExemption::try_extract(source)" --- -Some( - All, +Ok( + Some( + All, + ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_case_insensitive.snap index 30642fbd1c..55b08ffd6f 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_case_insensitive.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_case_insensitive.snap @@ -2,6 +2,8 @@ source: crates/ruff/src/noqa.rs expression: "ParsedFileExemption::try_extract(source)" --- -Some( - All, +Ok( + Some( + All, + ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_no_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_no_space.snap index 30642fbd1c..55b08ffd6f 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_no_space.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_no_space.snap @@ -2,6 +2,8 @@ source: crates/ruff/src/noqa.rs expression: "ParsedFileExemption::try_extract(source)" --- -Some( - All, +Ok( + Some( + All, + ), ) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_codes.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_codes.snap index cbf46f63ff..b45c9ca1ae 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_codes.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_codes.snap @@ -2,11 +2,13 @@ source: crates/ruff/src/noqa.rs expression: "ParsedFileExemption::try_extract(source)" --- -Some( - Codes( - [ - "F401", - "F841", - ], +Ok( + Some( + Codes( + [ + "F401", + "F841", + ], + ), ), ) From 456273a92e2cda02e7e6fc0b86ea1d4ab52655e7 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 8 Jul 2023 12:51:37 -0400 Subject: [PATCH 372/447] Support individual codes on `# flake8: noqa` directives (#5618) ## Summary We now treat `# flake8: noqa: F401` as turning off F401 for the entire file. (Flake8 treats this as turning off _all rules_ for the entire file). This deviates from Flake8, but I think it's a much more user-friendly deviation than what I introduced in #5571. See https://github.com/astral-sh/ruff/issues/5617 for an explanation. Closes https://github.com/astral-sh/ruff/issues/5617. --- crates/ruff/src/noqa.rs | 110 +++++++----------- ...__noqa__tests__flake8_exemption_codes.snap | 11 +- 2 files changed, 52 insertions(+), 69 deletions(-) diff --git a/crates/ruff/src/noqa.rs b/crates/ruff/src/noqa.rs index 4f015ed268..311bdd989e 100644 --- a/crates/ruff/src/noqa.rs +++ b/crates/ruff/src/noqa.rs @@ -21,6 +21,7 @@ use crate::codes::NoqaCode; use crate::registry::{AsRule, Rule, RuleSet}; use crate::rule_redirects::get_redirect_target; +// Let's replace this with a character-by-character matcher, I bet it's faster. static NOQA_MATCHER: Lazy = Lazy::new(|| { AhoCorasick::builder() .ascii_case_insensitive(true) @@ -280,72 +281,52 @@ impl<'a> ParsedFileExemption<'a> { }; let line = Self::lex_whitespace(line); - if let Some(line) = Self::lex_flake8(line) { - // Ex) `# flake8: noqa` - let line = Self::lex_whitespace(line); - let Some(line) = Self::lex_char(line, ':') else { - return Ok(None); - }; - let line = Self::lex_whitespace(line); - let Some(line) = Self::lex_noqa(line) else { - return Ok(None); - }; + let Some(line) = Self::lex_flake8(line).or_else(|| Self::lex_ruff(line)) else { + return Ok(None); + }; - // Guard against, e.g., `# flake8: noqa: F401`, which isn't supported by Flake8. - // Flake8 treats this as a blanket exemption, which is almost never the intent. - // Warn, but treat as a blanket exemption. + let line = Self::lex_whitespace(line); + let Some(line) = Self::lex_char(line, ':') else { + return Ok(None); + }; + let line = Self::lex_whitespace(line); + let Some(line) = Self::lex_noqa(line) else { + return Ok(None); + }; + let line = Self::lex_whitespace(line); + + Ok(Some(if line.is_empty() { + // Ex) `# ruff: noqa` + Self::All + } else { + // Ex) `# ruff: noqa: F401, F841` + let Some(line) = Self::lex_char(line, ':') else { + return Err(ParseError::InvalidSuffix); + }; let line = Self::lex_whitespace(line); - if Self::lex_char(line, ':').is_some() { - return Err(ParseError::UnsupportedCodes); + + // Extract the codes from the line (e.g., `F401, F841`). + let mut codes = vec![]; + let mut line = line; + while let Some(code) = Self::lex_code(line) { + codes.push(code); + line = &line[code.len()..]; + + // Codes can be comma- or whitespace-delimited. + if let Some(rest) = Self::lex_delimiter(line).map(Self::lex_whitespace) { + line = rest; + } else { + break; + } } - Ok(Some(Self::All)) - } else if let Some(line) = Self::lex_ruff(line) { - let line = Self::lex_whitespace(line); - let Some(line) = Self::lex_char(line, ':') else { - return Ok(None); - }; - let line = Self::lex_whitespace(line); - let Some(line) = Self::lex_noqa(line) else { - return Ok(None); - }; - let line = Self::lex_whitespace(line); + // If we didn't identify any codes, warn. + if codes.is_empty() { + return Err(ParseError::MissingCodes); + } - Ok(Some(if line.is_empty() { - // Ex) `# ruff: noqa` - Self::All - } else { - // Ex) `# ruff: noqa: F401, F841` - let Some(line) = Self::lex_char(line, ':') else { - return Err(ParseError::InvalidSuffix); - }; - let line = Self::lex_whitespace(line); - - // Extract the codes from the line (e.g., `F401, F841`). - let mut codes = vec![]; - let mut line = line; - while let Some(code) = Self::lex_code(line) { - codes.push(code); - line = &line[code.len()..]; - - // Codes can be comma- or whitespace-delimited. - if let Some(rest) = Self::lex_delimiter(line).map(Self::lex_whitespace) { - line = rest; - } else { - break; - } - } - - // If we didn't identify any codes, warn. - if codes.is_empty() { - return Err(ParseError::MissingCodes); - } - - Self::Codes(codes) - })) - } else { - Ok(None) - } + Self::Codes(codes) + })) } /// Lex optional leading whitespace. @@ -427,9 +408,6 @@ pub(crate) enum ParseError { MissingCodes, /// The `noqa` directive used an invalid suffix (e.g., `# noqa; F401` instead of `# noqa: F401`). InvalidSuffix, - /// The `noqa` directive attempted to exempt specific codes in an unsupported location (e.g., - /// `# flake8: noqa: F401, F841` instead of `# ruff: noqa: F401, F841`). - UnsupportedCodes, } impl Display for ParseError { @@ -439,9 +417,7 @@ impl Display for ParseError { ParseError::InvalidSuffix => { fmt.write_str("expected `:` followed by a comma-separated list of codes (e.g., `# noqa: F401, F841`).") } - ParseError::UnsupportedCodes => { - fmt.write_str("Flake8's blanket exemption does not support exempting specific codes. To exempt specific codes, use, e.g., `# ruff: noqa: F401, F841` instead.") - } + } } } diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_codes.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_codes.snap index 9b5fede6df..b45c9ca1ae 100644 --- a/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_codes.snap +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_codes.snap @@ -2,6 +2,13 @@ source: crates/ruff/src/noqa.rs expression: "ParsedFileExemption::try_extract(source)" --- -Err( - UnsupportedCodes, +Ok( + Some( + Codes( + [ + "F401", + "F841", + ], + ), + ), ) From 38fa305f358df8420f94ce62d072908010a38ec4 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 8 Jul 2023 15:05:44 -0400 Subject: [PATCH 373/447] Refactor isort directive skips to use iterators (#5623) ## Summary We're doing some unsafe accesses to advance these iterators. It's easier to model these as actual iterators to ensure safety everywhere. Also added some additional test cases. Closes #5621. --- .../resources/test/fixtures/isort/skip.py | 6 ++ .../resources/test/fixtures/isort/split.py | 10 +++ crates/ruff/src/rules/isort/block.rs | 76 +++++++++++-------- .../ruff__rules__isort__tests__skip.py.snap | 23 ++++++ .../ruff__rules__isort__tests__split.py.snap | 24 ++++++ 5 files changed, 109 insertions(+), 30 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/isort/skip.py b/crates/ruff/resources/test/fixtures/isort/skip.py index 5cf668879b..97d38fdb15 100644 --- a/crates/ruff/resources/test/fixtures/isort/skip.py +++ b/crates/ruff/resources/test/fixtures/isort/skip.py @@ -26,3 +26,9 @@ def f(): import os # isort:skip import collections import abc + + +def f(): + import sys; import os # isort:skip + import sys; import os # isort:skip # isort:skip + import sys; import os diff --git a/crates/ruff/resources/test/fixtures/isort/split.py b/crates/ruff/resources/test/fixtures/isort/split.py index e4beaec563..c82885853a 100644 --- a/crates/ruff/resources/test/fixtures/isort/split.py +++ b/crates/ruff/resources/test/fixtures/isort/split.py @@ -19,3 +19,13 @@ if True: import D import B + + +import e +import f + +# isort: split +# isort: split + +import d +import c diff --git a/crates/ruff/src/rules/isort/block.rs b/crates/ruff/src/rules/isort/block.rs index 73ee365186..e0eea8ae35 100644 --- a/crates/ruff/src/rules/isort/block.rs +++ b/crates/ruff/src/rules/isort/block.rs @@ -1,5 +1,7 @@ use ruff_text_size::{TextRange, TextSize}; use rustpython_parser::ast::{self, ExceptHandler, MatchCase, Ranged, Stmt}; +use std::iter::Peekable; +use std::slice; use ruff_python_ast::source_code::Locator; use ruff_python_ast::statement_visitor::StatementVisitor; @@ -30,8 +32,8 @@ pub(crate) struct BlockBuilder<'a> { locator: &'a Locator<'a>, is_stub: bool, blocks: Vec>, - splits: &'a [TextSize], - cell_offsets: Option<&'a [TextSize]>, + splits: Peekable>, + cell_offsets: Option>>, exclusions: &'a [TextRange], nested: bool, } @@ -47,12 +49,13 @@ impl<'a> BlockBuilder<'a> { locator, is_stub, blocks: vec![Block::default()], - splits: &directives.splits, + splits: directives.splits.iter().peekable(), exclusions: &directives.exclusions, nested: false, cell_offsets: source_kind .and_then(SourceKind::notebook) - .map(Notebook::cell_offsets), + .map(Notebook::cell_offsets) + .map(|offsets| offsets.iter().peekable()), } } @@ -126,45 +129,58 @@ where 'b: 'a, { fn visit_stmt(&mut self, stmt: &'b Stmt) { - // Track manual splits. - for (index, split) in self.splits.iter().enumerate() { - if stmt.start() >= *split { - self.finalize(self.trailer_for(stmt)); - self.splits = &self.splits[index + 1..]; - } else { - break; + // Track manual splits (e.g., `# isort: split`). + if self + .splits + .next_if(|split| stmt.start() >= **split) + .is_some() + { + // Skip any other splits that occur before the current statement, to support, e.g.: + // ```python + // # isort: split + // # isort: split + // import foo + // ``` + while self + .splits + .peek() + .map_or(false, |split| stmt.start() >= **split) + { + self.splits.next(); } + + self.finalize(self.trailer_for(stmt)); } // Track Jupyter notebook cell offsets as splits. This will make sure // that each cell is considered as an individual block to organize the // imports in. Thus, not creating an edit which spans across multiple // cells. - if let Some(cell_offsets) = self.cell_offsets { - for (index, split) in cell_offsets.iter().enumerate() { - if stmt.start() >= *split { - // We don't want any extra newlines between cells. - self.finalize(None); - self.cell_offsets = Some(&cell_offsets[index + 1..]); - } else { - break; + if let Some(cell_offsets) = self.cell_offsets.as_mut() { + if cell_offsets + .next_if(|cell_offset| stmt.start() >= **cell_offset) + .is_some() + { + // Skip any other cell offsets that occur before the current statement (e.g., in + // the case of multiple empty cells). + while cell_offsets + .peek() + .map_or(false, |split| stmt.start() >= **split) + { + cell_offsets.next(); } - } - } - // Test if the statement is in an excluded range - let mut is_excluded = false; - for (index, exclusion) in self.exclusions.iter().enumerate() { - if exclusion.end() < stmt.start() { - self.exclusions = &self.exclusions[index + 1..]; - } else { - is_excluded = exclusion.contains(stmt.start()); - break; + self.finalize(None); } } // Track imports. - if matches!(stmt, Stmt::Import(_) | Stmt::ImportFrom(_)) && !is_excluded { + if matches!(stmt, Stmt::Import(_) | Stmt::ImportFrom(_)) + && !self + .exclusions + .iter() + .any(|exclusion| exclusion.contains(stmt.start())) + { self.track_import(stmt); } else { self.finalize(self.trailer_for(stmt)); diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__skip.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__skip.py.snap index d3eb858cc5..498421b65b 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__skip.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__skip.py.snap @@ -31,6 +31,10 @@ skip.py:27:1: I001 [*] Import block is un-sorted or un-formatted 26 | import os # isort:skip 27 | / import collections 28 | | import abc +29 | | + | |_^ I001 +30 | +31 | def f(): | = help: Organize imports @@ -41,5 +45,24 @@ skip.py:27:1: I001 [*] Import block is un-sorted or un-formatted 27 |+ import abc 27 28 | import collections 28 |- import abc +29 29 | +30 30 | +31 31 | def f(): + +skip.py:34:1: I001 [*] Import block is un-sorted or un-formatted + | +32 | import sys; import os # isort:skip +33 | import sys; import os # isort:skip # isort:skip +34 | / import sys; import os + | + = help: Organize imports + +ℹ Fix +31 31 | def f(): +32 32 | import sys; import os # isort:skip +33 33 | import sys; import os # isort:skip # isort:skip +34 |- import sys; import os + 34 |+ import os + 35 |+ import sys diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__split.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__split.py.snap index 185696f70c..7556899b60 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__split.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__split.py.snap @@ -29,6 +29,10 @@ split.py:20:1: I001 [*] Import block is un-sorted or un-formatted 19 | 20 | / import D 21 | | import B +22 | | + | |_^ I001 +23 | +24 | import e | = help: Organize imports @@ -39,5 +43,25 @@ split.py:20:1: I001 [*] Import block is un-sorted or un-formatted 20 |+ import B 20 21 | import D 21 |- import B +22 22 | +23 23 | +24 24 | import e + +split.py:30:1: I001 [*] Import block is un-sorted or un-formatted + | +28 | # isort: split +29 | +30 | / import d +31 | | import c + | + = help: Organize imports + +ℹ Fix +27 27 | # isort: split +28 28 | # isort: split +29 29 | + 30 |+import c +30 31 | import d +31 |-import c From ac2e374a5ab23090755e0e1f7a72195e5c4cd32c Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Sun, 9 Jul 2023 11:56:31 +0100 Subject: [PATCH 374/447] Add `tkinter` import convention (#5626) ## Summary Adds `import tkinter as tk` to the list of default import conventions. Closes #5620. ## Test Plan Added `tkinter` to test fixture. `cargo test` --- .../flake8_import_conventions/defaults.py | 3 + .../flake8_import_conventions/settings.rs | 3 +- ...8_import_conventions__tests__defaults.snap | 80 ++++++++++++------- 3 files changed, 56 insertions(+), 30 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_import_conventions/defaults.py b/crates/ruff/resources/test/fixtures/flake8_import_conventions/defaults.py index 277b6ca10b..bbaf23ea44 100644 --- a/crates/ruff/resources/test/fixtures/flake8_import_conventions/defaults.py +++ b/crates/ruff/resources/test/fixtures/flake8_import_conventions/defaults.py @@ -5,15 +5,18 @@ import matplotlib.pyplot # unconventional import numpy # unconventional import pandas # unconventional import seaborn # unconventional +import tkinter # unconventional import altair as altr # unconventional import matplotlib.pyplot as plot # unconventional import numpy as nmp # unconventional import pandas as pdas # unconventional import seaborn as sbrn # unconventional +import tkinter as tkr # unconventional import altair as alt # conventional import matplotlib.pyplot as plt # conventional import numpy as np # conventional import pandas as pd # conventional import seaborn as sns # conventional +import tkinter as tk # conventional diff --git a/crates/ruff/src/rules/flake8_import_conventions/settings.rs b/crates/ruff/src/rules/flake8_import_conventions/settings.rs index 95a0fb7e2e..d5a038b54c 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/settings.rs +++ b/crates/ruff/src/rules/flake8_import_conventions/settings.rs @@ -13,6 +13,7 @@ const CONVENTIONAL_ALIASES: &[(&str, &str)] = &[ ("pandas", "pd"), ("seaborn", "sns"), ("tensorflow", "tf"), + ("tkinter", "tk"), ("holoviews", "hv"), ("panel", "pn"), ("plotly.express", "px"), @@ -31,7 +32,7 @@ const CONVENTIONAL_ALIASES: &[(&str, &str)] = &[ #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct Options { #[option( - default = r#"{"altair": "alt", "matplotlib": "mpl", "matplotlib.pyplot": "plt", "numpy": "np", "pandas": "pd", "seaborn": "sns", "tensorflow": "tf", "holoviews": "hv", "panel": "pn", "plotly.express": "px", "polars": "pl", "pyarrow": "pa"}"#, + default = r#"{"altair": "alt", "matplotlib": "mpl", "matplotlib.pyplot": "plt", "numpy": "np", "pandas": "pd", "seaborn": "sns", "tensorflow": "tf", "tkinter": "tk", "holoviews": "hv", "panel": "pn", "plotly.express": "px", "polars": "pl", "pyarrow": "pa"}"#, value_type = "dict[str, str]", example = r#" [tool.ruff.flake8-import-conventions.aliases] diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__defaults.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__defaults.snap index 42f1d32909..9197e9486a 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__defaults.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__defaults.snap @@ -40,6 +40,7 @@ defaults.py:6:8: ICN001 `pandas` should be imported as `pd` 6 | import pandas # unconventional | ^^^^^^ ICN001 7 | import seaborn # unconventional +8 | import tkinter # unconventional | = help: Alias `pandas` to `pd` @@ -49,62 +50,83 @@ defaults.py:7:8: ICN001 `seaborn` should be imported as `sns` 6 | import pandas # unconventional 7 | import seaborn # unconventional | ^^^^^^^ ICN001 -8 | -9 | import altair as altr # unconventional +8 | import tkinter # unconventional | = help: Alias `seaborn` to `sns` -defaults.py:9:18: ICN001 `altair` should be imported as `alt` +defaults.py:8:8: ICN001 `tkinter` should be imported as `tk` | + 6 | import pandas # unconventional 7 | import seaborn # unconventional - 8 | - 9 | import altair as altr # unconventional + 8 | import tkinter # unconventional + | ^^^^^^^ ICN001 + 9 | +10 | import altair as altr # unconventional + | + = help: Alias `tkinter` to `tk` + +defaults.py:10:18: ICN001 `altair` should be imported as `alt` + | + 8 | import tkinter # unconventional + 9 | +10 | import altair as altr # unconventional | ^^^^ ICN001 -10 | import matplotlib.pyplot as plot # unconventional -11 | import numpy as nmp # unconventional +11 | import matplotlib.pyplot as plot # unconventional +12 | import numpy as nmp # unconventional | = help: Alias `altair` to `alt` -defaults.py:10:29: ICN001 `matplotlib.pyplot` should be imported as `plt` +defaults.py:11:29: ICN001 `matplotlib.pyplot` should be imported as `plt` | - 9 | import altair as altr # unconventional -10 | import matplotlib.pyplot as plot # unconventional +10 | import altair as altr # unconventional +11 | import matplotlib.pyplot as plot # unconventional | ^^^^ ICN001 -11 | import numpy as nmp # unconventional -12 | import pandas as pdas # unconventional +12 | import numpy as nmp # unconventional +13 | import pandas as pdas # unconventional | = help: Alias `matplotlib.pyplot` to `plt` -defaults.py:11:17: ICN001 `numpy` should be imported as `np` +defaults.py:12:17: ICN001 `numpy` should be imported as `np` | - 9 | import altair as altr # unconventional -10 | import matplotlib.pyplot as plot # unconventional -11 | import numpy as nmp # unconventional +10 | import altair as altr # unconventional +11 | import matplotlib.pyplot as plot # unconventional +12 | import numpy as nmp # unconventional | ^^^ ICN001 -12 | import pandas as pdas # unconventional -13 | import seaborn as sbrn # unconventional +13 | import pandas as pdas # unconventional +14 | import seaborn as sbrn # unconventional | = help: Alias `numpy` to `np` -defaults.py:12:18: ICN001 `pandas` should be imported as `pd` +defaults.py:13:18: ICN001 `pandas` should be imported as `pd` | -10 | import matplotlib.pyplot as plot # unconventional -11 | import numpy as nmp # unconventional -12 | import pandas as pdas # unconventional +11 | import matplotlib.pyplot as plot # unconventional +12 | import numpy as nmp # unconventional +13 | import pandas as pdas # unconventional | ^^^^ ICN001 -13 | import seaborn as sbrn # unconventional +14 | import seaborn as sbrn # unconventional +15 | import tkinter as tkr # unconventional | = help: Alias `pandas` to `pd` -defaults.py:13:19: ICN001 `seaborn` should be imported as `sns` +defaults.py:14:19: ICN001 `seaborn` should be imported as `sns` | -11 | import numpy as nmp # unconventional -12 | import pandas as pdas # unconventional -13 | import seaborn as sbrn # unconventional +12 | import numpy as nmp # unconventional +13 | import pandas as pdas # unconventional +14 | import seaborn as sbrn # unconventional | ^^^^ ICN001 -14 | -15 | import altair as alt # conventional +15 | import tkinter as tkr # unconventional | = help: Alias `seaborn` to `sns` +defaults.py:15:19: ICN001 `tkinter` should be imported as `tk` + | +13 | import pandas as pdas # unconventional +14 | import seaborn as sbrn # unconventional +15 | import tkinter as tkr # unconventional + | ^^^ ICN001 +16 | +17 | import altair as alt # conventional + | + = help: Alias `tkinter` to `tk` + From 9dd05424c4384acccdd55d9d3ca7956c810737a8 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Mon, 10 Jul 2023 01:23:02 +0530 Subject: [PATCH 375/447] Update ecosystem script to account for 4 letter code (#5627) E.g., `PERF` --- scripts/check_ecosystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check_ecosystem.py b/scripts/check_ecosystem.py index a9904b305a..ff788f9edf 100755 --- a/scripts/check_ecosystem.py +++ b/scripts/check_ecosystem.py @@ -366,7 +366,7 @@ async def main( for line in diff_str.splitlines(): # Find rule change for current line or construction # + /::: - matches = re.search(r": ([A-Z]{1,3}[0-9]{3,4})", line) + matches = re.search(r": ([A-Z]{1,4}[0-9]{3,4})", line) if matches is None: # Handle case where there are no regex matches e.g. From 6a4b216362d87c7b66a3dbb83f21d1663ec3ad34 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Mon, 10 Jul 2023 01:23:27 +0530 Subject: [PATCH 376/447] Avoid `PERF401` if conditional depends on list var (#5603) ## Summary Avoid `PERF401` if conditional depends on list var ## Test Plan `cargo test` fixes: #5581 --- .../test/fixtures/perflint/PERF401.py | 8 ++++ .../rules/manual_list_comprehension.rs | 37 ++++++++++++++++--- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/perflint/PERF401.py b/crates/ruff/resources/test/fixtures/perflint/PERF401.py index 12b084e2f6..2c4434c8ed 100644 --- a/crates/ruff/resources/test/fixtures/perflint/PERF401.py +++ b/crates/ruff/resources/test/fixtures/perflint/PERF401.py @@ -37,3 +37,11 @@ def f(): result = {} for i in items: result[i].append(i) # OK + + +def f(): + items = [1, 2, 3, 4] + result = [] + for i in items: + if i not in result: + result.append(i) # OK 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 7e9607d591..b5bf5ea1ae 100644 --- a/crates/ruff/src/rules/perflint/rules/manual_list_comprehension.rs +++ b/crates/ruff/src/rules/perflint/rules/manual_list_comprehension.rs @@ -57,26 +57,28 @@ pub(crate) fn manual_list_comprehension(checker: &mut Checker, target: &Expr, bo return; }; - let (stmt, conditional) = match body { + let (stmt, if_test) = match body { // ```python // for x in y: // if z: // filtered.append(x) // ``` - [Stmt::If(ast::StmtIf { body, orelse, .. })] => { + [Stmt::If(ast::StmtIf { + body, orelse, test, .. + })] => { if !orelse.is_empty() { return; } let [stmt] = body.as_slice() else { return; }; - (stmt, true) + (stmt, Some(test)) } // ```python // for x in y: // filtered.append(f(x)) // ``` - [stmt] => (stmt, false), + [stmt] => (stmt, None), _ => return, }; @@ -103,7 +105,7 @@ pub(crate) fn manual_list_comprehension(checker: &mut Checker, target: &Expr, bo }; // Ignore direct list copies (e.g., `for x in y: filtered.append(x)`). - if !conditional { + if if_test.is_none() { if arg.as_name_expr().map_or(false, |arg| arg.id == *id) { return; } @@ -124,6 +126,31 @@ pub(crate) fn manual_list_comprehension(checker: &mut Checker, target: &Expr, bo return; } + // Avoid if the value is used in the conditional test, e.g., + // + // ```python + // for x in y: + // if x in filtered: + // filtered.append(x) + // ``` + // + // Converting this to a list comprehension would raise a `NameError` as + // `filtered` is not defined yet: + // + // ```python + // filtered = [x for x in y if x in filtered] + // ``` + if let Some(value_name) = value.as_name_expr() { + if if_test.map_or(false, |test| { + any_over_expr(test, &|expr| { + expr.as_name_expr() + .map_or(false, |expr| expr.id == value_name.id) + }) + }) { + return; + } + } + checker .diagnostics .push(Diagnostic::new(ManualListComprehension, *range)); From 401d172e47baa364e58585c04a3a3504c87466d4 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 9 Jul 2023 22:15:23 -0400 Subject: [PATCH 377/447] Use a simple match statement for case-insensitive noqa lookup (#5633) ## Summary It turns out that just doing this match directly without `AhoCorasick` is much faster, like 2x (and removes one dependency, though we likely already rely on this transitively). --- Cargo.lock | 1 - crates/ruff/Cargo.toml | 1 - crates/ruff/src/noqa.rs | 29 ++++++++++++++++------------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc1fa2a60b..541991d0fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1837,7 +1837,6 @@ dependencies = [ name = "ruff" version = "0.0.277" dependencies = [ - "aho-corasick 1.0.2", "annotate-snippets 0.9.1", "anyhow", "bitflags 2.3.3", diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index ad1372686c..09a8c50c71 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -27,7 +27,6 @@ ruff_rustpython = { path = "../ruff_rustpython" } ruff_text_size = { workspace = true } ruff_textwrap = { path = "../ruff_textwrap" } -aho-corasick = { version = "1.0.2" } annotate-snippets = { version = "0.9.1", features = ["color"] } anyhow = { workspace = true } bitflags = { workspace = true } diff --git a/crates/ruff/src/noqa.rs b/crates/ruff/src/noqa.rs index 311bdd989e..0660b36702 100644 --- a/crates/ruff/src/noqa.rs +++ b/crates/ruff/src/noqa.rs @@ -5,11 +5,9 @@ use std::fs; use std::ops::Add; use std::path::Path; -use aho_corasick::AhoCorasick; use anyhow::Result; use itertools::Itertools; use log::warn; -use once_cell::sync::Lazy; use ruff_text_size::{TextLen, TextRange, TextSize}; use rustpython_parser::ast::Ranged; @@ -21,14 +19,6 @@ use crate::codes::NoqaCode; use crate::registry::{AsRule, Rule, RuleSet}; use crate::rule_redirects::get_redirect_target; -// Let's replace this with a character-by-character matcher, I bet it's faster. -static NOQA_MATCHER: Lazy = Lazy::new(|| { - AhoCorasick::builder() - .ascii_case_insensitive(true) - .build(["noqa"]) - .unwrap() -}); - /// A directive to ignore a set of rules for a given line of Python source code (e.g., /// `# noqa: F401, F841`). #[derive(Debug)] @@ -42,8 +32,22 @@ pub(crate) enum Directive<'a> { impl<'a> Directive<'a> { /// Extract the noqa `Directive` from a line of Python source code. pub(crate) fn try_extract(text: &'a str, offset: TextSize) -> Result, ParseError> { - for mat in NOQA_MATCHER.find_iter(text) { - let noqa_literal_start = mat.start(); + for (char_index, char) in text.char_indices() { + // Only bother checking for the `noqa` literal if the character is `n` or `N`. + if !matches!(char, 'n' | 'N') { + continue; + } + + // Determine the start of the `noqa` literal. + if !matches!( + text[char_index..].as_bytes(), + [b'n' | b'N', b'o' | b'O', b'q' | b'Q', b'a' | b'A', ..] + ) { + continue; + } + + let noqa_literal_start = char_index; + let noqa_literal_end = noqa_literal_start + "noqa".len(); // Determine the start of the comment. let mut comment_start = noqa_literal_start; @@ -63,7 +67,6 @@ impl<'a> Directive<'a> { // If the next character is `:`, then it's a list of codes. Otherwise, it's a directive // to ignore all rules. - let noqa_literal_end = mat.end(); return Ok(Some( if text[noqa_literal_end..] .chars() From fa1341b0dbc76959f195e187940403e8b31027f5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 9 Jul 2023 22:24:46 -0400 Subject: [PATCH 378/447] Improve PERF203 example in docs (#5634) Closes #5624. --- .../perflint/rules/try_except_in_loop.rs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/ruff/src/rules/perflint/rules/try_except_in_loop.rs b/crates/ruff/src/rules/perflint/rules/try_except_in_loop.rs index 746668f311..44e3b0805d 100644 --- a/crates/ruff/src/rules/perflint/rules/try_except_in_loop.rs +++ b/crates/ruff/src/rules/perflint/rules/try_except_in_loop.rs @@ -27,20 +27,26 @@ use crate::settings::types::PythonVersion; /// /// ## Example /// ```python -/// for i in range(10): +/// string_numbers: list[str] = ["1", "2", "three", "4", "5"] +/// +/// int_numbers: list[int] = [] +/// for num in string_numbers: /// try: -/// print(i * i) -/// except: -/// break +/// int_numbers.append(int(num)) +/// except ValueError as e: +/// print(f"Couldn't convert to integer: {e}") /// ``` /// /// Use instead: /// ```python +/// string_numbers: list[str] = ["1", "2", "three", "4", "5"] +/// +/// int_numbers: list[int] = [] /// try: -/// for i in range(10): -/// print(i * i) -/// except: -/// break +/// for num in string_numbers: +/// int_numbers.append(int(num)) +/// except ValueError as e +/// print(f"Couldn't convert to integer: {e}") /// ``` /// /// ## Options From b4d6b7c2304f11d666ef6c175f2aac8c3cf7bb70 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 10 Jul 2023 05:24:57 +0300 Subject: [PATCH 379/447] docs: show nursery icon for nursery rules (#5439) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This changes the docs to show a nursery icon (🌅) for rules in the nursery. It currently doesn't do that for the rules that are in sub-categories (Pylint, Pycodestyle) because there is no `all_rules()` for the `RuleCodePrefix` that's returned by `UpstreamCategory` iteration (and as mentioned on Discord, I think `UpstreamCategory` maybe shouldn't be a thing). (That would be enabled by #5591.) ## Test Plan Generated docs to see new icons (with the caveat above). --- crates/ruff_dev/src/generate_rules_table.rs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/crates/ruff_dev/src/generate_rules_table.rs b/crates/ruff_dev/src/generate_rules_table.rs index 6f1b18cfea..3cfbd8dfec 100644 --- a/crates/ruff_dev/src/generate_rules_table.rs +++ b/crates/ruff_dev/src/generate_rules_table.rs @@ -8,17 +8,24 @@ use ruff::settings::options::Options; use ruff_diagnostics::AutofixKind; const FIX_SYMBOL: &str = "🛠"; +const NURSERY_SYMBOL: &str = "🌅"; fn generate_table(table_out: &mut String, rules: impl IntoIterator, linter: &Linter) { - table_out.push_str("| Code | Name | Message | Fix |"); + table_out.push_str("| Code | Name | Message | Status |"); table_out.push('\n'); - table_out.push_str("| ---- | ---- | ------- | --- |"); + table_out.push_str("| ---- | ---- | ------- | ------ |"); table_out.push('\n'); for rule in rules { let fix_token = match rule.autofixable() { AutofixKind::None => "", AutofixKind::Always | AutofixKind::Sometimes => FIX_SYMBOL, }; + let nursery_token = if rule.is_nursery() { + NURSERY_SYMBOL + } else { + "" + }; + let status_token = format!("{fix_token} {nursery_token}"); let rule_name = rule.as_ref(); @@ -32,7 +39,7 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator, .then_some(format_args!("[{rule_name}](rules/{rule_name}.md)")) .unwrap_or(format_args!("{rule_name}")), rule.message_formats()[0], - fix_token + status_token, )); table_out.push('\n'); } @@ -41,7 +48,10 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator, pub(crate) fn generate() -> String { // Generate the table string. - let mut table_out = format!("The {FIX_SYMBOL} emoji indicates that a rule is automatically fixable by the `--fix` command-line option.\n\n"); + let mut table_out = format!( + "The {FIX_SYMBOL} emoji indicates that a rule is automatically fixable by the `--fix` command-line option.\n\ + The {NURSERY_SYMBOL} emoji indicates that a rule is part of the nursery, a collection of newer lints that are still under development.\n\n" + ); for linter in Linter::iter() { let codes_csv: String = match linter.common_prefix() { "" => linter @@ -105,7 +115,7 @@ pub(crate) fn generate() -> String { generate_table(&mut table_out, prefix.clone().rules(), &linter); } } else { - generate_table(&mut table_out, linter.rules(), &linter); + generate_table(&mut table_out, linter.all_rules(), &linter); } } From 27011448eaca0d3cc552909f2d5f241fb2440b92 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 9 Jul 2023 22:35:34 -0400 Subject: [PATCH 380/447] Fix typo in complex-if-statement-in-stub message (#5635) --- .../flake8_pyi/rules/complex_if_statement_in_stub.rs | 2 +- ...ruff__rules__flake8_pyi__tests__PYI002_PYI002.pyi.snap | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) 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 index a8287ff671..2c7b1a5cfb 100644 --- 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 @@ -36,7 +36,7 @@ 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`" + "`if` test must be a simple comparison against `sys.platform` or `sys.version_info`" ) } } 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 103bef4bac..d5de9a5682 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,7 +1,7 @@ --- 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` +PYI002.pyi:3:4: PYI002 `if` test must be a simple comparison against `sys.platform` or `sys.version_info` | 1 | import sys 2 | @@ -11,7 +11,7 @@ PYI002.pyi:3:4: PYI002 `if`` test must be a simple comparison against `sys.platf 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` +PYI002.pyi:4:4: PYI002 `if` test must be a simple comparison against `sys.platform` or `sys.version_info` | 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 @@ -20,7 +20,7 @@ PYI002.pyi:4:4: PYI002 `if`` test must be a simple comparison against `sys.platf 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` +PYI002.pyi:5:4: PYI002 `if` test must be a simple comparison against `sys.platform` or `sys.version_info` | 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 @@ -29,7 +29,7 @@ PYI002.pyi:5:4: PYI002 `if`` test must be a simple comparison against `sys.platf 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` +PYI002.pyi:6:4: PYI002 `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 From eb69fe37bfc049b92f1604437f72165824df1ffd Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 9 Jul 2023 22:39:07 -0400 Subject: [PATCH 381/447] Render full-width tables in rules reference (#5636) --- docs/stylesheets/extra.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index cc60cb1295..2fca5ce9e6 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -84,3 +84,11 @@ [data-md-color-scheme="astral-dark"] img[src$="#gh-dark-mode-only"] { display: inline; /* Show dark images in dark mode */ } + +/* See: https://github.com/squidfunk/mkdocs-material/issues/175#issuecomment-616694465 */ +.md-typeset__table { + min-width: 100%; +} +.md-typeset table:not([class]) { + display: table; +} From c9d7c0d7d5d371c8658b65916ca99d704afefc4e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 9 Jul 2023 23:09:08 -0400 Subject: [PATCH 382/447] Add a link to the nursery; tweak icons (#5637) ## Summary We now always render the icons, but very faintly if inactive, and always right-align. This ensures consistent alignment as you scroll down the page: Screen Shot 2023-07-09 at 10 45 50 PM --- .../perflint/rules/try_except_in_loop.rs | 2 +- crates/ruff_dev/src/generate_rules_table.rs | 31 ++++++++++++------ docs/faq.md | 32 +++++++++++++++++++ 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/crates/ruff/src/rules/perflint/rules/try_except_in_loop.rs b/crates/ruff/src/rules/perflint/rules/try_except_in_loop.rs index 44e3b0805d..eeeed136ed 100644 --- a/crates/ruff/src/rules/perflint/rules/try_except_in_loop.rs +++ b/crates/ruff/src/rules/perflint/rules/try_except_in_loop.rs @@ -45,7 +45,7 @@ use crate::settings::types::PythonVersion; /// try: /// for num in string_numbers: /// int_numbers.append(int(num)) -/// except ValueError as e +/// except ValueError as e: /// print(f"Couldn't convert to integer: {e}") /// ``` /// diff --git a/crates/ruff_dev/src/generate_rules_table.rs b/crates/ruff_dev/src/generate_rules_table.rs index 3cfbd8dfec..b418851978 100644 --- a/crates/ruff_dev/src/generate_rules_table.rs +++ b/crates/ruff_dev/src/generate_rules_table.rs @@ -11,19 +11,21 @@ const FIX_SYMBOL: &str = "🛠"; const NURSERY_SYMBOL: &str = "🌅"; fn generate_table(table_out: &mut String, rules: impl IntoIterator, linter: &Linter) { - table_out.push_str("| Code | Name | Message | Status |"); + table_out.push_str("| Code | Name | Message | |"); table_out.push('\n'); - table_out.push_str("| ---- | ---- | ------- | ------ |"); + table_out.push_str("| ---- | ---- | ------- | ------: |"); table_out.push('\n'); for rule in rules { let fix_token = match rule.autofixable() { - AutofixKind::None => "", - AutofixKind::Always | AutofixKind::Sometimes => FIX_SYMBOL, + AutofixKind::Always | AutofixKind::Sometimes => { + format!("{FIX_SYMBOL}") + } + AutofixKind::None => format!("{FIX_SYMBOL}"), }; let nursery_token = if rule.is_nursery() { - NURSERY_SYMBOL + format!("{NURSERY_SYMBOL}") } else { - "" + format!("{NURSERY_SYMBOL}") }; let status_token = format!("{fix_token} {nursery_token}"); @@ -48,10 +50,19 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator, pub(crate) fn generate() -> String { // Generate the table string. - let mut table_out = format!( - "The {FIX_SYMBOL} emoji indicates that a rule is automatically fixable by the `--fix` command-line option.\n\ - The {NURSERY_SYMBOL} emoji indicates that a rule is part of the nursery, a collection of newer lints that are still under development.\n\n" - ); + let mut table_out = String::new(); + + table_out.push_str(&format!( + "The {FIX_SYMBOL} emoji indicates that a rule is automatically fixable by the `--fix` command-line option.")); + table_out.push('\n'); + table_out.push('\n'); + + table_out.push_str(&format!( + "The {NURSERY_SYMBOL} emoji indicates that a rule is part of the [\"nursery\"](../faq/#what-is-the-nursery)." + )); + table_out.push('\n'); + table_out.push('\n'); + for linter in Linter::iter() { let codes_csv: String = match linter.common_prefix() { "" => linter diff --git a/docs/faq.md b/docs/faq.md index 8d6e7b3652..3ce02b7f98 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -370,6 +370,38 @@ matter how they're provided, which avoids accidental incompatibilities and simpl By default, no `convention` is set, and so the enabled rules are determined by the `select` setting alone. +## What is the "nursery"? + +The "nursery" is a collection of newer rules that are considered experimental or unstable. + +If a rule is marked as part of the "nursery", it can only be enabled via direct selection. For +example, consider a hypothetical rule, `HYP001`. If `HYP001` were included in the "nursery", it +could be enabled by adding the following to your `pyproject.toml`: + +```toml +[tool.ruff] +extend-select = ["HYP001"] +``` + +However, it would _not_ be enabled by selecting the `HYP` category, like so: + +```toml +[tool.ruff] +extend-select = ["HYP"] +``` + +Similarly, it would _not_ be enabled via the `ALL` selector: + +```toml +[tool.ruff] +select = ["ALL"] +``` + +(The "nursery" terminology comes from [Clippy](https://doc.rust-lang.org/nightly/clippy/), a similar +tool for linting Rust code.) + +To see which rules are currently in the "nursery", visit the [rules reference](https://beta.ruff.rs/docs/rules/). + ## How can I tell what settings Ruff is using to check my code? Run `ruff check /path/to/code.py --show-settings` to view the resolved settings for a given file. From 52b22ceb6efd9e84668356e0cffa7d0f7d4d0bee Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Mon, 10 Jul 2023 09:25:26 +0530 Subject: [PATCH 383/447] Add links to ecosystem check result (#5631) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Add links for ecosystem check result. This is useful for developers to quickly check the added/removed violations with a single click. There are a few downsides of this approach: * Syntax highlighting is not available for the output * Content length is increased because of the additional anchor tags ## Test Plan `python scripts/check_ecosystem.py ./target/debug/ruff ../ruff-test/target/debug/ruff`
Example Output: ℹ️ ecosystem check **detected changes**. (+6, -0, 0 error(s))
airflow (+1, -0)

+ dev/breeze/src/airflow_breeze/commands/release_management_commands.py:654:25:
PERF401 Use a list comprehension to create a transformed list

bokeh (+3, -0)

+ src/bokeh/model/model.py:315:17:
PERF401 Use a list comprehension to create a transformed list
+ src/bokeh/resources.py:470:25:
PERF401 Use a list comprehension to create a transformed list
+ src/bokeh/sphinxext/bokeh_sampledata_xref.py:134:17:
PERF401 Use a list comprehension to create a transformed list

zulip (+2, -0)

+ zerver/actions/create_user.py:197:17:
PERF401 Use a list comprehension to create a transformed list
+ zerver/lib/markdown/__init__.py:2412:13:
PERF401 Use a list comprehension to create a transformed list

--------- Co-authored-by: konsti --- scripts/check_ecosystem.py | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/scripts/check_ecosystem.py b/scripts/check_ecosystem.py index ff788f9edf..59ebb9a056 100755 --- a/scripts/check_ecosystem.py +++ b/scripts/check_ecosystem.py @@ -82,6 +82,17 @@ class Repository(NamedTuple): yield Path(checkout_dir) + def url_for(self: Self, path: str, lnum: int | None = None) -> str: + """Return the GitHub URL for the given path and line number, if given.""" + # Default to main branch + url = ( + f"https://github.com/{self.org}/{self.repo}" + f"/blob/{self.ref or 'main'}/{path}" + ) + if lnum: + url += f"#L{lnum}" + return url + REPOSITORIES: list[Repository] = [ Repository("apache", "airflow", "main", select="ALL"), @@ -272,6 +283,11 @@ def read_projects_jsonl(projects_jsonl: Path) -> dict[tuple[str, str], Repositor return repositories +DIFF_LINE_RE = re.compile( + r"^(?P
[+-]) (?P(?P[^:]+):(?P\d+):\d+:) (?P.*)$",
+)
+
+
 T = TypeVar("T")
 
 
@@ -352,18 +368,27 @@ async def main(
                 print("

") print() - diff_str = "\n".join(diff) + repo = repositories[(org, repo)] + diff_lines = list(diff) - print("```diff") - print(diff_str) - print("```") + print("

")
+                for line in diff_lines:
+                    match = DIFF_LINE_RE.match(line)
+                    if match is None:
+                        print(line)
+                        continue
+
+                    pre, inner, path, lnum, post = match.groups()
+                    url = repo.url_for(path, int(lnum))
+                    print(f"{pre} {inner} {post}")
+                print("
") print() print("

") print("") # Count rule changes - for line in diff_str.splitlines(): + for line in diff_lines: # Find rule change for current line or construction # + /::: matches = re.search(r": ([A-Z]{1,4}[0-9]{3,4})", line) From 1e894f328c13faedec9b028ab87543dbb379e246 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Mon, 10 Jul 2023 09:00:59 +0100 Subject: [PATCH 384/447] formatter: multi char tokens in SimpleTokenizer (#5610) --- Cargo.lock | 1 + crates/ruff_python_formatter/Cargo.toml | 1 + .../src/comments/placement.rs | 45 +++---- ...__identifier_ending_in_non_start_char.snap | 10 ++ ...re_word_with_only_id_continuing_chars.snap | 18 +++ ...er__trivia__tests__tokenize_multichar.snap | 34 +++++ ...matter__trivia__tests__tricky_unicode.snap | 10 ++ crates/ruff_python_formatter/src/trivia.rs | 123 +++++++++++++++++- 8 files changed, 209 insertions(+), 33 deletions(-) create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__identifier_ending_in_non_start_char.snap create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__ignore_word_with_only_id_continuing_chars.snap create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_multichar.snap create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tricky_unicode.snap diff --git a/Cargo.lock b/Cargo.lock index 541991d0fb..d64986ffc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2101,6 +2101,7 @@ dependencies = [ "similar", "smallvec", "thiserror", + "unic-ucd-ident", ] [[package]] diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index c27d92a907..dac4a95d80 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -28,6 +28,7 @@ rustpython-parser = { workspace = true } serde = { workspace = true, optional = true } smallvec = { workspace = true } thiserror = { workspace = true } +unic-ucd-ident = "0.9.0" [dev-dependencies] ruff_formatter = { path = "../ruff_formatter", features = ["serde"]} diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 207b9a68a8..89dc52896b 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -1,6 +1,6 @@ use std::cmp::Ordering; -use ruff_text_size::{TextLen, TextRange}; +use ruff_text_size::TextRange; use rustpython_parser::ast; use rustpython_parser::ast::{Expr, ExprIfExp, ExprSlice, Ranged}; @@ -14,9 +14,7 @@ use crate::expression::expr_slice::{assign_comment_in_slice, ExprSliceCommentSec use crate::other::arguments::{ assign_argument_separator_comment_placement, find_argument_separators, }; -use crate::trivia::{ - first_non_trivia_token, first_non_trivia_token_rev, SimpleTokenizer, Token, TokenKind, -}; +use crate::trivia::{first_non_trivia_token_rev, SimpleTokenizer, Token, TokenKind}; /// Implements the custom comment placement logic. pub(super) fn place_comment<'a>( @@ -1191,10 +1189,16 @@ fn handle_expr_if_comment<'a>( } // Find the if and the else - let if_token = - find_only_token_str_in_range(TextRange::new(body.end(), test.start()), locator, "if"); - let else_token = - find_only_token_str_in_range(TextRange::new(test.end(), orelse.start()), locator, "else"); + let if_token = find_only_token_in_range( + TextRange::new(body.end(), test.start()), + locator, + TokenKind::If, + ); + let else_token = find_only_token_in_range( + TextRange::new(test.end(), orelse.start()), + locator, + TokenKind::Else, + ); // Between `if` and `test` if if_token.range.start() < comment.slice().start() && comment.slice().start() < test.start() { @@ -1211,25 +1215,12 @@ fn handle_expr_if_comment<'a>( CommentPlacement::Default(comment) } -/// Looks for a multi char token in the range that contains no other tokens. `SimpleTokenizer` only -/// works with single char tokens so we check that we have the right token by string comparison. -fn find_only_token_str_in_range(range: TextRange, locator: &Locator, token_str: &str) -> Token { - let token = - first_non_trivia_token(range.start(), locator.contents()).expect("Expected a token"); - debug_assert!( - locator.after(token.start()).starts_with(token_str), - "expected a `{token_str}` token", - ); - debug_assert!( - SimpleTokenizer::new( - locator.contents(), - TextRange::new(token.start() + token_str.text_len(), range.end()) - ) - .skip_trivia() - .next() - .is_none(), - "Didn't expect any other token" - ); +/// Looks for a token in the range that contains no other tokens. +fn find_only_token_in_range(range: TextRange, locator: &Locator, token_kind: TokenKind) -> Token { + let mut tokens = SimpleTokenizer::new(locator.contents(), range).skip_trivia(); + let token = tokens.next().expect("Expected a token"); + debug_assert_eq!(token.kind(), token_kind); + debug_assert_eq!(tokens.next(), None); token } diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__identifier_ending_in_non_start_char.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__identifier_ending_in_non_start_char.snap new file mode 100644 index 0000000000..15e9d84407 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__identifier_ending_in_non_start_char.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_python_formatter/src/trivia.rs +expression: test_case.tokens() +--- +[ + Token { + kind: Other, + range: 0..2, + }, +] diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__ignore_word_with_only_id_continuing_chars.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__ignore_word_with_only_id_continuing_chars.snap new file mode 100644 index 0000000000..26e9fd18bc --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__ignore_word_with_only_id_continuing_chars.snap @@ -0,0 +1,18 @@ +--- +source: crates/ruff_python_formatter/src/trivia.rs +expression: test_case.tokens() +--- +[ + Token { + kind: Other, + range: 0..1, + }, + Token { + kind: Bogus, + range: 1..2, + }, + Token { + kind: Bogus, + range: 2..3, + }, +] diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_multichar.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_multichar.snap new file mode 100644 index 0000000000..16a1293b44 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_multichar.snap @@ -0,0 +1,34 @@ +--- +source: crates/ruff_python_formatter/src/trivia.rs +expression: test_case.tokens() +--- +[ + Token { + kind: If, + range: 0..2, + }, + Token { + kind: Whitespace, + range: 2..3, + }, + Token { + kind: In, + range: 3..5, + }, + Token { + kind: Whitespace, + range: 5..6, + }, + Token { + kind: Else, + range: 6..10, + }, + Token { + kind: Whitespace, + range: 10..11, + }, + Token { + kind: Match, + range: 11..16, + }, +] diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tricky_unicode.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tricky_unicode.snap new file mode 100644 index 0000000000..91b9cb397a --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tricky_unicode.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_python_formatter/src/trivia.rs +expression: test_case.tokens() +--- +[ + Token { + kind: Other, + range: 0..6, + }, +] diff --git a/crates/ruff_python_formatter/src/trivia.rs b/crates/ruff_python_formatter/src/trivia.rs index 86516170a1..4d8a1fa8cd 100644 --- a/crates/ruff_python_formatter/src/trivia.rs +++ b/crates/ruff_python_formatter/src/trivia.rs @@ -1,8 +1,8 @@ use std::str::Chars; -use ruff_text_size::{TextLen, TextRange, TextSize}; - use ruff_python_whitespace::is_python_whitespace; +use ruff_text_size::{TextLen, TextRange, TextSize}; +use unic_ucd_ident::{is_xid_continue, is_xid_start}; /// Searches for the first non-trivia character in `range`. /// @@ -92,6 +92,24 @@ pub(crate) fn skip_trailing_trivia(offset: TextSize, code: &str) -> TextSize { offset } +fn is_identifier_start(c: char) -> bool { + c.is_ascii_alphabetic() || c == '_' || is_non_ascii_identifier_start(c) +} + +// Checks if the character c is a valid continuation character as described +// in https://docs.python.org/3/reference/lexical_analysis.html#identifiers +fn is_identifier_continuation(c: char) -> bool { + if c.is_ascii() { + matches!(c, 'a'..='z' | 'A'..='Z' | '_' | '0'..='9') + } else { + is_xid_continue(c) + } +} + +fn is_non_ascii_identifier_start(c: char) -> bool { + is_xid_start(c) +} + #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub(crate) struct Token { pub(crate) kind: TokenKind, @@ -167,7 +185,19 @@ pub(crate) enum TokenKind { /// `.`. Dot, - /// Any other non trivia token. Always has a length of 1 + /// `else` + Else, + + /// `if` + If, + + /// `in` + In, + + /// `match` + Match, + + /// Any other non trivia token. Other, /// Returned for each character after [`TokenKind::Other`] has been returned once. @@ -215,6 +245,7 @@ pub(crate) struct SimpleTokenizer<'a> { /// `true` when it is known that the current `back` line has no comment for sure. back_line_has_no_comment: bool, bogus: bool, + source: &'a str, cursor: Cursor<'a>, } @@ -225,6 +256,7 @@ impl<'a> SimpleTokenizer<'a> { back_offset: range.end(), back_line_has_no_comment: false, bogus: false, + source, cursor: Cursor::new(&source[range]), } } @@ -238,6 +270,18 @@ impl<'a> SimpleTokenizer<'a> { Self::new(source, TextRange::up_to(offset)) } + fn to_keyword_or_other(&self, range: TextRange) -> TokenKind { + let source = &self.source[range]; + match source { + "if" => TokenKind::If, + "else" => TokenKind::Else, + "in" => TokenKind::In, + "match" => TokenKind::Match, // Match is a soft keyword that depends on the context but we can always lex it as a keyword and leave it to the caller (parser) to decide if it should be handled as an identifier or keyword. + // ..., + _ => TokenKind::Other, // Potentially an identifier, but only if it isn't a string prefix. We can ignore this for now https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals + } + } + fn next_token(&mut self) -> Token { self.cursor.start_token(); @@ -279,12 +323,19 @@ impl<'a> SimpleTokenizer<'a> { '\\' => TokenKind::Continuation, c => { - let kind = TokenKind::from_non_trivia_char(c); + let kind = if is_identifier_start(c) { + self.cursor.eat_while(is_identifier_continuation); + let token_len = self.cursor.token_len(); + + let range = TextRange::at(self.offset, token_len); + self.to_keyword_or_other(range) + } else { + TokenKind::from_non_trivia_char(c) + }; if kind == TokenKind::Other { self.bogus = true; } - kind } }; @@ -386,7 +437,29 @@ impl<'a> SimpleTokenizer<'a> { } else if c == '\\' { TokenKind::Continuation } else { - let kind = TokenKind::from_non_trivia_char(c); + let kind = if is_identifier_continuation(c) { + // if we only have identifier continuations but no start (e.g. 555) we + // don't want to consume the chars, so in that case, we want to rewind the + // cursor to here + let savepoint = self.cursor.clone(); + self.cursor.eat_back_while(is_identifier_continuation); + + let token_len = self.cursor.token_len(); + let range = TextRange::at(self.back_offset - token_len, token_len); + + if self.source[range] + .chars() + .next() + .is_some_and(is_identifier_start) + { + self.to_keyword_or_other(range) + } else { + self.cursor = savepoint; + TokenKind::Other + } + } else { + TokenKind::from_non_trivia_char(c) + }; if kind == TokenKind::Other { self.bogus = true; @@ -624,6 +697,44 @@ mod tests { test_case.assert_reverse_tokenization(); } + #[test] + fn tricky_unicode() { + let source = "មុ"; + + let test_case = tokenize(source); + assert_debug_snapshot!(test_case.tokens()); + test_case.assert_reverse_tokenization(); + } + + #[test] + fn identifier_ending_in_non_start_char() { + let source = "i5"; + + let test_case = tokenize(source); + assert_debug_snapshot!(test_case.tokens()); + test_case.assert_reverse_tokenization(); + } + + #[test] + fn ignore_word_with_only_id_continuing_chars() { + let source = "555"; + + let test_case = tokenize(source); + assert_debug_snapshot!(test_case.tokens()); + + // note: not reversible: [other, bogus, bogus] vs [bogus, bogus, other] + } + + #[test] + fn tokenize_multichar() { + let source = "if in else match"; + + let test_case = tokenize(source); + + assert_debug_snapshot!(test_case.tokens()); + test_case.assert_reverse_tokenization(); + } + #[test] fn tokenize_substring() { let source = "('some string') # comment"; From bd8f65814c10b9592fabad4a4f7ba5bd2613c3a6 Mon Sep 17 00:00:00 2001 From: konsti Date: Mon, 10 Jul 2023 14:32:15 +0200 Subject: [PATCH 385/447] Format named expressions (walrus operator) (#5642) ## Summary Format named expressions (walrus operator) such a `value := f()`. Unlike tuples, named expression parentheses are not part of the range even when mandatory, so mapping optional parentheses to always gives us decent formatting without implementing all [PEP 572](https://peps.python.org/pep-0572/) rules on when we need parentheses where other expressions wouldn't. We might want to revisit this decision later and implement special cases, but for now this gives us what we need. ## Test Plan black fixtures, i added some fixtures and checked django and cpython for stability. Closes #5613 --- .../fixtures/ruff/expression/named_expr.py | 13 ++ .../src/expression/expr_named_expr.rs | 26 ++- ...ompatibility@py_310__pep_572_py310.py.snap | 12 +- ...black_compatibility@py_38__pep_572.py.snap | 136 +++++------ ..._compatibility@py_39__pep_572_py39.py.snap | 14 +- ...lack_compatibility@py_39__python39.py.snap | 15 +- ...bility@py_39__remove_with_brackets.py.snap | 216 ------------------ .../format@expression__named_expr.py.snap | 38 +++ 8 files changed, 152 insertions(+), 318 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/named_expr.py delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__remove_with_brackets.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@expression__named_expr.py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/named_expr.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/named_expr.py new file mode 100644 index 0000000000..9377e9704e --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/named_expr.py @@ -0,0 +1,13 @@ +y = 1 + +if ( + # 1 + x # 2 + := # 3 + y # 4 +): + pass + +y0 = (y1 := f(x)) + +f(x:=y, z=True) diff --git a/crates/ruff_python_formatter/src/expression/expr_named_expr.rs b/crates/ruff_python_formatter/src/expression/expr_named_expr.rs index 71d2bae891..437a8981e3 100644 --- a/crates/ruff_python_formatter/src/expression/expr_named_expr.rs +++ b/crates/ruff_python_formatter/src/expression/expr_named_expr.rs @@ -2,7 +2,8 @@ use crate::comments::Comments; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{space, text}; use ruff_formatter::{write, Buffer, FormatResult}; use rustpython_parser::ast::ExprNamedExpr; @@ -11,7 +12,21 @@ pub struct FormatExprNamedExpr; impl FormatNodeRule for FormatExprNamedExpr { fn fmt_fields(&self, item: &ExprNamedExpr, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let ExprNamedExpr { + target, + value, + range: _, + } = item; + write!( + f, + [ + target.format(), + space(), + text(":="), + space(), + value.format(), + ] + ) } } @@ -22,6 +37,11 @@ impl NeedsParentheses for ExprNamedExpr { source: &str, comments: &Comments, ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + // Unlike tuples, named expression parentheses are not part of the range even when + // mandatory. See [PEP 572](https://peps.python.org/pep-0572/) for details. + Parentheses::Optional => Parentheses::Always, + parentheses => parentheses, + } } } 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 974a2dc217..1204c05491 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 @@ -32,9 +32,9 @@ f(x, (a := b + c for c in range(10)), y=z, **q) -x[a:=0] -x[a:=0, b:=1] -x[5, b:=0] -+x[NOT_YET_IMPLEMENTED_ExprNamedExpr] -+x[NOT_YET_IMPLEMENTED_ExprNamedExpr, NOT_YET_IMPLEMENTED_ExprNamedExpr] -+x[5, NOT_YET_IMPLEMENTED_ExprNamedExpr] ++x[a := 0] ++x[a := 0, b := 1] ++x[5, b := 0] # Walruses are allowed inside generator expressions on function calls since 3.10. -if any(match := pattern_error.match(s) for s in buffer): @@ -62,9 +62,9 @@ f(x, (a := b + c for c in range(10)), y=z, **q) ```py # Unparenthesized walruses are now allowed in indices since Python 3.10. -x[NOT_YET_IMPLEMENTED_ExprNamedExpr] -x[NOT_YET_IMPLEMENTED_ExprNamedExpr, NOT_YET_IMPLEMENTED_ExprNamedExpr] -x[5, NOT_YET_IMPLEMENTED_ExprNamedExpr] +x[a := 0] +x[a := 0, b := 1] +x[5, b := 0] # Walruses are allowed inside generator expressions on function calls since 3.10. if any((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])): 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 a5be560338..53e4192d06 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 @@ -59,36 +59,20 @@ while x := f(x): ```diff --- Black +++ Ruff -@@ -1,47 +1,47 @@ --(a := 1) --(a := a) --if (match := pattern.search(data)) is None: -+(NOT_YET_IMPLEMENTED_ExprNamedExpr) -+(NOT_YET_IMPLEMENTED_ExprNamedExpr) -+if (NOT_YET_IMPLEMENTED_ExprNamedExpr) is None: +@@ -2,10 +2,10 @@ + (a := a) + if (match := pattern.search(data)) is None: pass -if match := pattern.search(data): -+if NOT_YET_IMPLEMENTED_ExprNamedExpr: ++if (match := pattern.search(data)): pass --[y := f(x), y**2, y**3] + [y := f(x), y**2, y**3] -filtered_data = [y for x in data if (y := f(x)) is None] --(y := f(x)) --y0 = (y1 := f(x)) --foo(x=(y := f(x))) -+[NOT_YET_IMPLEMENTED_ExprNamedExpr, y**2, y**3] +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)) - - --def foo(answer=(p := 42)): -+def foo(answer=(NOT_YET_IMPLEMENTED_ExprNamedExpr)): - pass - - --def foo(answer: (p := 42) = 5): -+def foo(answer: (NOT_YET_IMPLEMENTED_ExprNamedExpr) = 5): + (y := f(x)) + y0 = (y1 := f(x)) + foo(x=(y := f(x))) +@@ -19,29 +19,29 @@ pass @@ -96,99 +80,89 @@ while x := f(x): -(x := lambda: 1) -(x := lambda: (y := 1)) -lambda line: (m := re.match(pattern, line)) and m.group(1) --x = (y := 0) --(z := (y := (x := 0))) ++lambda x: True ++(x := lambda x: True) ++(x := lambda x: True) ++lambda x: True + x = (y := 0) + (z := (y := (x := 0))) -(info := (name, phone, *rest)) --(x := 1, 2) --(total := total + tax) --len(lines := f.readlines()) --foo(x := 3, cat="vector") --foo(cat=(category := "vector")) ++(info := (name, phone, *NOT_YET_IMPLEMENTED_ExprStarred)) + (x := 1, 2) + (total := total + tax) + len(lines := f.readlines()) + foo(x := 3, cat="vector") + foo(cat=(category := "vector")) -if any(len(longline := l) >= 100 for l in lines): -+lambda x: True -+(NOT_YET_IMPLEMENTED_ExprNamedExpr) -+(NOT_YET_IMPLEMENTED_ExprNamedExpr) -+lambda x: True -+x = NOT_YET_IMPLEMENTED_ExprNamedExpr -+(NOT_YET_IMPLEMENTED_ExprNamedExpr) -+(NOT_YET_IMPLEMENTED_ExprNamedExpr) -+(NOT_YET_IMPLEMENTED_ExprNamedExpr, 2) -+(NOT_YET_IMPLEMENTED_ExprNamedExpr) -+len(NOT_YET_IMPLEMENTED_ExprNamedExpr) -+foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, cat="vector") -+foo(cat=(NOT_YET_IMPLEMENTED_ExprNamedExpr)) +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: ++if (env_base := os.environ.get("PYTHONUSERBASE", None)): return env_base --if self._is_special and (ans := self._check_nans(context=context)): -+if self._is_special and (NOT_YET_IMPLEMENTED_ExprNamedExpr): + if self._is_special and (ans := self._check_nans(context=context)): return ans --foo(b := 2, a=1) + foo(b := 2, a=1) -foo((b := 2), a=1) --foo(c=(b := 2), a=1) -+foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, a=1) -+foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, a=1) -+foo(c=(NOT_YET_IMPLEMENTED_ExprNamedExpr), a=1) ++foo(b := 2, a=1) + foo(c=(b := 2), a=1) -while x := f(x): -+while NOT_YET_IMPLEMENTED_ExprNamedExpr: ++while (x := f(x)): pass -while x := f(x): -+while NOT_YET_IMPLEMENTED_ExprNamedExpr: ++while (x := f(x)): pass ``` ## Ruff Output ```py -(NOT_YET_IMPLEMENTED_ExprNamedExpr) -(NOT_YET_IMPLEMENTED_ExprNamedExpr) -if (NOT_YET_IMPLEMENTED_ExprNamedExpr) is None: +(a := 1) +(a := a) +if (match := pattern.search(data)) is None: pass -if NOT_YET_IMPLEMENTED_ExprNamedExpr: +if (match := pattern.search(data)): pass -[NOT_YET_IMPLEMENTED_ExprNamedExpr, y**2, y**3] +[y := f(x), y**2, y**3] 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)) +(y := f(x)) +y0 = (y1 := f(x)) +foo(x=(y := f(x))) -def foo(answer=(NOT_YET_IMPLEMENTED_ExprNamedExpr)): +def foo(answer=(p := 42)): pass -def foo(answer: (NOT_YET_IMPLEMENTED_ExprNamedExpr) = 5): +def foo(answer: (p := 42) = 5): pass lambda x: True -(NOT_YET_IMPLEMENTED_ExprNamedExpr) -(NOT_YET_IMPLEMENTED_ExprNamedExpr) +(x := lambda x: True) +(x := lambda x: True) lambda x: True -x = NOT_YET_IMPLEMENTED_ExprNamedExpr -(NOT_YET_IMPLEMENTED_ExprNamedExpr) -(NOT_YET_IMPLEMENTED_ExprNamedExpr) -(NOT_YET_IMPLEMENTED_ExprNamedExpr, 2) -(NOT_YET_IMPLEMENTED_ExprNamedExpr) -len(NOT_YET_IMPLEMENTED_ExprNamedExpr) -foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, cat="vector") -foo(cat=(NOT_YET_IMPLEMENTED_ExprNamedExpr)) +x = (y := 0) +(z := (y := (x := 0))) +(info := (name, phone, *NOT_YET_IMPLEMENTED_ExprStarred)) +(x := 1, 2) +(total := total + tax) +len(lines := f.readlines()) +foo(x := 3, cat="vector") +foo(cat=(category := "vector")) if any((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])): print(longline) -if NOT_YET_IMPLEMENTED_ExprNamedExpr: +if (env_base := os.environ.get("PYTHONUSERBASE", None)): return env_base -if self._is_special and (NOT_YET_IMPLEMENTED_ExprNamedExpr): +if self._is_special and (ans := self._check_nans(context=context)): return ans -foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, a=1) -foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, a=1) -foo(c=(NOT_YET_IMPLEMENTED_ExprNamedExpr), a=1) +foo(b := 2, a=1) +foo(b := 2, a=1) +foo(c=(b := 2), a=1) -while NOT_YET_IMPLEMENTED_ExprNamedExpr: +while (x := f(x)): pass -while NOT_YET_IMPLEMENTED_ExprNamedExpr: +while (x := f(x)): pass ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__pep_572_py39.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__pep_572_py39.py.snap index ec43406b54..4a2a8824aa 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__pep_572_py39.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__pep_572_py39.py.snap @@ -22,15 +22,13 @@ x[(a := 1), (b := 3)] @@ -1,7 +1,7 @@ # Unparenthesized walruses are now allowed in set literals & set comprehensions # since Python 3.9 --{x := 1, 2, 3} + {x := 1, 2, 3} -{x4 := x**5 for x in range(7)} -+{NOT_YET_IMPLEMENTED_ExprNamedExpr, 2, 3} +{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} # We better not remove the parentheses here (since it's a 3.10 feature) --x[(a := 1)] + x[(a := 1)] -x[(a := 1), (b := 3)] -+x[(NOT_YET_IMPLEMENTED_ExprNamedExpr)] -+x[((NOT_YET_IMPLEMENTED_ExprNamedExpr), (NOT_YET_IMPLEMENTED_ExprNamedExpr))] ++x[((a := 1), (b := 3))] ``` ## Ruff Output @@ -38,11 +36,11 @@ x[(a := 1), (b := 3)] ```py # Unparenthesized walruses are now allowed in set literals & set comprehensions # since Python 3.9 -{NOT_YET_IMPLEMENTED_ExprNamedExpr, 2, 3} +{x := 1, 2, 3} {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} # We better not remove the parentheses here (since it's a 3.10 feature) -x[(NOT_YET_IMPLEMENTED_ExprNamedExpr)] -x[((NOT_YET_IMPLEMENTED_ExprNamedExpr), (NOT_YET_IMPLEMENTED_ExprNamedExpr))] +x[(a := 1)] +x[((a := 1), (b := 3))] ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__python39.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__python39.py.snap index 86ad5e3c92..cef4b88f67 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__python39.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__python39.py.snap @@ -32,14 +32,17 @@ def f(): @relaxed_decorator[0] def f(): ... -@@ -13,8 +12,6 @@ +@@ -13,8 +12,10 @@ ... -@extremely_long_variable_name_that_doesnt_fit := complex.expression( - with_long="arguments_value_that_wont_fit_at_the_end_of_the_line" --) -+@NOT_YET_IMPLEMENTED_ExprNamedExpr ++@( ++ extremely_long_variable_name_that_doesnt_fit := complex.expression( ++ with_long="arguments_value_that_wont_fit_at_the_end_of_the_line" ++ ) + ) def f(): ... ``` @@ -61,7 +64,11 @@ def f(): ... -@NOT_YET_IMPLEMENTED_ExprNamedExpr +@( + extremely_long_variable_name_that_doesnt_fit := complex.expression( + with_long="arguments_value_that_wont_fit_at_the_end_of_the_line" + ) +) def f(): ... ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__remove_with_brackets.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__remove_with_brackets.py.snap deleted file mode 100644 index 8766515712..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__remove_with_brackets.py.snap +++ /dev/null @@ -1,216 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py ---- -## Input - -```py -with (open("bla.txt")): - pass - -with (open("bla.txt")), (open("bla.txt")): - pass - -with (open("bla.txt") as f): - pass - -# Remove brackets within alias expression -with (open("bla.txt")) as f: - pass - -# Remove brackets around one-line context managers -with (open("bla.txt") as f, (open("x"))): - pass - -with ((open("bla.txt")) as f, open("x")): - pass - -with (CtxManager1() as example1, CtxManager2() as example2): - ... - -# Brackets remain when using magic comma -with (CtxManager1() as example1, CtxManager2() as example2,): - ... - -# Brackets remain for multi-line context managers -with (CtxManager1() as example1, CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2): - ... - -# Don't touch assignment expressions -with (y := open("./test.py")) as f: - pass - -# Deeply nested examples -# N.B. Multiple brackets are only possible -# around the context manager itself. -# Only one brackets is allowed around the -# alias expression or comma-delimited context managers. -with (((open("bla.txt")))): - pass - -with (((open("bla.txt")))), (((open("bla.txt")))): - pass - -with (((open("bla.txt")))) as f: - pass - -with ((((open("bla.txt")))) as f): - pass - -with ((((CtxManager1()))) as example1, (((CtxManager2()))) as example2): - ... -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -39,7 +39,7 @@ - ... - - # Don't touch assignment expressions --with (y := open("./test.py")) as f: -+with NOT_YET_IMPLEMENTED_ExprNamedExpr as f: - pass - - # Deeply nested examples -``` - -## Ruff Output - -```py -with open("bla.txt"): - pass - -with open("bla.txt"), open("bla.txt"): - pass - -with open("bla.txt") as f: - pass - -# Remove brackets within alias expression -with open("bla.txt") as f: - pass - -# Remove brackets around one-line context managers -with open("bla.txt") as f, open("x"): - pass - -with open("bla.txt") as f, open("x"): - pass - -with CtxManager1() as example1, CtxManager2() as example2: - ... - -# Brackets remain when using magic comma -with ( - CtxManager1() as example1, - CtxManager2() as example2, -): - ... - -# Brackets remain for multi-line context managers -with ( - CtxManager1() as example1, - CtxManager2() as example2, - CtxManager2() as example2, - CtxManager2() as example2, - CtxManager2() as example2, -): - ... - -# Don't touch assignment expressions -with NOT_YET_IMPLEMENTED_ExprNamedExpr as f: - pass - -# Deeply nested examples -# N.B. Multiple brackets are only possible -# around the context manager itself. -# Only one brackets is allowed around the -# alias expression or comma-delimited context managers. -with open("bla.txt"): - pass - -with open("bla.txt"), open("bla.txt"): - pass - -with open("bla.txt") as f: - pass - -with open("bla.txt") as f: - pass - -with CtxManager1() as example1, CtxManager2() as example2: - ... -``` - -## Black Output - -```py -with open("bla.txt"): - pass - -with open("bla.txt"), open("bla.txt"): - pass - -with open("bla.txt") as f: - pass - -# Remove brackets within alias expression -with open("bla.txt") as f: - pass - -# Remove brackets around one-line context managers -with open("bla.txt") as f, open("x"): - pass - -with open("bla.txt") as f, open("x"): - pass - -with CtxManager1() as example1, CtxManager2() as example2: - ... - -# Brackets remain when using magic comma -with ( - CtxManager1() as example1, - CtxManager2() as example2, -): - ... - -# Brackets remain for multi-line context managers -with ( - CtxManager1() as example1, - CtxManager2() as example2, - CtxManager2() as example2, - CtxManager2() as example2, - CtxManager2() as example2, -): - ... - -# Don't touch assignment expressions -with (y := open("./test.py")) as f: - pass - -# Deeply nested examples -# N.B. Multiple brackets are only possible -# around the context manager itself. -# Only one brackets is allowed around the -# alias expression or comma-delimited context managers. -with open("bla.txt"): - pass - -with open("bla.txt"), open("bla.txt"): - pass - -with open("bla.txt") as f: - pass - -with open("bla.txt") as f: - pass - -with CtxManager1() as example1, CtxManager2() as example2: - ... -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__named_expr.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__named_expr.py.snap new file mode 100644 index 0000000000..9cf0484b4b --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__named_expr.py.snap @@ -0,0 +1,38 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/named_expr.py +--- +## Input +```py +y = 1 + +if ( + # 1 + x # 2 + := # 3 + y # 4 +): + pass + +y0 = (y1 := f(x)) + +f(x:=y, z=True) +``` + +## Output +```py +y = 1 + +if ( + # 1 + x := y # 2 # 3 # 4 +): + pass + +y0 = (y1 := f(x)) + +f(x := y, z=True) +``` + + + From 089a671adbef6eadecaeac2fa8e417056321af36 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 10 Jul 2023 15:00:18 +0200 Subject: [PATCH 386/447] Fix Black compatible snapshot deletion (#5646) --- .../ruff_python_formatter/tests/fixtures.rs | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/crates/ruff_python_formatter/tests/fixtures.rs b/crates/ruff_python_formatter/tests/fixtures.rs index d312ff7de6..7860cd3aae 100644 --- a/crates/ruff_python_formatter/tests/fixtures.rs +++ b/crates/ruff_python_formatter/tests/fixtures.rs @@ -32,18 +32,20 @@ fn black_compatibility() { // already perfectly captures the expected output. // The following code mimics insta's logic generating the snapshot name for a test. let workspace_path = std::env::var("CARGO_MANIFEST_DIR").unwrap(); - let snapshot_name = insta::_function_name!() - .strip_prefix(&format!("{}::", module_path!())) - .unwrap(); - let module_path = module_path!().replace("::", "__"); + + let mut components = input_path.components().rev(); + let file_name = components.next().unwrap(); + let test_suite = components.next().unwrap(); + + let snapshot_name = format!( + "black_compatibility@{}__{}.snap", + test_suite.as_os_str().to_string_lossy(), + file_name.as_os_str().to_string_lossy() + ); let snapshot_path = Path::new(&workspace_path) - .join("src/snapshots") - .join(format!( - "{module_path}__{}.snap", - snapshot_name.replace(&['/', '\\'][..], "__") - )); - + .join("tests/snapshots") + .join(snapshot_name); if snapshot_path.exists() && snapshot_path.is_file() { // SAFETY: This is a convenience feature. That's why we don't want to abort // when deleting a no longer needed snapshot fails. From 24bcbb85a19111a64bba9ed24059a6aa89d72267 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 10 Jul 2023 16:41:26 +0300 Subject: [PATCH 387/447] Rework upstream categories so we can `all_rules()` (#5591) ## Summary This PR reworks the `upstream_categories` mechanism that is only used for documentation purposes to make it easier to generate docs using `all_rules()`. The new implementation also relies on "tribal knowledge" about rule codes, so it's not the best implementation, but gets us forward. Another option would be to change the rule-defining proc macros to allow configuring an optional `RuleCategory`, but that seems more heavy-handed and possibly unnecessary in the long run... Draft since this builds on #5439. cc @charliermarsh :) --- crates/ruff/src/lib.rs | 1 + crates/ruff/src/registry.rs | 26 +------ crates/ruff/src/upstream_categories.rs | 79 +++++++++++++++++++++ crates/ruff_cli/src/commands/linter.rs | 10 +-- crates/ruff_dev/src/generate_rules_table.rs | 25 ++++--- 5 files changed, 101 insertions(+), 40 deletions(-) create mode 100644 crates/ruff/src/upstream_categories.rs diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs index 2a69194fad..b224744c31 100644 --- a/crates/ruff/src/lib.rs +++ b/crates/ruff/src/lib.rs @@ -38,6 +38,7 @@ mod rule_selector; pub mod rules; pub mod settings; pub mod source_kind; +pub mod upstream_categories; #[cfg(any(test, fuzzing))] pub mod test; diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index a6ac161b7b..4b05c341a8 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -7,7 +7,7 @@ pub use codes::Rule; use ruff_macros::RuleNamespace; pub use rule_set::{RuleSet, RuleSetIterator}; -use crate::codes::{self, RuleCodePrefix}; +use crate::codes::{self}; mod rule_set; @@ -218,30 +218,6 @@ pub trait RuleNamespace: Sized { fn url(&self) -> Option<&'static str>; } -/// The prefix and name for an upstream linter category. -pub struct UpstreamCategory(pub RuleCodePrefix, pub &'static str); - -impl Linter { - pub const fn upstream_categories(&self) -> Option<&'static [UpstreamCategory]> { - match self { - Linter::Pycodestyle => Some(&[ - UpstreamCategory(RuleCodePrefix::Pycodestyle(codes::Pycodestyle::E), "Error"), - UpstreamCategory( - RuleCodePrefix::Pycodestyle(codes::Pycodestyle::W), - "Warning", - ), - ]), - Linter::Pylint => Some(&[ - UpstreamCategory(RuleCodePrefix::Pylint(codes::Pylint::C), "Convention"), - UpstreamCategory(RuleCodePrefix::Pylint(codes::Pylint::E), "Error"), - UpstreamCategory(RuleCodePrefix::Pylint(codes::Pylint::R), "Refactor"), - UpstreamCategory(RuleCodePrefix::Pylint(codes::Pylint::W), "Warning"), - ]), - _ => None, - } - } -} - #[derive(is_macro::Is, Copy, Clone)] pub enum LintSource { Ast, diff --git a/crates/ruff/src/upstream_categories.rs b/crates/ruff/src/upstream_categories.rs new file mode 100644 index 0000000000..a00bfff3f0 --- /dev/null +++ b/crates/ruff/src/upstream_categories.rs @@ -0,0 +1,79 @@ +//! This module should probably not exist in this shape or form. +use crate::codes::Rule; +use crate::registry::Linter; + +#[derive(Hash, Eq, PartialEq, Copy, Clone, Debug)] +pub struct UpstreamCategoryAndPrefix { + pub category: &'static str, + pub prefix: &'static str, +} + +const PLC: UpstreamCategoryAndPrefix = UpstreamCategoryAndPrefix { + category: "Convention", + prefix: "PLC", +}; + +const PLE: UpstreamCategoryAndPrefix = UpstreamCategoryAndPrefix { + category: "Error", + prefix: "PLE", +}; + +const PLR: UpstreamCategoryAndPrefix = UpstreamCategoryAndPrefix { + category: "Refactor", + prefix: "PLR", +}; + +const PLW: UpstreamCategoryAndPrefix = UpstreamCategoryAndPrefix { + category: "Warning", + prefix: "PLW", +}; + +const E: UpstreamCategoryAndPrefix = UpstreamCategoryAndPrefix { + category: "Error", + prefix: "E", +}; + +const W: UpstreamCategoryAndPrefix = UpstreamCategoryAndPrefix { + category: "Warning", + prefix: "W", +}; + +impl Rule { + pub fn upstream_category(&self, linter: &Linter) -> Option { + let code = linter.code_for_rule(*self).unwrap(); + match linter { + Linter::Pycodestyle => { + if code.starts_with('E') { + Some(E) + } else if code.starts_with('W') { + Some(W) + } else { + None + } + } + Linter::Pylint => { + if code.starts_with("PLC") { + Some(PLC) + } else if code.starts_with("PLE") { + Some(PLE) + } else if code.starts_with("PLR") { + Some(PLR) + } else if code.starts_with("PLW") { + Some(PLW) + } else { + None + } + } + _ => None, + } + } +} +impl Linter { + pub const fn upstream_categories(&self) -> Option<&'static [UpstreamCategoryAndPrefix]> { + match self { + Linter::Pycodestyle => Some(&[E, W]), + Linter::Pylint => Some(&[PLC, PLE, PLR, PLW]), + _ => None, + } + } +} diff --git a/crates/ruff_cli/src/commands/linter.rs b/crates/ruff_cli/src/commands/linter.rs index ccbeb5ba45..76d6845b06 100644 --- a/crates/ruff_cli/src/commands/linter.rs +++ b/crates/ruff_cli/src/commands/linter.rs @@ -7,7 +7,7 @@ use itertools::Itertools; use serde::Serialize; use strum::IntoEnumIterator; -use ruff::registry::{Linter, RuleNamespace, UpstreamCategory}; +use ruff::registry::{Linter, RuleNamespace}; use crate::args::HelpFormat; @@ -37,7 +37,7 @@ pub(crate) fn linter(format: HelpFormat) -> Result<()> { .upstream_categories() .unwrap() .iter() - .map(|UpstreamCategory(prefix, ..)| prefix.short_code()) + .map(|c| c.prefix) .join("/"), prefix => prefix.to_string(), }; @@ -52,9 +52,9 @@ pub(crate) fn linter(format: HelpFormat) -> Result<()> { name: linter_info.name(), categories: linter_info.upstream_categories().map(|cats| { cats.iter() - .map(|UpstreamCategory(prefix, name)| LinterCategoryInfo { - prefix: prefix.short_code(), - name, + .map(|c| LinterCategoryInfo { + prefix: c.prefix, + name: c.category, }) .collect() }), diff --git a/crates/ruff_dev/src/generate_rules_table.rs b/crates/ruff_dev/src/generate_rules_table.rs index b418851978..a92b56cdd1 100644 --- a/crates/ruff_dev/src/generate_rules_table.rs +++ b/crates/ruff_dev/src/generate_rules_table.rs @@ -3,8 +3,9 @@ use itertools::Itertools; use strum::IntoEnumIterator; -use ruff::registry::{Linter, Rule, RuleNamespace, UpstreamCategory}; +use ruff::registry::{Linter, Rule, RuleNamespace}; use ruff::settings::options::Options; +use ruff::upstream_categories::UpstreamCategoryAndPrefix; use ruff_diagnostics::AutofixKind; const FIX_SYMBOL: &str = "🛠"; @@ -69,7 +70,7 @@ pub(crate) fn generate() -> String { .upstream_categories() .unwrap() .iter() - .map(|UpstreamCategory(prefix, ..)| prefix.short_code()) + .map(|c| c.prefix) .join(", "), prefix => prefix.to_string(), }; @@ -114,16 +115,20 @@ pub(crate) fn generate() -> String { table_out.push('\n'); } - if let Some(categories) = linter.upstream_categories() { - for UpstreamCategory(prefix, name) in categories { - table_out.push_str(&format!( - "#### {name} ({}{})", - linter.common_prefix(), - prefix.short_code() - )); + let rules_by_upstream_category = linter + .all_rules() + .map(|rule| (rule.upstream_category(&linter), rule)) + .into_group_map(); + + if rules_by_upstream_category.len() > 1 { + for (opt, rules) in &rules_by_upstream_category { + if opt.is_some() { + let UpstreamCategoryAndPrefix { category, prefix } = opt.unwrap(); + table_out.push_str(&format!("#### {category} ({prefix})")); + } table_out.push('\n'); table_out.push('\n'); - generate_table(&mut table_out, prefix.clone().rules(), &linter); + generate_table(&mut table_out, rules.clone(), &linter); } } else { generate_table(&mut table_out, linter.all_rules(), &linter); From 82317ba1fd7ba0ff49fb2d47f7c2fb9bc0193b26 Mon Sep 17 00:00:00 2001 From: Harutaka Kawamura Date: Mon, 10 Jul 2023 22:49:13 +0900 Subject: [PATCH 388/447] Support autofix for some multiline `str.format` calls (#5638) ## Summary Fixes #5531 ## Test Plan New test cases --- .../test/fixtures/pyupgrade/UP032_0.py | 11 ++ crates/ruff/src/checkers/ast/mod.rs | 14 ++- .../src/rules/pyupgrade/rules/f_strings.rs | 17 ++- ...__rules__pyupgrade__tests__UP032_0.py.snap | 114 +++++++++++++----- 4 files changed, 115 insertions(+), 41 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pyupgrade/UP032_0.py b/crates/ruff/resources/test/fixtures/pyupgrade/UP032_0.py index c45821bc96..fbdeb1dd7e 100644 --- a/crates/ruff/resources/test/fixtures/pyupgrade/UP032_0.py +++ b/crates/ruff/resources/test/fixtures/pyupgrade/UP032_0.py @@ -54,6 +54,14 @@ print("foo {} ".format(x)) '''{[b]}'''.format(a) +"{}".format( + 1 +) + +"123456789 {}".format( + 1111111111111111111111111111111111111111111111111111111111111111111111111, +) + ### # Non-errors ### @@ -87,6 +95,9 @@ r'"\N{snowman} {}".format(a)' "{a}" "{b}".format(a=1, b=1) +"123456789 {}".format( + 11111111111111111111111111111111111111111111111111111111111111111111111111, +) async def c(): return "{}".format(await 3) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index be8aa68af6..aae885b88b 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -2436,19 +2436,19 @@ where if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() { let attr = attr.as_str(); if let Expr::Constant(ast::ExprConstant { - value: Constant::Str(value), + value: Constant::Str(val), .. }) = value.as_ref() { if attr == "join" { // "...".join(...) call if self.enabled(Rule::StaticJoinToFString) { - flynt::rules::static_join_to_fstring(self, expr, value); + flynt::rules::static_join_to_fstring(self, expr, val); } } else if attr == "format" { // "...".format(...) call let location = expr.range(); - match pyflakes::format::FormatSummary::try_from(value.as_ref()) { + match pyflakes::format::FormatSummary::try_from(val.as_ref()) { Err(e) => { if self.enabled(Rule::StringDotFormatInvalidFormat) { self.diagnostics.push(Diagnostic::new( @@ -2492,7 +2492,13 @@ where } if self.enabled(Rule::FString) { - pyupgrade::rules::f_strings(self, &summary, expr); + pyupgrade::rules::f_strings( + self, + &summary, + expr, + value, + self.settings.line_length, + ); } } } diff --git a/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs b/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs index 9420335056..494274b3a3 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs @@ -13,6 +13,7 @@ use ruff_python_ast::source_code::Locator; use ruff_python_ast::str::{is_implicit_concatenation, leading_quote, trailing_quote}; use crate::checkers::ast::Checker; +use crate::line_width::LineLength; use crate::registry::AsRule; use crate::rules::pyflakes::format::FormatSummary; use crate::rules::pyupgrade::helpers::curly_escape; @@ -313,13 +314,19 @@ fn try_convert_to_f_string(expr: &Expr, locator: &Locator) -> Option { } /// UP032 -pub(crate) fn f_strings(checker: &mut Checker, summary: &FormatSummary, expr: &Expr) { +pub(crate) fn f_strings( + checker: &mut Checker, + summary: &FormatSummary, + expr: &Expr, + template: &Expr, + line_length: LineLength, +) { if summary.has_nested_parts { return; } // Avoid refactoring multi-line strings. - if checker.locator.contains_line_break(expr.range()) { + if checker.locator.contains_line_break(template.range()) { return; } @@ -329,9 +336,9 @@ pub(crate) fn f_strings(checker: &mut Checker, summary: &FormatSummary, expr: &E return; }; - // Avoid refactors that increase the resulting string length. - let existing = checker.locator.slice(expr.range()); - if contents.len() > existing.len() { + // Avoid refactors that exceed the line length limit. + let col_offset = template.start() - checker.locator.line_start(template.start()); + if col_offset.to_usize() + contents.len() > line_length.get() { return; } diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP032_0.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP032_0.py.snap index 16833914ee..2a1340df50 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP032_0.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP032_0.py.snap @@ -533,7 +533,7 @@ UP032_0.py:55:1: UP032 [*] Use f-string instead of `format` call 55 | '''{[b]}'''.format(a) | ^^^^^^^^^^^^^^^^^^^^^ UP032 56 | -57 | ### +57 | "{}".format( | = help: Convert to f-string @@ -544,57 +544,107 @@ UP032_0.py:55:1: UP032 [*] Use f-string instead of `format` call 55 |-'''{[b]}'''.format(a) 55 |+f'''{a["b"]}''' 56 56 | -57 57 | ### -58 58 | # Non-errors +57 57 | "{}".format( +58 58 | 1 -UP032_0.py:100:11: UP032 [*] Use f-string instead of `format` call +UP032_0.py:57:1: UP032 [*] Use f-string instead of `format` call + | +55 | '''{[b]}'''.format(a) +56 | +57 | / "{}".format( +58 | | 1 +59 | | ) + | |_^ UP032 +60 | +61 | "123456789 {}".format( + | + = help: Convert to f-string + +ℹ Suggested fix +54 54 | +55 55 | '''{[b]}'''.format(a) +56 56 | +57 |-"{}".format( +58 |- 1 +59 |-) + 57 |+f"{1}" +60 58 | +61 59 | "123456789 {}".format( +62 60 | 1111111111111111111111111111111111111111111111111111111111111111111111111, + +UP032_0.py:61:1: UP032 [*] Use f-string instead of `format` call + | +59 | ) +60 | +61 | / "123456789 {}".format( +62 | | 1111111111111111111111111111111111111111111111111111111111111111111111111, +63 | | ) + | |_^ UP032 +64 | +65 | ### + | + = help: Convert to f-string + +ℹ Suggested fix +58 58 | 1 +59 59 | ) +60 60 | +61 |-"123456789 {}".format( +62 |- 1111111111111111111111111111111111111111111111111111111111111111111111111, +63 |-) + 61 |+f"123456789 {1111111111111111111111111111111111111111111111111111111111111111111111111}" +64 62 | +65 63 | ### +66 64 | # Non-errors + +UP032_0.py:111:11: UP032 [*] Use f-string instead of `format` call | - 99 | def d(osname, version, release): -100 | return"{}-{}.{}".format(osname, version, release) +110 | def d(osname, version, release): +111 | return"{}-{}.{}".format(osname, version, release) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP032 | = help: Convert to f-string ℹ Suggested fix -97 97 | -98 98 | -99 99 | def d(osname, version, release): -100 |- return"{}-{}.{}".format(osname, version, release) - 100 |+ return f"{osname}-{version}.{release}" -101 101 | -102 102 | -103 103 | def e(): +108 108 | +109 109 | +110 110 | def d(osname, version, release): +111 |- return"{}-{}.{}".format(osname, version, release) + 111 |+ return f"{osname}-{version}.{release}" +112 112 | +113 113 | +114 114 | def e(): -UP032_0.py:104:10: UP032 [*] Use f-string instead of `format` call +UP032_0.py:115:10: UP032 [*] Use f-string instead of `format` call | -103 | def e(): -104 | yield"{}".format(1) +114 | def e(): +115 | yield"{}".format(1) | ^^^^^^^^^^^^^^ UP032 | = help: Convert to f-string ℹ Suggested fix -101 101 | -102 102 | -103 103 | def e(): -104 |- yield"{}".format(1) - 104 |+ yield f"{1}" -105 105 | -106 106 | -107 107 | assert"{}".format(1) +112 112 | +113 113 | +114 114 | def e(): +115 |- yield"{}".format(1) + 115 |+ yield f"{1}" +116 116 | +117 117 | +118 118 | assert"{}".format(1) -UP032_0.py:107:7: UP032 [*] Use f-string instead of `format` call +UP032_0.py:118:7: UP032 [*] Use f-string instead of `format` call | -107 | assert"{}".format(1) +118 | assert"{}".format(1) | ^^^^^^^^^^^^^^ UP032 | = help: Convert to f-string ℹ Suggested fix -104 104 | yield"{}".format(1) -105 105 | -106 106 | -107 |-assert"{}".format(1) - 107 |+assert f"{1}" +115 115 | yield"{}".format(1) +116 116 | +117 117 | +118 |-assert"{}".format(1) + 118 |+assert f"{1}" From cab3a507bcf3e40f8b493b48f875498348de681b Mon Sep 17 00:00:00 2001 From: konsti Date: Mon, 10 Jul 2023 15:55:19 +0200 Subject: [PATCH 389/447] Fix find_only_token_in_range with expression parentheses (#5645) ## Summary Fix an oversight in `find_only_token_in_range` where the following code would panic due do the closing and opening parentheses being in the range we scan: ```python d1 = [ ("a") if # 1 ("b") else # 2 ("c") ] ``` Closing and opening parentheses respectively are now correctly skipped. ## Test Plan I added a regression test --- .../test/fixtures/ruff/expression/if.py | 8 ++++++++ .../src/comments/placement.rs | 8 ++++++-- .../snapshots/format@expression__if.py.snap | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/if.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/if.py index ed8c1ab97b..df4c488603 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/if.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/if.py @@ -31,3 +31,11 @@ c2 = ( # 8 "b" # 9 ) + +# regression test: parentheses outside the expression ranges interfering with finding +# the `if` and `else` token finding +d1 = [ + ("a") if # 1 + ("b") else # 2 + ("c") +] diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 89dc52896b..85b6534f49 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -1215,11 +1215,15 @@ fn handle_expr_if_comment<'a>( CommentPlacement::Default(comment) } -/// Looks for a token in the range that contains no other tokens. +/// Looks for a token in the range that contains no other tokens except for parentheses outside +/// the expression ranges fn find_only_token_in_range(range: TextRange, locator: &Locator, token_kind: TokenKind) -> Token { - let mut tokens = SimpleTokenizer::new(locator.contents(), range).skip_trivia(); + let mut tokens = SimpleTokenizer::new(locator.contents(), range) + .skip_trivia() + .skip_while(|token| token.kind == TokenKind::RParen); let token = tokens.next().expect("Expected a token"); debug_assert_eq!(token.kind(), token_kind); + let mut tokens = tokens.skip_while(|token| token.kind == TokenKind::LParen); debug_assert_eq!(tokens.next(), None); token } diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__if.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__if.py.snap index 3b563f0454..f5a195c622 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__if.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__if.py.snap @@ -37,6 +37,14 @@ c2 = ( # 8 "b" # 9 ) + +# regression test: parentheses outside the expression ranges interfering with finding +# the `if` and `else` token finding +d1 = [ + ("a") if # 1 + ("b") else # 2 + ("c") +] ``` ## Output @@ -78,6 +86,16 @@ c2 = ( # 8 else "b" # 9 ) + +# regression test: parentheses outside the expression ranges interfering with finding +# the `if` and `else` token finding +d1 = [ + ("a") + # 1 + if ("b") + # 2 + else ("c") +] ``` From ae4a7ef0edb287b52adef004f08b57e5a2786aa4 Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Mon, 10 Jul 2023 09:00:43 -0500 Subject: [PATCH 390/447] Make TRY301 trigger only if a `raise` throws a caught exception (#5455) ## Summary Fixes #5246. We generate a hash set of all exception IDs caught by the `try` statement, then check that the inner `raise` actually raises a caught exception. ## Test Plan Added a new test, `cargo t`. --- .../test/fixtures/tryceratops/TRY301.py | 7 +++ .../tryceratops/rules/raise_within_try.rs | 44 ++++++++++++++++--- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/tryceratops/TRY301.py b/crates/ruff/resources/test/fixtures/tryceratops/TRY301.py index da7c82e487..00544aa856 100644 --- a/crates/ruff/resources/test/fixtures/tryceratops/TRY301.py +++ b/crates/ruff/resources/test/fixtures/tryceratops/TRY301.py @@ -57,3 +57,10 @@ def fine(): a = process() # This throws the exception now finally: print("finally") + + +def fine(): + try: + raise ValueError("a doesn't exist") + except TypeError: # A different exception is caught + print("A different exception is caught") diff --git a/crates/ruff/src/rules/tryceratops/rules/raise_within_try.rs b/crates/ruff/src/rules/tryceratops/rules/raise_within_try.rs index f980a21dee..37711de6f5 100644 --- a/crates/ruff/src/rules/tryceratops/rules/raise_within_try.rs +++ b/crates/ruff/src/rules/tryceratops/rules/raise_within_try.rs @@ -1,13 +1,18 @@ -use rustpython_parser::ast::{ExceptHandler, Ranged, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor}; +use ruff_python_ast::{ + comparable::ComparableExpr, + helpers::{self, map_callable}, + statement_visitor::{walk_stmt, StatementVisitor}, +}; use crate::checkers::ast::Checker; /// ## What it does -/// Checks for `raise` statements within `try` blocks. +/// Checks for `raise` statements within `try` blocks. The only `raise`s +/// caught are those that throw exceptions caught by the `try` statement itself. /// /// ## Why is this bad? /// Raising and catching exceptions within the same `try` block is redundant, @@ -83,9 +88,36 @@ pub(crate) fn raise_within_try(checker: &mut Checker, body: &[Stmt], handlers: & visitor.raises }; + if raises.is_empty() { + return; + } + + let handled_exceptions = helpers::extract_handled_exceptions(handlers); + let comparables: Vec = handled_exceptions + .iter() + .map(|handler| ComparableExpr::from(*handler)) + .collect(); + for stmt in raises { - checker - .diagnostics - .push(Diagnostic::new(RaiseWithinTry, stmt.range())); + let Stmt::Raise(ast::StmtRaise { exc: Some(exception), .. }) = stmt else { + continue; + }; + + // We can't check exception sub-classes without a type-checker implementation, so let's + // just catch the blanket `Exception` for now. + if comparables.contains(&ComparableExpr::from(map_callable(exception))) + || handled_exceptions.iter().any(|expr| { + checker + .semantic() + .resolve_call_path(expr) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["", "Exception" | "BaseException"]) + }) + }) + { + checker + .diagnostics + .push(Diagnostic::new(RaiseWithinTry, stmt.range())); + } } } From 35b04c2fabfcc43b26acf0f5c42bb5eb3d528a1e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 10 Jul 2023 10:49:17 -0400 Subject: [PATCH 391/447] Skip flake8-future-annotations checks in stub files (#5652) Closes https://github.com/astral-sh/ruff/issues/5649. --- crates/ruff/src/checkers/ast/mod.rs | 21 +++++++++++-------- .../tryceratops/rules/raise_within_try.rs | 6 +++++- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index aae885b88b..f319ee7fcf 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -2148,7 +2148,8 @@ where if let Some(operator) = typing::to_pep604_operator(value, slice, &self.semantic) { if self.enabled(Rule::FutureRewritableTypeAnnotation) { - if self.settings.target_version < PythonVersion::Py310 + if !self.is_stub + && self.settings.target_version < PythonVersion::Py310 && self.settings.target_version >= PythonVersion::Py37 && !self.semantic.future_annotations() && self.semantic.in_annotation() @@ -2176,7 +2177,8 @@ where // Ex) list[...] if self.enabled(Rule::FutureRequiredTypeAnnotation) { - if self.settings.target_version < PythonVersion::Py39 + if !self.is_stub + && self.settings.target_version < PythonVersion::Py39 && !self.semantic.future_annotations() && self.semantic.in_annotation() && typing::is_pep585_generic(value, &self.semantic) @@ -2274,15 +2276,16 @@ where typing::to_pep585_generic(expr, &self.semantic) { if self.enabled(Rule::FutureRewritableTypeAnnotation) { - if self.settings.target_version < PythonVersion::Py39 + if !self.is_stub + && self.settings.target_version < PythonVersion::Py39 && 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, - ); + self, expr, + ); } } if self.enabled(Rule::NonPEP585Annotation) { @@ -2351,7 +2354,8 @@ where ]) { if let Some(replacement) = typing::to_pep585_generic(expr, &self.semantic) { if self.enabled(Rule::FutureRewritableTypeAnnotation) { - if self.settings.target_version < PythonVersion::Py39 + if !self.is_stub + && self.settings.target_version < PythonVersion::Py39 && self.settings.target_version >= PythonVersion::Py37 && !self.semantic.future_annotations() && self.semantic.in_annotation() @@ -3143,7 +3147,8 @@ where }) => { // Ex) `str | None` if self.enabled(Rule::FutureRequiredTypeAnnotation) { - if self.settings.target_version < PythonVersion::Py310 + if !self.is_stub + && self.settings.target_version < PythonVersion::Py310 && !self.semantic.future_annotations() && self.semantic.in_annotation() { @@ -3154,7 +3159,6 @@ where ); } } - if self.is_stub { if self.enabled(Rule::DuplicateUnionMember) && self.semantic.in_type_definition() @@ -3166,7 +3170,6 @@ where { flake8_pyi::rules::duplicate_union_member(self, expr); } - if self.enabled(Rule::UnnecessaryLiteralUnion) // Avoid duplicate checks if the parent is an `|` && !matches!( diff --git a/crates/ruff/src/rules/tryceratops/rules/raise_within_try.rs b/crates/ruff/src/rules/tryceratops/rules/raise_within_try.rs index 37711de6f5..90baf84a06 100644 --- a/crates/ruff/src/rules/tryceratops/rules/raise_within_try.rs +++ b/crates/ruff/src/rules/tryceratops/rules/raise_within_try.rs @@ -99,7 +99,11 @@ pub(crate) fn raise_within_try(checker: &mut Checker, body: &[Stmt], handlers: & .collect(); for stmt in raises { - let Stmt::Raise(ast::StmtRaise { exc: Some(exception), .. }) = stmt else { + let Stmt::Raise(ast::StmtRaise { + exc: Some(exception), + .. + }) = stmt + else { continue; }; From ed872145feb993505feef46c2e77d188c5a9f6f6 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 10 Jul 2023 10:51:38 -0400 Subject: [PATCH 392/447] Always allow PEP 585 and PEP 604 rewrites in stub files (#5653) Closes https://github.com/astral-sh/ruff/issues/5640. --- crates/ruff/src/checkers/ast/mod.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index f319ee7fcf..8393ccf963 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -2161,7 +2161,8 @@ where } } if self.enabled(Rule::NonPEP604Annotation) { - if self.settings.target_version >= PythonVersion::Py310 + if self.is_stub + || self.settings.target_version >= PythonVersion::Py310 || (self.settings.target_version >= PythonVersion::Py37 && self.semantic.future_annotations() && self.semantic.in_annotation() @@ -2289,7 +2290,8 @@ where } } if self.enabled(Rule::NonPEP585Annotation) { - if self.settings.target_version >= PythonVersion::Py39 + if self.is_stub + || self.settings.target_version >= PythonVersion::Py39 || (self.settings.target_version >= PythonVersion::Py37 && self.semantic.future_annotations() && self.semantic.in_annotation() @@ -2367,7 +2369,8 @@ where } } if self.enabled(Rule::NonPEP585Annotation) { - if self.settings.target_version >= PythonVersion::Py39 + if self.is_stub + || self.settings.target_version >= PythonVersion::Py39 || (self.settings.target_version >= PythonVersion::Py37 && self.semantic.future_annotations() && self.semantic.in_annotation() @@ -2540,10 +2543,10 @@ where if self.enabled(Rule::OSErrorAlias) { pyupgrade::rules::os_error_alias_call(self, func); } - if self.enabled(Rule::NonPEP604Isinstance) - && self.settings.target_version >= PythonVersion::Py310 - { - pyupgrade::rules::use_pep604_isinstance(self, expr, func, args); + if self.enabled(Rule::NonPEP604Isinstance) { + if self.settings.target_version >= PythonVersion::Py310 { + pyupgrade::rules::use_pep604_isinstance(self, expr, func, args); + } } if self.enabled(Rule::BlockingHttpCallInAsyncFunction) { flake8_async::rules::blocking_http_call(self, expr); From 4cac75bc271104b1b9b80a979b9cb737f5ce5153 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Mon, 10 Jul 2023 16:45:36 +0100 Subject: [PATCH 393/447] Add documentation to `pandas-vet` rules (#5629) ## Summary Completes all the documentation for the `pandas-vet` rules, except for `pandas-use-of-dot-read-table` as I am unclear of the rule's motivation (see #5628). Related to #2646. ## Test Plan `python scripts/check_docs_formatted.py && mkdocs serve` --- .../pandas_vet/rules/assignment_to_df.rs | 23 +++ .../ruff/src/rules/pandas_vet/rules/attr.rs | 31 +++- .../ruff/src/rules/pandas_vet/rules/call.rs | 141 +++++++++++++++++- .../src/rules/pandas_vet/rules/pd_merge.rs | 36 +++++ .../src/rules/pandas_vet/rules/subscript.rs | 112 +++++++++++++- 5 files changed, 331 insertions(+), 12 deletions(-) diff --git a/crates/ruff/src/rules/pandas_vet/rules/assignment_to_df.rs b/crates/ruff/src/rules/pandas_vet/rules/assignment_to_df.rs index 6473d4a7a9..ba66f2950c 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/assignment_to_df.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/assignment_to_df.rs @@ -3,6 +3,29 @@ use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +/// ## What it does +/// Checks for assignments to the variable `df`. +/// +/// ## Why is this bad? +/// Although `df` is a common variable name for a Pandas DataFrame, it's not a +/// great variable name for production code, as it's non-descriptive and +/// prone to name conflicts. +/// +/// Instead, use a more descriptive variable name. +/// +/// ## Example +/// ```python +/// import pandas as pd +/// +/// df = pd.read_csv("animals.csv") +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// +/// animals = pd.read_csv("animals.csv") +/// ``` #[violation] pub struct PandasDfVariableName; diff --git a/crates/ruff/src/rules/pandas_vet/rules/attr.rs b/crates/ruff/src/rules/pandas_vet/rules/attr.rs index fd0de36a96..cb2ec2398d 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/attr.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/attr.rs @@ -8,6 +8,32 @@ use crate::checkers::ast::Checker; use crate::registry::Rule; use crate::rules::pandas_vet::helpers::{test_expression, Resolution}; +/// ## What it does +/// Checks for uses of `.values` on Pandas Series and Index objects. +/// +/// ## Why is this bad? +/// The `.values` attribute is ambiguous as it's return type is unclear. As +/// such, it is no longer recommended by the Pandas documentation. +/// +/// Instead, use `.to_numpy()` to return a NumPy array, or `.array` to return a +/// Pandas `ExtensionArray`. +/// +/// ## Example +/// ```python +/// import pandas as pd +/// +/// animals = pd.read_csv("animals.csv").values # Ambiguous. +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// +/// animals = pd.read_csv("animals.csv").to_numpy() # Explicit. +/// ``` +/// +/// ## References +/// - [Pandas documentation: Accessing the values in a Series or Index](https://pandas.pydata.org/pandas-docs/stable/whatsnew/v0.24.0.html#accessing-the-values-in-a-series-or-index) #[violation] pub struct PandasUseOfDotValues; @@ -19,9 +45,10 @@ impl Violation for PandasUseOfDotValues { } pub(crate) fn attr(checker: &mut Checker, attr: &str, value: &Expr, attr_expr: &Expr) { - let rules = &checker.settings.rules; let violation: DiagnosticKind = match attr { - "values" if rules.enabled(Rule::PandasUseOfDotValues) => PandasUseOfDotValues.into(), + "values" if checker.settings.rules.enabled(Rule::PandasUseOfDotValues) => { + PandasUseOfDotValues.into() + } _ => return, }; diff --git a/crates/ruff/src/rules/pandas_vet/rules/call.rs b/crates/ruff/src/rules/pandas_vet/rules/call.rs index a0ab67ca2c..74bf8de249 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/call.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/call.rs @@ -8,6 +8,36 @@ use crate::checkers::ast::Checker; use crate::registry::Rule; use crate::rules::pandas_vet::helpers::{test_expression, Resolution}; +/// ## What it does +/// Checks for uses of `.isnull` on Pandas objects. +/// +/// ## Why is this bad? +/// In the Pandas API, `.isna` and `.isnull` are equivalent. For consistency, +/// prefer `.isna` over `.isnull`. +/// +/// As a name, `.isna` more accurately reflects the behavior of the method, +/// since these methods check for `NaN` and `NaT` values in addition to `None` +/// values. +/// +/// ## Example +/// ```python +/// import pandas as pd +/// +/// animals_df = pd.read_csv("animals.csv") +/// pd.isnull(animals_df) +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// +/// animals_df = pd.read_csv("animals.csv") +/// pd.isna(animals_df) +/// ``` +/// +/// ## References +/// - [Pandas documentation: `isnull`](https://pandas.pydata.org/docs/reference/api/pandas.isnull.html#pandas.isnull) +/// - [Pandas documentation: `isna`](https://pandas.pydata.org/docs/reference/api/pandas.isna.html#pandas.isna) #[violation] pub struct PandasUseOfDotIsNull; @@ -18,6 +48,36 @@ impl Violation for PandasUseOfDotIsNull { } } +/// ## What it does +/// Checks for uses of `.notnull` on Pandas objects. +/// +/// ## Why is this bad? +/// In the Pandas API, `.notna` and `.notnull` are equivalent. For consistency, +/// prefer `.notna` over `.notnull`. +/// +/// As a name, `.notna` more accurately reflects the behavior of the method, +/// since these methods check for `NaN` and `NaT` values in addition to `None` +/// values. +/// +/// ## Example +/// ```python +/// import pandas as pd +/// +/// animals_df = pd.read_csv("animals.csv") +/// pd.notnull(animals_df) +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// +/// animals_df = pd.read_csv("animals.csv") +/// pd.notna(animals_df) +/// ``` +/// +/// ## References +/// - [Pandas documentation: `notnull`](https://pandas.pydata.org/docs/reference/api/pandas.notnull.html#pandas.notnull) +/// - [Pandas documentation: `notna`](https://pandas.pydata.org/docs/reference/api/pandas.notna.html#pandas.notna) #[violation] pub struct PandasUseOfDotNotNull; @@ -28,6 +88,32 @@ impl Violation for PandasUseOfDotNotNull { } } +/// ## What it does +/// Checks for uses of `.pivot` or `.unstack` on Pandas objects. +/// +/// ## Why is this bad? +/// Prefer `.pivot_table` to `.pivot` or `.unstack`. `.pivot_table` is more general +/// and can be used to implement the same behavior as `.pivot` and `.unstack`. +/// +/// ## Example +/// ```python +/// import pandas as pd +/// +/// df = pd.read_csv("cities.csv") +/// df.pivot(index="city", columns="year", values="population") +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// +/// df = pd.read_csv("cities.csv") +/// df.pivot_table(index="city", columns="year", values="population") +/// ``` +/// +/// ## References +/// - [Pandas documentation: Reshaping and pivot tables](https://pandas.pydata.org/docs/user_guide/reshaping.html) +/// - [Pandas documentation: `pivot_table`](https://pandas.pydata.org/docs/reference/api/pandas.pivot_table.html#pandas.pivot_table) #[violation] pub struct PandasUseOfDotPivotOrUnstack; @@ -40,6 +126,8 @@ impl Violation for PandasUseOfDotPivotOrUnstack { } } +// TODO(tjkuson): Add documentation for this rule once clarified. +// https://github.com/astral-sh/ruff/issues/5628 #[violation] pub struct PandasUseOfDotReadTable; @@ -50,6 +138,32 @@ impl Violation for PandasUseOfDotReadTable { } } +/// ## What it does +/// Checks for uses of `.stack` on Pandas objects. +/// +/// ## Why is this bad? +/// Prefer `.melt` to `.stack`, which has the same functionality but with +/// support for direct column renaming and no dependence on `MultiIndex`. +/// +/// ## Example +/// ```python +/// import pandas as pd +/// +/// cities_df = pd.read_csv("cities.csv") +/// cities_df.set_index("city").stack() +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// +/// cities_df = pd.read_csv("cities.csv") +/// cities_df.melt(id_vars="city") +/// ``` +/// +/// ## References +/// - [Pandas documentation: `melt`](https://pandas.pydata.org/docs/reference/api/pandas.melt.html) +/// - [Pandas documentation: `stack`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.stack.html) #[violation] pub struct PandasUseOfDotStack; @@ -61,20 +175,35 @@ 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 { return; }; let violation: DiagnosticKind = match attr.as_str() { - "isnull" if rules.enabled(Rule::PandasUseOfDotIsNull) => PandasUseOfDotIsNull.into(), - "notnull" if rules.enabled(Rule::PandasUseOfDotNotNull) => PandasUseOfDotNotNull.into(), - "pivot" | "unstack" if rules.enabled(Rule::PandasUseOfDotPivotOrUnstack) => { + "isnull" if checker.settings.rules.enabled(Rule::PandasUseOfDotIsNull) => { + PandasUseOfDotIsNull.into() + } + "notnull" if checker.settings.rules.enabled(Rule::PandasUseOfDotNotNull) => { + PandasUseOfDotNotNull.into() + } + "pivot" | "unstack" + if checker + .settings + .rules + .enabled(Rule::PandasUseOfDotPivotOrUnstack) => + { PandasUseOfDotPivotOrUnstack.into() } - "read_table" if rules.enabled(Rule::PandasUseOfDotReadTable) => { + "read_table" + if checker + .settings + .rules + .enabled(Rule::PandasUseOfDotReadTable) => + { PandasUseOfDotReadTable.into() } - "stack" if rules.enabled(Rule::PandasUseOfDotStack) => PandasUseOfDotStack.into(), + "stack" if checker.settings.rules.enabled(Rule::PandasUseOfDotStack) => { + PandasUseOfDotStack.into() + } _ => return, }; 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 873d5c7f68..a515cd3639 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/pd_merge.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/pd_merge.rs @@ -4,6 +4,42 @@ use crate::checkers::ast::Checker; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +/// ## What it does +/// Checks for uses of `pd.merge` on Pandas objects. +/// +/// ## Why is this bad? +/// In Pandas, the `.merge` method (exposed on, e.g., DataFrame objects) and +/// the `pd.merge` function (exposed on the Pandas module) are equivalent. +/// +/// For consistency, prefer calling `.merge` on an object over calling +/// `pd.merge` on the Pandas module, as the former is more idiomatic. +/// +/// Further, `pd.merge` is not a method, but a function, which prohibits it +/// from being used in method chains, a common pattern in Pandas code. +/// +/// ## Example +/// ```python +/// import pandas as pd +/// +/// cats_df = pd.read_csv("cats.csv") +/// dogs_df = pd.read_csv("dogs.csv") +/// rabbits_df = pd.read_csv("rabbits.csv") +/// pets_df = pd.merge(pd.merge(cats_df, dogs_df), rabbits_df) # Hard to read. +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// +/// cats_df = pd.read_csv("cats.csv") +/// dogs_df = pd.read_csv("dogs.csv") +/// rabbits_df = pd.read_csv("rabbits.csv") +/// pets_df = cats_df.merge(dogs_df).merge(rabbits_df) +/// ``` +/// +/// ## References +/// - [Pandas documentation: `merge`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.merge.html#pandas.DataFrame.merge) +/// - [Pandas documentation: `pd.merge`](https://pandas.pydata.org/docs/reference/api/pandas.merge.html#pandas.merge) #[violation] pub struct PandasUseOfPdMerge; diff --git a/crates/ruff/src/rules/pandas_vet/rules/subscript.rs b/crates/ruff/src/rules/pandas_vet/rules/subscript.rs index 6bcbb8298b..8916c7d29a 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/subscript.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/subscript.rs @@ -8,6 +8,36 @@ use crate::checkers::ast::Checker; use crate::registry::Rule; use crate::rules::pandas_vet::helpers::{test_expression, Resolution}; +/// ## What it does +/// Checks for uses of `.ix` on Pandas objects. +/// +/// ## Why is this bad? +/// The `.ix` method is deprecated as its behavior is ambiguous. Specifically, +/// it's often unclear whether `.ix` is indexing by label or by ordinal position. +/// +/// Instead, prefer the `.loc` method for label-based indexing, and `.iloc` for +/// ordinal indexing. +/// +/// ## Example +/// ```python +/// import pandas as pd +/// +/// students_df = pd.read_csv("students.csv") +/// students_df.ix[0] # 0th row or row with label 0? +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// +/// students_df = pd.read_csv("students.csv") +/// students_df.iloc[0] # 0th row. +/// ``` +/// +/// ## References +/// - [Pandas release notes: Deprecate `.ix`](https://pandas.pydata.org/pandas-docs/version/0.20/whatsnew.html#deprecate-ix) +/// - [Pandas documentation: `loc`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.loc.html) +/// - [Pandas documentation: `iloc`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.iloc.html) #[violation] pub struct PandasUseOfDotIx; @@ -18,6 +48,38 @@ impl Violation for PandasUseOfDotIx { } } +/// ## What it does +/// Checks for uses of `.at` on Pandas objects. +/// +/// ## Why is this bad? +/// The `.at` method selects a single value from a DataFrame or Series based on +/// a label index, and is slightly faster than using `.loc`. However, `.loc` is +/// more idiomatic and versatile, as it can be used to select multiple values at +/// once. +/// +/// If performance is an important consideration, convert the object to a NumPy +/// array, which will provide a much greater performance boost than using `.at` +/// over `.loc`. +/// +/// ## Example +/// ```python +/// import pandas as pd +/// +/// students_df = pd.read_csv("students.csv") +/// students_df.at["Maria"] +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// +/// students_df = pd.read_csv("students.csv") +/// students_df.loc["Maria"] +/// ``` +/// +/// ## References +/// - [Pandas documentation: `loc`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.loc.html) +/// - [Pandas documentation: `at`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.at.html) #[violation] pub struct PandasUseOfDotAt; @@ -28,6 +90,47 @@ impl Violation for PandasUseOfDotAt { } } +/// ## What it does +/// Checks for uses of `.iat` on Pandas objects. +/// +/// ## Why is this bad? +/// The `.iat` method selects a single value from a DataFrame or Series based +/// on an ordinal index, and is slightly faster than using `.iloc`. However, +/// `.iloc` is more idiomatic and versatile, as it can be used to select +/// multiple values at once. +/// +/// If performance is an important consideration, convert the object to a NumPy +/// array, which will provide a much greater performance boost than using `.iat` +/// over `.iloc`. +/// +/// ## Example +/// ```python +/// import pandas as pd +/// +/// students_df = pd.read_csv("students.csv") +/// students_df.iat[0] +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// +/// students_df = pd.read_csv("students.csv") +/// students_df.iloc[0] +/// ``` +/// +/// Or, using NumPy: +/// ```python +/// import numpy as np +/// import pandas as pd +/// +/// students_df = pd.read_csv("students.csv") +/// students_df.to_numpy()[0] +/// ``` +/// +/// ## References +/// - [Pandas documentation: `iloc`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.iloc.html) +/// - [Pandas documentation: `iat`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.iat.html) #[violation] pub struct PandasUseOfDotIat; @@ -43,11 +146,12 @@ pub(crate) fn subscript(checker: &mut Checker, value: &Expr, expr: &Expr) { return; }; - let rules = &checker.settings.rules; let violation: DiagnosticKind = match attr.as_str() { - "ix" if rules.enabled(Rule::PandasUseOfDotIx) => PandasUseOfDotIx.into(), - "at" if rules.enabled(Rule::PandasUseOfDotAt) => PandasUseOfDotAt.into(), - "iat" if rules.enabled(Rule::PandasUseOfDotIat) => PandasUseOfDotIat.into(), + "ix" if checker.settings.rules.enabled(Rule::PandasUseOfDotIx) => PandasUseOfDotIx.into(), + "at" if checker.settings.rules.enabled(Rule::PandasUseOfDotAt) => PandasUseOfDotAt.into(), + "iat" if checker.settings.rules.enabled(Rule::PandasUseOfDotIat) => { + PandasUseOfDotIat.into() + } _ => return, }; From 3562d809b2e1d70b7042d9b6d949bc3c35c4233b Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Mon, 10 Jul 2023 17:28:44 +0100 Subject: [PATCH 394/447] [`pylint`] Implement Pylint `typevar-name-incorrect-variance` (`C0105`) (#5651) ## Summary Implement Pylint `typevar-name-incorrect-variance` (`C0105`) as `type-name-incorrect-variance` (`PLC0105`). Includes documentation. Related to #970. The Pylint implementation checks only `TypeVar`, but this PR checks `ParamSpec` as well. ## Test Plan Added test fixture. `cargo test` --- .../pylint/type_name_incorrect_variance.py | 59 ++++ crates/ruff/src/checkers/ast/mod.rs | 3 + crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/pylint/mod.rs | 4 + crates/ruff/src/rules/pylint/rules/mod.rs | 2 + .../rules/type_name_incorrect_variance.rs | 154 +++++++++ ...C0105_type_name_incorrect_variance.py.snap | 318 ++++++++++++++++++ ruff.schema.json | 2 + 8 files changed, 543 insertions(+) create mode 100644 crates/ruff/resources/test/fixtures/pylint/type_name_incorrect_variance.py create mode 100644 crates/ruff/src/rules/pylint/rules/type_name_incorrect_variance.rs create mode 100644 crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0105_type_name_incorrect_variance.py.snap diff --git a/crates/ruff/resources/test/fixtures/pylint/type_name_incorrect_variance.py b/crates/ruff/resources/test/fixtures/pylint/type_name_incorrect_variance.py new file mode 100644 index 0000000000..33f68d2a04 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pylint/type_name_incorrect_variance.py @@ -0,0 +1,59 @@ +from typing import ParamSpec, TypeVar + +# Errors. + +T = TypeVar("T", covariant=True) +T = TypeVar("T", covariant=True, contravariant=False) +T = TypeVar("T", contravariant=True) +T = TypeVar("T", covariant=False, contravariant=True) +P = ParamSpec("P", covariant=True) +P = ParamSpec("P", covariant=True, contravariant=False) +P = ParamSpec("P", contravariant=True) +P = ParamSpec("P", covariant=False, contravariant=True) + +T_co = TypeVar("T_co") +T_co = TypeVar("T_co", covariant=False) +T_co = TypeVar("T_co", contravariant=False) +T_co = TypeVar("T_co", covariant=False, contravariant=False) +T_co = TypeVar("T_co", contravariant=True) +T_co = TypeVar("T_co", covariant=False, contravariant=True) +P_co = ParamSpec("P_co") +P_co = ParamSpec("P_co", covariant=False) +P_co = ParamSpec("P_co", contravariant=False) +P_co = ParamSpec("P_co", covariant=False, contravariant=False) +P_co = ParamSpec("P_co", contravariant=True) +P_co = ParamSpec("P_co", covariant=False, contravariant=True) + +T_contra = TypeVar("T_contra") +T_contra = TypeVar("T_contra", covariant=False) +T_contra = TypeVar("T_contra", contravariant=False) +T_contra = TypeVar("T_contra", covariant=False, contravariant=False) +T_contra = TypeVar("T_contra", covariant=True) +T_contra = TypeVar("T_contra", covariant=True, contravariant=False) +P_contra = ParamSpec("P_contra") +P_contra = ParamSpec("P_contra", covariant=False) +P_contra = ParamSpec("P_contra", contravariant=False) +P_contra = ParamSpec("P_contra", covariant=False, contravariant=False) +P_contra = ParamSpec("P_contra", covariant=True) +P_contra = ParamSpec("P_contra", covariant=True, contravariant=False) + +# Non-errors. + +T = TypeVar("T") +T = TypeVar("T", covariant=False) +T = TypeVar("T", contravariant=False) +T = TypeVar("T", covariant=False, contravariant=False) +P = ParamSpec("P") +P = ParamSpec("P", covariant=False) +P = ParamSpec("P", contravariant=False) +P = ParamSpec("P", covariant=False, contravariant=False) + +T_co = TypeVar("T_co", covariant=True) +T_co = TypeVar("T_co", covariant=True, contravariant=False) +P_co = ParamSpec("P_co", covariant=True) +P_co = ParamSpec("P_co", covariant=True, contravariant=False) + +T_contra = TypeVar("T_contra", contravariant=True) +T_contra = TypeVar("T_contra", covariant=False, contravariant=True) +P_contra = ParamSpec("P_contra", contravariant=True) +P_contra = ParamSpec("P_contra", covariant=False, contravariant=True) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 8393ccf963..df808a2ef1 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1658,6 +1658,9 @@ where if self.settings.rules.enabled(Rule::TypeParamNameMismatch) { pylint::rules::type_param_name_mismatch(self, value, targets); } + if self.settings.rules.enabled(Rule::TypeNameIncorrectVariance) { + pylint::rules::type_name_incorrect_variance(self, value); + } if self.settings.rules.enabled(Rule::TypeBivariance) { pylint::rules::type_bivariance(self, value); } diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index b73d2caa44..31b80fcff4 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -168,6 +168,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pyflakes, "901") => (RuleGroup::Unspecified, rules::pyflakes::rules::RaiseNotImplemented), // pylint + (Pylint, "C0105") => (RuleGroup::Unspecified, rules::pylint::rules::TypeNameIncorrectVariance), (Pylint, "C0131") => (RuleGroup::Unspecified, rules::pylint::rules::TypeBivariance), (Pylint, "C0132") => (RuleGroup::Unspecified, rules::pylint::rules::TypeParamNameMismatch), (Pylint, "C0205") => (RuleGroup::Unspecified, rules::pylint::rules::SingleStringSlots), diff --git a/crates/ruff/src/rules/pylint/mod.rs b/crates/ruff/src/rules/pylint/mod.rs index 9129aeb506..f6f5330d06 100644 --- a/crates/ruff/src/rules/pylint/mod.rs +++ b/crates/ruff/src/rules/pylint/mod.rs @@ -86,6 +86,10 @@ mod tests { )] #[test_case(Rule::TooManyStatements, Path::new("too_many_statements.py"))] #[test_case(Rule::TypeBivariance, Path::new("type_bivariance.py"))] + #[test_case( + Rule::TypeNameIncorrectVariance, + Path::new("type_name_incorrect_variance.py") + )] #[test_case(Rule::TypeParamNameMismatch, Path::new("type_param_name_mismatch.py"))] #[test_case( Rule::UnexpectedSpecialMethodSignature, diff --git a/crates/ruff/src/rules/pylint/rules/mod.rs b/crates/ruff/src/rules/pylint/rules/mod.rs index ad2b6d0d6c..551abb1ada 100644 --- a/crates/ruff/src/rules/pylint/rules/mod.rs +++ b/crates/ruff/src/rules/pylint/rules/mod.rs @@ -38,6 +38,7 @@ pub(crate) use too_many_branches::*; pub(crate) use too_many_return_statements::*; pub(crate) use too_many_statements::*; pub(crate) use type_bivariance::*; +pub(crate) use type_name_incorrect_variance::*; pub(crate) use type_param_name_mismatch::*; pub(crate) use unexpected_special_method_signature::*; pub(crate) use unnecessary_direct_lambda_call::*; @@ -87,6 +88,7 @@ mod too_many_branches; mod too_many_return_statements; mod too_many_statements; mod type_bivariance; +mod type_name_incorrect_variance; mod type_param_name_mismatch; mod unexpected_special_method_signature; mod unnecessary_direct_lambda_call; diff --git a/crates/ruff/src/rules/pylint/rules/type_name_incorrect_variance.rs b/crates/ruff/src/rules/pylint/rules/type_name_incorrect_variance.rs new file mode 100644 index 0000000000..541f5e7f8f --- /dev/null +++ b/crates/ruff/src/rules/pylint/rules/type_name_incorrect_variance.rs @@ -0,0 +1,154 @@ +use std::fmt; + +use rustpython_parser::ast::{self, Expr, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_true; + +use crate::checkers::ast::Checker; +use crate::rules::pylint::helpers::type_param_name; + +/// ## What it does +/// Checks for type names that do not match the variance of their associated +/// type parameter. +/// +/// ## Why is this bad? +/// [PEP 484] recommends the use of the `_co` and `_contra` suffixes for +/// covariant and contravariant type parameters, respectively (while invariant +/// type parameters should not have any such suffix). +/// +/// ## Example +/// ```python +/// from typing import TypeVar +/// +/// T = TypeVar("T", covariant=True) +/// U = TypeVar("U", contravariant=True) +/// V_co = TypeVar("V_co") +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import TypeVar +/// +/// T_co = TypeVar("T_co", covariant=True) +/// U_contra = TypeVar("U_contra", contravariant=True) +/// V = TypeVar("V") +/// ``` +/// +/// ## References +/// - [Python documentation: `typing` — Support for type hints](https://docs.python.org/3/library/typing.html) +/// - [PEP 483 – The Theory of Type Hints: Covariance and Contravariance](https://peps.python.org/pep-0483/#covariance-and-contravariance) +/// - [PEP 484 – Type Hints: Covariance and contravariance](https://peps.python.org/pep-0484/#covariance-and-contravariance) +/// +/// [PEP 484]: https://www.python.org/dev/peps/pep-0484/ +#[violation] +pub struct TypeNameIncorrectVariance { + kind: VarKind, + param_name: String, +} + +impl Violation for TypeNameIncorrectVariance { + #[derive_message_formats] + fn message(&self) -> String { + let TypeNameIncorrectVariance { kind, param_name } = self; + format!("`{kind}` name \"{param_name}\" does not match variance") + } +} + +/// PLC0105 +pub(crate) fn type_name_incorrect_variance(checker: &mut Checker, value: &Expr) { + let Expr::Call(ast::ExprCall { + func, + args, + keywords, + .. + }) = value + else { + return; + }; + + let Some(param_name) = type_param_name(args, keywords) else { + return; + }; + + let covariant = keywords + .iter() + .find(|keyword| { + keyword + .arg + .as_ref() + .map_or(false, |keyword| keyword.as_str() == "covariant") + }) + .map(|keyword| &keyword.value); + + let contravariant = keywords + .iter() + .find(|keyword| { + keyword + .arg + .as_ref() + .map_or(false, |keyword| keyword.as_str() == "contravariant") + }) + .map(|keyword| &keyword.value); + + if !mismatch(param_name, covariant, contravariant) { + return; + } + + 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 { + None + } + }) + else { + return; + }; + + checker.diagnostics.push(Diagnostic::new( + TypeNameIncorrectVariance { + kind, + param_name: param_name.to_string(), + }, + func.range(), + )); +} + +/// Returns `true` if the parameter name does not match its type variance. +fn mismatch(param_name: &str, covariant: Option<&Expr>, contravariant: Option<&Expr>) -> bool { + if param_name.ends_with("_co") { + covariant.map_or(true, |covariant| !is_const_true(covariant)) + } else if param_name.ends_with("_contra") { + contravariant.map_or(true, |contravariant| !is_const_true(contravariant)) + } else { + covariant.map_or(false, is_const_true) || contravariant.map_or(false, is_const_true) + } +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum VarKind { + TypeVar, + ParamSpec, +} + +impl fmt::Display for VarKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + VarKind::TypeVar => fmt.write_str("TypeVar"), + VarKind::ParamSpec => fmt.write_str("ParamSpec"), + } + } +} diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0105_type_name_incorrect_variance.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0105_type_name_incorrect_variance.py.snap new file mode 100644 index 0000000000..8e54cce037 --- /dev/null +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0105_type_name_incorrect_variance.py.snap @@ -0,0 +1,318 @@ +--- +source: crates/ruff/src/rules/pylint/mod.rs +--- +type_name_incorrect_variance.py:5:5: PLC0105 `TypeVar` name "T" does not match variance + | +3 | # Errors. +4 | +5 | T = TypeVar("T", covariant=True) + | ^^^^^^^ PLC0105 +6 | T = TypeVar("T", covariant=True, contravariant=False) +7 | T = TypeVar("T", contravariant=True) + | + +type_name_incorrect_variance.py:6:5: PLC0105 `TypeVar` name "T" does not match variance + | +5 | T = TypeVar("T", covariant=True) +6 | T = TypeVar("T", covariant=True, contravariant=False) + | ^^^^^^^ PLC0105 +7 | T = TypeVar("T", contravariant=True) +8 | T = TypeVar("T", covariant=False, contravariant=True) + | + +type_name_incorrect_variance.py:7:5: PLC0105 `TypeVar` name "T" does not match variance + | +5 | T = TypeVar("T", covariant=True) +6 | T = TypeVar("T", covariant=True, contravariant=False) +7 | T = TypeVar("T", contravariant=True) + | ^^^^^^^ PLC0105 +8 | T = TypeVar("T", covariant=False, contravariant=True) +9 | P = ParamSpec("P", covariant=True) + | + +type_name_incorrect_variance.py:8:5: PLC0105 `TypeVar` name "T" does not match variance + | + 6 | T = TypeVar("T", covariant=True, contravariant=False) + 7 | T = TypeVar("T", contravariant=True) + 8 | T = TypeVar("T", covariant=False, contravariant=True) + | ^^^^^^^ PLC0105 + 9 | P = ParamSpec("P", covariant=True) +10 | P = ParamSpec("P", covariant=True, contravariant=False) + | + +type_name_incorrect_variance.py:9:5: PLC0105 `ParamSpec` name "P" does not match variance + | + 7 | T = TypeVar("T", contravariant=True) + 8 | T = TypeVar("T", covariant=False, contravariant=True) + 9 | P = ParamSpec("P", covariant=True) + | ^^^^^^^^^ PLC0105 +10 | P = ParamSpec("P", covariant=True, contravariant=False) +11 | P = ParamSpec("P", contravariant=True) + | + +type_name_incorrect_variance.py:10:5: PLC0105 `ParamSpec` name "P" does not match variance + | + 8 | T = TypeVar("T", covariant=False, contravariant=True) + 9 | P = ParamSpec("P", covariant=True) +10 | P = ParamSpec("P", covariant=True, contravariant=False) + | ^^^^^^^^^ PLC0105 +11 | P = ParamSpec("P", contravariant=True) +12 | P = ParamSpec("P", covariant=False, contravariant=True) + | + +type_name_incorrect_variance.py:11:5: PLC0105 `ParamSpec` name "P" does not match variance + | + 9 | P = ParamSpec("P", covariant=True) +10 | P = ParamSpec("P", covariant=True, contravariant=False) +11 | P = ParamSpec("P", contravariant=True) + | ^^^^^^^^^ PLC0105 +12 | P = ParamSpec("P", covariant=False, contravariant=True) + | + +type_name_incorrect_variance.py:12:5: PLC0105 `ParamSpec` name "P" does not match variance + | +10 | P = ParamSpec("P", covariant=True, contravariant=False) +11 | P = ParamSpec("P", contravariant=True) +12 | P = ParamSpec("P", covariant=False, contravariant=True) + | ^^^^^^^^^ PLC0105 +13 | +14 | T_co = TypeVar("T_co") + | + +type_name_incorrect_variance.py:14:8: PLC0105 `TypeVar` name "T_co" does not match variance + | +12 | P = ParamSpec("P", covariant=False, contravariant=True) +13 | +14 | T_co = TypeVar("T_co") + | ^^^^^^^ PLC0105 +15 | T_co = TypeVar("T_co", covariant=False) +16 | T_co = TypeVar("T_co", contravariant=False) + | + +type_name_incorrect_variance.py:15:8: PLC0105 `TypeVar` name "T_co" does not match variance + | +14 | T_co = TypeVar("T_co") +15 | T_co = TypeVar("T_co", covariant=False) + | ^^^^^^^ PLC0105 +16 | T_co = TypeVar("T_co", contravariant=False) +17 | T_co = TypeVar("T_co", covariant=False, contravariant=False) + | + +type_name_incorrect_variance.py:16:8: PLC0105 `TypeVar` name "T_co" does not match variance + | +14 | T_co = TypeVar("T_co") +15 | T_co = TypeVar("T_co", covariant=False) +16 | T_co = TypeVar("T_co", contravariant=False) + | ^^^^^^^ PLC0105 +17 | T_co = TypeVar("T_co", covariant=False, contravariant=False) +18 | T_co = TypeVar("T_co", contravariant=True) + | + +type_name_incorrect_variance.py:17:8: PLC0105 `TypeVar` name "T_co" does not match variance + | +15 | T_co = TypeVar("T_co", covariant=False) +16 | T_co = TypeVar("T_co", contravariant=False) +17 | T_co = TypeVar("T_co", covariant=False, contravariant=False) + | ^^^^^^^ PLC0105 +18 | T_co = TypeVar("T_co", contravariant=True) +19 | T_co = TypeVar("T_co", covariant=False, contravariant=True) + | + +type_name_incorrect_variance.py:18:8: PLC0105 `TypeVar` name "T_co" does not match variance + | +16 | T_co = TypeVar("T_co", contravariant=False) +17 | T_co = TypeVar("T_co", covariant=False, contravariant=False) +18 | T_co = TypeVar("T_co", contravariant=True) + | ^^^^^^^ PLC0105 +19 | T_co = TypeVar("T_co", covariant=False, contravariant=True) +20 | P_co = ParamSpec("P_co") + | + +type_name_incorrect_variance.py:19:8: PLC0105 `TypeVar` name "T_co" does not match variance + | +17 | T_co = TypeVar("T_co", covariant=False, contravariant=False) +18 | T_co = TypeVar("T_co", contravariant=True) +19 | T_co = TypeVar("T_co", covariant=False, contravariant=True) + | ^^^^^^^ PLC0105 +20 | P_co = ParamSpec("P_co") +21 | P_co = ParamSpec("P_co", covariant=False) + | + +type_name_incorrect_variance.py:20:8: PLC0105 `ParamSpec` name "P_co" does not match variance + | +18 | T_co = TypeVar("T_co", contravariant=True) +19 | T_co = TypeVar("T_co", covariant=False, contravariant=True) +20 | P_co = ParamSpec("P_co") + | ^^^^^^^^^ PLC0105 +21 | P_co = ParamSpec("P_co", covariant=False) +22 | P_co = ParamSpec("P_co", contravariant=False) + | + +type_name_incorrect_variance.py:21:8: PLC0105 `ParamSpec` name "P_co" does not match variance + | +19 | T_co = TypeVar("T_co", covariant=False, contravariant=True) +20 | P_co = ParamSpec("P_co") +21 | P_co = ParamSpec("P_co", covariant=False) + | ^^^^^^^^^ PLC0105 +22 | P_co = ParamSpec("P_co", contravariant=False) +23 | P_co = ParamSpec("P_co", covariant=False, contravariant=False) + | + +type_name_incorrect_variance.py:22:8: PLC0105 `ParamSpec` name "P_co" does not match variance + | +20 | P_co = ParamSpec("P_co") +21 | P_co = ParamSpec("P_co", covariant=False) +22 | P_co = ParamSpec("P_co", contravariant=False) + | ^^^^^^^^^ PLC0105 +23 | P_co = ParamSpec("P_co", covariant=False, contravariant=False) +24 | P_co = ParamSpec("P_co", contravariant=True) + | + +type_name_incorrect_variance.py:23:8: PLC0105 `ParamSpec` name "P_co" does not match variance + | +21 | P_co = ParamSpec("P_co", covariant=False) +22 | P_co = ParamSpec("P_co", contravariant=False) +23 | P_co = ParamSpec("P_co", covariant=False, contravariant=False) + | ^^^^^^^^^ PLC0105 +24 | P_co = ParamSpec("P_co", contravariant=True) +25 | P_co = ParamSpec("P_co", covariant=False, contravariant=True) + | + +type_name_incorrect_variance.py:24:8: PLC0105 `ParamSpec` name "P_co" does not match variance + | +22 | P_co = ParamSpec("P_co", contravariant=False) +23 | P_co = ParamSpec("P_co", covariant=False, contravariant=False) +24 | P_co = ParamSpec("P_co", contravariant=True) + | ^^^^^^^^^ PLC0105 +25 | P_co = ParamSpec("P_co", covariant=False, contravariant=True) + | + +type_name_incorrect_variance.py:25:8: PLC0105 `ParamSpec` name "P_co" does not match variance + | +23 | P_co = ParamSpec("P_co", covariant=False, contravariant=False) +24 | P_co = ParamSpec("P_co", contravariant=True) +25 | P_co = ParamSpec("P_co", covariant=False, contravariant=True) + | ^^^^^^^^^ PLC0105 +26 | +27 | T_contra = TypeVar("T_contra") + | + +type_name_incorrect_variance.py:27:12: PLC0105 `TypeVar` name "T_contra" does not match variance + | +25 | P_co = ParamSpec("P_co", covariant=False, contravariant=True) +26 | +27 | T_contra = TypeVar("T_contra") + | ^^^^^^^ PLC0105 +28 | T_contra = TypeVar("T_contra", covariant=False) +29 | T_contra = TypeVar("T_contra", contravariant=False) + | + +type_name_incorrect_variance.py:28:12: PLC0105 `TypeVar` name "T_contra" does not match variance + | +27 | T_contra = TypeVar("T_contra") +28 | T_contra = TypeVar("T_contra", covariant=False) + | ^^^^^^^ PLC0105 +29 | T_contra = TypeVar("T_contra", contravariant=False) +30 | T_contra = TypeVar("T_contra", covariant=False, contravariant=False) + | + +type_name_incorrect_variance.py:29:12: PLC0105 `TypeVar` name "T_contra" does not match variance + | +27 | T_contra = TypeVar("T_contra") +28 | T_contra = TypeVar("T_contra", covariant=False) +29 | T_contra = TypeVar("T_contra", contravariant=False) + | ^^^^^^^ PLC0105 +30 | T_contra = TypeVar("T_contra", covariant=False, contravariant=False) +31 | T_contra = TypeVar("T_contra", covariant=True) + | + +type_name_incorrect_variance.py:30:12: PLC0105 `TypeVar` name "T_contra" does not match variance + | +28 | T_contra = TypeVar("T_contra", covariant=False) +29 | T_contra = TypeVar("T_contra", contravariant=False) +30 | T_contra = TypeVar("T_contra", covariant=False, contravariant=False) + | ^^^^^^^ PLC0105 +31 | T_contra = TypeVar("T_contra", covariant=True) +32 | T_contra = TypeVar("T_contra", covariant=True, contravariant=False) + | + +type_name_incorrect_variance.py:31:12: PLC0105 `TypeVar` name "T_contra" does not match variance + | +29 | T_contra = TypeVar("T_contra", contravariant=False) +30 | T_contra = TypeVar("T_contra", covariant=False, contravariant=False) +31 | T_contra = TypeVar("T_contra", covariant=True) + | ^^^^^^^ PLC0105 +32 | T_contra = TypeVar("T_contra", covariant=True, contravariant=False) +33 | P_contra = ParamSpec("P_contra") + | + +type_name_incorrect_variance.py:32:12: PLC0105 `TypeVar` name "T_contra" does not match variance + | +30 | T_contra = TypeVar("T_contra", covariant=False, contravariant=False) +31 | T_contra = TypeVar("T_contra", covariant=True) +32 | T_contra = TypeVar("T_contra", covariant=True, contravariant=False) + | ^^^^^^^ PLC0105 +33 | P_contra = ParamSpec("P_contra") +34 | P_contra = ParamSpec("P_contra", covariant=False) + | + +type_name_incorrect_variance.py:33:12: PLC0105 `ParamSpec` name "P_contra" does not match variance + | +31 | T_contra = TypeVar("T_contra", covariant=True) +32 | T_contra = TypeVar("T_contra", covariant=True, contravariant=False) +33 | P_contra = ParamSpec("P_contra") + | ^^^^^^^^^ PLC0105 +34 | P_contra = ParamSpec("P_contra", covariant=False) +35 | P_contra = ParamSpec("P_contra", contravariant=False) + | + +type_name_incorrect_variance.py:34:12: PLC0105 `ParamSpec` name "P_contra" does not match variance + | +32 | T_contra = TypeVar("T_contra", covariant=True, contravariant=False) +33 | P_contra = ParamSpec("P_contra") +34 | P_contra = ParamSpec("P_contra", covariant=False) + | ^^^^^^^^^ PLC0105 +35 | P_contra = ParamSpec("P_contra", contravariant=False) +36 | P_contra = ParamSpec("P_contra", covariant=False, contravariant=False) + | + +type_name_incorrect_variance.py:35:12: PLC0105 `ParamSpec` name "P_contra" does not match variance + | +33 | P_contra = ParamSpec("P_contra") +34 | P_contra = ParamSpec("P_contra", covariant=False) +35 | P_contra = ParamSpec("P_contra", contravariant=False) + | ^^^^^^^^^ PLC0105 +36 | P_contra = ParamSpec("P_contra", covariant=False, contravariant=False) +37 | P_contra = ParamSpec("P_contra", covariant=True) + | + +type_name_incorrect_variance.py:36:12: PLC0105 `ParamSpec` name "P_contra" does not match variance + | +34 | P_contra = ParamSpec("P_contra", covariant=False) +35 | P_contra = ParamSpec("P_contra", contravariant=False) +36 | P_contra = ParamSpec("P_contra", covariant=False, contravariant=False) + | ^^^^^^^^^ PLC0105 +37 | P_contra = ParamSpec("P_contra", covariant=True) +38 | P_contra = ParamSpec("P_contra", covariant=True, contravariant=False) + | + +type_name_incorrect_variance.py:37:12: PLC0105 `ParamSpec` name "P_contra" does not match variance + | +35 | P_contra = ParamSpec("P_contra", contravariant=False) +36 | P_contra = ParamSpec("P_contra", covariant=False, contravariant=False) +37 | P_contra = ParamSpec("P_contra", covariant=True) + | ^^^^^^^^^ PLC0105 +38 | P_contra = ParamSpec("P_contra", covariant=True, contravariant=False) + | + +type_name_incorrect_variance.py:38:12: PLC0105 `ParamSpec` name "P_contra" does not match variance + | +36 | P_contra = ParamSpec("P_contra", covariant=False, contravariant=False) +37 | P_contra = ParamSpec("P_contra", covariant=True) +38 | P_contra = ParamSpec("P_contra", covariant=True, contravariant=False) + | ^^^^^^^^^ PLC0105 +39 | +40 | # Non-errors. + | + + diff --git a/ruff.schema.json b/ruff.schema.json index f283f253b8..15cc68f752 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2129,6 +2129,8 @@ "PLC", "PLC0", "PLC01", + "PLC010", + "PLC0105", "PLC013", "PLC0131", "PLC0132", From 28fe2d334ae67d1a61c1190843702f27c6ad1028 Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Mon, 10 Jul 2023 11:32:41 -0500 Subject: [PATCH 395/447] Implement `UnnecessaryListAllocationForFirstElement` (#5549) ## Summary Fixes #5503. Ready for final review as the `mkdocs` issue involving SSH keys is fixed. Note that this will only throw on a `Name` - it will be refactorable once we have a type-checker. This means that this is the only sort of input that will throw. ```python x = range(10) list(x)[0] ``` I thought it'd be confusing if we supported direct function results. Consider this example, assuming we support direct results: ```python # throws list(range(10))[0] def createRange(bound): return range(bound) # "why doesn't this throw, but a direct `range(10)` call does?" list(createRange(10))[0] ``` If it's necessary, I can go through the list of built-ins and find those which produce iterables, then add them to the throwing list. ## Test Plan Added a new fixture, then ran `cargo t` --- .../resources/test/fixtures/ruff/RUF015.py | 44 +++ crates/ruff/src/checkers/ast/mod.rs | 5 +- crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/ruff/mod.rs | 4 + crates/ruff/src/rules/ruff/rules/mod.rs | 2 + ...y_iterable_allocation_for_first_element.rs | 236 ++++++++++++ ..._rules__ruff__tests__RUF015_RUF015.py.snap | 338 ++++++++++++++++++ ruff.schema.json | 1 + 8 files changed, 630 insertions(+), 1 deletion(-) create mode 100644 crates/ruff/resources/test/fixtures/ruff/RUF015.py create mode 100644 crates/ruff/src/rules/ruff/rules/unnecessary_iterable_allocation_for_first_element.rs create mode 100644 crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF015_RUF015.py.snap diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF015.py b/crates/ruff/resources/test/fixtures/ruff/RUF015.py new file mode 100644 index 0000000000..e19602de8d --- /dev/null +++ b/crates/ruff/resources/test/fixtures/ruff/RUF015.py @@ -0,0 +1,44 @@ +x = range(10) + +# RUF015 +list(x)[0] +list(x)[:1] +list(x)[:1:1] +list(x)[:1:2] +tuple(x)[0] +tuple(x)[:1] +tuple(x)[:1:1] +tuple(x)[:1:2] +list(i for i in x)[0] +list(i for i in x)[:1] +list(i for i in x)[:1:1] +list(i for i in x)[:1:2] +[i for i in x][0] +[i for i in x][:1] +[i for i in x][:1:1] +[i for i in x][:1:2] + +# OK (not indexing (solely) the first element) +list(x) +list(x)[1] +list(x)[-1] +list(x)[1:] +list(x)[:3:2] +list(x)[::2] +list(x)[::] +[i for i in x] +[i for i in x][1] +[i for i in x][-1] +[i for i in x][1:] +[i for i in x][:3:2] +[i for i in x][::2] +[i for i in x][::] + +# OK (doesn't mirror the underlying list) +[i + 1 for i in x][0] +[i for i in x if i > 5][0] +[(i, i + 1) for i in x][0] + +# OK (multiple generators) +y = range(10) +[i + j for i in x for j in y][0] diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index df808a2ef1..884f9efb7c 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -2142,7 +2142,7 @@ where // Pre-visit. match expr { - Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { + subscript @ Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { // Ex) Optional[...], Union[...] if self.any_enabled(&[ Rule::FutureRewritableTypeAnnotation, @@ -2225,6 +2225,9 @@ where if self.enabled(Rule::UncapitalizedEnvironmentVariables) { flake8_simplify::rules::use_capital_environment_variables(self, expr); } + if self.enabled(Rule::UnnecessaryIterableAllocationForFirstElement) { + ruff::rules::unnecessary_iterable_allocation_for_first_element(self, subscript); + } pandas_vet::rules::subscript(self, value, expr); } diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 31b80fcff4..9e9f01e9fb 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -779,6 +779,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "013") => (RuleGroup::Unspecified, rules::ruff::rules::ImplicitOptional), #[cfg(feature = "unreachable-code")] (Ruff, "014") => (RuleGroup::Nursery, rules::ruff::rules::UnreachableCode), + (Ruff, "015") => (RuleGroup::Unspecified, rules::ruff::rules::UnnecessaryIterableAllocationForFirstElement), (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 336a65c7e1..1b11784cab 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"))] + #[test_case( + Rule::UnnecessaryIterableAllocationForFirstElement, + Path::new("RUF015.py") + )] #[cfg_attr( feature = "unreachable-code", test_case(Rule::UnreachableCode, Path::new("RUF014.py")) diff --git a/crates/ruff/src/rules/ruff/rules/mod.rs b/crates/ruff/src/rules/ruff/rules/mod.rs index f79f5fafd7..e6654f1204 100644 --- a/crates/ruff/src/rules/ruff/rules/mod.rs +++ b/crates/ruff/src/rules/ruff/rules/mod.rs @@ -9,6 +9,7 @@ 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::*; +pub(crate) use unnecessary_iterable_allocation_for_first_element::*; #[cfg(feature = "unreachable-code")] pub(crate) use unreachable::*; pub(crate) use unused_noqa::*; @@ -26,6 +27,7 @@ mod mutable_class_default; mod mutable_dataclass_default; mod pairwise_over_zipped; mod static_key_dict_comprehension; +mod unnecessary_iterable_allocation_for_first_element; #[cfg(feature = "unreachable-code")] pub(crate) mod unreachable; mod unused_noqa; diff --git a/crates/ruff/src/rules/ruff/rules/unnecessary_iterable_allocation_for_first_element.rs b/crates/ruff/src/rules/ruff/rules/unnecessary_iterable_allocation_for_first_element.rs new file mode 100644 index 0000000000..62f2cca624 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/unnecessary_iterable_allocation_for_first_element.rs @@ -0,0 +1,236 @@ +use num_bigint::BigInt; +use num_traits::{One, Zero}; +use rustpython_parser::ast::{self, Comprehension, Constant, Expr}; + +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_semantic::SemanticModel; + +use crate::checkers::ast::Checker; +use crate::registry::AsRule; + +/// ## What it does +/// Checks for uses of `list(...)[0]` that can be replaced with +/// `next(iter(...))`. +/// +/// ## Why is this bad? +/// Calling `list(...)` will create a new list of the entire collection, which +/// can be very expensive for large collections. If you only need the first +/// element of the collection, you can use `next(iter(...))` to lazily fetch +/// the first element without creating a new list. +/// +/// Note that migrating from `list(...)[0]` to `next(iter(...))` can change +/// the behavior of your program in two ways: +/// +/// 1. First, `list(...)` will eagerly evaluate the entire collection, while +/// `next(iter(...))` will only evaluate the first element. As such, any +/// side effects that occur during iteration will be delayed. +/// 2. Second, `list(...)[0]` will raise `IndexError` if the collection is +/// empty, while `next(iter(...))` will raise `StopIteration`. +/// +/// ## Example +/// ```python +/// head = list(range(1000000000000))[0] +/// ``` +/// +/// Use instead: +/// ```python +/// head = next(iter(range(1000000000000))) +/// ``` +/// +/// ## References +/// - [Iterators and Iterables in Python: Run Efficient Iterations](https://realpython.com/python-iterators-iterables/#when-to-use-an-iterator-in-python) +#[violation] +pub(crate) struct UnnecessaryIterableAllocationForFirstElement { + iterable: String, + subscript_kind: HeadSubscriptKind, +} + +impl AlwaysAutofixableViolation for UnnecessaryIterableAllocationForFirstElement { + #[derive_message_formats] + fn message(&self) -> String { + let UnnecessaryIterableAllocationForFirstElement { + iterable, + subscript_kind, + } = self; + match subscript_kind { + HeadSubscriptKind::Index => { + format!("Prefer `next(iter({iterable}))` over `list({iterable})[0]`") + } + HeadSubscriptKind::Slice => { + format!("Prefer `[next(iter({iterable}))]` over `list({iterable})[:1]`") + } + } + } + + fn autofix_title(&self) -> String { + let UnnecessaryIterableAllocationForFirstElement { + iterable, + subscript_kind, + } = self; + match subscript_kind { + HeadSubscriptKind::Index => format!("Replace with `next(iter({iterable}))`"), + HeadSubscriptKind::Slice => format!("Replace with `[next(iter({iterable}))]"), + } + } +} + +/// RUF015 +pub(crate) fn unnecessary_iterable_allocation_for_first_element( + checker: &mut Checker, + subscript: &Expr, +) { + let Expr::Subscript(ast::ExprSubscript { + value, + slice, + range, + .. + }) = subscript + else { + return; + }; + + let Some(subscript_kind) = classify_subscript(slice) else { + return; + }; + + let Some(iterable) = iterable_name(value, checker.semantic()) else { + return; + }; + + let mut diagnostic = Diagnostic::new( + UnnecessaryIterableAllocationForFirstElement { + iterable: iterable.to_string(), + subscript_kind, + }, + *range, + ); + + if checker.patch(diagnostic.kind.rule()) { + let replacement = match subscript_kind { + HeadSubscriptKind::Index => format!("next(iter({iterable}))"), + HeadSubscriptKind::Slice => format!("[next(iter({iterable}))]"), + }; + diagnostic.set_fix(Fix::suggested(Edit::range_replacement(replacement, *range))); + } + + checker.diagnostics.push(diagnostic); +} + +/// A subscript slice that represents the first element of a list. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum HeadSubscriptKind { + /// The subscript is an index (e.g., `[0]`). + Index, + /// The subscript is a slice (e.g., `[:1]`). + Slice, +} + +/// Check that the slice [`Expr`] is functionally equivalent to slicing into the first element. The +/// first `bool` checks that the element is in fact first, the second checks if it's a slice or an +/// index. +fn classify_subscript(expr: &Expr) -> Option { + match expr { + Expr::Constant(ast::ExprConstant { + value: Constant::Int(value), + .. + }) if value.is_zero() => Some(HeadSubscriptKind::Index), + Expr::Slice(ast::ExprSlice { + step, lower, upper, .. + }) => { + // Avoid, e.g., `list(...)[:2]` + let upper = upper.as_ref()?; + let upper = as_int(upper)?; + if !upper.is_one() { + return None; + } + + // Avoid, e.g., `list(...)[2:]`. + if let Some(lower) = lower.as_ref() { + let lower = as_int(lower)?; + if !lower.is_zero() { + return None; + } + } + + // Avoid, e.g., `list(...)[::-1]` + if let Some(step) = step.as_ref() { + let step = as_int(step)?; + if step < upper { + return None; + } + } + + Some(HeadSubscriptKind::Slice) + } + _ => None, + } +} + +/// Fetch the name of the iterable from an expression if the expression returns an unmodified list +/// which can be sliced into. +fn iterable_name<'a>(expr: &'a Expr, model: &SemanticModel) -> Option<&'a str> { + match expr { + Expr::Call(ast::ExprCall { func, args, .. }) => { + let ast::ExprName { id, .. } = func.as_name_expr()?; + + if !matches!(id.as_str(), "tuple" | "list") { + return None; + } + + if !model.is_builtin(id.as_str()) { + return None; + } + + match args.first() { + Some(Expr::Name(ast::ExprName { id: arg_name, .. })) => Some(arg_name.as_str()), + Some(Expr::GeneratorExp(ast::ExprGeneratorExp { + elt, generators, .. + })) => generator_iterable(elt, generators), + _ => None, + } + } + Expr::ListComp(ast::ExprListComp { + elt, generators, .. + }) => generator_iterable(elt, generators), + _ => None, + } +} + +/// Given a comprehension, returns the name of the iterable over which it iterates, if it's +/// a simple comprehension (e.g., `x` for `[i for i in x]`). +fn generator_iterable<'a>(elt: &'a Expr, generators: &'a Vec) -> Option<&'a str> { + // If the `elt` field is anything other than a [`Expr::Name`], we can't be sure that it + // doesn't modify the elements of the underlying iterator (e.g., `[i + 1 for i in x][0]`). + if !elt.is_name_expr() { + return None; + } + + // If there's more than 1 generator, we can't safely say that it fits the diagnostic conditions + // (e.g., `[(i, j) for i in x for j in y][0]`). + let [generator] = generators.as_slice() else { + return None; + }; + + // Ignore if there's an `if` statement in the comprehension, since it filters the list. + if !generator.ifs.is_empty() { + return None; + } + + let ast::ExprName { id, .. } = generator.iter.as_name_expr()?; + Some(id.as_str()) +} + +/// If an expression is a constant integer, returns the value of that integer; otherwise, +/// returns `None`. +fn as_int(expr: &Expr) -> Option<&BigInt> { + if let Expr::Constant(ast::ExprConstant { + value: Constant::Int(value), + .. + }) = expr + { + Some(value) + } else { + None + } +} diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF015_RUF015.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF015_RUF015.py.snap new file mode 100644 index 0000000000..4eb011f0fe --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF015_RUF015.py.snap @@ -0,0 +1,338 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF015.py:4:1: RUF015 [*] Prefer `next(iter(x))` over `list(x)[0]` + | +3 | # RUF015 +4 | list(x)[0] + | ^^^^^^^^^^ RUF015 +5 | list(x)[:1] +6 | list(x)[:1:1] + | + = help: Replace with `next(iter(x))` + +ℹ Suggested fix +1 1 | x = range(10) +2 2 | +3 3 | # RUF015 +4 |-list(x)[0] + 4 |+next(iter(x)) +5 5 | list(x)[:1] +6 6 | list(x)[:1:1] +7 7 | list(x)[:1:2] + +RUF015.py:5:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | +3 | # RUF015 +4 | list(x)[0] +5 | list(x)[:1] + | ^^^^^^^^^^^ RUF015 +6 | list(x)[:1:1] +7 | list(x)[:1:2] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +2 2 | +3 3 | # RUF015 +4 4 | list(x)[0] +5 |-list(x)[:1] + 5 |+[next(iter(x))] +6 6 | list(x)[:1:1] +7 7 | list(x)[:1:2] +8 8 | tuple(x)[0] + +RUF015.py:6:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | +4 | list(x)[0] +5 | list(x)[:1] +6 | list(x)[:1:1] + | ^^^^^^^^^^^^^ RUF015 +7 | list(x)[:1:2] +8 | tuple(x)[0] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +3 3 | # RUF015 +4 4 | list(x)[0] +5 5 | list(x)[:1] +6 |-list(x)[:1:1] + 6 |+[next(iter(x))] +7 7 | list(x)[:1:2] +8 8 | tuple(x)[0] +9 9 | tuple(x)[:1] + +RUF015.py:7:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | +5 | list(x)[:1] +6 | list(x)[:1:1] +7 | list(x)[:1:2] + | ^^^^^^^^^^^^^ RUF015 +8 | tuple(x)[0] +9 | tuple(x)[:1] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +4 4 | list(x)[0] +5 5 | list(x)[:1] +6 6 | list(x)[:1:1] +7 |-list(x)[:1:2] + 7 |+[next(iter(x))] +8 8 | tuple(x)[0] +9 9 | tuple(x)[:1] +10 10 | tuple(x)[:1:1] + +RUF015.py:8:1: RUF015 [*] Prefer `next(iter(x))` over `list(x)[0]` + | + 6 | list(x)[:1:1] + 7 | list(x)[:1:2] + 8 | tuple(x)[0] + | ^^^^^^^^^^^ RUF015 + 9 | tuple(x)[:1] +10 | tuple(x)[:1:1] + | + = help: Replace with `next(iter(x))` + +ℹ Suggested fix +5 5 | list(x)[:1] +6 6 | list(x)[:1:1] +7 7 | list(x)[:1:2] +8 |-tuple(x)[0] + 8 |+next(iter(x)) +9 9 | tuple(x)[:1] +10 10 | tuple(x)[:1:1] +11 11 | tuple(x)[:1:2] + +RUF015.py:9:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | + 7 | list(x)[:1:2] + 8 | tuple(x)[0] + 9 | tuple(x)[:1] + | ^^^^^^^^^^^^ RUF015 +10 | tuple(x)[:1:1] +11 | tuple(x)[:1:2] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +6 6 | list(x)[:1:1] +7 7 | list(x)[:1:2] +8 8 | tuple(x)[0] +9 |-tuple(x)[:1] + 9 |+[next(iter(x))] +10 10 | tuple(x)[:1:1] +11 11 | tuple(x)[:1:2] +12 12 | list(i for i in x)[0] + +RUF015.py:10:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | + 8 | tuple(x)[0] + 9 | tuple(x)[:1] +10 | tuple(x)[:1:1] + | ^^^^^^^^^^^^^^ RUF015 +11 | tuple(x)[:1:2] +12 | list(i for i in x)[0] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +7 7 | list(x)[:1:2] +8 8 | tuple(x)[0] +9 9 | tuple(x)[:1] +10 |-tuple(x)[:1:1] + 10 |+[next(iter(x))] +11 11 | tuple(x)[:1:2] +12 12 | list(i for i in x)[0] +13 13 | list(i for i in x)[:1] + +RUF015.py:11:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | + 9 | tuple(x)[:1] +10 | tuple(x)[:1:1] +11 | tuple(x)[:1:2] + | ^^^^^^^^^^^^^^ RUF015 +12 | list(i for i in x)[0] +13 | list(i for i in x)[:1] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +8 8 | tuple(x)[0] +9 9 | tuple(x)[:1] +10 10 | tuple(x)[:1:1] +11 |-tuple(x)[:1:2] + 11 |+[next(iter(x))] +12 12 | list(i for i in x)[0] +13 13 | list(i for i in x)[:1] +14 14 | list(i for i in x)[:1:1] + +RUF015.py:12:1: RUF015 [*] Prefer `next(iter(x))` over `list(x)[0]` + | +10 | tuple(x)[:1:1] +11 | tuple(x)[:1:2] +12 | list(i for i in x)[0] + | ^^^^^^^^^^^^^^^^^^^^^ RUF015 +13 | list(i for i in x)[:1] +14 | list(i for i in x)[:1:1] + | + = help: Replace with `next(iter(x))` + +ℹ Suggested fix +9 9 | tuple(x)[:1] +10 10 | tuple(x)[:1:1] +11 11 | tuple(x)[:1:2] +12 |-list(i for i in x)[0] + 12 |+next(iter(x)) +13 13 | list(i for i in x)[:1] +14 14 | list(i for i in x)[:1:1] +15 15 | list(i for i in x)[:1:2] + +RUF015.py:13:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | +11 | tuple(x)[:1:2] +12 | list(i for i in x)[0] +13 | list(i for i in x)[:1] + | ^^^^^^^^^^^^^^^^^^^^^^ RUF015 +14 | list(i for i in x)[:1:1] +15 | list(i for i in x)[:1:2] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +10 10 | tuple(x)[:1:1] +11 11 | tuple(x)[:1:2] +12 12 | list(i for i in x)[0] +13 |-list(i for i in x)[:1] + 13 |+[next(iter(x))] +14 14 | list(i for i in x)[:1:1] +15 15 | list(i for i in x)[:1:2] +16 16 | [i for i in x][0] + +RUF015.py:14:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | +12 | list(i for i in x)[0] +13 | list(i for i in x)[:1] +14 | list(i for i in x)[:1:1] + | ^^^^^^^^^^^^^^^^^^^^^^^^ RUF015 +15 | list(i for i in x)[:1:2] +16 | [i for i in x][0] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +11 11 | tuple(x)[:1:2] +12 12 | list(i for i in x)[0] +13 13 | list(i for i in x)[:1] +14 |-list(i for i in x)[:1:1] + 14 |+[next(iter(x))] +15 15 | list(i for i in x)[:1:2] +16 16 | [i for i in x][0] +17 17 | [i for i in x][:1] + +RUF015.py:15:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | +13 | list(i for i in x)[:1] +14 | list(i for i in x)[:1:1] +15 | list(i for i in x)[:1:2] + | ^^^^^^^^^^^^^^^^^^^^^^^^ RUF015 +16 | [i for i in x][0] +17 | [i for i in x][:1] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +12 12 | list(i for i in x)[0] +13 13 | list(i for i in x)[:1] +14 14 | list(i for i in x)[:1:1] +15 |-list(i for i in x)[:1:2] + 15 |+[next(iter(x))] +16 16 | [i for i in x][0] +17 17 | [i for i in x][:1] +18 18 | [i for i in x][:1:1] + +RUF015.py:16:1: RUF015 [*] Prefer `next(iter(x))` over `list(x)[0]` + | +14 | list(i for i in x)[:1:1] +15 | list(i for i in x)[:1:2] +16 | [i for i in x][0] + | ^^^^^^^^^^^^^^^^^ RUF015 +17 | [i for i in x][:1] +18 | [i for i in x][:1:1] + | + = help: Replace with `next(iter(x))` + +ℹ Suggested fix +13 13 | list(i for i in x)[:1] +14 14 | list(i for i in x)[:1:1] +15 15 | list(i for i in x)[:1:2] +16 |-[i for i in x][0] + 16 |+next(iter(x)) +17 17 | [i for i in x][:1] +18 18 | [i for i in x][:1:1] +19 19 | [i for i in x][:1:2] + +RUF015.py:17:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | +15 | list(i for i in x)[:1:2] +16 | [i for i in x][0] +17 | [i for i in x][:1] + | ^^^^^^^^^^^^^^^^^^ RUF015 +18 | [i for i in x][:1:1] +19 | [i for i in x][:1:2] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +14 14 | list(i for i in x)[:1:1] +15 15 | list(i for i in x)[:1:2] +16 16 | [i for i in x][0] +17 |-[i for i in x][:1] + 17 |+[next(iter(x))] +18 18 | [i for i in x][:1:1] +19 19 | [i for i in x][:1:2] +20 20 | + +RUF015.py:18:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | +16 | [i for i in x][0] +17 | [i for i in x][:1] +18 | [i for i in x][:1:1] + | ^^^^^^^^^^^^^^^^^^^^ RUF015 +19 | [i for i in x][:1:2] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +15 15 | list(i for i in x)[:1:2] +16 16 | [i for i in x][0] +17 17 | [i for i in x][:1] +18 |-[i for i in x][:1:1] + 18 |+[next(iter(x))] +19 19 | [i for i in x][:1:2] +20 20 | +21 21 | # OK (not indexing (solely) the first element) + +RUF015.py:19:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | +17 | [i for i in x][:1] +18 | [i for i in x][:1:1] +19 | [i for i in x][:1:2] + | ^^^^^^^^^^^^^^^^^^^^ RUF015 +20 | +21 | # OK (not indexing (solely) the first element) + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +16 16 | [i for i in x][0] +17 17 | [i for i in x][:1] +18 18 | [i for i in x][:1:1] +19 |-[i for i in x][:1:2] + 19 |+[next(iter(x))] +20 20 | +21 21 | # OK (not indexing (solely) the first element) +22 22 | list(x) + + diff --git a/ruff.schema.json b/ruff.schema.json index 15cc68f752..84345a1cf2 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2403,6 +2403,7 @@ "RUF012", "RUF013", "RUF014", + "RUF015", "RUF1", "RUF10", "RUF100", From 120e9d37f16aba3519baee8f24a951336b01d9c5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 10 Jul 2023 13:10:08 -0400 Subject: [PATCH 396/447] Audit some `SemanticModel#is_builtin` usages (#5659) ## Summary Non-behavior-changing refactors to delay some `.is_builtin` calls in a few older rules. Cheaper pre-conditions should always be checked first. --- .../flake8_blind_except/rules/blind_except.rs | 87 ++++++------ .../rules/open_file_with_context_handler.rs | 54 +++---- .../pylint/rules/property_with_parameters.rs | 2 +- .../src/rules/pylint/rules/sys_exit_alias.rs | 51 +++---- .../rules/pyupgrade/rules/native_literals.rs | 124 ++++++++-------- .../pyupgrade/rules/redundant_open_modes.rs | 133 ++++++++++-------- 6 files changed, 237 insertions(+), 214 deletions(-) diff --git a/crates/ruff/src/rules/flake8_blind_except/rules/blind_except.rs b/crates/ruff/src/rules/flake8_blind_except/rules/blind_except.rs index b5f0eae9e9..f0650afdda 100644 --- a/crates/ruff/src/rules/flake8_blind_except/rules/blind_except.rs +++ b/crates/ruff/src/rules/flake8_blind_except/rules/blind_except.rs @@ -63,59 +63,64 @@ pub(crate) fn blind_except( let Expr::Name(ast::ExprName { id, .. }) = &type_ else { return; }; - for exception in ["BaseException", "Exception"] { - if id == exception && checker.semantic().is_builtin(exception) { - // If the exception is re-raised, don't flag an error. - if body.iter().any(|stmt| { - if let Stmt::Raise(ast::StmtRaise { exc, .. }) = stmt { - if let Some(exc) = exc { - if let Expr::Name(ast::ExprName { id, .. }) = exc.as_ref() { - name.map_or(false, |name| id == name) - } else { - false - } - } else { - true - } + + if !matches!(id.as_str(), "BaseException" | "Exception") { + return; + } + + if !checker.semantic().is_builtin(id) { + return; + } + + // If the exception is re-raised, don't flag an error. + if body.iter().any(|stmt| { + if let Stmt::Raise(ast::StmtRaise { exc, .. }) = stmt { + if let Some(exc) = exc { + if let Expr::Name(ast::ExprName { id, .. }) = exc.as_ref() { + name.map_or(false, |name| id == name) } else { false } - }) { - continue; + } else { + true } + } else { + false + } + }) { + return; + } - // If the exception is logged, don't flag an error. - if body.iter().any(|stmt| { - if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt { - if let Expr::Call(ast::ExprCall { func, keywords, .. }) = value.as_ref() { - if logging::is_logger_candidate(func, checker.semantic()) { - if let Some(attribute) = func.as_attribute_expr() { - let attr = attribute.attr.as_str(); - if attr == "exception" { + // If the exception is logged, don't flag an error. + if body.iter().any(|stmt| { + if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt { + if let Expr::Call(ast::ExprCall { func, keywords, .. }) = value.as_ref() { + if logging::is_logger_candidate(func, checker.semantic()) { + if let Some(attribute) = func.as_attribute_expr() { + let attr = attribute.attr.as_str(); + if attr == "exception" { + return true; + } + if attr == "error" { + if let Some(keyword) = find_keyword(keywords, "exc_info") { + if is_const_true(&keyword.value) { return true; } - if attr == "error" { - if let Some(keyword) = find_keyword(keywords, "exc_info") { - if is_const_true(&keyword.value) { - return true; - } - } - } } } } } - false - }) { - continue; } - - checker.diagnostics.push(Diagnostic::new( - BlindExcept { - name: id.to_string(), - }, - type_.range(), - )); } + false + }) { + return; } + + checker.diagnostics.push(Diagnostic::new( + BlindExcept { + name: id.to_string(), + }, + type_.range(), + )); } 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 090d9c3e1d..41da294234 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 @@ -107,32 +107,34 @@ fn match_exit_stack(semantic: &SemanticModel) -> bool { /// SIM115 pub(crate) fn open_file_with_context_handler(checker: &mut Checker, func: &Expr) { - if checker - .semantic() - .resolve_call_path(func) - .map_or(false, |call_path| { - matches!(call_path.as_slice(), ["", "open"]) - }) - { - if checker.semantic().is_builtin("open") { - // Ex) `with open("foo.txt") as f: ...` - if matches!(checker.semantic().stmt(), Stmt::With(_)) { - return; - } + let Expr::Name(ast::ExprName { id, .. }) = func else { + return; + }; - // Ex) `with contextlib.ExitStack() as exit_stack: ...` - if match_exit_stack(checker.semantic()) { - return; - } - - // Ex) `with contextlib.AsyncExitStack() as exit_stack: ...` - if match_async_exit_stack(checker.semantic()) { - return; - } - - checker - .diagnostics - .push(Diagnostic::new(OpenFileWithContextHandler, func.range())); - } + if id.as_str() != "open" { + return; } + + // Ex) `with open("foo.txt") as f: ...` + if checker.semantic().stmt().is_with_stmt() { + return; + } + + if !checker.semantic().is_builtin("open") { + return; + } + + // Ex) `with contextlib.ExitStack() as exit_stack: ...` + if match_exit_stack(checker.semantic()) { + return; + } + + // Ex) `with contextlib.AsyncExitStack() as exit_stack: ...` + if match_async_exit_stack(checker.semantic()) { + return; + } + + checker + .diagnostics + .push(Diagnostic::new(OpenFileWithContextHandler, func.range())); } diff --git a/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs b/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs index f88f99d50e..08d229a25f 100644 --- a/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs +++ b/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs @@ -55,7 +55,7 @@ pub(crate) fn property_with_parameters( ) { if !decorator_list .iter() - .any(|d| matches!(&d.expression, Expr::Name(ast::ExprName { id, .. }) if id == "property")) + .any(|decorator| matches!(&decorator.expression, Expr::Name(ast::ExprName { id, .. }) if id == "property")) { return; } diff --git a/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs b/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs index c23bc22e6a..2e07090bf4 100644 --- a/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs +++ b/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs @@ -61,30 +61,31 @@ pub(crate) fn sys_exit_alias(checker: &mut Checker, func: &Expr) { let Expr::Name(ast::ExprName { id, .. }) = func else { return; }; - for name in ["exit", "quit"] { - if id != name { - continue; - } - if !checker.semantic().is_builtin(name) { - continue; - } - let mut diagnostic = Diagnostic::new( - SysExitAlias { - name: name.to_string(), - }, - func.range(), - ); - if checker.patch(diagnostic.kind.rule()) { - diagnostic.try_set_fix(|| { - let (import_edit, binding) = checker.importer.get_or_import_symbol( - &ImportRequest::import("sys", "exit"), - func.start(), - checker.semantic(), - )?; - let reference_edit = Edit::range_replacement(binding, func.range()); - Ok(Fix::suggested_edits(import_edit, [reference_edit])) - }); - } - checker.diagnostics.push(diagnostic); + + if !matches!(id.as_str(), "exit" | "quit") { + return; } + + if !checker.semantic().is_builtin(id.as_str()) { + return; + } + + let mut diagnostic = Diagnostic::new( + SysExitAlias { + name: id.to_string(), + }, + func.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.try_set_fix(|| { + let (import_edit, binding) = checker.importer.get_or_import_symbol( + &ImportRequest::import("sys", "exit"), + func.start(), + checker.semantic(), + )?; + let reference_edit = Edit::range_replacement(binding, func.range()); + Ok(Fix::suggested_edits(import_edit, [reference_edit])) + }); + } + checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs b/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs index e9265ee090..14d7c5298b 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs @@ -86,66 +86,15 @@ pub(crate) fn native_literals( return; } - 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(), - ); - if checker.patch(diagnostic.kind.rule()) { - let constant = if id == "bytes" { - Constant::Bytes(vec![]) - } else { - Constant::Str(String::new()) - }; - let content = checker.generator().constant(&constant); - diagnostic.set_fix(Fix::automatic(Edit::range_replacement( - content, - expr.range(), - ))); - } - checker.diagnostics.push(diagnostic); - return; - }; + if !matches!(id.as_str(), "str" | "bytes") { + return; + } - // Look for `str("")`. - if id == "str" - && !matches!( - &arg, - Expr::Constant(ast::ExprConstant { - value: Constant::Str(_), - .. - }), - ) - { - return; - } - - // Look for `bytes(b"")` - if id == "bytes" - && !matches!( - &arg, - Expr::Constant(ast::ExprConstant { - value: Constant::Bytes(_), - .. - }), - ) - { - return; - } - - // Skip implicit string concatenations. - let arg_code = checker.locator.slice(arg.range()); - if is_implicit_concatenation(arg_code) { - return; - } + if !checker.semantic().is_builtin(id) { + return; + } + let Some(arg) = args.get(0) else { let mut diagnostic = Diagnostic::new( NativeLiterals { literal_type: if id == "str" { @@ -157,11 +106,68 @@ pub(crate) fn native_literals( expr.range(), ); if checker.patch(diagnostic.kind.rule()) { + let constant = if id == "bytes" { + Constant::Bytes(vec![]) + } else { + Constant::Str(String::new()) + }; + let content = checker.generator().constant(&constant); diagnostic.set_fix(Fix::automatic(Edit::range_replacement( - arg_code.to_string(), + content, expr.range(), ))); } checker.diagnostics.push(diagnostic); + return; + }; + + // Look for `str("")`. + if id == "str" + && !matches!( + &arg, + Expr::Constant(ast::ExprConstant { + value: Constant::Str(_), + .. + }), + ) + { + return; } + + // Look for `bytes(b"")` + if id == "bytes" + && !matches!( + &arg, + Expr::Constant(ast::ExprConstant { + value: Constant::Bytes(_), + .. + }), + ) + { + return; + } + + // Skip implicit string concatenations. + let arg_code = checker.locator.slice(arg.range()); + if is_implicit_concatenation(arg_code) { + return; + } + + let mut diagnostic = Diagnostic::new( + NativeLiterals { + literal_type: if id == "str" { + LiteralType::Str + } else { + LiteralType::Bytes + }, + }, + expr.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( + arg_code.to_string(), + expr.range(), + ))); + } + checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs b/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs index 47fa960d18..0b7548498c 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs @@ -9,6 +9,7 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::find_keyword; use ruff_python_ast::source_code::Locator; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; use crate::registry::Rule; @@ -36,7 +37,7 @@ use crate::registry::Rule; /// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open) #[violation] pub struct RedundantOpenModes { - pub replacement: Option, + replacement: Option, } impl AlwaysAutofixableViolation for RedundantOpenModes { @@ -62,10 +63,78 @@ impl AlwaysAutofixableViolation for RedundantOpenModes { } } +/// UP015 +pub(crate) fn redundant_open_modes(checker: &mut Checker, expr: &Expr) { + let Some((mode_param, keywords)) = match_open(expr, checker.semantic()) else { + return; + }; + match mode_param { + None => { + if !keywords.is_empty() { + if let Some(keyword) = find_keyword(keywords, MODE_KEYWORD_ARGUMENT) { + if let Expr::Constant(ast::ExprConstant { + value: Constant::Str(mode_param_value), + .. + }) = &keyword.value + { + if let Ok(mode) = OpenMode::from_str(mode_param_value.as_str()) { + checker.diagnostics.push(create_check( + expr, + &keyword.value, + mode.replacement_value(), + checker.locator, + checker.patch(Rule::RedundantOpenModes), + )); + } + } + } + } + } + Some(mode_param) => { + if let Expr::Constant(ast::ExprConstant { + value: Constant::Str(value), + .. + }) = &mode_param + { + if let Ok(mode) = OpenMode::from_str(value.as_str()) { + checker.diagnostics.push(create_check( + expr, + mode_param, + mode.replacement_value(), + checker.locator, + checker.patch(Rule::RedundantOpenModes), + )); + } + } + } + } +} + const OPEN_FUNC_NAME: &str = "open"; const MODE_KEYWORD_ARGUMENT: &str = "mode"; -#[derive(Copy, Clone)] +fn match_open<'a>( + expr: &'a Expr, + model: &SemanticModel, +) -> Option<(Option<&'a Expr>, &'a [Keyword])> { + let ast::ExprCall { + func, + args, + keywords, + range: _, + } = expr.as_call_expr()?; + + let ast::ExprName { id, .. } = func.as_name_expr()?; + + if id.as_str() == OPEN_FUNC_NAME && model.is_builtin(id) { + // Return the "open mode" parameter and keywords. + Some((args.get(1), keywords)) + } else { + None + } +} + +#[derive(Debug, Copy, Clone)] enum OpenMode { U, Ur, @@ -107,22 +176,6 @@ impl OpenMode { } } -fn match_open(expr: &Expr) -> (Option<&Expr>, Vec) { - if let Expr::Call(ast::ExprCall { - func, - args, - keywords, - range: _, - }) = expr - { - if matches!(func.as_ref(), Expr::Name(ast::ExprName {id, ..}) if id == OPEN_FUNC_NAME) { - // Return the "open mode" parameter and keywords. - return (args.get(1), keywords.clone()); - } - } - (None, vec![]) -} - fn create_check( expr: &Expr, mode_param: &Expr, @@ -190,47 +243,3 @@ fn create_remove_param_fix(locator: &Locator, expr: &Expr, mode_param: &Expr) -> )), } } - -/// UP015 -pub(crate) fn redundant_open_modes(checker: &mut Checker, expr: &Expr) { - // If `open` has been rebound, skip this check entirely. - if !checker.semantic().is_builtin(OPEN_FUNC_NAME) { - return; - } - let (mode_param, keywords): (Option<&Expr>, Vec) = match_open(expr); - if mode_param.is_none() && !keywords.is_empty() { - if let Some(keyword) = find_keyword(&keywords, MODE_KEYWORD_ARGUMENT) { - if let Expr::Constant(ast::ExprConstant { - value: Constant::Str(mode_param_value), - .. - }) = &keyword.value - { - if let Ok(mode) = OpenMode::from_str(mode_param_value.as_str()) { - checker.diagnostics.push(create_check( - expr, - &keyword.value, - mode.replacement_value(), - checker.locator, - checker.patch(Rule::RedundantOpenModes), - )); - } - } - } - } else if let Some(mode_param) = mode_param { - if let Expr::Constant(ast::ExprConstant { - value: Constant::Str(mode_param_value), - .. - }) = &mode_param - { - if let Ok(mode) = OpenMode::from_str(mode_param_value.as_str()) { - checker.diagnostics.push(create_check( - expr, - mode_param, - mode.replacement_value(), - checker.locator, - checker.patch(Rule::RedundantOpenModes), - )); - } - } - } -} From 8dc06d1035ec2635856664d1a7e62fea9f466d78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jul 2023 13:10:25 -0400 Subject: [PATCH 397/447] ci(deps): bump webfactory/ssh-agent from 0.7.0 to 0.8.0 (#5657) --- .github/workflows/ci.yaml | 2 +- .github/workflows/docs.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index da672bea87..af305c0d67 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -257,7 +257,7 @@ jobs: - uses: actions/setup-python@v4 - name: "Add SSH key" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} - uses: webfactory/ssh-agent@v0.7.0 + uses: webfactory/ssh-agent@v0.8.0 with: ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }} - name: "Install Rust toolchain" diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 21f17c1f89..f5ed1a6e31 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -16,7 +16,7 @@ jobs: - uses: actions/setup-python@v4 - name: "Add SSH key" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} - uses: webfactory/ssh-agent@v0.7.0 + uses: webfactory/ssh-agent@v0.8.0 with: ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }} - name: "Install Rust toolchain" From d19839fe0fc2b3d4b2dbe40227734baf0103ab6a Mon Sep 17 00:00:00 2001 From: Zanie Date: Mon, 10 Jul 2023 12:11:54 -0500 Subject: [PATCH 398/447] Add support for `Union` declarations without `|` to PYI016 (#5598) Previously, PYI016 only supported reporting violations for unions defined with `|`. Now, union declarations with `typing.Union` are supported. --- .../test/fixtures/flake8_pyi/PYI016.pyi | 41 ++ crates/ruff/src/checkers/ast/mod.rs | 28 +- crates/ruff/src/rules/flake8_pyi/helpers.rs | 54 ++ crates/ruff/src/rules/flake8_pyi/mod.rs | 1 + .../rules/duplicate_union_member.rs | 120 ++-- .../rules/unnecessary_literal_union.rs | 53 +- ..._flake8_pyi__tests__PYI016_PYI016.pyi.snap | 565 +++++++++++++----- 7 files changed, 575 insertions(+), 287 deletions(-) create mode 100644 crates/ruff/src/rules/flake8_pyi/helpers.rs diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI016.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI016.pyi index c1b2d7711c..fd19cf669e 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI016.pyi +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI016.pyi @@ -1,3 +1,5 @@ +import typing + # Shouldn't affect non-union field types. field1: str @@ -30,3 +32,42 @@ field10: (str | int) | str # PYI016: Duplicate union member `str` # Should emit for nested unions. field11: dict[int | int, str] + +# Should emit for unions with more than two cases +field12: int | int | int # Error +field13: int | int | int | int # Error + +# Should emit for unions with more than two cases, even if not directly adjacent +field14: int | int | str | int # Error + +# Should emit for duplicate literal types; also covered by PYI030 +field15: typing.Literal[1] | typing.Literal[1] # Error + +# Shouldn't emit if in new parent type +field16: int | dict[int, str] # OK + +# Shouldn't emit if not in a union parent +field17: dict[int, int] # OK + +# Should emit in cases with newlines +field18: typing.Union[ + set[ + int # foo + ], + set[ + int # bar + ], +] # Error, newline and comment will not be emitted in message + + +# Should emit in cases with `typing.Union` instead of `|` +field19: typing.Union[int, int] # Error + +# Should emit in cases with nested `typing.Union` +field20: typing.Union[int, typing.Union[int, str]] # Error + +# Should emit in cases with mixed `typing.Union` and `|` +field21: typing.Union[int, int | str] # Error + +# Should emit only once in cases with multiple nested `typing.Union` +field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 884f9efb7c..c522703d8c 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -2196,18 +2196,24 @@ where } // Ex) Union[...] - if self.enabled(Rule::UnnecessaryLiteralUnion) { - let mut check = true; + if self.any_enabled(&[Rule::UnnecessaryLiteralUnion, Rule::DuplicateUnionMember]) { + // Determine if the current expression is an union + // Avoid duplicate checks if the parent is an `Union[...]` since these rules traverse nested unions + let is_unchecked_union = self + .semantic + .expr_grandparent() + .and_then(Expr::as_subscript_expr) + .map_or(true, |parent| { + !self.semantic.match_typing_expr(&parent.value, "Union") + }); - // Avoid duplicate checks if the parent is an `Union[...]` - if let Some(Expr::Subscript(ast::ExprSubscript { value, .. })) = - self.semantic.expr_grandparent() - { - check = !self.semantic.match_typing_expr(value, "Union"); - } - - if check { - flake8_pyi::rules::unnecessary_literal_union(self, expr); + if is_unchecked_union { + if self.enabled(Rule::UnnecessaryLiteralUnion) { + flake8_pyi::rules::unnecessary_literal_union(self, expr); + } + if self.enabled(Rule::DuplicateUnionMember) { + flake8_pyi::rules::duplicate_union_member(self, expr); + } } } diff --git a/crates/ruff/src/rules/flake8_pyi/helpers.rs b/crates/ruff/src/rules/flake8_pyi/helpers.rs new file mode 100644 index 0000000000..0f37e94470 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/helpers.rs @@ -0,0 +1,54 @@ +use ruff_python_semantic::SemanticModel; +use rustpython_parser::ast::{self, Expr, Operator}; + +/// Traverse a "union" type annotation, applying `func` to each union member. +/// Supports traversal of `Union` and `|` union expressions. +/// The function is called with each expression in the union (excluding declarations of nested unions) +/// and the parent expression (if any). +pub(super) fn traverse_union<'a, F>( + func: &mut F, + semantic: &SemanticModel, + expr: &'a Expr, + parent: Option<&'a Expr>, +) where + F: FnMut(&'a Expr, Option<&'a Expr>), +{ + // Ex) x | y + if let Expr::BinOp(ast::ExprBinOp { + op: Operator::BitOr, + left, + right, + range: _, + }) = expr + { + // The union data structure usually looks like this: + // a | b | c -> (a | b) | c + // + // However, parenthesized expressions can coerce it into any structure: + // a | (b | c) + // + // So we have to traverse both branches in order (left, then right), to report members + // in the order they appear in the source code. + + // Traverse the left then right arms + traverse_union(func, semantic, left, Some(expr)); + traverse_union(func, semantic, right, Some(expr)); + return; + } + + // Ex) Union[x, y] + if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr { + if semantic.match_typing_expr(value, "Union") { + if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() { + // Traverse each element of the tuple within the union recursively to handle cases + // such as `Union[..., Union[...]] + elts.iter() + .for_each(|elt| traverse_union(func, semantic, elt, Some(expr))); + return; + } + } + } + + // Otherwise, call the function on expression + func(expr, parent); +} diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index dda789c12a..8dd20e1f11 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -1,4 +1,5 @@ //! Rules from [flake8-pyi](https://pypi.org/project/flake8-pyi/). +mod helpers; pub(crate) mod rules; #[cfg(test)] diff --git a/crates/ruff/src/rules/flake8_pyi/rules/duplicate_union_member.rs b/crates/ruff/src/rules/flake8_pyi/rules/duplicate_union_member.rs index 121333ddc5..10f111ad10 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/duplicate_union_member.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/duplicate_union_member.rs @@ -1,89 +1,77 @@ use rustc_hash::FxHashSet; -use rustpython_parser::ast::{self, Expr, Operator, Ranged}; - -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::comparable::ComparableExpr; +use rustpython_parser::ast::{self, Expr, Ranged}; +use std::collections::HashSet; use crate::checkers::ast::Checker; use crate::registry::AsRule; +use crate::rules::flake8_pyi::helpers::traverse_union; +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::comparable::ComparableExpr; #[violation] pub struct DuplicateUnionMember { duplicate_name: String, } -impl AlwaysAutofixableViolation for DuplicateUnionMember { +impl Violation for DuplicateUnionMember { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { format!("Duplicate union member `{}`", self.duplicate_name) } - fn autofix_title(&self) -> String { - format!("Remove duplicate union member `{}`", self.duplicate_name) + fn autofix_title(&self) -> Option { + Some(format!( + "Remove duplicate union member `{}`", + self.duplicate_name + )) } } /// PYI016 -pub(crate) fn duplicate_union_member(checker: &mut Checker, expr: &Expr) { - let mut seen_nodes = FxHashSet::default(); - traverse_union(&mut seen_nodes, checker, expr, None); -} +pub(crate) fn duplicate_union_member<'a>(checker: &mut Checker, expr: &'a Expr) { + let mut seen_nodes: HashSet, _> = FxHashSet::default(); + let mut diagnostics: Vec = Vec::new(); -fn traverse_union<'a>( - seen_nodes: &mut FxHashSet>, - checker: &mut Checker, - expr: &'a Expr, - parent: Option<&'a Expr>, -) { - // The union data structure usually looks like this: - // a | b | c -> (a | b) | c - // - // However, parenthesized expressions can coerce it into any structure: - // a | (b | c) - // - // So we have to traverse both branches in order (left, then right), to report duplicates - // in the order they appear in the source code. - if let Expr::BinOp(ast::ExprBinOp { - op: Operator::BitOr, - left, - right, - range: _, - }) = expr - { - // Traverse left subtree, then the right subtree, propagating the previous node. - traverse_union(seen_nodes, checker, left, Some(expr)); - traverse_union(seen_nodes, checker, right, Some(expr)); - } + // Adds a member to `literal_exprs` if it is a `Literal` annotation + let mut check_for_duplicate_members = |expr: &'a Expr, parent: Option<&'a Expr>| { + // If we've already seen this union member, raise a violation. + if !seen_nodes.insert(expr.into()) { + let mut diagnostic = Diagnostic::new( + DuplicateUnionMember { + duplicate_name: checker.generator().expr(expr), + }, + expr.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + // Delete the "|" character as well as the duplicate value by reconstructing the + // parent without the duplicate. - // If we've already seen this union member, raise a violation. - if !seen_nodes.insert(expr.into()) { - let mut diagnostic = Diagnostic::new( - DuplicateUnionMember { - duplicate_name: checker.generator().expr(expr), - }, - expr.range(), - ); - if checker.patch(diagnostic.kind.rule()) { - // Delete the "|" character as well as the duplicate value by reconstructing the - // parent without the duplicate. - - // SAFETY: impossible to have a duplicate without a `parent` node. - let parent = parent.expect("Parent node must exist"); - - // SAFETY: Parent node must have been a `BinOp` in order for us to have traversed it. - let Expr::BinOp(ast::ExprBinOp { left, right, .. }) = parent else { - panic!("Parent node must be a BinOp"); - }; - - // Replace the parent with its non-duplicate child. - diagnostic.set_fix(Fix::automatic(Edit::range_replacement( - checker - .generator() - .expr(if expr == left.as_ref() { right } else { left }), - parent.range(), - ))); + // If the parent node is not a `BinOp` we will not perform a fix + if let Some(Expr::BinOp(ast::ExprBinOp { left, right, .. })) = parent { + // Replace the parent with its non-duplicate child. + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( + checker + .generator() + .expr(if expr == left.as_ref() { right } else { left }), + parent.unwrap().range(), + ))); + } + } + diagnostics.push(diagnostic); } - checker.diagnostics.push(diagnostic); - } + }; + + // Traverse the union, collect all diagnostic members + traverse_union( + &mut check_for_duplicate_members, + checker.semantic(), + expr, + None, + ); + + // Add all diagnostics to the checker + checker.diagnostics.append(&mut diagnostics); } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/unnecessary_literal_union.rs b/crates/ruff/src/rules/flake8_pyi/rules/unnecessary_literal_union.rs index 6d5735ac32..16f21549a9 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/unnecessary_literal_union.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/unnecessary_literal_union.rs @@ -1,11 +1,11 @@ -use ruff_python_semantic::SemanticModel; -use rustpython_parser::ast::{self, Expr, Operator, Ranged}; +use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use smallvec::SmallVec; use crate::checkers::ast::Checker; +use crate::rules::flake8_pyi::helpers::traverse_union; /// ## What it does /// Checks for the presence of multiple literal types in a union. @@ -47,7 +47,7 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Exp let mut literal_exprs = SmallVec::<[&Box; 1]>::new(); // Adds a member to `literal_exprs` if it is a `Literal` annotation - let mut collect_literal_expr = |expr: &'a Expr| { + let mut collect_literal_expr = |expr: &'a Expr, _| { if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr { if checker.semantic().match_typing_expr(value, "Literal") { literal_exprs.push(slice); @@ -56,7 +56,7 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Exp }; // Traverse the union, collect all literal members - traverse_union(&mut collect_literal_expr, expr, checker.semantic()); + traverse_union(&mut collect_literal_expr, checker.semantic(), expr, None); // Raise a violation if more than one if literal_exprs.len() > 1 { @@ -73,48 +73,3 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Exp checker.diagnostics.push(diagnostic); } } - -/// Traverse a "union" type annotation, calling `func` on each expression in the union. -fn traverse_union<'a, F>(func: &mut F, expr: &'a Expr, semantic: &SemanticModel) -where - F: FnMut(&'a Expr), -{ - // Ex) x | y - if let Expr::BinOp(ast::ExprBinOp { - op: Operator::BitOr, - left, - right, - range: _, - }) = expr - { - // The union data structure usually looks like this: - // a | b | c -> (a | b) | c - // - // However, parenthesized expressions can coerce it into any structure: - // a | (b | c) - // - // So we have to traverse both branches in order (left, then right), to report members - // in the order they appear in the source code. - - // Traverse the left then right arms - traverse_union(func, left, semantic); - traverse_union(func, right, semantic); - return; - } - - // Ex) Union[x, y] - if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr { - if semantic.match_typing_expr(value, "Union") { - if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() { - // Traverse each element of the tuple within the union recursively to handle cases - // such as `Union[..., Union[...]] - elts.iter() - .for_each(|elt| traverse_union(func, elt, semantic)); - return; - } - } - } - - // Otherwise, call the function on expression - func(expr); -} diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap index 102e686c02..6cfac77210 100644 --- a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap @@ -1,219 +1,462 @@ --- source: crates/ruff/src/rules/flake8_pyi/mod.rs --- -PYI016.pyi:5:15: PYI016 [*] Duplicate union member `str` +PYI016.pyi:7:15: PYI016 [*] Duplicate union member `str` | -4 | # Should emit for duplicate field types. -5 | field2: str | str # PYI016: Duplicate union member `str` +6 | # Should emit for duplicate field types. +7 | field2: str | str # PYI016: Duplicate union member `str` | ^^^ PYI016 -6 | -7 | # Should emit for union types in arguments. +8 | +9 | # Should emit for union types in arguments. | = help: Remove duplicate union member `str` ℹ Fix -2 2 | field1: str -3 3 | -4 4 | # Should emit for duplicate field types. -5 |-field2: str | str # PYI016: Duplicate union member `str` - 5 |+field2: str # PYI016: Duplicate union member `str` -6 6 | -7 7 | # Should emit for union types in arguments. -8 8 | def func1(arg1: int | int): # PYI016: Duplicate union member `int` +4 4 | field1: str +5 5 | +6 6 | # Should emit for duplicate field types. +7 |-field2: str | str # PYI016: Duplicate union member `str` + 7 |+field2: str # PYI016: Duplicate union member `str` +8 8 | +9 9 | # Should emit for union types in arguments. +10 10 | def func1(arg1: int | int): # PYI016: Duplicate union member `int` -PYI016.pyi:8:23: PYI016 [*] Duplicate union member `int` - | -7 | # Should emit for union types in arguments. -8 | def func1(arg1: int | int): # PYI016: Duplicate union member `int` - | ^^^ PYI016 -9 | print(arg1) - | - = help: Remove duplicate union member `int` - -ℹ Fix -5 5 | field2: str | str # PYI016: Duplicate union member `str` -6 6 | -7 7 | # Should emit for union types in arguments. -8 |-def func1(arg1: int | int): # PYI016: Duplicate union member `int` - 8 |+def func1(arg1: int): # PYI016: Duplicate union member `int` -9 9 | print(arg1) -10 10 | -11 11 | # Should emit for unions in return types. - -PYI016.pyi:12:22: PYI016 [*] Duplicate union member `str` +PYI016.pyi:10:23: PYI016 [*] Duplicate union member `int` | -11 | # Should emit for unions in return types. -12 | def func2() -> str | str: # PYI016: Duplicate union member `str` - | ^^^ PYI016 -13 | return "my string" - | - = help: Remove duplicate union member `str` - -ℹ Fix -9 9 | print(arg1) -10 10 | -11 11 | # Should emit for unions in return types. -12 |-def func2() -> str | str: # PYI016: Duplicate union member `str` - 12 |+def func2() -> str: # PYI016: Duplicate union member `str` -13 13 | return "my string" -14 14 | -15 15 | # Should emit in longer unions, even if not directly adjacent. - -PYI016.pyi:16:15: PYI016 [*] Duplicate union member `str` - | -15 | # Should emit in longer unions, even if not directly adjacent. -16 | field3: str | str | int # PYI016: Duplicate union member `str` - | ^^^ PYI016 -17 | field4: int | int | str # PYI016: Duplicate union member `int` -18 | field5: str | int | str # PYI016: Duplicate union member `str` - | - = help: Remove duplicate union member `str` - -ℹ Fix -13 13 | return "my string" -14 14 | -15 15 | # Should emit in longer unions, even if not directly adjacent. -16 |-field3: str | str | int # PYI016: Duplicate union member `str` - 16 |+field3: str | int # PYI016: Duplicate union member `str` -17 17 | field4: int | int | str # PYI016: Duplicate union member `int` -18 18 | field5: str | int | str # PYI016: Duplicate union member `str` -19 19 | field6: int | bool | str | int # PYI016: Duplicate union member `int` - -PYI016.pyi:17:15: PYI016 [*] Duplicate union member `int` - | -15 | # Should emit in longer unions, even if not directly adjacent. -16 | field3: str | str | int # PYI016: Duplicate union member `str` -17 | field4: int | int | str # PYI016: Duplicate union member `int` - | ^^^ PYI016 -18 | field5: str | int | str # PYI016: Duplicate union member `str` -19 | field6: int | bool | str | int # PYI016: Duplicate union member `int` + 9 | # Should emit for union types in arguments. +10 | def func1(arg1: int | int): # PYI016: Duplicate union member `int` + | ^^^ PYI016 +11 | print(arg1) | = help: Remove duplicate union member `int` ℹ Fix -14 14 | -15 15 | # Should emit in longer unions, even if not directly adjacent. -16 16 | field3: str | str | int # PYI016: Duplicate union member `str` -17 |-field4: int | int | str # PYI016: Duplicate union member `int` - 17 |+field4: int | str # PYI016: Duplicate union member `int` -18 18 | field5: str | int | str # PYI016: Duplicate union member `str` -19 19 | field6: int | bool | str | int # PYI016: Duplicate union member `int` -20 20 | +7 7 | field2: str | str # PYI016: Duplicate union member `str` +8 8 | +9 9 | # Should emit for union types in arguments. +10 |-def func1(arg1: int | int): # PYI016: Duplicate union member `int` + 10 |+def func1(arg1: int): # PYI016: Duplicate union member `int` +11 11 | print(arg1) +12 12 | +13 13 | # Should emit for unions in return types. -PYI016.pyi:18:21: PYI016 [*] Duplicate union member `str` +PYI016.pyi:14:22: PYI016 [*] Duplicate union member `str` | -16 | field3: str | str | int # PYI016: Duplicate union member `str` -17 | field4: int | int | str # PYI016: Duplicate union member `int` -18 | field5: str | int | str # PYI016: Duplicate union member `str` - | ^^^ PYI016 -19 | field6: int | bool | str | int # PYI016: Duplicate union member `int` +13 | # Should emit for unions in return types. +14 | def func2() -> str | str: # PYI016: Duplicate union member `str` + | ^^^ PYI016 +15 | return "my string" | = help: Remove duplicate union member `str` ℹ Fix -15 15 | # Should emit in longer unions, even if not directly adjacent. -16 16 | field3: str | str | int # PYI016: Duplicate union member `str` -17 17 | field4: int | int | str # PYI016: Duplicate union member `int` -18 |-field5: str | int | str # PYI016: Duplicate union member `str` - 18 |+field5: str | int # PYI016: Duplicate union member `str` -19 19 | field6: int | bool | str | int # PYI016: Duplicate union member `int` -20 20 | -21 21 | # Shouldn't emit for non-type unions. +11 11 | print(arg1) +12 12 | +13 13 | # Should emit for unions in return types. +14 |-def func2() -> str | str: # PYI016: Duplicate union member `str` + 14 |+def func2() -> str: # PYI016: Duplicate union member `str` +15 15 | return "my string" +16 16 | +17 17 | # Should emit in longer unions, even if not directly adjacent. -PYI016.pyi:19:28: PYI016 [*] Duplicate union member `int` +PYI016.pyi:18:15: PYI016 [*] Duplicate union member `str` | -17 | field4: int | int | str # PYI016: Duplicate union member `int` -18 | field5: str | int | str # PYI016: Duplicate union member `str` -19 | field6: int | bool | str | int # PYI016: Duplicate union member `int` +17 | # Should emit in longer unions, even if not directly adjacent. +18 | field3: str | str | int # PYI016: Duplicate union member `str` + | ^^^ PYI016 +19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 | field5: str | int | str # PYI016: Duplicate union member `str` + | + = help: Remove duplicate union member `str` + +ℹ Fix +15 15 | return "my string" +16 16 | +17 17 | # Should emit in longer unions, even if not directly adjacent. +18 |-field3: str | str | int # PYI016: Duplicate union member `str` + 18 |+field3: str | int # PYI016: Duplicate union member `str` +19 19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 20 | field5: str | int | str # PYI016: Duplicate union member `str` +21 21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` + +PYI016.pyi:19:15: PYI016 [*] Duplicate union member `int` + | +17 | # Should emit in longer unions, even if not directly adjacent. +18 | field3: str | str | int # PYI016: Duplicate union member `str` +19 | field4: int | int | str # PYI016: Duplicate union member `int` + | ^^^ PYI016 +20 | field5: str | int | str # PYI016: Duplicate union member `str` +21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` + | + = help: Remove duplicate union member `int` + +ℹ Fix +16 16 | +17 17 | # Should emit in longer unions, even if not directly adjacent. +18 18 | field3: str | str | int # PYI016: Duplicate union member `str` +19 |-field4: int | int | str # PYI016: Duplicate union member `int` + 19 |+field4: int | str # PYI016: Duplicate union member `int` +20 20 | field5: str | int | str # PYI016: Duplicate union member `str` +21 21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` +22 22 | + +PYI016.pyi:20:21: PYI016 [*] Duplicate union member `str` + | +18 | field3: str | str | int # PYI016: Duplicate union member `str` +19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 | field5: str | int | str # PYI016: Duplicate union member `str` + | ^^^ PYI016 +21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` + | + = help: Remove duplicate union member `str` + +ℹ Fix +17 17 | # Should emit in longer unions, even if not directly adjacent. +18 18 | field3: str | str | int # PYI016: Duplicate union member `str` +19 19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 |-field5: str | int | str # PYI016: Duplicate union member `str` + 20 |+field5: str | int # PYI016: Duplicate union member `str` +21 21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` +22 22 | +23 23 | # Shouldn't emit for non-type unions. + +PYI016.pyi:21:28: PYI016 [*] Duplicate union member `int` + | +19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 | field5: str | int | str # PYI016: Duplicate union member `str` +21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` | ^^^ PYI016 -20 | -21 | # Shouldn't emit for non-type unions. +22 | +23 | # Shouldn't emit for non-type unions. | = help: Remove duplicate union member `int` ℹ Fix -16 16 | field3: str | str | int # PYI016: Duplicate union member `str` -17 17 | field4: int | int | str # PYI016: Duplicate union member `int` -18 18 | field5: str | int | str # PYI016: Duplicate union member `str` -19 |-field6: int | bool | str | int # PYI016: Duplicate union member `int` - 19 |+field6: int | bool | str # PYI016: Duplicate union member `int` -20 20 | -21 21 | # Shouldn't emit for non-type unions. -22 22 | field7 = str | str +18 18 | field3: str | str | int # PYI016: Duplicate union member `str` +19 19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 20 | field5: str | int | str # PYI016: Duplicate union member `str` +21 |-field6: int | bool | str | int # PYI016: Duplicate union member `int` + 21 |+field6: int | bool | str # PYI016: Duplicate union member `int` +22 22 | +23 23 | # Shouldn't emit for non-type unions. +24 24 | field7 = str | str -PYI016.pyi:25:22: PYI016 [*] Duplicate union member `int` +PYI016.pyi:27:22: PYI016 [*] Duplicate union member `int` | -24 | # Should emit for strangely-bracketed unions. -25 | field8: int | (str | int) # PYI016: Duplicate union member `int` +26 | # Should emit for strangely-bracketed unions. +27 | field8: int | (str | int) # PYI016: Duplicate union member `int` | ^^^ PYI016 -26 | -27 | # Should handle user brackets when fixing. +28 | +29 | # Should handle user brackets when fixing. | = help: Remove duplicate union member `int` ℹ Fix -22 22 | field7 = str | str -23 23 | -24 24 | # Should emit for strangely-bracketed unions. -25 |-field8: int | (str | int) # PYI016: Duplicate union member `int` - 25 |+field8: int | (str) # PYI016: Duplicate union member `int` -26 26 | -27 27 | # Should handle user brackets when fixing. -28 28 | field9: int | (int | str) # PYI016: Duplicate union member `int` +24 24 | field7 = str | str +25 25 | +26 26 | # Should emit for strangely-bracketed unions. +27 |-field8: int | (str | int) # PYI016: Duplicate union member `int` + 27 |+field8: int | (str) # PYI016: Duplicate union member `int` +28 28 | +29 29 | # Should handle user brackets when fixing. +30 30 | field9: int | (int | str) # PYI016: Duplicate union member `int` -PYI016.pyi:28:16: PYI016 [*] Duplicate union member `int` +PYI016.pyi:30:16: PYI016 [*] Duplicate union member `int` | -27 | # Should handle user brackets when fixing. -28 | field9: int | (int | str) # PYI016: Duplicate union member `int` +29 | # Should handle user brackets when fixing. +30 | field9: int | (int | str) # PYI016: Duplicate union member `int` | ^^^ PYI016 -29 | field10: (str | int) | str # PYI016: Duplicate union member `str` +31 | field10: (str | int) | str # PYI016: Duplicate union member `str` | = help: Remove duplicate union member `int` ℹ Fix -25 25 | field8: int | (str | int) # PYI016: Duplicate union member `int` -26 26 | -27 27 | # Should handle user brackets when fixing. -28 |-field9: int | (int | str) # PYI016: Duplicate union member `int` - 28 |+field9: int | (str) # PYI016: Duplicate union member `int` -29 29 | field10: (str | int) | str # PYI016: Duplicate union member `str` -30 30 | -31 31 | # Should emit for nested unions. +27 27 | field8: int | (str | int) # PYI016: Duplicate union member `int` +28 28 | +29 29 | # Should handle user brackets when fixing. +30 |-field9: int | (int | str) # PYI016: Duplicate union member `int` + 30 |+field9: int | (str) # PYI016: Duplicate union member `int` +31 31 | field10: (str | int) | str # PYI016: Duplicate union member `str` +32 32 | +33 33 | # Should emit for nested unions. -PYI016.pyi:29:24: PYI016 [*] Duplicate union member `str` +PYI016.pyi:31:24: PYI016 [*] Duplicate union member `str` | -27 | # Should handle user brackets when fixing. -28 | field9: int | (int | str) # PYI016: Duplicate union member `int` -29 | field10: (str | int) | str # PYI016: Duplicate union member `str` +29 | # Should handle user brackets when fixing. +30 | field9: int | (int | str) # PYI016: Duplicate union member `int` +31 | field10: (str | int) | str # PYI016: Duplicate union member `str` | ^^^ PYI016 -30 | -31 | # Should emit for nested unions. +32 | +33 | # Should emit for nested unions. | = help: Remove duplicate union member `str` ℹ Fix -26 26 | -27 27 | # Should handle user brackets when fixing. -28 28 | field9: int | (int | str) # PYI016: Duplicate union member `int` -29 |-field10: (str | int) | str # PYI016: Duplicate union member `str` - 29 |+field10: str | int # PYI016: Duplicate union member `str` -30 30 | -31 31 | # Should emit for nested unions. -32 32 | field11: dict[int | int, str] +28 28 | +29 29 | # Should handle user brackets when fixing. +30 30 | field9: int | (int | str) # PYI016: Duplicate union member `int` +31 |-field10: (str | int) | str # PYI016: Duplicate union member `str` + 31 |+field10: str | int # PYI016: Duplicate union member `str` +32 32 | +33 33 | # Should emit for nested unions. +34 34 | field11: dict[int | int, str] -PYI016.pyi:32:21: PYI016 [*] Duplicate union member `int` +PYI016.pyi:34:21: PYI016 [*] Duplicate union member `int` | -31 | # Should emit for nested unions. -32 | field11: dict[int | int, str] +33 | # Should emit for nested unions. +34 | field11: dict[int | int, str] | ^^^ PYI016 +35 | +36 | # Should emit for unions with more than two cases | = help: Remove duplicate union member `int` ℹ Fix -29 29 | field10: (str | int) | str # PYI016: Duplicate union member `str` -30 30 | -31 31 | # Should emit for nested unions. -32 |-field11: dict[int | int, str] - 32 |+field11: dict[int, str] +31 31 | field10: (str | int) | str # PYI016: Duplicate union member `str` +32 32 | +33 33 | # Should emit for nested unions. +34 |-field11: dict[int | int, str] + 34 |+field11: dict[int, str] +35 35 | +36 36 | # Should emit for unions with more than two cases +37 37 | field12: int | int | int # Error + +PYI016.pyi:37:16: PYI016 [*] Duplicate union member `int` + | +36 | # Should emit for unions with more than two cases +37 | field12: int | int | int # Error + | ^^^ PYI016 +38 | field13: int | int | int | int # Error + | + = help: Remove duplicate union member `int` + +ℹ Fix +34 34 | field11: dict[int | int, str] +35 35 | +36 36 | # Should emit for unions with more than two cases +37 |-field12: int | int | int # Error + 37 |+field12: int | int # Error +38 38 | field13: int | int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent + +PYI016.pyi:37:22: PYI016 [*] Duplicate union member `int` + | +36 | # Should emit for unions with more than two cases +37 | field12: int | int | int # Error + | ^^^ PYI016 +38 | field13: int | int | int | int # Error + | + = help: Remove duplicate union member `int` + +ℹ Fix +34 34 | field11: dict[int | int, str] +35 35 | +36 36 | # Should emit for unions with more than two cases +37 |-field12: int | int | int # Error + 37 |+field12: int | int # Error +38 38 | field13: int | int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent + +PYI016.pyi:38:16: PYI016 [*] Duplicate union member `int` + | +36 | # Should emit for unions with more than two cases +37 | field12: int | int | int # Error +38 | field13: int | int | int | int # Error + | ^^^ PYI016 +39 | +40 | # Should emit for unions with more than two cases, even if not directly adjacent + | + = help: Remove duplicate union member `int` + +ℹ Fix +35 35 | +36 36 | # Should emit for unions with more than two cases +37 37 | field12: int | int | int # Error +38 |-field13: int | int | int | int # Error + 38 |+field13: int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 41 | field14: int | int | str | int # Error + +PYI016.pyi:38:22: PYI016 [*] Duplicate union member `int` + | +36 | # Should emit for unions with more than two cases +37 | field12: int | int | int # Error +38 | field13: int | int | int | int # Error + | ^^^ PYI016 +39 | +40 | # Should emit for unions with more than two cases, even if not directly adjacent + | + = help: Remove duplicate union member `int` + +ℹ Fix +35 35 | +36 36 | # Should emit for unions with more than two cases +37 37 | field12: int | int | int # Error +38 |-field13: int | int | int | int # Error + 38 |+field13: int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 41 | field14: int | int | str | int # Error + +PYI016.pyi:38:28: PYI016 [*] Duplicate union member `int` + | +36 | # Should emit for unions with more than two cases +37 | field12: int | int | int # Error +38 | field13: int | int | int | int # Error + | ^^^ PYI016 +39 | +40 | # Should emit for unions with more than two cases, even if not directly adjacent + | + = help: Remove duplicate union member `int` + +ℹ Fix +35 35 | +36 36 | # Should emit for unions with more than two cases +37 37 | field12: int | int | int # Error +38 |-field13: int | int | int | int # Error + 38 |+field13: int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 41 | field14: int | int | str | int # Error + +PYI016.pyi:41:16: PYI016 [*] Duplicate union member `int` + | +40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 | field14: int | int | str | int # Error + | ^^^ PYI016 +42 | +43 | # Should emit for duplicate literal types; also covered by PYI030 + | + = help: Remove duplicate union member `int` + +ℹ Fix +38 38 | field13: int | int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 |-field14: int | int | str | int # Error + 41 |+field14: int | str | int # Error +42 42 | +43 43 | # Should emit for duplicate literal types; also covered by PYI030 +44 44 | field15: typing.Literal[1] | typing.Literal[1] # Error + +PYI016.pyi:41:28: PYI016 [*] Duplicate union member `int` + | +40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 | field14: int | int | str | int # Error + | ^^^ PYI016 +42 | +43 | # Should emit for duplicate literal types; also covered by PYI030 + | + = help: Remove duplicate union member `int` + +ℹ Fix +38 38 | field13: int | int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 |-field14: int | int | str | int # Error + 41 |+field14: int | int | str # Error +42 42 | +43 43 | # Should emit for duplicate literal types; also covered by PYI030 +44 44 | field15: typing.Literal[1] | typing.Literal[1] # Error + +PYI016.pyi:44:30: PYI016 [*] Duplicate union member `typing.Literal[1]` + | +43 | # Should emit for duplicate literal types; also covered by PYI030 +44 | field15: typing.Literal[1] | typing.Literal[1] # Error + | ^^^^^^^^^^^^^^^^^ PYI016 +45 | +46 | # Shouldn't emit if in new parent type + | + = help: Remove duplicate union member `typing.Literal[1]` + +ℹ Fix +41 41 | field14: int | int | str | int # Error +42 42 | +43 43 | # Should emit for duplicate literal types; also covered by PYI030 +44 |-field15: typing.Literal[1] | typing.Literal[1] # Error + 44 |+field15: typing.Literal[1] # Error +45 45 | +46 46 | # Shouldn't emit if in new parent type +47 47 | field16: int | dict[int, str] # OK + +PYI016.pyi:57:5: PYI016 Duplicate union member `set[int]` + | +55 | int # foo +56 | ], +57 | set[ + | _____^ +58 | | int # bar +59 | | ], + | |_____^ PYI016 +60 | ] # Error, newline and comment will not be emitted in message + | + = help: Remove duplicate union member `set[int]` + +PYI016.pyi:64:28: PYI016 Duplicate union member `int` + | +63 | # Should emit in cases with `typing.Union` instead of `|` +64 | field19: typing.Union[int, int] # Error + | ^^^ PYI016 +65 | +66 | # Should emit in cases with nested `typing.Union` + | + = help: Remove duplicate union member `int` + +PYI016.pyi:67:41: PYI016 Duplicate union member `int` + | +66 | # Should emit in cases with nested `typing.Union` +67 | field20: typing.Union[int, typing.Union[int, str]] # Error + | ^^^ PYI016 +68 | +69 | # Should emit in cases with mixed `typing.Union` and `|` + | + = help: Remove duplicate union member `int` + +PYI016.pyi:70:28: PYI016 [*] Duplicate union member `int` + | +69 | # Should emit in cases with mixed `typing.Union` and `|` +70 | field21: typing.Union[int, int | str] # Error + | ^^^ PYI016 +71 | +72 | # Should emit only once in cases with multiple nested `typing.Union` + | + = help: Remove duplicate union member `int` + +ℹ Fix +67 67 | field20: typing.Union[int, typing.Union[int, str]] # Error +68 68 | +69 69 | # Should emit in cases with mixed `typing.Union` and `|` +70 |-field21: typing.Union[int, int | str] # Error + 70 |+field21: typing.Union[int, str] # Error +71 71 | +72 72 | # Should emit only once in cases with multiple nested `typing.Union` +73 73 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + +PYI016.pyi:73:41: PYI016 Duplicate union member `int` + | +72 | # Should emit only once in cases with multiple nested `typing.Union` +73 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + | ^^^ PYI016 + | + = help: Remove duplicate union member `int` + +PYI016.pyi:73:59: PYI016 Duplicate union member `int` + | +72 | # Should emit only once in cases with multiple nested `typing.Union` +73 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + | ^^^ PYI016 + | + = help: Remove duplicate union member `int` + +PYI016.pyi:73:64: PYI016 Duplicate union member `int` + | +72 | # Should emit only once in cases with multiple nested `typing.Union` +73 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + | ^^^ PYI016 + | + = help: Remove duplicate union member `int` From 5ab9538573d3439e0f4f853b228967f44ac18206 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Mon, 10 Jul 2023 18:33:37 +0100 Subject: [PATCH 399/447] Improve `type-name-incorrect-variance` message (#5658) ## Summary Change the `type-name-incorrect-variance` diagnostic message to include the detected variance and a name change recommendation. For example, ``` `TypeVar` name "T_co" does not reflect its contravariance; consider renaming it to "T_contra" ``` Related to #5651. ## Test Plan `cargo test` --- .../rules/type_name_incorrect_variance.rs | 72 +++++++++++++++---- ...C0105_type_name_incorrect_variance.py.snap | 64 ++++++++--------- 2 files changed, 92 insertions(+), 44 deletions(-) diff --git a/crates/ruff/src/rules/pylint/rules/type_name_incorrect_variance.rs b/crates/ruff/src/rules/pylint/rules/type_name_incorrect_variance.rs index 541f5e7f8f..9af30526ae 100644 --- a/crates/ruff/src/rules/pylint/rules/type_name_incorrect_variance.rs +++ b/crates/ruff/src/rules/pylint/rules/type_name_incorrect_variance.rs @@ -46,27 +46,34 @@ use crate::rules::pylint::helpers::type_param_name; pub struct TypeNameIncorrectVariance { kind: VarKind, param_name: String, + variance: VarVariance, + replacement_name: String, } impl Violation for TypeNameIncorrectVariance { #[derive_message_formats] fn message(&self) -> String { - let TypeNameIncorrectVariance { kind, param_name } = self; - format!("`{kind}` name \"{param_name}\" does not match variance") + let TypeNameIncorrectVariance { + kind, + param_name, + variance, + replacement_name, + } = self; + format!("`{kind}` name \"{param_name}\" does not reflect its {variance}; consider renaming it to \"{replacement_name}\"") } } /// PLC0105 pub(crate) fn type_name_incorrect_variance(checker: &mut Checker, value: &Expr) { let Expr::Call(ast::ExprCall { - func, - args, - keywords, - .. - }) = value - else { - return; - }; + func, + args, + keywords, + .. + }) = value + else { + return; + }; let Some(param_name) = type_param_name(args, keywords) else { return; @@ -114,14 +121,26 @@ pub(crate) fn type_name_incorrect_variance(checker: &mut Checker, value: &Expr) None } }) - else { - return; + else { + return; + }; + + let variance = variance(covariant, contravariant); + let name_root = param_name + .trim_end_matches("_co") + .trim_end_matches("_contra"); + let replacement_name: String = match variance { + VarVariance::Covariance => format!("{name_root}_co"), + VarVariance::Contravariance => format!("{name_root}_contra"), + VarVariance::Invariance => name_root.to_string(), }; checker.diagnostics.push(Diagnostic::new( TypeNameIncorrectVariance { kind, param_name: param_name.to_string(), + variance, + replacement_name, }, func.range(), )); @@ -138,6 +157,18 @@ fn mismatch(param_name: &str, covariant: Option<&Expr>, contravariant: Option<&E } } +/// Return the variance of the type parameter. +fn variance(covariant: Option<&Expr>, contravariant: Option<&Expr>) -> VarVariance { + match ( + covariant.map(is_const_true), + contravariant.map(is_const_true), + ) { + (Some(true), _) => VarVariance::Covariance, + (_, Some(true)) => VarVariance::Contravariance, + _ => VarVariance::Invariance, + } +} + #[derive(Debug, PartialEq, Eq, Copy, Clone)] enum VarKind { TypeVar, @@ -152,3 +183,20 @@ impl fmt::Display for VarKind { } } } + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum VarVariance { + Covariance, + Contravariance, + Invariance, +} + +impl fmt::Display for VarVariance { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + VarVariance::Covariance => fmt.write_str("covariance"), + VarVariance::Contravariance => fmt.write_str("contravariance"), + VarVariance::Invariance => fmt.write_str("invariance"), + } + } +} diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0105_type_name_incorrect_variance.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0105_type_name_incorrect_variance.py.snap index 8e54cce037..675130c6ea 100644 --- a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0105_type_name_incorrect_variance.py.snap +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0105_type_name_incorrect_variance.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/pylint/mod.rs --- -type_name_incorrect_variance.py:5:5: PLC0105 `TypeVar` name "T" does not match variance +type_name_incorrect_variance.py:5:5: PLC0105 `TypeVar` name "T" does not reflect its covariance; consider renaming it to "T_co" | 3 | # Errors. 4 | @@ -11,7 +11,7 @@ type_name_incorrect_variance.py:5:5: PLC0105 `TypeVar` name "T" does not match v 7 | T = TypeVar("T", contravariant=True) | -type_name_incorrect_variance.py:6:5: PLC0105 `TypeVar` name "T" does not match variance +type_name_incorrect_variance.py:6:5: PLC0105 `TypeVar` name "T" does not reflect its covariance; consider renaming it to "T_co" | 5 | T = TypeVar("T", covariant=True) 6 | T = TypeVar("T", covariant=True, contravariant=False) @@ -20,7 +20,7 @@ type_name_incorrect_variance.py:6:5: PLC0105 `TypeVar` name "T" does not match v 8 | T = TypeVar("T", covariant=False, contravariant=True) | -type_name_incorrect_variance.py:7:5: PLC0105 `TypeVar` name "T" does not match variance +type_name_incorrect_variance.py:7:5: PLC0105 `TypeVar` name "T" does not reflect its contravariance; consider renaming it to "T_contra" | 5 | T = TypeVar("T", covariant=True) 6 | T = TypeVar("T", covariant=True, contravariant=False) @@ -30,7 +30,7 @@ type_name_incorrect_variance.py:7:5: PLC0105 `TypeVar` name "T" does not match v 9 | P = ParamSpec("P", covariant=True) | -type_name_incorrect_variance.py:8:5: PLC0105 `TypeVar` name "T" does not match variance +type_name_incorrect_variance.py:8:5: PLC0105 `TypeVar` name "T" does not reflect its contravariance; consider renaming it to "T_contra" | 6 | T = TypeVar("T", covariant=True, contravariant=False) 7 | T = TypeVar("T", contravariant=True) @@ -40,7 +40,7 @@ type_name_incorrect_variance.py:8:5: PLC0105 `TypeVar` name "T" does not match v 10 | P = ParamSpec("P", covariant=True, contravariant=False) | -type_name_incorrect_variance.py:9:5: PLC0105 `ParamSpec` name "P" does not match variance +type_name_incorrect_variance.py:9:5: PLC0105 `ParamSpec` name "P" does not reflect its covariance; consider renaming it to "P_co" | 7 | T = TypeVar("T", contravariant=True) 8 | T = TypeVar("T", covariant=False, contravariant=True) @@ -50,7 +50,7 @@ type_name_incorrect_variance.py:9:5: PLC0105 `ParamSpec` name "P" does not match 11 | P = ParamSpec("P", contravariant=True) | -type_name_incorrect_variance.py:10:5: PLC0105 `ParamSpec` name "P" does not match variance +type_name_incorrect_variance.py:10:5: PLC0105 `ParamSpec` name "P" does not reflect its covariance; consider renaming it to "P_co" | 8 | T = TypeVar("T", covariant=False, contravariant=True) 9 | P = ParamSpec("P", covariant=True) @@ -60,7 +60,7 @@ type_name_incorrect_variance.py:10:5: PLC0105 `ParamSpec` name "P" does not matc 12 | P = ParamSpec("P", covariant=False, contravariant=True) | -type_name_incorrect_variance.py:11:5: PLC0105 `ParamSpec` name "P" does not match variance +type_name_incorrect_variance.py:11:5: PLC0105 `ParamSpec` name "P" does not reflect its contravariance; consider renaming it to "P_contra" | 9 | P = ParamSpec("P", covariant=True) 10 | P = ParamSpec("P", covariant=True, contravariant=False) @@ -69,7 +69,7 @@ type_name_incorrect_variance.py:11:5: PLC0105 `ParamSpec` name "P" does not matc 12 | P = ParamSpec("P", covariant=False, contravariant=True) | -type_name_incorrect_variance.py:12:5: PLC0105 `ParamSpec` name "P" does not match variance +type_name_incorrect_variance.py:12:5: PLC0105 `ParamSpec` name "P" does not reflect its contravariance; consider renaming it to "P_contra" | 10 | P = ParamSpec("P", covariant=True, contravariant=False) 11 | P = ParamSpec("P", contravariant=True) @@ -79,7 +79,7 @@ type_name_incorrect_variance.py:12:5: PLC0105 `ParamSpec` name "P" does not matc 14 | T_co = TypeVar("T_co") | -type_name_incorrect_variance.py:14:8: PLC0105 `TypeVar` name "T_co" does not match variance +type_name_incorrect_variance.py:14:8: PLC0105 `TypeVar` name "T_co" does not reflect its invariance; consider renaming it to "T" | 12 | P = ParamSpec("P", covariant=False, contravariant=True) 13 | @@ -89,7 +89,7 @@ type_name_incorrect_variance.py:14:8: PLC0105 `TypeVar` name "T_co" does not mat 16 | T_co = TypeVar("T_co", contravariant=False) | -type_name_incorrect_variance.py:15:8: PLC0105 `TypeVar` name "T_co" does not match variance +type_name_incorrect_variance.py:15:8: PLC0105 `TypeVar` name "T_co" does not reflect its invariance; consider renaming it to "T" | 14 | T_co = TypeVar("T_co") 15 | T_co = TypeVar("T_co", covariant=False) @@ -98,7 +98,7 @@ type_name_incorrect_variance.py:15:8: PLC0105 `TypeVar` name "T_co" does not mat 17 | T_co = TypeVar("T_co", covariant=False, contravariant=False) | -type_name_incorrect_variance.py:16:8: PLC0105 `TypeVar` name "T_co" does not match variance +type_name_incorrect_variance.py:16:8: PLC0105 `TypeVar` name "T_co" does not reflect its invariance; consider renaming it to "T" | 14 | T_co = TypeVar("T_co") 15 | T_co = TypeVar("T_co", covariant=False) @@ -108,7 +108,7 @@ type_name_incorrect_variance.py:16:8: PLC0105 `TypeVar` name "T_co" does not mat 18 | T_co = TypeVar("T_co", contravariant=True) | -type_name_incorrect_variance.py:17:8: PLC0105 `TypeVar` name "T_co" does not match variance +type_name_incorrect_variance.py:17:8: PLC0105 `TypeVar` name "T_co" does not reflect its invariance; consider renaming it to "T" | 15 | T_co = TypeVar("T_co", covariant=False) 16 | T_co = TypeVar("T_co", contravariant=False) @@ -118,7 +118,7 @@ type_name_incorrect_variance.py:17:8: PLC0105 `TypeVar` name "T_co" does not mat 19 | T_co = TypeVar("T_co", covariant=False, contravariant=True) | -type_name_incorrect_variance.py:18:8: PLC0105 `TypeVar` name "T_co" does not match variance +type_name_incorrect_variance.py:18:8: PLC0105 `TypeVar` name "T_co" does not reflect its contravariance; consider renaming it to "T_contra" | 16 | T_co = TypeVar("T_co", contravariant=False) 17 | T_co = TypeVar("T_co", covariant=False, contravariant=False) @@ -128,7 +128,7 @@ type_name_incorrect_variance.py:18:8: PLC0105 `TypeVar` name "T_co" does not mat 20 | P_co = ParamSpec("P_co") | -type_name_incorrect_variance.py:19:8: PLC0105 `TypeVar` name "T_co" does not match variance +type_name_incorrect_variance.py:19:8: PLC0105 `TypeVar` name "T_co" does not reflect its contravariance; consider renaming it to "T_contra" | 17 | T_co = TypeVar("T_co", covariant=False, contravariant=False) 18 | T_co = TypeVar("T_co", contravariant=True) @@ -138,7 +138,7 @@ type_name_incorrect_variance.py:19:8: PLC0105 `TypeVar` name "T_co" does not mat 21 | P_co = ParamSpec("P_co", covariant=False) | -type_name_incorrect_variance.py:20:8: PLC0105 `ParamSpec` name "P_co" does not match variance +type_name_incorrect_variance.py:20:8: PLC0105 `ParamSpec` name "P_co" does not reflect its invariance; consider renaming it to "P" | 18 | T_co = TypeVar("T_co", contravariant=True) 19 | T_co = TypeVar("T_co", covariant=False, contravariant=True) @@ -148,7 +148,7 @@ type_name_incorrect_variance.py:20:8: PLC0105 `ParamSpec` name "P_co" does not m 22 | P_co = ParamSpec("P_co", contravariant=False) | -type_name_incorrect_variance.py:21:8: PLC0105 `ParamSpec` name "P_co" does not match variance +type_name_incorrect_variance.py:21:8: PLC0105 `ParamSpec` name "P_co" does not reflect its invariance; consider renaming it to "P" | 19 | T_co = TypeVar("T_co", covariant=False, contravariant=True) 20 | P_co = ParamSpec("P_co") @@ -158,7 +158,7 @@ type_name_incorrect_variance.py:21:8: PLC0105 `ParamSpec` name "P_co" does not m 23 | P_co = ParamSpec("P_co", covariant=False, contravariant=False) | -type_name_incorrect_variance.py:22:8: PLC0105 `ParamSpec` name "P_co" does not match variance +type_name_incorrect_variance.py:22:8: PLC0105 `ParamSpec` name "P_co" does not reflect its invariance; consider renaming it to "P" | 20 | P_co = ParamSpec("P_co") 21 | P_co = ParamSpec("P_co", covariant=False) @@ -168,7 +168,7 @@ type_name_incorrect_variance.py:22:8: PLC0105 `ParamSpec` name "P_co" does not m 24 | P_co = ParamSpec("P_co", contravariant=True) | -type_name_incorrect_variance.py:23:8: PLC0105 `ParamSpec` name "P_co" does not match variance +type_name_incorrect_variance.py:23:8: PLC0105 `ParamSpec` name "P_co" does not reflect its invariance; consider renaming it to "P" | 21 | P_co = ParamSpec("P_co", covariant=False) 22 | P_co = ParamSpec("P_co", contravariant=False) @@ -178,7 +178,7 @@ type_name_incorrect_variance.py:23:8: PLC0105 `ParamSpec` name "P_co" does not m 25 | P_co = ParamSpec("P_co", covariant=False, contravariant=True) | -type_name_incorrect_variance.py:24:8: PLC0105 `ParamSpec` name "P_co" does not match variance +type_name_incorrect_variance.py:24:8: PLC0105 `ParamSpec` name "P_co" does not reflect its contravariance; consider renaming it to "P_contra" | 22 | P_co = ParamSpec("P_co", contravariant=False) 23 | P_co = ParamSpec("P_co", covariant=False, contravariant=False) @@ -187,7 +187,7 @@ type_name_incorrect_variance.py:24:8: PLC0105 `ParamSpec` name "P_co" does not m 25 | P_co = ParamSpec("P_co", covariant=False, contravariant=True) | -type_name_incorrect_variance.py:25:8: PLC0105 `ParamSpec` name "P_co" does not match variance +type_name_incorrect_variance.py:25:8: PLC0105 `ParamSpec` name "P_co" does not reflect its contravariance; consider renaming it to "P_contra" | 23 | P_co = ParamSpec("P_co", covariant=False, contravariant=False) 24 | P_co = ParamSpec("P_co", contravariant=True) @@ -197,7 +197,7 @@ type_name_incorrect_variance.py:25:8: PLC0105 `ParamSpec` name "P_co" does not m 27 | T_contra = TypeVar("T_contra") | -type_name_incorrect_variance.py:27:12: PLC0105 `TypeVar` name "T_contra" does not match variance +type_name_incorrect_variance.py:27:12: PLC0105 `TypeVar` name "T_contra" does not reflect its invariance; consider renaming it to "T" | 25 | P_co = ParamSpec("P_co", covariant=False, contravariant=True) 26 | @@ -207,7 +207,7 @@ type_name_incorrect_variance.py:27:12: PLC0105 `TypeVar` name "T_contra" does no 29 | T_contra = TypeVar("T_contra", contravariant=False) | -type_name_incorrect_variance.py:28:12: PLC0105 `TypeVar` name "T_contra" does not match variance +type_name_incorrect_variance.py:28:12: PLC0105 `TypeVar` name "T_contra" does not reflect its invariance; consider renaming it to "T" | 27 | T_contra = TypeVar("T_contra") 28 | T_contra = TypeVar("T_contra", covariant=False) @@ -216,7 +216,7 @@ type_name_incorrect_variance.py:28:12: PLC0105 `TypeVar` name "T_contra" does no 30 | T_contra = TypeVar("T_contra", covariant=False, contravariant=False) | -type_name_incorrect_variance.py:29:12: PLC0105 `TypeVar` name "T_contra" does not match variance +type_name_incorrect_variance.py:29:12: PLC0105 `TypeVar` name "T_contra" does not reflect its invariance; consider renaming it to "T" | 27 | T_contra = TypeVar("T_contra") 28 | T_contra = TypeVar("T_contra", covariant=False) @@ -226,7 +226,7 @@ type_name_incorrect_variance.py:29:12: PLC0105 `TypeVar` name "T_contra" does no 31 | T_contra = TypeVar("T_contra", covariant=True) | -type_name_incorrect_variance.py:30:12: PLC0105 `TypeVar` name "T_contra" does not match variance +type_name_incorrect_variance.py:30:12: PLC0105 `TypeVar` name "T_contra" does not reflect its invariance; consider renaming it to "T" | 28 | T_contra = TypeVar("T_contra", covariant=False) 29 | T_contra = TypeVar("T_contra", contravariant=False) @@ -236,7 +236,7 @@ type_name_incorrect_variance.py:30:12: PLC0105 `TypeVar` name "T_contra" does no 32 | T_contra = TypeVar("T_contra", covariant=True, contravariant=False) | -type_name_incorrect_variance.py:31:12: PLC0105 `TypeVar` name "T_contra" does not match variance +type_name_incorrect_variance.py:31:12: PLC0105 `TypeVar` name "T_contra" does not reflect its covariance; consider renaming it to "T_co" | 29 | T_contra = TypeVar("T_contra", contravariant=False) 30 | T_contra = TypeVar("T_contra", covariant=False, contravariant=False) @@ -246,7 +246,7 @@ type_name_incorrect_variance.py:31:12: PLC0105 `TypeVar` name "T_contra" does no 33 | P_contra = ParamSpec("P_contra") | -type_name_incorrect_variance.py:32:12: PLC0105 `TypeVar` name "T_contra" does not match variance +type_name_incorrect_variance.py:32:12: PLC0105 `TypeVar` name "T_contra" does not reflect its covariance; consider renaming it to "T_co" | 30 | T_contra = TypeVar("T_contra", covariant=False, contravariant=False) 31 | T_contra = TypeVar("T_contra", covariant=True) @@ -256,7 +256,7 @@ type_name_incorrect_variance.py:32:12: PLC0105 `TypeVar` name "T_contra" does no 34 | P_contra = ParamSpec("P_contra", covariant=False) | -type_name_incorrect_variance.py:33:12: PLC0105 `ParamSpec` name "P_contra" does not match variance +type_name_incorrect_variance.py:33:12: PLC0105 `ParamSpec` name "P_contra" does not reflect its invariance; consider renaming it to "P" | 31 | T_contra = TypeVar("T_contra", covariant=True) 32 | T_contra = TypeVar("T_contra", covariant=True, contravariant=False) @@ -266,7 +266,7 @@ type_name_incorrect_variance.py:33:12: PLC0105 `ParamSpec` name "P_contra" does 35 | P_contra = ParamSpec("P_contra", contravariant=False) | -type_name_incorrect_variance.py:34:12: PLC0105 `ParamSpec` name "P_contra" does not match variance +type_name_incorrect_variance.py:34:12: PLC0105 `ParamSpec` name "P_contra" does not reflect its invariance; consider renaming it to "P" | 32 | T_contra = TypeVar("T_contra", covariant=True, contravariant=False) 33 | P_contra = ParamSpec("P_contra") @@ -276,7 +276,7 @@ type_name_incorrect_variance.py:34:12: PLC0105 `ParamSpec` name "P_contra" does 36 | P_contra = ParamSpec("P_contra", covariant=False, contravariant=False) | -type_name_incorrect_variance.py:35:12: PLC0105 `ParamSpec` name "P_contra" does not match variance +type_name_incorrect_variance.py:35:12: PLC0105 `ParamSpec` name "P_contra" does not reflect its invariance; consider renaming it to "P" | 33 | P_contra = ParamSpec("P_contra") 34 | P_contra = ParamSpec("P_contra", covariant=False) @@ -286,7 +286,7 @@ type_name_incorrect_variance.py:35:12: PLC0105 `ParamSpec` name "P_contra" does 37 | P_contra = ParamSpec("P_contra", covariant=True) | -type_name_incorrect_variance.py:36:12: PLC0105 `ParamSpec` name "P_contra" does not match variance +type_name_incorrect_variance.py:36:12: PLC0105 `ParamSpec` name "P_contra" does not reflect its invariance; consider renaming it to "P" | 34 | P_contra = ParamSpec("P_contra", covariant=False) 35 | P_contra = ParamSpec("P_contra", contravariant=False) @@ -296,7 +296,7 @@ type_name_incorrect_variance.py:36:12: PLC0105 `ParamSpec` name "P_contra" does 38 | P_contra = ParamSpec("P_contra", covariant=True, contravariant=False) | -type_name_incorrect_variance.py:37:12: PLC0105 `ParamSpec` name "P_contra" does not match variance +type_name_incorrect_variance.py:37:12: PLC0105 `ParamSpec` name "P_contra" does not reflect its covariance; consider renaming it to "P_co" | 35 | P_contra = ParamSpec("P_contra", contravariant=False) 36 | P_contra = ParamSpec("P_contra", covariant=False, contravariant=False) @@ -305,7 +305,7 @@ type_name_incorrect_variance.py:37:12: PLC0105 `ParamSpec` name "P_contra" does 38 | P_contra = ParamSpec("P_contra", covariant=True, contravariant=False) | -type_name_incorrect_variance.py:38:12: PLC0105 `ParamSpec` name "P_contra" does not match variance +type_name_incorrect_variance.py:38:12: PLC0105 `ParamSpec` name "P_contra" does not reflect its covariance; consider renaming it to "P_co" | 36 | P_contra = ParamSpec("P_contra", covariant=False, contravariant=False) 37 | P_contra = ParamSpec("P_contra", covariant=True) From b8a6ce43a27e9b79159f214e49cb7aee4bed4c6f Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Mon, 10 Jul 2023 19:19:17 +0100 Subject: [PATCH 400/447] Properly ignore bivariate types in `type-name-incorrect-variance` (#5660) ## Summary #5658 didn't actually ignore bivariate types in some all cases (sorry about that). This PR fixes that and adds bivariate types to the test fixture. ## Test Plan `cargo test` --- .../test/fixtures/pylint/type_name_incorrect_variance.py | 9 +++++++++ .../rules/pylint/rules/type_name_incorrect_variance.rs | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/crates/ruff/resources/test/fixtures/pylint/type_name_incorrect_variance.py b/crates/ruff/resources/test/fixtures/pylint/type_name_incorrect_variance.py index 33f68d2a04..f000a2ba0b 100644 --- a/crates/ruff/resources/test/fixtures/pylint/type_name_incorrect_variance.py +++ b/crates/ruff/resources/test/fixtures/pylint/type_name_incorrect_variance.py @@ -57,3 +57,12 @@ T_contra = TypeVar("T_contra", contravariant=True) T_contra = TypeVar("T_contra", covariant=False, contravariant=True) P_contra = ParamSpec("P_contra", contravariant=True) P_contra = ParamSpec("P_contra", covariant=False, contravariant=True) + +# Bivariate types are errors, but not covered by this check. + +T = TypeVar("T", covariant=True, contravariant=True) +P = ParamSpec("P", covariant=True, contravariant=True) +T_co = TypeVar("T_co", covariant=True, contravariant=True) +P_co = ParamSpec("P_co", covariant=True, contravariant=True) +T_contra = TypeVar("T_contra", covariant=True, contravariant=True) +P_contra = ParamSpec("P_contra", covariant=True, contravariant=True) diff --git a/crates/ruff/src/rules/pylint/rules/type_name_incorrect_variance.rs b/crates/ruff/src/rules/pylint/rules/type_name_incorrect_variance.rs index 9af30526ae..8683e09d15 100644 --- a/crates/ruff/src/rules/pylint/rules/type_name_incorrect_variance.rs +++ b/crates/ruff/src/rules/pylint/rules/type_name_incorrect_variance.rs @@ -130,6 +130,7 @@ pub(crate) fn type_name_incorrect_variance(checker: &mut Checker, value: &Expr) .trim_end_matches("_co") .trim_end_matches("_contra"); let replacement_name: String = match variance { + VarVariance::Bivariance => return, // Bivariate type are invalid, so ignore them for this rule. VarVariance::Covariance => format!("{name_root}_co"), VarVariance::Contravariance => format!("{name_root}_contra"), VarVariance::Invariance => name_root.to_string(), @@ -163,6 +164,7 @@ fn variance(covariant: Option<&Expr>, contravariant: Option<&Expr>) -> VarVarian covariant.map(is_const_true), contravariant.map(is_const_true), ) { + (Some(true), Some(true)) => VarVariance::Bivariance, (Some(true), _) => VarVariance::Covariance, (_, Some(true)) => VarVariance::Contravariance, _ => VarVariance::Invariance, @@ -186,6 +188,7 @@ impl fmt::Display for VarKind { #[derive(Debug, PartialEq, Eq, Copy, Clone)] enum VarVariance { + Bivariance, Covariance, Contravariance, Invariance, @@ -194,6 +197,7 @@ enum VarVariance { impl fmt::Display for VarVariance { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { match self { + VarVariance::Bivariance => fmt.write_str("bivariance"), VarVariance::Covariance => fmt.write_str("covariance"), VarVariance::Contravariance => fmt.write_str("contravariance"), VarVariance::Invariance => fmt.write_str("invariance"), From 14f2158e5dfa9dc7af9158958a05fc71835393dd Mon Sep 17 00:00:00 2001 From: monosans Date: Mon, 10 Jul 2023 18:52:59 +0000 Subject: [PATCH 401/447] [`flake8-self`] Ignore `_name_` and `_value_` (#5663) ## Summary `Enum._name_` and `Enum._value_` are so named to prevent conflicts. See . ## Test Plan Tests for `ignore-names` already exist. --- crates/ruff/src/rules/flake8_self/settings.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/ruff/src/rules/flake8_self/settings.rs b/crates/ruff/src/rules/flake8_self/settings.rs index 728b64c97c..004815516d 100644 --- a/crates/ruff/src/rules/flake8_self/settings.rs +++ b/crates/ruff/src/rules/flake8_self/settings.rs @@ -4,9 +4,18 @@ use serde::{Deserialize, Serialize}; use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions}; -// By default, ignore the `namedtuple` methods and attributes, which are underscore-prefixed to -// prevent conflicts with field names. -const IGNORE_NAMES: [&str; 5] = ["_make", "_asdict", "_replace", "_fields", "_field_defaults"]; +// By default, ignore the `namedtuple` methods and attributes, as well as the +// _sunder_ names in Enum, which are underscore-prefixed to prevent conflicts +// with field names. +const IGNORE_NAMES: [&str; 7] = [ + "_make", + "_asdict", + "_replace", + "_fields", + "_field_defaults", + "_name_", + "_value_", +]; #[derive( Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions, CombineOptions, @@ -19,7 +28,7 @@ const IGNORE_NAMES: [&str; 5] = ["_make", "_asdict", "_replace", "_fields", "_fi #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct Options { #[option( - default = r#"["_make", "_asdict", "_replace", "_fields", "_field_defaults"]"#, + default = r#"["_make", "_asdict", "_replace", "_fields", "_field_defaults", "_name_", "_value_"]"#, value_type = "list[str]", example = r#" ignore-names = ["_new"] From 93bfa239b71bc7e53a3ffc65764231811dc7a34d Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Tue, 11 Jul 2023 00:47:05 +0530 Subject: [PATCH 402/447] Add Jupyter Notebook usage with `pre-commit` in docs (#5666) Similar to https://github.com/astral-sh/ruff-pre-commit/pull/45 --- docs/usage.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/usage.md b/docs/usage.md index 1a84c8eddd..2e1d4d77d2 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -38,6 +38,17 @@ Or, to enable autofix: args: [ --fix, --exit-non-zero-on-fix ] ``` +Or, to run the hook on Jupyter Notebooks too: + +```yaml +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.0.277 + hooks: + - id: ruff + types_or: [python, pyi, jupyter] +``` + Ruff's pre-commit hook should be placed after other formatting tools, such as Black and isort, _unless_ you enable autofix, in which case, Ruff's pre-commit hook should run _before_ Black, isort, and other formatting tools, as Ruff's autofix behavior can output code changes that require From e7e2f44440bb9d7cc61260f66f82a20efdac8eff Mon Sep 17 00:00:00 2001 From: Louis Dispa Date: Mon, 10 Jul 2023 21:23:49 +0200 Subject: [PATCH 403/447] Format `raise` statement (#5595) ## Summary This PR implements the formatting of `raise` statements. I haven't looked at the black implementation, this is inspired from from the `return` statements formatting. ## Test Plan The black differences with insta. I also compared manually some edge cases with very long string and call chaining and it seems to do the same formatting as black. There is one issue: ```python # input raise OsError( "aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa" ) from a.aaaaa(aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa).a(aaaa) # black raise OsError( "aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa" ) from a.aaaaa( aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa ).a( aaaa ) # ruff raise OsError( "aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa" ) from a.aaaaa( aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa ).a(aaaa) ``` But I'm not sure this diff is the raise formatting implementation. --------- Co-authored-by: Louis Dispa --- .../test/fixtures/ruff/expression/tuple.py | 9 +- .../test/fixtures/ruff/statement/raise.py | 85 +++++++ .../ruff_python_formatter/src/comments/mod.rs | 4 +- .../src/expression/expr_tuple.rs | 31 ++- .../src/statement/stmt_raise.rs | 35 ++- ...ompatibility@py_311__pep_654_style.py.snap | 70 +----- ...mpatibility@simple_cases__fmtonoff.py.snap | 4 +- ...mpatibility@simple_cases__function.py.snap | 11 +- ...simple_cases__remove_except_parens.py.snap | 54 +---- .../format@expression__tuple.py.snap | 16 +- .../snapshots/format@statement__raise.py.snap | 220 ++++++++++++++++++ 11 files changed, 409 insertions(+), 130 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/raise.py create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@statement__raise.py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tuple.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tuple.py index 517f5d39e7..13859fb27d 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tuple.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tuple.py @@ -35,9 +35,16 @@ e4 = ( # Empty tuples and comments f1 = ( - # empty + # empty ) f2 = () +f3 = ( # end-of-line + # own-line +) # trailing +f4 = ( # end-of-line + # own-line + # own-line 2 +) # trailing # Comments in other tuples g1 = ( # a diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/raise.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/raise.py new file mode 100644 index 0000000000..db15be22e8 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/raise.py @@ -0,0 +1,85 @@ +raise a from aksjdhflsakhdflkjsadlfajkslhf +raise a from (aksjdhflsakhdflkjsadlfajkslhf,) +raise (aaaaa.aaa(a).a) from (aksjdhflsakhdflkjsadlfajkslhf) + +raise a from (aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa,) +raise a from OsError("aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa") + +# some comment +raise a from aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa # some comment +# some comment + +raise OsError("aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa") from e + + +raise OsError( + # should i stay + "aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa" # mhhh very long + # or should i go +) from e # here is e + +raise OsError("aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa") from OsError("aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa") + +raise OsError( + "aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa" +) from a.aaaaa( + aaa +).a(aaaa) + +raise OsError( + "aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa" +) from a.aaaaa(aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa).a(aaaa) + +raise aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccc + ddddddddddddddddddddddddd +raise aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbb + (cccccccccccccccccccccc + ddddddddddddddddddddddddd) +raise (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccc + ddddddddddddddddddddddddd) + + +raise ( # hey + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + # Holala + + bbbbbbbbbbbbbbbbbbbbbbbbbb # stay + + cccccccccccccccccccccc + ddddddddddddddddddddddddd # where I'm going + # I don't know +) # whaaaaat +# the end + +raise ( # hey 2 + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + # Holala + "bbbbbbbbbbbbbbbbbbbbbbbb" # stay + "ccccccccccccccccccccccc" "dddddddddddddddddddddddd" # where I'm going + # I don't know +) # whaaaaat + +# some comment +raise aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:bbbbbbbbbbbbbbbbbbbbbbbbbb] + +raise aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa < aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa = MultiMap, SourceComment>; /// The comments of a syntax tree stored by node. /// /// Cloning `comments` is cheap as it only involves bumping a reference counter. -#[derive(Clone, Default)] +#[derive(Debug, Clone, Default)] pub(crate) struct Comments<'a> { /// The implementation uses an [Rc] so that [Comments] has a lifetime independent from the [crate::Formatter]. /// Independent lifetimes are necessary to support the use case where a (formattable object)[crate::Format] @@ -400,7 +400,7 @@ impl<'a> Comments<'a> { } } -#[derive(Default)] +#[derive(Debug, Default)] struct CommentsData<'a> { comments: CommentsMap<'a>, } diff --git a/crates/ruff_python_formatter/src/expression/expr_tuple.rs b/crates/ruff_python_formatter/src/expression/expr_tuple.rs index de97a5bac9..82d716ae1d 100644 --- a/crates/ruff_python_formatter/src/expression/expr_tuple.rs +++ b/crates/ruff_python_formatter/src/expression/expr_tuple.rs @@ -1,5 +1,5 @@ use crate::builders::optional_parentheses; -use crate::comments::{dangling_node_comments, Comments}; +use crate::comments::{dangling_comments, CommentLinePosition, Comments}; use crate::expression::parentheses::{ default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, Parenthesize, @@ -57,16 +57,33 @@ impl FormatNodeRule for FormatExprTuple { } = item; // Handle the edge cases of an empty tuple and a tuple with one element + // + // there can be dangling comments, and they can be in two + // positions: + // ```python + // a3 = ( # end-of-line + // # own line + // ) + // ``` + // In all other cases comments get assigned to a list element match elts.as_slice() { [] => { + let comments = f.context().comments().clone(); + let dangling = comments.dangling_comments(item); + let end_of_line_split = dangling.partition_point(|comment| { + comment.line_position() == CommentLinePosition::EndOfLine + }); + debug_assert!(dangling[end_of_line_split..] + .iter() + .all(|comment| comment.line_position() == CommentLinePosition::OwnLine)); write!( f, - [ - // An empty tuple always needs parentheses, but does not have a comma - &text("("), - block_indent(&dangling_node_comments(item)), - &text(")"), - ] + [group(&format_args![ + text("("), + dangling_comments(&dangling[..end_of_line_split]), + soft_block_indent(&dangling_comments(&dangling[end_of_line_split..])), + text(")") + ])] ) } [single] => { diff --git a/crates/ruff_python_formatter/src/statement/stmt_raise.rs b/crates/ruff_python_formatter/src/statement/stmt_raise.rs index 47372372a5..5159d3ab35 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_raise.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_raise.rs @@ -1,5 +1,8 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::expression::parentheses::Parenthesize; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{space, text}; +use ruff_formatter::{write, Buffer, Format, FormatResult}; + use rustpython_parser::ast::StmtRaise; #[derive(Default)] @@ -7,6 +10,32 @@ pub struct FormatStmtRaise; impl FormatNodeRule for FormatStmtRaise { fn fmt_fields(&self, item: &StmtRaise, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let StmtRaise { + range: _, + exc, + cause, + } = item; + + text("raise").fmt(f)?; + + if let Some(value) = exc { + write!( + f, + [space(), value.format().with_options(Parenthesize::Optional)] + )?; + } + + if let Some(value) = cause { + write!( + f, + [ + space(), + text("from"), + space(), + value.format().with_options(Parenthesize::Optional) + ] + )?; + } + Ok(()) } } diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654_style.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654_style.py.snap index 19568a1176..b0bc8e8f17 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654_style.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654_style.py.snap @@ -67,68 +67,22 @@ except ExceptionGroup as e: ```diff --- Black +++ Ruff -@@ -1,5 +1,5 @@ - try: -- raise OSError("blah") -+ NOT_YET_IMPLEMENTED_StmtRaise - except* ExceptionGroup as e: - pass - -@@ -14,10 +14,10 @@ - +@@ -39,7 +39,7 @@ try: try: -- raise ValueError(42) -+ NOT_YET_IMPLEMENTED_StmtRaise - except: - try: -- raise TypeError(int) -+ NOT_YET_IMPLEMENTED_StmtRaise - except* Exception: - pass - 1 / 0 -@@ -26,10 +26,10 @@ - - try: - try: -- raise FalsyEG("eg", [TypeError(1), ValueError(2)]) -+ NOT_YET_IMPLEMENTED_StmtRaise - except* TypeError as e: - tes = e -- raise -+ NOT_YET_IMPLEMENTED_StmtRaise - except* ValueError as e: - ves = e - pass -@@ -38,16 +38,16 @@ - - try: - try: -- raise orig + raise orig - except* (TypeError, ValueError, *OTHER_EXCEPTIONS) as e: -- raise SyntaxError(3) from e -+ NOT_YET_IMPLEMENTED_StmtRaise + except* (TypeError, ValueError, *NOT_YET_IMPLEMENTED_ExprStarred) as e: -+ NOT_YET_IMPLEMENTED_StmtRaise + raise SyntaxError(3) from e except BaseException as e: exc = e - - try: - try: -- raise orig -+ NOT_YET_IMPLEMENTED_StmtRaise - except* OSError as e: -- raise TypeError(3) from e -+ NOT_YET_IMPLEMENTED_StmtRaise - except ExceptionGroup as e: - exc = e ``` ## Ruff Output ```py try: - NOT_YET_IMPLEMENTED_StmtRaise + raise OSError("blah") except* ExceptionGroup as e: pass @@ -143,10 +97,10 @@ except* ValueError: try: try: - NOT_YET_IMPLEMENTED_StmtRaise + raise ValueError(42) except: try: - NOT_YET_IMPLEMENTED_StmtRaise + raise TypeError(int) except* Exception: pass 1 / 0 @@ -155,10 +109,10 @@ except Exception as e: try: try: - NOT_YET_IMPLEMENTED_StmtRaise + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) except* TypeError as e: tes = e - NOT_YET_IMPLEMENTED_StmtRaise + raise except* ValueError as e: ves = e pass @@ -167,17 +121,17 @@ except Exception as e: try: try: - NOT_YET_IMPLEMENTED_StmtRaise + raise orig except* (TypeError, ValueError, *NOT_YET_IMPLEMENTED_ExprStarred) as e: - NOT_YET_IMPLEMENTED_StmtRaise + raise SyntaxError(3) from e except BaseException as e: exc = e try: try: - NOT_YET_IMPLEMENTED_StmtRaise + raise orig except* OSError as e: - NOT_YET_IMPLEMENTED_StmtRaise + raise TypeError(3) from e except ExceptionGroup as e: exc = e ``` 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 48167498b5..883baf2298 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 @@ -227,7 +227,7 @@ d={'a':1, + b + c + if True: -+ NOT_YET_IMPLEMENTED_StmtRaise ++ raise RuntimeError + if False: + ... + for i in range(10): @@ -425,7 +425,7 @@ def func_no_args(): b c if True: - NOT_YET_IMPLEMENTED_StmtRaise + raise RuntimeError if False: ... for i in range(10): 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 2ccef6cead..87611ac5b9 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 @@ -117,15 +117,6 @@ def __await__(): return (yield) def func_no_args(): -@@ -14,7 +13,7 @@ - b - c - if True: -- raise RuntimeError -+ NOT_YET_IMPLEMENTED_StmtRaise - if False: - ... - for i in range(10): @@ -41,12 +40,12 @@ debug: bool = False, **kwargs, @@ -207,7 +198,7 @@ def func_no_args(): b c if True: - NOT_YET_IMPLEMENTED_StmtRaise + raise RuntimeError if False: ... for i in range(10): diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_except_parens.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_except_parens.py.snap index 5990c185cd..1b9f9e5d08 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_except_parens.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_except_parens.py.snap @@ -47,55 +47,17 @@ except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.ov ```diff --- Black +++ Ruff -@@ -2,7 +2,7 @@ - try: - a.something - except AttributeError as err: -- raise err -+ NOT_YET_IMPLEMENTED_StmtRaise - - # This is tuple of exceptions. - # Although this could be replaced with just the exception, -@@ -10,28 +10,26 @@ - try: - a.something - except (AttributeError,) as err: -- raise err -+ NOT_YET_IMPLEMENTED_StmtRaise - - # This is a tuple of exceptions. Do not remove brackets. - try: - a.something - except (AttributeError, ValueError) as err: -- raise err -+ NOT_YET_IMPLEMENTED_StmtRaise - +@@ -21,9 +21,7 @@ # Test long variants. try: a.something -except ( - some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error -) as err: -- raise err +except some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error as err: -+ NOT_YET_IMPLEMENTED_StmtRaise + raise err try: - a.something - except ( - some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, - ) as err: -- raise err -+ NOT_YET_IMPLEMENTED_StmtRaise - - try: - a.something -@@ -39,4 +37,4 @@ - some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, - some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, - ) as err: -- raise err -+ NOT_YET_IMPLEMENTED_StmtRaise ``` ## Ruff Output @@ -105,7 +67,7 @@ except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.ov try: a.something except AttributeError as err: - NOT_YET_IMPLEMENTED_StmtRaise + raise err # This is tuple of exceptions. # Although this could be replaced with just the exception, @@ -113,26 +75,26 @@ except AttributeError as err: try: a.something except (AttributeError,) as err: - NOT_YET_IMPLEMENTED_StmtRaise + raise err # This is a tuple of exceptions. Do not remove brackets. try: a.something except (AttributeError, ValueError) as err: - NOT_YET_IMPLEMENTED_StmtRaise + raise err # Test long variants. try: a.something except some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error as err: - NOT_YET_IMPLEMENTED_StmtRaise + raise err try: a.something except ( some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, ) as err: - NOT_YET_IMPLEMENTED_StmtRaise + raise err try: a.something @@ -140,7 +102,7 @@ except ( some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, ) as err: - NOT_YET_IMPLEMENTED_StmtRaise + raise err ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__tuple.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__tuple.py.snap index 39b0d747e8..e398db039b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__tuple.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__tuple.py.snap @@ -41,9 +41,16 @@ e4 = ( # Empty tuples and comments f1 = ( - # empty + # empty ) f2 = () +f3 = ( # end-of-line + # own-line +) # trailing +f4 = ( # end-of-line + # own-line + # own-line 2 +) # trailing # Comments in other tuples g1 = ( # a @@ -232,6 +239,13 @@ f1 = ( # empty ) f2 = () +f3 = ( # end-of-line + # own-line +) # trailing +f4 = ( # end-of-line + # own-line + # own-line 2 +) # trailing # Comments in other tuples g1 = ( diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__raise.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__raise.py.snap new file mode 100644 index 0000000000..d36d48fbfb --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__raise.py.snap @@ -0,0 +1,220 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/raise.py +--- +## Input +```py +raise a from aksjdhflsakhdflkjsadlfajkslhf +raise a from (aksjdhflsakhdflkjsadlfajkslhf,) +raise (aaaaa.aaa(a).a) from (aksjdhflsakhdflkjsadlfajkslhf) + +raise a from (aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa,) +raise a from OsError("aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa") + +# some comment +raise a from aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa # some comment +# some comment + +raise OsError("aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa") from e + + +raise OsError( + # should i stay + "aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa" # mhhh very long + # or should i go +) from e # here is e + +raise OsError("aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa") from OsError("aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa") + +raise OsError( + "aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa" +) from a.aaaaa( + aaa +).a(aaaa) + +raise OsError( + "aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa" +) from a.aaaaa(aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa).a(aaaa) + +raise aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccc + ddddddddddddddddddddddddd +raise aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbb + (cccccccccccccccccccccc + ddddddddddddddddddddddddd) +raise (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccc + ddddddddddddddddddddddddd) + + +raise ( # hey + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + # Holala + + bbbbbbbbbbbbbbbbbbbbbbbbbb # stay + + cccccccccccccccccccccc + ddddddddddddddddddddddddd # where I'm going + # I don't know +) # whaaaaat +# the end + +raise ( # hey 2 + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + # Holala + "bbbbbbbbbbbbbbbbbbbbbbbb" # stay + "ccccccccccccccccccccccc" "dddddddddddddddddddddddd" # where I'm going + # I don't know +) # whaaaaat + +# some comment +raise aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:bbbbbbbbbbbbbbbbbbbbbbbbbb] + +raise aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa < aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa Date: Mon, 10 Jul 2023 23:44:38 -0400 Subject: [PATCH 404/447] Run nightly Clippy over the Ruff repo (#5670) ## Summary This is the result of running `cargo +nightly clippy --workspace --all-targets --all-features -- -D warnings` and fixing all violations. Just wanted to see if there were any interesting new checks on nightly :eyes: --- crates/ruff/src/autofix/edits.rs | 8 +- crates/ruff/src/checkers/ast/mod.rs | 8 +- crates/ruff/src/directives.rs | 12 +- crates/ruff/src/message/azure.rs | 2 +- crates/ruff/src/message/github.rs | 2 +- crates/ruff/src/message/json.rs | 2 +- crates/ruff/src/message/json_lines.rs | 4 +- crates/ruff/src/message/junit.rs | 2 +- crates/ruff/src/message/pylint.rs | 2 +- .../flake8_bandit/rules/shell_injection.rs | 2 +- .../ruff/src/rules/flake8_bandit/settings.rs | 7 +- .../rules/reuse_of_groupby_generator.rs | 2 +- .../src/rules/flake8_comprehensions/fixes.rs | 2 +- .../rules/all_with_model_form.rs | 6 +- .../rules/exclude_with_model_form.rs | 6 +- .../rules/model_without_dunder_str.rs | 8 +- .../rules/nullable_model_string_field.rs | 4 +- .../rules/unordered_body_content_in_model.rs | 2 +- .../ruff/src/rules/flake8_gettext/settings.rs | 7 +- .../rules/flake8_quotes/rules/from_tokens.rs | 2 +- .../src/rules/flake8_todos/rules/todos.rs | 6 +- .../src/rules/flake8_type_checking/helpers.rs | 4 +- crates/ruff/src/rules/isort/order.rs | 2 +- .../src/rules/pycodestyle/rules/not_tests.rs | 2 +- .../rules/lru_cache_with_maxsize_none.rs | 2 +- .../rules/lru_cache_without_parameters.rs | 2 +- crates/ruff/src/settings/configuration.rs | 8 +- crates/ruff/src/settings/options_base.rs | 2 +- crates/ruff_cli/src/printer.rs | 12 +- crates/ruff_diagnostics/src/fix.rs | 8 +- crates/ruff_formatter/src/arguments.rs | 2 +- crates/ruff_macros/src/config.rs | 2 +- .../src/source_code/indexer.rs | 4 +- crates/ruff_python_ast/src/types.rs | 2 +- crates/ruff_python_formatter/generate.py | 4 +- .../ruff_python_formatter/src/comments/mod.rs | 2 +- crates/ruff_python_formatter/src/generated.rs | 526 +++++------------- .../ruff_python_formatter/src/module/mod.rs | 4 +- .../src/statement/mod.rs | 4 +- .../src/statement/stmt_function_def.rs | 2 +- 40 files changed, 220 insertions(+), 470 deletions(-) diff --git a/crates/ruff/src/autofix/edits.rs b/crates/ruff/src/autofix/edits.rs index 25272394c4..8526815d2a 100644 --- a/crates/ruff/src/autofix/edits.rs +++ b/crates/ruff/src/autofix/edits.rs @@ -317,10 +317,10 @@ mod tests { Some(TextSize::from(6)) ); - let contents = r#" + let contents = r" x = 1 \ ; y = 1 -"# +" .trim(); let program = Suite::parse(contents, "")?; let stmt = program.first().unwrap(); @@ -349,10 +349,10 @@ x = 1 \ TextSize::from(6) ); - let contents = r#" + let contents = r" x = 1 \ ; y = 1 -"# +" .trim(); let locator = Locator::new(contents); assert_eq!( diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index c522703d8c..48593c06d5 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -776,7 +776,7 @@ where pycodestyle::rules::module_import_not_at_top_of_file(self, stmt, self.locator); } if self.enabled(Rule::GlobalStatement) { - for name in names.iter() { + for name in names { if let Some(asname) = name.asname.as_ref() { pylint::rules::global_statement(self, asname); } else { @@ -972,7 +972,7 @@ where pycodestyle::rules::module_import_not_at_top_of_file(self, stmt, self.locator); } if self.enabled(Rule::GlobalStatement) { - for name in names.iter() { + for name in names { if let Some(asname) = name.asname.as_ref() { pylint::rules::global_statement(self, asname); } else { @@ -1617,7 +1617,7 @@ where flake8_bandit::rules::assign_hardcoded_password_string(self, value, targets); } if self.enabled(Rule::GlobalStatement) { - for target in targets.iter() { + for target in targets { if let Expr::Name(ast::ExprName { id, .. }) = target { pylint::rules::global_statement(self, id); } @@ -1749,7 +1749,7 @@ where } Stmt::Delete(ast::StmtDelete { targets, range: _ }) => { if self.enabled(Rule::GlobalStatement) { - for target in targets.iter() { + for target in targets { if let Expr::Name(ast::ExprName { id, .. }) = target { pylint::rules::global_statement(self, id); } diff --git a/crates/ruff/src/directives.rs b/crates/ruff/src/directives.rs index 2f3c0187ac..4fc919a91c 100644 --- a/crates/ruff/src/directives.rs +++ b/crates/ruff/src/directives.rs @@ -427,22 +427,22 @@ ghi NoqaMapping::from_iter([TextRange::new(TextSize::from(6), TextSize::from(28))]) ); - let contents = r#"x = \ - 1"#; + let contents = r"x = \ + 1"; assert_eq!( noqa_mappings(contents), NoqaMapping::from_iter([TextRange::new(TextSize::from(0), TextSize::from(6))]) ); - let contents = r#"from foo import \ + let contents = r"from foo import \ bar as baz, \ - qux as quux"#; + qux as quux"; assert_eq!( noqa_mappings(contents), NoqaMapping::from_iter([TextRange::new(TextSize::from(0), TextSize::from(36))]) ); - let contents = r#" + let contents = r" # Foo from foo import \ bar as baz, \ @@ -450,7 +450,7 @@ from foo import \ x = \ 1 y = \ - 2"#; + 2"; assert_eq!( noqa_mappings(contents), NoqaMapping::from_iter([ diff --git a/crates/ruff/src/message/azure.rs b/crates/ruff/src/message/azure.rs index d5119faca0..f775fe27ab 100644 --- a/crates/ruff/src/message/azure.rs +++ b/crates/ruff/src/message/azure.rs @@ -51,7 +51,7 @@ mod tests { #[test] fn output() { - let mut emitter = AzureEmitter::default(); + let mut emitter = AzureEmitter; let content = capture_emitter_output(&mut emitter, &create_messages()); assert_snapshot!(content); diff --git a/crates/ruff/src/message/github.rs b/crates/ruff/src/message/github.rs index 23ddae5d67..97a28a4e97 100644 --- a/crates/ruff/src/message/github.rs +++ b/crates/ruff/src/message/github.rs @@ -66,7 +66,7 @@ mod tests { #[test] fn output() { - let mut emitter = GithubEmitter::default(); + let mut emitter = GithubEmitter; let content = capture_emitter_output(&mut emitter, &create_messages()); assert_snapshot!(content); diff --git a/crates/ruff/src/message/json.rs b/crates/ruff/src/message/json.rs index 835d6ec067..8a80635be9 100644 --- a/crates/ruff/src/message/json.rs +++ b/crates/ruff/src/message/json.rs @@ -108,7 +108,7 @@ mod tests { #[test] fn output() { - let mut emitter = JsonEmitter::default(); + let mut emitter = JsonEmitter; let content = capture_emitter_output(&mut emitter, &create_messages()); assert_snapshot!(content); diff --git a/crates/ruff/src/message/json_lines.rs b/crates/ruff/src/message/json_lines.rs index 931d7b3ade..360e7ec6a7 100644 --- a/crates/ruff/src/message/json_lines.rs +++ b/crates/ruff/src/message/json_lines.rs @@ -24,14 +24,14 @@ impl Emitter for JsonLinesEmitter { #[cfg(test)] mod tests { - use crate::message::json_lines::JsonLinesEmitter; use insta::assert_snapshot; + use crate::message::json_lines::JsonLinesEmitter; use crate::message::tests::{capture_emitter_output, create_messages}; #[test] fn output() { - let mut emitter = JsonLinesEmitter::default(); + let mut emitter = JsonLinesEmitter; let content = capture_emitter_output(&mut emitter, &create_messages()); assert_snapshot!(content); diff --git a/crates/ruff/src/message/junit.rs b/crates/ruff/src/message/junit.rs index f910b7e6ed..26d7161f2e 100644 --- a/crates/ruff/src/message/junit.rs +++ b/crates/ruff/src/message/junit.rs @@ -93,7 +93,7 @@ mod tests { #[test] fn output() { - let mut emitter = JunitEmitter::default(); + let mut emitter = JunitEmitter; let content = capture_emitter_output(&mut emitter, &create_messages()); assert_snapshot!(content); diff --git a/crates/ruff/src/message/pylint.rs b/crates/ruff/src/message/pylint.rs index edede90422..7453495bb9 100644 --- a/crates/ruff/src/message/pylint.rs +++ b/crates/ruff/src/message/pylint.rs @@ -49,7 +49,7 @@ mod tests { #[test] fn output() { - let mut emitter = PylintEmitter::default(); + let mut emitter = PylintEmitter; let content = capture_emitter_output(&mut emitter, &create_messages()); assert_snapshot!(content); diff --git a/crates/ruff/src/rules/flake8_bandit/rules/shell_injection.rs b/crates/ruff/src/rules/flake8_bandit/rules/shell_injection.rs index ae2e92211a..3617a56eb6 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/shell_injection.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/shell_injection.rs @@ -350,7 +350,7 @@ fn is_wildcard_command(expr: &Expr) -> bool { if let Expr::List(ast::ExprList { elts, .. }) = expr { let mut has_star = false; let mut has_command = false; - for elt in elts.iter() { + for elt in elts { if let Some(text) = string_literal(elt) { has_star |= text.contains('*'); has_command |= text.contains("chown") diff --git a/crates/ruff/src/rules/flake8_bandit/settings.rs b/crates/ruff/src/rules/flake8_bandit/settings.rs index d43bd48700..168feeec14 100644 --- a/crates/ruff/src/rules/flake8_bandit/settings.rs +++ b/crates/ruff/src/rules/flake8_bandit/settings.rs @@ -59,12 +59,7 @@ impl From for Settings { .hardcoded_tmp_directory .unwrap_or_else(default_tmp_dirs) .into_iter() - .chain( - options - .hardcoded_tmp_directory_extend - .unwrap_or_default() - .into_iter(), - ) + .chain(options.hardcoded_tmp_directory_extend.unwrap_or_default()) .collect(), check_typed_exception: options.check_typed_exception.unwrap_or(false), } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs b/crates/ruff/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs index 976010ee8c..1618f0f9b2 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs @@ -351,7 +351,7 @@ pub(crate) fn reuse_of_groupby_generator( return; } let mut finder = GroupNameFinder::new(group_name); - for stmt in body.iter() { + for stmt in body { finder.visit_stmt(stmt); } for expr in finder.exprs { diff --git a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs index 1017bea5ac..80260fb5f3 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs @@ -512,7 +512,7 @@ fn pad_expression(content: String, range: TextRange, checker: &Checker) -> Strin // If the expression is immediately preceded by an opening brace, then // we need to add a space before the expression. let prefix = checker.locator.up_to(range.start()); - let left_pad = matches!(prefix.chars().rev().next(), Some('{')); + let left_pad = matches!(prefix.chars().next_back(), Some('{')); // If the expression is immediately preceded by an opening brace, then // we need to add a space before the expression. diff --git a/crates/ruff/src/rules/flake8_django/rules/all_with_model_form.rs b/crates/ruff/src/rules/flake8_django/rules/all_with_model_form.rs index 3a3769bda8..63dc9551d6 100644 --- a/crates/ruff/src/rules/flake8_django/rules/all_with_model_form.rs +++ b/crates/ruff/src/rules/flake8_django/rules/all_with_model_form.rs @@ -58,18 +58,18 @@ pub(crate) fn all_with_model_form( { return None; } - for element in body.iter() { + for element in body { let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else { continue; }; if name != "Meta" { continue; } - for element in body.iter() { + for element in body { let Stmt::Assign(ast::StmtAssign { targets, value, .. }) = element else { continue; }; - for target in targets.iter() { + for target in targets { let Expr::Name(ast::ExprName { id, .. }) = target else { continue; }; diff --git a/crates/ruff/src/rules/flake8_django/rules/exclude_with_model_form.rs b/crates/ruff/src/rules/flake8_django/rules/exclude_with_model_form.rs index c920525a19..8079eb3099 100644 --- a/crates/ruff/src/rules/flake8_django/rules/exclude_with_model_form.rs +++ b/crates/ruff/src/rules/flake8_django/rules/exclude_with_model_form.rs @@ -56,18 +56,18 @@ pub(crate) fn exclude_with_model_form( { return None; } - for element in body.iter() { + for element in body { let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else { continue; }; if name != "Meta" { continue; } - for element in body.iter() { + for element in body { let Stmt::Assign(ast::StmtAssign { targets, .. }) = element else { continue; }; - for target in targets.iter() { + for target in targets { let Expr::Name(ast::ExprName { id, .. }) = target else { continue; }; 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 6c30cb52c1..6c0718f95a 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 @@ -81,7 +81,7 @@ fn has_dunder_method(body: &[Stmt]) -> bool { } fn is_non_abstract_model(bases: &[Expr], body: &[Stmt], semantic: &SemanticModel) -> bool { - for base in bases.iter() { + for base in bases { if is_model_abstract(body) { continue; } @@ -94,18 +94,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() { + for element in body { let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else { continue; }; if name != "Meta" { continue; } - for element in body.iter() { + for element in body { let Stmt::Assign(ast::StmtAssign { targets, value, .. }) = element else { continue; }; - for target in targets.iter() { + for target in targets { let Expr::Name(ast::ExprName { id, .. }) = target else { continue; }; 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 b74bf19a8c..2fc19f4f28 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 @@ -53,7 +53,7 @@ impl Violation for DjangoNullableModelStringField { /// DJ001 pub(crate) fn nullable_model_string_field(checker: &mut Checker, body: &[Stmt]) { - for statement in body.iter() { + for statement in body { let Stmt::Assign(ast::StmtAssign { value, .. }) = statement else { continue; }; @@ -87,7 +87,7 @@ fn is_nullable_field<'a>(checker: &'a Checker, value: &'a Expr) -> Option<&'a st let mut null_key = false; let mut blank_key = false; let mut unique_key = false; - for keyword in keywords.iter() { + for keyword in keywords { let Some(argument) = &keyword.arg else { continue; }; 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 d3734537a6..b46d825ca2 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 @@ -156,7 +156,7 @@ pub(crate) fn unordered_body_content_in_model( // 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() { + for element in body { let Some(element_type) = get_element_type(element, checker.semantic()) else { continue; }; diff --git a/crates/ruff/src/rules/flake8_gettext/settings.rs b/crates/ruff/src/rules/flake8_gettext/settings.rs index a16d61e15c..a845688dde 100644 --- a/crates/ruff/src/rules/flake8_gettext/settings.rs +++ b/crates/ruff/src/rules/flake8_gettext/settings.rs @@ -57,12 +57,7 @@ impl From for Settings { .function_names .unwrap_or_else(default_func_names) .into_iter() - .chain( - options - .extend_function_names - .unwrap_or_default() - .into_iter(), - ) + .chain(options.extend_function_names.unwrap_or_default()) .collect(), } } 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 db40775805..fa77834113 100644 --- a/crates/ruff/src/rules/flake8_quotes/rules/from_tokens.rs +++ b/crates/ruff/src/rules/flake8_quotes/rules/from_tokens.rs @@ -322,7 +322,7 @@ fn strings(locator: &Locator, sequence: &[TextRange], settings: &Settings) -> Ve string_contents.contains(good_single(quotes_settings.inline_quotes)) }); - for (range, trivia) in sequence.iter().zip(trivia.into_iter()) { + for (range, trivia) in sequence.iter().zip(trivia) { if trivia.is_multiline { // If our string is or contains a known good string, ignore it. if trivia diff --git a/crates/ruff/src/rules/flake8_todos/rules/todos.rs b/crates/ruff/src/rules/flake8_todos/rules/todos.rs index fc94be27eb..7bd52d9ba1 100644 --- a/crates/ruff/src/rules/flake8_todos/rules/todos.rs +++ b/crates/ruff/src/rules/flake8_todos/rules/todos.rs @@ -227,9 +227,9 @@ impl Violation for MissingSpaceAfterTodoColon { static ISSUE_LINK_REGEX_SET: Lazy = Lazy::new(|| { RegexSet::new([ - r#"^#\s*(http|https)://.*"#, // issue link - r#"^#\s*\d+$"#, // issue code - like "003" - r#"^#\s*[A-Z]{1,6}\-?\d+$"#, // issue code - like "TD003" + r"^#\s*(http|https)://.*", // issue link + r"^#\s*\d+$", // issue code - like "003" + r"^#\s*[A-Z]{1,6}\-?\d+$", // issue code - like "TD003" ]) .unwrap() }); diff --git a/crates/ruff/src/rules/flake8_type_checking/helpers.rs b/crates/ruff/src/rules/flake8_type_checking/helpers.rs index 48bda7481b..144e5eee12 100644 --- a/crates/ruff/src/rules/flake8_type_checking/helpers.rs +++ b/crates/ruff/src/rules/flake8_type_checking/helpers.rs @@ -38,7 +38,7 @@ pub(crate) fn runtime_evaluated( fn runtime_evaluated_base_class(base_classes: &[String], semantic: &SemanticModel) -> bool { if let ScopeKind::Class(ast::StmtClassDef { bases, .. }) = &semantic.scope().kind { - for base in bases.iter() { + for base in bases { if let Some(call_path) = semantic.resolve_call_path(base) { if base_classes .iter() @@ -54,7 +54,7 @@ fn runtime_evaluated_base_class(base_classes: &[String], semantic: &SemanticMode fn runtime_evaluated_decorators(decorators: &[String], semantic: &SemanticModel) -> bool { if let ScopeKind::Class(ast::StmtClassDef { decorator_list, .. }) = &semantic.scope().kind { - for decorator in decorator_list.iter() { + for decorator in decorator_list { if let Some(call_path) = semantic.resolve_call_path(map_callable(&decorator.expression)) { if decorators diff --git a/crates/ruff/src/rules/isort/order.rs b/crates/ruff/src/rules/isort/order.rs index 66509a797b..a2210ed43a 100644 --- a/crates/ruff/src/rules/isort/order.rs +++ b/crates/ruff/src/rules/isort/order.rs @@ -47,7 +47,7 @@ pub(crate) fn order_imports<'a>( ) .chain( // Include all star imports. - block.import_from_star.into_iter(), + block.import_from_star, ) .map( |( diff --git a/crates/ruff/src/rules/pycodestyle/rules/not_tests.rs b/crates/ruff/src/rules/pycodestyle/rules/not_tests.rs index af2808c9db..60ef31a5c4 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/not_tests.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/not_tests.rs @@ -93,7 +93,7 @@ pub(crate) fn not_tests( if !matches!(&ops[..], [CmpOp::In | CmpOp::Is]) { return; } - for op in ops.iter() { + for op in ops { match op { CmpOp::In => { if check_not_in { 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 29da844358..f02c6543d2 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 @@ -58,7 +58,7 @@ impl AlwaysAutofixableViolation for LRUCacheWithMaxsizeNone { /// UP033 pub(crate) fn lru_cache_with_maxsize_none(checker: &mut Checker, decorator_list: &[Decorator]) { - for decorator in decorator_list.iter() { + for decorator in decorator_list { let Expr::Call(ast::ExprCall { func, args, 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 1a42ab66dc..594025e287 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 @@ -56,7 +56,7 @@ impl AlwaysAutofixableViolation for LRUCacheWithoutParameters { /// UP011 pub(crate) fn lru_cache_without_parameters(checker: &mut Checker, decorator_list: &[Decorator]) { - for decorator in decorator_list.iter() { + for decorator in decorator_list { let Expr::Call(ast::ExprCall { func, args, diff --git a/crates/ruff/src/settings/configuration.rs b/crates/ruff/src/settings/configuration.rs index 653c328011..d385444d6e 100644 --- a/crates/ruff/src/settings/configuration.rs +++ b/crates/ruff/src/settings/configuration.rs @@ -258,7 +258,7 @@ impl Configuration { rule_selections: config .rule_selections .into_iter() - .chain(self.rule_selections.into_iter()) + .chain(self.rule_selections) .collect(), allowed_confusables: self.allowed_confusables.or(config.allowed_confusables), builtins: self.builtins.or(config.builtins), @@ -269,17 +269,17 @@ impl Configuration { extend_exclude: config .extend_exclude .into_iter() - .chain(self.extend_exclude.into_iter()) + .chain(self.extend_exclude) .collect(), extend_include: config .extend_include .into_iter() - .chain(self.extend_include.into_iter()) + .chain(self.extend_include) .collect(), extend_per_file_ignores: config .extend_per_file_ignores .into_iter() - .chain(self.extend_per_file_ignores.into_iter()) + .chain(self.extend_per_file_ignores) .collect(), external: self.external.or(config.external), fix: self.fix.or(config.fix), diff --git a/crates/ruff/src/settings/options_base.rs b/crates/ruff/src/settings/options_base.rs index ed325db917..b15f2b7fd0 100644 --- a/crates/ruff/src/settings/options_base.rs +++ b/crates/ruff/src/settings/options_base.rs @@ -153,7 +153,7 @@ impl IntoIterator for OptionGroup { impl Display for OptionGroup { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - for (name, _) in self.iter() { + for (name, _) in self { writeln!(f, "{name}")?; } diff --git a/crates/ruff_cli/src/printer.rs b/crates/ruff_cli/src/printer.rs index 2774706703..0c8070e6e4 100644 --- a/crates/ruff_cli/src/printer.rs +++ b/crates/ruff_cli/src/printer.rs @@ -181,13 +181,13 @@ impl Printer { match self.format { SerializationFormat::Json => { - JsonEmitter::default().emit(writer, &diagnostics.messages, &context)?; + JsonEmitter.emit(writer, &diagnostics.messages, &context)?; } SerializationFormat::JsonLines => { - JsonLinesEmitter::default().emit(writer, &diagnostics.messages, &context)?; + JsonLinesEmitter.emit(writer, &diagnostics.messages, &context)?; } SerializationFormat::Junit => { - JunitEmitter::default().emit(writer, &diagnostics.messages, &context)?; + JunitEmitter.emit(writer, &diagnostics.messages, &context)?; } SerializationFormat::Text => { TextEmitter::default() @@ -222,16 +222,16 @@ impl Printer { self.write_summary_text(writer, diagnostics)?; } SerializationFormat::Github => { - GithubEmitter::default().emit(writer, &diagnostics.messages, &context)?; + GithubEmitter.emit(writer, &diagnostics.messages, &context)?; } SerializationFormat::Gitlab => { GitlabEmitter::default().emit(writer, &diagnostics.messages, &context)?; } SerializationFormat::Pylint => { - PylintEmitter::default().emit(writer, &diagnostics.messages, &context)?; + PylintEmitter.emit(writer, &diagnostics.messages, &context)?; } SerializationFormat::Azure => { - AzureEmitter::default().emit(writer, &diagnostics.messages, &context)?; + AzureEmitter.emit(writer, &diagnostics.messages, &context)?; } } diff --git a/crates/ruff_diagnostics/src/fix.rs b/crates/ruff_diagnostics/src/fix.rs index ae54282d04..c96b428c8d 100644 --- a/crates/ruff_diagnostics/src/fix.rs +++ b/crates/ruff_diagnostics/src/fix.rs @@ -66,7 +66,7 @@ impl Fix { )] pub fn unspecified_edits(edit: Edit, rest: impl IntoIterator) -> Self { Self { - edits: std::iter::once(edit).chain(rest.into_iter()).collect(), + edits: std::iter::once(edit).chain(rest).collect(), applicability: Applicability::Unspecified, isolation_level: IsolationLevel::default(), } @@ -84,7 +84,7 @@ impl Fix { /// Create a new [`Fix`] with [automatic applicability](Applicability::Automatic) from multiple [`Edit`] elements. pub fn automatic_edits(edit: Edit, rest: impl IntoIterator) -> Self { Self { - edits: std::iter::once(edit).chain(rest.into_iter()).collect(), + edits: std::iter::once(edit).chain(rest).collect(), applicability: Applicability::Automatic, isolation_level: IsolationLevel::default(), } @@ -102,7 +102,7 @@ impl Fix { /// Create a new [`Fix`] with [suggested applicability](Applicability::Suggested) from multiple [`Edit`] elements. pub fn suggested_edits(edit: Edit, rest: impl IntoIterator) -> Self { Self { - edits: std::iter::once(edit).chain(rest.into_iter()).collect(), + edits: std::iter::once(edit).chain(rest).collect(), applicability: Applicability::Suggested, isolation_level: IsolationLevel::default(), } @@ -120,7 +120,7 @@ impl Fix { /// Create a new [`Fix`] with [manual applicability](Applicability::Manual) from multiple [`Edit`] elements. pub fn manual_edits(edit: Edit, rest: impl IntoIterator) -> Self { Self { - edits: std::iter::once(edit).chain(rest.into_iter()).collect(), + edits: std::iter::once(edit).chain(rest).collect(), applicability: Applicability::Manual, isolation_level: IsolationLevel::default(), } diff --git a/crates/ruff_formatter/src/arguments.rs b/crates/ruff_formatter/src/arguments.rs index d2fd95eecd..01ee8f91e8 100644 --- a/crates/ruff_formatter/src/arguments.rs +++ b/crates/ruff_formatter/src/arguments.rs @@ -96,7 +96,7 @@ impl Copy for Arguments<'_, Context> {} impl Clone for Arguments<'_, Context> { fn clone(&self) -> Self { - Self(self.0) + *self } } diff --git a/crates/ruff_macros/src/config.rs b/crates/ruff_macros/src/config.rs index 38dcaef14b..bfbfc8227e 100644 --- a/crates/ruff_macros/src/config.rs +++ b/crates/ruff_macros/src/config.rs @@ -19,7 +19,7 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result { let mut output = vec![]; - for field in fields.named.iter() { + for field in &fields.named { let docs: Vec<&Attribute> = field .attrs .iter() diff --git a/crates/ruff_python_ast/src/source_code/indexer.rs b/crates/ruff_python_ast/src/source_code/indexer.rs index 3d10678a50..23227965a5 100644 --- a/crates/ruff_python_ast/src/source_code/indexer.rs +++ b/crates/ruff_python_ast/src/source_code/indexer.rs @@ -201,7 +201,7 @@ if True: ] ); - let contents = r#" + let contents = r" x = 1; import sys import os @@ -215,7 +215,7 @@ if True: x = 1; \ import os -"# +" .trim(); let lxr: Vec = lexer::lex(contents, Mode::Module).collect(); let indexer = Indexer::from_tokens(lxr.as_slice(), &Locator::new(contents)); diff --git a/crates/ruff_python_ast/src/types.rs b/crates/ruff_python_ast/src/types.rs index 7596cfae44..baa2839e1f 100644 --- a/crates/ruff_python_ast/src/types.rs +++ b/crates/ruff_python_ast/src/types.rs @@ -28,7 +28,7 @@ impl<'a, T> AsRef for RefEquality<'a, T> { impl<'a, T> Clone for RefEquality<'a, T> { fn clone(&self) -> Self { - Self(self.0) + *self } } diff --git a/crates/ruff_python_formatter/generate.py b/crates/ruff_python_formatter/generate.py index bcbf59871a..ad94528941 100644 --- a/crates/ruff_python_formatter/generate.py +++ b/crates/ruff_python_formatter/generate.py @@ -136,7 +136,7 @@ for node in nodes: fn format(&self) -> Self::Format<'_> {{ FormatRefWithRule::new( self, - crate::{groups[group_for_node(node)]}::{to_camel_case(node)}::Format{node}::default(), + crate::{groups[group_for_node(node)]}::{to_camel_case(node)}::Format{node}, ) }} }} @@ -149,7 +149,7 @@ for node in nodes: fn into_format(self) -> Self::Format {{ FormatOwnedWithRule::new( self, - crate::{groups[group_for_node(node)]}::{to_camel_case(node)}::Format{node}::default(), + crate::{groups[group_for_node(node)]}::{to_camel_case(node)}::Format{node}, ) }} }} diff --git a/crates/ruff_python_formatter/src/comments/mod.rs b/crates/ruff_python_formatter/src/comments/mod.rs index 847cd4b8a1..63c148fff8 100644 --- a/crates/ruff_python_formatter/src/comments/mod.rs +++ b/crates/ruff_python_formatter/src/comments/mod.rs @@ -436,7 +436,7 @@ mod tests { let comment_ranges = comment_ranges.finish(); - let parsed = parse_tokens(tokens.into_iter(), Mode::Module, "test.py") + let parsed = parse_tokens(tokens, Mode::Module, "test.py") .expect("Expect source to be valid Python"); CommentsTestCase { diff --git a/crates/ruff_python_formatter/src/generated.rs b/crates/ruff_python_formatter/src/generated.rs index d9bf041c64..24bb09cd3b 100644 --- a/crates/ruff_python_formatter/src/generated.rs +++ b/crates/ruff_python_formatter/src/generated.rs @@ -1,4 +1,4 @@ -//! This is a generated file. Don't modify it by hand! Run `scripts/generate.py` to re-generate the file. +//! This is a generated file. Don't modify it by hand! Run `crates/ruff_python_formatter/generate.py` to re-generate the file. use crate::context::PyFormatContext; use crate::{AsFormat, FormatNodeRule, IntoFormat}; use ruff_formatter::formatter::Formatter; @@ -25,7 +25,7 @@ impl<'ast> AsFormat> for ast::ModModule { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::module::mod_module::FormatModModule::default()) + FormatRefWithRule::new(self, crate::module::mod_module::FormatModModule) } } impl<'ast> IntoFormat> for ast::ModModule { @@ -35,7 +35,7 @@ impl<'ast> IntoFormat> for ast::ModModule { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::module::mod_module::FormatModModule::default()) + FormatOwnedWithRule::new(self, crate::module::mod_module::FormatModModule) } } @@ -59,10 +59,7 @@ impl<'ast> AsFormat> for ast::ModInteractive { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::module::mod_interactive::FormatModInteractive::default(), - ) + FormatRefWithRule::new(self, crate::module::mod_interactive::FormatModInteractive) } } impl<'ast> IntoFormat> for ast::ModInteractive { @@ -72,10 +69,7 @@ impl<'ast> IntoFormat> for ast::ModInteractive { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::module::mod_interactive::FormatModInteractive::default(), - ) + FormatOwnedWithRule::new(self, crate::module::mod_interactive::FormatModInteractive) } } @@ -99,10 +93,7 @@ impl<'ast> AsFormat> for ast::ModExpression { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::module::mod_expression::FormatModExpression::default(), - ) + FormatRefWithRule::new(self, crate::module::mod_expression::FormatModExpression) } } impl<'ast> IntoFormat> for ast::ModExpression { @@ -112,10 +103,7 @@ impl<'ast> IntoFormat> for ast::ModExpression { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::module::mod_expression::FormatModExpression::default(), - ) + FormatOwnedWithRule::new(self, crate::module::mod_expression::FormatModExpression) } } @@ -141,7 +129,7 @@ impl<'ast> AsFormat> for ast::ModFunctionType { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::module::mod_function_type::FormatModFunctionType::default(), + crate::module::mod_function_type::FormatModFunctionType, ) } } @@ -154,7 +142,7 @@ impl<'ast> IntoFormat> for ast::ModFunctionType { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::module::mod_function_type::FormatModFunctionType::default(), + crate::module::mod_function_type::FormatModFunctionType, ) } } @@ -181,7 +169,7 @@ impl<'ast> AsFormat> for ast::StmtFunctionDef { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::statement::stmt_function_def::FormatStmtFunctionDef::default(), + crate::statement::stmt_function_def::FormatStmtFunctionDef, ) } } @@ -194,7 +182,7 @@ impl<'ast> IntoFormat> for ast::StmtFunctionDef { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::statement::stmt_function_def::FormatStmtFunctionDef::default(), + crate::statement::stmt_function_def::FormatStmtFunctionDef, ) } } @@ -221,7 +209,7 @@ impl<'ast> AsFormat> for ast::StmtAsyncFunctionDef { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::statement::stmt_async_function_def::FormatStmtAsyncFunctionDef::default(), + crate::statement::stmt_async_function_def::FormatStmtAsyncFunctionDef, ) } } @@ -234,7 +222,7 @@ impl<'ast> IntoFormat> for ast::StmtAsyncFunctionDef { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::statement::stmt_async_function_def::FormatStmtAsyncFunctionDef::default(), + crate::statement::stmt_async_function_def::FormatStmtAsyncFunctionDef, ) } } @@ -259,10 +247,7 @@ impl<'ast> AsFormat> for ast::StmtClassDef { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_class_def::FormatStmtClassDef::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_class_def::FormatStmtClassDef) } } impl<'ast> IntoFormat> for ast::StmtClassDef { @@ -272,10 +257,7 @@ impl<'ast> IntoFormat> for ast::StmtClassDef { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_class_def::FormatStmtClassDef::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_class_def::FormatStmtClassDef) } } @@ -299,10 +281,7 @@ impl<'ast> AsFormat> for ast::StmtReturn { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_return::FormatStmtReturn::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_return::FormatStmtReturn) } } impl<'ast> IntoFormat> for ast::StmtReturn { @@ -312,10 +291,7 @@ impl<'ast> IntoFormat> for ast::StmtReturn { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_return::FormatStmtReturn::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_return::FormatStmtReturn) } } @@ -339,10 +315,7 @@ impl<'ast> AsFormat> for ast::StmtDelete { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_delete::FormatStmtDelete::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_delete::FormatStmtDelete) } } impl<'ast> IntoFormat> for ast::StmtDelete { @@ -352,10 +325,7 @@ impl<'ast> IntoFormat> for ast::StmtDelete { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_delete::FormatStmtDelete::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_delete::FormatStmtDelete) } } @@ -379,10 +349,7 @@ impl<'ast> AsFormat> for ast::StmtAssign { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_assign::FormatStmtAssign::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_assign::FormatStmtAssign) } } impl<'ast> IntoFormat> for ast::StmtAssign { @@ -392,10 +359,7 @@ impl<'ast> IntoFormat> for ast::StmtAssign { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_assign::FormatStmtAssign::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_assign::FormatStmtAssign) } } @@ -419,10 +383,7 @@ impl<'ast> AsFormat> for ast::StmtAugAssign { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_aug_assign::FormatStmtAugAssign::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_aug_assign::FormatStmtAugAssign) } } impl<'ast> IntoFormat> for ast::StmtAugAssign { @@ -432,10 +393,7 @@ impl<'ast> IntoFormat> for ast::StmtAugAssign { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_aug_assign::FormatStmtAugAssign::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_aug_assign::FormatStmtAugAssign) } } @@ -459,10 +417,7 @@ impl<'ast> AsFormat> for ast::StmtAnnAssign { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_ann_assign::FormatStmtAnnAssign::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_ann_assign::FormatStmtAnnAssign) } } impl<'ast> IntoFormat> for ast::StmtAnnAssign { @@ -472,10 +427,7 @@ impl<'ast> IntoFormat> for ast::StmtAnnAssign { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_ann_assign::FormatStmtAnnAssign::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_ann_assign::FormatStmtAnnAssign) } } @@ -493,7 +445,7 @@ impl<'ast> AsFormat> for ast::StmtFor { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::statement::stmt_for::FormatStmtFor::default()) + FormatRefWithRule::new(self, crate::statement::stmt_for::FormatStmtFor) } } impl<'ast> IntoFormat> for ast::StmtFor { @@ -503,7 +455,7 @@ impl<'ast> IntoFormat> for ast::StmtFor { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::statement::stmt_for::FormatStmtFor::default()) + FormatOwnedWithRule::new(self, crate::statement::stmt_for::FormatStmtFor) } } @@ -527,10 +479,7 @@ impl<'ast> AsFormat> for ast::StmtAsyncFor { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_async_for::FormatStmtAsyncFor::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_async_for::FormatStmtAsyncFor) } } impl<'ast> IntoFormat> for ast::StmtAsyncFor { @@ -540,10 +489,7 @@ impl<'ast> IntoFormat> for ast::StmtAsyncFor { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_async_for::FormatStmtAsyncFor::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_async_for::FormatStmtAsyncFor) } } @@ -567,10 +513,7 @@ impl<'ast> AsFormat> for ast::StmtWhile { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_while::FormatStmtWhile::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_while::FormatStmtWhile) } } impl<'ast> IntoFormat> for ast::StmtWhile { @@ -580,10 +523,7 @@ impl<'ast> IntoFormat> for ast::StmtWhile { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_while::FormatStmtWhile::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_while::FormatStmtWhile) } } @@ -601,7 +541,7 @@ impl<'ast> AsFormat> for ast::StmtIf { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::statement::stmt_if::FormatStmtIf::default()) + FormatRefWithRule::new(self, crate::statement::stmt_if::FormatStmtIf) } } impl<'ast> IntoFormat> for ast::StmtIf { @@ -611,7 +551,7 @@ impl<'ast> IntoFormat> for ast::StmtIf { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::statement::stmt_if::FormatStmtIf::default()) + FormatOwnedWithRule::new(self, crate::statement::stmt_if::FormatStmtIf) } } @@ -635,7 +575,7 @@ impl<'ast> AsFormat> for ast::StmtWith { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::statement::stmt_with::FormatStmtWith::default()) + FormatRefWithRule::new(self, crate::statement::stmt_with::FormatStmtWith) } } impl<'ast> IntoFormat> for ast::StmtWith { @@ -645,7 +585,7 @@ impl<'ast> IntoFormat> for ast::StmtWith { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::statement::stmt_with::FormatStmtWith::default()) + FormatOwnedWithRule::new(self, crate::statement::stmt_with::FormatStmtWith) } } @@ -669,10 +609,7 @@ impl<'ast> AsFormat> for ast::StmtAsyncWith { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_async_with::FormatStmtAsyncWith::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_async_with::FormatStmtAsyncWith) } } impl<'ast> IntoFormat> for ast::StmtAsyncWith { @@ -682,10 +619,7 @@ impl<'ast> IntoFormat> for ast::StmtAsyncWith { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_async_with::FormatStmtAsyncWith::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_async_with::FormatStmtAsyncWith) } } @@ -709,10 +643,7 @@ impl<'ast> AsFormat> for ast::StmtMatch { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_match::FormatStmtMatch::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_match::FormatStmtMatch) } } impl<'ast> IntoFormat> for ast::StmtMatch { @@ -722,10 +653,7 @@ impl<'ast> IntoFormat> for ast::StmtMatch { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_match::FormatStmtMatch::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_match::FormatStmtMatch) } } @@ -749,10 +677,7 @@ impl<'ast> AsFormat> for ast::StmtRaise { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_raise::FormatStmtRaise::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_raise::FormatStmtRaise) } } impl<'ast> IntoFormat> for ast::StmtRaise { @@ -762,10 +687,7 @@ impl<'ast> IntoFormat> for ast::StmtRaise { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_raise::FormatStmtRaise::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_raise::FormatStmtRaise) } } @@ -783,7 +705,7 @@ impl<'ast> AsFormat> for ast::StmtTry { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::statement::stmt_try::FormatStmtTry::default()) + FormatRefWithRule::new(self, crate::statement::stmt_try::FormatStmtTry) } } impl<'ast> IntoFormat> for ast::StmtTry { @@ -793,7 +715,7 @@ impl<'ast> IntoFormat> for ast::StmtTry { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::statement::stmt_try::FormatStmtTry::default()) + FormatOwnedWithRule::new(self, crate::statement::stmt_try::FormatStmtTry) } } @@ -817,10 +739,7 @@ impl<'ast> AsFormat> for ast::StmtTryStar { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_try_star::FormatStmtTryStar::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_try_star::FormatStmtTryStar) } } impl<'ast> IntoFormat> for ast::StmtTryStar { @@ -830,10 +749,7 @@ impl<'ast> IntoFormat> for ast::StmtTryStar { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_try_star::FormatStmtTryStar::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_try_star::FormatStmtTryStar) } } @@ -857,10 +773,7 @@ impl<'ast> AsFormat> for ast::StmtAssert { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_assert::FormatStmtAssert::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_assert::FormatStmtAssert) } } impl<'ast> IntoFormat> for ast::StmtAssert { @@ -870,10 +783,7 @@ impl<'ast> IntoFormat> for ast::StmtAssert { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_assert::FormatStmtAssert::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_assert::FormatStmtAssert) } } @@ -897,10 +807,7 @@ impl<'ast> AsFormat> for ast::StmtImport { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_import::FormatStmtImport::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_import::FormatStmtImport) } } impl<'ast> IntoFormat> for ast::StmtImport { @@ -910,10 +817,7 @@ impl<'ast> IntoFormat> for ast::StmtImport { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_import::FormatStmtImport::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_import::FormatStmtImport) } } @@ -939,7 +843,7 @@ impl<'ast> AsFormat> for ast::StmtImportFrom { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::statement::stmt_import_from::FormatStmtImportFrom::default(), + crate::statement::stmt_import_from::FormatStmtImportFrom, ) } } @@ -952,7 +856,7 @@ impl<'ast> IntoFormat> for ast::StmtImportFrom { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::statement::stmt_import_from::FormatStmtImportFrom::default(), + crate::statement::stmt_import_from::FormatStmtImportFrom, ) } } @@ -977,10 +881,7 @@ impl<'ast> AsFormat> for ast::StmtGlobal { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_global::FormatStmtGlobal::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_global::FormatStmtGlobal) } } impl<'ast> IntoFormat> for ast::StmtGlobal { @@ -990,10 +891,7 @@ impl<'ast> IntoFormat> for ast::StmtGlobal { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_global::FormatStmtGlobal::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_global::FormatStmtGlobal) } } @@ -1017,10 +915,7 @@ impl<'ast> AsFormat> for ast::StmtNonlocal { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_nonlocal::FormatStmtNonlocal::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_nonlocal::FormatStmtNonlocal) } } impl<'ast> IntoFormat> for ast::StmtNonlocal { @@ -1030,10 +925,7 @@ impl<'ast> IntoFormat> for ast::StmtNonlocal { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_nonlocal::FormatStmtNonlocal::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_nonlocal::FormatStmtNonlocal) } } @@ -1057,7 +949,7 @@ impl<'ast> AsFormat> for ast::StmtExpr { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::statement::stmt_expr::FormatStmtExpr::default()) + FormatRefWithRule::new(self, crate::statement::stmt_expr::FormatStmtExpr) } } impl<'ast> IntoFormat> for ast::StmtExpr { @@ -1067,7 +959,7 @@ impl<'ast> IntoFormat> for ast::StmtExpr { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::statement::stmt_expr::FormatStmtExpr::default()) + FormatOwnedWithRule::new(self, crate::statement::stmt_expr::FormatStmtExpr) } } @@ -1091,7 +983,7 @@ impl<'ast> AsFormat> for ast::StmtPass { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::statement::stmt_pass::FormatStmtPass::default()) + FormatRefWithRule::new(self, crate::statement::stmt_pass::FormatStmtPass) } } impl<'ast> IntoFormat> for ast::StmtPass { @@ -1101,7 +993,7 @@ impl<'ast> IntoFormat> for ast::StmtPass { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::statement::stmt_pass::FormatStmtPass::default()) + FormatOwnedWithRule::new(self, crate::statement::stmt_pass::FormatStmtPass) } } @@ -1125,10 +1017,7 @@ impl<'ast> AsFormat> for ast::StmtBreak { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_break::FormatStmtBreak::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_break::FormatStmtBreak) } } impl<'ast> IntoFormat> for ast::StmtBreak { @@ -1138,10 +1027,7 @@ impl<'ast> IntoFormat> for ast::StmtBreak { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_break::FormatStmtBreak::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_break::FormatStmtBreak) } } @@ -1165,10 +1051,7 @@ impl<'ast> AsFormat> for ast::StmtContinue { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_continue::FormatStmtContinue::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_continue::FormatStmtContinue) } } impl<'ast> IntoFormat> for ast::StmtContinue { @@ -1178,10 +1061,7 @@ impl<'ast> IntoFormat> for ast::StmtContinue { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_continue::FormatStmtContinue::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_continue::FormatStmtContinue) } } @@ -1247,7 +1127,7 @@ impl<'ast> AsFormat> for ast::ExprNamedExpr { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::expression::expr_named_expr::FormatExprNamedExpr::default(), + crate::expression::expr_named_expr::FormatExprNamedExpr, ) } } @@ -1260,7 +1140,7 @@ impl<'ast> IntoFormat> for ast::ExprNamedExpr { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::expression::expr_named_expr::FormatExprNamedExpr::default(), + crate::expression::expr_named_expr::FormatExprNamedExpr, ) } } @@ -1325,10 +1205,7 @@ impl<'ast> AsFormat> for ast::ExprUnaryOp { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_unary_op::FormatExprUnaryOp::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_unary_op::FormatExprUnaryOp) } } impl<'ast> IntoFormat> for ast::ExprUnaryOp { @@ -1338,10 +1215,7 @@ impl<'ast> IntoFormat> for ast::ExprUnaryOp { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_unary_op::FormatExprUnaryOp::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_unary_op::FormatExprUnaryOp) } } @@ -1365,10 +1239,7 @@ impl<'ast> AsFormat> for ast::ExprLambda { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_lambda::FormatExprLambda::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_lambda::FormatExprLambda) } } impl<'ast> IntoFormat> for ast::ExprLambda { @@ -1378,10 +1249,7 @@ impl<'ast> IntoFormat> for ast::ExprLambda { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_lambda::FormatExprLambda::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_lambda::FormatExprLambda) } } @@ -1405,10 +1273,7 @@ impl<'ast> AsFormat> for ast::ExprIfExp { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_if_exp::FormatExprIfExp::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_if_exp::FormatExprIfExp) } } impl<'ast> IntoFormat> for ast::ExprIfExp { @@ -1418,10 +1283,7 @@ impl<'ast> IntoFormat> for ast::ExprIfExp { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_if_exp::FormatExprIfExp::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_if_exp::FormatExprIfExp) } } @@ -1445,10 +1307,7 @@ impl<'ast> AsFormat> for ast::ExprDict { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_dict::FormatExprDict::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_dict::FormatExprDict) } } impl<'ast> IntoFormat> for ast::ExprDict { @@ -1458,10 +1317,7 @@ impl<'ast> IntoFormat> for ast::ExprDict { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_dict::FormatExprDict::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_dict::FormatExprDict) } } @@ -1479,7 +1335,7 @@ impl<'ast> AsFormat> for ast::ExprSet { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::expression::expr_set::FormatExprSet::default()) + FormatRefWithRule::new(self, crate::expression::expr_set::FormatExprSet) } } impl<'ast> IntoFormat> for ast::ExprSet { @@ -1489,7 +1345,7 @@ impl<'ast> IntoFormat> for ast::ExprSet { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::expression::expr_set::FormatExprSet::default()) + FormatOwnedWithRule::new(self, crate::expression::expr_set::FormatExprSet) } } @@ -1513,10 +1369,7 @@ impl<'ast> AsFormat> for ast::ExprListComp { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_list_comp::FormatExprListComp::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_list_comp::FormatExprListComp) } } impl<'ast> IntoFormat> for ast::ExprListComp { @@ -1526,10 +1379,7 @@ impl<'ast> IntoFormat> for ast::ExprListComp { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_list_comp::FormatExprListComp::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_list_comp::FormatExprListComp) } } @@ -1553,10 +1403,7 @@ impl<'ast> AsFormat> for ast::ExprSetComp { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_set_comp::FormatExprSetComp::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_set_comp::FormatExprSetComp) } } impl<'ast> IntoFormat> for ast::ExprSetComp { @@ -1566,10 +1413,7 @@ impl<'ast> IntoFormat> for ast::ExprSetComp { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_set_comp::FormatExprSetComp::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_set_comp::FormatExprSetComp) } } @@ -1593,10 +1437,7 @@ impl<'ast> AsFormat> for ast::ExprDictComp { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_dict_comp::FormatExprDictComp::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_dict_comp::FormatExprDictComp) } } impl<'ast> IntoFormat> for ast::ExprDictComp { @@ -1606,10 +1447,7 @@ impl<'ast> IntoFormat> for ast::ExprDictComp { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_dict_comp::FormatExprDictComp::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_dict_comp::FormatExprDictComp) } } @@ -1635,7 +1473,7 @@ impl<'ast> AsFormat> for ast::ExprGeneratorExp { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::expression::expr_generator_exp::FormatExprGeneratorExp::default(), + crate::expression::expr_generator_exp::FormatExprGeneratorExp, ) } } @@ -1648,7 +1486,7 @@ impl<'ast> IntoFormat> for ast::ExprGeneratorExp { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::expression::expr_generator_exp::FormatExprGeneratorExp::default(), + crate::expression::expr_generator_exp::FormatExprGeneratorExp, ) } } @@ -1673,10 +1511,7 @@ impl<'ast> AsFormat> for ast::ExprAwait { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_await::FormatExprAwait::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_await::FormatExprAwait) } } impl<'ast> IntoFormat> for ast::ExprAwait { @@ -1686,10 +1521,7 @@ impl<'ast> IntoFormat> for ast::ExprAwait { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_await::FormatExprAwait::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_await::FormatExprAwait) } } @@ -1713,10 +1545,7 @@ impl<'ast> AsFormat> for ast::ExprYield { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_yield::FormatExprYield::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_yield::FormatExprYield) } } impl<'ast> IntoFormat> for ast::ExprYield { @@ -1726,10 +1555,7 @@ impl<'ast> IntoFormat> for ast::ExprYield { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_yield::FormatExprYield::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_yield::FormatExprYield) } } @@ -1755,7 +1581,7 @@ impl<'ast> AsFormat> for ast::ExprYieldFrom { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::expression::expr_yield_from::FormatExprYieldFrom::default(), + crate::expression::expr_yield_from::FormatExprYieldFrom, ) } } @@ -1768,7 +1594,7 @@ impl<'ast> IntoFormat> for ast::ExprYieldFrom { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::expression::expr_yield_from::FormatExprYieldFrom::default(), + crate::expression::expr_yield_from::FormatExprYieldFrom, ) } } @@ -1833,10 +1659,7 @@ impl<'ast> AsFormat> for ast::ExprCall { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_call::FormatExprCall::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_call::FormatExprCall) } } impl<'ast> IntoFormat> for ast::ExprCall { @@ -1846,10 +1669,7 @@ impl<'ast> IntoFormat> for ast::ExprCall { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_call::FormatExprCall::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_call::FormatExprCall) } } @@ -1875,7 +1695,7 @@ impl<'ast> AsFormat> for ast::ExprFormattedValue { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::expression::expr_formatted_value::FormatExprFormattedValue::default(), + crate::expression::expr_formatted_value::FormatExprFormattedValue, ) } } @@ -1888,7 +1708,7 @@ impl<'ast> IntoFormat> for ast::ExprFormattedValue { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::expression::expr_formatted_value::FormatExprFormattedValue::default(), + crate::expression::expr_formatted_value::FormatExprFormattedValue, ) } } @@ -1915,7 +1735,7 @@ impl<'ast> AsFormat> for ast::ExprJoinedStr { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::expression::expr_joined_str::FormatExprJoinedStr::default(), + crate::expression::expr_joined_str::FormatExprJoinedStr, ) } } @@ -1928,7 +1748,7 @@ impl<'ast> IntoFormat> for ast::ExprJoinedStr { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::expression::expr_joined_str::FormatExprJoinedStr::default(), + crate::expression::expr_joined_str::FormatExprJoinedStr, ) } } @@ -1993,10 +1813,7 @@ impl<'ast> AsFormat> for ast::ExprAttribute { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_attribute::FormatExprAttribute::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_attribute::FormatExprAttribute) } } impl<'ast> IntoFormat> for ast::ExprAttribute { @@ -2006,10 +1823,7 @@ impl<'ast> IntoFormat> for ast::ExprAttribute { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_attribute::FormatExprAttribute::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_attribute::FormatExprAttribute) } } @@ -2033,10 +1847,7 @@ impl<'ast> AsFormat> for ast::ExprSubscript { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_subscript::FormatExprSubscript::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_subscript::FormatExprSubscript) } } impl<'ast> IntoFormat> for ast::ExprSubscript { @@ -2046,10 +1857,7 @@ impl<'ast> IntoFormat> for ast::ExprSubscript { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_subscript::FormatExprSubscript::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_subscript::FormatExprSubscript) } } @@ -2073,10 +1881,7 @@ impl<'ast> AsFormat> for ast::ExprStarred { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_starred::FormatExprStarred::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_starred::FormatExprStarred) } } impl<'ast> IntoFormat> for ast::ExprStarred { @@ -2086,10 +1891,7 @@ impl<'ast> IntoFormat> for ast::ExprStarred { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_starred::FormatExprStarred::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_starred::FormatExprStarred) } } @@ -2113,10 +1915,7 @@ impl<'ast> AsFormat> for ast::ExprName { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_name::FormatExprName::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_name::FormatExprName) } } impl<'ast> IntoFormat> for ast::ExprName { @@ -2126,10 +1925,7 @@ impl<'ast> IntoFormat> for ast::ExprName { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_name::FormatExprName::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_name::FormatExprName) } } @@ -2153,10 +1949,7 @@ impl<'ast> AsFormat> for ast::ExprList { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_list::FormatExprList::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_list::FormatExprList) } } impl<'ast> IntoFormat> for ast::ExprList { @@ -2166,10 +1959,7 @@ impl<'ast> IntoFormat> for ast::ExprList { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_list::FormatExprList::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_list::FormatExprList) } } @@ -2233,10 +2023,7 @@ impl<'ast> AsFormat> for ast::ExprSlice { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_slice::FormatExprSlice::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_slice::FormatExprSlice) } } impl<'ast> IntoFormat> for ast::ExprSlice { @@ -2246,10 +2033,7 @@ impl<'ast> IntoFormat> for ast::ExprSlice { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_slice::FormatExprSlice::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_slice::FormatExprSlice) } } @@ -2317,7 +2101,7 @@ impl<'ast> AsFormat> for ast::PatternMatchValue { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::pattern::pattern_match_value::FormatPatternMatchValue::default(), + crate::pattern::pattern_match_value::FormatPatternMatchValue, ) } } @@ -2330,7 +2114,7 @@ impl<'ast> IntoFormat> for ast::PatternMatchValue { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::pattern::pattern_match_value::FormatPatternMatchValue::default(), + crate::pattern::pattern_match_value::FormatPatternMatchValue, ) } } @@ -2357,7 +2141,7 @@ impl<'ast> AsFormat> for ast::PatternMatchSingleton { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::pattern::pattern_match_singleton::FormatPatternMatchSingleton::default(), + crate::pattern::pattern_match_singleton::FormatPatternMatchSingleton, ) } } @@ -2370,7 +2154,7 @@ impl<'ast> IntoFormat> for ast::PatternMatchSingleton { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::pattern::pattern_match_singleton::FormatPatternMatchSingleton::default(), + crate::pattern::pattern_match_singleton::FormatPatternMatchSingleton, ) } } @@ -2397,7 +2181,7 @@ impl<'ast> AsFormat> for ast::PatternMatchSequence { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::pattern::pattern_match_sequence::FormatPatternMatchSequence::default(), + crate::pattern::pattern_match_sequence::FormatPatternMatchSequence, ) } } @@ -2410,7 +2194,7 @@ impl<'ast> IntoFormat> for ast::PatternMatchSequence { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::pattern::pattern_match_sequence::FormatPatternMatchSequence::default(), + crate::pattern::pattern_match_sequence::FormatPatternMatchSequence, ) } } @@ -2437,7 +2221,7 @@ impl<'ast> AsFormat> for ast::PatternMatchMapping { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::pattern::pattern_match_mapping::FormatPatternMatchMapping::default(), + crate::pattern::pattern_match_mapping::FormatPatternMatchMapping, ) } } @@ -2450,7 +2234,7 @@ impl<'ast> IntoFormat> for ast::PatternMatchMapping { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::pattern::pattern_match_mapping::FormatPatternMatchMapping::default(), + crate::pattern::pattern_match_mapping::FormatPatternMatchMapping, ) } } @@ -2477,7 +2261,7 @@ impl<'ast> AsFormat> for ast::PatternMatchClass { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::pattern::pattern_match_class::FormatPatternMatchClass::default(), + crate::pattern::pattern_match_class::FormatPatternMatchClass, ) } } @@ -2490,7 +2274,7 @@ impl<'ast> IntoFormat> for ast::PatternMatchClass { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::pattern::pattern_match_class::FormatPatternMatchClass::default(), + crate::pattern::pattern_match_class::FormatPatternMatchClass, ) } } @@ -2517,7 +2301,7 @@ impl<'ast> AsFormat> for ast::PatternMatchStar { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::pattern::pattern_match_star::FormatPatternMatchStar::default(), + crate::pattern::pattern_match_star::FormatPatternMatchStar, ) } } @@ -2530,7 +2314,7 @@ impl<'ast> IntoFormat> for ast::PatternMatchStar { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::pattern::pattern_match_star::FormatPatternMatchStar::default(), + crate::pattern::pattern_match_star::FormatPatternMatchStar, ) } } @@ -2555,10 +2339,7 @@ impl<'ast> AsFormat> for ast::PatternMatchAs { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::pattern::pattern_match_as::FormatPatternMatchAs::default(), - ) + FormatRefWithRule::new(self, crate::pattern::pattern_match_as::FormatPatternMatchAs) } } impl<'ast> IntoFormat> for ast::PatternMatchAs { @@ -2568,10 +2349,7 @@ impl<'ast> IntoFormat> for ast::PatternMatchAs { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::pattern::pattern_match_as::FormatPatternMatchAs::default(), - ) + FormatOwnedWithRule::new(self, crate::pattern::pattern_match_as::FormatPatternMatchAs) } } @@ -2595,10 +2373,7 @@ impl<'ast> AsFormat> for ast::PatternMatchOr { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::pattern::pattern_match_or::FormatPatternMatchOr::default(), - ) + FormatRefWithRule::new(self, crate::pattern::pattern_match_or::FormatPatternMatchOr) } } impl<'ast> IntoFormat> for ast::PatternMatchOr { @@ -2608,10 +2383,7 @@ impl<'ast> IntoFormat> for ast::PatternMatchOr { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::pattern::pattern_match_or::FormatPatternMatchOr::default(), - ) + FormatOwnedWithRule::new(self, crate::pattern::pattern_match_or::FormatPatternMatchOr) } } @@ -2637,7 +2409,7 @@ impl<'ast> AsFormat> for ast::TypeIgnoreTypeIgnore { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::other::type_ignore_type_ignore::FormatTypeIgnoreTypeIgnore::default(), + crate::other::type_ignore_type_ignore::FormatTypeIgnoreTypeIgnore, ) } } @@ -2650,7 +2422,7 @@ impl<'ast> IntoFormat> for ast::TypeIgnoreTypeIgnore { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::other::type_ignore_type_ignore::FormatTypeIgnoreTypeIgnore::default(), + crate::other::type_ignore_type_ignore::FormatTypeIgnoreTypeIgnore, ) } } @@ -2675,10 +2447,7 @@ impl<'ast> AsFormat> for ast::Comprehension { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::other::comprehension::FormatComprehension::default(), - ) + FormatRefWithRule::new(self, crate::other::comprehension::FormatComprehension) } } impl<'ast> IntoFormat> for ast::Comprehension { @@ -2688,10 +2457,7 @@ impl<'ast> IntoFormat> for ast::Comprehension { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::other::comprehension::FormatComprehension::default(), - ) + FormatOwnedWithRule::new(self, crate::other::comprehension::FormatComprehension) } } @@ -2713,7 +2479,7 @@ impl<'ast> AsFormat> for ast::Arguments { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::other::arguments::FormatArguments::default()) + FormatRefWithRule::new(self, crate::other::arguments::FormatArguments) } } impl<'ast> IntoFormat> for ast::Arguments { @@ -2723,7 +2489,7 @@ impl<'ast> IntoFormat> for ast::Arguments { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::other::arguments::FormatArguments::default()) + FormatOwnedWithRule::new(self, crate::other::arguments::FormatArguments) } } @@ -2737,14 +2503,14 @@ impl<'ast> AsFormat> for ast::Arg { type Format<'a> = FormatRefWithRule<'a, ast::Arg, crate::other::arg::FormatArg, PyFormatContext<'ast>>; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::other::arg::FormatArg::default()) + FormatRefWithRule::new(self, crate::other::arg::FormatArg) } } impl<'ast> IntoFormat> for ast::Arg { type Format = FormatOwnedWithRule>; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::other::arg::FormatArg::default()) + FormatOwnedWithRule::new(self, crate::other::arg::FormatArg) } } @@ -2768,10 +2534,7 @@ impl<'ast> AsFormat> for ast::ArgWithDefault { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::other::arg_with_default::FormatArgWithDefault::default(), - ) + FormatRefWithRule::new(self, crate::other::arg_with_default::FormatArgWithDefault) } } impl<'ast> IntoFormat> for ast::ArgWithDefault { @@ -2781,10 +2544,7 @@ impl<'ast> IntoFormat> for ast::ArgWithDefault { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::other::arg_with_default::FormatArgWithDefault::default(), - ) + FormatOwnedWithRule::new(self, crate::other::arg_with_default::FormatArgWithDefault) } } @@ -2802,7 +2562,7 @@ impl<'ast> AsFormat> for ast::Keyword { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::other::keyword::FormatKeyword::default()) + FormatRefWithRule::new(self, crate::other::keyword::FormatKeyword) } } impl<'ast> IntoFormat> for ast::Keyword { @@ -2812,7 +2572,7 @@ impl<'ast> IntoFormat> for ast::Keyword { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::other::keyword::FormatKeyword::default()) + FormatOwnedWithRule::new(self, crate::other::keyword::FormatKeyword) } } @@ -2826,14 +2586,14 @@ impl<'ast> AsFormat> for ast::Alias { type Format<'a> = FormatRefWithRule<'a, ast::Alias, crate::other::alias::FormatAlias, PyFormatContext<'ast>>; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::other::alias::FormatAlias::default()) + FormatRefWithRule::new(self, crate::other::alias::FormatAlias) } } impl<'ast> IntoFormat> for ast::Alias { type Format = FormatOwnedWithRule>; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::other::alias::FormatAlias::default()) + FormatOwnedWithRule::new(self, crate::other::alias::FormatAlias) } } @@ -2855,7 +2615,7 @@ impl<'ast> AsFormat> for ast::WithItem { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::other::with_item::FormatWithItem::default()) + FormatRefWithRule::new(self, crate::other::with_item::FormatWithItem) } } impl<'ast> IntoFormat> for ast::WithItem { @@ -2865,7 +2625,7 @@ impl<'ast> IntoFormat> for ast::WithItem { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::other::with_item::FormatWithItem::default()) + FormatOwnedWithRule::new(self, crate::other::with_item::FormatWithItem) } } @@ -2887,7 +2647,7 @@ impl<'ast> AsFormat> for ast::MatchCase { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::other::match_case::FormatMatchCase::default()) + FormatRefWithRule::new(self, crate::other::match_case::FormatMatchCase) } } impl<'ast> IntoFormat> for ast::MatchCase { @@ -2897,7 +2657,7 @@ impl<'ast> IntoFormat> for ast::MatchCase { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::other::match_case::FormatMatchCase::default()) + FormatOwnedWithRule::new(self, crate::other::match_case::FormatMatchCase) } } @@ -2919,7 +2679,7 @@ impl<'ast> AsFormat> for ast::Decorator { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::other::decorator::FormatDecorator::default()) + FormatRefWithRule::new(self, crate::other::decorator::FormatDecorator) } } impl<'ast> IntoFormat> for ast::Decorator { @@ -2929,6 +2689,6 @@ impl<'ast> IntoFormat> for ast::Decorator { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::other::decorator::FormatDecorator::default()) + FormatOwnedWithRule::new(self, crate::other::decorator::FormatDecorator) } } diff --git a/crates/ruff_python_formatter/src/module/mod.rs b/crates/ruff_python_formatter/src/module/mod.rs index 34295fab4f..0638d4907c 100644 --- a/crates/ruff_python_formatter/src/module/mod.rs +++ b/crates/ruff_python_formatter/src/module/mod.rs @@ -26,7 +26,7 @@ impl<'ast> AsFormat> for Mod { type Format<'a> = FormatRefWithRule<'a, Mod, FormatMod, PyFormatContext<'ast>>; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, FormatMod::default()) + FormatRefWithRule::new(self, FormatMod) } } @@ -34,6 +34,6 @@ impl<'ast> IntoFormat> for Mod { type Format = FormatOwnedWithRule>; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, FormatMod::default()) + FormatOwnedWithRule::new(self, FormatMod) } } diff --git a/crates/ruff_python_formatter/src/statement/mod.rs b/crates/ruff_python_formatter/src/statement/mod.rs index 3abc1e6f93..4d5804a847 100644 --- a/crates/ruff_python_formatter/src/statement/mod.rs +++ b/crates/ruff_python_formatter/src/statement/mod.rs @@ -72,7 +72,7 @@ impl<'ast> AsFormat> for Stmt { type Format<'a> = FormatRefWithRule<'a, Stmt, FormatStmt, PyFormatContext<'ast>>; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, FormatStmt::default()) + FormatRefWithRule::new(self, FormatStmt) } } @@ -80,6 +80,6 @@ impl<'ast> IntoFormat> for Stmt { type Format = FormatOwnedWithRule>; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, FormatStmt::default()) + FormatOwnedWithRule::new(self, FormatStmt) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs index 491429b2af..6654a70958 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs @@ -124,7 +124,7 @@ impl<'def, 'ast> AsFormat> for AnyFunctionDefinition<'def> > where Self: 'a; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, FormatAnyFunctionDef::default()) + FormatRefWithRule::new(self, FormatAnyFunctionDef) } } From 9f486fa841f4c9f9ce4e09ec65df69dea87e76e2 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 10 Jul 2023 23:52:55 -0400 Subject: [PATCH 405/447] [`flake8-bugbear`] Implement `re-sub-positional-args` (`B034`) (#5669) ## Summary Needed to do some coding to end the day. Closes #5665. --- .../test/fixtures/flake8_bugbear/B034.py | 27 +++++ crates/ruff/src/checkers/ast/mod.rs | 17 ++- crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/flake8_bugbear/mod.rs | 3 +- .../src/rules/flake8_bugbear/rules/mod.rs | 2 + .../rules/re_sub_positional_args.rs | 112 ++++++++++++++++++ ...__flake8_bugbear__tests__B034_B034.py.snap | 102 ++++++++++++++++ .../rules/type_name_incorrect_variance.rs | 22 ++-- ruff.schema.json | 1 + 9 files changed, 269 insertions(+), 18 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/flake8_bugbear/B034.py create mode 100644 crates/ruff/src/rules/flake8_bugbear/rules/re_sub_positional_args.rs create mode 100644 crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B034_B034.py.snap diff --git a/crates/ruff/resources/test/fixtures/flake8_bugbear/B034.py b/crates/ruff/resources/test/fixtures/flake8_bugbear/B034.py new file mode 100644 index 0000000000..1236259c6b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_bugbear/B034.py @@ -0,0 +1,27 @@ +import re +from re import sub + +# B034 +re.sub("a", "b", "aaa", re.IGNORECASE) +re.sub("a", "b", "aaa", 5) +re.sub("a", "b", "aaa", 5, re.IGNORECASE) +re.subn("a", "b", "aaa", re.IGNORECASE) +re.subn("a", "b", "aaa", 5) +re.subn("a", "b", "aaa", 5, re.IGNORECASE) +re.split(" ", "a a a a", re.I) +re.split(" ", "a a a a", 2) +re.split(" ", "a a a a", 2, re.I) +sub("a", "b", "aaa", re.IGNORECASE) + +# OK +re.sub("a", "b", "aaa") +re.sub("a", "b", "aaa", flags=re.IGNORECASE) +re.sub("a", "b", "aaa", count=5) +re.sub("a", "b", "aaa", count=5, flags=re.IGNORECASE) +re.subn("a", "b", "aaa") +re.subn("a", "b", "aaa", flags=re.IGNORECASE) +re.subn("a", "b", "aaa", count=5) +re.subn("a", "b", "aaa", count=5, flags=re.IGNORECASE) +re.split(" ", "a a a a", flags=re.I) +re.split(" ", "a a a a", maxsplit=2) +re.split(" ", "a a a a", maxsplit=2, flags=re.I) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 48593c06d5..11f8d3600f 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -2426,12 +2426,14 @@ where } pandas_vet::rules::attr(self, attr, value, expr); } - Expr::Call(ast::ExprCall { - func, - args, - keywords, - range: _, - }) => { + Expr::Call( + call @ ast::ExprCall { + func, + args, + keywords, + range: _, + }, + ) => { if let Expr::Name(ast::ExprName { id, ctx, range: _ }) = func.as_ref() { if id == "locals" && matches!(ctx, ExprContext::Load) { let scope = self.semantic.scope_mut(); @@ -2597,6 +2599,9 @@ where ]) { flake8_bandit::rules::suspicious_function_call(self, expr); } + if self.enabled(Rule::ReSubPositionalArgs) { + flake8_bugbear::rules::re_sub_positional_args(self, call); + } if self.enabled(Rule::UnreliableCallableCheck) { flake8_bugbear::rules::unreliable_callable_check(self, expr, func, args); } diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 9e9f01e9fb..03dd26fb82 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -266,6 +266,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Bugbear, "031") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::ReuseOfGroupbyGenerator), (Flake8Bugbear, "032") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::UnintentionalTypeAnnotation), (Flake8Bugbear, "033") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::DuplicateValue), + (Flake8Bugbear, "034") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::ReSubPositionalArgs), (Flake8Bugbear, "904") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::RaiseWithoutFromInsideExcept), (Flake8Bugbear, "905") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::ZipWithoutExplicitStrict), diff --git a/crates/ruff/src/rules/flake8_bugbear/mod.rs b/crates/ruff/src/rules/flake8_bugbear/mod.rs index 7e4864a60d..df1c463250 100644 --- a/crates/ruff/src/rules/flake8_bugbear/mod.rs +++ b/crates/ruff/src/rules/flake8_bugbear/mod.rs @@ -19,7 +19,6 @@ mod tests { #[test_case(Rule::AssertRaisesException, Path::new("B017.py"))] #[test_case(Rule::AssignmentToOsEnviron, Path::new("B003.py"))] #[test_case(Rule::CachedInstanceMethod, Path::new("B019.py"))] - #[test_case(Rule::RaiseLiteral, Path::new("B016.py"))] #[test_case(Rule::DuplicateHandlerException, Path::new("B014.py"))] #[test_case(Rule::DuplicateTryBlockException, Path::new("B025.py"))] #[test_case(Rule::DuplicateValue, Path::new("B033.py"))] @@ -35,7 +34,9 @@ mod tests { #[test_case(Rule::LoopVariableOverridesIterator, Path::new("B020.py"))] #[test_case(Rule::MutableArgumentDefault, Path::new("B006_B008.py"))] #[test_case(Rule::NoExplicitStacklevel, Path::new("B028.py"))] + #[test_case(Rule::RaiseLiteral, Path::new("B016.py"))] #[test_case(Rule::RaiseWithoutFromInsideExcept, Path::new("B904.py"))] + #[test_case(Rule::ReSubPositionalArgs, Path::new("B034.py"))] #[test_case(Rule::RedundantTupleInExceptionHandler, Path::new("B013.py"))] #[test_case(Rule::ReuseOfGroupbyGenerator, Path::new("B031.py"))] #[test_case(Rule::SetAttrWithConstant, Path::new("B009_B010.py"))] diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs b/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs index e9f707a0a1..0cb4402221 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs @@ -17,6 +17,7 @@ pub(crate) use mutable_argument_default::*; pub(crate) use no_explicit_stacklevel::*; pub(crate) use raise_literal::*; pub(crate) use raise_without_from_inside_except::*; +pub(crate) use re_sub_positional_args::*; pub(crate) use redundant_tuple_in_exception_handler::*; pub(crate) use reuse_of_groupby_generator::*; pub(crate) use setattr_with_constant::*; @@ -50,6 +51,7 @@ mod mutable_argument_default; mod no_explicit_stacklevel; mod raise_literal; mod raise_without_from_inside_except; +mod re_sub_positional_args; mod redundant_tuple_in_exception_handler; mod reuse_of_groupby_generator; mod setattr_with_constant; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/re_sub_positional_args.rs b/crates/ruff/src/rules/flake8_bugbear/rules/re_sub_positional_args.rs new file mode 100644 index 0000000000..5e9cdddb4c --- /dev/null +++ b/crates/ruff/src/rules/flake8_bugbear/rules/re_sub_positional_args.rs @@ -0,0 +1,112 @@ +use std::fmt; + +use rustpython_parser::ast::{self, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for calls to `re.sub`, `re.subn`, and `re.split` that pass `count`, +/// `maxsplit`, or `flags` as positional arguments. +/// +/// ## Why is this bad? +/// Passing `count`, `maxsplit`, or `flags` as positional arguments to +/// `re.sub`, re.subn`, or `re.split` can lead to confusion, as most methods in +/// the `re` module accepts `flags` as the third positional argument, while +/// `re.sub`, `re.subn`, and `re.split` have different signatures. +/// +/// Instead, pass `count`, `maxsplit`, and `flags` as keyword arguments. +/// +/// ## Example +/// ```python +/// import re +/// +/// re.split("pattern", "replacement", 1) +/// ``` +/// +/// Use instead: +/// ```python +/// import re +/// +/// re.split("pattern", "replacement", maxsplit=1) +/// ``` +/// +/// ## References +/// - [Python documentation: `re.sub`](https://docs.python.org/3/library/re.html#re.sub) +/// - [Python documentation: `re.subn`](https://docs.python.org/3/library/re.html#re.subn) +/// - [Python documentation: `re.split`](https://docs.python.org/3/library/re.html#re.split) +#[violation] +pub struct ReSubPositionalArgs { + method: Method, +} + +impl Violation for ReSubPositionalArgs { + #[derive_message_formats] + fn message(&self) -> String { + let ReSubPositionalArgs { method } = self; + let param_name = method.param_name(); + format!( + "`{method}` should pass `{param_name}` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions" + ) + } +} + +/// B034 +pub(crate) fn re_sub_positional_args(checker: &mut Checker, call: &ast::ExprCall) { + let Some(method) = checker + .semantic() + .resolve_call_path(&call.func) + .and_then(|call_path| match call_path.as_slice() { + ["re", "sub"] => Some(Method::Sub), + ["re", "subn"] => Some(Method::Subn), + ["re", "split"] => Some(Method::Split), + _ => None, + }) + else { + return; + }; + + if call.args.len() > method.num_args() { + checker.diagnostics.push(Diagnostic::new( + ReSubPositionalArgs { method }, + call.range(), + )); + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum Method { + Sub, + Subn, + Split, +} + +impl fmt::Display for Method { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Sub => fmt.write_str("re.sub"), + Self::Subn => fmt.write_str("re.subn"), + Self::Split => fmt.write_str("re.split"), + } + } +} + +impl Method { + const fn param_name(self) -> &'static str { + match self { + Self::Sub => "count", + Self::Subn => "count", + Self::Split => "maxsplit", + } + } + + const fn num_args(self) -> usize { + match self { + Self::Sub => 3, + Self::Subn => 3, + Self::Split => 2, + } + } +} diff --git a/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B034_B034.py.snap b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B034_B034.py.snap new file mode 100644 index 0000000000..44efbb7d97 --- /dev/null +++ b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B034_B034.py.snap @@ -0,0 +1,102 @@ +--- +source: crates/ruff/src/rules/flake8_bugbear/mod.rs +--- +B034.py:5:1: B034 `re.sub` should pass `count` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions + | +4 | # B034 +5 | re.sub("a", "b", "aaa", re.IGNORECASE) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B034 +6 | re.sub("a", "b", "aaa", 5) +7 | re.sub("a", "b", "aaa", 5, re.IGNORECASE) + | + +B034.py:6:1: B034 `re.sub` should pass `count` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions + | +4 | # B034 +5 | re.sub("a", "b", "aaa", re.IGNORECASE) +6 | re.sub("a", "b", "aaa", 5) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ B034 +7 | re.sub("a", "b", "aaa", 5, re.IGNORECASE) +8 | re.subn("a", "b", "aaa", re.IGNORECASE) + | + +B034.py:7:1: B034 `re.sub` should pass `count` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions + | +5 | re.sub("a", "b", "aaa", re.IGNORECASE) +6 | re.sub("a", "b", "aaa", 5) +7 | re.sub("a", "b", "aaa", 5, re.IGNORECASE) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B034 +8 | re.subn("a", "b", "aaa", re.IGNORECASE) +9 | re.subn("a", "b", "aaa", 5) + | + +B034.py:8:1: B034 `re.subn` should pass `count` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions + | + 6 | re.sub("a", "b", "aaa", 5) + 7 | re.sub("a", "b", "aaa", 5, re.IGNORECASE) + 8 | re.subn("a", "b", "aaa", re.IGNORECASE) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B034 + 9 | re.subn("a", "b", "aaa", 5) +10 | re.subn("a", "b", "aaa", 5, re.IGNORECASE) + | + +B034.py:9:1: B034 `re.subn` should pass `count` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions + | + 7 | re.sub("a", "b", "aaa", 5, re.IGNORECASE) + 8 | re.subn("a", "b", "aaa", re.IGNORECASE) + 9 | re.subn("a", "b", "aaa", 5) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B034 +10 | re.subn("a", "b", "aaa", 5, re.IGNORECASE) +11 | re.split(" ", "a a a a", re.I) + | + +B034.py:10:1: B034 `re.subn` should pass `count` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions + | + 8 | re.subn("a", "b", "aaa", re.IGNORECASE) + 9 | re.subn("a", "b", "aaa", 5) +10 | re.subn("a", "b", "aaa", 5, re.IGNORECASE) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B034 +11 | re.split(" ", "a a a a", re.I) +12 | re.split(" ", "a a a a", 2) + | + +B034.py:11:1: B034 `re.split` should pass `maxsplit` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions + | + 9 | re.subn("a", "b", "aaa", 5) +10 | re.subn("a", "b", "aaa", 5, re.IGNORECASE) +11 | re.split(" ", "a a a a", re.I) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B034 +12 | re.split(" ", "a a a a", 2) +13 | re.split(" ", "a a a a", 2, re.I) + | + +B034.py:12:1: B034 `re.split` should pass `maxsplit` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions + | +10 | re.subn("a", "b", "aaa", 5, re.IGNORECASE) +11 | re.split(" ", "a a a a", re.I) +12 | re.split(" ", "a a a a", 2) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B034 +13 | re.split(" ", "a a a a", 2, re.I) +14 | sub("a", "b", "aaa", re.IGNORECASE) + | + +B034.py:13:1: B034 `re.split` should pass `maxsplit` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions + | +11 | re.split(" ", "a a a a", re.I) +12 | re.split(" ", "a a a a", 2) +13 | re.split(" ", "a a a a", 2, re.I) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B034 +14 | sub("a", "b", "aaa", re.IGNORECASE) + | + +B034.py:14:1: B034 `re.sub` should pass `count` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions + | +12 | re.split(" ", "a a a a", 2) +13 | re.split(" ", "a a a a", 2, re.I) +14 | sub("a", "b", "aaa", re.IGNORECASE) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B034 +15 | +16 | # OK + | + + diff --git a/crates/ruff/src/rules/pylint/rules/type_name_incorrect_variance.rs b/crates/ruff/src/rules/pylint/rules/type_name_incorrect_variance.rs index 8683e09d15..45e9622584 100644 --- a/crates/ruff/src/rules/pylint/rules/type_name_incorrect_variance.rs +++ b/crates/ruff/src/rules/pylint/rules/type_name_incorrect_variance.rs @@ -66,14 +66,14 @@ impl Violation for TypeNameIncorrectVariance { /// PLC0105 pub(crate) fn type_name_incorrect_variance(checker: &mut Checker, value: &Expr) { let Expr::Call(ast::ExprCall { - func, - args, - keywords, - .. - }) = value - else { - return; - }; + func, + args, + keywords, + .. + }) = value + else { + return; + }; let Some(param_name) = type_param_name(args, keywords) else { return; @@ -121,9 +121,9 @@ pub(crate) fn type_name_incorrect_variance(checker: &mut Checker, value: &Expr) None } }) - else { - return; - }; + else { + return; + }; let variance = variance(covariant, contravariant); let name_root = param_name diff --git a/ruff.schema.json b/ruff.schema.json index 84345a1cf2..ae611fd379 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1712,6 +1712,7 @@ "B031", "B032", "B033", + "B034", "B9", "B90", "B904", From 987111f5fbc9bd7b1243a1bed7002248bfaded7d Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 11 Jul 2023 08:08:08 +0200 Subject: [PATCH 406/447] Format `ExpressionStarred` nodes (#5654) --- .../test/fixtures/ruff/expression/starred.py | 15 ++ .../ruff_python_formatter/src/comments/mod.rs | 14 +- .../src/comments/placement.rs | 16 ++ .../src/comments/visitor.rs | 7 + .../src/expression/expr_starred.rs | 30 ++- ...tibility@miscellaneous__decorators.py.snap | 36 +-- ...ibility@py_310__starred_for_target.py.snap | 25 +- ...lack_compatibility@py_311__pep_654.py.snap | 240 ------------------ ...ompatibility@py_311__pep_654_style.py.snap | 197 -------------- ...black_compatibility@py_38__pep_572.py.snap | 10 +- ...lack_compatibility@py_38__python38.py.snap | 10 +- ...patibility@simple_cases__comments6.py.snap | 11 +- ...atibility@simple_cases__expression.py.snap | 95 ++----- ...mpatibility@simple_cases__fmtonoff.py.snap | 4 +- .../format@expression__starred.py.snap | 61 +++++ .../snapshots/format@statement__with.py.snap | 2 +- 16 files changed, 192 insertions(+), 581 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/starred.py delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654.py.snap delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654_style.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@expression__starred.py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/starred.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/starred.py new file mode 100644 index 0000000000..76483d7fa1 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/starred.py @@ -0,0 +1,15 @@ +call( + # Leading starred comment + * # Trailing star comment + [ + # Leading value commnt + [What, i, this, s, very, long, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa] + ] # trailing value comment +) + +call( + # Leading starred comment + * ( # Leading value commnt + [What, i, this, s, very, long, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa] + ) # trailing value comment +) diff --git a/crates/ruff_python_formatter/src/comments/mod.rs b/crates/ruff_python_formatter/src/comments/mod.rs index 63c148fff8..71912c60fd 100644 --- a/crates/ruff_python_formatter/src/comments/mod.rs +++ b/crates/ruff_python_formatter/src/comments/mod.rs @@ -87,11 +87,12 @@ //! //! It is possible to add an additional optional label to [`SourceComment`] If ever the need arises to distinguish two *dangling comments* in the formatting logic, +use ruff_text_size::TextRange; use std::cell::Cell; use std::fmt::Debug; use std::rc::Rc; -use rustpython_parser::ast::Mod; +use rustpython_parser::ast::{Mod, Ranged}; pub(crate) use format::{ dangling_comments, dangling_node_comments, leading_alternate_branch_comments, leading_comments, @@ -114,7 +115,7 @@ mod placement; mod visitor; /// A comment in the source document. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq)] pub(crate) struct SourceComment { /// The location of the comment in the source document. slice: SourceCodeSlice, @@ -155,15 +156,20 @@ impl SourceComment { pub(crate) fn is_unformatted(&self) -> bool { !self.is_formatted() } -} -impl SourceComment { /// Returns a nice debug representation that prints the source code for every comment (and not just the range). pub(crate) fn debug<'a>(&'a self, source_code: SourceCode<'a>) -> DebugComment<'a> { DebugComment::new(self, source_code) } } +impl Ranged for SourceComment { + #[inline] + fn range(&self) -> TextRange { + self.slice.range() + } +} + /// The position of a comment in the source text. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub(crate) enum CommentLinePosition { diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 85b6534f49..bc8df853de 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -38,6 +38,7 @@ pub(super) fn place_comment<'a>( handle_slice_comments, handle_attribute_comment, handle_expr_if_comment, + handle_trailing_expression_starred_star_end_of_line_comment, ]; for handler in HANDLERS { comment = match handler(comment, locator) { @@ -1215,6 +1216,21 @@ fn handle_expr_if_comment<'a>( CommentPlacement::Default(comment) } +fn handle_trailing_expression_starred_star_end_of_line_comment<'a>( + comment: DecoratedComment<'a>, + _locator: &Locator, +) -> CommentPlacement<'a> { + if comment.line_position().is_own_line() || comment.following_node().is_none() { + return CommentPlacement::Default(comment); + } + + let AnyNodeRef::ExprStarred(starred) = comment.enclosing_node() else { + return CommentPlacement::Default(comment); + }; + + CommentPlacement::leading(starred.as_any_node_ref(), comment) +} + /// Looks for a token in the range that contains no other tokens except for parentheses outside /// the expression ranges fn find_only_token_in_range(range: TextRange, locator: &Locator, token_kind: TokenKind) -> Token { diff --git a/crates/ruff_python_formatter/src/comments/visitor.rs b/crates/ruff_python_formatter/src/comments/visitor.rs index 143d381321..947dc4b31b 100644 --- a/crates/ruff_python_formatter/src/comments/visitor.rs +++ b/crates/ruff_python_formatter/src/comments/visitor.rs @@ -454,6 +454,13 @@ impl<'a> DecoratedComment<'a> { } } +impl Ranged for DecoratedComment<'_> { + #[inline] + fn range(&self) -> TextRange { + self.slice.range() + } +} + impl From> for SourceComment { fn from(decorated: DecoratedComment) -> Self { Self::new(decorated.slice, decorated.line_position) diff --git a/crates/ruff_python_formatter/src/expression/expr_starred.rs b/crates/ruff_python_formatter/src/expression/expr_starred.rs index 3711deb92e..bb8492e0da 100644 --- a/crates/ruff_python_formatter/src/expression/expr_starred.rs +++ b/crates/ruff_python_formatter/src/expression/expr_starred.rs @@ -1,22 +1,32 @@ +use rustpython_parser::ast::ExprStarred; + +use ruff_formatter::write; + use crate::comments::Comments; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; -use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::ExprStarred; +use crate::prelude::*; +use crate::FormatNodeRule; #[derive(Default)] pub struct FormatExprStarred; impl FormatNodeRule for FormatExprStarred { - fn fmt_fields(&self, _item: &ExprStarred, f: &mut PyFormatter) -> FormatResult<()> { - write!( - f, - [not_yet_implemented_custom_text( - "*NOT_YET_IMPLEMENTED_ExprStarred" - )] - ) + fn fmt_fields(&self, item: &ExprStarred, f: &mut PyFormatter) -> FormatResult<()> { + let ExprStarred { + range: _, + value, + ctx: _, + } = item; + + write!(f, [text("*"), value.format()]) + } + + fn fmt_dangling_comments(&self, node: &ExprStarred, f: &mut PyFormatter) -> FormatResult<()> { + debug_assert_eq!(f.context().comments().dangling_comments(node), []); + + Ok(()) } } diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__decorators.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__decorators.py.snap index 020bb3c340..7793a98149 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__decorators.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__decorators.py.snap @@ -200,7 +200,7 @@ def f(): + +## + -+@decorator(*NOT_YET_IMPLEMENTED_ExprStarred) ++@decorator(*args) +def f(): + ... + @@ -216,7 +216,7 @@ def f(): ## -@(decorator) -+@decorator(*NOT_YET_IMPLEMENTED_ExprStarred, **kwargs) ++@decorator(*args, **kwargs) def f(): ... @@ -225,7 +225,7 @@ def f(): -@sequence["decorator"] +@decorator( -+ *NOT_YET_IMPLEMENTED_ExprStarred, ++ *args, + **kwargs, +) def f(): @@ -257,7 +257,7 @@ def f(): + +## + -+@dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred) ++@dotted.decorator(*args) +def f(): + ... + @@ -271,7 +271,7 @@ def f(): + +## + -+@dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred, **kwargs) ++@dotted.decorator(*args, **kwargs) +def f(): + ... + @@ -279,7 +279,7 @@ def f(): +## + +@dotted.decorator( -+ *NOT_YET_IMPLEMENTED_ExprStarred, ++ *args, + **kwargs, +) +def f(): @@ -309,7 +309,7 @@ def f(): + +## + -+@double.dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred) ++@double.dotted.decorator(*args) +def f(): + ... + @@ -323,7 +323,7 @@ def f(): + +## + -+@double.dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred, **kwargs) ++@double.dotted.decorator(*args, **kwargs) +def f(): + ... + @@ -331,7 +331,7 @@ def f(): +## + +@double.dotted.decorator( -+ *NOT_YET_IMPLEMENTED_ExprStarred, ++ *args, + **kwargs, +) +def f(): @@ -392,7 +392,7 @@ def f(): ## -@decorator(*NOT_YET_IMPLEMENTED_ExprStarred) +@decorator(*args) def f(): ... @@ -406,7 +406,7 @@ def f(): ## -@decorator(*NOT_YET_IMPLEMENTED_ExprStarred, **kwargs) +@decorator(*args, **kwargs) def f(): ... @@ -414,7 +414,7 @@ def f(): ## @decorator( - *NOT_YET_IMPLEMENTED_ExprStarred, + *args, **kwargs, ) def f(): @@ -444,7 +444,7 @@ def f(): ## -@dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred) +@dotted.decorator(*args) def f(): ... @@ -458,7 +458,7 @@ def f(): ## -@dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred, **kwargs) +@dotted.decorator(*args, **kwargs) def f(): ... @@ -466,7 +466,7 @@ def f(): ## @dotted.decorator( - *NOT_YET_IMPLEMENTED_ExprStarred, + *args, **kwargs, ) def f(): @@ -496,7 +496,7 @@ def f(): ## -@double.dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred) +@double.dotted.decorator(*args) def f(): ... @@ -510,7 +510,7 @@ def f(): ## -@double.dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred, **kwargs) +@double.dotted.decorator(*args, **kwargs) def f(): ... @@ -518,7 +518,7 @@ def f(): ## @double.dotted.decorator( - *NOT_YET_IMPLEMENTED_ExprStarred, + *args, **kwargs, ) def f(): diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__starred_for_target.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__starred_for_target.py.snap index da4a8b4b11..dad8c213dc 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__starred_for_target.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__starred_for_target.py.snap @@ -39,21 +39,8 @@ async for x in ( ```diff --- Black +++ Ruff -@@ -1,27 +1,19 @@ --for x in *a, *b: -+for x in *NOT_YET_IMPLEMENTED_ExprStarred, *NOT_YET_IMPLEMENTED_ExprStarred: - print(x) - --for x in a, b, *c: -+for x in a, b, *NOT_YET_IMPLEMENTED_ExprStarred: - print(x) - --for x in *a, b, c: -+for x in *NOT_YET_IMPLEMENTED_ExprStarred, b, c: - print(x) - --for x in *a, b, *c: -+for x in *NOT_YET_IMPLEMENTED_ExprStarred, b, *NOT_YET_IMPLEMENTED_ExprStarred: +@@ -10,18 +10,10 @@ + for x in *a, b, *c: print(x) -async for x in *a, *b: @@ -80,16 +67,16 @@ async for x in ( ## Ruff Output ```py -for x in *NOT_YET_IMPLEMENTED_ExprStarred, *NOT_YET_IMPLEMENTED_ExprStarred: +for x in *a, *b: print(x) -for x in a, b, *NOT_YET_IMPLEMENTED_ExprStarred: +for x in a, b, *c: print(x) -for x in *NOT_YET_IMPLEMENTED_ExprStarred, b, c: +for x in *a, b, c: print(x) -for x in *NOT_YET_IMPLEMENTED_ExprStarred, b, *NOT_YET_IMPLEMENTED_ExprStarred: +for x in *a, b, *c: print(x) NOT_YET_IMPLEMENTED_StmtAsyncFor diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654.py.snap deleted file mode 100644 index b7995833b5..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654.py.snap +++ /dev/null @@ -1,240 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py ---- -## Input - -```py -try: - raise OSError("blah") -except* ExceptionGroup as e: - pass - - -try: - async with trio.open_nursery() as nursery: - # Make two concurrent calls to child() - nursery.start_soon(child) - nursery.start_soon(child) -except* ValueError: - pass - -try: - try: - raise ValueError(42) - except: - try: - raise TypeError(int) - except* Exception: - pass - 1 / 0 -except Exception as e: - exc = e - -try: - try: - raise FalsyEG("eg", [TypeError(1), ValueError(2)]) - except* TypeError as e: - tes = e - raise - except* ValueError as e: - ves = e - pass -except Exception as e: - exc = e - -try: - try: - raise orig - except* (TypeError, ValueError) as e: - raise SyntaxError(3) from e -except BaseException as e: - exc = e - -try: - try: - raise orig - except* OSError as e: - raise TypeError(3) from e -except ExceptionGroup as e: - exc = e -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,5 +1,5 @@ - try: -- raise OSError("blah") -+ NOT_YET_IMPLEMENTED_StmtRaise - except* ExceptionGroup as e: - pass - -@@ -14,10 +14,10 @@ - - try: - try: -- raise ValueError(42) -+ NOT_YET_IMPLEMENTED_StmtRaise - except: - try: -- raise TypeError(int) -+ NOT_YET_IMPLEMENTED_StmtRaise - except* Exception: - pass - 1 / 0 -@@ -26,10 +26,10 @@ - - try: - try: -- raise FalsyEG("eg", [TypeError(1), ValueError(2)]) -+ NOT_YET_IMPLEMENTED_StmtRaise - except* TypeError as e: - tes = e -- raise -+ NOT_YET_IMPLEMENTED_StmtRaise - except* ValueError as e: - ves = e - pass -@@ -38,16 +38,16 @@ - - try: - try: -- raise orig -+ NOT_YET_IMPLEMENTED_StmtRaise - except* (TypeError, ValueError) as e: -- raise SyntaxError(3) from e -+ NOT_YET_IMPLEMENTED_StmtRaise - except BaseException as e: - exc = e - - try: - try: -- raise orig -+ NOT_YET_IMPLEMENTED_StmtRaise - except* OSError as e: -- raise TypeError(3) from e -+ NOT_YET_IMPLEMENTED_StmtRaise - except ExceptionGroup as e: - exc = e -``` - -## Ruff Output - -```py -try: - NOT_YET_IMPLEMENTED_StmtRaise -except* ExceptionGroup as e: - pass - - -try: - async with trio.open_nursery() as nursery: - # Make two concurrent calls to child() - nursery.start_soon(child) - nursery.start_soon(child) -except* ValueError: - pass - -try: - try: - NOT_YET_IMPLEMENTED_StmtRaise - except: - try: - NOT_YET_IMPLEMENTED_StmtRaise - except* Exception: - pass - 1 / 0 -except Exception as e: - exc = e - -try: - try: - NOT_YET_IMPLEMENTED_StmtRaise - except* TypeError as e: - tes = e - NOT_YET_IMPLEMENTED_StmtRaise - except* ValueError as e: - ves = e - pass -except Exception as e: - exc = e - -try: - try: - NOT_YET_IMPLEMENTED_StmtRaise - except* (TypeError, ValueError) as e: - NOT_YET_IMPLEMENTED_StmtRaise -except BaseException as e: - exc = e - -try: - try: - NOT_YET_IMPLEMENTED_StmtRaise - except* OSError as e: - NOT_YET_IMPLEMENTED_StmtRaise -except ExceptionGroup as e: - exc = e -``` - -## Black Output - -```py -try: - raise OSError("blah") -except* ExceptionGroup as e: - pass - - -try: - async with trio.open_nursery() as nursery: - # Make two concurrent calls to child() - nursery.start_soon(child) - nursery.start_soon(child) -except* ValueError: - pass - -try: - try: - raise ValueError(42) - except: - try: - raise TypeError(int) - except* Exception: - pass - 1 / 0 -except Exception as e: - exc = e - -try: - try: - raise FalsyEG("eg", [TypeError(1), ValueError(2)]) - except* TypeError as e: - tes = e - raise - except* ValueError as e: - ves = e - pass -except Exception as e: - exc = e - -try: - try: - raise orig - except* (TypeError, ValueError) as e: - raise SyntaxError(3) from e -except BaseException as e: - exc = e - -try: - try: - raise orig - except* OSError as e: - raise TypeError(3) from e -except ExceptionGroup as e: - exc = e -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654_style.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654_style.py.snap deleted file mode 100644 index b0bc8e8f17..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654_style.py.snap +++ /dev/null @@ -1,197 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py ---- -## Input - -```py -try: - raise OSError("blah") -except * ExceptionGroup as e: - pass - - -try: - async with trio.open_nursery() as nursery: - # Make two concurrent calls to child() - nursery.start_soon(child) - nursery.start_soon(child) -except *ValueError: - pass - -try: - try: - raise ValueError(42) - except: - try: - raise TypeError(int) - except *(Exception): - pass - 1 / 0 -except Exception as e: - exc = e - -try: - try: - raise FalsyEG("eg", [TypeError(1), ValueError(2)]) - except \ - *TypeError as e: - tes = e - raise - except * ValueError as e: - ves = e - pass -except Exception as e: - exc = e - -try: - try: - raise orig - except *(TypeError, ValueError, *OTHER_EXCEPTIONS) as e: - raise SyntaxError(3) from e -except BaseException as e: - exc = e - -try: - try: - raise orig - except\ - * OSError as e: - raise TypeError(3) from e -except ExceptionGroup as e: - exc = e -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -39,7 +39,7 @@ - try: - try: - raise orig -- except* (TypeError, ValueError, *OTHER_EXCEPTIONS) as e: -+ except* (TypeError, ValueError, *NOT_YET_IMPLEMENTED_ExprStarred) as e: - raise SyntaxError(3) from e - except BaseException as e: - exc = e -``` - -## Ruff Output - -```py -try: - raise OSError("blah") -except* ExceptionGroup as e: - pass - - -try: - async with trio.open_nursery() as nursery: - # Make two concurrent calls to child() - nursery.start_soon(child) - nursery.start_soon(child) -except* ValueError: - pass - -try: - try: - raise ValueError(42) - except: - try: - raise TypeError(int) - except* Exception: - pass - 1 / 0 -except Exception as e: - exc = e - -try: - try: - raise FalsyEG("eg", [TypeError(1), ValueError(2)]) - except* TypeError as e: - tes = e - raise - except* ValueError as e: - ves = e - pass -except Exception as e: - exc = e - -try: - try: - raise orig - except* (TypeError, ValueError, *NOT_YET_IMPLEMENTED_ExprStarred) as e: - raise SyntaxError(3) from e -except BaseException as e: - exc = e - -try: - try: - raise orig - except* OSError as e: - raise TypeError(3) from e -except ExceptionGroup as e: - exc = e -``` - -## Black Output - -```py -try: - raise OSError("blah") -except* ExceptionGroup as e: - pass - - -try: - async with trio.open_nursery() as nursery: - # Make two concurrent calls to child() - nursery.start_soon(child) - nursery.start_soon(child) -except* ValueError: - pass - -try: - try: - raise ValueError(42) - except: - try: - raise TypeError(int) - except* Exception: - pass - 1 / 0 -except Exception as e: - exc = e - -try: - try: - raise FalsyEG("eg", [TypeError(1), ValueError(2)]) - except* TypeError as e: - tes = e - raise - except* ValueError as e: - ves = e - pass -except Exception as e: - exc = e - -try: - try: - raise orig - except* (TypeError, ValueError, *OTHER_EXCEPTIONS) as e: - raise SyntaxError(3) from e -except BaseException as e: - exc = e - -try: - try: - raise orig - except* OSError as e: - raise TypeError(3) from e -except ExceptionGroup as e: - exc = e -``` - - 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 53e4192d06..f64c729232 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 @@ -72,7 +72,7 @@ while x := f(x): (y := f(x)) y0 = (y1 := f(x)) foo(x=(y := f(x))) -@@ -19,29 +19,29 @@ +@@ -19,10 +19,10 @@ pass @@ -86,10 +86,8 @@ while x := f(x): +lambda x: True x = (y := 0) (z := (y := (x := 0))) --(info := (name, phone, *rest)) -+(info := (name, phone, *NOT_YET_IMPLEMENTED_ExprStarred)) - (x := 1, 2) - (total := total + tax) + (info := (name, phone, *rest)) +@@ -31,17 +31,17 @@ len(lines := f.readlines()) foo(x := 3, cat="vector") foo(cat=(category := "vector")) @@ -144,7 +142,7 @@ lambda x: True lambda x: True x = (y := 0) (z := (y := (x := 0))) -(info := (name, phone, *NOT_YET_IMPLEMENTED_ExprStarred)) +(info := (name, phone, *rest)) (x := 1, 2) (total := total + tax) len(lines := f.readlines()) diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__python38.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__python38.py.snap index 4f4ceeb54b..d706374274 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__python38.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__python38.py.snap @@ -31,13 +31,7 @@ def t(): ```diff --- Black +++ Ruff -@@ -3,19 +3,19 @@ - - def starred_return(): - my_list = ["value2", "value3"] -- return "value1", *my_list -+ return "value1", *NOT_YET_IMPLEMENTED_ExprStarred - +@@ -8,14 +8,14 @@ def starred_yield(): my_list = ["value2", "value3"] @@ -66,7 +60,7 @@ def t(): def starred_return(): my_list = ["value2", "value3"] - return "value1", *NOT_YET_IMPLEMENTED_ExprStarred + return "value1", *my_list def starred_yield(): 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 a27f99c5bf..6be2a77bc1 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 @@ -141,7 +141,7 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite an_element_with_a_long_value = calls() or more_calls() and more() # type: bool tup = ( -@@ -100,19 +98,32 @@ +@@ -100,19 +98,30 @@ ) c = call( @@ -177,10 +177,7 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite + ], # type: ignore ) --aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type] -+aaaaaaaaaaaaa, bbbbbbbbb = map( -+ list, map(itertools.chain.from_iterable, zip(*NOT_YET_IMPLEMENTED_ExprStarred)) -+) # type: ignore[arg-type] + aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type] ``` ## Ruff Output @@ -312,9 +309,7 @@ call_to_some_function_asdf( ], # type: ignore ) -aaaaaaaaaaaaa, bbbbbbbbb = map( - list, map(itertools.chain.from_iterable, zip(*NOT_YET_IMPLEMENTED_ExprStarred)) -) # type: ignore[arg-type] +aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type] ``` ## Black Output 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 b743d9c808..b8aff61aa7 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 @@ -316,32 +316,9 @@ last_call() () (1,) (1, 2) -@@ -69,40 +72,37 @@ - 2, - 3, - ] --[*a] --[*range(10)] -+[*NOT_YET_IMPLEMENTED_ExprStarred] -+[*NOT_YET_IMPLEMENTED_ExprStarred] - [ -- *a, -+ *NOT_YET_IMPLEMENTED_ExprStarred, - 4, - 5, - ] - [ - 4, -- *a, -+ *NOT_YET_IMPLEMENTED_ExprStarred, - 5, - ] - [ - this_is_a_very_long_variable_which_will_force_a_delimiter_split, - element, +@@ -87,22 +90,19 @@ another, -- *more, -+ *NOT_YET_IMPLEMENTED_ExprStarred, + *more, ] -{i for i in (1, 2, 3)} -{(i**2) for i in (1, 2, 3)} @@ -375,20 +352,15 @@ last_call() Python3 > Python2 > COBOL Life is Life call() -@@ -115,10 +115,10 @@ +@@ -115,7 +115,7 @@ arg, another, kwarg="hey", - **kwargs + **kwargs, ) # note: no trailing comma pre-3.6 --call(*gidgets[:2]) --call(a, *gidgets[:2]) -+call(*NOT_YET_IMPLEMENTED_ExprStarred) -+call(a, *NOT_YET_IMPLEMENTED_ExprStarred) - call(**self.screen_kwargs) - call(b, **self.screen_kwargs) - lukasz.langa.pl + call(*gidgets[:2]) + call(a, *gidgets[:2]) @@ -131,34 +131,28 @@ tuple[str, ...] tuple[str, int, float, dict[str, int]] @@ -397,6 +369,9 @@ last_call() - int, - float, - dict[str, int], +-] +-very_long_variable_name_filters: t.List[ +- t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], + ( + str, + int, @@ -404,9 +379,6 @@ last_call() + dict[str, int], + ) ] --very_long_variable_name_filters: t.List[ -- t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], --] -xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore - sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) -) @@ -446,7 +418,7 @@ last_call() numpy[np.newaxis, :] (str or None) if (sys.version_info[0] > (3,)) else (str or bytes or None) {"2.7": dead, "3.7": long_live or die_hard} -@@ -181,11 +175,11 @@ +@@ -181,10 +175,10 @@ (SomeName) SomeName (Good, Bad, Ugly) @@ -454,26 +426,14 @@ last_call() -((i**2) for i in (1, 2, 3)) -((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,) +(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,) + (*starred,) { "id": "1", - "type": "type", -@@ -200,32 +194,22 @@ - c = 1 - d = (1,) + a + (2,) - e = (1,).count(1) --f = 1, *range(10) --g = 1, *"ten" -+f = 1, *NOT_YET_IMPLEMENTED_ExprStarred -+g = 1, *NOT_YET_IMPLEMENTED_ExprStarred - what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set( - vars_to_remove - ) +@@ -208,24 +202,14 @@ what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( vars_to_remove ) @@ -506,7 +466,7 @@ last_call() Ø = set() authors.łukasz.say_thanks() mapping = { -@@ -237,29 +221,33 @@ +@@ -237,10 +221,10 @@ def gen(): @@ -521,10 +481,10 @@ last_call() async def f(): - await some.complicated[0].call(with_args=(True or (1 is not 1))) +@@ -248,18 +232,22 @@ --print(*[] or [1]) + print(*[] or [1]) -print(**{1: 3} if False else {x: x for x in range(3)}) -print(*lambda x: x) -assert not Test, "Short message" @@ -532,13 +492,12 @@ last_call() - force=False -), "Short message" -assert parens is TooMany -+print(*NOT_YET_IMPLEMENTED_ExprStarred) +print( + **{1: 3} + if False + else {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} +) -+print(*NOT_YET_IMPLEMENTED_ExprStarred) ++print(*lambda x: True) +NOT_YET_IMPLEMENTED_StmtAssert +NOT_YET_IMPLEMENTED_StmtAssert +NOT_YET_IMPLEMENTED_StmtAssert @@ -664,23 +623,23 @@ str or None if (1 if True else 2) else str or bytes or None 2, 3, ] -[*NOT_YET_IMPLEMENTED_ExprStarred] -[*NOT_YET_IMPLEMENTED_ExprStarred] +[*a] +[*range(10)] [ - *NOT_YET_IMPLEMENTED_ExprStarred, + *a, 4, 5, ] [ 4, - *NOT_YET_IMPLEMENTED_ExprStarred, + *a, 5, ] [ this_is_a_very_long_variable_which_will_force_a_delimiter_split, element, another, - *NOT_YET_IMPLEMENTED_ExprStarred, + *more, ] {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} @@ -709,8 +668,8 @@ call( kwarg="hey", **kwargs, ) # note: no trailing comma pre-3.6 -call(*NOT_YET_IMPLEMENTED_ExprStarred) -call(a, *NOT_YET_IMPLEMENTED_ExprStarred) +call(*gidgets[:2]) +call(a, *gidgets[:2]) call(**self.screen_kwargs) call(b, **self.screen_kwargs) lukasz.langa.pl @@ -771,7 +730,7 @@ SomeName (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,) +(*starred,) { "id": "1", "type": "type", @@ -786,8 +745,8 @@ b = (1,) c = 1 d = (1,) + a + (2,) e = (1,).count(1) -f = 1, *NOT_YET_IMPLEMENTED_ExprStarred -g = 1, *NOT_YET_IMPLEMENTED_ExprStarred +f = 1, *range(10) +g = 1, *"ten" what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set( vars_to_remove ) @@ -823,13 +782,13 @@ async def f(): await some.complicated[0].call(with_args=(True or (1 is not 1))) -print(*NOT_YET_IMPLEMENTED_ExprStarred) +print(*[] or [1]) print( **{1: 3} if False else {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} ) -print(*NOT_YET_IMPLEMENTED_ExprStarred) +print(*lambda x: True) NOT_YET_IMPLEMENTED_StmtAssert NOT_YET_IMPLEMENTED_StmtAssert NOT_YET_IMPLEMENTED_StmtAssert 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 883baf2298..51aa7fb138 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 @@ -316,7 +316,7 @@ d={'a':1, # fmt: off - a , b = *hello - 'unformatted' -+ a, b = *NOT_YET_IMPLEMENTED_ExprStarred ++ a, b = *hello + "unformatted" # fmt: on @@ -507,7 +507,7 @@ def import_as_names(): def testlist_star_expr(): # fmt: off - a, b = *NOT_YET_IMPLEMENTED_ExprStarred + a, b = *hello "unformatted" # fmt: on diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__starred.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__starred.py.snap new file mode 100644 index 0000000000..57108edf12 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__starred.py.snap @@ -0,0 +1,61 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/starred.py +--- +## Input +```py +call( + # Leading starred comment + * # Trailing star comment + [ + # Leading value commnt + [What, i, this, s, very, long, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa] + ] # trailing value comment +) + +call( + # Leading starred comment + * ( # Leading value commnt + [What, i, this, s, very, long, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa] + ) # trailing value comment +) +``` + +## Output +```py +call( + # Leading starred comment + # Trailing star comment + *[ + # Leading value commnt + [ + What, + i, + this, + s, + very, + long, + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + ] + ] # trailing value comment +) + +call( + # Leading starred comment + # Leading value commnt + *( + [ + What, + i, + this, + s, + very, + long, + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + ] + ) # trailing value comment +) +``` + + + 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 52be9741ad..9f1f6eded7 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 (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []): pass -with (a, *NOT_YET_IMPLEMENTED_ExprStarred): +with (a, *b): pass ``` From 1782fb8c304d3dd68a91f8b787063a12cfc919d2 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Tue, 11 Jul 2023 07:35:51 +0100 Subject: [PATCH 407/447] format ExprListComp (#5600) Co-authored-by: Micha Reiser --- .../test/fixtures/ruff/statement/list_comp.py | 32 +++++ .../src/comments/placement.rs | 132 ++++++++++++++++++ .../src/expression/expr_list_comp.rs | 33 ++++- .../src/other/comprehension.rs | 83 ++++++++++- ...lack_compatibility@py_37__python37.py.snap | 21 ++- ...black_compatibility@py_38__pep_572.py.snap | 10 +- ...patibility@simple_cases__comments2.py.snap | 86 ++++++++---- ...patibility@simple_cases__comments3.py.snap | 32 +++-- ...atibility@simple_cases__expression.py.snap | 41 +++--- ...ity@simple_cases__power_op_spacing.py.snap | 31 ++-- ...compatibility@simple_cases__slices.py.snap | 22 +-- .../format@expression__binary.py.snap | 8 +- .../format@statement__list_comp.py.snap | 86 ++++++++++++ 13 files changed, 489 insertions(+), 128 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/list_comp.py create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@statement__list_comp.py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/list_comp.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/list_comp.py new file mode 100644 index 0000000000..4684d4dd0e --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/list_comp.py @@ -0,0 +1,32 @@ +[i for i in []] + +[i for i in [1,]] + +[ + a # a + for # for + c # c + in # in + e # e +] + +[ + # above a + a # a + # above for + for # for + # above c + c # c + # above in + in # in + # above e + e # e + # above if + if # if + # above f + f # f + # above if2 + if # if2 + # above g + g # g +] diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index bc8df853de..b82d4947c4 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -38,6 +38,7 @@ pub(super) fn place_comment<'a>( handle_slice_comments, handle_attribute_comment, handle_expr_if_comment, + handle_comprehension_comment, handle_trailing_expression_starred_star_end_of_line_comment, ]; for handler in HANDLERS { @@ -1244,6 +1245,137 @@ fn find_only_token_in_range(range: TextRange, locator: &Locator, token_kind: Tok token } +// Handle comments inside comprehensions, e.g. +// +// ```python +// [ +// a +// for # dangling on the comprehension +// b +// # dangling on the comprehension +// in # dangling on comprehension.iter +// # leading on the iter +// c +// # dangling on comprehension.if.n +// if # dangling on comprehension.if.n +// d +// ] +// ``` +fn handle_comprehension_comment<'a>( + comment: DecoratedComment<'a>, + locator: &Locator, +) -> CommentPlacement<'a> { + let AnyNodeRef::Comprehension(comprehension) = comment.enclosing_node() else { + return CommentPlacement::Default(comment); + }; + let is_own_line = comment.line_position().is_own_line(); + + // Comments between the `for` and target + // ```python + // [ + // a + // for # attache as dangling on the comprehension + // b in c + // ] + // ``` + if comment.slice().end() < comprehension.target.range().start() { + return if is_own_line { + // own line comments are correctly assigned as leading the target + CommentPlacement::Default(comment) + } else { + // after the `for` + CommentPlacement::dangling(comment.enclosing_node(), comment) + }; + } + + let in_token = find_only_token_in_range( + TextRange::new( + comprehension.target.range().end(), + comprehension.iter.range().start(), + ), + locator, + TokenKind::In, + ); + + // Comments between the target and the `in` + // ```python + // [ + // a for b + // # attach as dangling on the target + // # (to be rendered as leading on the "in") + // in c + // ] + // ``` + if comment.slice().start() < in_token.start() { + // attach as dangling comments on the target + // (to be rendered as leading on the "in") + return if is_own_line { + CommentPlacement::dangling(comment.enclosing_node(), comment) + } else { + // correctly trailing on the target + CommentPlacement::Default(comment) + }; + } + + // Comments between the `in` and the iter + // ```python + // [ + // a for b + // in # attach as dangling on the iter + // c + // ] + // ``` + if comment.slice().start() < comprehension.iter.range().start() { + return if is_own_line { + CommentPlacement::Default(comment) + } else { + // after the `in` but same line, turn into trailing on the `in` token + CommentPlacement::dangling((&comprehension.iter).into(), comment) + }; + } + + let mut last_end = comprehension.iter.range().end(); + + for if_node in &comprehension.ifs { + // ```python + // [ + // a + // for + // c + // in + // e + // # above if <-- find these own-line between previous and `if` token + // if # if <-- find these end-of-line between `if` and if node (`f`) + // # above f <-- already correctly assigned as leading `f` + // f # f <-- already correctly assigned as trailing `f` + // # above if2 + // if # if2 + // # above g + // g # g + // ] + // ``` + let if_token = find_only_token_in_range( + TextRange::new(last_end, if_node.range().start()), + locator, + TokenKind::If, + ); + if is_own_line { + if last_end < comment.slice().start() && comment.slice().start() < if_token.start() { + return CommentPlacement::dangling((if_node).into(), comment); + } + } else { + if if_token.start() < comment.slice().start() + && comment.slice().start() < if_node.range().start() + { + return CommentPlacement::dangling((if_node).into(), comment); + } + } + last_end = if_node.range().end(); + } + + CommentPlacement::Default(comment) +} + /// Returns `true` if `right` is `Some` and `left` and `right` are referentially equal. fn are_same_optional<'a, T>(left: AnyNodeRef, right: Option) -> bool where 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 5bc6a3017a..a6d5e236fd 100644 --- a/crates/ruff_python_formatter/src/expression/expr_list_comp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_list_comp.rs @@ -1,20 +1,41 @@ use crate::comments::Comments; use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, + default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, + Parenthesize, }; -use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::prelude::*; +use crate::AsFormat; +use crate::{FormatNodeRule, PyFormatter}; +use ruff_formatter::{format_args, write, Buffer, FormatResult}; use rustpython_parser::ast::ExprListComp; #[derive(Default)] pub struct FormatExprListComp; impl FormatNodeRule for FormatExprListComp { - fn fmt_fields(&self, _item: &ExprListComp, f: &mut PyFormatter) -> FormatResult<()> { + fn fmt_fields(&self, item: &ExprListComp, f: &mut PyFormatter) -> FormatResult<()> { + let ExprListComp { + range: _, + elt, + generators, + } = item; + + let joined = format_with(|f| { + f.join_with(soft_line_break_or_space()) + .entries(generators.iter().formatted()) + .finish() + }); + write!( f, - [not_yet_implemented_custom_text( - "[NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []]" + [parenthesized( + "[", + &format_args!( + group(&elt.format()), + soft_line_break_or_space(), + group(&joined) + ), + "]" )] ) } diff --git a/crates/ruff_python_formatter/src/other/comprehension.rs b/crates/ruff_python_formatter/src/other/comprehension.rs index edf64d8bee..022c5b82e6 100644 --- a/crates/ruff_python_formatter/src/other/comprehension.rs +++ b/crates/ruff_python_formatter/src/other/comprehension.rs @@ -1,12 +1,87 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::Comprehension; +use crate::comments::{leading_comments, trailing_comments}; +use crate::prelude::*; +use crate::AsFormat; +use crate::{FormatNodeRule, PyFormatter}; +use ruff_formatter::{format_args, write, Buffer, FormatResult}; +use rustpython_parser::ast::{Comprehension, Ranged}; #[derive(Default)] pub struct FormatComprehension; impl FormatNodeRule for FormatComprehension { fn fmt_fields(&self, item: &Comprehension, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let Comprehension { + range: _, + target, + iter, + ifs, + is_async, + } = item; + + let comments = f.context().comments().clone(); + + if *is_async { + write!(f, [text("async"), space()])?; + } + + let dangling_item_comments = comments.dangling_comments(item); + + let (before_target_comments, before_in_comments) = dangling_item_comments.split_at( + dangling_item_comments + .partition_point(|comment| comment.slice().end() < target.range().start()), + ); + + let trailing_in_comments = comments.dangling_comments(iter); + write!( + f, + [ + text("for"), + trailing_comments(before_target_comments), + group(&format_args!( + soft_line_break_or_space(), + target.format(), + soft_line_break_or_space(), + leading_comments(before_in_comments), + text("in"), + trailing_comments(trailing_in_comments), + soft_line_break_or_space(), + iter.format(), + )), + ] + )?; + if !ifs.is_empty() { + let joined = format_with(|f| { + let mut joiner = f.join_with(soft_line_break_or_space()); + for if_case in ifs { + let dangling_if_comments = comments.dangling_comments(if_case); + + let (own_line_if_comments, end_of_line_if_comments) = dangling_if_comments + .split_at( + dangling_if_comments + .partition_point(|comment| comment.line_position().is_own_line()), + ); + joiner.entry(&group(&format_args!( + leading_comments(own_line_if_comments), + text("if"), + trailing_comments(end_of_line_if_comments), + soft_line_break_or_space(), + if_case.format(), + ))); + } + joiner.finish() + }); + + write!(f, [soft_line_break_or_space(), group(&joined)])?; + } + Ok(()) + } + + fn fmt_dangling_comments( + &self, + _node: &Comprehension, + _f: &mut PyFormatter, + ) -> FormatResult<()> { + // dangling comments are formatted as part of fmt_fields + Ok(()) } } 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 785d395e09..8af05b54ea 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 @@ -42,7 +42,7 @@ def make_arange(n): ```diff --- Black +++ Ruff -@@ -2,29 +2,21 @@ +@@ -2,29 +2,27 @@ def f(): @@ -60,13 +60,16 @@ def make_arange(n): async def func(): if test: -- out_batched = [ -- i + out_batched = [ + i - async for i in aitertools._async_map( - self.async_inc, arange(8), batch_size=3 - ) -- ] -+ out_batched = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] ++ async for ++ i ++ in ++ aitertools._async_map(self.async_inc, arange(8), batch_size=3) + ] def awaited_generator_value(n): @@ -95,7 +98,13 @@ def g(): async def func(): if test: - out_batched = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] + out_batched = [ + i + async for + i + in + aitertools._async_map(self.async_inc, arange(8), batch_size=3) + ] def awaited_generator_value(n): 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 f64c729232..62a7d893b3 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 @@ -59,7 +59,7 @@ while x := f(x): ```diff --- Black +++ Ruff -@@ -2,10 +2,10 @@ +@@ -2,7 +2,7 @@ (a := a) if (match := pattern.search(data)) is None: pass @@ -67,11 +67,7 @@ while x := f(x): +if (match := pattern.search(data)): pass [y := f(x), y**2, y**3] --filtered_data = [y for x in data if (y := f(x)) is None] -+filtered_data = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] - (y := f(x)) - y0 = (y1 := f(x)) - foo(x=(y := f(x))) + filtered_data = [y for x in data if (y := f(x)) is None] @@ -19,10 +19,10 @@ pass @@ -122,7 +118,7 @@ if (match := pattern.search(data)) is None: if (match := pattern.search(data)): pass [y := f(x), y**2, y**3] -filtered_data = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] +filtered_data = [y for x in data if (y := f(x)) is None] (y := f(x)) y0 = (y1 := f(x)) foo(x=(y := f(x))) 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 e7ae03c930..ef5229fdb1 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 @@ -224,36 +224,43 @@ instruction()#comment with bad spacing if ( self._proc is not None # has the child process finished? -@@ -114,25 +122,9 @@ - # yup +@@ -115,7 +123,12 @@ arg3=True, ) -- lcomp = [ + lcomp = [ - element for element in collection if element is not None # yup # yup # right -- ] -- lcomp2 = [ -- # hello -- element -- # yup -- for element in collection -- # right ++ element # yup ++ for ++ element ++ in ++ collection # yup ++ if element is not None # right + ] + lcomp2 = [ + # hello +@@ -123,7 +136,9 @@ + # yup + for element in collection + # right - if element is not None -- ] -- lcomp3 = [ -- # This one is actually too long to fit in a single line. -- element.split("\n", 1)[0] -- # yup -- for element in collection.select_elements() -- # right ++ if ++ element ++ is not None + ] + lcomp3 = [ + # This one is actually too long to fit in a single line. +@@ -131,7 +146,9 @@ + # yup + for element in collection.select_elements() + # right - if element is not None -- ] -+ 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 []] ++ if ++ element ++ is not None + ] while True: if False: - continue -@@ -143,7 +135,10 @@ +@@ -143,7 +160,10 @@ # let's return return Node( syms.simple_stmt, @@ -265,7 +272,7 @@ instruction()#comment with bad spacing ) -@@ -158,7 +153,11 @@ +@@ -158,7 +178,11 @@ class Test: def _init_host(self, parsed) -> None: @@ -407,9 +414,34 @@ short # yup arg3=True, ) - 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 []] + lcomp = [ + element # yup + for + element + in + collection # yup + if element is not None # right + ] + lcomp2 = [ + # hello + element + # yup + for element in collection + # right + if + element + is not None + ] + lcomp3 = [ + # This one is actually too long to fit in a single line. + element.split("\n", 1)[0] + # yup + for element in collection.select_elements() + # right + if + element + is not None + ] 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 2812b9d37a..d41e20b68f 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 @@ -60,22 +60,17 @@ def func(): ```diff --- Black +++ Ruff -@@ -6,14 +6,7 @@ - x = """ - a really long string - """ -- lcomp3 = [ -- # This one is actually too long to fit in a single line. -- element.split("\n", 1)[0] -- # yup -- for element in collection.select_elements() -- # right +@@ -12,7 +12,9 @@ + # yup + for element in collection.select_elements() + # right - if element is not None -- ] -+ lcomp3 = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] ++ if ++ element ++ is not None + ] # Capture each of the exceptions in the MultiError along with each of their causes and contexts if isinstance(exc_value, MultiError): - embedded = [] ``` ## Ruff Output @@ -89,7 +84,16 @@ def func(): x = """ a really long string """ - lcomp3 = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] + lcomp3 = [ + # This one is actually too long to fit in a single line. + element.split("\n", 1)[0] + # yup + for element in collection.select_elements() + # right + if + element + is not None + ] # 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 b8aff61aa7..8fc2eb367f 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 @@ -324,10 +324,14 @@ last_call() -{(i**2) for i in (1, 2, 3)} -{(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)} --[i for i in (1, 2, 3)] --[(i**2) for i in (1, 2, 3)] --[(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)] ++{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} ++{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} + [i for i in (1, 2, 3)] + [(i**2) for i in (1, 2, 3)] + [(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)] -{i: 0 for i in (1, 2, 3)} -{i: j for i, j in ((1, "a"), (2, "b"), (3, "c"))} -{a: b * 2 for a, b in dictionary.items()} @@ -336,14 +340,6 @@ last_call() - k: v - for k, v in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension -} -+{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} -+{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} -+[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} @@ -369,9 +365,6 @@ last_call() - int, - float, - dict[str, int], --] --very_long_variable_name_filters: t.List[ -- t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], + ( + str, + int, @@ -379,6 +372,9 @@ last_call() + dict[str, int], + ) ] +-very_long_variable_name_filters: t.List[ +- t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], +-] -xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore - sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) -) @@ -433,10 +429,11 @@ last_call() (*starred,) { "id": "1", -@@ -208,24 +202,14 @@ +@@ -207,25 +201,15 @@ + ) 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( @@ -444,7 +441,7 @@ last_call() - ) - .order_by(models.Customer.id.asc()) - .all() --) + ) -result = ( - session.query(models.Customer.id) - .filter( @@ -645,10 +642,10 @@ str or None if (1 if True else 2) else str or bytes or None {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} -[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 []] +[i for i in (1, 2, 3)] +[(i**2) for i in (1, 2, 3)] +[(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)] {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} 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 40cab8fa6b..d315462fd0 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 @@ -75,7 +75,7 @@ return np.divide( ```diff --- Black +++ Ruff -@@ -15,38 +15,38 @@ +@@ -15,7 +15,7 @@ b = 5 ** f() c = -(5**2) d = 5 ** f["hi"] @@ -84,21 +84,16 @@ return np.divide( f = f() ** 5 g = a.b**c.d h = 5 ** funcs.f() - i = funcs.f() ** 5 - j = super().name ** 5 --k = [(2**idx, value) for idx, value in pairs] -+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) +@@ -26,7 +26,7 @@ m = [([2**63], [1, 2**63])] n = count <= 10**5 o = settings(max_examples=10**6) -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 = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] + q = [10**i for i in range(6)] r = x**y - a = 5.0**~4.0 +@@ -34,7 +34,7 @@ b = 5.0 ** f() c = -(5.0**2.0) d = 5.0 ** f["hi"] @@ -107,21 +102,15 @@ return np.divide( f = f() ** 5.0 g = a.b**c.d h = 5.0 ** funcs.f() - i = funcs.f() ** 5.0 - j = super().name ** 5.0 --k = [(2.0**idx, value) for idx, value in pairs] -+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) +@@ -45,7 +45,7 @@ m = [([2.0**63.0], [1.0, 2**63.0])] n = count <= 10**5.0 o = settings(max_examples=10**6.0) -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 = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] + q = [10.5**i for i in range(6)] - # WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873) @@ -55,9 +55,11 @@ view.variance, # type: ignore[union-attr] view.sum_of_weights, # type: ignore[union-attr] @@ -164,13 +153,13 @@ g = a.b**c.d h = 5 ** funcs.f() i = funcs.f() ** 5 j = super().name ** 5 -k = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] +k = [(2**idx, value) for idx, value in pairs] 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 = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] +q = [10**i for i in range(6)] r = x**y a = 5.0**~4.0 @@ -183,13 +172,13 @@ g = a.b**c.d h = 5.0 ** funcs.f() i = funcs.f() ** 5.0 j = super().name ** 5.0 -k = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] +k = [(2.0**idx, value) for idx, value in pairs] 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 = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] +q = [10.5**i for i in range(6)] # 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 cfa0bebf04..848f65c25e 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,34 @@ +@@ -4,19 +4,21 @@ slice[d::d] slice[0] slice[-1] @@ -68,19 +68,11 @@ ham[lower + offset : upper + offset] +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 []] -+] ++slice[ :: [i for i in range(42)]] async def f(): -- slice[await x : [i async for i in arange(42)] : 42] -+ slice[ -+ await x : [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] : 42 -+ ] - - - # These are from PEP-8: +@@ -27,5 +29,5 @@ ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:] ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] @@ -112,15 +104,11 @@ slice[not so_simple : 1 < val <= 10] 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 []] -] +slice[ :: [i for i in range(42)]] async def f(): - slice[ - await x : [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] : 42 - ] + slice[await x : [i async for i in arange(42)] : 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 40a69332f5..890bfd41cb 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,10 +277,10 @@ aaaaaaaaaaaaaa + { dddddddddddddddd, eeeeeee, } -( - aaaaaaaaaaaaaa - + [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] -) +aaaaaaaaaaaaaa + [ + a + for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +] ( aaaaaaaaaaaaaa + (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__list_comp.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__list_comp.py.snap new file mode 100644 index 0000000000..36ef7fa6eb --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__list_comp.py.snap @@ -0,0 +1,86 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/list_comp.py +--- +## Input +```py +[i for i in []] + +[i for i in [1,]] + +[ + a # a + for # for + c # c + in # in + e # e +] + +[ + # above a + a # a + # above for + for # for + # above c + c # c + # above in + in # in + # above e + e # e + # above if + if # if + # above f + f # f + # above if2 + if # if2 + # above g + g # g +] +``` + +## Output +```py +[i for i in []] + +[ + i + for + i + in + [ + 1, + ] +] + +[ + a # a + for # for + c # c + in # in + e # e +] + +[ + # above a + a # a + # above for + for # for + # above c + c # c + # above in + in # in + # above e + e # e + # above if + if # if + # above f + f # f + # above if2 + if # if2 + # above g + g # g +] +``` + + + From 15c7b6bcf7acb9b6518a5efb92b81b1f5e0ee7f3 Mon Sep 17 00:00:00 2001 From: Chris Pryer <14341145+cnpryer@users.noreply.github.com> Date: Tue, 11 Jul 2023 02:36:26 -0400 Subject: [PATCH 408/447] Format `delete` statement (#5169) --- .../test/fixtures/ruff/statement/delete.py | 72 ++++++ .../src/statement/stmt_delete.rs | 42 +++- .../format@statement__delete.py.snap | 217 ++++++++++++++++++ 3 files changed, 328 insertions(+), 3 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/delete.py create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@statement__delete.py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/delete.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/delete.py new file mode 100644 index 0000000000..fa5efd652a --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/delete.py @@ -0,0 +1,72 @@ +x = 1 +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = 1 +a, b, c, d = 1, 2, 3, 4 + +del a, b, c, d +del a, b, c, d # Trailing + +del a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a +del a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a # Trailing + +del ( + a, + a +) + +del ( + # Dangling comment +) + +# Delete something +del x # Deleted something +# Done deleting + +# Delete something +del ( + # Deleting something + x # Deleted something + # Finishing deletes +) # Completed +# Done deleting + +# Delete something +del ( + # Deleting something + x # Deleted something + # Finishing deletes +) # Completed +# Done deleting + +# Delete something +del ( + # Deleting something + x, # Deleted something + # Finishing deletes +) # Completed +# Done deleting + +# Delete something +del ( + # Deleting something + x # Deleted something + # Finishing deletes + # Dangling comment +) # Completed +# Done deleting + +# NOTE: This shouldn't format. See https://github.com/astral-sh/ruff/issues/5630. +# Delete something +del x, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, b, c, d # Delete these +# Ready to delete + +# Delete something +del ( + x, + # Deleting this + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + b, + c, + d, + # Deleted +) # Completed +# Done diff --git a/crates/ruff_python_formatter/src/statement/stmt_delete.rs b/crates/ruff_python_formatter/src/statement/stmt_delete.rs index 0a75dcd016..e53ff784c7 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_delete.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_delete.rs @@ -1,5 +1,9 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::builders::{optional_parentheses, PyFormatterExtensions}; +use crate::comments::dangling_node_comments; +use crate::expression::parentheses::Parenthesize; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{block_indent, format_with, space, text}; +use ruff_formatter::{write, Buffer, Format, FormatResult}; use rustpython_parser::ast::StmtDelete; #[derive(Default)] @@ -7,6 +11,38 @@ pub struct FormatStmtDelete; impl FormatNodeRule for FormatStmtDelete { fn fmt_fields(&self, item: &StmtDelete, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let StmtDelete { range: _, targets } = item; + + write!(f, [text("del"), space()])?; + + match targets.as_slice() { + [] => { + write!( + f, + [ + // Handle special case of delete statements without targets. + // ``` + // del ( + // # Dangling comment + // ) + &text("("), + block_indent(&dangling_node_comments(item)), + &text(")"), + ] + ) + } + [single] => { + write!(f, [single.format().with_options(Parenthesize::IfBreaks)]) + } + targets => { + let item = format_with(|f| f.join_comma_separated().nodes(targets.iter()).finish()); + optional_parentheses(&item).fmt(f) + } + } + } + + fn fmt_dangling_comments(&self, _node: &StmtDelete, _f: &mut PyFormatter) -> FormatResult<()> { + // Handled in `fmt_fields` + Ok(()) } } diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__delete.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__delete.py.snap new file mode 100644 index 0000000000..9ccbf27444 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__delete.py.snap @@ -0,0 +1,217 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/delete.py +--- +## Input +```py +x = 1 +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = 1 +a, b, c, d = 1, 2, 3, 4 + +del a, b, c, d +del a, b, c, d # Trailing + +del a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a +del a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a # Trailing + +del ( + a, + a +) + +del ( + # Dangling comment +) + +# Delete something +del x # Deleted something +# Done deleting + +# Delete something +del ( + # Deleting something + x # Deleted something + # Finishing deletes +) # Completed +# Done deleting + +# Delete something +del ( + # Deleting something + x # Deleted something + # Finishing deletes +) # Completed +# Done deleting + +# Delete something +del ( + # Deleting something + x, # Deleted something + # Finishing deletes +) # Completed +# Done deleting + +# Delete something +del ( + # Deleting something + x # Deleted something + # Finishing deletes + # Dangling comment +) # Completed +# Done deleting + +# NOTE: This shouldn't format. See https://github.com/astral-sh/ruff/issues/5630. +# Delete something +del x, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, b, c, d # Delete these +# Ready to delete + +# Delete something +del ( + x, + # Deleting this + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + b, + c, + d, + # Deleted +) # Completed +# Done +``` + +## Output +```py +x = 1 +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = 1 +a, b, c, d = 1, 2, 3, 4 + +del a, b, c, d +del a, b, c, d # Trailing + +del ( + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, +) +del ( + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, +) # Trailing + +del (a, a) + +del ( + # Dangling comment +) + +# Delete something +del x # Deleted something +# Done deleting + +# Delete something +del ( + # Deleting something + x # Deleted something + # Finishing deletes +) # Completed +# Done deleting + +# Delete something +del ( + # Deleting something + x # Deleted something + # Finishing deletes +) # Completed +# Done deleting + +# Delete something +del ( + # Deleting something + x, # Deleted something + # Finishing deletes +) # Completed +# Done deleting + +# Delete something +del ( + # Deleting something + x # Deleted something + # Finishing deletes + # Dangling comment +) # Completed +# Done deleting + +# NOTE: This shouldn't format. See https://github.com/astral-sh/ruff/issues/5630. +# Delete something +del x, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, b, c, d # Delete these +# Ready to delete + +# Delete something +del ( + x, + # Deleting this + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + b, + c, + d, + # Deleted +) # Completed +# Done +``` + + + From b7794f855b3363169212ac33c879c4d5e67948bc Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 11 Jul 2023 09:06:23 +0200 Subject: [PATCH 409/447] Format StmtAugAssign (#5655) ## Summary Format statements such as `tree_depth += 1`. This is a statement that does not allow any line breaks, the only thing to be mindful of is to parenthesize the assigned expression Jaccard index on django: 0.915 -> 0.918 ## Test Plan black tests, and two new tests, a basic one and one that ensures that the child gets parentheses. I ran the django stability check. --- .../fixtures/ruff/statement/ann_assign.py | 5 ++++ .../src/statement/stmt_aug_assign.rs | 22 ++++++++++++++-- ...ility@miscellaneous__debug_visitor.py.snap | 10 +++----- ...aneous__long_strings_flag_disabled.py.snap | 10 +------- ...y@py_310__pattern_matching_generic.py.snap | 11 +------- .../format@statement__ann_assign.py.snap | 25 +++++++++++++++++++ 6 files changed, 56 insertions(+), 27 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/ann_assign.py create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/ann_assign.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/ann_assign.py new file mode 100644 index 0000000000..6c9d3155f6 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/ann_assign.py @@ -0,0 +1,5 @@ +tree_depth += 1 + +greeting += "This is very long, formal greeting for whomever is name here. Dear %s, it will break the line" % len( + name +) diff --git a/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs index 8db5a6a8d4..c09c58ed62 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs @@ -1,4 +1,6 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; +use crate::expression::parentheses::Parenthesize; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{space, text}; use ruff_formatter::{write, Buffer, FormatResult}; use rustpython_parser::ast::StmtAugAssign; @@ -7,6 +9,22 @@ pub struct FormatStmtAugAssign; impl FormatNodeRule for FormatStmtAugAssign { fn fmt_fields(&self, item: &StmtAugAssign, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let StmtAugAssign { + target, + op, + value, + range: _, + } = item; + write!( + f, + [ + target.format(), + space(), + op.format(), + text("="), + space(), + value.format().with_options(Parenthesize::IfBreaks) + ] + ) } } diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap index 0e00e30129..eb2adafb48 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap @@ -56,16 +56,14 @@ class DebugVisitor(Visitor[T]): if isinstance(node, Node): _type = type_repr(node.type) - out(f'{indent}{_type}', fg='yellow') -- self.tree_depth += 1 + out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="yellow") -+ NOT_YET_IMPLEMENTED_StmtAugAssign + self.tree_depth += 1 for child in node.children: - yield from self.visit(child) + NOT_YET_IMPLEMENTED_ExprYieldFrom -- self.tree_depth -= 1 + self.tree_depth -= 1 - out(f'{indent}/{_type}', fg='yellow', bold=False) -+ NOT_YET_IMPLEMENTED_StmtAugAssign + out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="yellow", bold=False) else: _type = token.tok_name.get(node.type, str(node.type)) @@ -102,11 +100,11 @@ class DebugVisitor(Visitor[T]): if isinstance(node, Node): _type = type_repr(node.type) out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="yellow") - NOT_YET_IMPLEMENTED_StmtAugAssign + self.tree_depth += 1 for child in node.children: NOT_YET_IMPLEMENTED_ExprYieldFrom - NOT_YET_IMPLEMENTED_StmtAugAssign + self.tree_depth -= 1 out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="yellow", bold=False) else: _type = token.tok_name.get(node.type, str(node.type)) 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 a3f36ae2e6..b4d5258ad2 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 @@ -304,14 +304,6 @@ long_unmergable_string_with_pragma = ( ```diff --- Black +++ Ruff -@@ -1,6 +1,6 @@ - x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." - --x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." -+NOT_YET_IMPLEMENTED_StmtAugAssign - - y = "Short string" - @@ -70,8 +70,8 @@ bad_split3 = ( "What if we have inline comments on " # First Comment @@ -471,7 +463,7 @@ long_unmergable_string_with_pragma = ( ```py x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." -NOT_YET_IMPLEMENTED_StmtAugAssign +x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." y = "Short string" 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 0f93e60a69..7c2a145131 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 @@ -187,15 +187,6 @@ 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 +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"): -- src_txt += "\n" -+ NOT_YET_IMPLEMENTED_StmtAugAssign - - grammars = get_grammars(set(target_versions)) - @@ -99,9 +91,9 @@ re.match() match = a @@ -298,7 +289,7 @@ def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: 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"): - NOT_YET_IMPLEMENTED_StmtAugAssign + src_txt += "\n" grammars = get_grammars(set(target_versions)) diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap new file mode 100644 index 0000000000..9385c1b1ba --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap @@ -0,0 +1,25 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/ann_assign.py +--- +## Input +```py +tree_depth += 1 + +greeting += "This is very long, formal greeting for whomever is name here. Dear %s, it will break the line" % len( + name +) +``` + +## Output +```py +tree_depth += 1 + +greeting += ( + "This is very long, formal greeting for whomever is name here. Dear %s, it will break the line" + % len(name) +) +``` + + + From 4b58a9c0926327cdcfcb0e68d2facfeeb6c73bc3 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Tue, 11 Jul 2023 09:00:10 +0100 Subject: [PATCH 410/447] formatter: tidy: list_comp is an expression, not a statement (#5677) --- .../test/fixtures/ruff/{statement => expression}/list_comp.py | 0 ...__list_comp.py.snap => format@expression__list_comp.py.snap} | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename crates/ruff_python_formatter/resources/test/fixtures/ruff/{statement => expression}/list_comp.py (100%) rename crates/ruff_python_formatter/tests/snapshots/{format@statement__list_comp.py.snap => format@expression__list_comp.py.snap} (96%) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/list_comp.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list_comp.py similarity index 100% rename from crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/list_comp.py rename to crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list_comp.py diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__list_comp.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__list_comp.py.snap similarity index 96% rename from crates/ruff_python_formatter/tests/snapshots/format@statement__list_comp.py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@expression__list_comp.py.snap index 36ef7fa6eb..dd17b1d97f 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__list_comp.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__list_comp.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/list_comp.py +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list_comp.py --- ## Input ```py From 212fd86bf08fa3210ebd898f434f75e8c50b152e Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 11 Jul 2023 13:03:44 +0200 Subject: [PATCH 411/447] Switch from jaccard index to similarity index (#5679) ## Summary The similarity index, the fraction of unchanged lines, is easier to understand than the jaccard index, the fraction between intersection and union. ## Test Plan I ran this on django and git a 0.945 index, meaning 5.5% of lines are currently reformatted when compared to black --- crates/ruff_dev/src/format_dev.rs | 39 ++++++++++++++++++-------- crates/ruff_python_formatter/README.md | 10 +++---- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/crates/ruff_dev/src/format_dev.rs b/crates/ruff_dev/src/format_dev.rs index d4bf4157be..566efb8290 100644 --- a/crates/ruff_dev/src/format_dev.rs +++ b/crates/ruff_dev/src/format_dev.rs @@ -48,16 +48,27 @@ fn ruff_check_paths(dirs: &[PathBuf]) -> anyhow::Result f32 { self.intersection as f32 / (self.black_input + self.ruff_output + self.intersection) as f32 } + + #[allow(clippy::cast_precision_loss)] + pub(crate) fn similarity_index(&self) -> f32 { + self.intersection as f32 / (self.black_input + self.intersection) as f32 + } } impl Add for Statistics { @@ -171,10 +188,10 @@ pub(crate) fn main(args: &Args) -> anyhow::Result { { print!("{}", result.display(args.format)); println!( - "Found {} stability errors in {} files (jaccard index {:.3}) in {:.2}s", + "Found {} stability errors in {} files (similarity index {:.3}) in {:.2}s", error_count, result.file_count, - result.statistics.jaccard_index(), + result.statistics.similarity_index(), result.duration.as_secs_f32(), ); } @@ -253,10 +270,10 @@ fn format_dev_multi_project(args: &Args) -> bool { total_files += result.file_count; bar.println(format!( - "Finished {} with {} files (jaccard index {:.3}) in {:.2}s", + "Finished {} with {} files (similarity index {:.3}) in {:.2}s", path.display(), result.file_count, - result.statistics.jaccard_index(), + result.statistics.similarity_index(), result.duration.as_secs_f32(), )); bar.println(result.display(args.format).to_string().trim_end()); diff --git a/crates/ruff_python_formatter/README.md b/crates/ruff_python_formatter/README.md index ab7c451e2c..9c2d918e19 100644 --- a/crates/ruff_python_formatter/README.md +++ b/crates/ruff_python_formatter/README.md @@ -249,11 +249,11 @@ It's possible to format an entire project: cargo run --bin ruff_dev -- format-dev --write my_project ``` -This will format all files that `ruff check` would lint and computes the -[Jaccard index](https://en.wikipedia.org/wiki/Jaccard_index), a measure for how close the original -and formatted versions are. The Jaccard index is 1 if there were no changes at all, while 0 means -every line was changed. If you run this on a black formatted projects, this tells you how similar -the ruff formatter is to black for the given project, with our goal being as close to 1 as possible. +This will format all files that `ruff check` would lint and computes the similarity index, the +fraction of changed lines. The similarity index is 1 if there were no changes at all, while 0 means +we changed every single line. If you run this on a black formatted projects, this tells you how +similar the ruff formatter is to black for the given project, with our goal being as close to 1 as +possible. There are three 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 From d30e9125eb216ddc0bcb056629aaad1592dae87c Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 11 Jul 2023 13:20:04 +0200 Subject: [PATCH 412/447] Extend formatter IR to support Black's expression formatting (#5596) --- crates/ruff_formatter/src/builders.rs | 211 ++++++++++++++++- .../src/format_element/document.rs | 101 +++++++- .../ruff_formatter/src/format_element/tag.rs | 99 +++++++- crates/ruff_formatter/src/printer/mod.rs | 215 ++++++++++++++---- .../src/comments/debug.rs | 6 +- 5 files changed, 576 insertions(+), 56 deletions(-) diff --git a/crates/ruff_formatter/src/builders.rs b/crates/ruff_formatter/src/builders.rs index 441fb00a95..29a3f914ea 100644 --- a/crates/ruff_formatter/src/builders.rs +++ b/crates/ruff_formatter/src/builders.rs @@ -1410,7 +1410,7 @@ impl Format for Group<'_, Context> { impl std::fmt::Debug for Group<'_, Context> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("GroupElements") + f.debug_struct("Group") .field("group_id", &self.group_id) .field("should_expand", &self.should_expand) .field("content", &"{{content}}") @@ -1418,6 +1418,135 @@ impl std::fmt::Debug for Group<'_, Context> { } } +/// Sets the `condition` for the group. The element will behave as a regular group if `condition` is met, +/// and as *ungrouped* content if the condition is not met. +/// +/// ## Examples +/// +/// Only expand before operators if the parentheses are necessary. +/// +/// ``` +/// # use ruff_formatter::prelude::*; +/// # use ruff_formatter::{format, format_args, LineWidth, SimpleFormatOptions}; +/// +/// # fn main() -> FormatResult<()> { +/// use ruff_formatter::Formatted; +/// let content = format_with(|f| { +/// let parentheses_id = f.group_id("parentheses"); +/// group(&format_args![ +/// if_group_breaks(&text("(")), +/// indent_if_group_breaks(&format_args![ +/// soft_line_break(), +/// conditional_group(&format_args![ +/// text("'aaaaaaa'"), +/// soft_line_break_or_space(), +/// text("+"), +/// space(), +/// fits_expanded(&conditional_group(&format_args![ +/// text("["), +/// soft_block_indent(&format_args![ +/// text("'Good morning!',"), +/// soft_line_break_or_space(), +/// text("'How are you?'"), +/// ]), +/// text("]"), +/// ], tag::Condition::if_group_fits_on_line(parentheses_id))), +/// soft_line_break_or_space(), +/// text("+"), +/// space(), +/// conditional_group(&format_args![ +/// text("'bbbb'"), +/// soft_line_break_or_space(), +/// text("and"), +/// space(), +/// text("'c'") +/// ], tag::Condition::if_group_fits_on_line(parentheses_id)) +/// ], tag::Condition::if_breaks()), +/// ], parentheses_id), +/// soft_line_break(), +/// if_group_breaks(&text(")")) +/// ]) +/// .with_group_id(Some(parentheses_id)) +/// .fmt(f) +/// }); +/// +/// let formatted = format!(SimpleFormatContext::default(), [content])?; +/// let document = formatted.into_document(); +/// +/// // All content fits +/// let all_fits = Formatted::new(document.clone(), SimpleFormatContext::new(SimpleFormatOptions { +/// line_width: LineWidth::try_from(65).unwrap(), +/// ..SimpleFormatOptions::default() +/// })); +/// +/// assert_eq!( +/// "'aaaaaaa' + ['Good morning!', 'How are you?'] + 'bbbb' and 'c'", +/// all_fits.print()?.as_code() +/// ); +/// +/// // The parentheses group fits, because it can expand the list, +/// let list_expanded = Formatted::new(document.clone(), SimpleFormatContext::new(SimpleFormatOptions { +/// line_width: LineWidth::try_from(21).unwrap(), +/// ..SimpleFormatOptions::default() +/// })); +/// +/// assert_eq!( +/// "'aaaaaaa' + [\n\t'Good morning!',\n\t'How are you?'\n] + 'bbbb' and 'c'", +/// list_expanded.print()?.as_code() +/// ); +/// +/// // It is necessary to split all groups to fit the content +/// let all_expanded = Formatted::new(document, SimpleFormatContext::new(SimpleFormatOptions { +/// line_width: LineWidth::try_from(11).unwrap(), +/// ..SimpleFormatOptions::default() +/// })); +/// +/// assert_eq!( +/// "(\n\t'aaaaaaa'\n\t+ [\n\t\t'Good morning!',\n\t\t'How are you?'\n\t]\n\t+ 'bbbb'\n\tand 'c'\n)", +/// all_expanded.print()?.as_code() +/// ); +/// # Ok(()) +/// # } +/// ``` +#[inline] +pub fn conditional_group( + content: &Content, + condition: Condition, +) -> ConditionalGroup +where + Content: Format, +{ + ConditionalGroup { + content: Argument::new(content), + condition, + } +} + +#[derive(Clone)] +pub struct ConditionalGroup<'content, Context> { + content: Argument<'content, Context>, + condition: Condition, +} + +impl Format for ConditionalGroup<'_, Context> { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + f.write_element(FormatElement::Tag(StartConditionalGroup( + tag::ConditionalGroup::new(self.condition), + )))?; + f.write_fmt(Arguments::from(&self.content))?; + f.write_element(FormatElement::Tag(EndConditionalGroup)) + } +} + +impl std::fmt::Debug for ConditionalGroup<'_, Context> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ConditionalGroup") + .field("condition", &self.condition) + .field("content", &"{{content}}") + .finish() + } +} + /// IR element that forces the parent group to print in expanded mode. /// /// Has no effect if used outside of a group or element that introduce implicit groups (fill element). @@ -1841,6 +1970,86 @@ impl std::fmt::Debug for IndentIfGroupBreaks<'_, Context> { } } +/// Changes the definition of *fits* for `content`. Instead of measuring it in *flat*, measure it with +/// all line breaks expanded and test if no line exceeds the line width. The [`FitsExpanded`] acts +/// as a expands boundary similar to best fitting, meaning that a [hard_line_break] will not cause the parent group to expand. +/// +/// Useful in conjunction with a group with a condition. +/// +/// ## Examples +/// The outer group with the binary expression remains *flat* regardless of the array expression +/// that spans multiple lines. +/// +/// ``` +/// # use ruff_formatter::{format, format_args, LineWidth, SimpleFormatOptions, write}; +/// # use ruff_formatter::prelude::*; +/// +/// # fn main() -> FormatResult<()> { +/// let content = format_with(|f| { +/// let group_id = f.group_id("header"); +/// +/// write!(f, [ +/// group(&format_args![ +/// text("a"), +/// soft_line_break_or_space(), +/// text("+"), +/// space(), +/// fits_expanded(&group(&format_args![ +/// text("["), +/// soft_block_indent(&format_args![ +/// text("a,"), space(), text("# comment"), expand_parent(), soft_line_break_or_space(), +/// text("b") +/// ]), +/// text("]") +/// ])) +/// ]), +/// ]) +/// }); +/// +/// let formatted = format!(SimpleFormatContext::default(), [content])?; +/// +/// assert_eq!( +/// "a + [\n\ta, # comment\n\tb\n]", +/// formatted.print()?.as_code() +/// ); +/// # Ok(()) +/// # } +/// ``` +pub fn fits_expanded(content: &Content) -> FitsExpanded +where + Content: Format, +{ + FitsExpanded { + content: Argument::new(content), + condition: None, + } +} + +#[derive(Clone)] +pub struct FitsExpanded<'a, Context> { + content: Argument<'a, Context>, + condition: Option, +} + +impl FitsExpanded<'_, Context> { + /// Sets a `condition` to when the content should fit in expanded mode. The content uses the regular fits + /// definition if the `condition` is not met. + pub fn with_condition(mut self, condition: Option) -> Self { + self.condition = condition; + self + } +} + +impl Format for FitsExpanded<'_, Context> { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + f.write_element(FormatElement::Tag(StartFitsExpanded( + tag::FitsExpanded::new().with_condition(self.condition), + )))?; + f.write_fmt(Arguments::from(&self.content))?; + f.write_element(FormatElement::Tag(EndFitsExpanded)) + } +} + /// Utility for formatting some content with an inline lambda function. #[derive(Copy, Clone)] pub struct FormatWith { diff --git a/crates/ruff_formatter/src/format_element/document.rs b/crates/ruff_formatter/src/format_element/document.rs index 68baa0558e..1f031f0e1d 100644 --- a/crates/ruff_formatter/src/format_element/document.rs +++ b/crates/ruff_formatter/src/format_element/document.rs @@ -1,5 +1,5 @@ use super::tag::Tag; -use crate::format_element::tag::DedentMode; +use crate::format_element::tag::{Condition, DedentMode}; use crate::prelude::tag::GroupMode; use crate::prelude::*; use crate::printer::LineEnding; @@ -33,12 +33,17 @@ impl Document { #[derive(Debug)] enum Enclosing<'a> { Group(&'a tag::Group), + ConditionalGroup(&'a tag::ConditionalGroup), + FitsExpanded(&'a tag::FitsExpanded), BestFitting, } fn expand_parent(enclosing: &[Enclosing]) { - if let Some(Enclosing::Group(group)) = enclosing.last() { - group.propagate_expand(); + match enclosing.last() { + Some(Enclosing::Group(group)) => group.propagate_expand(), + Some(Enclosing::ConditionalGroup(group)) => group.propagate_expand(), + Some(Enclosing::FitsExpanded(fits_expanded)) => fits_expanded.propagate_expand(), + _ => {} } } @@ -58,6 +63,14 @@ impl Document { Some(Enclosing::Group(group)) => !group.mode().is_flat(), _ => false, }, + FormatElement::Tag(Tag::StartConditionalGroup(group)) => { + enclosing.push(Enclosing::ConditionalGroup(group)); + false + } + FormatElement::Tag(Tag::EndConditionalGroup) => match enclosing.pop() { + Some(Enclosing::ConditionalGroup(group)) => !group.mode().is_flat(), + _ => false, + }, FormatElement::Interned(interned) => match checked_interned.get(interned) { Some(interned_expands) => *interned_expands, None => { @@ -79,6 +92,16 @@ impl Document { enclosing.pop(); continue; } + FormatElement::Tag(Tag::StartFitsExpanded(fits_expanded)) => { + enclosing.push(Enclosing::FitsExpanded(fits_expanded)); + false + } + FormatElement::Tag(Tag::EndFitsExpanded) => { + enclosing.pop(); + // Fits expanded acts as a boundary + expands = false; + continue; + } FormatElement::StaticText { text } => text.contains('\n'), FormatElement::DynamicText { text, .. } => text.contains('\n'), FormatElement::SourceCodeSlice { @@ -441,6 +464,29 @@ impl Format> for &[FormatElement] { } } + StartConditionalGroup(group) => { + write!( + f, + [ + text("conditional_group(condition:"), + space(), + group.condition(), + text(","), + space() + ] + )?; + + match group.mode() { + GroupMode::Flat => {} + GroupMode::Expand => { + write!(f, [text("expand: true,"), space()])?; + } + GroupMode::Propagated => { + write!(f, [text("expand: propagated,"), space()])?; + } + } + } + StartIndentIfGroupBreaks(id) => { write!( f, @@ -491,6 +537,28 @@ impl Format> for &[FormatElement] { write!(f, [text("fill(")])?; } + StartFitsExpanded(tag::FitsExpanded { + condition, + propagate_expand, + }) => { + write!(f, [text("fits_expanded(propagate_expand:"), space()])?; + + if propagate_expand.get() { + write!(f, [text("true")])?; + } else { + write!(f, [text("false")])?; + } + + write!(f, [text(","), space()])?; + + if let Some(condition) = condition { + write!( + f, + [text("condition:"), space(), condition, text(","), space()] + )?; + } + } + StartEntry => { // handled after the match for all start tags } @@ -503,8 +571,10 @@ impl Format> for &[FormatElement] { | EndAlign | EndIndent | EndGroup + | EndConditionalGroup | EndLineSuffix | EndDedent + | EndFitsExpanded | EndVerbatim => { write!(f, [ContentArrayEnd, text(")")])?; } @@ -658,6 +728,31 @@ impl FormatElements for [FormatElement] { } } +impl Format> for Condition { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + match (self.mode, self.group_id) { + (PrintMode::Flat, None) => write!(f, [text("if_fits_on_line")]), + (PrintMode::Flat, Some(id)) => write!( + f, + [ + text("if_group_fits_on_line("), + dynamic_text(&std::format!("\"{id:?}\""), None), + text(")") + ] + ), + (PrintMode::Expanded, None) => write!(f, [text("if_breaks")]), + (PrintMode::Expanded, Some(id)) => write!( + f, + [ + text("if_group_breaks("), + dynamic_text(&std::format!("\"{id:?}\""), None), + text(")") + ] + ), + } + } +} + #[cfg(test)] mod tests { use crate::prelude::*; diff --git a/crates/ruff_formatter/src/format_element/tag.rs b/crates/ruff_formatter/src/format_element/tag.rs index a443f909f0..f586cc8b1c 100644 --- a/crates/ruff_formatter/src/format_element/tag.rs +++ b/crates/ruff_formatter/src/format_element/tag.rs @@ -33,6 +33,16 @@ pub enum Tag { StartGroup(Group), EndGroup, + /// Creates a logical group similar to [`Tag::StartGroup`] but only if the condition is met. + /// This is an optimized representation for (assuming the content should only be grouped if another group fits): + /// + /// ```text + /// if_group_breaks(content, other_group_id), + /// if_group_fits_on_line(group(&content), other_group_id) + /// ``` + StartConditionalGroup(ConditionalGroup), + EndConditionalGroup, + /// Allows to specify content that gets printed depending on whatever the enclosing group /// is printed on a single line or multiple lines. See [crate::builders::if_group_breaks] for examples. StartConditionalContent(Condition), @@ -67,6 +77,9 @@ pub enum Tag { /// See [crate::builders::labelled] for documentation. StartLabelled(LabelId), EndLabelled, + + StartFitsExpanded(FitsExpanded), + EndFitsExpanded, } impl Tag { @@ -77,7 +90,8 @@ impl Tag { Tag::StartIndent | Tag::StartAlign(_) | Tag::StartDedent(_) - | Tag::StartGroup { .. } + | Tag::StartGroup(_) + | Tag::StartConditionalGroup(_) | Tag::StartConditionalContent(_) | Tag::StartIndentIfGroupBreaks(_) | Tag::StartFill @@ -85,6 +99,7 @@ impl Tag { | Tag::StartLineSuffix | Tag::StartVerbatim(_) | Tag::StartLabelled(_) + | Tag::StartFitsExpanded(_) ) } @@ -101,6 +116,7 @@ impl Tag { StartAlign(_) | EndAlign => TagKind::Align, StartDedent(_) | EndDedent => TagKind::Dedent, StartGroup(_) | EndGroup => TagKind::Group, + StartConditionalGroup(_) | EndConditionalGroup => TagKind::ConditionalGroup, StartConditionalContent(_) | EndConditionalContent => TagKind::ConditionalContent, StartIndentIfGroupBreaks(_) | EndIndentIfGroupBreaks => TagKind::IndentIfGroupBreaks, StartFill | EndFill => TagKind::Fill, @@ -108,6 +124,7 @@ impl Tag { StartLineSuffix | EndLineSuffix => TagKind::LineSuffix, StartVerbatim(_) | EndVerbatim => TagKind::Verbatim, StartLabelled(_) | EndLabelled => TagKind::Labelled, + StartFitsExpanded { .. } | EndFitsExpanded => TagKind::FitsExpanded, } } } @@ -122,6 +139,7 @@ pub enum TagKind { Align, Dedent, Group, + ConditionalGroup, ConditionalContent, IndentIfGroupBreaks, Fill, @@ -129,6 +147,7 @@ pub enum TagKind { LineSuffix, Verbatim, Labelled, + FitsExpanded, } #[derive(Debug, Copy, Default, Clone, Eq, PartialEq)] @@ -150,6 +169,27 @@ impl GroupMode { } } +#[derive(Debug, Clone, Eq, PartialEq, Default)] +pub struct FitsExpanded { + pub(crate) condition: Option, + pub(crate) propagate_expand: Cell, +} + +impl FitsExpanded { + pub fn new() -> Self { + Self::default() + } + + pub fn with_condition(mut self, condition: Option) -> Self { + self.condition = condition; + self + } + + pub fn propagate_expand(&self) { + self.propagate_expand.set(true) + } +} + #[derive(Debug, Clone, Eq, PartialEq, Default)] pub struct Group { id: Option, @@ -189,6 +229,33 @@ impl Group { } } +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct ConditionalGroup { + mode: Cell, + condition: Condition, +} + +impl ConditionalGroup { + pub fn new(condition: Condition) -> Self { + Self { + mode: Cell::new(GroupMode::Flat), + condition, + } + } + + pub fn condition(&self) -> Condition { + self.condition + } + + pub fn propagate_expand(&self) { + self.mode.set(GroupMode::Propagated) + } + + pub fn mode(&self) -> GroupMode { + self.mode.get() + } +} + #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum DedentMode { /// Reduces the indent by a level (if the current indent is > 0) @@ -198,7 +265,7 @@ pub enum DedentMode { Root, } -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct Condition { /// - `Flat` -> Omitted if the enclosing group is a multiline group, printed for groups fitting on a single line /// - `Expanded` -> Omitted if the enclosing group fits on a single line, printed if the group breaks over multiple lines. @@ -217,6 +284,34 @@ impl Condition { } } + pub fn if_fits_on_line() -> Self { + Self { + mode: PrintMode::Flat, + group_id: None, + } + } + + pub fn if_group_fits_on_line(group_id: GroupId) -> Self { + Self { + mode: PrintMode::Flat, + group_id: Some(group_id), + } + } + + pub fn if_breaks() -> Self { + Self { + mode: PrintMode::Expanded, + group_id: None, + } + } + + pub fn if_group_breaks(group_id: GroupId) -> Self { + Self { + mode: PrintMode::Expanded, + group_id: Some(group_id), + } + } + pub fn with_group_id(mut self, id: Option) -> Self { self.group_id = id; self diff --git a/crates/ruff_formatter/src/printer/mod.rs b/crates/ruff_formatter/src/printer/mod.rs index 25687b7631..a58514c102 100644 --- a/crates/ruff_formatter/src/printer/mod.rs +++ b/crates/ruff_formatter/src/printer/mod.rs @@ -7,6 +7,7 @@ mod stack; use crate::format_element::document::Document; use crate::format_element::tag::{Condition, GroupMode}; use crate::format_element::{BestFittingMode, BestFittingVariants, LineMode, PrintMode}; +use crate::prelude::tag; use crate::prelude::tag::{DedentMode, Tag, TagKind, VerbatimKind}; use crate::printer::call_stack::{ CallStack, FitsCallStack, PrintCallStack, PrintElementArgs, StackFrame, @@ -143,40 +144,26 @@ impl<'a> Printer<'a> { } FormatElement::Tag(StartGroup(group)) => { - let group_mode = match group.mode() { - GroupMode::Expand | GroupMode::Propagated => PrintMode::Expanded, - GroupMode::Flat => { - match args.mode() { - PrintMode::Flat if self.state.measured_group_fits => { - // A parent group has already verified that this group fits on a single line - // Thus, just continue in flat mode - PrintMode::Flat - } - // The printer is either in expanded mode or it's necessary to re-measure if the group fits - // because the printer printed a line break - _ => { - self.state.measured_group_fits = true; - - // Measure to see if the group fits up on a single line. If that's the case, - // print the group in "flat" mode, otherwise continue in expanded mode - stack.push(TagKind::Group, args.with_print_mode(PrintMode::Flat)); - let fits = self.fits(queue, stack)?; - stack.pop(TagKind::Group)?; - - if fits { - PrintMode::Flat - } else { - PrintMode::Expanded - } - } - } - } - }; - - stack.push(TagKind::Group, args.with_print_mode(group_mode)); + let print_mode = + self.print_group(TagKind::Group, group.mode(), args, queue, stack)?; if let Some(id) = group.id() { - self.state.group_modes.insert_print_mode(id, group_mode); + self.state.group_modes.insert_print_mode(id, print_mode); + } + } + + FormatElement::Tag(StartConditionalGroup(group)) => { + let condition = group.condition(); + let expected_mode = match condition.group_id { + None => args.mode(), + Some(id) => self.state.group_modes.unwrap_print_mode(id, element), + }; + + if expected_mode == condition.mode { + self.print_group(TagKind::ConditionalGroup, group.mode(), args, queue, stack)?; + } else { + // Condition isn't met, render as normal content + stack.push(TagKind::ConditionalGroup, args); } } @@ -244,6 +231,29 @@ impl<'a> Printer<'a> { stack.push(TagKind::Verbatim, args); } + FormatElement::Tag(StartFitsExpanded(tag::FitsExpanded { condition, .. })) => { + let condition_met = match condition { + Some(condition) => { + let group_mode = match condition.group_id { + Some(group_id) => { + self.state.group_modes.unwrap_print_mode(group_id, element) + } + None => args.mode(), + }; + + condition.mode == group_mode + } + None => true, + }; + + if condition_met { + // We measured the inner groups all in expanded. It now is necessary to measure if the inner groups fit as well. + self.state.measured_group_fits = false; + } + + stack.push(TagKind::FitsExpanded, args); + } + FormatElement::Tag(tag @ (StartLabelled(_) | StartEntry)) => { stack.push(tag.kind(), args); } @@ -252,11 +262,13 @@ impl<'a> Printer<'a> { tag @ (EndLabelled | EndEntry | EndGroup + | EndConditionalGroup | EndIndent | EndDedent | EndAlign | EndConditionalContent | EndIndentIfGroupBreaks + | EndFitsExpanded | EndVerbatim | EndLineSuffix | EndFill), @@ -275,6 +287,49 @@ impl<'a> Printer<'a> { result } + fn print_group( + &mut self, + kind: TagKind, + mode: GroupMode, + args: PrintElementArgs, + queue: &mut PrintQueue<'a>, + stack: &mut PrintCallStack, + ) -> PrintResult { + let group_mode = match mode { + GroupMode::Expand | GroupMode::Propagated => PrintMode::Expanded, + GroupMode::Flat => { + match args.mode() { + PrintMode::Flat if self.state.measured_group_fits => { + // A parent group has already verified that this group fits on a single line + // Thus, just continue in flat mode + PrintMode::Flat + } + // The printer is either in expanded mode or it's necessary to re-measure if the group fits + // because the printer printed a line break + _ => { + self.state.measured_group_fits = true; + + // Measure to see if the group fits up on a single line. If that's the case, + // print the group in "flat" mode, otherwise continue in expanded mode + stack.push(kind, args.with_print_mode(PrintMode::Flat)); + let fits = self.fits(queue, stack)?; + stack.pop(kind)?; + + if fits { + PrintMode::Flat + } else { + PrintMode::Expanded + } + } + } + } + }; + + stack.push(kind, args.with_print_mode(group_mode)); + + Ok(group_mode) + } + fn print_text(&mut self, text: &str, source_range: Option) { if !self.state.pending_indent.is_empty() { let (indent_char, repeat_count) = match self.options.indent_style() { @@ -1050,22 +1105,24 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { } FormatElement::Tag(StartGroup(group)) => { - if self.must_be_flat && !group.mode().is_flat() { - return Ok(Fits::No); - } + return self.fits_group(TagKind::Group, group.mode(), group.id(), args); + } - // Continue printing groups in expanded mode if measuring a `fits_expanded` element - let print_mode = if !group.mode().is_flat() { - PrintMode::Expanded - } else { - args.mode() + FormatElement::Tag(StartConditionalGroup(group)) => { + let condition = group.condition(); + + let print_mode = match condition.group_id { + None => args.mode(), + Some(group_id) => self + .group_modes() + .get_print_mode(group_id) + .unwrap_or_else(|| args.mode()), }; - self.stack - .push(TagKind::Group, args.with_print_mode(print_mode)); - - if let Some(id) = group.id() { - self.group_modes_mut().insert_print_mode(id, print_mode); + if condition.mode == print_mode { + return self.fits_group(TagKind::ConditionalGroup, group.mode(), None, args); + } else { + self.stack.push(TagKind::ConditionalGroup, args); } } @@ -1113,6 +1170,42 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { return invalid_end_tag(TagKind::LineSuffix, self.stack.top_kind()); } + FormatElement::Tag(StartFitsExpanded(tag::FitsExpanded { + condition, + propagate_expand, + })) => { + let condition_met = match condition { + Some(condition) => { + let group_mode = match condition.group_id { + Some(group_id) => self + .group_modes() + .get_print_mode(group_id) + .unwrap_or_else(|| args.mode()), + None => args.mode(), + }; + + condition.mode == group_mode + } + None => true, + }; + + if condition_met { + // Measure in fully expanded mode. + self.stack.push( + TagKind::FitsExpanded, + args.with_print_mode(PrintMode::Expanded) + .with_measure_mode(MeasureMode::AllLines), + ) + } else { + if propagate_expand.get() && args.mode().is_flat() { + return Ok(Fits::No); + } + + // As usual + self.stack.push(TagKind::FitsExpanded, args) + } + } + FormatElement::Tag( tag @ (StartFill | StartVerbatim(_) | StartLabelled(_) | StartEntry), ) => { @@ -1125,11 +1218,13 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { | EndLabelled | EndEntry | EndGroup + | EndConditionalGroup | EndIndentIfGroupBreaks | EndConditionalContent | EndAlign | EndDedent - | EndIndent), + | EndIndent + | EndFitsExpanded), ) => { self.stack.pop(tag.kind())?; } @@ -1138,6 +1233,34 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { Ok(Fits::Maybe) } + fn fits_group( + &mut self, + kind: TagKind, + mode: GroupMode, + id: Option, + args: PrintElementArgs, + ) -> PrintResult { + if self.must_be_flat && !mode.is_flat() { + return Ok(Fits::No); + } + + // Continue printing groups in expanded mode if measuring a `best_fitting` element where + // a group expands. + let print_mode = if !mode.is_flat() { + PrintMode::Expanded + } else { + args.mode() + }; + + self.stack.push(kind, args.with_print_mode(print_mode)); + + if let Some(id) = id { + self.group_modes_mut().insert_print_mode(id, print_mode); + } + + Ok(Fits::Maybe) + } + fn fits_text(&mut self, text: &str) -> Fits { let indent = std::mem::take(&mut self.state.pending_indent); self.state.line_width += indent.level() as usize * self.options().indent_width() as usize diff --git a/crates/ruff_python_formatter/src/comments/debug.rs b/crates/ruff_python_formatter/src/comments/debug.rs index c166f4c561..37be2ed9c0 100644 --- a/crates/ruff_python_formatter/src/comments/debug.rs +++ b/crates/ruff_python_formatter/src/comments/debug.rs @@ -29,10 +29,8 @@ impl Debug for DebugComment<'_> { strut .field("text", &self.comment.slice.text(self.source_code)) - .field("position", &self.comment.line_position); - - #[cfg(debug_assertions)] - strut.field("formatted", &self.comment.formatted.get()); + .field("position", &self.comment.line_position) + .field("formatted", &self.comment.formatted.get()); strut.finish() } From 715250a17940e634e4fc0c0abec2edfeabe0dee7 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 11 Jul 2023 14:07:39 +0200 Subject: [PATCH 413/447] Prefer expanding parenthesized expressions before operands ## Summary This PR implements Black's behavior where it first splits off parenthesized expressions before splitting before operands to avoid unnecessary parentheses: ```python # We want if a + [ b, c ]: pass # Rather than if ( a + [b, c] ): pass ``` This is implemented by using the new IR elements introduced in #5596. * We give the group wrapping the optional parentheses an ID (`parentheses_id`) * We use `conditional_group` for the lower priority groups (all non-parenthesized expressions) with the condition that the `parentheses_id` group breaks (we want to split before operands only if the parentheses are necessary) * We use `fits_expanded` to wrap all other parenthesized expressions (lists, dicts, sets), to prevent that expanding e.g. a list expands the `parentheses_id` group. We gate the `fits_expand` to only apply if the `parentheses_id` group fits (because we prefer `a\n+[b, c]` over expanding `[b, c]` if the whole expression gets parenthesized). We limit using `fits_expanded` and `conditional_group` only to expressions that themselves are not in parentheses (checking the conditions isn't free) ## Test Plan It increases the Jaccard index for Django from 0.915 to 0.917 ## Incompatibilites There are two incompatibilities left that I'm aware of (there may be more, I didn't go through all snapshot differences). ### Long string literals I commented on the regression. The issue is that a very long string (or any content without a split point) may not fit when only breaking the right side. The formatter than inserts the optional parentheses. But this is kind of useless because the overlong string will still not fit, because there are no new split points. I think we should ignore this incompatibility for now ### Expressions on statement level I don't fully understand the logic behind this yet, but black doesn't break before the operators for the following example even though the expression exceeds the configured line width ```python aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa < bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb > ccccccccccccccccccccccccccccc == ddddddddddddddddddddd ``` But it would if the expression is used inside of a condition. What I understand so far is that Black doesn't insert optional parentheses on the expression statement level (and a few other places) and, therefore, only breaks after opening parentheses. I propose to keep this deviation for now to avoid overlong-lines and use the compatibility report to make a decision if we should implement the same behavior. --- crates/ruff_python_formatter/src/builders.rs | 21 +- .../src/comments/format.rs | 4 +- crates/ruff_python_formatter/src/context.rs | 9 +- .../src/expression/binary_like.rs | 216 ------------ .../src/expression/expr_bin_op.rs | 55 +-- .../src/expression/expr_bool_op.rs | 101 ++---- .../src/expression/expr_compare.rs | 73 +--- .../src/expression/expr_if_exp.rs | 12 +- .../src/expression/expr_subscript.rs | 25 +- .../src/expression/mod.rs | 327 ++++++++++++++++-- .../src/expression/parentheses.rs | 71 +++- .../src/other/arguments.rs | 3 +- ...tibility@simple_cases__composition.py.snap | 47 +-- ...ses__composition_no_trailing_comma.py.snap | 47 +-- ...tibility@simple_cases__empty_lines.py.snap | 131 ++----- ...ple_cases__function_trailing_comma.py.snap | 31 +- ...@simple_cases__remove_await_parens.py.snap | 97 ++---- ...ompatibility@simple_cases__torture.py.snap | 83 ++--- ...s__trailing_comma_optional_parens1.py.snap | 56 +-- ...s__trailing_comma_optional_parens2.py.snap | 52 --- ...s__trailing_comma_optional_parens3.py.snap | 77 +++++ ...__trailing_commas_in_leading_parts.py.snap | 22 +- .../format@expression__binary.py.snap | 6 +- .../format@expression__compare.py.snap | 40 +-- .../format@expression__unary.py.snap | 11 +- .../snapshots/format@statement__raise.py.snap | 6 +- 26 files changed, 680 insertions(+), 943 deletions(-) delete mode 100644 crates/ruff_python_formatter/src/expression/binary_like.rs delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens2.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens3.py.snap diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 4541acda74..d39ff8a46d 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -21,12 +21,21 @@ pub(crate) struct OptionalParentheses<'a, 'ast> { impl<'ast> Format> for OptionalParentheses<'_, 'ast> { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { - group(&format_args![ + let saved_level = f.context().node_level(); + + f.context_mut() + .set_node_level(NodeLevel::ParenthesizedExpression); + + let result = group(&format_args![ if_group_breaks(&text("(")), soft_block_indent(&Arguments::from(&self.inner)), - if_group_breaks(&text(")")) + if_group_breaks(&text(")")), ]) - .fmt(f) + .fmt(f); + + f.context_mut().set_node_level(saved_level); + + result } } @@ -113,7 +122,9 @@ impl<'fmt, 'ast, 'buf> JoinNodesBuilder<'fmt, 'ast, 'buf> { 0 | 1 => hard_line_break().fmt(self.fmt), _ => empty_line().fmt(self.fmt), }, - NodeLevel::Expression => hard_line_break().fmt(self.fmt), + NodeLevel::Expression(_) | NodeLevel::ParenthesizedExpression => { + hard_line_break().fmt(self.fmt) + } }?; } @@ -353,7 +364,7 @@ no_leading_newline = 30"# // Removes all empty lines #[test] fn ranged_builder_parenthesized_level() { - let printed = format_ranged(NodeLevel::Expression); + let printed = format_ranged(NodeLevel::Expression(None)); assert_eq!( &printed, diff --git a/crates/ruff_python_formatter/src/comments/format.rs b/crates/ruff_python_formatter/src/comments/format.rs index 31c7e42cd1..c0a0a2a5bd 100644 --- a/crates/ruff_python_formatter/src/comments/format.rs +++ b/crates/ruff_python_formatter/src/comments/format.rs @@ -318,7 +318,9 @@ impl Format> for FormatEmptyLines { }, // Remove all whitespace in parenthesized expressions - NodeLevel::Expression => write!(f, [hard_line_break()]), + NodeLevel::Expression(_) | NodeLevel::ParenthesizedExpression => { + write!(f, [hard_line_break()]) + } } } } diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index cdf587c35f..2683641dd8 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -1,6 +1,6 @@ use crate::comments::Comments; use crate::PyFormatOptions; -use ruff_formatter::{FormatContext, SourceCode}; +use ruff_formatter::{FormatContext, GroupId, SourceCode}; use ruff_python_ast::source_code::Locator; use std::fmt::{Debug, Formatter}; @@ -78,6 +78,9 @@ pub(crate) enum NodeLevel { /// (`if`, `while`, `match`, etc.). CompoundStatement, - /// Formatting nodes that are enclosed in a parenthesized expression. - Expression, + /// The root or any sub-expression. + Expression(Option), + + /// Formatting nodes that are enclosed by a parenthesized (any `[]`, `{}` or `()`) expression. + ParenthesizedExpression, } diff --git a/crates/ruff_python_formatter/src/expression/binary_like.rs b/crates/ruff_python_formatter/src/expression/binary_like.rs deleted file mode 100644 index c934322e55..0000000000 --- a/crates/ruff_python_formatter/src/expression/binary_like.rs +++ /dev/null @@ -1,216 +0,0 @@ -//! This module provides helper utilities to format an expression that has a left side, an operator, -//! and a right side (binary like). - -use rustpython_parser::ast::{self, Expr}; - -use ruff_formatter::{format_args, write}; - -use crate::expression::parentheses::{is_expression_parenthesized, Parentheses}; -use crate::prelude::*; - -/// Trait to implement a binary like syntax that has a left operand, an operator, and a right operand. -pub(super) trait FormatBinaryLike<'ast> { - /// The type implementing the formatting of the operator. - type FormatOperator: Format>; - - /// Formats the binary like expression to `f`. - fn fmt_binary( - &self, - parentheses: Option, - f: &mut PyFormatter<'ast, '_>, - ) -> FormatResult<()> { - let left = self.left()?; - let operator = self.operator(); - let right = self.right()?; - - let layout = if parentheses == Some(Parentheses::Custom) { - self.binary_layout(f.context().contents()) - } else { - BinaryLayout::Default - }; - - match layout { - BinaryLayout::Default => self.fmt_default(f), - BinaryLayout::ExpandLeft => { - let left = left.format().memoized(); - let right = right.format().memoized(); - write!( - f, - [best_fitting![ - // Everything on a single line - format_args![group(&left), space(), operator, space(), right], - // Break the left over multiple lines, keep the right flat - format_args![ - group(&left).should_expand(true), - space(), - operator, - space(), - right - ], - // The content doesn't fit, indent the content and break before the operator. - format_args![ - text("("), - block_indent(&format_args![ - left, - hard_line_break(), - operator, - space(), - right - ]), - text(")") - ] - ] - .with_mode(BestFittingMode::AllLines)] - ) - } - BinaryLayout::ExpandRight => { - let left_group = f.group_id("BinaryLeft"); - - write!( - f, - [ - // Wrap the left in a group and gives it an id. The printer first breaks the - // right side if `right` contains any line break because the printer breaks - // sequences of groups from right to left. - // Indents the left side if the group breaks. - group(&format_args![ - if_group_breaks(&text("(")), - indent_if_group_breaks( - &format_args![ - soft_line_break(), - left.format(), - soft_line_break_or_space(), - operator, - space() - ], - left_group - ) - ]) - .with_group_id(Some(left_group)), - // Wrap the right in a group and indents its content but only if the left side breaks - group(&indent_if_group_breaks(&right.format(), left_group)), - // If the left side breaks, insert a hard line break to finish the indent and close the open paren. - if_group_breaks(&format_args![hard_line_break(), text(")")]) - .with_group_id(Some(left_group)) - ] - ) - } - BinaryLayout::ExpandRightThenLeft => { - // The formatter expands group-sequences from right to left, and expands both if - // there isn't enough space when expanding only one of them. - write!( - f, - [ - group(&left.format()), - space(), - operator, - space(), - group(&right.format()) - ] - ) - } - } - } - - /// Determines which binary layout to use. - fn binary_layout(&self, source: &str) -> BinaryLayout { - if let (Ok(left), Ok(right)) = (self.left(), self.right()) { - BinaryLayout::from_left_right(left, right, source) - } else { - BinaryLayout::Default - } - } - - /// Formats the node according to the default layout. - fn fmt_default(&self, f: &mut PyFormatter<'ast, '_>) -> FormatResult<()>; - - /// Returns the left operator - fn left(&self) -> FormatResult<&Expr>; - - /// Returns the right operator. - fn right(&self) -> FormatResult<&Expr>; - - /// Returns the object that formats the operator. - fn operator(&self) -> Self::FormatOperator; -} - -fn can_break_expr(expr: &Expr, source: &str) -> bool { - let can_break = match expr { - Expr::Tuple(ast::ExprTuple { - elts: expressions, .. - }) - | Expr::List(ast::ExprList { - elts: expressions, .. - }) - | Expr::Set(ast::ExprSet { - elts: expressions, .. - }) - | Expr::Dict(ast::ExprDict { - values: expressions, - .. - }) => !expressions.is_empty(), - Expr::Call(ast::ExprCall { args, keywords, .. }) => { - !(args.is_empty() && keywords.is_empty()) - } - Expr::ListComp(_) | Expr::SetComp(_) | Expr::DictComp(_) | Expr::GeneratorExp(_) => true, - Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => can_break_expr(operand.as_ref(), source), - _ => false, - }; - - can_break || is_expression_parenthesized(expr.into(), source) -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub(super) enum BinaryLayout { - /// Put each operand on their own line if either side expands - Default, - - /// Try to expand the left to make it fit. Add parentheses if the left or right don't fit. - /// - ///```python - /// [ - /// a, - /// b - /// ] & c - ///``` - ExpandLeft, - - /// Try to expand the right to make it fix. Add parentheses if the left or right don't fit. - /// - /// ```python - /// a & [ - /// b, - /// c - /// ] - /// ``` - ExpandRight, - - /// Both the left and right side can be expanded. Try in the following order: - /// * expand the right side - /// * expand the left side - /// * expand both sides - /// - /// to make the expression fit - /// - /// ```python - /// [ - /// a, - /// b - /// ] & [ - /// c, - /// d - /// ] - /// ``` - ExpandRightThenLeft, -} - -impl BinaryLayout { - pub(super) fn from_left_right(left: &Expr, right: &Expr, source: &str) -> BinaryLayout { - match (can_break_expr(left, source), can_break_expr(right, source)) { - (false, false) => BinaryLayout::Default, - (true, false) => BinaryLayout::ExpandLeft, - (false, true) => BinaryLayout::ExpandRight, - (true, true) => BinaryLayout::ExpandRightThenLeft, - } - } -} diff --git a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs index 859e03ce4f..9307ed2043 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs @@ -1,8 +1,7 @@ use crate::comments::{trailing_comments, trailing_node_comments, Comments}; -use crate::expression::binary_like::{BinaryLayout, FormatBinaryLike}; use crate::expression::parentheses::{ - default_expression_needs_parentheses, is_expression_parenthesized, NeedsParentheses, - Parenthesize, + default_expression_needs_parentheses, in_parentheses_only_group, is_expression_parenthesized, + NeedsParentheses, Parenthesize, }; use crate::expression::Parentheses; use crate::prelude::*; @@ -31,24 +30,11 @@ impl FormatRuleWithOptions> for FormatExprBinOp { impl FormatNodeRule for FormatExprBinOp { fn fmt_fields(&self, item: &ExprBinOp, f: &mut PyFormatter) -> FormatResult<()> { - item.fmt_binary(self.parentheses, f) - } - - fn fmt_dangling_comments(&self, _node: &ExprBinOp, _f: &mut PyFormatter) -> FormatResult<()> { - // Handled inside of `fmt_fields` - Ok(()) - } -} - -impl<'ast> FormatBinaryLike<'ast> for ExprBinOp { - type FormatOperator = FormatOwnedWithRule>; - - fn fmt_default(&self, f: &mut PyFormatter<'ast, '_>) -> FormatResult<()> { let comments = f.context().comments().clone(); let format_inner = format_with(|f: &mut PyFormatter| { let source = f.context().contents(); - let binary_chain: SmallVec<[&ExprBinOp; 4]> = iter::successors(Some(self), |parent| { + let binary_chain: SmallVec<[&ExprBinOp; 4]> = iter::successors(Some(item), |parent| { parent.left.as_bin_op_expr().and_then(|bin_expression| { if is_expression_parenthesized(bin_expression.as_any_node_ref(), source) { None @@ -63,7 +49,7 @@ impl<'ast> FormatBinaryLike<'ast> for ExprBinOp { let left_most = binary_chain.last().unwrap(); // Format the left most expression - group(&left_most.left.format()).fmt(f)?; + in_parentheses_only_group(&left_most.left.format()).fmt(f)?; // Iterate upwards in the binary expression tree and, for each level, format the operator // and the right expression. @@ -100,13 +86,13 @@ impl<'ast> FormatBinaryLike<'ast> for ExprBinOp { space().fmt(f)?; } - group(&right.format()).fmt(f)?; + in_parentheses_only_group(&right.format()).fmt(f)?; // It's necessary to format the trailing comments because the code bypasses // `FormatNodeRule::fmt` for the nested binary expressions. // Don't call the formatting function for the most outer binary expression because // these comments have already been formatted. - if current != self { + if current != item { trailing_node_comments(current).fmt(f)?; } } @@ -114,19 +100,12 @@ impl<'ast> FormatBinaryLike<'ast> for ExprBinOp { Ok(()) }); - group(&format_inner).fmt(f) + in_parentheses_only_group(&format_inner).fmt(f) } - fn left(&self) -> FormatResult<&Expr> { - Ok(&self.left) - } - - fn right(&self) -> FormatResult<&Expr> { - Ok(&self.right) - } - - fn operator(&self) -> Self::FormatOperator { - self.op.into_format() + fn fmt_dangling_comments(&self, _node: &ExprBinOp, _f: &mut PyFormatter) -> FormatResult<()> { + // Handled inside of `fmt_fields` + Ok(()) } } @@ -200,18 +179,6 @@ impl NeedsParentheses for ExprBinOp { source: &str, comments: &Comments, ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { - Parentheses::Optional => { - if self.binary_layout(source) == BinaryLayout::Default - || comments.has_leading_comments(self.right.as_ref()) - || comments.has_dangling_comments(self) - { - Parentheses::Optional - } else { - Parentheses::Custom - } - } - parentheses => parentheses, - } + default_expression_needs_parentheses(self.into(), parenthesize, source, comments) } } 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 8fb9b42eb2..5a35064509 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bool_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bool_op.rs @@ -1,13 +1,11 @@ use crate::comments::{leading_comments, Comments}; -use crate::expression::binary_like::{BinaryLayout, FormatBinaryLike}; use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, + default_expression_needs_parentheses, in_parentheses_only_group, NeedsParentheses, Parentheses, + Parenthesize, }; use crate::prelude::*; -use ruff_formatter::{ - write, FormatError, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions, -}; -use rustpython_parser::ast::{BoolOp, Expr, ExprBoolOp}; +use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions}; +use rustpython_parser::ast::{BoolOp, ExprBoolOp}; #[derive(Default)] pub struct FormatExprBoolOp { @@ -24,64 +22,48 @@ impl FormatRuleWithOptions> for FormatExprBoolOp impl FormatNodeRule for FormatExprBoolOp { fn fmt_fields(&self, item: &ExprBoolOp, f: &mut PyFormatter) -> FormatResult<()> { - item.fmt_binary(self.parentheses, f) - } -} - -impl<'ast> FormatBinaryLike<'ast> for ExprBoolOp { - type FormatOperator = FormatOwnedWithRule>; - - fn binary_layout(&self, source: &str) -> BinaryLayout { - match self.values.as_slice() { - [left, right] => BinaryLayout::from_left_right(left, right, source), - [..] => BinaryLayout::Default, - } - } - - fn fmt_default(&self, f: &mut PyFormatter<'ast, '_>) -> FormatResult<()> { let ExprBoolOp { range: _, op, values, - } = self; + } = item; - let mut values = values.iter(); - let comments = f.context().comments().clone(); + let inner = format_with(|f: &mut PyFormatter| { + let mut values = values.iter(); + let comments = f.context().comments().clone(); - let Some(first) = values.next() else { - return Ok(()); - }; + let Some(first) = values.next() else { + return Ok(()); + }; - write!(f, [group(&first.format())])?; + write!(f, [in_parentheses_only_group(&first.format())])?; + + for value in values { + let leading_value_comments = comments.leading_comments(value); + // Format the expressions leading comments **before** the operator + if leading_value_comments.is_empty() { + write!(f, [soft_line_break_or_space()])?; + } else { + write!( + f, + [hard_line_break(), leading_comments(leading_value_comments)] + )?; + } - for value in values { - let leading_value_comments = comments.leading_comments(value); - // Format the expressions leading comments **before** the operator - if leading_value_comments.is_empty() { - write!(f, [soft_line_break_or_space()])?; - } else { write!( f, - [hard_line_break(), leading_comments(leading_value_comments)] + [ + op.format(), + space(), + in_parentheses_only_group(&value.format()) + ] )?; } - write!(f, [op.format(), space(), group(&value.format())])?; - } + Ok(()) + }); - Ok(()) - } - - fn left(&self) -> FormatResult<&Expr> { - self.values.first().ok_or(FormatError::SyntaxError) - } - - fn right(&self) -> FormatResult<&Expr> { - self.values.last().ok_or(FormatError::SyntaxError) - } - - fn operator(&self) -> Self::FormatOperator { - self.op.into_format() + in_parentheses_only_group(&inner).fmt(f) } } @@ -92,24 +74,7 @@ impl NeedsParentheses for ExprBoolOp { source: &str, comments: &Comments, ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { - Parentheses::Optional => match self.binary_layout(source) { - BinaryLayout::Default => Parentheses::Optional, - - BinaryLayout::ExpandRight - | BinaryLayout::ExpandLeft - | BinaryLayout::ExpandRightThenLeft - if self - .values - .last() - .map_or(false, |right| comments.has_leading_comments(right)) => - { - Parentheses::Optional - } - _ => Parentheses::Custom, - }, - parentheses => parentheses, - } + default_expression_needs_parentheses(self.into(), parenthesize, source, comments) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_compare.rs b/crates/ruff_python_formatter/src/expression/expr_compare.rs index 094344be61..10bffe948b 100644 --- a/crates/ruff_python_formatter/src/expression/expr_compare.rs +++ b/crates/ruff_python_formatter/src/expression/expr_compare.rs @@ -1,14 +1,11 @@ use crate::comments::{leading_comments, Comments}; -use crate::expression::binary_like::{BinaryLayout, FormatBinaryLike}; use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, + default_expression_needs_parentheses, in_parentheses_only_group, NeedsParentheses, Parentheses, + Parenthesize, }; use crate::prelude::*; use crate::FormatNodeRule; -use ruff_formatter::{ - write, FormatError, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions, -}; -use rustpython_parser::ast::Expr; +use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions}; use rustpython_parser::ast::{CmpOp, ExprCompare}; #[derive(Default)] @@ -27,35 +24,16 @@ impl FormatRuleWithOptions> for FormatExprCompa impl FormatNodeRule for FormatExprCompare { fn fmt_fields(&self, item: &ExprCompare, f: &mut PyFormatter) -> FormatResult<()> { - item.fmt_binary(self.parentheses, f) - } -} - -impl<'ast> FormatBinaryLike<'ast> for ExprCompare { - type FormatOperator = FormatOwnedWithRule>; - - fn binary_layout(&self, source: &str) -> BinaryLayout { - if self.ops.len() == 1 { - match self.comparators.as_slice() { - [right] => BinaryLayout::from_left_right(&self.left, right, source), - [..] => BinaryLayout::Default, - } - } else { - BinaryLayout::Default - } - } - - fn fmt_default(&self, f: &mut PyFormatter<'ast, '_>) -> FormatResult<()> { let ExprCompare { range: _, left, ops, comparators, - } = self; + } = item; let comments = f.context().comments().clone(); - write!(f, [group(&left.format())])?; + write!(f, [in_parentheses_only_group(&left.format())])?; assert_eq!(comparators.len(), ops.len()); @@ -74,24 +52,18 @@ impl<'ast> FormatBinaryLike<'ast> for ExprCompare { )?; } - write!(f, [operator.format(), space(), group(&comparator.format())])?; + write!( + f, + [ + operator.format(), + space(), + in_parentheses_only_group(&comparator.format()) + ] + )?; } Ok(()) } - - fn left(&self) -> FormatResult<&Expr> { - Ok(self.left.as_ref()) - } - - fn right(&self) -> FormatResult<&Expr> { - self.comparators.last().ok_or(FormatError::SyntaxError) - } - - fn operator(&self) -> Self::FormatOperator { - let op = *self.ops.first().unwrap(); - op.into_format() - } } impl NeedsParentheses for ExprCompare { @@ -101,24 +73,7 @@ impl NeedsParentheses for ExprCompare { source: &str, comments: &Comments, ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { - parentheses @ Parentheses::Optional => match self.binary_layout(source) { - BinaryLayout::Default => parentheses, - - BinaryLayout::ExpandRight - | BinaryLayout::ExpandLeft - | BinaryLayout::ExpandRightThenLeft - if self - .comparators - .last() - .map_or(false, |right| comments.has_leading_comments(right)) => - { - parentheses - } - _ => Parentheses::Custom, - }, - parentheses => parentheses, - } + default_expression_needs_parentheses(self.into(), parenthesize, source, comments) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_if_exp.rs b/crates/ruff_python_formatter/src/expression/expr_if_exp.rs index 0a2ffcf584..ae142e7122 100644 --- a/crates/ruff_python_formatter/src/expression/expr_if_exp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_if_exp.rs @@ -1,10 +1,11 @@ use crate::comments::{leading_comments, Comments}; use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, + default_expression_needs_parentheses, in_parentheses_only_group, NeedsParentheses, Parentheses, + Parenthesize, }; -use crate::{AsFormat, FormatNodeRule, PyFormatter}; -use ruff_formatter::prelude::{group, soft_line_break_or_space, space, text}; -use ruff_formatter::{format_args, write, Buffer, FormatResult}; +use crate::prelude::*; +use crate::FormatNodeRule; +use ruff_formatter::{format_args, write}; use rustpython_parser::ast::ExprIfExp; #[derive(Default)] @@ -19,12 +20,13 @@ impl FormatNodeRule for FormatExprIfExp { orelse, } = item; let comments = f.context().comments().clone(); + // We place `if test` and `else orelse` on a single line, so the `test` and `orelse` leading // comments go on the line before the `if` or `else` instead of directly ahead `test` or // `orelse` write!( f, - [group(&format_args![ + [in_parentheses_only_group(&format_args![ body.format(), soft_line_break_or_space(), leading_comments(comments.leading_comments(test.as_ref())), diff --git a/crates/ruff_python_formatter/src/expression/expr_subscript.rs b/crates/ruff_python_formatter/src/expression/expr_subscript.rs index 55f8d22490..7010af60d6 100644 --- a/crates/ruff_python_formatter/src/expression/expr_subscript.rs +++ b/crates/ruff_python_formatter/src/expression/expr_subscript.rs @@ -1,12 +1,15 @@ +use rustpython_parser::ast::ExprSubscript; + +use ruff_formatter::{format_args, write}; +use ruff_python_ast::node::AstNode; + use crate::comments::{trailing_comments, Comments}; +use crate::context::NodeLevel; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; -use crate::{AsFormat, FormatNodeRule, PyFormatter}; -use ruff_formatter::prelude::{group, soft_block_indent, text}; -use ruff_formatter::{format_args, write, Buffer, FormatResult}; -use ruff_python_ast::node::AstNode; -use rustpython_parser::ast::ExprSubscript; +use crate::prelude::*; +use crate::FormatNodeRule; #[derive(Default)] pub struct FormatExprSubscript; @@ -27,10 +30,20 @@ impl FormatNodeRule for FormatExprSubscript { "The subscript expression must have at most a single comment, the one after the bracket" ); + if let NodeLevel::Expression(Some(group_id)) = f.context().node_level() { + // Enforce the optional parentheses for parenthesized values. + f.context_mut().set_node_level(NodeLevel::Expression(None)); + let result = value.format().fmt(f); + f.context_mut() + .set_node_level(NodeLevel::Expression(Some(group_id))); + result?; + } else { + value.format().fmt(f)?; + } + write!( f, [group(&format_args![ - value.format(), text("["), trailing_comments(dangling_comments), soft_block_indent(&slice.format()), diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index ac3a4d682c..cc395311de 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -1,16 +1,23 @@ +use rustpython_parser::ast; +use rustpython_parser::ast::{Expr, Operator}; +use std::cmp::Ordering; + use crate::builders::optional_parentheses; +use ruff_formatter::{ + format_args, FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions, +}; +use ruff_python_ast::node::AnyNodeRef; +use ruff_python_ast::visitor::preorder::{walk_expr, PreorderVisitor}; + use crate::comments::Comments; use crate::context::NodeLevel; use crate::expression::expr_tuple::TupleParentheses; -use crate::expression::parentheses::{NeedsParentheses, Parentheses, Parenthesize}; +use crate::expression::parentheses::{ + is_expression_parenthesized, parenthesized, NeedsParentheses, Parentheses, Parenthesize, +}; use crate::expression::string::StringLayout; use crate::prelude::*; -use ruff_formatter::{ - format_args, write, FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions, -}; -use rustpython_parser::ast::Expr; -pub(crate) mod binary_like; pub(crate) mod expr_attribute; pub(crate) mod expr_await; pub(crate) mod expr_bin_op; @@ -99,26 +106,63 @@ impl FormatRule> for FormatExpr { Expr::Slice(expr) => expr.format().fmt(f), }); - let saved_level = f.context().node_level(); - f.context_mut().set_node_level(NodeLevel::Expression); - let result = match parentheses { - Parentheses::Always => { - write!( - f, - [group(&format_args![ - text("("), - soft_block_indent(&format_expr), - text(")") - ])] - ) - } + Parentheses::Always => parenthesized("(", &format_expr, ")").fmt(f), // Add optional parentheses. Ignore if the item renders parentheses itself. - Parentheses::Optional => optional_parentheses(&format_expr).fmt(f), - Parentheses::Custom | Parentheses::Never => Format::fmt(&format_expr, f), - }; + Parentheses::Optional => { + if can_omit_optional_parentheses(item, f.context()) { + let saved_level = f.context().node_level(); - f.context_mut().set_node_level(saved_level); + // The group id is used as a condition in [`in_parentheses_only`] to create a conditional group + // that is only active if the optional parentheses group expands. + let parens_id = f.group_id("optional_parentheses"); + + f.context_mut() + .set_node_level(NodeLevel::Expression(Some(parens_id))); + + // We can't use `soft_block_indent` here because that would always increment the indent, + // even if the group does not break (the indent is not soft). This would result in + // too deep indentations if a `parenthesized` group expands. Using `indent_if_group_breaks` + // gives us the desired *soft* indentation that is only present if the optional parentheses + // are shown. + let result = group(&format_args![ + if_group_breaks(&text("(")), + indent_if_group_breaks( + &format_args![soft_line_break(), format_expr], + parens_id + ), + soft_line_break(), + if_group_breaks(&text(")")) + ]) + .with_group_id(Some(parens_id)) + .fmt(f); + + f.context_mut().set_node_level(saved_level); + + result + } else { + optional_parentheses(&format_expr).fmt(f) + } + } + Parentheses::Custom | Parentheses::Never => { + let saved_level = f.context().node_level(); + + let new_level = match saved_level { + NodeLevel::TopLevel | NodeLevel::CompoundStatement => { + NodeLevel::Expression(None) + } + level @ (NodeLevel::Expression(_) | NodeLevel::ParenthesizedExpression) => { + level + } + }; + + f.context_mut().set_node_level(new_level); + + let result = Format::fmt(&format_expr, f); + f.context_mut().set_node_level(saved_level); + result + } + }; result } @@ -178,3 +222,240 @@ impl<'ast> IntoFormat> for Expr { FormatOwnedWithRule::new(self, FormatExpr::default()) } } + +/// Tests if it is safe to omit the optional parentheses. +/// +/// We prefer parentheses at least in the following cases: +/// * The expression contains more than one unparenthesized expression with the same priority. For example, +/// the expression `a * b * c` contains two multiply operations. We prefer parentheses in that case. +/// `(a * b) * c` or `a * b + c` are okay, because the subexpression is parenthesized, or the expression uses operands with a lower priority +/// * The expression contains at least one parenthesized sub expression (optimization to avoid unnecessary work) +/// +/// This mimics Black's [`_maybe_split_omitting_optional_parens`](https://github.com/psf/black/blob/d1248ca9beaf0ba526d265f4108836d89cf551b7/src/black/linegen.py#L746-L820) +fn can_omit_optional_parentheses(expr: &Expr, context: &PyFormatContext) -> bool { + let mut visitor = MaxOperatorPriorityVisitor::new(context.contents()); + + visitor.visit_subexpression(expr); + + let (max_operator_priority, operation_count, any_parenthesized_expression) = visitor.finish(); + + if operation_count > 1 { + false + } else if max_operator_priority == OperatorPriority::Attribute { + true + } else { + // Only use the more complex IR when there is any expression that we can possibly split by + any_parenthesized_expression + } +} + +#[derive(Clone, Debug)] +struct MaxOperatorPriorityVisitor<'input> { + max_priority: OperatorPriority, + max_priority_count: u32, + any_parenthesized_expressions: bool, + source: &'input str, +} + +impl<'input> MaxOperatorPriorityVisitor<'input> { + fn new(source: &'input str) -> Self { + Self { + source, + max_priority: OperatorPriority::None, + max_priority_count: 0, + any_parenthesized_expressions: false, + } + } + + fn update_max_priority(&mut self, current_priority: OperatorPriority) { + self.update_max_priority_with_count(current_priority, 1); + } + + fn update_max_priority_with_count(&mut self, current_priority: OperatorPriority, count: u32) { + match self.max_priority.cmp(¤t_priority) { + Ordering::Less => { + self.max_priority_count = count; + self.max_priority = current_priority; + } + Ordering::Equal => { + self.max_priority_count += count; + } + Ordering::Greater => {} + } + } + + // Visits a subexpression, ignoring whether it is parenthesized or not + fn visit_subexpression(&mut self, expr: &'input Expr) { + match expr { + Expr::Dict(_) | Expr::List(_) | Expr::Tuple(_) | Expr::Set(_) => { + self.any_parenthesized_expressions = true; + // The values are always parenthesized, don't visit. + return; + } + Expr::ListComp(_) | Expr::SetComp(_) | Expr::DictComp(_) => { + self.any_parenthesized_expressions = true; + self.update_max_priority(OperatorPriority::Comprehension); + return; + } + // It's impossible for a file smaller or equal to 4GB to contain more than 2^32 comparisons + // because each comparison requires a left operand, and `n` `operands` and right sides. + #[allow(clippy::cast_possible_truncation)] + Expr::BoolOp(ast::ExprBoolOp { + range: _, + op: _, + values, + }) => self.update_max_priority_with_count( + OperatorPriority::BooleanOperation, + values.len().saturating_sub(1) as u32, + ), + Expr::BinOp(ast::ExprBinOp { + op, + left: _, + right: _, + range: _, + }) => self.update_max_priority(OperatorPriority::from(*op)), + + Expr::IfExp(_) => { + // + 1 for the if and one for the else + self.update_max_priority_with_count(OperatorPriority::Conditional, 2); + } + + // It's impossible for a file smaller or equal to 4GB to contain more than 2^32 comparisons + // because each comparison requires a left operand, and `n` `operands` and right sides. + #[allow(clippy::cast_possible_truncation)] + Expr::Compare(ast::ExprCompare { + range: _, + left: _, + ops, + comparators: _, + }) => { + self.update_max_priority_with_count(OperatorPriority::Comparator, ops.len() as u32); + } + Expr::Call(ast::ExprCall { + range: _, + func, + args: _, + keywords: _, + }) => { + self.any_parenthesized_expressions = true; + // Only walk the function, the arguments are always parenthesized + self.visit_expr(func); + return; + } + Expr::Subscript(_) => { + // Don't walk the value. Splitting before the value looks weird. + // Don't walk the slice, because the slice is always parenthesized. + return; + } + Expr::UnaryOp(ast::ExprUnaryOp { + range: _, + op, + operand: _, + }) => { + if op.is_invert() { + self.update_max_priority(OperatorPriority::BitwiseInversion); + } + } + + // `[a, b].test[300].dot` + Expr::Attribute(ast::ExprAttribute { + range: _, + value, + attr: _, + ctx: _, + }) => { + if has_parentheses(value, self.source) { + self.update_max_priority(OperatorPriority::Attribute); + } + } + + Expr::NamedExpr(_) + | Expr::GeneratorExp(_) + | Expr::Lambda(_) + | Expr::Await(_) + | Expr::Yield(_) + | Expr::YieldFrom(_) + | Expr::FormattedValue(_) + | Expr::JoinedStr(_) + | Expr::Constant(_) + | Expr::Starred(_) + | Expr::Name(_) + | Expr::Slice(_) => {} + }; + + walk_expr(self, expr); + } + + fn finish(self) -> (OperatorPriority, u32, bool) { + ( + self.max_priority, + self.max_priority_count, + self.any_parenthesized_expressions, + ) + } +} + +impl<'input> PreorderVisitor<'input> for MaxOperatorPriorityVisitor<'input> { + fn visit_expr(&mut self, expr: &'input Expr) { + // Rule only applies for non-parenthesized expressions. + if is_expression_parenthesized(AnyNodeRef::from(expr), self.source) { + self.any_parenthesized_expressions = true; + } else { + self.visit_subexpression(expr); + } + } +} + +fn has_parentheses(expr: &Expr, source: &str) -> bool { + matches!( + expr, + Expr::Dict(_) + | Expr::List(_) + | Expr::Tuple(_) + | Expr::Set(_) + | Expr::ListComp(_) + | Expr::SetComp(_) + | Expr::DictComp(_) + | Expr::Call(_) + | Expr::Subscript(_) + ) || is_expression_parenthesized(AnyNodeRef::from(expr), source) +} + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +enum OperatorPriority { + None, + Attribute, + Comparator, + Exponential, + BitwiseInversion, + Multiplicative, + Additive, + Shift, + BitwiseAnd, + BitwiseOr, + BitwiseXor, + // TODO(micha) + #[allow(unused)] + String, + BooleanOperation, + Conditional, + Comprehension, +} + +impl From for OperatorPriority { + fn from(value: Operator) -> Self { + match value { + Operator::Add | Operator::Sub => OperatorPriority::Additive, + Operator::Mult + | Operator::MatMult + | Operator::Div + | Operator::Mod + | Operator::FloorDiv => OperatorPriority::Multiplicative, + Operator::Pow => OperatorPriority::Exponential, + Operator::LShift | Operator::RShift => OperatorPriority::Shift, + Operator::BitOr => OperatorPriority::BitwiseOr, + Operator::BitXor => OperatorPriority::BitwiseXor, + Operator::BitAnd => OperatorPriority::BitwiseAnd, + } + } +} diff --git a/crates/ruff_python_formatter/src/expression/parentheses.rs b/crates/ruff_python_formatter/src/expression/parentheses.rs index c73b5773ce..444697213f 100644 --- a/crates/ruff_python_formatter/src/expression/parentheses.rs +++ b/crates/ruff_python_formatter/src/expression/parentheses.rs @@ -1,7 +1,9 @@ use crate::comments::Comments; +use crate::context::NodeLevel; use crate::prelude::*; use crate::trivia::{first_non_trivia_token, first_non_trivia_token_rev, Token, TokenKind}; -use ruff_formatter::{format_args, write, Argument, Arguments}; +use ruff_formatter::prelude::tag::Condition; +use ruff_formatter::{format_args, Argument, Arguments}; use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::Ranged; @@ -122,6 +124,9 @@ pub(crate) fn is_expression_parenthesized(expr: AnyNodeRef, contents: &str) -> b ) } +/// Formats `content` enclosed by the `left` and `right` parentheses. The implementation also ensures +/// that expanding the parenthesized expression (or any of its children) doesn't enforce the +/// optional parentheses around the outer-most expression to materialize. pub(crate) fn parenthesized<'content, 'ast, Content>( left: &'static str, content: &'content Content, @@ -145,13 +150,67 @@ pub(crate) struct FormatParenthesized<'content, 'ast> { impl<'ast> Format> for FormatParenthesized<'_, 'ast> { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { - write!( - f, - [group(&format_args![ + let inner = format_with(|f| { + group(&format_args![ text(self.left), &soft_block_indent(&Arguments::from(&self.content)), text(self.right) - ])] - ) + ]) + .fmt(f) + }); + + let current_level = f.context().node_level(); + + f.context_mut() + .set_node_level(NodeLevel::ParenthesizedExpression); + + let result = if let NodeLevel::Expression(Some(group_id)) = current_level { + // Use fits expanded if there's an enclosing group that adds the optional parentheses. + // This ensures that expanding this parenthesized expression does not expand the optional parentheses group. + fits_expanded(&inner) + .with_condition(Some(Condition::if_group_fits_on_line(group_id))) + .fmt(f) + } else { + // It's not necessary to wrap the content if it is not inside of an optional_parentheses group. + inner.fmt(f) + }; + + f.context_mut().set_node_level(current_level); + + result + } +} + +/// Makes `content` a group, but only if the outer expression is parenthesized (a list, parenthesized expression, dict, ...) +/// or if the expression gets parenthesized because it expands over multiple lines. +pub(crate) fn in_parentheses_only_group<'content, 'ast, Content>( + content: &'content Content, +) -> FormatInParenthesesOnlyGroup<'content, 'ast> +where + Content: Format>, +{ + FormatInParenthesesOnlyGroup { + content: Argument::new(content), + } +} + +pub(crate) struct FormatInParenthesesOnlyGroup<'content, 'ast> { + content: Argument<'content, PyFormatContext<'ast>>, +} + +impl<'ast> Format> for FormatInParenthesesOnlyGroup<'_, 'ast> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + if let NodeLevel::Expression(Some(group_id)) = f.context().node_level() { + // If this content is enclosed by a group that adds the optional parentheses, then *disable* + // this group *except* if the optional parentheses are shown. + conditional_group( + &Arguments::from(&self.content), + Condition::if_group_breaks(group_id), + ) + .fmt(f) + } else { + // Unconditionally group the content if it is not enclosed by an optional parentheses group. + group(&Arguments::from(&self.content)).fmt(f) + } } } diff --git a/crates/ruff_python_formatter/src/other/arguments.rs b/crates/ruff_python_formatter/src/other/arguments.rs index e6d91f7b69..0682a25632 100644 --- a/crates/ruff_python_formatter/src/other/arguments.rs +++ b/crates/ruff_python_formatter/src/other/arguments.rs @@ -31,7 +31,8 @@ impl FormatNodeRule for FormatArguments { } = item; let saved_level = f.context().node_level(); - f.context_mut().set_node_level(NodeLevel::Expression); + f.context_mut() + .set_node_level(NodeLevel::ParenthesizedExpression); let comments = f.context().comments().clone(); let dangling = comments.dangling_comments(item); 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 399e4ab915..71b76f27cd 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,33 +203,7 @@ class C: print(i) xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( push_manager=context.request.resource_manager, -@@ -47,113 +47,46 @@ - def omitting_trailers(self) -> None: - get_collection( - hey_this_is_a_very_long_call, it_has_funny_attributes, really=True -- )[OneLevelIndex] -+ )[ -+ OneLevelIndex -+ ] - get_collection( - hey_this_is_a_very_long_call, it_has_funny_attributes, really=True -- )[OneLevelIndex][TwoLevelIndex][ThreeLevelIndex][FourLevelIndex] -+ )[ -+ OneLevelIndex -+ ][ -+ TwoLevelIndex -+ ][ -+ ThreeLevelIndex -+ ][ -+ FourLevelIndex -+ ] - d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ - 22 - ] - assignment = ( -- some.rather.elaborate.rule() and another.rule.ending_with.index[123] -+ some.rather.elaborate.rule() -+ and another.rule.ending_with.index[123] +@@ -59,101 +59,23 @@ ) def easy_asserts(self) -> None: @@ -340,7 +314,7 @@ class C: %3d 0 LOAD_FAST 1 (x) 2 LOAD_CONST 1 (1) 4 COMPARE_OP 2 (==) -@@ -161,21 +94,8 @@ +@@ -161,21 +83,8 @@ 8 STORE_ATTR 0 (x) 10 LOAD_CONST 0 (None) 12 RETURN_VALUE @@ -419,26 +393,15 @@ class C: def omitting_trailers(self) -> None: get_collection( hey_this_is_a_very_long_call, it_has_funny_attributes, really=True - )[ - OneLevelIndex - ] + )[OneLevelIndex] get_collection( hey_this_is_a_very_long_call, it_has_funny_attributes, really=True - )[ - OneLevelIndex - ][ - TwoLevelIndex - ][ - ThreeLevelIndex - ][ - FourLevelIndex - ] + )[OneLevelIndex][TwoLevelIndex][ThreeLevelIndex][FourLevelIndex] d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ 22 ] assignment = ( - some.rather.elaborate.rule() - and another.rule.ending_with.index[123] + some.rather.elaborate.rule() and another.rule.ending_with.index[123] ) def easy_asserts(self) -> None: 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 69415159ce..975059e7a0 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,33 +203,7 @@ class C: print(i) xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( push_manager=context.request.resource_manager, -@@ -47,113 +47,46 @@ - def omitting_trailers(self) -> None: - get_collection( - hey_this_is_a_very_long_call, it_has_funny_attributes, really=True -- )[OneLevelIndex] -+ )[ -+ OneLevelIndex -+ ] - get_collection( - hey_this_is_a_very_long_call, it_has_funny_attributes, really=True -- )[OneLevelIndex][TwoLevelIndex][ThreeLevelIndex][FourLevelIndex] -+ )[ -+ OneLevelIndex -+ ][ -+ TwoLevelIndex -+ ][ -+ ThreeLevelIndex -+ ][ -+ FourLevelIndex -+ ] - d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ - 22 - ] - assignment = ( -- some.rather.elaborate.rule() and another.rule.ending_with.index[123] -+ some.rather.elaborate.rule() -+ and another.rule.ending_with.index[123] +@@ -59,101 +59,23 @@ ) def easy_asserts(self) -> None: @@ -340,7 +314,7 @@ class C: %3d 0 LOAD_FAST 1 (x) 2 LOAD_CONST 1 (1) 4 COMPARE_OP 2 (==) -@@ -161,21 +94,8 @@ +@@ -161,21 +83,8 @@ 8 STORE_ATTR 0 (x) 10 LOAD_CONST 0 (None) 12 RETURN_VALUE @@ -419,26 +393,15 @@ class C: def omitting_trailers(self) -> None: get_collection( hey_this_is_a_very_long_call, it_has_funny_attributes, really=True - )[ - OneLevelIndex - ] + )[OneLevelIndex] get_collection( hey_this_is_a_very_long_call, it_has_funny_attributes, really=True - )[ - OneLevelIndex - ][ - TwoLevelIndex - ][ - ThreeLevelIndex - ][ - FourLevelIndex - ] + )[OneLevelIndex][TwoLevelIndex][ThreeLevelIndex][FourLevelIndex] d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ 22 ] assignment = ( - some.rather.elaborate.rule() - and another.rule.ending_with.index[123] + some.rather.elaborate.rule() and another.rule.ending_with.index[123] ) def easy_asserts(self) -> None: diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__empty_lines.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__empty_lines.py.snap index 598235141b..4cfde0d1a6 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__empty_lines.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__empty_lines.py.snap @@ -113,61 +113,15 @@ def g(): prev = leaf.prev_sibling if not prev: -@@ -25,23 +25,31 @@ - return NO - - if prevp.type == token.EQUAL: -- if prevp.parent and prevp.parent.type in { -- syms.typedargslist, -- syms.varargslist, -- syms.parameters, -- syms.arglist, -- syms.argument, -- }: -+ if ( -+ prevp.parent -+ and prevp.parent.type -+ in { -+ syms.typedargslist, -+ syms.varargslist, -+ syms.parameters, -+ syms.arglist, -+ syms.argument, -+ } -+ ): - return NO - - elif prevp.type == token.DOUBLESTAR: -- if prevp.parent and prevp.parent.type in { -- syms.typedargslist, -- syms.varargslist, -- syms.parameters, -- syms.arglist, -- syms.dictsetmaker, -- }: -+ if ( -+ prevp.parent -+ and prevp.parent.type -+ in { -+ syms.typedargslist, -+ syms.varargslist, -+ syms.parameters, -+ syms.arglist, -+ syms.dictsetmaker, -+ } -+ ): - return NO - - -@@ -49,7 +57,6 @@ +@@ -48,7 +48,6 @@ + ############################################################################### # SECTION BECAUSE SECTIONS ############################################################################### - - + def g(): NO = "" - SPACE = " " -@@ -67,7 +74,7 @@ +@@ -67,7 +66,7 @@ return DOUBLESPACE # Another comment because more comments @@ -176,29 +130,6 @@ def g(): prev = leaf.prev_sibling if not prev: -@@ -79,11 +86,15 @@ - return NO - - if prevp.type == token.EQUAL: -- if prevp.parent and prevp.parent.type in { -- syms.typedargslist, -- syms.varargslist, -- syms.parameters, -- syms.arglist, -- syms.argument, -- }: -+ if ( -+ prevp.parent -+ and prevp.parent.type -+ in { -+ syms.typedargslist, -+ syms.varargslist, -+ syms.parameters, -+ syms.arglist, -+ syms.argument, -+ } -+ ): - return NO ``` ## Ruff Output @@ -231,31 +162,23 @@ def f(): return NO if prevp.type == token.EQUAL: - if ( - prevp.parent - and prevp.parent.type - in { - syms.typedargslist, - syms.varargslist, - syms.parameters, - syms.arglist, - syms.argument, - } - ): + if prevp.parent and prevp.parent.type in { + syms.typedargslist, + syms.varargslist, + syms.parameters, + syms.arglist, + syms.argument, + }: return NO elif prevp.type == token.DOUBLESTAR: - if ( - prevp.parent - and prevp.parent.type - in { - syms.typedargslist, - syms.varargslist, - syms.parameters, - syms.arglist, - syms.dictsetmaker, - } - ): + if prevp.parent and prevp.parent.type in { + syms.typedargslist, + syms.varargslist, + syms.parameters, + syms.arglist, + syms.dictsetmaker, + }: return NO @@ -292,17 +215,13 @@ def g(): return NO if prevp.type == token.EQUAL: - if ( - prevp.parent - and prevp.parent.type - in { - syms.typedargslist, - syms.varargslist, - syms.parameters, - syms.arglist, - syms.argument, - } - ): + if prevp.parent and prevp.parent.type in { + syms.typedargslist, + syms.varargslist, + syms.parameters, + syms.arglist, + syms.argument, + }: return NO ``` 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 fcb6ee100b..388d0c3d4e 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,26 +73,7 @@ some_module.some_function( ```diff --- Black +++ Ruff -@@ -35,7 +35,9 @@ - x = { - "a": 1, - "b": 2, -- }["a"] -+ }[ -+ "a" -+ ] - if ( - a - == { -@@ -47,14 +49,16 @@ - "f": 6, - "g": 7, - "h": 8, -- }["a"] -+ }[ -+ "a" -+ ] - ): +@@ -52,9 +52,9 @@ pass @@ -105,7 +86,7 @@ some_module.some_function( json = { "k": { "k2": { -@@ -80,18 +84,14 @@ +@@ -80,18 +80,14 @@ pass @@ -170,9 +151,7 @@ def f( x = { "a": 1, "b": 2, - }[ - "a" - ] + }["a"] if ( a == { @@ -184,9 +163,7 @@ def f( "f": 6, "g": 7, "h": 8, - }[ - "a" - ] + }["a"] ): pass 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 d529f18f49..2468093c5f 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,7 +93,7 @@ async def main(): ```diff --- Black +++ Ruff -@@ -8,59 +8,70 @@ +@@ -8,28 +8,33 @@ # Remove brackets for short coroutine/task async def main(): @@ -122,10 +122,8 @@ async def main(): async def main(): - await asyncio.sleep(1) # Hello -+ ( -+ await ( -+ asyncio.sleep(1) # Hello -+ ) ++ await ( ++ asyncio.sleep(1) # Hello + ) @@ -135,50 +133,7 @@ async def main(): # Long lines - async def main(): -- await asyncio.gather( -- asyncio.sleep(1), -- asyncio.sleep(1), -- asyncio.sleep(1), -- asyncio.sleep(1), -- asyncio.sleep(1), -- asyncio.sleep(1), -- asyncio.sleep(1), -+ ( -+ await asyncio.gather( -+ asyncio.sleep(1), -+ asyncio.sleep(1), -+ asyncio.sleep(1), -+ asyncio.sleep(1), -+ asyncio.sleep(1), -+ asyncio.sleep(1), -+ asyncio.sleep(1), -+ ) - ) - - - # Same as above but with magic trailing comma in function - async def main(): -- await asyncio.gather( -- asyncio.sleep(1), -- asyncio.sleep(1), -- asyncio.sleep(1), -- asyncio.sleep(1), -- asyncio.sleep(1), -- asyncio.sleep(1), -- asyncio.sleep(1), -+ ( -+ await asyncio.gather( -+ asyncio.sleep(1), -+ asyncio.sleep(1), -+ asyncio.sleep(1), -+ asyncio.sleep(1), -+ asyncio.sleep(1), -+ asyncio.sleep(1), -+ asyncio.sleep(1), -+ ) - ) - +@@ -60,7 +65,7 @@ # Cr@zY Br@ck3Tz async def main(): @@ -187,7 +142,7 @@ async def main(): # Keep brackets around non power operations and nested awaits -@@ -78,16 +89,16 @@ +@@ -78,16 +83,16 @@ async def main(): @@ -243,10 +198,8 @@ async def main(): async def main(): - ( - await ( - asyncio.sleep(1) # Hello - ) + await ( + asyncio.sleep(1) # Hello ) @@ -256,31 +209,27 @@ async def main(): # Long lines async def main(): - ( - await asyncio.gather( - asyncio.sleep(1), - asyncio.sleep(1), - asyncio.sleep(1), - asyncio.sleep(1), - asyncio.sleep(1), - asyncio.sleep(1), - asyncio.sleep(1), - ) + await asyncio.gather( + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), ) # Same as above but with magic trailing comma in function async def main(): - ( - await asyncio.gather( - asyncio.sleep(1), - asyncio.sleep(1), - asyncio.sleep(1), - asyncio.sleep(1), - asyncio.sleep(1), - asyncio.sleep(1), - asyncio.sleep(1), - ) + await asyncio.gather( + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), ) diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__torture.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__torture.py.snap index 246fcc69bc..058819773d 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__torture.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__torture.py.snap @@ -64,7 +64,7 @@ assert ( importA 0 -@@ -25,34 +15,33 @@ +@@ -25,9 +15,7 @@ class A: def foo(self): for _ in range(10): @@ -75,46 +75,7 @@ assert ( def test(self, othr): -- return 1 == 2 and ( -- name, -- description, -- self.default, -- self.selected, -- self.auto_generated, -- self.parameters, -- self.meta_data, -- self.schedule, -- ) == ( -- name, -- description, -- othr.default, -- othr.selected, -- othr.auto_generated, -- othr.parameters, -- othr.meta_data, -- othr.schedule, -+ return ( -+ 1 == 2 -+ and ( -+ name, -+ description, -+ self.default, -+ self.selected, -+ self.auto_generated, -+ self.parameters, -+ self.meta_data, -+ self.schedule, -+ ) -+ == ( -+ name, -+ description, -+ othr.default, -+ othr.selected, -+ othr.auto_generated, -+ othr.parameters, -+ othr.meta_data, -+ othr.schedule, -+ ) +@@ -52,7 +40,4 @@ ) @@ -149,28 +110,24 @@ class A: def test(self, othr): - return ( - 1 == 2 - and ( - name, - description, - self.default, - self.selected, - self.auto_generated, - self.parameters, - self.meta_data, - self.schedule, - ) - == ( - name, - description, - othr.default, - othr.selected, - othr.auto_generated, - othr.parameters, - othr.meta_data, - othr.schedule, - ) + return 1 == 2 and ( + name, + description, + self.default, + self.selected, + self.auto_generated, + self.parameters, + self.meta_data, + self.schedule, + ) == ( + name, + description, + othr.default, + othr.selected, + othr.auto_generated, + othr.parameters, + othr.meta_data, + othr.schedule, ) diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens1.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens1.py.snap index 35f9fc85aa..7fb41572f3 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens1.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens1.py.snap @@ -37,17 +37,7 @@ class A: ```diff --- Black +++ Ruff -@@ -1,18 +1,16 @@ --if e1234123412341234.winerror not in ( -- _winapi.ERROR_SEM_TIMEOUT, -- _winapi.ERROR_PIPE_BUSY, --) or _check_timeout(t): -+if ( -+ e1234123412341234.winerror -+ not in (_winapi.ERROR_SEM_TIMEOUT, _winapi.ERROR_PIPE_BUSY) -+ or _check_timeout(t) -+): - pass +@@ -6,13 +6,10 @@ if x: if y: @@ -65,36 +55,15 @@ class A: class X: -@@ -26,9 +24,14 @@ - - class A: - def b(self): -- if self.connection.mysql_is_mariadb and ( -- 10, -- 4, -- 3, -- ) < self.connection.mysql_version < (10, 5, 2): -+ if ( -+ self.connection.mysql_is_mariadb -+ and ( -+ 10, -+ 4, -+ 3, -+ ) -+ < self.connection.mysql_version -+ < (10, 5, 2) -+ ): - pass ``` ## Ruff Output ```py -if ( - e1234123412341234.winerror - not in (_winapi.ERROR_SEM_TIMEOUT, _winapi.ERROR_PIPE_BUSY) - or _check_timeout(t) -): +if e1234123412341234.winerror not in ( + _winapi.ERROR_SEM_TIMEOUT, + _winapi.ERROR_PIPE_BUSY, +) or _check_timeout(t): pass if x: @@ -116,16 +85,11 @@ class X: class A: def b(self): - if ( - self.connection.mysql_is_mariadb - and ( - 10, - 4, - 3, - ) - < self.connection.mysql_version - < (10, 5, 2) - ): + if self.connection.mysql_is_mariadb and ( + 10, + 4, + 3, + ) < self.connection.mysql_version < (10, 5, 2): pass ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens2.py.snap deleted file mode 100644 index bc155f8982..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens2.py.snap +++ /dev/null @@ -1,52 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_comma_optional_parens2.py ---- -## Input - -```py -if (e123456.get_tk_patchlevel() >= (8, 6, 0, 'final') or - (8, 5, 8) <= get_tk_patchlevel() < (8, 6)): - pass -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,6 +1,5 @@ --if e123456.get_tk_patchlevel() >= (8, 6, 0, "final") or ( -- 8, -- 5, -- 8, --) <= get_tk_patchlevel() < (8, 6): -+if ( -+ e123456.get_tk_patchlevel() >= (8, 6, 0, "final") -+ or (8, 5, 8) <= get_tk_patchlevel() < (8, 6) -+): - pass -``` - -## Ruff Output - -```py -if ( - e123456.get_tk_patchlevel() >= (8, 6, 0, "final") - or (8, 5, 8) <= get_tk_patchlevel() < (8, 6) -): - pass -``` - -## Black Output - -```py -if e123456.get_tk_patchlevel() >= (8, 6, 0, "final") or ( - 8, - 5, - 8, -) <= get_tk_patchlevel() < (8, 6): - pass -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens3.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens3.py.snap new file mode 100644 index 0000000000..d13dff5ee4 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens3.py.snap @@ -0,0 +1,77 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_comma_optional_parens3.py +--- +## Input + +```py +if True: + if True: + if True: + return _( + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas " + + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", + ) % {"reported_username": reported_username, "report_reason": report_reason} +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,8 +1,14 @@ + if True: + if True: + if True: +- return _( +- "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas " +- + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", +- "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", +- ) % {"reported_username": reported_username, "report_reason": report_reason} ++ return ( ++ _( ++ "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas " ++ + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", ++ "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", ++ ) ++ % { ++ "reported_username": reported_username, ++ "report_reason": report_reason, ++ } ++ ) +``` + +## Ruff Output + +```py +if True: + if True: + if True: + return ( + _( + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas " + + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", + ) + % { + "reported_username": reported_username, + "report_reason": report_reason, + } + ) +``` + +## Black Output + +```py +if True: + if True: + if True: + return _( + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas " + + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", + ) % {"reported_username": reported_username, "report_reason": report_reason} +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_commas_in_leading_parts.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_commas_in_leading_parts.py.snap index b50a6c5ff7..5084e5caa4 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_commas_in_leading_parts.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_commas_in_leading_parts.py.snap @@ -56,19 +56,7 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( # Example from https://github.com/psf/black/issues/3229 -@@ -32,19 +30,18 @@ - "refreshToken": refresh_token, - }, - api_key=api_key, -- )["extensions"]["sdk"]["token"] -+ )[ -+ "extensions" -+ ][ -+ "sdk" -+ ][ -+ "token" -+ ] - +@@ -37,14 +35,7 @@ # Edge case where a bug in a working-in-progress version of # https://github.com/psf/black/pull/3370 causes an infinite recursion. @@ -122,13 +110,7 @@ def refresh_token(self, device_family, refresh_token, api_key): "refreshToken": refresh_token, }, api_key=api_key, - )[ - "extensions" - ][ - "sdk" - ][ - "token" - ] + )["extensions"]["sdk"]["token"] # Edge case where a bug in a working-in-progress version of 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 890bfd41cb..9dea01bb94 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 @@ -476,9 +476,9 @@ if ( # Unstable formatting in https://github.com/realtyem/synapse-unraid/blob/unraid_develop/synapse/handlers/presence.py -for user_id in set( - target_user_ids -) - {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set}: +for user_id in ( + set(target_user_ids) - {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} +): updates.append(UserPresenceState.default(user_id)) # Keeps parenthesized left hand sides 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 03d75d057b..9535adb8a0 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 @@ -110,28 +110,24 @@ a < b > c == d < ddddddddddddddddddddddddddddddddddddddddddd ) -return ( - 1 == 2 - and ( - name, - description, - self_default, - self_selected, - self_auto_generated, - self_parameters, - self_meta_data, - self_schedule, - ) - == ( - name, - description, - othr_default, - othr_selected, - othr_auto_generated, - othr_parameters, - othr_meta_data, - othr_schedule, - ) +return 1 == 2 and ( + name, + description, + self_default, + self_selected, + self_auto_generated, + self_parameters, + self_meta_data, + self_schedule, +) == ( + name, + description, + othr_default, + othr_selected, + othr_auto_generated, + othr_parameters, + othr_meta_data, + othr_schedule, ) ( diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap index c6f1534b31..e0325c897b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap @@ -256,13 +256,10 @@ if aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & ( ): pass -if ( - not ( - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb - ) - & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -): +if not ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +) & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: pass diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__raise.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__raise.py.snap index d36d48fbfb..f9e1808758 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__raise.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__raise.py.snap @@ -143,8 +143,10 @@ raise ( + cccccccccccccccccccccc + ddddddddddddddddddddddddd ) -raise aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbb + ( - cccccccccccccccccccccc + ddddddddddddddddddddddddd +raise ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbb + + (cccccccccccccccccccccc + ddddddddddddddddddddddddd) ) raise ( aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa From 9a8ba58b4cbb2dcbebfe1397ebdb867b641e30ce Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 11 Jul 2023 14:19:26 +0200 Subject: [PATCH 414/447] Remove `mode` from `BestFitting` ## Summary This PR removes the `mode` field from `BestFitting` because it is no longer used (we now use `conditional_group` and `fits_expanded). ## Test Plan `cargo test` --- crates/ruff_formatter/src/builders.rs | 112 +----------------- crates/ruff_formatter/src/format_element.rs | 31 +---- .../src/format_element/document.rs | 14 +-- crates/ruff_formatter/src/printer/mod.rs | 31 ++--- 4 files changed, 13 insertions(+), 175 deletions(-) diff --git a/crates/ruff_formatter/src/builders.rs b/crates/ruff_formatter/src/builders.rs index 29a3f914ea..8e5ed0b8a0 100644 --- a/crates/ruff_formatter/src/builders.rs +++ b/crates/ruff_formatter/src/builders.rs @@ -2357,7 +2357,6 @@ impl<'a, 'buf, Context> FillBuilder<'a, 'buf, Context> { #[derive(Copy, Clone)] pub struct BestFitting<'a, Context> { variants: Arguments<'a, Context>, - mode: BestFittingMode, } impl<'a, Context> BestFitting<'a, Context> { @@ -2375,115 +2374,7 @@ impl<'a, Context> BestFitting<'a, Context> { "Requires at least the least expanded and most expanded variants" ); - Self { - variants, - mode: BestFittingMode::default(), - } - } - - /// Changes the mode used by this best fitting element to determine whether a variant fits. - /// - /// ## Examples - /// - /// ### All Lines - /// - /// ``` - /// use ruff_formatter::{Formatted, LineWidth, format, format_args, SimpleFormatOptions}; - /// use ruff_formatter::prelude::*; - /// - /// # fn main() -> FormatResult<()> { - /// let formatted = format!( - /// SimpleFormatContext::default(), - /// [ - /// best_fitting!( - /// // Everything fits on a single line - /// format_args!( - /// group(&format_args![ - /// text("["), - /// soft_block_indent(&format_args![ - /// text("1,"), - /// soft_line_break_or_space(), - /// text("2,"), - /// soft_line_break_or_space(), - /// text("3"), - /// ]), - /// text("]") - /// ]), - /// space(), - /// text("+"), - /// space(), - /// text("aVeryLongIdentifier") - /// ), - /// - /// // Breaks after `[` and prints each elements on a single line - /// // The group is necessary because the variant, by default is printed in flat mode and a - /// // hard line break indicates that the content doesn't fit. - /// format_args!( - /// text("["), - /// group(&block_indent(&format_args![text("1,"), hard_line_break(), text("2,"), hard_line_break(), text("3")])).should_expand(true), - /// text("]"), - /// space(), - /// text("+"), - /// space(), - /// text("aVeryLongIdentifier") - /// ), - /// - /// // Adds parentheses and indents the body, breaks after the operator - /// format_args!( - /// text("("), - /// block_indent(&format_args![ - /// text("["), - /// block_indent(&format_args![ - /// text("1,"), - /// hard_line_break(), - /// text("2,"), - /// hard_line_break(), - /// text("3"), - /// ]), - /// text("]"), - /// hard_line_break(), - /// text("+"), - /// space(), - /// text("aVeryLongIdentifier") - /// ]), - /// text(")") - /// ) - /// ).with_mode(BestFittingMode::AllLines) - /// ] - /// )?; - /// - /// let document = formatted.into_document(); - /// - /// // Takes the first variant if everything fits on a single line - /// assert_eq!( - /// "[1, 2, 3] + aVeryLongIdentifier", - /// Formatted::new(document.clone(), SimpleFormatContext::default()) - /// .print()? - /// .as_code() - /// ); - /// - /// // It takes the second if the first variant doesn't fit on a single line. The second variant - /// // has some additional line breaks to make sure inner groups don't break - /// assert_eq!( - /// "[\n\t1,\n\t2,\n\t3\n] + aVeryLongIdentifier", - /// Formatted::new(document.clone(), SimpleFormatContext::new(SimpleFormatOptions { line_width: 23.try_into().unwrap(), ..SimpleFormatOptions::default() })) - /// .print()? - /// .as_code() - /// ); - /// - /// // Prints the last option as last resort - /// assert_eq!( - /// "(\n\t[\n\t\t1,\n\t\t2,\n\t\t3\n\t]\n\t+ aVeryLongIdentifier\n)", - /// Formatted::new(document.clone(), SimpleFormatContext::new(SimpleFormatOptions { line_width: 22.try_into().unwrap(), ..SimpleFormatOptions::default() })) - /// .print()? - /// .as_code() - /// ); - /// # Ok(()) - /// # } - /// ``` - pub fn with_mode(mut self, mode: BestFittingMode) -> Self { - self.mode = mode; - self + Self { variants } } } @@ -2509,7 +2400,6 @@ impl Format for BestFitting<'_, Context> { variants: format_element::BestFittingVariants::from_vec_unchecked( formatted_variants, ), - mode: self.mode, } }; diff --git a/crates/ruff_formatter/src/format_element.rs b/crates/ruff_formatter/src/format_element.rs index 5c5b3ff3e9..7506c9cb75 100644 --- a/crates/ruff_formatter/src/format_element.rs +++ b/crates/ruff_formatter/src/format_element.rs @@ -57,10 +57,7 @@ pub enum FormatElement { /// A list of different variants representing the same content. The printer picks the best fitting content. /// Line breaks inside of a best fitting don't propagate to parent groups. - BestFitting { - variants: BestFittingVariants, - mode: BestFittingMode, - }, + BestFitting { variants: BestFittingVariants }, /// A [Tag] that marks the start/end of some content to which some special formatting is applied. Tag(Tag), @@ -87,10 +84,9 @@ impl std::fmt::Debug for FormatElement { .field(contains_newlines) .finish(), FormatElement::LineSuffixBoundary => write!(fmt, "LineSuffixBoundary"), - FormatElement::BestFitting { variants, mode } => fmt + FormatElement::BestFitting { variants } => fmt .debug_struct("BestFitting") .field("variants", variants) - .field("mode", &mode) .finish(), FormatElement::Interned(interned) => { fmt.debug_list().entries(interned.deref()).finish() @@ -301,29 +297,6 @@ impl FormatElements for FormatElement { } } -/// Mode used to determine if any variant (except the most expanded) fits for [`BestFittingVariants`]. -#[repr(u8)] -#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] -pub enum BestFittingMode { - /// The variant fits if the content up to the first hard or a soft line break inside a [`Group`] with - /// [`PrintMode::Expanded`] fits on the line. The default mode. - /// - /// [`Group`]: tag::Group - #[default] - FirstLine, - - /// A variant fits if all lines fit into the configured print width. A line ends if by any - /// hard or a soft line break inside a [`Group`] with [`PrintMode::Expanded`]. - /// The content doesn't fit if there's any hard line break outside a [`Group`] with [`PrintMode::Expanded`] - /// (a hard line break in content that should be considered in [`PrintMode::Flat`]. - /// - /// Use this mode with caution as it requires measuring all content of the variant which is more - /// expensive than using [`BestFittingMode::FirstLine`]. - /// - /// [`Group`]: tag::Group - AllLines, -} - /// The different variants for this element. /// The first element is the one that takes up the most space horizontally (the most flat), /// The last element takes up the least space horizontally (but most horizontal space). diff --git a/crates/ruff_formatter/src/format_element/document.rs b/crates/ruff_formatter/src/format_element/document.rs index 1f031f0e1d..7eb1c37156 100644 --- a/crates/ruff_formatter/src/format_element/document.rs +++ b/crates/ruff_formatter/src/format_element/document.rs @@ -80,7 +80,7 @@ impl Document { interned_expands } }, - FormatElement::BestFitting { variants, mode: _ } => { + FormatElement::BestFitting { variants } => { enclosing.push(Enclosing::BestFitting); for variant in variants { @@ -303,7 +303,7 @@ impl Format> for &[FormatElement] { write!(f, [text("line_suffix_boundary")])?; } - FormatElement::BestFitting { variants, mode } => { + FormatElement::BestFitting { variants } => { write!(f, [text("best_fitting([")])?; f.write_elements([ FormatElement::Tag(StartIndent), @@ -319,16 +319,6 @@ impl Format> for &[FormatElement] { FormatElement::Line(LineMode::Hard), ])?; - if *mode != BestFittingMode::FirstLine { - write!( - f, - [ - dynamic_text(&std::format!("mode: {mode:?},"), None), - space() - ] - )?; - } - write!(f, [text("])")])?; } diff --git a/crates/ruff_formatter/src/printer/mod.rs b/crates/ruff_formatter/src/printer/mod.rs index a58514c102..2bcb7fe582 100644 --- a/crates/ruff_formatter/src/printer/mod.rs +++ b/crates/ruff_formatter/src/printer/mod.rs @@ -6,7 +6,7 @@ mod stack; use crate::format_element::document::Document; use crate::format_element::tag::{Condition, GroupMode}; -use crate::format_element::{BestFittingMode, BestFittingVariants, LineMode, PrintMode}; +use crate::format_element::{BestFittingVariants, LineMode, PrintMode}; use crate::prelude::tag; use crate::prelude::tag::{DedentMode, Tag, TagKind, VerbatimKind}; use crate::printer::call_stack::{ @@ -135,8 +135,8 @@ impl<'a> Printer<'a> { self.flush_line_suffixes(queue, stack, Some(HARD_BREAK)); } - FormatElement::BestFitting { variants, mode } => { - self.print_best_fitting(variants, *mode, queue, stack)?; + FormatElement::BestFitting { variants } => { + self.print_best_fitting(variants, queue, stack)?; } FormatElement::Interned(content) => { @@ -426,7 +426,6 @@ impl<'a> Printer<'a> { fn print_best_fitting( &mut self, variants: &'a BestFittingVariants, - mode: BestFittingMode, queue: &mut PrintQueue<'a>, stack: &mut PrintCallStack, ) -> PrintResult<()> { @@ -452,9 +451,7 @@ impl<'a> Printer<'a> { // args must be popped from the stack as soon as it sees the matching end entry. let content = &variant[1..]; - let entry_args = args - .with_print_mode(PrintMode::Flat) - .with_measure_mode(MeasureMode::from(mode)); + let entry_args = args.with_print_mode(PrintMode::Flat); queue.extend_back(content); stack.push(TagKind::Entry, entry_args); @@ -1065,13 +1062,10 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { FormatElement::SourcePosition(_) => {} - FormatElement::BestFitting { variants, mode } => { - let (slice, args) = match args.mode() { - PrintMode::Flat => ( - variants.most_flat(), - args.with_measure_mode(MeasureMode::from(*mode)), - ), - PrintMode::Expanded => (variants.most_expanded(), args), + FormatElement::BestFitting { variants } => { + let slice = match args.mode() { + PrintMode::Flat => variants.most_flat(), + PrintMode::Expanded => variants.most_expanded(), }; if !matches!(slice.first(), Some(FormatElement::Tag(Tag::StartEntry))) { @@ -1385,15 +1379,6 @@ enum MeasureMode { AllLines, } -impl From for MeasureMode { - fn from(value: BestFittingMode) -> Self { - match value { - BestFittingMode::FirstLine => Self::FirstLine, - BestFittingMode::AllLines => Self::AllLines, - } - } -} - #[cfg(test)] mod tests { use crate::prelude::*; From 8665a1a19d80fcba85446c844c409e8702b1a078 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 11 Jul 2023 14:28:50 +0200 Subject: [PATCH 415/447] Pass `FormatContext` to `NeedsParentheses` ## Summary I started working on this because I assumed that I would need access to options inside of `NeedsParantheses` but it then turned out that I won't. Anyway, it kind of felt nice to pass fewer arguments. So I'm gonna put this out here to get your feedback if you prefer this over passing individual fiels. Oh, I sneeked in another change. I renamed `context.contents` to `source`. `contents` is too generic and doesn't tell you anything. ## Test Plan It compiles --- crates/ruff_python_formatter/src/builders.rs | 4 +- .../src/comments/format.rs | 14 ++-- crates/ruff_python_formatter/src/context.rs | 2 +- .../src/expression/expr_attribute.rs | 7 +- .../src/expression/expr_await.rs | 7 +- .../src/expression/expr_bin_op.rs | 9 ++- .../src/expression/expr_bool_op.rs | 7 +- .../src/expression/expr_call.rs | 8 +-- .../src/expression/expr_compare.rs | 7 +- .../src/expression/expr_constant.rs | 6 +- .../src/expression/expr_dict.rs | 7 +- .../src/expression/expr_dict_comp.rs | 7 +- .../src/expression/expr_formatted_value.rs | 7 +- .../src/expression/expr_generator_exp.rs | 7 +- .../src/expression/expr_if_exp.rs | 7 +- .../src/expression/expr_joined_str.rs | 7 +- .../src/expression/expr_lambda.rs | 7 +- .../src/expression/expr_list.rs | 7 +- .../src/expression/expr_list_comp.rs | 7 +- .../src/expression/expr_name.rs | 6 +- .../src/expression/expr_named_expr.rs | 7 +- .../src/expression/expr_set.rs | 6 +- .../src/expression/expr_set_comp.rs | 7 +- .../src/expression/expr_slice.rs | 11 ++-- .../src/expression/expr_starred.rs | 7 +- .../src/expression/expr_subscript.rs | 8 +-- .../src/expression/expr_tuple.rs | 9 ++- .../src/expression/expr_unary_op.rs | 10 +-- .../src/expression/expr_yield.rs | 7 +- .../src/expression/expr_yield_from.rs | 7 +- .../src/expression/mod.rs | 66 +++++++++---------- .../src/expression/parentheses.rs | 11 ++-- .../src/other/arguments.rs | 6 +- .../src/statement/stmt_class_def.rs | 2 +- .../src/statement/stmt_expr.rs | 2 +- .../src/statement/stmt_function_def.rs | 4 +- .../src/statement/suite.rs | 2 +- 37 files changed, 138 insertions(+), 174 deletions(-) diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index d39ff8a46d..8eeb07a819 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -94,7 +94,7 @@ impl<'fmt, 'ast, 'buf> JoinNodesBuilder<'fmt, 'ast, 'buf> { self.result = self.result.and_then(|_| { if let Some(last_end) = self.last_end.replace(node.end()) { - let source = self.fmt.context().contents(); + let source = self.fmt.context().source(); let count_lines = |offset| { // It's necessary to skip any trailing line comment because RustPython doesn't include trailing comments // in the node's range @@ -262,7 +262,7 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { 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()), + first_non_trivia_token(last_end, self.fmt.context().source()), Some(Token { kind: TokenKind::Comma, .. diff --git a/crates/ruff_python_formatter/src/comments/format.rs b/crates/ruff_python_formatter/src/comments/format.rs index c0a0a2a5bd..84b0e3b654 100644 --- a/crates/ruff_python_formatter/src/comments/format.rs +++ b/crates/ruff_python_formatter/src/comments/format.rs @@ -43,7 +43,7 @@ impl Format> for FormatLeadingComments<'_> { { let slice = comment.slice(); - let lines_after_comment = lines_after(slice.end(), f.context().contents()); + let lines_after_comment = lines_after(slice.end(), f.context().source()); write!( f, [format_comment(comment), empty_lines(lines_after_comment)] @@ -84,16 +84,16 @@ impl Format> for FormatLeadingAlternateBranchComments<'_> { if let Some(first_leading) = self.comments.first() { // Leading comments only preserves the lines after the comment but not before. // Insert the necessary lines. - if lines_before(first_leading.slice().start(), f.context().contents()) > 1 { + if lines_before(first_leading.slice().start(), f.context().source()) > 1 { write!(f, [empty_line()])?; } write!(f, [leading_comments(self.comments)])?; } else if let Some(last_preceding) = self.last_node { - let full_end = skip_trailing_trivia(last_preceding.end(), f.context().contents()); + let full_end = skip_trailing_trivia(last_preceding.end(), f.context().source()); // The leading comments formatting ensures that it preserves the right amount of lines after // We need to take care of this ourselves, if there's no leading `else` comment. - if lines_after(full_end, f.context().contents()) > 1 { + if lines_after(full_end, f.context().source()) > 1 { write!(f, [empty_line()])?; } } @@ -140,7 +140,7 @@ impl Format> for FormatTrailingComments<'_> { has_trailing_own_line_comment |= trailing.line_position().is_own_line(); if has_trailing_own_line_comment { - let lines_before_comment = lines_before(slice.start(), f.context().contents()); + let lines_before_comment = lines_before(slice.start(), f.context().source()); // A trailing comment at the end of a body or list // ```python @@ -217,7 +217,7 @@ impl Format> for FormatDanglingComments<'_> { f, [ format_comment(comment), - empty_lines(lines_after(comment.slice().end(), f.context().contents())) + empty_lines(lines_after(comment.slice().end(), f.context().source())) ] )?; @@ -245,7 +245,7 @@ struct FormatComment<'a> { impl Format> for FormatComment<'_> { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { let slice = self.comment.slice(); - let comment_text = slice.text(SourceCode::new(f.context().contents())); + let comment_text = slice.text(SourceCode::new(f.context().source())); let trimmed = comment_text.trim_end(); let trailing_whitespace_len = comment_text.text_len() - trimmed.text_len(); diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index 2683641dd8..d0828745c8 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -22,7 +22,7 @@ impl<'a> PyFormatContext<'a> { } } - pub(crate) fn contents(&self) -> &'a str { + pub(crate) fn source(&self) -> &'a str { self.contents } diff --git a/crates/ruff_python_formatter/src/expression/expr_attribute.rs b/crates/ruff_python_formatter/src/expression/expr_attribute.rs index 16ccaa330c..2fd19d768f 100644 --- a/crates/ruff_python_formatter/src/expression/expr_attribute.rs +++ b/crates/ruff_python_formatter/src/expression/expr_attribute.rs @@ -97,14 +97,13 @@ impl NeedsParentheses for ExprAttribute { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - if has_breaking_comments_attribute_chain(self, comments) { + if has_breaking_comments_attribute_chain(self, context.comments()) { return Parentheses::Always; } - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + match default_expression_needs_parentheses(self.into(), parenthesize, context) { Parentheses::Optional => Parentheses::Never, parentheses => parentheses, } diff --git a/crates/ruff_python_formatter/src/expression/expr_await.rs b/crates/ruff_python_formatter/src/expression/expr_await.rs index 19c6bf5196..6e275fd9a1 100644 --- a/crates/ruff_python_formatter/src/expression/expr_await.rs +++ b/crates/ruff_python_formatter/src/expression/expr_await.rs @@ -1,4 +1,4 @@ -use crate::comments::Comments; +use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; @@ -21,9 +21,8 @@ impl NeedsParentheses for ExprAwait { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + default_expression_needs_parentheses(self.into(), parenthesize, context) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs index 9307ed2043..f6f051066a 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs @@ -1,4 +1,4 @@ -use crate::comments::{trailing_comments, trailing_node_comments, Comments}; +use crate::comments::{trailing_comments, trailing_node_comments}; use crate::expression::parentheses::{ default_expression_needs_parentheses, in_parentheses_only_group, is_expression_parenthesized, NeedsParentheses, Parenthesize, @@ -33,7 +33,7 @@ impl FormatNodeRule for FormatExprBinOp { let comments = f.context().comments().clone(); let format_inner = format_with(|f: &mut PyFormatter| { - let source = f.context().contents(); + let source = f.context().source(); let binary_chain: SmallVec<[&ExprBinOp; 4]> = iter::successors(Some(item), |parent| { parent.left.as_bin_op_expr().and_then(|bin_expression| { if is_expression_parenthesized(bin_expression.as_any_node_ref(), source) { @@ -176,9 +176,8 @@ impl NeedsParentheses for ExprBinOp { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + default_expression_needs_parentheses(self.into(), parenthesize, context) } } 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 5a35064509..9ba2f9a2b2 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bool_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bool_op.rs @@ -1,4 +1,4 @@ -use crate::comments::{leading_comments, Comments}; +use crate::comments::leading_comments; use crate::expression::parentheses::{ default_expression_needs_parentheses, in_parentheses_only_group, NeedsParentheses, Parentheses, Parenthesize, @@ -71,10 +71,9 @@ impl NeedsParentheses for ExprBoolOp { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + default_expression_needs_parentheses(self.into(), parenthesize, context) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_call.rs b/crates/ruff_python_formatter/src/expression/expr_call.rs index 87ed281572..070027fc21 100644 --- a/crates/ruff_python_formatter/src/expression/expr_call.rs +++ b/crates/ruff_python_formatter/src/expression/expr_call.rs @@ -1,5 +1,6 @@ use crate::builders::PyFormatterExtensions; -use crate::comments::{dangling_comments, Comments}; +use crate::comments::dangling_comments; +use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, Parenthesize, @@ -88,10 +89,9 @@ impl NeedsParentheses for ExprCall { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + match default_expression_needs_parentheses(self.into(), parenthesize, context) { Parentheses::Optional => Parentheses::Never, parentheses => parentheses, } diff --git a/crates/ruff_python_formatter/src/expression/expr_compare.rs b/crates/ruff_python_formatter/src/expression/expr_compare.rs index 10bffe948b..988a64ddf4 100644 --- a/crates/ruff_python_formatter/src/expression/expr_compare.rs +++ b/crates/ruff_python_formatter/src/expression/expr_compare.rs @@ -1,4 +1,4 @@ -use crate::comments::{leading_comments, Comments}; +use crate::comments::leading_comments; use crate::expression::parentheses::{ default_expression_needs_parentheses, in_parentheses_only_group, NeedsParentheses, Parentheses, Parenthesize, @@ -70,10 +70,9 @@ impl NeedsParentheses for ExprCompare { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + default_expression_needs_parentheses(self.into(), parenthesize, context) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_constant.rs b/crates/ruff_python_formatter/src/expression/expr_constant.rs index 68d538ae8a..b657b16c28 100644 --- a/crates/ruff_python_formatter/src/expression/expr_constant.rs +++ b/crates/ruff_python_formatter/src/expression/expr_constant.rs @@ -1,4 +1,3 @@ -use crate::comments::Comments; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; @@ -63,10 +62,9 @@ impl NeedsParentheses for ExprConstant { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + match default_expression_needs_parentheses(self.into(), parenthesize, context) { Parentheses::Optional if self.value.is_str() && parenthesize.is_if_breaks() => { // Custom handling that only adds parentheses for implicit concatenated strings. if parenthesize.is_if_breaks() { diff --git a/crates/ruff_python_formatter/src/expression/expr_dict.rs b/crates/ruff_python_formatter/src/expression/expr_dict.rs index 6299fc0845..1f9fcca568 100644 --- a/crates/ruff_python_formatter/src/expression/expr_dict.rs +++ b/crates/ruff_python_formatter/src/expression/expr_dict.rs @@ -1,4 +1,4 @@ -use crate::comments::{dangling_node_comments, leading_comments, Comments}; +use crate::comments::{dangling_node_comments, leading_comments}; use crate::expression::parentheses::{ default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, Parenthesize, @@ -100,10 +100,9 @@ impl NeedsParentheses for ExprDict { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + match default_expression_needs_parentheses(self.into(), parenthesize, context) { Parentheses::Optional => Parentheses::Never, parentheses => parentheses, } diff --git a/crates/ruff_python_formatter/src/expression/expr_dict_comp.rs b/crates/ruff_python_formatter/src/expression/expr_dict_comp.rs index 4121acb76a..12af04579a 100644 --- a/crates/ruff_python_formatter/src/expression/expr_dict_comp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_dict_comp.rs @@ -1,4 +1,4 @@ -use crate::comments::Comments; +use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; @@ -24,10 +24,9 @@ impl NeedsParentheses for ExprDictComp { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + match default_expression_needs_parentheses(self.into(), parenthesize, context) { Parentheses::Optional => Parentheses::Never, parentheses => parentheses, } diff --git a/crates/ruff_python_formatter/src/expression/expr_formatted_value.rs b/crates/ruff_python_formatter/src/expression/expr_formatted_value.rs index ef2551de32..7cc71b967d 100644 --- a/crates/ruff_python_formatter/src/expression/expr_formatted_value.rs +++ b/crates/ruff_python_formatter/src/expression/expr_formatted_value.rs @@ -1,4 +1,4 @@ -use crate::comments::Comments; +use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; @@ -19,9 +19,8 @@ impl NeedsParentheses for ExprFormattedValue { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + default_expression_needs_parentheses(self.into(), parenthesize, context) } } 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 8cf7e8d38c..2f91b0c518 100644 --- a/crates/ruff_python_formatter/src/expression/expr_generator_exp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_generator_exp.rs @@ -1,4 +1,4 @@ -use crate::comments::Comments; +use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; @@ -24,10 +24,9 @@ impl NeedsParentheses for ExprGeneratorExp { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + match default_expression_needs_parentheses(self.into(), parenthesize, context) { Parentheses::Optional => Parentheses::Never, parentheses => parentheses, } diff --git a/crates/ruff_python_formatter/src/expression/expr_if_exp.rs b/crates/ruff_python_formatter/src/expression/expr_if_exp.rs index ae142e7122..85bcff304c 100644 --- a/crates/ruff_python_formatter/src/expression/expr_if_exp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_if_exp.rs @@ -1,4 +1,4 @@ -use crate::comments::{leading_comments, Comments}; +use crate::comments::leading_comments; use crate::expression::parentheses::{ default_expression_needs_parentheses, in_parentheses_only_group, NeedsParentheses, Parentheses, Parenthesize, @@ -47,9 +47,8 @@ impl NeedsParentheses for ExprIfExp { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + default_expression_needs_parentheses(self.into(), parenthesize, context) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_joined_str.rs b/crates/ruff_python_formatter/src/expression/expr_joined_str.rs index a13ff5f389..bd4bcf6fc9 100644 --- a/crates/ruff_python_formatter/src/expression/expr_joined_str.rs +++ b/crates/ruff_python_formatter/src/expression/expr_joined_str.rs @@ -1,4 +1,4 @@ -use crate::comments::Comments; +use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; @@ -19,9 +19,8 @@ impl NeedsParentheses for ExprJoinedStr { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + default_expression_needs_parentheses(self.into(), parenthesize, context) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_lambda.rs b/crates/ruff_python_formatter/src/expression/expr_lambda.rs index bd63bfa0f6..fa2a4ce3e7 100644 --- a/crates/ruff_python_formatter/src/expression/expr_lambda.rs +++ b/crates/ruff_python_formatter/src/expression/expr_lambda.rs @@ -1,4 +1,4 @@ -use crate::comments::Comments; +use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; @@ -19,9 +19,8 @@ impl NeedsParentheses for ExprLambda { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + default_expression_needs_parentheses(self.into(), parenthesize, context) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_list.rs b/crates/ruff_python_formatter/src/expression/expr_list.rs index 90d5e244e1..c090182762 100644 --- a/crates/ruff_python_formatter/src/expression/expr_list.rs +++ b/crates/ruff_python_formatter/src/expression/expr_list.rs @@ -1,4 +1,4 @@ -use crate::comments::{dangling_comments, CommentLinePosition, Comments}; +use crate::comments::{dangling_comments, CommentLinePosition}; use crate::expression::parentheses::{ default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, Parenthesize, @@ -68,10 +68,9 @@ impl NeedsParentheses for ExprList { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + match default_expression_needs_parentheses(self.into(), parenthesize, context) { Parentheses::Optional => Parentheses::Never, parentheses => parentheses, } 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 a6d5e236fd..a911ecf266 100644 --- a/crates/ruff_python_formatter/src/expression/expr_list_comp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_list_comp.rs @@ -1,4 +1,4 @@ -use crate::comments::Comments; +use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, Parenthesize, @@ -45,10 +45,9 @@ impl NeedsParentheses for ExprListComp { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + match default_expression_needs_parentheses(self.into(), parenthesize, context) { Parentheses::Optional => Parentheses::Never, parentheses => parentheses, } diff --git a/crates/ruff_python_formatter/src/expression/expr_name.rs b/crates/ruff_python_formatter/src/expression/expr_name.rs index 2567349481..ca07981ea1 100644 --- a/crates/ruff_python_formatter/src/expression/expr_name.rs +++ b/crates/ruff_python_formatter/src/expression/expr_name.rs @@ -1,4 +1,3 @@ -use crate::comments::Comments; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; @@ -30,10 +29,9 @@ impl NeedsParentheses for ExprName { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + default_expression_needs_parentheses(self.into(), parenthesize, context) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_named_expr.rs b/crates/ruff_python_formatter/src/expression/expr_named_expr.rs index 437a8981e3..e1ba5c1789 100644 --- a/crates/ruff_python_formatter/src/expression/expr_named_expr.rs +++ b/crates/ruff_python_formatter/src/expression/expr_named_expr.rs @@ -1,4 +1,4 @@ -use crate::comments::Comments; +use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; @@ -34,10 +34,9 @@ impl NeedsParentheses for ExprNamedExpr { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + match default_expression_needs_parentheses(self.into(), parenthesize, context) { // Unlike tuples, named expression parentheses are not part of the range even when // mandatory. See [PEP 572](https://peps.python.org/pep-0572/) for details. Parentheses::Optional => Parentheses::Always, diff --git a/crates/ruff_python_formatter/src/expression/expr_set.rs b/crates/ruff_python_formatter/src/expression/expr_set.rs index c1b1a90091..3af7376461 100644 --- a/crates/ruff_python_formatter/src/expression/expr_set.rs +++ b/crates/ruff_python_formatter/src/expression/expr_set.rs @@ -1,4 +1,3 @@ -use crate::comments::Comments; use crate::expression::parentheses::{ default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, Parenthesize, @@ -31,10 +30,9 @@ impl NeedsParentheses for ExprSet { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + match default_expression_needs_parentheses(self.into(), parenthesize, context) { Parentheses::Optional => Parentheses::Never, parentheses => parentheses, } diff --git a/crates/ruff_python_formatter/src/expression/expr_set_comp.rs b/crates/ruff_python_formatter/src/expression/expr_set_comp.rs index d5174782e0..e661b13386 100644 --- a/crates/ruff_python_formatter/src/expression/expr_set_comp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_set_comp.rs @@ -1,4 +1,4 @@ -use crate::comments::Comments; +use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; @@ -24,10 +24,9 @@ impl NeedsParentheses for ExprSetComp { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + match default_expression_needs_parentheses(self.into(), parenthesize, context) { Parentheses::Optional => Parentheses::Never, parentheses => parentheses, } diff --git a/crates/ruff_python_formatter/src/expression/expr_slice.rs b/crates/ruff_python_formatter/src/expression/expr_slice.rs index bc4fbba228..1dcf71099b 100644 --- a/crates/ruff_python_formatter/src/expression/expr_slice.rs +++ b/crates/ruff_python_formatter/src/expression/expr_slice.rs @@ -1,4 +1,5 @@ -use crate::comments::{dangling_comments, Comments, SourceComment}; +use crate::comments::{dangling_comments, SourceComment}; +use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; @@ -27,8 +28,7 @@ impl FormatNodeRule for FormatExprSlice { step, } = item; - let (first_colon, second_colon) = - find_colons(f.context().contents(), *range, lower, upper)?; + let (first_colon, second_colon) = find_colons(f.context().source(), *range, lower, upper)?; // Handle comment placement // In placements.rs, we marked comment for None nodes a dangling and associated all others @@ -263,9 +263,8 @@ impl NeedsParentheses for ExprSlice { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + default_expression_needs_parentheses(self.into(), parenthesize, context) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_starred.rs b/crates/ruff_python_formatter/src/expression/expr_starred.rs index bb8492e0da..e45e445676 100644 --- a/crates/ruff_python_formatter/src/expression/expr_starred.rs +++ b/crates/ruff_python_formatter/src/expression/expr_starred.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::ExprStarred; use ruff_formatter::write; -use crate::comments::Comments; +use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; @@ -34,9 +34,8 @@ impl NeedsParentheses for ExprStarred { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + default_expression_needs_parentheses(self.into(), parenthesize, context) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_subscript.rs b/crates/ruff_python_formatter/src/expression/expr_subscript.rs index 7010af60d6..7ac3073adc 100644 --- a/crates/ruff_python_formatter/src/expression/expr_subscript.rs +++ b/crates/ruff_python_formatter/src/expression/expr_subscript.rs @@ -3,8 +3,9 @@ use rustpython_parser::ast::ExprSubscript; use ruff_formatter::{format_args, write}; use ruff_python_ast::node::AstNode; -use crate::comments::{trailing_comments, Comments}; +use crate::comments::trailing_comments; use crate::context::NodeLevel; +use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; @@ -66,10 +67,9 @@ impl NeedsParentheses for ExprSubscript { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + match default_expression_needs_parentheses(self.into(), parenthesize, context) { Parentheses::Optional => Parentheses::Never, parentheses => parentheses, } diff --git a/crates/ruff_python_formatter/src/expression/expr_tuple.rs b/crates/ruff_python_formatter/src/expression/expr_tuple.rs index 82d716ae1d..81107f8422 100644 --- a/crates/ruff_python_formatter/src/expression/expr_tuple.rs +++ b/crates/ruff_python_formatter/src/expression/expr_tuple.rs @@ -1,5 +1,5 @@ use crate::builders::optional_parentheses; -use crate::comments::{dangling_comments, CommentLinePosition, Comments}; +use crate::comments::{dangling_comments, CommentLinePosition}; use crate::expression::parentheses::{ default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, Parenthesize, @@ -131,10 +131,9 @@ impl NeedsParentheses for ExprTuple { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + match default_expression_needs_parentheses(self.into(), parenthesize, context) { Parentheses::Optional => Parentheses::Never, parentheses => parentheses, } @@ -148,7 +147,7 @@ fn is_parenthesized( f: &mut Formatter>, ) -> bool { let parentheses = '('; - let first_char = &f.context().contents()[usize::from(tuple_range.start())..] + let first_char = &f.context().source()[usize::from(tuple_range.start())..] .chars() .next(); let Some(first_char) = first_char else { diff --git a/crates/ruff_python_formatter/src/expression/expr_unary_op.rs b/crates/ruff_python_formatter/src/expression/expr_unary_op.rs index 41a7feced2..8e2655d40b 100644 --- a/crates/ruff_python_formatter/src/expression/expr_unary_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_unary_op.rs @@ -1,4 +1,5 @@ -use crate::comments::{trailing_comments, Comments}; +use crate::comments::trailing_comments; +use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; @@ -70,13 +71,12 @@ impl NeedsParentheses for ExprUnaryOp { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { + match default_expression_needs_parentheses(self.into(), parenthesize, context) { Parentheses::Optional => { // We preserve the parentheses of the operand. It should not be necessary to break this expression. - if is_operand_parenthesized(self, source) { + if is_operand_parenthesized(self, context.source()) { Parentheses::Never } else { Parentheses::Optional diff --git a/crates/ruff_python_formatter/src/expression/expr_yield.rs b/crates/ruff_python_formatter/src/expression/expr_yield.rs index f2ece7ecac..a31c56bfbb 100644 --- a/crates/ruff_python_formatter/src/expression/expr_yield.rs +++ b/crates/ruff_python_formatter/src/expression/expr_yield.rs @@ -1,4 +1,4 @@ -use crate::comments::Comments; +use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; @@ -19,9 +19,8 @@ impl NeedsParentheses for ExprYield { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + default_expression_needs_parentheses(self.into(), parenthesize, context) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_yield_from.rs b/crates/ruff_python_formatter/src/expression/expr_yield_from.rs index bfaf101162..ae122ded3a 100644 --- a/crates/ruff_python_formatter/src/expression/expr_yield_from.rs +++ b/crates/ruff_python_formatter/src/expression/expr_yield_from.rs @@ -1,4 +1,4 @@ -use crate::comments::Comments; +use crate::context::PyFormatContext; use crate::expression::parentheses::{ default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, }; @@ -19,9 +19,8 @@ impl NeedsParentheses for ExprYieldFrom { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + default_expression_needs_parentheses(self.into(), parenthesize, context) } } diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index cc395311de..87ba364b56 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -9,7 +9,6 @@ use ruff_formatter::{ use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::visitor::preorder::{walk_expr, PreorderVisitor}; -use crate::comments::Comments; use crate::context::NodeLevel; use crate::expression::expr_tuple::TupleParentheses; use crate::expression::parentheses::{ @@ -64,11 +63,7 @@ impl FormatRuleWithOptions> for FormatExpr { impl FormatRule> for FormatExpr { fn fmt(&self, item: &Expr, f: &mut PyFormatter) -> FormatResult<()> { - let parentheses = item.needs_parentheses( - self.parenthesize, - f.context().contents(), - f.context().comments(), - ); + let parentheses = item.needs_parentheses(self.parenthesize, f.context()); let format_expr = format_with(|f| match item { Expr::BoolOp(expr) => expr.format().with_options(Some(parentheses)).fmt(f), @@ -172,37 +167,36 @@ impl NeedsParentheses for Expr { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { match self { - Expr::BoolOp(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::NamedExpr(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::BinOp(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::UnaryOp(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Lambda(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::IfExp(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Dict(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Set(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::ListComp(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::SetComp(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::DictComp(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::GeneratorExp(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Await(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Yield(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::YieldFrom(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Compare(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Call(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::FormattedValue(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::JoinedStr(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Constant(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Attribute(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Subscript(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Starred(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Name(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::List(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Tuple(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Slice(expr) => expr.needs_parentheses(parenthesize, source, comments), + Expr::BoolOp(expr) => expr.needs_parentheses(parenthesize, context), + Expr::NamedExpr(expr) => expr.needs_parentheses(parenthesize, context), + Expr::BinOp(expr) => expr.needs_parentheses(parenthesize, context), + Expr::UnaryOp(expr) => expr.needs_parentheses(parenthesize, context), + Expr::Lambda(expr) => expr.needs_parentheses(parenthesize, context), + Expr::IfExp(expr) => expr.needs_parentheses(parenthesize, context), + Expr::Dict(expr) => expr.needs_parentheses(parenthesize, context), + Expr::Set(expr) => expr.needs_parentheses(parenthesize, context), + Expr::ListComp(expr) => expr.needs_parentheses(parenthesize, context), + Expr::SetComp(expr) => expr.needs_parentheses(parenthesize, context), + Expr::DictComp(expr) => expr.needs_parentheses(parenthesize, context), + Expr::GeneratorExp(expr) => expr.needs_parentheses(parenthesize, context), + Expr::Await(expr) => expr.needs_parentheses(parenthesize, context), + Expr::Yield(expr) => expr.needs_parentheses(parenthesize, context), + Expr::YieldFrom(expr) => expr.needs_parentheses(parenthesize, context), + Expr::Compare(expr) => expr.needs_parentheses(parenthesize, context), + Expr::Call(expr) => expr.needs_parentheses(parenthesize, context), + Expr::FormattedValue(expr) => expr.needs_parentheses(parenthesize, context), + Expr::JoinedStr(expr) => expr.needs_parentheses(parenthesize, context), + Expr::Constant(expr) => expr.needs_parentheses(parenthesize, context), + Expr::Attribute(expr) => expr.needs_parentheses(parenthesize, context), + Expr::Subscript(expr) => expr.needs_parentheses(parenthesize, context), + Expr::Starred(expr) => expr.needs_parentheses(parenthesize, context), + Expr::Name(expr) => expr.needs_parentheses(parenthesize, context), + Expr::List(expr) => expr.needs_parentheses(parenthesize, context), + Expr::Tuple(expr) => expr.needs_parentheses(parenthesize, context), + Expr::Slice(expr) => expr.needs_parentheses(parenthesize, context), } } } @@ -233,7 +227,7 @@ impl<'ast> IntoFormat> for Expr { /// /// This mimics Black's [`_maybe_split_omitting_optional_parens`](https://github.com/psf/black/blob/d1248ca9beaf0ba526d265f4108836d89cf551b7/src/black/linegen.py#L746-L820) fn can_omit_optional_parentheses(expr: &Expr, context: &PyFormatContext) -> bool { - let mut visitor = MaxOperatorPriorityVisitor::new(context.contents()); + let mut visitor = MaxOperatorPriorityVisitor::new(context.source()); visitor.visit_subexpression(expr); diff --git a/crates/ruff_python_formatter/src/expression/parentheses.rs b/crates/ruff_python_formatter/src/expression/parentheses.rs index 444697213f..132e1cf942 100644 --- a/crates/ruff_python_formatter/src/expression/parentheses.rs +++ b/crates/ruff_python_formatter/src/expression/parentheses.rs @@ -1,4 +1,3 @@ -use crate::comments::Comments; use crate::context::NodeLevel; use crate::prelude::*; use crate::trivia::{first_non_trivia_token, first_non_trivia_token_rev, Token, TokenKind}; @@ -11,16 +10,14 @@ pub(crate) trait NeedsParentheses { fn needs_parentheses( &self, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses; } pub(super) fn default_expression_needs_parentheses( node: AnyNodeRef, parenthesize: Parenthesize, - source: &str, - comments: &Comments, + context: &PyFormatContext, ) -> Parentheses { debug_assert!( node.is_expression(), @@ -34,13 +31,13 @@ pub(super) fn default_expression_needs_parentheses( Parentheses::Never } // `Optional` or `Preserve` and expression has parentheses in source code. - else if !parenthesize.is_if_breaks() && is_expression_parenthesized(node, source) { + else if !parenthesize.is_if_breaks() && is_expression_parenthesized(node, context.source()) { Parentheses::Always } // `Optional` or `IfBreaks`: Add parentheses if the expression doesn't fit on a line but enforce // parentheses if the expression has leading comments else if !parenthesize.is_preserve() { - if comments.has_leading_comments(node) { + if context.comments().has_leading_comments(node) { Parentheses::Always } else { Parentheses::Optional diff --git a/crates/ruff_python_formatter/src/other/arguments.rs b/crates/ruff_python_formatter/src/other/arguments.rs index 0682a25632..3e84558ad2 100644 --- a/crates/ruff_python_formatter/src/other/arguments.rs +++ b/crates/ruff_python_formatter/src/other/arguments.rs @@ -36,7 +36,7 @@ impl FormatNodeRule for FormatArguments { let comments = f.context().comments().clone(); let dangling = comments.dangling_comments(item); - let (slash, star) = find_argument_separators(f.context().contents(), item); + let (slash, star) = find_argument_separators(f.context().source(), item); let format_inner = format_with(|f: &mut PyFormatter| { let separator = format_with(|f| write!(f, [text(","), soft_line_break_or_space()])); @@ -142,7 +142,7 @@ impl FormatNodeRule for FormatArguments { let maybe_comma_token = if ends_with_pos_only_argument_separator { // `def a(b, c, /): ... ` let mut tokens = - SimpleTokenizer::starts_at(last_node.end(), f.context().contents()) + SimpleTokenizer::starts_at(last_node.end(), f.context().source()) .skip_trivia(); let comma = tokens.next(); @@ -153,7 +153,7 @@ impl FormatNodeRule for FormatArguments { tokens.next() } else { - first_non_trivia_token(last_node.end(), f.context().contents()) + first_non_trivia_token(last_node.end(), f.context().source()) }; if maybe_comma_token.map_or(false, |token| token.kind() == TokenKind::Comma) { diff --git a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs index 3fba6d4a55..2388323ae1 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs @@ -79,7 +79,7 @@ impl Format> for FormatInheritanceClause<'_> { .. } = self.class_definition; - let source = f.context().contents(); + let source = f.context().source(); let mut joiner = f.join_comma_separated(); diff --git a/crates/ruff_python_formatter/src/statement/stmt_expr.rs b/crates/ruff_python_formatter/src/statement/stmt_expr.rs index b0c451fa6a..4e415f77cf 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_expr.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_expr.rs @@ -13,7 +13,7 @@ impl FormatNodeRule for FormatStmtExpr { if let Some(constant) = value.as_constant_expr() { if constant.value.is_str() - && !is_expression_parenthesized(value.as_ref().into(), f.context().contents()) + && !is_expression_parenthesized(value.as_ref().into(), f.context().source()) { return constant.format().with_options(StringLayout::Flat).fmt(f); } diff --git a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs index 6654a70958..d88263e894 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs @@ -56,9 +56,9 @@ impl FormatRule, PyFormatContext<'_>> for FormatAnyFun // while maintaining the right amount of empty lines between the comment // and the last decorator. let decorator_end = - skip_trailing_trivia(last_decorator.end(), f.context().contents()); + skip_trailing_trivia(last_decorator.end(), f.context().source()); - let leading_line = if lines_after(decorator_end, f.context().contents()) <= 1 { + let leading_line = if lines_after(decorator_end, f.context().source()) <= 1 { hard_line_break() } else { empty_line() diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index be25b63399..92fc32ed4e 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -43,7 +43,7 @@ impl FormatRule> for FormatSuite { }; let comments = f.context().comments().clone(); - let source = f.context().contents(); + let source = f.context().source(); let saved_level = f.context().node_level(); f.context_mut().set_node_level(node_level); From df15ad96965c0c95317e79479dee7c3398fb502c Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 11 Jul 2023 15:03:18 +0200 Subject: [PATCH 416/447] Print files that are slow to format (#5681) Co-authored-by: konsti --- crates/ruff_dev/src/format_dev.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/crates/ruff_dev/src/format_dev.rs b/crates/ruff_dev/src/format_dev.rs index 566efb8290..9daf4554c5 100644 --- a/crates/ruff_dev/src/format_dev.rs +++ b/crates/ruff_dev/src/format_dev.rs @@ -2,6 +2,7 @@ use anyhow::{bail, Context}; use clap::{CommandFactory, FromArgMatches}; use ignore::DirEntry; use indicatif::ProgressBar; + use rayon::iter::{IntoParallelIterator, ParallelIterator}; use ruff::resolver::python_files_in_path; use ruff::settings::types::{FilePattern, FilePatternSet}; @@ -531,6 +532,15 @@ Formatted twice: CheckFileError::IoError(error) => { writeln!(f, "Error reading {}: {}", file.display(), error)?; } + #[cfg(not(debug_assertions))] + CheckFileError::Slow(duration) => { + writeln!( + f, + "Slow formatting {}: Formatting the file took {}ms", + file.display(), + duration.as_millis() + )?; + } } Ok(()) } @@ -558,6 +568,10 @@ enum CheckFileError { IoError(io::Error), /// From `catch_unwind` Panic { message: String }, + + /// Formatting a file took too long + #[cfg(not(debug_assertions))] + Slow(Duration), } impl CheckFileError { @@ -570,6 +584,8 @@ impl CheckFileError { | CheckFileError::FormatError(_) | CheckFileError::PrintError(_) | CheckFileError::Panic { .. } => false, + #[cfg(not(debug_assertions))] + CheckFileError::Slow(_) => false, } } } @@ -586,6 +602,8 @@ fn format_dev_file( write: bool, ) -> Result { let content = fs::read_to_string(input_path)?; + #[cfg(not(debug_assertions))] + let start = Instant::now(); let printed = match format_module(&content, PyFormatOptions::default()) { Ok(printed) => printed, Err(err @ (FormatModuleError::LexError(_) | FormatModuleError::ParseError(_))) => { @@ -599,6 +617,8 @@ fn format_dev_file( } }; let formatted = printed.as_code(); + #[cfg(not(debug_assertions))] + let format_duration = Instant::now() - start; if write && content != formatted { // Simple atomic write. @@ -635,5 +655,10 @@ fn format_dev_file( } } + #[cfg(not(debug_assertions))] + if format_duration > Duration::from_millis(50) { + return Err(CheckFileError::Slow(format_duration)); + } + Ok(Statistics::from_versions(&content, formatted)) } From 0c8ec80d7bd9f15f4a0865c6c1fa6c21e956135b Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 11 Jul 2023 15:16:18 +0200 Subject: [PATCH 417/447] Change lambda dummy to NOT_YET_IMPLEMENTED_lambda (#5687) This only changes the dummy to be easier to identify. --- .../src/expression/expr_lambda.rs | 7 +++- ...black_compatibility@py_38__pep_570.py.snap | 16 +++++----- ...black_compatibility@py_38__pep_572.py.snap | 16 +++++----- ...ibility@simple_cases__bracketmatch.py.snap | 8 ++--- ...atibility@simple_cases__expression.py.snap | 32 +++++++++---------- ...mpatibility@simple_cases__fmtonoff.py.snap | 4 +-- ...mpatibility@simple_cases__function.py.snap | 4 +-- ...ity@simple_cases__power_op_spacing.py.snap | 8 ++--- ...@simple_cases__remove_for_brackets.py.snap | 4 +-- ...compatibility@simple_cases__slices.py.snap | 8 ++--- 10 files changed, 56 insertions(+), 51 deletions(-) diff --git a/crates/ruff_python_formatter/src/expression/expr_lambda.rs b/crates/ruff_python_formatter/src/expression/expr_lambda.rs index fa2a4ce3e7..8f2f81e5b3 100644 --- a/crates/ruff_python_formatter/src/expression/expr_lambda.rs +++ b/crates/ruff_python_formatter/src/expression/expr_lambda.rs @@ -11,7 +11,12 @@ pub struct FormatExprLambda; impl FormatNodeRule for FormatExprLambda { fn fmt_fields(&self, _item: &ExprLambda, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented_custom_text("lambda x: True")]) + write!( + f, + [not_yet_implemented_custom_text( + "lambda NOT_YET_IMPLEMENTED_lambda: True" + )] + ) } } diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_570.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_570.py.snap index fb9745c292..984c4bb1f9 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_570.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_570.py.snap @@ -61,16 +61,16 @@ lambda a, b=1, /, c=2, d=3, *, e=4, f=5: 1 -lambda a, /: a -+lambda x: True ++lambda NOT_YET_IMPLEMENTED_lambda: True -lambda a, b, /, c, d, *, e, f: a -+lambda x: True ++lambda NOT_YET_IMPLEMENTED_lambda: True -lambda a, b, /, c, d, *args, e, f, **kwargs: args -+lambda x: True ++lambda NOT_YET_IMPLEMENTED_lambda: True -lambda a, b=1, /, c=2, d=3, *, e=4, f=5: 1 -+lambda x: True ++lambda NOT_YET_IMPLEMENTED_lambda: True ``` ## Ruff Output @@ -113,13 +113,13 @@ def long_one_with_long_parameter_names( pass -lambda x: True +lambda NOT_YET_IMPLEMENTED_lambda: True -lambda x: True +lambda NOT_YET_IMPLEMENTED_lambda: True -lambda x: True +lambda NOT_YET_IMPLEMENTED_lambda: True -lambda x: True +lambda NOT_YET_IMPLEMENTED_lambda: True ``` ## 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 62a7d893b3..cb2a031c4e 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,10 +76,10 @@ while x := f(x): -(x := lambda: 1) -(x := lambda: (y := 1)) -lambda line: (m := re.match(pattern, line)) and m.group(1) -+lambda x: True -+(x := lambda x: True) -+(x := lambda x: True) -+lambda x: True ++lambda NOT_YET_IMPLEMENTED_lambda: True ++(x := lambda NOT_YET_IMPLEMENTED_lambda: True) ++(x := lambda NOT_YET_IMPLEMENTED_lambda: True) ++lambda NOT_YET_IMPLEMENTED_lambda: True x = (y := 0) (z := (y := (x := 0))) (info := (name, phone, *rest)) @@ -132,10 +132,10 @@ def foo(answer: (p := 42) = 5): pass -lambda x: True -(x := lambda x: True) -(x := lambda x: True) -lambda x: True +lambda NOT_YET_IMPLEMENTED_lambda: True +(x := lambda NOT_YET_IMPLEMENTED_lambda: True) +(x := lambda NOT_YET_IMPLEMENTED_lambda: True) +lambda NOT_YET_IMPLEMENTED_lambda: True x = (y := 0) (z := (y := (x := 0))) (info := (name, phone, *rest)) diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__bracketmatch.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__bracketmatch.py.snap index ae89b4e872..8201dc55c6 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__bracketmatch.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__bracketmatch.py.snap @@ -23,8 +23,8 @@ lambda x=lambda y={1: 3}: y['x':lambda y: {1: 2}]: x pass -pem_spam = lambda l, spam={"x": 3}: not spam.get(l.strip()) -lambda x=lambda y={1: 3}: y["x" : lambda y: {1: 2}]: x -+pem_spam = lambda x: True -+lambda x: True ++pem_spam = lambda NOT_YET_IMPLEMENTED_lambda: True ++lambda NOT_YET_IMPLEMENTED_lambda: True ``` ## Ruff Output @@ -32,8 +32,8 @@ lambda x=lambda y={1: 3}: y['x':lambda y: {1: 2}]: x ```py for ((x in {}) or {})["a"] in x: pass -pem_spam = lambda x: True -lambda x: True +pem_spam = lambda NOT_YET_IMPLEMENTED_lambda: True +lambda NOT_YET_IMPLEMENTED_lambda: True ``` ## Black Output 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 8fc2eb367f..97dc2ee72d 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 @@ -291,13 +291,13 @@ last_call() - "port1": port1_resource, - "port2": port2_resource, -}[port_id] -+lambda x: True -+lambda x: True -+lambda x: True -+lambda x: True -+lambda x: True -+manylambdas = lambda x: True -+foo = lambda x: True ++lambda NOT_YET_IMPLEMENTED_lambda: True ++lambda NOT_YET_IMPLEMENTED_lambda: True ++lambda NOT_YET_IMPLEMENTED_lambda: True ++lambda NOT_YET_IMPLEMENTED_lambda: True ++lambda NOT_YET_IMPLEMENTED_lambda: True ++manylambdas = lambda NOT_YET_IMPLEMENTED_lambda: True ++foo = lambda NOT_YET_IMPLEMENTED_lambda: True 1 if True else 2 str or None if True else str or bytes or None (str or None) if True else (str or bytes or None) @@ -494,7 +494,7 @@ last_call() + if False + else {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} +) -+print(*lambda x: True) ++print(*lambda NOT_YET_IMPLEMENTED_lambda: True) +NOT_YET_IMPLEMENTED_StmtAssert +NOT_YET_IMPLEMENTED_StmtAssert +NOT_YET_IMPLEMENTED_StmtAssert @@ -581,13 +581,13 @@ not great (~int) and (not ((v1 ^ (123 + v2)) | True)) +really ** -confusing ** ~operator**-precedence flags & ~select.EPOLLIN and waiters.write_task is not None -lambda x: True -lambda x: True -lambda x: True -lambda x: True -lambda x: True -manylambdas = lambda x: True -foo = lambda x: True +lambda NOT_YET_IMPLEMENTED_lambda: True +lambda NOT_YET_IMPLEMENTED_lambda: True +lambda NOT_YET_IMPLEMENTED_lambda: True +lambda NOT_YET_IMPLEMENTED_lambda: True +lambda NOT_YET_IMPLEMENTED_lambda: True +manylambdas = lambda NOT_YET_IMPLEMENTED_lambda: True +foo = lambda NOT_YET_IMPLEMENTED_lambda: True 1 if True else 2 str or None if True else str or bytes or None (str or None) if True else (str or bytes or None) @@ -785,7 +785,7 @@ print( if False else {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} ) -print(*lambda x: True) +print(*lambda NOT_YET_IMPLEMENTED_lambda: True) NOT_YET_IMPLEMENTED_StmtAssert NOT_YET_IMPLEMENTED_StmtAssert NOT_YET_IMPLEMENTED_StmtAssert 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 51aa7fb138..7ed99b9322 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 @@ -271,7 +271,7 @@ d={'a':1, def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): - offset = attr.ib(default=attr.Factory(lambda: _r.uniform(1, 2))) - assert task._cancel_stack[: len(old_stack)] == old_stack -+ offset = attr.ib(default=attr.Factory(lambda x: True)) ++ offset = attr.ib(default=attr.Factory(lambda NOT_YET_IMPLEMENTED_lambda: True)) + NOT_YET_IMPLEMENTED_StmtAssert @@ -457,7 +457,7 @@ def function_signature_stress_test( # fmt: on def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): - offset = attr.ib(default=attr.Factory(lambda x: True)) + offset = attr.ib(default=attr.Factory(lambda NOT_YET_IMPLEMENTED_lambda: True)) NOT_YET_IMPLEMENTED_StmtAssert 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 87611ac5b9..0dfa48b142 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 @@ -128,7 +128,7 @@ def __await__(): return (yield) def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): - offset = attr.ib(default=attr.Factory(lambda: _r.uniform(10000, 200000))) - assert task._cancel_stack[: len(old_stack)] == old_stack -+ offset = attr.ib(default=attr.Factory(lambda x: True)) ++ offset = attr.ib(default=attr.Factory(lambda NOT_YET_IMPLEMENTED_lambda: True)) + NOT_YET_IMPLEMENTED_StmtAssert @@ -229,7 +229,7 @@ def function_signature_stress_test( def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): - offset = attr.ib(default=attr.Factory(lambda x: True)) + offset = attr.ib(default=attr.Factory(lambda NOT_YET_IMPLEMENTED_lambda: True)) NOT_YET_IMPLEMENTED_StmtAssert 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 d315462fd0..1356a5ef71 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 @@ -80,7 +80,7 @@ return np.divide( c = -(5**2) d = 5 ** f["hi"] -e = lazy(lambda **kwargs: 5) -+e = lazy(lambda x: True) ++e = lazy(lambda NOT_YET_IMPLEMENTED_lambda: True) f = f() ** 5 g = a.b**c.d h = 5 ** funcs.f() @@ -98,7 +98,7 @@ return np.divide( c = -(5.0**2.0) d = 5.0 ** f["hi"] -e = lazy(lambda **kwargs: 5) -+e = lazy(lambda x: True) ++e = lazy(lambda NOT_YET_IMPLEMENTED_lambda: True) f = f() ** 5.0 g = a.b**c.d h = 5.0 ** funcs.f() @@ -147,7 +147,7 @@ a = 5**~4 b = 5 ** f() c = -(5**2) d = 5 ** f["hi"] -e = lazy(lambda x: True) +e = lazy(lambda NOT_YET_IMPLEMENTED_lambda: True) f = f() ** 5 g = a.b**c.d h = 5 ** funcs.f() @@ -166,7 +166,7 @@ a = 5.0**~4.0 b = 5.0 ** f() c = -(5.0**2.0) d = 5.0 ** f["hi"] -e = lazy(lambda x: True) +e = lazy(lambda NOT_YET_IMPLEMENTED_lambda: True) f = f() ** 5.0 g = a.b**c.d h = 5.0 ** funcs.f() diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_for_brackets.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_for_brackets.py.snap index a6a1c1c29f..7b10496c12 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_for_brackets.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_for_brackets.py.snap @@ -36,7 +36,7 @@ for (((((k, v))))) in d.items(): for module in (core, _unicodefun): if hasattr(module, "_verify_python3_env"): - module._verify_python3_env = lambda: None -+ module._verify_python3_env = lambda x: True ++ module._verify_python3_env = lambda NOT_YET_IMPLEMENTED_lambda: True # Brackets remain for long for loop lines for ( @@ -63,7 +63,7 @@ for k, v in d.items(): # Don't touch tuple brackets after `in` for module in (core, _unicodefun): if hasattr(module, "_verify_python3_env"): - module._verify_python3_env = lambda x: True + module._verify_python3_env = lambda NOT_YET_IMPLEMENTED_lambda: True # Brackets remain for long for loop lines for ( 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 848f65c25e..08f07ad1d3 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 @@ -59,8 +59,8 @@ ham[lower + offset : upper + offset] -slice[lambda: None : lambda: None] -slice[lambda x, y, *args, really=2, **kwargs: None :, None::] +slice[ : -1 :] -+slice[lambda x: True : lambda x: True] -+slice[lambda x: True :, None::] ++slice[lambda NOT_YET_IMPLEMENTED_lambda: True : lambda NOT_YET_IMPLEMENTED_lambda: True] ++slice[lambda NOT_YET_IMPLEMENTED_lambda: True :, None::] slice[1 or 2 : True and False] slice[not so_simple : 1 < val <= 10] -slice[(1 for i in range(42)) : x] @@ -97,8 +97,8 @@ slice[c, c + 1, d::] slice[ham[c::d] :: 1] slice[ham[cheese**2 : -1] : 1 : 1, ham[1:2]] slice[ : -1 :] -slice[lambda x: True : lambda x: True] -slice[lambda x: True :, None::] +slice[lambda NOT_YET_IMPLEMENTED_lambda: True : lambda NOT_YET_IMPLEMENTED_lambda: True] +slice[lambda NOT_YET_IMPLEMENTED_lambda: True :, None::] slice[1 or 2 : True and False] slice[not so_simple : 1 < val <= 10] slice[ From f1d367655b80f403454a1c6e90ffaf21ccb95c24 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 11 Jul 2023 16:40:28 +0200 Subject: [PATCH 418/447] Format `target: annotation = value?` expressions (#5661) --- .../skip_magic_trailing_comma.options.json | 3 + .../ruff/expression/annotated_assign.py | 13 + crates/ruff_python_formatter/src/builders.rs | 8 +- .../src/expression/expr_subscript.rs | 29 +- .../src/expression/expr_tuple.rs | 40 +- .../src/expression/mod.rs | 41 +-- .../src/expression/parentheses.rs | 51 +++ .../src/expression/string.rs | 4 +- crates/ruff_python_formatter/src/lib.rs | 8 +- .../src/statement/stmt_ann_assign.rs | 24 +- .../src/statement/stmt_assign.rs | 42 +-- .../src/statement/stmt_delete.rs | 4 +- .../src/statement/stmt_function_def.rs | 8 +- .../src/statement/stmt_import_from.rs | 4 +- .../src/statement/stmt_with.rs | 4 +- .../ruff_python_formatter/tests/fixtures.rs | 10 +- ...ility@miscellaneous__debug_visitor.py.snap | 18 +- ...atibility@miscellaneous__force_pyi.py.snap | 27 +- ...aneous__long_strings_flag_disabled.py.snap | 35 +- ...ty@py_310__pattern_matching_extras.py.snap | 27 +- ...lack_compatibility@py_38__python38.py.snap | 16 +- ...atibility@simple_cases__expression.py.snap | 78 ++-- ...mpatibility@simple_cases__fmtonoff.py.snap | 40 +- ...ple_cases__function_trailing_comma.py.snap | 348 ------------------ ...imple_cases__one_element_subscript.py.snap | 113 ------ ..._cases__return_annotation_brackets.py.snap | 47 +-- ...e_cases__skip_magic_trailing_comma.py.snap | 227 ------------ ...ormat@expression__annotated_assign.py.snap | 37 ++ 28 files changed, 318 insertions(+), 988 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/skip_magic_trailing_comma.options.json create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/annotated_assign.py delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function_trailing_comma.py.snap delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__one_element_subscript.py.snap delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__skip_magic_trailing_comma.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@expression__annotated_assign.py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/skip_magic_trailing_comma.options.json b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/skip_magic_trailing_comma.options.json new file mode 100644 index 0000000000..e01e786cb6 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/skip_magic_trailing_comma.options.json @@ -0,0 +1,3 @@ +{ + "magic_trailing_comma": "ignore" +} \ No newline at end of file diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/annotated_assign.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/annotated_assign.py new file mode 100644 index 0000000000..0809374ffc --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/annotated_assign.py @@ -0,0 +1,13 @@ +a: string + +b: string = "test" + +b: list[ + string, + int +] = [1, 2] + +b: list[ + string, + int, +] = [1, 2] diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 8eeb07a819..368c651c6d 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -6,20 +6,20 @@ use ruff_text_size::TextSize; use rustpython_parser::ast::Ranged; /// Adds parentheses and indents `content` if it doesn't fit on a line. -pub(crate) fn optional_parentheses<'ast, T>(content: &T) -> OptionalParentheses<'_, 'ast> +pub(crate) fn parenthesize_if_expands<'ast, T>(content: &T) -> ParenthesizeIfExpands<'_, 'ast> where T: Format>, { - OptionalParentheses { + ParenthesizeIfExpands { inner: Argument::new(content), } } -pub(crate) struct OptionalParentheses<'a, 'ast> { +pub(crate) struct ParenthesizeIfExpands<'a, 'ast> { inner: Argument<'a, PyFormatContext<'ast>>, } -impl<'ast> Format> for OptionalParentheses<'_, 'ast> { +impl<'ast> Format> for ParenthesizeIfExpands<'_, 'ast> { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { let saved_level = f.context().node_level(); diff --git a/crates/ruff_python_formatter/src/expression/expr_subscript.rs b/crates/ruff_python_formatter/src/expression/expr_subscript.rs index 7ac3073adc..2a5e90c444 100644 --- a/crates/ruff_python_formatter/src/expression/expr_subscript.rs +++ b/crates/ruff_python_formatter/src/expression/expr_subscript.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::ExprSubscript; +use rustpython_parser::ast::{Expr, ExprSubscript}; use ruff_formatter::{format_args, write}; use ruff_python_ast::node::AstNode; @@ -6,8 +6,10 @@ use ruff_python_ast::node::AstNode; use crate::comments::trailing_comments; use crate::context::NodeLevel; use crate::context::PyFormatContext; +use crate::expression::expr_tuple::TupleParentheses; use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, + default_expression_needs_parentheses, in_parentheses_only_group, NeedsParentheses, Parentheses, + Parenthesize, }; use crate::prelude::*; use crate::FormatNodeRule; @@ -42,12 +44,31 @@ impl FormatNodeRule for FormatExprSubscript { value.format().fmt(f)?; } + let format_slice = format_with(|f: &mut PyFormatter| { + let saved_level = f.context().node_level(); + f.context_mut() + .set_node_level(NodeLevel::ParenthesizedExpression); + + let result = if let Expr::Tuple(tuple) = slice.as_ref() { + tuple + .format() + .with_options(TupleParentheses::Subscript) + .fmt(f) + } else { + slice.format().fmt(f) + }; + + f.context_mut().set_node_level(saved_level); + + result + }); + write!( f, - [group(&format_args![ + [in_parentheses_only_group(&format_args![ text("["), trailing_comments(dangling_comments), - soft_block_indent(&slice.format()), + soft_block_indent(&format_slice), text("]") ])] ) diff --git a/crates/ruff_python_formatter/src/expression/expr_tuple.rs b/crates/ruff_python_formatter/src/expression/expr_tuple.rs index 81107f8422..78ad8279d6 100644 --- a/crates/ruff_python_formatter/src/expression/expr_tuple.rs +++ b/crates/ruff_python_formatter/src/expression/expr_tuple.rs @@ -1,4 +1,4 @@ -use crate::builders::optional_parentheses; +use crate::builders::parenthesize_if_expands; use crate::comments::{dangling_comments, CommentLinePosition}; use crate::expression::parentheses::{ default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, @@ -17,6 +17,11 @@ pub enum TupleParentheses { Default, /// Effectively `Some(Parentheses)` in `Option` Expr(Parentheses), + + /// Black omits parentheses for tuples inside of subscripts except if the tuple is parenthesized + /// in the source code. + Subscript, + /// Handle the special case where we remove parentheses even if they were initially present /// /// Normally, black keeps parentheses, but in the case of loops it formats @@ -86,21 +91,32 @@ impl FormatNodeRule for FormatExprTuple { ])] ) } - [single] => { - // A single element tuple always needs parentheses and a trailing comma - parenthesized("(", &format_args![single.format(), &text(",")], ")").fmt(f) - } + [single] => match self.parentheses { + TupleParentheses::Subscript + if !is_parenthesized(*range, elts, f.context().source()) => + { + write!(f, [single.format(), text(",")]) + } + _ => + // A single element tuple always needs parentheses and a trailing comma, except when inside of a subscript + { + parenthesized("(", &format_args![single.format(), text(",")], ")").fmt(f) + } + }, // If the tuple has parentheses, we generally want to keep them. The exception are for // loops, see `TupleParentheses::StripInsideForLoop` doc comment. // // Unlike other expression parentheses, tuple parentheses are part of the range of the // tuple itself. - elts if is_parenthesized(*range, elts, f) + elts if is_parenthesized(*range, elts, f.context().source()) && self.parentheses != TupleParentheses::StripInsideForLoop => { parenthesized("(", &ExprSequence::new(elts), ")").fmt(f) } - elts => optional_parentheses(&ExprSequence::new(elts)).fmt(f), + elts => match self.parentheses { + TupleParentheses::Subscript => group(&ExprSequence::new(elts)).fmt(f), + _ => parenthesize_if_expands(&ExprSequence::new(elts)).fmt(f), + }, } } @@ -141,15 +157,9 @@ impl NeedsParentheses for ExprTuple { } /// Check if a tuple has already had parentheses in the input -fn is_parenthesized( - tuple_range: TextRange, - elts: &[Expr], - f: &mut Formatter>, -) -> bool { +fn is_parenthesized(tuple_range: TextRange, elts: &[Expr], source: &str) -> bool { let parentheses = '('; - let first_char = &f.context().source()[usize::from(tuple_range.start())..] - .chars() - .next(); + let first_char = &source[usize::from(tuple_range.start())..].chars().next(); let Some(first_char) = first_char else { return false; }; diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 87ba364b56..97c43cf75f 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -2,17 +2,16 @@ use rustpython_parser::ast; use rustpython_parser::ast::{Expr, Operator}; use std::cmp::Ordering; -use crate::builders::optional_parentheses; -use ruff_formatter::{ - format_args, FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions, -}; +use crate::builders::parenthesize_if_expands; +use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions}; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::visitor::preorder::{walk_expr, PreorderVisitor}; use crate::context::NodeLevel; use crate::expression::expr_tuple::TupleParentheses; use crate::expression::parentheses::{ - is_expression_parenthesized, parenthesized, NeedsParentheses, Parentheses, Parenthesize, + is_expression_parenthesized, optional_parentheses, parenthesized, NeedsParentheses, + Parentheses, Parenthesize, }; use crate::expression::string::StringLayout; use crate::prelude::*; @@ -106,37 +105,9 @@ impl FormatRule> for FormatExpr { // Add optional parentheses. Ignore if the item renders parentheses itself. Parentheses::Optional => { if can_omit_optional_parentheses(item, f.context()) { - let saved_level = f.context().node_level(); - - // The group id is used as a condition in [`in_parentheses_only`] to create a conditional group - // that is only active if the optional parentheses group expands. - let parens_id = f.group_id("optional_parentheses"); - - f.context_mut() - .set_node_level(NodeLevel::Expression(Some(parens_id))); - - // We can't use `soft_block_indent` here because that would always increment the indent, - // even if the group does not break (the indent is not soft). This would result in - // too deep indentations if a `parenthesized` group expands. Using `indent_if_group_breaks` - // gives us the desired *soft* indentation that is only present if the optional parentheses - // are shown. - let result = group(&format_args![ - if_group_breaks(&text("(")), - indent_if_group_breaks( - &format_args![soft_line_break(), format_expr], - parens_id - ), - soft_line_break(), - if_group_breaks(&text(")")) - ]) - .with_group_id(Some(parens_id)) - .fmt(f); - - f.context_mut().set_node_level(saved_level); - - result - } else { optional_parentheses(&format_expr).fmt(f) + } else { + parenthesize_if_expands(&format_expr).fmt(f) } } Parentheses::Custom | Parentheses::Never => { diff --git a/crates/ruff_python_formatter/src/expression/parentheses.rs b/crates/ruff_python_formatter/src/expression/parentheses.rs index 132e1cf942..2d387d5f46 100644 --- a/crates/ruff_python_formatter/src/expression/parentheses.rs +++ b/crates/ruff_python_formatter/src/expression/parentheses.rs @@ -178,6 +178,57 @@ impl<'ast> Format> for FormatParenthesized<'_, 'ast> { } } +/// Wraps an expression in parentheses only if it still does not fit after expanding all expressions that start or end with +/// a parentheses (`()`, `[]`, `{}`). +pub(crate) fn optional_parentheses<'content, 'ast, Content>( + content: &'content Content, +) -> OptionalParentheses<'content, 'ast> +where + Content: Format>, +{ + OptionalParentheses { + content: Argument::new(content), + } +} + +pub(crate) struct OptionalParentheses<'content, 'ast> { + content: Argument<'content, PyFormatContext<'ast>>, +} + +impl<'ast> Format> for OptionalParentheses<'_, 'ast> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let saved_level = f.context().node_level(); + + // The group id is used as a condition in [`in_parentheses_only`] to create a conditional group + // that is only active if the optional parentheses group expands. + let parens_id = f.group_id("optional_parentheses"); + + f.context_mut() + .set_node_level(NodeLevel::Expression(Some(parens_id))); + + // We can't use `soft_block_indent` here because that would always increment the indent, + // even if the group does not break (the indent is not soft). This would result in + // too deep indentations if a `parenthesized` group expands. Using `indent_if_group_breaks` + // gives us the desired *soft* indentation that is only present if the optional parentheses + // are shown. + let result = group(&format_args![ + if_group_breaks(&text("(")), + indent_if_group_breaks( + &format_args![soft_line_break(), Arguments::from(&self.content)], + parens_id + ), + soft_line_break(), + if_group_breaks(&text(")")) + ]) + .with_group_id(Some(parens_id)) + .fmt(f); + + f.context_mut().set_node_level(saved_level); + + result + } +} + /// Makes `content` a group, but only if the outer expression is parenthesized (a list, parenthesized expression, dict, ...) /// or if the expression gets parenthesized because it expands over multiple lines. pub(crate) fn in_parentheses_only_group<'content, 'ast, Content>( diff --git a/crates/ruff_python_formatter/src/expression/string.rs b/crates/ruff_python_formatter/src/expression/string.rs index 7b8369d088..34ffebae7d 100644 --- a/crates/ruff_python_formatter/src/expression/string.rs +++ b/crates/ruff_python_formatter/src/expression/string.rs @@ -1,4 +1,4 @@ -use crate::builders::optional_parentheses; +use crate::builders::parenthesize_if_expands; use crate::comments::{leading_comments, trailing_comments}; use crate::expression::parentheses::Parentheses; use crate::prelude::*; @@ -48,7 +48,7 @@ impl<'a> Format> for FormatString<'a> { let format_continuation = FormatStringContinuation::new(self.constant, self.layout); if let StringLayout::Default(Some(Parentheses::Custom)) = self.layout { - optional_parentheses(&format_continuation).fmt(f) + parenthesize_if_expands(&format_continuation).fmt(f) } else { format_continuation.fmt(f) } diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index eebcf05664..79bc92d667 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -280,11 +280,9 @@ if True: #[test] fn quick_test() { let src = r#" -if [ - aaaaaa, - BBBB,ccccccccc,ddddddd,eeeeeeeeee,ffffff -] & bbbbbbbbbbbbbbbbbbddddddddddddddddddddddddddddbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: - ... +def foo() -> tuple[int, int, int,]: + return 2 + "#; // Tokenize once let mut tokens = Vec::new(); diff --git a/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs index 645808d940..c9d5cf2fdf 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs @@ -1,5 +1,6 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::prelude::*; +use crate::FormatNodeRule; +use ruff_formatter::write; use rustpython_parser::ast::StmtAnnAssign; #[derive(Default)] @@ -7,6 +8,23 @@ pub struct FormatStmtAnnAssign; impl FormatNodeRule for FormatStmtAnnAssign { fn fmt_fields(&self, item: &StmtAnnAssign, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let StmtAnnAssign { + range: _, + target, + annotation, + value, + simple: _, + } = item; + + write!( + f, + [target.format(), text(":"), space(), annotation.format()] + )?; + + if let Some(value) = value { + write!(f, [space(), text("="), space(), value.format()])?; + } + + Ok(()) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_assign.rs index eaff2ef1a3..e8f1175a3e 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_assign.rs @@ -1,12 +1,11 @@ -use crate::context::PyFormatContext; -use crate::expression::parentheses::Parenthesize; -use crate::{AsFormat, FormatNodeRule, PyFormatter}; -use ruff_formatter::formatter::Formatter; -use ruff_formatter::prelude::{space, text}; -use ruff_formatter::{write, Buffer, Format, FormatResult}; -use rustpython_parser::ast::Expr; use rustpython_parser::ast::StmtAssign; +use ruff_formatter::write; + +use crate::expression::parentheses::Parenthesize; +use crate::prelude::*; +use crate::FormatNodeRule; + // Note: This currently does wrap but not the black way so the types below likely need to be // replaced entirely // @@ -22,32 +21,11 @@ impl FormatNodeRule for FormatStmtAssign { value, type_comment: _, } = item; - write!( - f, - [ - LhsAssignList::new(targets), - value.format().with_options(Parenthesize::IfBreaks) - ] - ) - } -} -#[derive(Debug)] -struct LhsAssignList<'a> { - lhs_assign_list: &'a [Expr], -} - -impl<'a> LhsAssignList<'a> { - const fn new(lhs_assign_list: &'a [Expr]) -> Self { - Self { lhs_assign_list } - } -} - -impl Format> for LhsAssignList<'_> { - fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { - for element in self.lhs_assign_list { - write!(f, [&element.format(), space(), text("="), space(),])?; + for target in targets { + write!(f, [target.format(), space(), text("="), space()])?; } - Ok(()) + + write!(f, [value.format().with_options(Parenthesize::IfBreaks)]) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_delete.rs b/crates/ruff_python_formatter/src/statement/stmt_delete.rs index e53ff784c7..c3ba7814e8 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_delete.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_delete.rs @@ -1,4 +1,4 @@ -use crate::builders::{optional_parentheses, PyFormatterExtensions}; +use crate::builders::{parenthesize_if_expands, PyFormatterExtensions}; use crate::comments::dangling_node_comments; use crate::expression::parentheses::Parenthesize; use crate::{AsFormat, FormatNodeRule, PyFormatter}; @@ -36,7 +36,7 @@ impl FormatNodeRule for FormatStmtDelete { } targets => { let item = format_with(|f| f.join_comma_separated().nodes(targets.iter()).finish()); - optional_parentheses(&item).fmt(f) + parenthesize_if_expands(&item).fmt(f) } } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs index d88263e894..a0079b4975 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs @@ -1,6 +1,6 @@ use crate::comments::{leading_comments, trailing_comments}; use crate::context::NodeLevel; -use crate::expression::parentheses::Parenthesize; +use crate::expression::parentheses::{optional_parentheses, Parenthesize}; use crate::prelude::*; use crate::trivia::{lines_after, skip_trailing_trivia}; use crate::FormatNodeRule; @@ -97,9 +97,9 @@ impl FormatRule, PyFormatContext<'_>> for FormatAnyFun space(), text("->"), space(), - return_annotation - .format() - .with_options(Parenthesize::IfBreaks) + optional_parentheses( + &return_annotation.format().with_options(Parenthesize::Never) + ) ] )?; } 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 ef8cc13584..353f920e2b 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_import_from.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_import_from.rs @@ -1,4 +1,4 @@ -use crate::builders::{optional_parentheses, PyFormatterExtensions}; +use crate::builders::{parenthesize_if_expands, PyFormatterExtensions}; use crate::{AsFormat, FormatNodeRule, PyFormatter}; use ruff_formatter::prelude::{dynamic_text, format_with, space, text}; use ruff_formatter::{write, Buffer, Format, FormatResult}; @@ -43,6 +43,6 @@ impl FormatNodeRule for FormatStmtImportFrom { .entries(names.iter().map(|name| (name, name.format()))) .finish() }); - optional_parentheses(&names).fmt(f) + parenthesize_if_expands(&names).fmt(f) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_with.rs b/crates/ruff_python_formatter/src/statement/stmt_with.rs index c4adb3de4d..03932ff2c1 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_with.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_with.rs @@ -3,7 +3,7 @@ use ruff_python_ast::node::AnyNodeRef; use ruff_text_size::TextRange; use rustpython_parser::ast::{Ranged, StmtAsyncWith, StmtWith, Suite, WithItem}; -use crate::builders::optional_parentheses; +use crate::builders::parenthesize_if_expands; use crate::comments::trailing_comments; use crate::prelude::*; use crate::FormatNodeRule; @@ -80,7 +80,7 @@ impl Format> for AnyStatementWith<'_> { [ text("with"), space(), - group(&optional_parentheses(&joined_items)), + group(&parenthesize_if_expands(&joined_items)), text(":"), trailing_comments(dangling_comments), block_indent(&self.body().format()) diff --git a/crates/ruff_python_formatter/tests/fixtures.rs b/crates/ruff_python_formatter/tests/fixtures.rs index 7860cd3aae..5d9be66a41 100644 --- a/crates/ruff_python_formatter/tests/fixtures.rs +++ b/crates/ruff_python_formatter/tests/fixtures.rs @@ -11,7 +11,15 @@ fn black_compatibility() { let test_file = |input_path: &Path| { let content = fs::read_to_string(input_path).unwrap(); - let options = PyFormatOptions::default(); + let options_path = input_path.with_extension("options.json"); + + let options: PyFormatOptions = if let Ok(options_file) = fs::File::open(options_path) { + let reader = BufReader::new(options_file); + serde_json::from_reader(reader).expect("Options to be a valid Json file") + } else { + PyFormatOptions::default() + }; + let printed = format_module(&content, options.clone()).unwrap_or_else(|err| { panic!( "Formatting of {} to succeed but encountered error {err}", diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap index eb2adafb48..784fe8d950 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap @@ -44,11 +44,8 @@ class DebugVisitor(Visitor[T]): ```diff --- Black +++ Ruff -@@ -1,26 +1,26 @@ - @dataclass - class DebugVisitor(Visitor[T]): -- tree_depth: int = 0 -+ NOT_YET_IMPLEMENTED_StmtAnnAssign +@@ -3,24 +3,24 @@ + tree_depth: int = 0 def visit_default(self, node: LN) -> Iterator[T]: - indent = ' ' * (2 * self.tree_depth) @@ -79,13 +76,6 @@ class DebugVisitor(Visitor[T]): @classmethod def show(cls, code: str) -> None: -@@ -28,5 +28,5 @@ - - Convenience method for debugging. - """ -- v: DebugVisitor[None] = DebugVisitor() -+ NOT_YET_IMPLEMENTED_StmtAnnAssign - list(v.visit(lib2to3_parse(code))) ``` ## Ruff Output @@ -93,7 +83,7 @@ class DebugVisitor(Visitor[T]): ```py @dataclass class DebugVisitor(Visitor[T]): - NOT_YET_IMPLEMENTED_StmtAnnAssign + tree_depth: int = 0 def visit_default(self, node: LN) -> Iterator[T]: indent = " " * (2 * self.tree_depth) @@ -121,7 +111,7 @@ class DebugVisitor(Visitor[T]): Convenience method for debugging. """ - NOT_YET_IMPLEMENTED_StmtAnnAssign + v: DebugVisitor[None] = DebugVisitor() list(v.visit(lib2to3_parse(code))) ``` 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 f8f00344e5..b2ea142296 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 @@ -67,13 +67,13 @@ def eggs() -> Union[str, int]: ... - def BMethod(self, arg: List[str]) -> None: ... + def BMethod(self, arg: List[str]) -> None: + ... ++ ++ ++class C: ++ ... -class C: ... -+class C: -+ ... -+ -+ @hmm -class D: ... +class D: @@ -89,29 +89,28 @@ def eggs() -> Union[str, int]: ... -def foo() -> None: ... +def foo() -> None: + ... -+ -+ + +-class F(A, C): ... + +-def spam() -> None: ... +class F(A, C): + ... + + +def spam() -> None: + ... - --class F(A, C): ... - --def spam() -> None: ... ++ ++ @overload -def spam(arg: str) -> str: ... +def spam(arg: str) -> str: + ... + -+ -+NOT_YET_IMPLEMENTED_StmtAnnAssign --var: int = 1 + var: int = 1 -def eggs() -> Union[str, int]: ... ++ +def eggs() -> Union[str, int]: + ... ``` @@ -172,7 +171,7 @@ def spam(arg: str) -> str: ... -NOT_YET_IMPLEMENTED_StmtAnnAssign +var: int = 1 def eggs() -> Union[str, int]: 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 b4d5258ad2..908e9c1cc9 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 @@ -417,7 +417,7 @@ long_unmergable_string_with_pragma = ( x, y, z, -@@ -243,21 +225,13 @@ +@@ -243,7 +225,7 @@ func_with_bad_parens( x, y, @@ -426,24 +426,7 @@ long_unmergable_string_with_pragma = ( z, ) --annotated_variable: Final = ( -- "This is a large " -- + STRING -- + " that has been " -- + CONCATENATED -- + "using the '+' operator." --) --annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." --annotated_variable: Literal[ -- "fakse_literal" --] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." -+NOT_YET_IMPLEMENTED_StmtAnnAssign -+NOT_YET_IMPLEMENTED_StmtAnnAssign -+NOT_YET_IMPLEMENTED_StmtAnnAssign - - backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" - backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" -@@ -271,10 +245,10 @@ +@@ -271,10 +253,10 @@ def foo(): @@ -692,9 +675,17 @@ func_with_bad_parens( z, ) -NOT_YET_IMPLEMENTED_StmtAnnAssign -NOT_YET_IMPLEMENTED_StmtAnnAssign -NOT_YET_IMPLEMENTED_StmtAnnAssign +annotated_variable: Final = ( + "This is a large " + + STRING + + " that has been " + + CONCATENATED + + "using the '+' operator." +) +annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: Literal[ + "fakse_literal" +] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" 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 8752abc340..23dd576815 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 @@ -147,8 +147,7 @@ match bar1: match = 1 --case: int = re.match(something) -+NOT_YET_IMPLEMENTED_StmtAnnAssign + case: int = re.match(something) -match re.match(case): - case type("match", match): @@ -214,21 +213,19 @@ match bar1: - case case: - pass -+NOT_YET_IMPLEMENTED_StmtMatch - - +- +- -match match: - case case: - pass +- +NOT_YET_IMPLEMENTED_StmtMatch - -match a, *b(), c: - case d, *f, g: - pass -+NOT_YET_IMPLEMENTED_StmtMatch - +- -match something: - case { - "key": key as key_1, @@ -237,28 +234,30 @@ 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 @@ -276,7 +275,7 @@ NOT_YET_IMPLEMENTED_StmtMatch match = 1 -NOT_YET_IMPLEMENTED_StmtAnnAssign +case: int = re.match(something) NOT_YET_IMPLEMENTED_StmtMatch diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__python38.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__python38.py.snap index d706374274..f92c9acaf0 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__python38.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__python38.py.snap @@ -31,7 +31,7 @@ def t(): ```diff --- Black +++ Ruff -@@ -8,14 +8,14 @@ +@@ -8,7 +8,7 @@ def starred_yield(): my_list = ["value2", "value3"] @@ -40,16 +40,12 @@ def t(): # all right hand side expressions allowed in regular assignments are now also allowed in - # annotated assignments --a: Tuple[str, int] = "1", 2 --a: Tuple[int, ...] = b, *c, d -+NOT_YET_IMPLEMENTED_StmtAnnAssign -+NOT_YET_IMPLEMENTED_StmtAnnAssign +@@ -18,4 +18,4 @@ def t(): - a: str = yield "a" -+ NOT_YET_IMPLEMENTED_StmtAnnAssign ++ a: str = NOT_YET_IMPLEMENTED_ExprYield ``` ## Ruff Output @@ -70,12 +66,12 @@ def starred_yield(): # all right hand side expressions allowed in regular assignments are now also allowed in # annotated assignments -NOT_YET_IMPLEMENTED_StmtAnnAssign -NOT_YET_IMPLEMENTED_StmtAnnAssign +a: Tuple[str, int] = "1", 2 +a: Tuple[int, ...] = b, *c, d def t(): - NOT_YET_IMPLEMENTED_StmtAnnAssign + a: str = NOT_YET_IMPLEMENTED_ExprYield ``` ## Black Output 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 97dc2ee72d..4f4798dc42 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 @@ -357,38 +357,7 @@ last_call() ) # note: no trailing comma pre-3.6 call(*gidgets[:2]) call(a, *gidgets[:2]) -@@ -131,34 +131,28 @@ - tuple[str, ...] - tuple[str, int, float, dict[str, int]] - tuple[ -- str, -- int, -- float, -- dict[str, int], -+ ( -+ str, -+ int, -+ float, -+ dict[str, int], -+ ) - ] --very_long_variable_name_filters: t.List[ -- t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], --] --xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore -- sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) --) --xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore -- sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) --) --xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( -- sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) --) # type: ignore -+NOT_YET_IMPLEMENTED_StmtAnnAssign -+NOT_YET_IMPLEMENTED_StmtAnnAssign -+NOT_YET_IMPLEMENTED_StmtAnnAssign -+NOT_YET_IMPLEMENTED_StmtAnnAssign # type: ignore - slice[0] +@@ -152,13 +152,13 @@ slice[0:1] slice[0:1:2] slice[:] @@ -405,7 +374,7 @@ last_call() numpy[0, :] numpy[:, i] numpy[0, :2] -@@ -172,7 +166,7 @@ +@@ -172,7 +172,7 @@ numpy[1 : c + 1, c] numpy[-(c + 1) :, d] numpy[:, l[-2]] @@ -414,7 +383,7 @@ last_call() numpy[np.newaxis, :] (str or None) if (sys.version_info[0] > (3,)) else (str or bytes or None) {"2.7": dead, "3.7": long_live or die_hard} -@@ -181,10 +175,10 @@ +@@ -181,10 +181,10 @@ (SomeName) SomeName (Good, Bad, Ugly) @@ -429,11 +398,10 @@ last_call() (*starred,) { "id": "1", -@@ -207,25 +201,15 @@ - ) +@@ -208,24 +208,14 @@ 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( @@ -441,7 +409,7 @@ last_call() - ) - .order_by(models.Customer.id.asc()) - .all() - ) +-) -result = ( - session.query(models.Customer.id) - .filter( @@ -463,7 +431,7 @@ last_call() Ø = set() authors.łukasz.say_thanks() mapping = { -@@ -237,10 +221,10 @@ +@@ -237,10 +227,10 @@ def gen(): @@ -478,7 +446,7 @@ last_call() async def f(): -@@ -248,18 +232,22 @@ +@@ -248,18 +238,22 @@ print(*[] or [1]) @@ -509,7 +477,7 @@ last_call() ... for i in call(): ... -@@ -328,13 +316,18 @@ +@@ -328,13 +322,18 @@ ): return True if ( @@ -531,7 +499,7 @@ last_call() ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n ): return True -@@ -342,7 +335,8 @@ +@@ -342,7 +341,8 @@ ~aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e @@ -679,17 +647,23 @@ dict[str, int] tuple[str, ...] tuple[str, int, float, dict[str, int]] tuple[ - ( - str, - int, - float, - dict[str, int], - ) + str, + int, + float, + dict[str, int], ] -NOT_YET_IMPLEMENTED_StmtAnnAssign -NOT_YET_IMPLEMENTED_StmtAnnAssign -NOT_YET_IMPLEMENTED_StmtAnnAssign -NOT_YET_IMPLEMENTED_StmtAnnAssign # type: ignore +very_long_variable_name_filters: t.List[ + t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], +] +xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore + sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) +) +xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore + sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) +) +xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( + sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) +) # type: ignore slice[0] slice[0:1] slice[0:1:2] 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 7ed99b9322..218da54c3e 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 @@ -276,7 +276,7 @@ d={'a':1, def spaces_types( -@@ -63,55 +76,54 @@ +@@ -63,15 +76,15 @@ something = { # fmt: off @@ -290,18 +290,12 @@ d={'a':1, # fmt: off - 'some big and', - 'complex subscript', -- # fmt: on -- goes + here, -- andhere, -+ ( -+ "some big and", -+ "complex subscript", -+ # fmt: on -+ goes + here, -+ andhere, -+ ) - ] - ++ "some big and", ++ "complex subscript", + # fmt: on + goes + here, + andhere, +@@ -80,38 +93,35 @@ def import_as_names(): # fmt: off @@ -351,7 +345,7 @@ d={'a':1, # fmt: on -@@ -132,10 +144,10 @@ +@@ -132,10 +142,10 @@ """Another known limitation.""" # fmt: on # fmt: off @@ -366,7 +360,7 @@ d={'a':1, # fmt: on # fmt: off # ...but comments still get reformatted even though they should not be -@@ -153,9 +165,7 @@ +@@ -153,9 +163,7 @@ ) ) # fmt: off @@ -377,7 +371,7 @@ d={'a':1, # fmt: on _type_comment_re = re.compile( r""" -@@ -178,7 +188,7 @@ +@@ -178,7 +186,7 @@ $ """, # fmt: off @@ -386,7 +380,7 @@ d={'a':1, # fmt: on ) -@@ -216,8 +226,7 @@ +@@ -216,8 +224,7 @@ xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5, ) # fmt: off @@ -488,13 +482,11 @@ something = { def subscriptlist(): atom[ # fmt: off - ( - "some big and", - "complex subscript", - # fmt: on - goes + here, - andhere, - ) + "some big and", + "complex subscript", + # fmt: on + goes + here, + andhere, ] 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 deleted file mode 100644 index 388d0c3d4e..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function_trailing_comma.py.snap +++ /dev/null @@ -1,348 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function_trailing_comma.py ---- -## Input - -```py -def f(a,): - d = {'key': 'value',} - tup = (1,) - -def f2(a,b,): - d = {'key': 'value', 'key2': 'value2',} - tup = (1,2,) - -def f(a:int=1,): - call(arg={'explode': 'this',}) - call2(arg=[1,2,3],) - x = { - "a": 1, - "b": 2, - }["a"] - if a == {"a": 1,"b": 2,"c": 3,"d": 4,"e": 5,"f": 6,"g": 7,"h": 8,}["a"]: - pass - -def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -]: - json = {"k": {"k2": {"k3": [1,]}}} - - - -# The type annotation shouldn't get a trailing comma since that would change its type. -# Relevant bug report: https://github.com/psf/black/issues/2381. -def some_function_with_a_really_long_name() -> ( - returning_a_deeply_nested_import_of_a_type_i_suppose -): - pass - - -def some_method_with_a_really_long_name(very_long_parameter_so_yeah: str, another_long_parameter: int) -> ( - another_case_of_returning_a_deeply_nested_import_of_a_type_i_suppose_cause_why_not -): - pass - - -def func() -> ( - also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black(this_shouldn_t_get_a_trailing_comma_too) -): - pass - - -def func() -> ((also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( - this_shouldn_t_get_a_trailing_comma_too - )) -): - pass - - -# Make sure inner one-element tuple won't explode -some_module.some_function( - argument1, (one_element_tuple,), argument4, argument5, argument6 -) - -# Inner trailing comma causes outer to explode -some_module.some_function( - argument1, (one, two,), argument4, argument5, argument6 -) -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -52,9 +52,9 @@ - pass - - --def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> ( -- Set["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"] --): -+def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ -+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -+]: - json = { - "k": { - "k2": { -@@ -80,18 +80,14 @@ - pass - - --def func() -> ( -- also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( -- this_shouldn_t_get_a_trailing_comma_too -- ) -+def func() -> also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( -+ this_shouldn_t_get_a_trailing_comma_too - ): - pass - - --def func() -> ( -- also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( -- this_shouldn_t_get_a_trailing_comma_too -- ) -+def func() -> also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( -+ this_shouldn_t_get_a_trailing_comma_too - ): - pass - -``` - -## Ruff Output - -```py -def f( - a, -): - d = { - "key": "value", - } - tup = (1,) - - -def f2( - a, - b, -): - d = { - "key": "value", - "key2": "value2", - } - tup = ( - 1, - 2, - ) - - -def f( - a: int = 1, -): - call( - arg={ - "explode": "this", - } - ) - call2( - arg=[1, 2, 3], - ) - x = { - "a": 1, - "b": 2, - }["a"] - if ( - a - == { - "a": 1, - "b": 2, - "c": 3, - "d": 4, - "e": 5, - "f": 6, - "g": 7, - "h": 8, - }["a"] - ): - pass - - -def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -]: - json = { - "k": { - "k2": { - "k3": [ - 1, - ] - } - } - } - - -# The type annotation shouldn't get a trailing comma since that would change its type. -# Relevant bug report: https://github.com/psf/black/issues/2381. -def some_function_with_a_really_long_name() -> ( - returning_a_deeply_nested_import_of_a_type_i_suppose -): - pass - - -def some_method_with_a_really_long_name( - very_long_parameter_so_yeah: str, another_long_parameter: int -) -> another_case_of_returning_a_deeply_nested_import_of_a_type_i_suppose_cause_why_not: - pass - - -def func() -> also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( - this_shouldn_t_get_a_trailing_comma_too -): - pass - - -def func() -> also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( - this_shouldn_t_get_a_trailing_comma_too -): - pass - - -# Make sure inner one-element tuple won't explode -some_module.some_function( - argument1, (one_element_tuple,), argument4, argument5, argument6 -) - -# Inner trailing comma causes outer to explode -some_module.some_function( - argument1, - ( - one, - two, - ), - argument4, - argument5, - argument6, -) -``` - -## Black Output - -```py -def f( - a, -): - d = { - "key": "value", - } - tup = (1,) - - -def f2( - a, - b, -): - d = { - "key": "value", - "key2": "value2", - } - tup = ( - 1, - 2, - ) - - -def f( - a: int = 1, -): - call( - arg={ - "explode": "this", - } - ) - call2( - arg=[1, 2, 3], - ) - x = { - "a": 1, - "b": 2, - }["a"] - if ( - a - == { - "a": 1, - "b": 2, - "c": 3, - "d": 4, - "e": 5, - "f": 6, - "g": 7, - "h": 8, - }["a"] - ): - pass - - -def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> ( - Set["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"] -): - json = { - "k": { - "k2": { - "k3": [ - 1, - ] - } - } - } - - -# The type annotation shouldn't get a trailing comma since that would change its type. -# Relevant bug report: https://github.com/psf/black/issues/2381. -def some_function_with_a_really_long_name() -> ( - returning_a_deeply_nested_import_of_a_type_i_suppose -): - pass - - -def some_method_with_a_really_long_name( - very_long_parameter_so_yeah: str, another_long_parameter: int -) -> another_case_of_returning_a_deeply_nested_import_of_a_type_i_suppose_cause_why_not: - pass - - -def func() -> ( - also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( - this_shouldn_t_get_a_trailing_comma_too - ) -): - pass - - -def func() -> ( - also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( - this_shouldn_t_get_a_trailing_comma_too - ) -): - pass - - -# Make sure inner one-element tuple won't explode -some_module.some_function( - argument1, (one_element_tuple,), argument4, argument5, argument6 -) - -# Inner trailing comma causes outer to explode -some_module.some_function( - argument1, - ( - one, - two, - ), - argument4, - argument5, - argument6, -) -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__one_element_subscript.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__one_element_subscript.py.snap deleted file mode 100644 index 1db4d103ba..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__one_element_subscript.py.snap +++ /dev/null @@ -1,113 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/one_element_subscript.py ---- -## Input - -```py -# We should not treat the trailing comma -# in a single-element subscript. -a: tuple[int,] -b = tuple[int,] - -# The magic comma still applies to multi-element subscripts. -c: tuple[int, int,] -d = tuple[int, int,] - -# Magic commas still work as expected for non-subscripts. -small_list = [1,] -list_of_types = [tuple[int,],] -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,16 +1,15 @@ - # We should not treat the trailing comma - # in a single-element subscript. --a: tuple[int,] --b = tuple[int,] -+NOT_YET_IMPLEMENTED_StmtAnnAssign -+b = tuple[(int,)] - - # The magic comma still applies to multi-element subscripts. --c: tuple[ -- int, -- int, --] -+NOT_YET_IMPLEMENTED_StmtAnnAssign - d = tuple[ -- int, -- int, -+ ( -+ int, -+ int, -+ ) - ] - - # Magic commas still work as expected for non-subscripts. -@@ -18,5 +17,5 @@ - 1, - ] - list_of_types = [ -- tuple[int,], -+ tuple[(int,)], - ] -``` - -## Ruff Output - -```py -# We should not treat the trailing comma -# in a single-element subscript. -NOT_YET_IMPLEMENTED_StmtAnnAssign -b = tuple[(int,)] - -# The magic comma still applies to multi-element subscripts. -NOT_YET_IMPLEMENTED_StmtAnnAssign -d = tuple[ - ( - int, - int, - ) -] - -# Magic commas still work as expected for non-subscripts. -small_list = [ - 1, -] -list_of_types = [ - tuple[(int,)], -] -``` - -## Black Output - -```py -# We should not treat the trailing comma -# in a single-element subscript. -a: tuple[int,] -b = tuple[int,] - -# The magic comma still applies to multi-element subscripts. -c: tuple[ - int, - int, -] -d = tuple[ - int, - int, -] - -# Magic commas still work as expected for non-subscripts. -small_list = [ - 1, -] -list_of_types = [ - tuple[int,], -] -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__return_annotation_brackets.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__return_annotation_brackets.py.snap index 664ee153eb..c35111a8b1 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__return_annotation_brackets.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__return_annotation_brackets.py.snap @@ -122,37 +122,6 @@ def foo() -> tuple[int, int, int,]: return 2 -@@ -99,22 +103,22 @@ - return 2 - - --def foo() -> ( -- tuple[ -+def foo() -> tuple[ -+ ( - loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, - loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, - loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, -- ] --): -+ ) -+]: - return 2 - - - # Magic trailing comma example --def foo() -> ( -- tuple[ -+def foo() -> tuple[ -+ ( - int, - int, - int, -- ] --): -+ ) -+]: - return 2 ``` ## Ruff Output @@ -263,24 +232,24 @@ def foo() -> tuple[int, int, int]: return 2 -def foo() -> tuple[ - ( +def foo() -> ( + tuple[ loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, - ) -]: + ] +): return 2 # Magic trailing comma example -def foo() -> tuple[ - ( +def foo() -> ( + tuple[ int, int, int, - ) -]: + ] +): return 2 ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__skip_magic_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__skip_magic_trailing_comma.py.snap deleted file mode 100644 index d87ef4d8a0..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__skip_magic_trailing_comma.py.snap +++ /dev/null @@ -1,227 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/skip_magic_trailing_comma.py ---- -## Input - -```py -# We should not remove the trailing comma in a single-element subscript. -a: tuple[int,] -b = tuple[int,] - -# But commas in multiple element subscripts should be removed. -c: tuple[int, int,] -d = tuple[int, int,] - -# Remove commas for non-subscripts. -small_list = [1,] -list_of_types = [tuple[int,],] -small_set = {1,} -set_of_types = {tuple[int,],} - -# Except single element tuples -small_tuple = (1,) - -# Trailing commas in multiple chained non-nested parens. -zero( - one, -).two( - three, -).four( - five, -) - -func1(arg1).func2(arg2,).func3(arg3).func4(arg4,).func5(arg5) - -( - a, - b, - c, - d, -) = func1( - arg1 -) and func2(arg2) - -func( - argument1, - ( - one, - two, - ), - argument4, - argument5, - argument6, -) -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,25 +1,58 @@ - # We should not remove the trailing comma in a single-element subscript. --a: tuple[int,] --b = tuple[int,] -+NOT_YET_IMPLEMENTED_StmtAnnAssign -+b = tuple[(int,)] - - # But commas in multiple element subscripts should be removed. --c: tuple[int, int] --d = tuple[int, int] -+NOT_YET_IMPLEMENTED_StmtAnnAssign -+d = tuple[ -+ ( -+ int, -+ int, -+ ) -+] - - # Remove commas for non-subscripts. --small_list = [1] --list_of_types = [tuple[int,]] -+small_list = [ -+ 1, -+] -+list_of_types = [ -+ tuple[(int,)], -+] - small_set = {1} --set_of_types = {tuple[int,]} -+set_of_types = {tuple[(int,)]} - - # Except single element tuples - small_tuple = (1,) - - # Trailing commas in multiple chained non-nested parens. --zero(one).two(three).four(five) -+zero( -+ one, -+).two( -+ three, -+).four( -+ five, -+) - --func1(arg1).func2(arg2).func3(arg3).func4(arg4).func5(arg5) -+func1(arg1).func2( -+ arg2, -+).func3(arg3).func4( -+ arg4, -+).func5(arg5) - --(a, b, c, d) = func1(arg1) and func2(arg2) -+( -+ a, -+ b, -+ c, -+ d, -+) = func1(arg1) and func2(arg2) - --func(argument1, (one, two), argument4, argument5, argument6) -+func( -+ argument1, -+ ( -+ one, -+ two, -+ ), -+ argument4, -+ argument5, -+ argument6, -+) -``` - -## Ruff Output - -```py -# We should not remove the trailing comma in a single-element subscript. -NOT_YET_IMPLEMENTED_StmtAnnAssign -b = tuple[(int,)] - -# But commas in multiple element subscripts should be removed. -NOT_YET_IMPLEMENTED_StmtAnnAssign -d = tuple[ - ( - int, - int, - ) -] - -# Remove commas for non-subscripts. -small_list = [ - 1, -] -list_of_types = [ - tuple[(int,)], -] -small_set = {1} -set_of_types = {tuple[(int,)]} - -# Except single element tuples -small_tuple = (1,) - -# Trailing commas in multiple chained non-nested parens. -zero( - one, -).two( - three, -).four( - five, -) - -func1(arg1).func2( - arg2, -).func3(arg3).func4( - arg4, -).func5(arg5) - -( - a, - b, - c, - d, -) = func1(arg1) and func2(arg2) - -func( - argument1, - ( - one, - two, - ), - argument4, - argument5, - argument6, -) -``` - -## Black Output - -```py -# We should not remove the trailing comma in a single-element subscript. -a: tuple[int,] -b = tuple[int,] - -# But commas in multiple element subscripts should be removed. -c: tuple[int, int] -d = tuple[int, int] - -# Remove commas for non-subscripts. -small_list = [1] -list_of_types = [tuple[int,]] -small_set = {1} -set_of_types = {tuple[int,]} - -# Except single element tuples -small_tuple = (1,) - -# Trailing commas in multiple chained non-nested parens. -zero(one).two(three).four(five) - -func1(arg1).func2(arg2).func3(arg3).func4(arg4).func5(arg5) - -(a, b, c, d) = func1(arg1) and func2(arg2) - -func(argument1, (one, two), argument4, argument5, argument6) -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__annotated_assign.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__annotated_assign.py.snap new file mode 100644 index 0000000000..55c646d9fd --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__annotated_assign.py.snap @@ -0,0 +1,37 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/annotated_assign.py +--- +## Input +```py +a: string + +b: string = "test" + +b: list[ + string, + int +] = [1, 2] + +b: list[ + string, + int, +] = [1, 2] +``` + +## Output +```py +a: string + +b: string = "test" + +b: list[string, int] = [1, 2] + +b: list[ + string, + int, +] = [1, 2] +``` + + + From 62a24e1028f279e876c654005bb49a05daf58d31 Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 11 Jul 2023 16:41:10 +0200 Subject: [PATCH 419/447] Format `ModExpression` (#5689) ## Summary We don't use `ModExpression` anywhere but it's part of the AST, removes one `not_implemented_yet` and is a trivial 2-liner, so i implemented formatting for `ModExpression`. ## Test Plan None, this kind of node does not occur in file input. Otherwise all the tests for expressions --- crates/ruff_python_formatter/src/module/mod_expression.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/ruff_python_formatter/src/module/mod_expression.rs b/crates/ruff_python_formatter/src/module/mod_expression.rs index d7a8c40db6..f6e49fb696 100644 --- a/crates/ruff_python_formatter/src/module/mod_expression.rs +++ b/crates/ruff_python_formatter/src/module/mod_expression.rs @@ -1,5 +1,5 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::{Format, FormatResult}; use rustpython_parser::ast::ModExpression; #[derive(Default)] @@ -7,6 +7,7 @@ pub struct FormatModExpression; impl FormatNodeRule for FormatModExpression { fn fmt_fields(&self, item: &ModExpression, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let ModExpression { body, range: _ } = item; + body.format().fmt(f) } } From 8b9193ab1faba8ecc31d1dd8bf53ae9e3808ccf6 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 11 Jul 2023 16:51:24 +0200 Subject: [PATCH 420/447] Improve comprehension line break beheavior ## Summary This PR improves the Black compatibility when it comes to breaking comprehensions. We want to avoid line breaks before the target and `in` whenever possible. Furthermore, `if X is not None` should be grouped together, similar to other binary like expressions ## Test Plan `cargo test` --- .../fixtures/ruff/expression/list_comp.py | 13 ++ .../src/expression/expr_compare.rs | 44 +++-- .../src/other/comprehension.rs | 35 +++- ...lack_compatibility@py_37__python37.py.snap | 21 +- ...patibility@simple_cases__comments2.py.snap | 52 +---- ...patibility@simple_cases__comments3.py.snap | 184 ------------------ ...mpatibility@simple_cases__fmtskip5.py.snap | 8 +- ...mpatibility@simple_cases__function.py.snap | 16 +- ...ity@simple_cases__power_op_spacing.py.snap | 20 +- .../snapshots/format@expression__call.py.snap | 6 +- .../format@expression__list_comp.py.snap | 38 +++- 11 files changed, 121 insertions(+), 316 deletions(-) delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments3.py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list_comp.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list_comp.py index 4684d4dd0e..6f8a4dbd31 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list_comp.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list_comp.py @@ -30,3 +30,16 @@ # above g g # g ] + +[ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + [dddddddddddddddddd, eeeeeeeeeeeeeeeeeee] + for + ccccccccccccccccccccccccccccccccccccccc, + ddddddddddddddddddd, [eeeeeeeeeeeeeeeeeeeeee, fffffffffffffffffffffffff] + in + eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffggggggggggggggggggggghhhhhhhhhhhhhhothermoreeand_even_moreddddddddddddddddddddd + if + fffffffffffffffffffffffffffffffffffffffffff < gggggggggggggggggggggggggggggggggggggggggggggg < hhhhhhhhhhhhhhhhhhhhhhhhhh + if + gggggggggggggggggggggggggggggggggggggggggggg +] diff --git a/crates/ruff_python_formatter/src/expression/expr_compare.rs b/crates/ruff_python_formatter/src/expression/expr_compare.rs index 988a64ddf4..d03094c7de 100644 --- a/crates/ruff_python_formatter/src/expression/expr_compare.rs +++ b/crates/ruff_python_formatter/src/expression/expr_compare.rs @@ -33,36 +33,40 @@ impl FormatNodeRule for FormatExprCompare { let comments = f.context().comments().clone(); - write!(f, [in_parentheses_only_group(&left.format())])?; + let inner = format_with(|f| { + write!(f, [in_parentheses_only_group(&left.format())])?; - assert_eq!(comparators.len(), ops.len()); + assert_eq!(comparators.len(), ops.len()); + + for (operator, comparator) in ops.iter().zip(comparators) { + let leading_comparator_comments = comments.leading_comments(comparator); + if leading_comparator_comments.is_empty() { + write!(f, [soft_line_break_or_space()])?; + } else { + // Format the expressions leading comments **before** the operator + write!( + f, + [ + hard_line_break(), + leading_comments(leading_comparator_comments) + ] + )?; + } - for (operator, comparator) in ops.iter().zip(comparators) { - let leading_comparator_comments = comments.leading_comments(comparator); - if leading_comparator_comments.is_empty() { - write!(f, [soft_line_break_or_space()])?; - } else { - // Format the expressions leading comments **before** the operator write!( f, [ - hard_line_break(), - leading_comments(leading_comparator_comments) + operator.format(), + space(), + in_parentheses_only_group(&comparator.format()) ] )?; } - write!( - f, - [ - operator.format(), - space(), - in_parentheses_only_group(&comparator.format()) - ] - )?; - } + Ok(()) + }); - Ok(()) + in_parentheses_only_group(&inner).fmt(f) } } diff --git a/crates/ruff_python_formatter/src/other/comprehension.rs b/crates/ruff_python_formatter/src/other/comprehension.rs index 022c5b82e6..0503f2d93b 100644 --- a/crates/ruff_python_formatter/src/other/comprehension.rs +++ b/crates/ruff_python_formatter/src/other/comprehension.rs @@ -3,13 +3,25 @@ use crate::prelude::*; use crate::AsFormat; use crate::{FormatNodeRule, PyFormatter}; use ruff_formatter::{format_args, write, Buffer, FormatResult}; -use rustpython_parser::ast::{Comprehension, Ranged}; +use rustpython_parser::ast::{Comprehension, Expr, Ranged}; #[derive(Default)] pub struct FormatComprehension; impl FormatNodeRule for FormatComprehension { fn fmt_fields(&self, item: &Comprehension, f: &mut PyFormatter) -> FormatResult<()> { + struct Spacer<'a>(&'a Expr); + + impl Format> for Spacer<'_> { + fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { + if f.context().comments().has_leading_comments(self.0) { + soft_line_break_or_space().fmt(f) + } else { + space().fmt(f) + } + } + } + let Comprehension { range: _, target, @@ -18,33 +30,40 @@ impl FormatNodeRule for FormatComprehension { is_async, } = item; - let comments = f.context().comments().clone(); - if *is_async { write!(f, [text("async"), space()])?; } + let comments = f.context().comments().clone(); let dangling_item_comments = comments.dangling_comments(item); - let (before_target_comments, before_in_comments) = dangling_item_comments.split_at( dangling_item_comments .partition_point(|comment| comment.slice().end() < target.range().start()), ); let trailing_in_comments = comments.dangling_comments(iter); + + let in_spacer = format_with(|f| { + if before_in_comments.is_empty() { + space().fmt(f) + } else { + soft_line_break_or_space().fmt(f) + } + }); + write!( f, [ text("for"), trailing_comments(before_target_comments), group(&format_args!( - soft_line_break_or_space(), + Spacer(target), target.format(), - soft_line_break_or_space(), + in_spacer, leading_comments(before_in_comments), text("in"), trailing_comments(trailing_in_comments), - soft_line_break_or_space(), + Spacer(iter), iter.format(), )), ] @@ -64,7 +83,7 @@ impl FormatNodeRule for FormatComprehension { leading_comments(own_line_if_comments), text("if"), trailing_comments(end_of_line_if_comments), - soft_line_break_or_space(), + Spacer(if_case), if_case.format(), ))); } 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 8af05b54ea..4a12cae071 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 @@ -42,7 +42,7 @@ def make_arange(n): ```diff --- Black +++ Ruff -@@ -2,29 +2,27 @@ +@@ -2,14 +2,11 @@ def f(): @@ -59,17 +59,7 @@ def make_arange(n): async def func(): - if test: - out_batched = [ - i -- async for i in aitertools._async_map( -- self.async_inc, arange(8), batch_size=3 -- ) -+ async for -+ i -+ in -+ aitertools._async_map(self.async_inc, arange(8), batch_size=3) - ] +@@ -23,8 +20,8 @@ def awaited_generator_value(n): @@ -100,10 +90,9 @@ async def func(): if test: out_batched = [ i - async for - i - in - aitertools._async_map(self.async_inc, arange(8), batch_size=3) + async for i in aitertools._async_map( + self.async_inc, arange(8), batch_size=3 + ) ] 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 ef5229fdb1..a363f5c7fb 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 @@ -224,43 +224,18 @@ instruction()#comment with bad spacing if ( self._proc is not None # has the child process finished? -@@ -115,7 +123,12 @@ +@@ -115,7 +123,9 @@ arg3=True, ) lcomp = [ - element for element in collection if element is not None # yup # yup # right + element # yup -+ for -+ element -+ in -+ collection # yup ++ for element in collection # yup + if element is not None # right ] lcomp2 = [ # hello -@@ -123,7 +136,9 @@ - # yup - for element in collection - # right -- if element is not None -+ if -+ element -+ is not None - ] - lcomp3 = [ - # This one is actually too long to fit in a single line. -@@ -131,7 +146,9 @@ - # yup - for element in collection.select_elements() - # right -- if element is not None -+ if -+ element -+ is not None - ] - while True: - if False: -@@ -143,7 +160,10 @@ +@@ -143,7 +153,10 @@ # let's return return Node( syms.simple_stmt, @@ -272,14 +247,13 @@ instruction()#comment with bad spacing ) -@@ -158,7 +178,11 @@ +@@ -158,7 +171,10 @@ class Test: def _init_host(self, parsed) -> None: - if parsed.hostname is None or not parsed.hostname.strip(): # type: ignore + if ( -+ parsed.hostname -+ is None # type: ignore ++ parsed.hostname is None # type: ignore + or not parsed.hostname.strip() + ): pass @@ -416,10 +390,7 @@ short ) lcomp = [ element # yup - for - element - in - collection # yup + for element in collection # yup if element is not None # right ] lcomp2 = [ @@ -428,9 +399,7 @@ short # yup for element in collection # right - if - element - is not None + if element is not None ] lcomp3 = [ # This one is actually too long to fit in a single line. @@ -438,9 +407,7 @@ short # yup for element in collection.select_elements() # right - if - element - is not None + if element is not None ] while True: if False: @@ -471,8 +438,7 @@ CONFIG_FILES = ( class Test: def _init_host(self, parsed) -> None: if ( - parsed.hostname - is None # type: ignore + parsed.hostname is None # type: ignore or not parsed.hostname.strip() ): pass 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 deleted file mode 100644 index d41e20b68f..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments3.py.snap +++ /dev/null @@ -1,184 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments3.py ---- -## Input - -```py -# The percent-percent comments are Spyder IDE cells. - - -# %% -def func(): - x = """ - a really long string - """ - lcomp3 = [ - # This one is actually too long to fit in a single line. - element.split("\n", 1)[0] - # yup - for element in collection.select_elements() - # right - if element is not None - ] - # Capture each of the exceptions in the MultiError along with each of their causes and contexts - if isinstance(exc_value, MultiError): - embedded = [] - for exc in exc_value.exceptions: - if exc not in _seen: - embedded.append( - # This should be left alone (before) - traceback.TracebackException.from_exception( - exc, - limit=limit, - lookup_lines=lookup_lines, - capture_locals=capture_locals, - # 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) - ) - - # everything is fine if the expression isn't nested - traceback.TracebackException.from_exception( - exc, - limit=limit, - lookup_lines=lookup_lines, - capture_locals=capture_locals, - # copy the set of _seen exceptions so that duplicates - # shared between sub-exceptions are not omitted - _seen=set(_seen), - ) - - -# %% -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -12,7 +12,9 @@ - # yup - for element in collection.select_elements() - # right -- if element is not None -+ if -+ element -+ is not None - ] - # Capture each of the exceptions in the MultiError along with each of their causes and contexts - if isinstance(exc_value, MultiError): -``` - -## Ruff Output - -```py -# The percent-percent comments are Spyder IDE cells. - - -# %% -def func(): - x = """ - a really long string - """ - lcomp3 = [ - # This one is actually too long to fit in a single line. - element.split("\n", 1)[0] - # yup - for element in collection.select_elements() - # right - if - element - is not None - ] - # Capture each of the exceptions in the MultiError along with each of their causes and contexts - if isinstance(exc_value, MultiError): - embedded = [] - for exc in exc_value.exceptions: - if exc not in _seen: - embedded.append( - # This should be left alone (before) - traceback.TracebackException.from_exception( - exc, - limit=limit, - lookup_lines=lookup_lines, - capture_locals=capture_locals, - # 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) - ) - - # everything is fine if the expression isn't nested - traceback.TracebackException.from_exception( - exc, - limit=limit, - lookup_lines=lookup_lines, - capture_locals=capture_locals, - # copy the set of _seen exceptions so that duplicates - # shared between sub-exceptions are not omitted - _seen=set(_seen), - ) - - -# %% -``` - -## Black Output - -```py -# The percent-percent comments are Spyder IDE cells. - - -# %% -def func(): - x = """ - a really long string - """ - lcomp3 = [ - # This one is actually too long to fit in a single line. - element.split("\n", 1)[0] - # yup - for element in collection.select_elements() - # right - if element is not None - ] - # Capture each of the exceptions in the MultiError along with each of their causes and contexts - if isinstance(exc_value, MultiError): - embedded = [] - for exc in exc_value.exceptions: - if exc not in _seen: - embedded.append( - # This should be left alone (before) - traceback.TracebackException.from_exception( - exc, - limit=limit, - lookup_lines=lookup_lines, - capture_locals=capture_locals, - # 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) - ) - - # everything is fine if the expression isn't nested - traceback.TracebackException.from_exception( - exc, - limit=limit, - lookup_lines=lookup_lines, - capture_locals=capture_locals, - # copy the set of _seen exceptions so that duplicates - # shared between sub-exceptions are not omitted - _seen=set(_seen), - ) - - -# %% -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip5.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip5.py.snap index cb7904beff..58985692cd 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip5.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip5.py.snap @@ -21,13 +21,12 @@ else: ```diff --- Black +++ Ruff -@@ -1,7 +1,8 @@ +@@ -1,7 +1,7 @@ a, b, c = 3, 4, 5 if ( a == 3 - and b != 9 # fmt: skip -+ and b -+ != 9 # fmt: skip ++ and b != 9 # fmt: skip and c is not None ): print("I'm good!") @@ -39,8 +38,7 @@ else: a, b, c = 3, 4, 5 if ( a == 3 - and b - != 9 # fmt: skip + and b != 9 # fmt: skip and c is not None ): print("I'm good!") 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 0dfa48b142..169478f47f 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 @@ -133,7 +133,7 @@ def __await__(): return (yield) def spaces_types( -@@ -64,19 +63,17 @@ +@@ -64,19 +63,15 @@ def spaces2(result=_core.Value(None)): @@ -153,15 +153,13 @@ def __await__(): return (yield) - .all() - ) + result = session.query(models.Customer.id).filter( -+ models.Customer.account_id -+ == account_id, -+ models.Customer.email -+ == email_address, ++ models.Customer.account_id == account_id, ++ models.Customer.email == email_address, + ).order_by(models.Customer.id.asc()).all() def long_lines(): -@@ -135,14 +132,8 @@ +@@ -135,14 +130,8 @@ a, **kwargs, ) -> A: @@ -254,10 +252,8 @@ def spaces2(result=_core.Value(None)): def example(session): result = session.query(models.Customer.id).filter( - models.Customer.account_id - == account_id, - models.Customer.email - == email_address, + models.Customer.account_id == account_id, + models.Customer.email == email_address, ).order_by(models.Customer.id.asc()).all() 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 1356a5ef71..846734242d 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 @@ -111,20 +111,6 @@ return np.divide( q = [10.5**i for i in range(6)] -@@ -55,9 +55,11 @@ - view.variance, # type: ignore[union-attr] - view.sum_of_weights, # type: ignore[union-attr] - out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr] -- where=view.sum_of_weights**2 > view.sum_of_weights_squared, # type: ignore[union-attr] -+ where=view.sum_of_weights**2 -+ > view.sum_of_weights_squared, # type: ignore[union-attr] - ) - - return np.divide( -- where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared, # type: ignore -+ where=view.sum_of_weights_of_weight_long**2 -+ > view.sum_of_weights_squared, # type: ignore - ) ``` ## Ruff Output @@ -187,13 +173,11 @@ if hasattr(view, "sum_of_weights"): view.variance, # type: ignore[union-attr] view.sum_of_weights, # type: ignore[union-attr] out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr] - where=view.sum_of_weights**2 - > view.sum_of_weights_squared, # type: ignore[union-attr] + where=view.sum_of_weights**2 > view.sum_of_weights_squared, # type: ignore[union-attr] ) return np.divide( - where=view.sum_of_weights_of_weight_long**2 - > view.sum_of_weights_squared, # type: ignore + where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared, # type: ignore ) ``` 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 f8a09bc04e..bac556ce5f 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 @@ -154,10 +154,8 @@ f( # TODO(konstin): Call chains/fluent interface (https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#call-chains) result = session.query(models.Customer.id).filter( - models.Customer.account_id - == 10000, - models.Customer.email - == "user@example.org", + models.Customer.account_id == 10000, + models.Customer.email == "user@example.org", ).order_by(models.Customer.id.asc()).all() # TODO(konstin): Black has this special case for comment placement where everything stays in one line f("aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa") diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__list_comp.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__list_comp.py.snap index dd17b1d97f..ee8cb00842 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__list_comp.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__list_comp.py.snap @@ -36,6 +36,19 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression # above g g # g ] + +[ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + [dddddddddddddddddd, eeeeeeeeeeeeeeeeeee] + for + ccccccccccccccccccccccccccccccccccccccc, + ddddddddddddddddddd, [eeeeeeeeeeeeeeeeeeeeee, fffffffffffffffffffffffff] + in + eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffggggggggggggggggggggghhhhhhhhhhhhhhothermoreeand_even_moreddddddddddddddddddddd + if + fffffffffffffffffffffffffffffffffffffffffff < gggggggggggggggggggggggggggggggggggggggggggggg < hhhhhhhhhhhhhhhhhhhhhhhhhh + if + gggggggggggggggggggggggggggggggggggggggggggg +] ``` ## Output @@ -44,20 +57,14 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression [ i - for - i - in - [ + for i in [ 1, ] ] [ a # a - for # for - c # c - in # in - e # e + for c in e # for # c # in # e ] [ @@ -80,6 +87,21 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression # above g g # g ] + +[ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + + [dddddddddddddddddd, eeeeeeeeeeeeeeeeeee] + for ( + ccccccccccccccccccccccccccccccccccccccc, + ddddddddddddddddddd, + [eeeeeeeeeeeeeeeeeeeeee, fffffffffffffffffffffffff], + ) in eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffggggggggggggggggggggghhhhhhhhhhhhhhothermoreeand_even_moreddddddddddddddddddddd + if fffffffffffffffffffffffffffffffffffffffffff + < gggggggggggggggggggggggggggggggggggggggggggggg + < hhhhhhhhhhhhhhhhhhhhhhhhhh + if gggggggggggggggggggggggggggggggggggggggggggg +] ``` From 30bec3fcfa01c044ea232aae9d3c69b59f422d30 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 11 Jul 2023 17:05:25 +0200 Subject: [PATCH 421/447] Only omit optinal parens if the expression ends or starts with a parenthesized expression ## Summary This PR matches Black' behavior where it only omits the optional parentheses if the expression starts or ends with a parenthesized expression: ```python a + [aaa, bbb, cccc] * c # Don't omit [aaa, bbb, cccc] + a * c # Split a + c * [aaa, bbb, ccc] # Split ``` ## Test Plan This improves the Jaccard index from 0.945 to 0.946 --- .../src/expression/mod.rs | 56 +++++--- crates/ruff_python_formatter/src/lib.rs | 11 +- ...s__trailing_comma_optional_parens1.py.snap | 135 ------------------ 3 files changed, 43 insertions(+), 159 deletions(-) delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens1.py.snap diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 97c43cf75f..cfe701d35a 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -198,37 +198,30 @@ impl<'ast> IntoFormat> for Expr { /// /// This mimics Black's [`_maybe_split_omitting_optional_parens`](https://github.com/psf/black/blob/d1248ca9beaf0ba526d265f4108836d89cf551b7/src/black/linegen.py#L746-L820) fn can_omit_optional_parentheses(expr: &Expr, context: &PyFormatContext) -> bool { - let mut visitor = MaxOperatorPriorityVisitor::new(context.source()); - + let mut visitor = CanOmitOptionalParenthesesVisitor::new(context.source()); visitor.visit_subexpression(expr); - - let (max_operator_priority, operation_count, any_parenthesized_expression) = visitor.finish(); - - if operation_count > 1 { - false - } else if max_operator_priority == OperatorPriority::Attribute { - true - } else { - // Only use the more complex IR when there is any expression that we can possibly split by - any_parenthesized_expression - } + visitor.can_omit() } #[derive(Clone, Debug)] -struct MaxOperatorPriorityVisitor<'input> { +struct CanOmitOptionalParenthesesVisitor<'input> { max_priority: OperatorPriority, max_priority_count: u32, any_parenthesized_expressions: bool, + last: Option<&'input Expr>, + first: Option<&'input Expr>, source: &'input str, } -impl<'input> MaxOperatorPriorityVisitor<'input> { +impl<'input> CanOmitOptionalParenthesesVisitor<'input> { fn new(source: &'input str) -> Self { Self { source, max_priority: OperatorPriority::None, max_priority_count: 0, any_parenthesized_expressions: false, + last: None, + first: None, } } @@ -305,6 +298,7 @@ impl<'input> MaxOperatorPriorityVisitor<'input> { self.any_parenthesized_expressions = true; // Only walk the function, the arguments are always parenthesized self.visit_expr(func); + self.last = Some(expr); return; } Expr::Subscript(_) => { @@ -351,23 +345,41 @@ impl<'input> MaxOperatorPriorityVisitor<'input> { walk_expr(self, expr); } - fn finish(self) -> (OperatorPriority, u32, bool) { - ( - self.max_priority, - self.max_priority_count, - self.any_parenthesized_expressions, - ) + fn can_omit(self) -> bool { + if self.max_priority_count > 1 { + false + } else if self.max_priority == OperatorPriority::Attribute { + true + } else if !self.any_parenthesized_expressions { + // Only use the more complex IR when there is any expression that we can possibly split by + false + } else { + // Only use the layout if the first or last expression has parentheses of some sort. + let first_parenthesized = self + .first + .map_or(false, |first| has_parentheses(first, self.source)); + let last_parenthesized = self + .last + .map_or(false, |last| has_parentheses(last, self.source)); + first_parenthesized || last_parenthesized + } } } -impl<'input> PreorderVisitor<'input> for MaxOperatorPriorityVisitor<'input> { +impl<'input> PreorderVisitor<'input> for CanOmitOptionalParenthesesVisitor<'input> { fn visit_expr(&mut self, expr: &'input Expr) { + self.last = Some(expr); + // Rule only applies for non-parenthesized expressions. if is_expression_parenthesized(AnyNodeRef::from(expr), self.source) { self.any_parenthesized_expressions = true; } else { self.visit_subexpression(expr); } + + if self.first.is_none() { + self.first = Some(expr); + } } } diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 79bc92d667..44ab2e8de4 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -280,8 +280,15 @@ if True: #[test] fn quick_test() { let src = r#" -def foo() -> tuple[int, int, int,]: - return 2 +if a * [ + bbbbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccccccccccccdddddddddddddddddddddddddd, +] + a * e * [ + ffff, + gggg, + hhhhhhhhhhhhhh, +] * c: + pass "#; // Tokenize once diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens1.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens1.py.snap deleted file mode 100644 index 7fb41572f3..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens1.py.snap +++ /dev/null @@ -1,135 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_comma_optional_parens1.py ---- -## Input - -```py -if e1234123412341234.winerror not in (_winapi.ERROR_SEM_TIMEOUT, - _winapi.ERROR_PIPE_BUSY) or _check_timeout(t): - pass - -if x: - if y: - new_id = max(Vegetable.objects.order_by('-id')[0].id, - Mineral.objects.order_by('-id')[0].id) + 1 - -class X: - def get_help_text(self): - return ngettext( - "Your password must contain at least %(min_length)d character.", - "Your password must contain at least %(min_length)d characters.", - self.min_length, - ) % {'min_length': self.min_length} - -class A: - def b(self): - if self.connection.mysql_is_mariadb and ( - 10, - 4, - 3, - ) < self.connection.mysql_version < (10, 5, 2): - pass -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -6,13 +6,10 @@ - - if x: - if y: -- new_id = ( -- max( -- Vegetable.objects.order_by("-id")[0].id, -- Mineral.objects.order_by("-id")[0].id, -- ) -- + 1 -- ) -+ new_id = max( -+ Vegetable.objects.order_by("-id")[0].id, -+ Mineral.objects.order_by("-id")[0].id, -+ ) + 1 - - - class X: -``` - -## Ruff Output - -```py -if e1234123412341234.winerror not in ( - _winapi.ERROR_SEM_TIMEOUT, - _winapi.ERROR_PIPE_BUSY, -) or _check_timeout(t): - pass - -if x: - if y: - new_id = max( - Vegetable.objects.order_by("-id")[0].id, - Mineral.objects.order_by("-id")[0].id, - ) + 1 - - -class X: - def get_help_text(self): - return ngettext( - "Your password must contain at least %(min_length)d character.", - "Your password must contain at least %(min_length)d characters.", - self.min_length, - ) % {"min_length": self.min_length} - - -class A: - def b(self): - if self.connection.mysql_is_mariadb and ( - 10, - 4, - 3, - ) < self.connection.mysql_version < (10, 5, 2): - pass -``` - -## Black Output - -```py -if e1234123412341234.winerror not in ( - _winapi.ERROR_SEM_TIMEOUT, - _winapi.ERROR_PIPE_BUSY, -) or _check_timeout(t): - pass - -if x: - if y: - new_id = ( - max( - Vegetable.objects.order_by("-id")[0].id, - Mineral.objects.order_by("-id")[0].id, - ) - + 1 - ) - - -class X: - def get_help_text(self): - return ngettext( - "Your password must contain at least %(min_length)d character.", - "Your password must contain at least %(min_length)d characters.", - self.min_length, - ) % {"min_length": self.min_length} - - -class A: - def b(self): - if self.connection.mysql_is_mariadb and ( - 10, - 4, - 3, - ) < self.connection.mysql_version < (10, 5, 2): - pass -``` - - From 511ec0d7bca34a08c286abf34a7de14e6f3c56eb Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 11 Jul 2023 16:30:38 -0400 Subject: [PATCH 422/447] Refactor shebang parsing to remove regex dependency (#5690) ## Summary Similar to #5567, we can remove the use of regex, plus simplify the representation (use `Option`), add snapshot tests, etc. This is about 100x faster than using a regex for cases that match (2.5ns vs. 250ns). It's obviously not a hot path, but I prefer the consistency with other similar comment-parsing. I may DRY these up into some common functionality later on. --- crates/ruff/src/checkers/physical_lines.rs | 50 +++---- .../src/rules/flake8_executable/helpers.rs | 122 +++++++++--------- .../rules/shebang_newline.rs | 19 ++- .../rules/shebang_not_executable.rs | 17 +-- .../flake8_executable/rules/shebang_python.rs | 19 +-- .../rules/shebang_whitespace.rs | 34 ++--- ...__helpers__tests__shebang_end_of_line.snap | 5 + ...helpers__tests__shebang_leading_space.snap | 10 ++ ...utable__helpers__tests__shebang_match.snap | 10 ++ ...le__helpers__tests__shebang_non_match.snap | 5 + 10 files changed, 162 insertions(+), 129 deletions(-) create mode 100644 crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_end_of_line.snap create mode 100644 crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_leading_space.snap create mode 100644 crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_match.snap create mode 100644 crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_non_match.snap diff --git a/crates/ruff/src/checkers/physical_lines.rs b/crates/ruff/src/checkers/physical_lines.rs index 1a553f3d7d..007f02a003 100644 --- a/crates/ruff/src/checkers/physical_lines.rs +++ b/crates/ruff/src/checkers/physical_lines.rs @@ -9,7 +9,7 @@ use ruff_python_whitespace::UniversalNewlines; use crate::registry::Rule; use crate::rules::flake8_copyright::rules::missing_copyright_notice; -use crate::rules::flake8_executable::helpers::{extract_shebang, ShebangDirective}; +use crate::rules::flake8_executable::helpers::ShebangDirective; use crate::rules::flake8_executable::rules::{ shebang_missing, shebang_newline, shebang_not_executable, shebang_python, shebang_whitespace, }; @@ -87,33 +87,35 @@ pub(crate) fn check_physical_lines( || enforce_shebang_newline || enforce_shebang_python { - let shebang = extract_shebang(&line); - if enforce_shebang_not_executable { - if let Some(diagnostic) = shebang_not_executable(path, line.range(), &shebang) { - diagnostics.push(diagnostic); + if let Some(shebang) = ShebangDirective::try_extract(&line) { + has_any_shebang = true; + if enforce_shebang_not_executable { + if let Some(diagnostic) = + shebang_not_executable(path, line.range(), &shebang) + { + diagnostics.push(diagnostic); + } } - } - if enforce_shebang_missing { - if !has_any_shebang && matches!(shebang, ShebangDirective::Match(..)) { - has_any_shebang = true; + if enforce_shebang_whitespace { + if let Some(diagnostic) = + shebang_whitespace(line.range(), &shebang, fix_shebang_whitespace) + { + diagnostics.push(diagnostic); + } } - } - if enforce_shebang_whitespace { - if let Some(diagnostic) = - shebang_whitespace(line.range(), &shebang, fix_shebang_whitespace) - { - diagnostics.push(diagnostic); + if enforce_shebang_newline { + if let Some(diagnostic) = + shebang_newline(line.range(), &shebang, index == 0) + { + diagnostics.push(diagnostic); + } } - } - if enforce_shebang_newline { - if let Some(diagnostic) = shebang_newline(line.range(), &shebang, index == 0) { - diagnostics.push(diagnostic); - } - } - if enforce_shebang_python { - if let Some(diagnostic) = shebang_python(line.range(), &shebang) { - diagnostics.push(diagnostic); + if enforce_shebang_python { + if let Some(diagnostic) = shebang_python(line.range(), &shebang) { + diagnostics.push(diagnostic); + } } + } else { } } } diff --git a/crates/ruff/src/rules/flake8_executable/helpers.rs b/crates/ruff/src/rules/flake8_executable/helpers.rs index f52e746bd0..7f23613bc5 100644 --- a/crates/ruff/src/rules/flake8_executable/helpers.rs +++ b/crates/ruff/src/rules/flake8_executable/helpers.rs @@ -5,84 +5,88 @@ use std::path::Path; #[cfg(target_family = "unix")] use anyhow::Result; -use once_cell::sync::Lazy; -use regex::Regex; use ruff_text_size::{TextLen, TextSize}; -static SHEBANG_REGEX: Lazy = - Lazy::new(|| Regex::new(r"^(?P\s*)#!(?P.*)").unwrap()); - +/// A shebang directive (e.g., `#!/usr/bin/env python3`). #[derive(Debug, PartialEq, Eq)] -pub(crate) enum ShebangDirective<'a> { - None, - // whitespace length, start of the shebang, contents - Match(TextSize, TextSize, &'a str), +pub(crate) struct ShebangDirective<'a> { + /// The offset of the directive contents (e.g., `/usr/bin/env python3`) from the start of the + /// line. + pub(crate) offset: TextSize, + /// The contents of the directive (e.g., `"/usr/bin/env python3"`). + pub(crate) contents: &'a str, } -pub(crate) fn extract_shebang(line: &str) -> ShebangDirective { - // Minor optimization to avoid matches in the common case. - if !line.contains('!') { - return ShebangDirective::None; +impl<'a> ShebangDirective<'a> { + /// Parse a shebang directive from a line, or return `None` if the line does not contain a + /// shebang directive. + pub(crate) fn try_extract(line: &'a str) -> Option { + // Trim whitespace. + let directive = Self::lex_whitespace(line); + + // Trim the `#!` prefix. + let directive = Self::lex_char(directive, '#')?; + let directive = Self::lex_char(directive, '!')?; + + Some(Self { + offset: line.text_len() - directive.text_len(), + contents: directive, + }) } - match SHEBANG_REGEX.captures(line) { - Some(caps) => match caps.name("spaces") { - Some(spaces) => match caps.name("directive") { - Some(matches) => ShebangDirective::Match( - spaces.as_str().text_len(), - TextSize::try_from(matches.start()).unwrap(), - matches.as_str(), - ), - None => ShebangDirective::None, - }, - None => ShebangDirective::None, - }, - None => ShebangDirective::None, + + /// Lex optional leading whitespace. + #[inline] + fn lex_whitespace(line: &str) -> &str { + line.trim_start() + } + + /// Lex a specific character, or return `None` if the character is not the first character in + /// the line. + #[inline] + fn lex_char(line: &str, c: char) -> Option<&str> { + let mut chars = line.chars(); + if chars.next() == Some(c) { + Some(chars.as_str()) + } else { + None + } } } #[cfg(target_family = "unix")] -pub(crate) fn is_executable(filepath: &Path) -> Result { - { - let metadata = filepath.metadata()?; - let permissions = metadata.permissions(); - Ok(permissions.mode() & 0o111 != 0) - } +pub(super) fn is_executable(filepath: &Path) -> Result { + let metadata = filepath.metadata()?; + let permissions = metadata.permissions(); + Ok(permissions.mode() & 0o111 != 0) } #[cfg(test)] mod tests { - use ruff_text_size::TextSize; + use insta::assert_debug_snapshot; - use crate::rules::flake8_executable::helpers::{ - extract_shebang, ShebangDirective, SHEBANG_REGEX, - }; + use crate::rules::flake8_executable::helpers::ShebangDirective; #[test] - fn shebang_regex() { - // Positive cases - assert!(SHEBANG_REGEX.is_match("#!/usr/bin/python")); - assert!(SHEBANG_REGEX.is_match("#!/usr/bin/env python")); - assert!(SHEBANG_REGEX.is_match(" #!/usr/bin/env python")); - assert!(SHEBANG_REGEX.is_match(" #!/usr/bin/env python")); - - // Negative cases - assert!(!SHEBANG_REGEX.is_match("hello world")); + fn shebang_non_match() { + let source = "not a match"; + assert_debug_snapshot!(ShebangDirective::try_extract(source)); } #[test] - fn shebang_extract_match() { - assert_eq!(extract_shebang("not a match"), ShebangDirective::None); - assert_eq!( - extract_shebang("#!/usr/bin/env python"), - ShebangDirective::Match(TextSize::from(0), TextSize::from(2), "/usr/bin/env python") - ); - assert_eq!( - extract_shebang(" #!/usr/bin/env python"), - ShebangDirective::Match(TextSize::from(2), TextSize::from(4), "/usr/bin/env python") - ); - assert_eq!( - extract_shebang("print('test') #!/usr/bin/python"), - ShebangDirective::None - ); + fn shebang_end_of_line() { + let source = "print('test') #!/usr/bin/python"; + assert_debug_snapshot!(ShebangDirective::try_extract(source)); + } + + #[test] + fn shebang_match() { + let source = "#!/usr/bin/env python"; + assert_debug_snapshot!(ShebangDirective::try_extract(source)); + } + + #[test] + fn shebang_leading_space() { + let source = " #!/usr/bin/env python"; + assert_debug_snapshot!(ShebangDirective::try_extract(source)); } } diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs index 322c68be19..d1ae29ee77 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs @@ -47,17 +47,14 @@ pub(crate) fn shebang_newline( shebang: &ShebangDirective, first_line: bool, ) -> Option { - if let ShebangDirective::Match(_, start, content) = shebang { - if first_line { - None - } else { - let diagnostic = Diagnostic::new( - ShebangNotFirstLine, - TextRange::at(range.start() + start, content.text_len()), - ); - Some(diagnostic) - } - } else { + let ShebangDirective { offset, contents } = shebang; + + if first_line { None + } else { + Some(Diagnostic::new( + ShebangNotFirstLine, + TextRange::at(range.start() + offset, contents.text_len()), + )) } } diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs index 20c542d0d4..b5b37037fc 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs @@ -48,15 +48,16 @@ pub(crate) fn shebang_not_executable( range: TextRange, shebang: &ShebangDirective, ) -> Option { - if let ShebangDirective::Match(_, start, content) = shebang { - if let Ok(false) = is_executable(filepath) { - let diagnostic = Diagnostic::new( - ShebangNotExecutable, - TextRange::at(range.start() + start, content.text_len()), - ); - return Some(diagnostic); - } + let ShebangDirective { offset, contents } = shebang; + + if let Ok(false) = is_executable(filepath) { + let diagnostic = Diagnostic::new( + ShebangNotExecutable, + TextRange::at(range.start() + offset, contents.text_len()), + ); + return Some(diagnostic); } + None } diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs index b68eb42be9..0552de6f05 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs @@ -42,19 +42,14 @@ impl Violation for ShebangMissingPython { /// EXE003 pub(crate) fn shebang_python(range: TextRange, shebang: &ShebangDirective) -> Option { - if let ShebangDirective::Match(_, start, content) = shebang { - if content.contains("python") || content.contains("pytest") { - None - } else { - let diagnostic = Diagnostic::new( - ShebangMissingPython, - TextRange::at(range.start() + start, content.text_len()) - .sub_start(TextSize::from(2)), - ); + let ShebangDirective { offset, contents } = shebang; - Some(diagnostic) - } - } else { + if contents.contains("python") || contents.contains("pytest") { None + } else { + Some(Diagnostic::new( + ShebangMissingPython, + TextRange::at(range.start() + offset, contents.text_len()).sub_start(TextSize::from(2)), + )) } } diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs index 93921bfc32..b0394344a8 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs @@ -1,4 +1,5 @@ use ruff_text_size::{TextRange, TextSize}; +use std::ops::Sub; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -49,22 +50,25 @@ pub(crate) fn shebang_whitespace( shebang: &ShebangDirective, autofix: bool, ) -> Option { - if let ShebangDirective::Match(n_spaces, start, ..) = shebang { - if *n_spaces > TextSize::from(0) && *start == n_spaces + TextSize::from(2) { - let mut diagnostic = Diagnostic::new( - ShebangLeadingWhitespace, - TextRange::at(range.start(), *n_spaces), - ); - if autofix { - diagnostic.set_fix(Fix::automatic(Edit::range_deletion(TextRange::at( - range.start(), - *n_spaces, - )))); - } - Some(diagnostic) - } else { - None + let ShebangDirective { + offset, + contents: _, + } = shebang; + + if *offset > TextSize::from(2) { + let leading_space_start = range.start(); + let leading_space_len = offset.sub(TextSize::new(2)); + let mut diagnostic = Diagnostic::new( + ShebangLeadingWhitespace, + TextRange::at(leading_space_start, leading_space_len), + ); + if autofix { + diagnostic.set_fix(Fix::automatic(Edit::range_deletion(TextRange::at( + leading_space_start, + leading_space_len, + )))); } + Some(diagnostic) } else { None } diff --git a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_end_of_line.snap b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_end_of_line.snap new file mode 100644 index 0000000000..e5550fcc2c --- /dev/null +++ b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_end_of_line.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff/src/rules/flake8_executable/helpers.rs +expression: "ShebangDirective::try_extract(source)" +--- +None diff --git a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_leading_space.snap b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_leading_space.snap new file mode 100644 index 0000000000..abb2535298 --- /dev/null +++ b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_leading_space.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff/src/rules/flake8_executable/helpers.rs +expression: "ShebangDirective::try_extract(source)" +--- +Some( + ShebangDirective { + offset: 4, + contents: "/usr/bin/env python", + }, +) diff --git a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_match.snap b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_match.snap new file mode 100644 index 0000000000..05f3d5fe3b --- /dev/null +++ b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_match.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff/src/rules/flake8_executable/helpers.rs +expression: "ShebangDirective::try_extract(source)" +--- +Some( + ShebangDirective { + offset: 2, + contents: "/usr/bin/env python", + }, +) diff --git a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_non_match.snap b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_non_match.snap new file mode 100644 index 0000000000..e5550fcc2c --- /dev/null +++ b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_non_match.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff/src/rules/flake8_executable/helpers.rs +expression: "ShebangDirective::try_extract(source)" +--- +None From f8173daf4ce5bdf8811daf57911f371a0f992af9 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Wed, 12 Jul 2023 00:56:51 +0100 Subject: [PATCH 423/447] Add documentation to the `S3XX` rules (#5592) ## Summary Add documentation to the `S3XX` rules (the `flake8-bandit` ['blacklists'](https://bandit.readthedocs.io/en/latest/plugins/index.html#plugin-id-groupings) rule group). Related to #2646 . Changed the `lxml`-based message to reflect that [`defusedxml` doesn't support `lxml`](https://github.com/tiran/defusedxml/issues/31). ## Test Plan `python scripts/check_docs_formatted.py && mkdocs serve` --- .../rules/hashlib_insecure_hash_functions.rs | 38 ++ .../rules/suspicious_function_call.rs | 613 +++++++++++++++++- 2 files changed, 650 insertions(+), 1 deletion(-) diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs b/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs index 91ee429343..7a8385e8d5 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs @@ -8,6 +8,44 @@ use crate::checkers::ast::Checker; use super::super::helpers::string_literal; +/// ## What it does +/// Checks for uses of weak or broken cryptographic hash functions. +/// +/// ## Why is this bad? +/// Weak or broken cryptographic hash functions may be susceptible to +/// collision attacks (where two different inputs produce the same hash) or +/// pre-image attacks (where an attacker can find an input that produces a +/// given hash). This can lead to security vulnerabilities in applications +/// that rely on these hash functions. +/// +/// Avoid using weak or broken cryptographic hash functions in security +/// contexts. Instead, use a known secure hash function such as SHA256. +/// +/// ## Example +/// ```python +/// import hashlib +/// +/// +/// def certificate_is_valid(certificate: bytes, known_hash: str) -> bool: +/// hash = hashlib.md5(certificate).hexdigest() +/// return hash == known_hash +/// ``` +/// +/// Use instead: +/// ```python +/// import hashlib +/// +/// +/// def certificate_is_valid(certificate: bytes, known_hash: str) -> bool: +/// hash = hashlib.sha256(certificate).hexdigest() +/// return hash == known_hash +/// ``` +/// +/// ## References +/// - [Python documentation: `hashlib` — Secure hashes and message digests](https://docs.python.org/3/library/hashlib.html) +/// - [Common Weakness Enumeration: CWE-327](https://cwe.mitre.org/data/definitions/327.html) +/// - [Common Weakness Enumeration: CWE-328](https://cwe.mitre.org/data/definitions/328.html) +/// - [Common Weakness Enumeration: CWE-916](https://cwe.mitre.org/data/definitions/916.html) #[violation] pub struct HashlibInsecureHashFunction { string: String, 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 6a2add760d..e7c2773823 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 @@ -9,6 +9,42 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for calls to `pickle` functions or modules that wrap them. +/// +/// ## Why is this bad? +/// Deserializing untrusted data with `pickle` and other deserialization +/// modules is insecure as it can allow for the creation of arbitrary objects, +/// which can then be used to achieve arbitrary code execution and otherwise +/// unexpected behavior. +/// +/// Avoid deserializing untrusted data with `pickle` and other deserialization +/// modules. Instead, consider safer formats, such as JSON. +/// +/// If you must deserialize untrusted data with `pickle`, consider signing the +/// data with a secret key and verifying the signature before deserializing +/// (such as with `hmac`). This will prevent an attacker from modifying the +/// serialized data to inject arbitrary objects. +/// +/// ## Example +/// ```python +/// import pickle +/// +/// with open("foo.pickle", "rb") as file: +/// foo = pickle.load(file) +/// ``` +/// +/// Use instead: +/// ```python +/// import json +/// +/// with open("foo.json", "rb") as file: +/// foo = json.load(file) +/// ``` +/// +/// ## References +/// - [Python documentation: `pickle` — Python object serialization](https://docs.python.org/3/library/pickle.html) +/// - [Common Weakness Enumeration: CWE-502](https://cwe.mitre.org/data/definitions/502.html) #[violation] pub struct SuspiciousPickleUsage; @@ -19,6 +55,41 @@ impl Violation for SuspiciousPickleUsage { } } +/// ## What it does +/// Checks for calls to `marshal` functions. +/// +/// ## Why is this bad? +/// Deserializing untrusted data with `marshal` is insecure as it can allow for +/// the creation of arbitrary objects, which can then be used to achieve +/// arbitrary code execution and otherwise unexpected behavior. +/// +/// Avoid deserializing untrusted data with `marshal`. Instead, consider safer +/// formats, such as JSON. +/// +/// If you must deserialize untrusted data with `marshal`, consider signing the +/// data with a secret key and verifying the signature before deserializing +/// (such as with `hmac`). This will prevent an attacker from modifying the +/// serialized data to inject arbitrary objects. +/// +/// ## Example +/// ```python +/// import marshal +/// +/// with open("foo.marshal", "rb") as file: +/// foo = pickle.load(file) +/// ``` +/// +/// Use instead: +/// ```python +/// import json +/// +/// with open("foo.json", "rb") as file: +/// foo = json.load(file) +/// ``` +/// +/// ## References +/// - [Python documentation: `marshal` — Internal Python object serialization](https://docs.python.org/3/library/marshal.html) +/// - [Common Weakness Enumeration: CWE-502](https://cwe.mitre.org/data/definitions/502.html) #[violation] pub struct SuspiciousMarshalUsage; @@ -29,6 +100,42 @@ impl Violation for SuspiciousMarshalUsage { } } +/// ## What it does +/// Checks for uses of weak or broken cryptographic hash functions. +/// +/// ## Why is this bad? +/// Weak or broken cryptographic hash functions may be susceptible to +/// collision attacks (where two different inputs produce the same hash) or +/// pre-image attacks (where an attacker can find an input that produces a +/// given hash). This can lead to security vulnerabilities in applications +/// that rely on these hash functions. +/// +/// Avoid using weak or broken cryptographic hash functions in security +/// contexts. Instead, use a known secure hash function such as SHA256. +/// +/// ## Example +/// ```python +/// from cryptography.hazmat.primitives import hashes +/// +/// digest = hashes.Hash(hashes.MD5()) +/// digest.update(b"Hello, world!") +/// digest.finalize() +/// ``` +/// +/// Use instead: +/// ```python +/// from cryptography.hazmat.primitives import hashes +/// +/// digest = hashes.Hash(hashes.SHA256()) +/// digest.update(b"Hello, world!") +/// digest.finalize() +/// ``` +/// +/// ## References +/// - [Python documentation: `hashlib` — Secure hashes and message digests](https://docs.python.org/3/library/hashlib.html) +/// - [Common Weakness Enumeration: CWE-327](https://cwe.mitre.org/data/definitions/327.html) +/// - [Common Weakness Enumeration: CWE-328](https://cwe.mitre.org/data/definitions/328.html) +/// - [Common Weakness Enumeration: CWE-916](https://cwe.mitre.org/data/definitions/916.html) #[violation] pub struct SuspiciousInsecureHashUsage; @@ -39,6 +146,34 @@ impl Violation for SuspiciousInsecureHashUsage { } } +/// ## What it does +/// Checks for uses of weak or broken cryptographic ciphers. +/// +/// ## Why is this bad? +/// Weak or broken cryptographic ciphers may be susceptible to attacks that +/// allow an attacker to decrypt ciphertext without knowing the key or +/// otherwise compromise the security of the cipher, such as forgeries. +/// +/// Use strong, modern cryptographic ciphers instead of weak or broken ones. +/// +/// ## Example +/// ```python +/// from cryptography.hazmat.primitives.ciphers import Cipher, algorithms +/// +/// algorithm = algorithms.ARC4(key) +/// cipher = Cipher(algorithm, mode=None) +/// encryptor = cipher.encryptor() +/// ``` +/// +/// Use instead: +/// ```python +/// from cryptography.fernet import Fernet +/// +/// fernet = Fernet(key) +/// ``` +/// +/// ## References +/// - [Common Weakness Enumeration: CWE-327](https://cwe.mitre.org/data/definitions/327.html) #[violation] pub struct SuspiciousInsecureCipherUsage; @@ -49,6 +184,36 @@ impl Violation for SuspiciousInsecureCipherUsage { } } +/// ## What it does +/// Checks for uses of weak or broken cryptographic cipher modes. +/// +/// ## Why is this bad? +/// Weak or broken cryptographic ciphers may be susceptible to attacks that +/// allow an attacker to decrypt ciphertext without knowing the key or +/// otherwise compromise the security of the cipher, such as forgeries. +/// +/// Use strong, modern cryptographic ciphers instead of weak or broken ones. +/// +/// ## Example +/// ```python +/// from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +/// +/// algorithm = algorithms.ARC4(key) +/// cipher = Cipher(algorithm, mode=modes.ECB(iv)) +/// encryptor = cipher.encryptor() +/// ``` +/// +/// Use instead: +/// ```python +/// from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +/// +/// algorithm = algorithms.ARC4(key) +/// cipher = Cipher(algorithm, mode=modes.CTR(iv)) +/// encryptor = cipher.encryptor() +/// ``` +/// +/// ## References +/// - [Common Weakness Enumeration: CWE-327](https://cwe.mitre.org/data/definitions/327.html) #[violation] pub struct SuspiciousInsecureCipherModeUsage; @@ -59,6 +224,41 @@ impl Violation for SuspiciousInsecureCipherModeUsage { } } +/// ## What it does +/// Checks for uses of `tempfile.mktemp`. +/// +/// ## Why is this bad? +/// `tempfile.mktemp` returns a pathname of a file that does not exist at the +/// time the call is made; then, the caller is responsible for creating the +/// file and subsequently using it. This is insecure because another process +/// could create a file with the same name between the time the function +/// returns and the time the caller creates the file. +/// +/// `tempfile.mktemp` is deprecated in favor of `tempfile.mkstemp` which +/// creates the file when it is called. Consider using `tempfile.mkstemp` +/// instead, either directly or via a context manager such as +/// `tempfile.TemporaryFile`. +/// +/// +/// ## Example +/// ```python +/// import tempfile +/// +/// tmp_file = tempfile.mktemp() +/// with open(tmp_file, "w") as file: +/// file.write("Hello, world!") +/// ``` +/// +/// Use instead: +/// ```python +/// import tempfile +/// +/// with tempfile.TemporaryFile() as file: +/// file.write("Hello, world!") +/// ``` +/// +/// ## References +/// - [Python documentation:`mktemp`](https://docs.python.org/3/library/tempfile.html#tempfile.mktemp) #[violation] pub struct SuspiciousMktempUsage; @@ -69,6 +269,32 @@ impl Violation for SuspiciousMktempUsage { } } +/// ## What it does +/// Checks for uses of the builtin `eval()` function. +/// +/// ## Why is this bad? +/// The `eval()` function is insecure as it enables arbitrary code execution. +/// +/// If you need to evaluate an expression from a string, consider using +/// `ast.literal_eval()` instead, which will raise an exception if the +/// expression is not a valid Python literal. +/// +/// ## Example +/// ```python +/// x = eval(input("Enter a number: ")) +/// ``` +/// +/// Use instead: +/// ```python +/// from ast import literal_eval +/// +/// x = literal_eval(input("Enter a number: ")) +/// ``` +/// +/// ## References +/// - [Python documentation: `eval`](https://docs.python.org/3/library/functions.html#eval) +/// - [Python documentation: `literal_eval`](https://docs.python.org/3/library/ast.html#ast.literal_eval) +/// - [_Eval really is dangerous_ by Ned Batchelder](https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html) #[violation] pub struct SuspiciousEvalUsage; @@ -79,6 +305,35 @@ impl Violation for SuspiciousEvalUsage { } } +/// ## What it does +/// Checks for uses of calls to `django.utils.safestring.mark_safe`. +/// +/// ## Why is this bad? +/// Cross-site scripting (XSS) vulnerabilities allow attackers to execute +/// arbitrary JavaScript. To guard against XSS attacks, Django templates +/// assumes that data is unsafe and automatically escapes malicious strings +/// before rending them. +/// +/// `django.utils.safestring.mark_safe` marks a string as safe for use in HTML +/// templates, bypassing XSS protection. This is dangerous because it may allow +/// cross-site scripting attacks if the string is not properly escaped. +/// +/// ## Example +/// ```python +/// from django.utils.safestring import mark_safe +/// +/// content = mark_safe("") # XSS. +/// ``` +/// +/// Use instead: +/// ```python +/// content = "" # Safe if rendered. +/// ``` +/// +/// ## References +/// - [Django documentation: `mark_safe`](https://docs.djangoproject.com/en/dev/ref/utils/#django.utils.safestring.mark_safe) +/// - [Django documentation: Cross Site Scripting (XSS) protection](https://docs.djangoproject.com/en/dev/topics/security/#cross-site-scripting-xss-protection) +/// - [Common Weakness Enumeration: CWE-80](https://cwe.mitre.org/data/definitions/80.html) #[violation] pub struct SuspiciousMarkSafeUsage; @@ -89,6 +344,44 @@ impl Violation for SuspiciousMarkSafeUsage { } } +/// ## What it does +/// Checks for uses of URL open functions that unexpected schemes. +/// +/// ## Why is this bad? +/// Some URL open functions allow the use of `file:` or custom schemes (for use +/// instead of `http:` or `https:`). An attacker may be able to use these +/// schemes to access or modify unauthorized resources, and cause unexpected +/// behavior. +/// +/// To mitigate this risk, audit all uses of URL open functions and ensure that +/// only permitted schemes are used (e.g., allowing `http:` and `https:` and +/// disallowing `file:` and `ftp:`). +/// +/// ## Example +/// ```python +/// from urllib.request import urlopen +/// +/// url = input("Enter a URL: ") +/// +/// with urlopen(url) as response: +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// from urllib.request import urlopen +/// +/// url = input("Enter a URL: ") +/// +/// if not url.startswith(("http:", "https:")): +/// raise ValueError("URL must start with 'http:' or 'https:'") +/// +/// with urlopen(url) as response: +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: `urlopen`](https://docs.python.org/3/library/urllib.request.html#urllib.request.urlopen) #[violation] pub struct SuspiciousURLOpenUsage; @@ -99,6 +392,34 @@ impl Violation for SuspiciousURLOpenUsage { } } +/// ## What it does +/// Checks for uses of cryptographically weak pseudo-random number generators. +/// +/// ## Why is this bad? +/// Cryptographically weak pseudo-random number generators are insecure as they +/// are easily predictable. This can allow an attacker to guess the generated +/// numbers and compromise the security of the system. +/// +/// Instead, use a cryptographically secure pseudo-random number generator +/// (such as using the [`secrets` module](https://docs.python.org/3/library/secrets.html)) +/// when generating random numbers for security purposes. +/// +/// ## Example +/// ```python +/// import random +/// +/// random.randrange(10) +/// ``` +/// +/// Use instead: +/// ```python +/// import secrets +/// +/// secrets.randbelow(10) +/// ``` +/// +/// ## References +/// - [Python documentation: `random` — Generate pseudo-random numbers](https://docs.python.org/3/library/random.html) #[violation] pub struct SuspiciousNonCryptographicRandomUsage; @@ -109,6 +430,37 @@ impl Violation for SuspiciousNonCryptographicRandomUsage { } } +/// ## What it does +/// Checks for uses of insecure XML parsers. +/// +/// ## Why is this bad? +/// Many XML parsers are vulnerable to XML attacks, such as entity expansion +/// which cause excessive memory and CPU usage by exploiting recursion. In some +/// situations, it may be possible for an attacker to access unauthorized +/// resources. +/// +/// Consider using the `defusedxml` packaging when parsing untrusted XML data, +/// which protects against XML attacks. +/// +/// ## Example +/// ```python +/// from xml.etree.cElementTree import parse +/// +/// tree = parse("untrusted.xml") # Vulnerable to XML attacks. +/// ``` +/// +/// Use instead: +/// ```python +/// from defusedxml.cElementTree import parse +/// +/// tree = parse("untrusted.xml") +/// ``` +/// +/// ## References +/// - [Python documentation: `xml` — XML processing modules](https://docs.python.org/3/library/xml.html) +/// - [PyPI: `defusedxml`](https://pypi.org/project/defusedxml/) +/// - [Common Weakness Enumeration: CWE-400](https://cwe.mitre.org/data/definitions/400.html) +/// - [Common Weakness Enumeration: CWE-776](https://cwe.mitre.org/data/definitions/776.html) #[violation] pub struct SuspiciousXMLCElementTreeUsage; @@ -119,6 +471,37 @@ impl Violation for SuspiciousXMLCElementTreeUsage { } } +/// ## What it does +/// Checks for uses of insecure XML parsers. +/// +/// ## Why is this bad? +/// Many XML parsers are vulnerable to XML attacks, such as entity expansion +/// which cause excessive memory and CPU usage by exploiting recursion. In some +/// situations, it may be possible for an attacker to access unauthorized +/// resources. +/// +/// Consider using the `defusedxml` packaging when parsing untrusted XML data, +/// which protects against XML attacks. +/// +/// ## Example +/// ```python +/// from xml.etree.ElementTree import parse +/// +/// tree = parse("untrusted.xml") # Vulnerable to XML attacks. +/// ``` +/// +/// Use instead: +/// ```python +/// from defusedxml.ElementTree import parse +/// +/// tree = parse("untrusted.xml") +/// ``` +/// +/// ## References +/// - [Python documentation: `xml` — XML processing modules](https://docs.python.org/3/library/xml.html) +/// - [PyPI: `defusedxml`](https://pypi.org/project/defusedxml/) +/// - [Common Weakness Enumeration: CWE-400](https://cwe.mitre.org/data/definitions/400.html) +/// - [Common Weakness Enumeration: CWE-776](https://cwe.mitre.org/data/definitions/776.html) #[violation] pub struct SuspiciousXMLElementTreeUsage; @@ -129,6 +512,37 @@ impl Violation for SuspiciousXMLElementTreeUsage { } } +/// ## What it does +/// Checks for uses of insecure XML parsers. +/// +/// ## Why is this bad? +/// Many XML parsers are vulnerable to XML attacks, such as entity expansion +/// which cause excessive memory and CPU usage by exploiting recursion. In some +/// situations, it may be possible for an attacker to access unauthorized +/// resources. +/// +/// Consider using the `defusedxml` packaging when parsing untrusted XML data, +/// which protects against XML attacks. +/// +/// ## Example +/// ```python +/// from xml.sax.expatreader import create_parser +/// +/// parser = create_parser() +/// ``` +/// +/// Use instead: +/// ```python +/// from defusedxml.sax import create_parser +/// +/// parser = create_parser() +/// ``` +/// +/// ## References +/// - [Python documentation: `xml` — XML processing modules](https://docs.python.org/3/library/xml.html) +/// - [PyPI: `defusedxml`](https://pypi.org/project/defusedxml/) +/// - [Common Weakness Enumeration: CWE-400](https://cwe.mitre.org/data/definitions/400.html) +/// - [Common Weakness Enumeration: CWE-776](https://cwe.mitre.org/data/definitions/776.html) #[violation] pub struct SuspiciousXMLExpatReaderUsage; @@ -139,6 +553,37 @@ impl Violation for SuspiciousXMLExpatReaderUsage { } } +/// ## What it does +/// Checks for uses of insecure XML parsers. +/// +/// ## Why is this bad? +/// Many XML parsers are vulnerable to XML attacks, such as entity expansion +/// which cause excessive memory and CPU usage by exploiting recursion. In some +/// situations, it may be possible for an attacker to access unauthorized +/// resources. +/// +/// Consider using the `defusedxml` packaging when parsing untrusted XML data, +/// which protects against XML attacks. +/// +/// ## Example +/// ```python +/// from xml.dom.expatbuilder import parse +/// +/// parse("untrusted.xml") +/// ``` +/// +/// Use instead: +/// ```python +/// from defusedxml.expatbuilder import parse +/// +/// tree = parse("untrusted.xml") +/// ``` +/// +/// ## References +/// - [Python documentation: `xml` — XML processing modules](https://docs.python.org/3/library/xml.html) +/// - [PyPI: `defusedxml`](https://pypi.org/project/defusedxml/) +/// - [Common Weakness Enumeration: CWE-400](https://cwe.mitre.org/data/definitions/400.html) +/// - [Common Weakness Enumeration: CWE-776](https://cwe.mitre.org/data/definitions/776.html) #[violation] pub struct SuspiciousXMLExpatBuilderUsage; @@ -149,6 +594,37 @@ impl Violation for SuspiciousXMLExpatBuilderUsage { } } +/// ## What it does +/// Checks for uses of insecure XML parsers. +/// +/// ## Why is this bad? +/// Many XML parsers are vulnerable to XML attacks, such as entity expansion +/// which cause excessive memory and CPU usage by exploiting recursion. In some +/// situations, it may be possible for an attacker to access unauthorized +/// resources. +/// +/// Consider using the `defusedxml` packaging when parsing untrusted XML data, +/// which protects against XML attacks. +/// +/// ## Example +/// ```python +/// from xml.sax import make_parser +/// +/// make_parser() +/// ``` +/// +/// Use instead: +/// ```python +/// from defusedxml.sax import make_parser +/// +/// make_parser() +/// ``` +/// +/// ## References +/// - [Python documentation: `xml` — XML processing modules](https://docs.python.org/3/library/xml.html) +/// - [PyPI: `defusedxml`](https://pypi.org/project/defusedxml/) +/// - [Common Weakness Enumeration: CWE-400](https://cwe.mitre.org/data/definitions/400.html) +/// - [Common Weakness Enumeration: CWE-776](https://cwe.mitre.org/data/definitions/776.html) #[violation] pub struct SuspiciousXMLSaxUsage; @@ -159,6 +635,37 @@ impl Violation for SuspiciousXMLSaxUsage { } } +/// ## What it does +/// Checks for uses of insecure XML parsers. +/// +/// ## Why is this bad? +/// Many XML parsers are vulnerable to XML attacks, such as entity expansion +/// which cause excessive memory and CPU usage by exploiting recursion. In some +/// situations, it may be possible for an attacker to access unauthorized +/// resources. +/// +/// Consider using the `defusedxml` packaging when parsing untrusted XML data, +/// which protects against XML attacks. +/// +/// ## Example +/// ```python +/// from xml.dom.minidom import parse +/// +/// content = parse("untrusted.xml") +/// ``` +/// +/// Use instead: +/// ```python +/// from defusedxml.minidom import parse +/// +/// content = parse("untrusted.xml") +/// ``` +/// +/// ## References +/// - [Python documentation: `xml` — XML processing modules](https://docs.python.org/3/library/xml.html) +/// - [PyPI: `defusedxml`](https://pypi.org/project/defusedxml/) +/// - [Common Weakness Enumeration: CWE-400](https://cwe.mitre.org/data/definitions/400.html) +/// - [Common Weakness Enumeration: CWE-776](https://cwe.mitre.org/data/definitions/776.html) #[violation] pub struct SuspiciousXMLMiniDOMUsage; @@ -169,6 +676,37 @@ impl Violation for SuspiciousXMLMiniDOMUsage { } } +/// ## What it does +/// Checks for uses of insecure XML parsers. +/// +/// ## Why is this bad? +/// Many XML parsers are vulnerable to XML attacks, such as entity expansion +/// which cause excessive memory and CPU usage by exploiting recursion. In some +/// situations, it may be possible for an attacker to access unauthorized +/// resources. +/// +/// Consider using the `defusedxml` packaging when parsing untrusted XML data, +/// which protects against XML attacks. +/// +/// ## Example +/// ```python +/// from xml.dom.pulldom import parse +/// +/// content = parse("untrusted.xml") +/// ``` +/// +/// Use instead: +/// ```python +/// from defusedxml.pulldom import parse +/// +/// content = parse("untrusted.xml") +/// ``` +/// +/// ## References +/// - [Python documentation: `xml` — XML processing modules](https://docs.python.org/3/library/xml.html) +/// - [PyPI: `defusedxml`](https://pypi.org/project/defusedxml/) +/// - [Common Weakness Enumeration: CWE-400](https://cwe.mitre.org/data/definitions/400.html) +/// - [Common Weakness Enumeration: CWE-776](https://cwe.mitre.org/data/definitions/776.html) #[violation] pub struct SuspiciousXMLPullDOMUsage; @@ -179,16 +717,67 @@ impl Violation for SuspiciousXMLPullDOMUsage { } } +/// ## What it does +/// Checks for uses of insecure XML parsers. +/// +/// ## Why is this bad? +/// Many XML parsers are vulnerable to XML attacks, such as entity expansion +/// which cause excessive memory and CPU usage by exploiting recursion. In some +/// situations, it may be possible for an attacker to access unauthorized +/// resources. +/// +/// ## Example +/// ```python +/// from lxml import etree +/// +/// content = etree.parse("untrusted.xml") +/// ``` +/// +/// ## References +/// - [PyPI: `lxml`](https://pypi.org/project/lxml/) +/// - [Common Weakness Enumeration: CWE-400](https://cwe.mitre.org/data/definitions/400.html) +/// - [Common Weakness Enumeration: CWE-776](https://cwe.mitre.org/data/definitions/776.html) #[violation] pub struct SuspiciousXMLETreeUsage; impl Violation for SuspiciousXMLETreeUsage { #[derive_message_formats] fn message(&self) -> String { - format!("Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents") + format!("Using `lxml` to parse untrusted data is known to be vulnerable to XML attacks") } } +/// ## What it does +/// Checks for uses of `ssl._create_unverified_context`. +/// +/// ## Why is this bad? +/// [PEP 476](https://peps.python.org/pep-0476/) enabled certificate and +/// hostname validation by default in Python standard library HTTP clients. +/// Previously, Python did not validate certificates by default, which could +/// allow an attacker to perform a "man in the middle" attack where they +/// intercept and modify the traffic between the client and server. +/// +/// To support legacy environments, `ssl._create_unverified_context` reverts to +/// the previous behavior that does perform verification. Otherwise, use +/// `ssl.create_default_context` to create a secure context. +/// +/// ## Example +/// ```python +/// import ssl +/// +/// context = ssl._create_unverified_context() +/// ``` +/// +/// Use instead: +/// ```python +/// import ssl +/// +/// context = ssl.create_default_context() +/// ``` +/// +/// ## References +/// - [PEP 476 – Enabling certificate verification by default for stdlib http clients: Opting out](https://peps.python.org/pep-0476/#opting-out) +/// - [Python documentation: `ssl` — TLS/SSL wrapper for socket objects](https://docs.python.org/3/library/ssl.html) #[violation] pub struct SuspiciousUnverifiedContextUsage; @@ -199,6 +788,17 @@ impl Violation for SuspiciousUnverifiedContextUsage { } } +/// ## What it does +/// Checks for the use of Telnet-related functions. +/// +/// ## Why is this bad? +/// Telnet is considered insecure because it does not encrypt data sent over +/// the connection and is vulnerable to numerous attacks. +/// +/// Instead, consider using a more secure protocol such as SSH. +/// +/// ## References +/// - [Python documentation: `telnetlib` — Telnet client](https://docs.python.org/3/library/telnetlib.html) #[violation] pub struct SuspiciousTelnetUsage; @@ -209,6 +809,17 @@ impl Violation for SuspiciousTelnetUsage { } } +/// ## What it does +/// Checks for the use of FTP-related functions. +/// +/// ## Why is this bad? +/// FTP is considered insecure because it does not encrypt data sent over the +/// connection and is vulnerable to numerous attacks. +/// +/// Instead, consider using FTPS (which secures FTP using SSL/TLS) or SFTP. +/// +/// ## References +/// - [Python documentation: `ftplib` — FTP protocol client](https://docs.python.org/3/library/ftplib.html) #[violation] pub struct SuspiciousFTPLibUsage; From 5dd9e567481fb4d83214be4998833f8b11945727 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 11 Jul 2023 23:32:15 -0400 Subject: [PATCH 424/447] Misc. tweaks to bandit documentation (#5701) --- .../rules/suspicious_function_call.rs | 115 ++++++++---------- 1 file changed, 54 insertions(+), 61 deletions(-) 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 e7c2773823..79687fe458 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 @@ -22,9 +22,9 @@ use crate::registry::AsRule; /// modules. Instead, consider safer formats, such as JSON. /// /// If you must deserialize untrusted data with `pickle`, consider signing the -/// data with a secret key and verifying the signature before deserializing -/// (such as with `hmac`). This will prevent an attacker from modifying the -/// serialized data to inject arbitrary objects. +/// data with a secret key and verifying the signature before deserializing the +/// payload, This will prevent an attacker from injecting arbitrary objects +/// into the serialized data. /// /// ## Example /// ```python @@ -67,9 +67,9 @@ impl Violation for SuspiciousPickleUsage { /// formats, such as JSON. /// /// If you must deserialize untrusted data with `marshal`, consider signing the -/// data with a secret key and verifying the signature before deserializing -/// (such as with `hmac`). This will prevent an attacker from modifying the -/// serialized data to inject arbitrary objects. +/// data with a secret key and verifying the signature before deserializing the +/// payload, This will prevent an attacker from injecting arbitrary objects +/// into the serialized data. /// /// ## Example /// ```python @@ -111,7 +111,7 @@ impl Violation for SuspiciousMarshalUsage { /// that rely on these hash functions. /// /// Avoid using weak or broken cryptographic hash functions in security -/// contexts. Instead, use a known secure hash function such as SHA256. +/// contexts. Instead, use a known secure hash function such as SHA-256. /// /// ## Example /// ```python @@ -239,7 +239,6 @@ impl Violation for SuspiciousInsecureCipherModeUsage { /// instead, either directly or via a context manager such as /// `tempfile.TemporaryFile`. /// -/// /// ## Example /// ```python /// import tempfile @@ -434,13 +433,12 @@ impl Violation for SuspiciousNonCryptographicRandomUsage { /// Checks for uses of insecure XML parsers. /// /// ## Why is this bad? -/// Many XML parsers are vulnerable to XML attacks, such as entity expansion -/// which cause excessive memory and CPU usage by exploiting recursion. In some -/// situations, it may be possible for an attacker to access unauthorized -/// resources. +/// Many XML parsers are vulnerable to XML attacks (such as entity expansion), +/// which cause excessive memory and CPU usage by exploiting recursion. An +/// attacker could use such methods to access unauthorized resources. /// -/// Consider using the `defusedxml` packaging when parsing untrusted XML data, -/// which protects against XML attacks. +/// Consider using the `defusedxml` package when parsing untrusted XML data, +/// to protect against XML attacks. /// /// ## Example /// ```python @@ -475,13 +473,12 @@ impl Violation for SuspiciousXMLCElementTreeUsage { /// Checks for uses of insecure XML parsers. /// /// ## Why is this bad? -/// Many XML parsers are vulnerable to XML attacks, such as entity expansion -/// which cause excessive memory and CPU usage by exploiting recursion. In some -/// situations, it may be possible for an attacker to access unauthorized -/// resources. +/// Many XML parsers are vulnerable to XML attacks (such as entity expansion), +/// which cause excessive memory and CPU usage by exploiting recursion. An +/// attacker could use such methods to access unauthorized resources. /// -/// Consider using the `defusedxml` packaging when parsing untrusted XML data, -/// which protects against XML attacks. +/// Consider using the `defusedxml` package when parsing untrusted XML data, +/// to protect against XML attacks. /// /// ## Example /// ```python @@ -516,13 +513,12 @@ impl Violation for SuspiciousXMLElementTreeUsage { /// Checks for uses of insecure XML parsers. /// /// ## Why is this bad? -/// Many XML parsers are vulnerable to XML attacks, such as entity expansion -/// which cause excessive memory and CPU usage by exploiting recursion. In some -/// situations, it may be possible for an attacker to access unauthorized -/// resources. +/// Many XML parsers are vulnerable to XML attacks (such as entity expansion), +/// which cause excessive memory and CPU usage by exploiting recursion. An +/// attacker could use such methods to access unauthorized resources. /// -/// Consider using the `defusedxml` packaging when parsing untrusted XML data, -/// which protects against XML attacks. +/// Consider using the `defusedxml` package when parsing untrusted XML data, +/// to protect against XML attacks. /// /// ## Example /// ```python @@ -557,13 +553,12 @@ impl Violation for SuspiciousXMLExpatReaderUsage { /// Checks for uses of insecure XML parsers. /// /// ## Why is this bad? -/// Many XML parsers are vulnerable to XML attacks, such as entity expansion -/// which cause excessive memory and CPU usage by exploiting recursion. In some -/// situations, it may be possible for an attacker to access unauthorized -/// resources. +/// Many XML parsers are vulnerable to XML attacks (such as entity expansion), +/// which cause excessive memory and CPU usage by exploiting recursion. An +/// attacker could use such methods to access unauthorized resources. /// -/// Consider using the `defusedxml` packaging when parsing untrusted XML data, -/// which protects against XML attacks. +/// Consider using the `defusedxml` package when parsing untrusted XML data, +/// to protect against XML attacks. /// /// ## Example /// ```python @@ -598,13 +593,12 @@ impl Violation for SuspiciousXMLExpatBuilderUsage { /// Checks for uses of insecure XML parsers. /// /// ## Why is this bad? -/// Many XML parsers are vulnerable to XML attacks, such as entity expansion -/// which cause excessive memory and CPU usage by exploiting recursion. In some -/// situations, it may be possible for an attacker to access unauthorized -/// resources. +/// Many XML parsers are vulnerable to XML attacks (such as entity expansion), +/// which cause excessive memory and CPU usage by exploiting recursion. An +/// attacker could use such methods to access unauthorized resources. /// -/// Consider using the `defusedxml` packaging when parsing untrusted XML data, -/// which protects against XML attacks. +/// Consider using the `defusedxml` package when parsing untrusted XML data, +/// to protect against XML attacks. /// /// ## Example /// ```python @@ -639,13 +633,12 @@ impl Violation for SuspiciousXMLSaxUsage { /// Checks for uses of insecure XML parsers. /// /// ## Why is this bad? -/// Many XML parsers are vulnerable to XML attacks, such as entity expansion -/// which cause excessive memory and CPU usage by exploiting recursion. In some -/// situations, it may be possible for an attacker to access unauthorized -/// resources. +/// Many XML parsers are vulnerable to XML attacks (such as entity expansion), +/// which cause excessive memory and CPU usage by exploiting recursion. An +/// attacker could use such methods to access unauthorized resources. /// -/// Consider using the `defusedxml` packaging when parsing untrusted XML data, -/// which protects against XML attacks. +/// Consider using the `defusedxml` package when parsing untrusted XML data, +/// to protect against XML attacks. /// /// ## Example /// ```python @@ -680,13 +673,12 @@ impl Violation for SuspiciousXMLMiniDOMUsage { /// Checks for uses of insecure XML parsers. /// /// ## Why is this bad? -/// Many XML parsers are vulnerable to XML attacks, such as entity expansion -/// which cause excessive memory and CPU usage by exploiting recursion. In some -/// situations, it may be possible for an attacker to access unauthorized -/// resources. +/// Many XML parsers are vulnerable to XML attacks (such as entity expansion), +/// which cause excessive memory and CPU usage by exploiting recursion. An +/// attacker could use such methods to access unauthorized resources. /// -/// Consider using the `defusedxml` packaging when parsing untrusted XML data, -/// which protects against XML attacks. +/// Consider using the `defusedxml` package when parsing untrusted XML data, +/// to protect against XML attacks. /// /// ## Example /// ```python @@ -721,10 +713,9 @@ impl Violation for SuspiciousXMLPullDOMUsage { /// Checks for uses of insecure XML parsers. /// /// ## Why is this bad? -/// Many XML parsers are vulnerable to XML attacks, such as entity expansion -/// which cause excessive memory and CPU usage by exploiting recursion. In some -/// situations, it may be possible for an attacker to access unauthorized -/// resources. +/// Many XML parsers are vulnerable to XML attacks (such as entity expansion), +/// which cause excessive memory and CPU usage by exploiting recursion. An +/// attacker could use such methods to access unauthorized resources. /// /// ## Example /// ```python @@ -751,11 +742,11 @@ impl Violation for SuspiciousXMLETreeUsage { /// Checks for uses of `ssl._create_unverified_context`. /// /// ## Why is this bad? -/// [PEP 476](https://peps.python.org/pep-0476/) enabled certificate and -/// hostname validation by default in Python standard library HTTP clients. -/// Previously, Python did not validate certificates by default, which could -/// allow an attacker to perform a "man in the middle" attack where they -/// intercept and modify the traffic between the client and server. +/// [PEP 476] enabled certificate and hostname validation by default in Python +/// standard library HTTP clients. Previously, Python did not validate +/// certificates by default, which could allow an attacker to perform a "man in +/// the middle" attack by intercepting and modifying traffic between client and +/// server. /// /// To support legacy environments, `ssl._create_unverified_context` reverts to /// the previous behavior that does perform verification. Otherwise, use @@ -778,6 +769,8 @@ impl Violation for SuspiciousXMLETreeUsage { /// ## References /// - [PEP 476 – Enabling certificate verification by default for stdlib http clients: Opting out](https://peps.python.org/pep-0476/#opting-out) /// - [Python documentation: `ssl` — TLS/SSL wrapper for socket objects](https://docs.python.org/3/library/ssl.html) +/// +/// [PEP 476]: https://peps.python.org/pep-0476/ #[violation] pub struct SuspiciousUnverifiedContextUsage; @@ -813,8 +806,8 @@ impl Violation for SuspiciousTelnetUsage { /// Checks for the use of FTP-related functions. /// /// ## Why is this bad? -/// FTP is considered insecure because it does not encrypt data sent over the -/// connection and is vulnerable to numerous attacks. +/// FTP is considered insecure as it does not encrypt data sent over the +/// connection and is thus vulnerable to numerous attacks. /// /// Instead, consider using FTPS (which secures FTP using SSL/TLS) or SFTP. /// From 7566ca8ff7caa28ea44f1053fd1f2963fafcefcb Mon Sep 17 00:00:00 2001 From: qdegraaf <34540841+qdegraaf@users.noreply.github.com> Date: Wed, 12 Jul 2023 05:46:53 +0200 Subject: [PATCH 425/447] Refactor `repeated_keys()` to use `ComparableExpr` (#5696) ## Summary Replaces `DictionaryKey` enum with the more general `ComparableExpr` when checking for duplicate keys ## Test Plan Added test fixture from issue. Can potentially be expanded further depending on what exactly we want to flag (e.g. do we also want to check for unhashable types?) and which `ComparableExpr::XYZ` types we consider literals. ## Issue link Closes: https://github.com/astral-sh/ruff/issues/5691 --- .../resources/test/fixtures/pyflakes/F601.py | 5 + .../src/rules/pyflakes/rules/repeated_keys.rs | 149 +++++++----------- ..._rules__pyflakes__tests__F601_F601.py.snap | 41 +++++ ..._rules__pyflakes__tests__F602_F602.py.snap | 12 ++ 4 files changed, 114 insertions(+), 93 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F601.py b/crates/ruff/resources/test/fixtures/pyflakes/F601.py index 3a42484852..e8e9e6a89f 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F601.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F601.py @@ -48,3 +48,8 @@ x = { x = {"a": 1, "a": 1} x = {"a": 1, "b": 2, "a": 1} + +x = { + ('a', 'b'): 'asdf', + ('a', 'b'): 'qwer', +} diff --git a/crates/ruff/src/rules/pyflakes/rules/repeated_keys.rs b/crates/ruff/src/rules/pyflakes/rules/repeated_keys.rs index 13c4ceaf9a..ec4412042c 100644 --- a/crates/ruff/src/rules/pyflakes/rules/repeated_keys.rs +++ b/crates/ruff/src/rules/pyflakes/rules/repeated_keys.rs @@ -1,11 +1,11 @@ -use std::hash::{BuildHasherDefault, Hash}; +use std::hash::BuildHasherDefault; use rustc_hash::{FxHashMap, FxHashSet}; -use rustpython_parser::ast::{self, Expr, Ranged}; +use rustpython_parser::ast::{Expr, Ranged}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::comparable::{ComparableConstant, ComparableExpr}; +use ruff_python_ast::comparable::ComparableExpr; use crate::checkers::ast::Checker; use crate::registry::{AsRule, Rule}; @@ -43,7 +43,6 @@ use crate::registry::{AsRule, Rule}; #[violation] pub struct MultiValueRepeatedKeyLiteral { name: String, - repeated_value: bool, } impl Violation for MultiValueRepeatedKeyLiteral { @@ -51,20 +50,13 @@ impl Violation for MultiValueRepeatedKeyLiteral { #[derive_message_formats] fn message(&self) -> String { - let MultiValueRepeatedKeyLiteral { name, .. } = self; + let MultiValueRepeatedKeyLiteral { name } = self; format!("Dictionary key literal `{name}` repeated") } fn autofix_title(&self) -> Option { - let MultiValueRepeatedKeyLiteral { - repeated_value, - name, - } = self; - if *repeated_value { - Some(format!("Remove repeated key literal `{name}`")) - } else { - None - } + let MultiValueRepeatedKeyLiteral { name } = self; + Some(format!("Remove repeated key literal `{name}`")) } } @@ -100,7 +92,6 @@ impl Violation for MultiValueRepeatedKeyLiteral { #[violation] pub struct MultiValueRepeatedKeyVariable { name: String, - repeated_value: bool, } impl Violation for MultiValueRepeatedKeyVariable { @@ -108,43 +99,20 @@ impl Violation for MultiValueRepeatedKeyVariable { #[derive_message_formats] fn message(&self) -> String { - let MultiValueRepeatedKeyVariable { name, .. } = self; + let MultiValueRepeatedKeyVariable { name } = self; format!("Dictionary key `{name}` repeated") } fn autofix_title(&self) -> Option { - let MultiValueRepeatedKeyVariable { - repeated_value, - name, - } = self; - if *repeated_value { - Some(format!("Remove repeated key `{name}`")) - } else { - None - } - } -} - -#[derive(Debug, Eq, PartialEq, Hash)] -enum DictionaryKey<'a> { - Constant(ComparableConstant<'a>), - Variable(&'a str), -} - -fn into_dictionary_key(expr: &Expr) -> Option { - match expr { - Expr::Constant(ast::ExprConstant { value, .. }) => { - Some(DictionaryKey::Constant(value.into())) - } - Expr::Name(ast::ExprName { id, .. }) => Some(DictionaryKey::Variable(id)), - _ => None, + let MultiValueRepeatedKeyVariable { name } = self; + Some(format!("Remove repeated key `{name}`")) } } /// F601, F602 pub(crate) fn repeated_keys(checker: &mut Checker, keys: &[Option], values: &[Expr]) { // Generate a map from key to (index, value). - let mut seen: FxHashMap> = + let mut seen: FxHashMap> = FxHashMap::with_capacity_and_hasher(keys.len(), BuildHasherDefault::default()); // Detect duplicate keys. @@ -152,61 +120,56 @@ pub(crate) fn repeated_keys(checker: &mut Checker, keys: &[Option], values let Some(key) = key else { continue; }; - if let Some(dict_key) = into_dictionary_key(key) { - if let Some(seen_values) = seen.get_mut(&dict_key) { - match dict_key { - DictionaryKey::Constant(..) => { - if checker.enabled(Rule::MultiValueRepeatedKeyLiteral) { - let comparable_value: ComparableExpr = (&values[i]).into(); - let is_duplicate_value = seen_values.contains(&comparable_value); - let mut diagnostic = Diagnostic::new( - MultiValueRepeatedKeyLiteral { - name: checker.generator().expr(key), - repeated_value: is_duplicate_value, - }, - key.range(), - ); - if is_duplicate_value { - if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Fix::suggested(Edit::deletion( - values[i - 1].end(), - values[i].end(), - ))); - } - } else { - seen_values.insert(comparable_value); - } - checker.diagnostics.push(diagnostic); - } - } - DictionaryKey::Variable(dict_key) => { - if checker.enabled(Rule::MultiValueRepeatedKeyVariable) { - let comparable_value: ComparableExpr = (&values[i]).into(); - let is_duplicate_value = seen_values.contains(&comparable_value); - let mut diagnostic = Diagnostic::new( - MultiValueRepeatedKeyVariable { - name: dict_key.to_string(), - repeated_value: is_duplicate_value, - }, - key.range(), - ); - if is_duplicate_value { - if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Fix::suggested(Edit::deletion( - values[i - 1].end(), - values[i].end(), - ))); - } - } else { - seen_values.insert(comparable_value); - } - checker.diagnostics.push(diagnostic); + + let comparable_key = ComparableExpr::from(key); + let comparable_value = ComparableExpr::from(&values[i]); + + let Some(seen_values) = seen.get_mut(&comparable_key) else { + seen.insert(comparable_key, FxHashSet::from_iter([comparable_value])); + continue; + }; + + match key { + Expr::Constant(_) | Expr::Tuple(_) | Expr::JoinedStr(_) => { + if checker.enabled(Rule::MultiValueRepeatedKeyLiteral) { + let mut diagnostic = Diagnostic::new( + MultiValueRepeatedKeyLiteral { + name: checker.locator.slice(key.range()).to_string(), + }, + key.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + if !seen_values.insert(comparable_value) { + diagnostic.set_fix(Fix::suggested(Edit::deletion( + values[i - 1].end(), + values[i].end(), + ))); } } + checker.diagnostics.push(diagnostic); } - } else { - seen.insert(dict_key, FxHashSet::from_iter([(&values[i]).into()])); } + Expr::Name(_) => { + if checker.enabled(Rule::MultiValueRepeatedKeyVariable) { + let mut diagnostic = Diagnostic::new( + MultiValueRepeatedKeyVariable { + name: checker.locator.slice(key.range()).to_string(), + }, + key.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + let comparable_value: ComparableExpr = (&values[i]).into(); + if !seen_values.insert(comparable_value) { + diagnostic.set_fix(Fix::suggested(Edit::deletion( + values[i - 1].end(), + values[i].end(), + ))); + } + } + checker.diagnostics.push(diagnostic); + } + } + _ => {} } } } diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F601_F601.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F601_F601.py.snap index f6fa5fdd65..1a5b94a092 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F601_F601.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F601_F601.py.snap @@ -10,6 +10,18 @@ F601.py:3:5: F601 Dictionary key literal `"a"` repeated 4 | "b": 3, 5 | ("a", "b"): 3, | + = help: Remove repeated key literal `"a"` + +F601.py:6:5: F601 Dictionary key literal `("a", "b")` repeated + | +4 | "b": 3, +5 | ("a", "b"): 3, +6 | ("a", "b"): 4, + | ^^^^^^^^^^ F601 +7 | 1.0: 2, +8 | 1: 0, + | + = help: Remove repeated key literal `("a", "b")` F601.py:9:5: F601 Dictionary key literal `1` repeated | @@ -20,6 +32,7 @@ F601.py:9:5: F601 Dictionary key literal `1` repeated 10 | b"123": 1, 11 | b"123": 4, | + = help: Remove repeated key literal `1` F601.py:11:5: F601 Dictionary key literal `b"123"` repeated | @@ -29,6 +42,7 @@ F601.py:11:5: F601 Dictionary key literal `b"123"` repeated | ^^^^^^ F601 12 | } | + = help: Remove repeated key literal `b"123"` F601.py:16:5: F601 Dictionary key literal `"a"` repeated | @@ -39,6 +53,7 @@ F601.py:16:5: F601 Dictionary key literal `"a"` repeated 17 | "a": 3, 18 | "a": 3, | + = help: Remove repeated key literal `"a"` F601.py:17:5: F601 Dictionary key literal `"a"` repeated | @@ -49,6 +64,7 @@ F601.py:17:5: F601 Dictionary key literal `"a"` repeated 18 | "a": 3, 19 | } | + = help: Remove repeated key literal `"a"` F601.py:18:5: F601 [*] Dictionary key literal `"a"` repeated | @@ -78,6 +94,7 @@ F601.py:23:5: F601 Dictionary key literal `"a"` repeated 24 | "a": 3, 25 | "a": 3, | + = help: Remove repeated key literal `"a"` F601.py:24:5: F601 Dictionary key literal `"a"` repeated | @@ -88,6 +105,7 @@ F601.py:24:5: F601 Dictionary key literal `"a"` repeated 25 | "a": 3, 26 | "a": 4, | + = help: Remove repeated key literal `"a"` F601.py:25:5: F601 [*] Dictionary key literal `"a"` repeated | @@ -117,6 +135,7 @@ F601.py:26:5: F601 Dictionary key literal `"a"` repeated | ^^^ F601 27 | } | + = help: Remove repeated key literal `"a"` F601.py:31:5: F601 [*] Dictionary key literal `"a"` repeated | @@ -147,6 +166,7 @@ F601.py:32:5: F601 Dictionary key literal `"a"` repeated 33 | "a": 3, 34 | "a": 4, | + = help: Remove repeated key literal `"a"` F601.py:33:5: F601 Dictionary key literal `"a"` repeated | @@ -157,6 +177,7 @@ F601.py:33:5: F601 Dictionary key literal `"a"` repeated 34 | "a": 4, 35 | } | + = help: Remove repeated key literal `"a"` F601.py:34:5: F601 Dictionary key literal `"a"` repeated | @@ -166,6 +187,7 @@ F601.py:34:5: F601 Dictionary key literal `"a"` repeated | ^^^ F601 35 | } | + = help: Remove repeated key literal `"a"` F601.py:41:5: F601 Dictionary key literal `"a"` repeated | @@ -176,6 +198,7 @@ F601.py:41:5: F601 Dictionary key literal `"a"` repeated 42 | a: 2, 43 | "a": 3, | + = help: Remove repeated key literal `"a"` F601.py:43:5: F601 Dictionary key literal `"a"` repeated | @@ -186,6 +209,7 @@ F601.py:43:5: F601 Dictionary key literal `"a"` repeated 44 | a: 3, 45 | "a": 3, | + = help: Remove repeated key literal `"a"` F601.py:45:5: F601 [*] Dictionary key literal `"a"` repeated | @@ -224,12 +248,16 @@ F601.py:49:14: F601 [*] Dictionary key literal `"a"` repeated 49 |-x = {"a": 1, "a": 1} 49 |+x = {"a": 1} 50 50 | x = {"a": 1, "b": 2, "a": 1} +51 51 | +52 52 | x = { F601.py:50:22: F601 [*] Dictionary key literal `"a"` repeated | 49 | x = {"a": 1, "a": 1} 50 | x = {"a": 1, "b": 2, "a": 1} | ^^^ F601 +51 | +52 | x = { | = help: Remove repeated key literal `"a"` @@ -239,5 +267,18 @@ F601.py:50:22: F601 [*] Dictionary key literal `"a"` repeated 49 49 | x = {"a": 1, "a": 1} 50 |-x = {"a": 1, "b": 2, "a": 1} 50 |+x = {"a": 1, "b": 2} +51 51 | +52 52 | x = { +53 53 | ('a', 'b'): 'asdf', + +F601.py:54:5: F601 Dictionary key literal `('a', 'b')` repeated + | +52 | x = { +53 | ('a', 'b'): 'asdf', +54 | ('a', 'b'): 'qwer', + | ^^^^^^^^^^ F601 +55 | } + | + = help: Remove repeated key literal `('a', 'b')` diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F602_F602.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F602_F602.py.snap index cdbc8c3290..c29e85b114 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F602_F602.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F602_F602.py.snap @@ -10,6 +10,7 @@ F602.py:5:5: F602 Dictionary key `a` repeated 6 | b: 3, 7 | } | + = help: Remove repeated key `a` F602.py:11:5: F602 Dictionary key `a` repeated | @@ -20,6 +21,7 @@ F602.py:11:5: F602 Dictionary key `a` repeated 12 | a: 3, 13 | a: 3, | + = help: Remove repeated key `a` F602.py:12:5: F602 Dictionary key `a` repeated | @@ -30,6 +32,7 @@ F602.py:12:5: F602 Dictionary key `a` repeated 13 | a: 3, 14 | } | + = help: Remove repeated key `a` F602.py:13:5: F602 [*] Dictionary key `a` repeated | @@ -59,6 +62,7 @@ F602.py:18:5: F602 Dictionary key `a` repeated 19 | a: 3, 20 | a: 3, | + = help: Remove repeated key `a` F602.py:19:5: F602 Dictionary key `a` repeated | @@ -69,6 +73,7 @@ F602.py:19:5: F602 Dictionary key `a` repeated 20 | a: 3, 21 | a: 4, | + = help: Remove repeated key `a` F602.py:20:5: F602 [*] Dictionary key `a` repeated | @@ -98,6 +103,7 @@ F602.py:21:5: F602 Dictionary key `a` repeated | ^ F602 22 | } | + = help: Remove repeated key `a` F602.py:26:5: F602 [*] Dictionary key `a` repeated | @@ -128,6 +134,7 @@ F602.py:27:5: F602 Dictionary key `a` repeated 28 | a: 3, 29 | a: 4, | + = help: Remove repeated key `a` F602.py:28:5: F602 Dictionary key `a` repeated | @@ -138,6 +145,7 @@ F602.py:28:5: F602 Dictionary key `a` repeated 29 | a: 4, 30 | } | + = help: Remove repeated key `a` F602.py:29:5: F602 Dictionary key `a` repeated | @@ -147,6 +155,7 @@ F602.py:29:5: F602 Dictionary key `a` repeated | ^ F602 30 | } | + = help: Remove repeated key `a` F602.py:35:5: F602 [*] Dictionary key `a` repeated | @@ -177,6 +186,7 @@ F602.py:37:5: F602 Dictionary key `a` repeated 38 | "a": 3, 39 | a: 3, | + = help: Remove repeated key `a` F602.py:39:5: F602 Dictionary key `a` repeated | @@ -187,6 +197,7 @@ F602.py:39:5: F602 Dictionary key `a` repeated 40 | "a": 3, 41 | a: 4, | + = help: Remove repeated key `a` F602.py:41:5: F602 Dictionary key `a` repeated | @@ -196,6 +207,7 @@ F602.py:41:5: F602 Dictionary key `a` repeated | ^ F602 42 | } | + = help: Remove repeated key `a` F602.py:44:12: F602 [*] Dictionary key `a` repeated | From 0666added9902990b6ea22d1a48f6fa1ae5ea605 Mon Sep 17 00:00:00 2001 From: Zanie Date: Wed, 12 Jul 2023 00:23:06 -0500 Subject: [PATCH 426/447] Add RUF016: Detection of invalid index types (#5602) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Detects invalid types for tuple, list, bytes, string indices. For example, the following will raise a `TypeError` at runtime and when imported Python will display a `SyntaxWarning` ```python var = [1, 2, 3]["x"] ``` ``` example.py:1: SyntaxWarning: list indices must be integers or slices, not str; perhaps you missed a comma? var = [1, 2, 3]["x"] Traceback (most recent call last): File "example.py", line 1, in var = [1, 2, 3]["x"] ~~~~~~~~~^^^^^ TypeError: list indices must be integers or slices, not str ``` Previously, Ruff would not report the invalid syntax but now a violation will be reported. This does not apply to cases where a variable, call, or complex expression is used in the index — detection is roughly limited to static definitions, which matches Python's warnings. ``` ❯ ./target/debug/ruff example.py --select RUF015 --show-source --no-cache example.py:1:17: RUF015 Indexed access to type `list` uses type `str` instead of an integer or slice. | 1 | var = [1, 2, 3]["x"] | ^^^ RUF015 | ``` Closes https://github.com/astral-sh/ruff/issues/5082 xref https://github.com/python/cpython/commit/ffff1440d118cae189a6c2baf595dda52cdc7c3a --- .../resources/test/fixtures/ruff/RUF016.py | 115 ++++++ crates/ruff/src/checkers/ast/mod.rs | 6 +- crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/ruff/mod.rs | 1 + .../rules/ruff/rules/invalid_index_type.rs | 214 ++++++++++ crates/ruff/src/rules/ruff/rules/mod.rs | 2 + ...y_iterable_allocation_for_first_element.rs | 11 +- ..._rules__ruff__tests__RUF016_RUF016.py.snap | 379 ++++++++++++++++++ ruff.schema.json | 1 + 9 files changed, 722 insertions(+), 8 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/ruff/RUF016.py create mode 100644 crates/ruff/src/rules/ruff/rules/invalid_index_type.rs create mode 100644 crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF016_RUF016.py.snap diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF016.py b/crates/ruff/resources/test/fixtures/ruff/RUF016.py new file mode 100644 index 0000000000..545ad2ec53 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/ruff/RUF016.py @@ -0,0 +1,115 @@ +# Should not emit for valid access with index +var = "abc"[0] +var = f"abc"[0] +var = [1, 2, 3][0] +var = (1, 2, 3)[0] +var = b"abc"[0] + +# Should not emit for valid access with slice +var = "abc"[0:2] +var = f"abc"[0:2] +var = b"abc"[0:2] +var = [1, 2, 3][0:2] +var = (1, 2, 3)[0:2] +var = [1, 2, 3][None:2] +var = [1, 2, 3][0:None] +var = [1, 2, 3][:2] +var = [1, 2, 3][0:] + +# Should emit for invalid access on strings +var = "abc"["x"] +var = f"abc"["x"] + +# Should emit for invalid access on bytes +var = b"abc"["x"] + +# Should emit for invalid access on lists and tuples +var = [1, 2, 3]["x"] +var = (1, 2, 3)["x"] + +# Should emit for invalid access on list comprehensions +var = [x for x in range(10)]["x"] + +# Should emit for invalid access using tuple +var = "abc"[1, 2] + +# Should emit for invalid access using string +var = [1, 2]["x"] + +# Should emit for invalid access using float +var = [1, 2][0.25] + +# Should emit for invalid access using dict +var = [1, 2][{"x": "y"}] + +# Should emit for invalid access using dict comp +var = [1, 2][{x: "y" for x in range(2)}] + +# Should emit for invalid access using list +var = [1, 2][2, 3] + +# Should emit for invalid access using list comp +var = [1, 2][[x for x in range(2)]] + +# Should emit on invalid access using set +var = [1, 2][{"x", "y"}] + +# Should emit on invalid access using set comp +var = [1, 2][{x for x in range(2)}] + +# Should emit on invalid access using bytes +var = [1, 2][b"x"] + +# Should emit for non-integer slice start +var = [1, 2, 3]["x":2] +var = [1, 2, 3][f"x":2] +var = [1, 2, 3][1.2:2] +var = [1, 2, 3][{"x"}:2] +var = [1, 2, 3][{x for x in range(2)}:2] +var = [1, 2, 3][{"x": x for x in range(2)}:2] +var = [1, 2, 3][[x for x in range(2)]:2] + +# Should emit for non-integer slice end +var = [1, 2, 3][0:"x"] +var = [1, 2, 3][0:f"x"] +var = [1, 2, 3][0:1.2] +var = [1, 2, 3][0:{"x"}] +var = [1, 2, 3][0:{x for x in range(2)}] +var = [1, 2, 3][0:{"x": x for x in range(2)}] +var = [1, 2, 3][0:[x for x in range(2)]] + +# Should emit for non-integer slice step +var = [1, 2, 3][0:1:"x"] +var = [1, 2, 3][0:1:f"x"] +var = [1, 2, 3][0:1:1.2] +var = [1, 2, 3][0:1:{"x"}] +var = [1, 2, 3][0:1:{x for x in range(2)}] +var = [1, 2, 3][0:1:{"x": x for x in range(2)}] +var = [1, 2, 3][0:1:[x for x in range(2)]] + +# Should emit for non-integer slice start and end; should emit twice with specific ranges +var = [1, 2, 3]["x":"y"] + +# Should emit once for repeated invalid access +var = [1, 2, 3]["x"]["y"]["z"] + +# Cannot emit on invalid access using variable in index +x = "x" +var = "abc"[x] + +# Cannot emit on invalid access using call +def func(): + return 1 +var = "abc"[func()] + +# Cannot emit on invalid access using a variable in parent +x = [1, 2, 3] +var = x["y"] + +# Cannot emit for invalid access on byte array +var = bytearray(b"abc")["x"] + +# Cannot emit for slice bound using variable +x = "x" +var = [1, 2, 3][0:x] +var = [1, 2, 3][x:1] diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 11f8d3600f..aff756000b 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -2142,7 +2142,7 @@ where // Pre-visit. match expr { - subscript @ Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { + Expr::Subscript(subscript @ ast::ExprSubscript { value, slice, .. }) => { // Ex) Optional[...], Union[...] if self.any_enabled(&[ Rule::FutureRewritableTypeAnnotation, @@ -2235,6 +2235,10 @@ where ruff::rules::unnecessary_iterable_allocation_for_first_element(self, subscript); } + if self.enabled(Rule::InvalidIndexType) { + ruff::rules::invalid_index_type(self, subscript); + } + pandas_vet::rules::subscript(self, value, expr); } Expr::Tuple(ast::ExprTuple { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 03dd26fb82..704deb1437 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -781,6 +781,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { #[cfg(feature = "unreachable-code")] (Ruff, "014") => (RuleGroup::Nursery, rules::ruff::rules::UnreachableCode), (Ruff, "015") => (RuleGroup::Unspecified, rules::ruff::rules::UnnecessaryIterableAllocationForFirstElement), + (Ruff, "016") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidIndexType), (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 1b11784cab..0c9aded910 100644 --- a/crates/ruff/src/rules/ruff/mod.rs +++ b/crates/ruff/src/rules/ruff/mod.rs @@ -34,6 +34,7 @@ mod tests { Rule::UnnecessaryIterableAllocationForFirstElement, Path::new("RUF015.py") )] + #[test_case(Rule::InvalidIndexType, Path::new("RUF016.py"))] #[cfg_attr( feature = "unreachable-code", test_case(Rule::UnreachableCode, Path::new("RUF014.py")) diff --git a/crates/ruff/src/rules/ruff/rules/invalid_index_type.rs b/crates/ruff/src/rules/ruff/rules/invalid_index_type.rs new file mode 100644 index 0000000000..399a77dbab --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/invalid_index_type.rs @@ -0,0 +1,214 @@ +use rustpython_parser::ast::{Constant, Expr, ExprConstant, ExprSlice, ExprSubscript, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use std::fmt; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for indexed access to lists, strings, tuples, bytes, and comprehensions +/// using a type other than an integer or slice. +/// +/// ## Why is this bad? +/// Only integers or slices can be used as indices to these types. Using +/// other types will result in a `TypeError` at runtime and a `SyntaxWarning` at +/// import time. +/// +/// ## Example +/// ```python +/// var = [1, 2, 3]["x"] +/// ``` +/// +/// Use instead: +/// ```python +/// var = [1, 2, 3][0] +/// ``` +#[violation] +pub struct InvalidIndexType { + value_type: String, + index_type: String, + is_slice: bool, +} + +impl Violation for InvalidIndexType { + #[derive_message_formats] + fn message(&self) -> String { + let InvalidIndexType { + value_type, + index_type, + is_slice, + } = self; + if *is_slice { + format!("Slice in indexed access to type `{value_type}` uses type `{index_type}` instead of an integer.") + } else { + format!( + "Indexed access to type `{value_type}` uses type `{index_type}` instead of an integer or slice." + ) + } + } +} + +/// RUF015 +pub(crate) fn invalid_index_type(checker: &mut Checker, expr: &ExprSubscript) { + let ExprSubscript { + value, + slice: index, + .. + } = expr; + + // Check the value being indexed is a list, tuple, string, f-string, bytes, or comprehension + if !matches!( + value.as_ref(), + Expr::List(_) + | Expr::ListComp(_) + | Expr::Tuple(_) + | Expr::JoinedStr(_) + | Expr::Constant(ExprConstant { + value: Constant::Str(_) | Constant::Bytes(_), + .. + }) + ) { + return; + } + + // The value types supported by this rule should always be checkable + let Some(value_type) = CheckableExprType::try_from(value) else { + debug_assert!(false, "Index value must be a checkable type to generate a violation message."); + return; + }; + + // If the index is not a checkable type then we can't easily determine if there is a violation + let Some(index_type) = CheckableExprType::try_from(index) else { + return; + }; + + // Then check the contents of the index + match index.as_ref() { + Expr::Constant(ExprConstant { + value: index_value, .. + }) => { + // If the index is a constant, require an integer + if !index_value.is_int() { + checker.diagnostics.push(Diagnostic::new( + InvalidIndexType { + value_type: value_type.to_string(), + index_type: constant_type_name(index_value).to_string(), + is_slice: false, + }, + index.range(), + )); + } + } + Expr::Slice(ExprSlice { + lower, upper, step, .. + }) => { + // If the index is a slice, require integer or null bounds + for is_slice in [lower, upper, step].into_iter().flatten() { + if let Expr::Constant(ExprConstant { + value: index_value, .. + }) = is_slice.as_ref() + { + if !(index_value.is_int() || index_value.is_none()) { + checker.diagnostics.push(Diagnostic::new( + InvalidIndexType { + value_type: value_type.to_string(), + index_type: constant_type_name(index_value).to_string(), + is_slice: true, + }, + is_slice.range(), + )); + } + } else if let Some(is_slice_type) = CheckableExprType::try_from(is_slice.as_ref()) { + checker.diagnostics.push(Diagnostic::new( + InvalidIndexType { + value_type: value_type.to_string(), + index_type: is_slice_type.to_string(), + is_slice: true, + }, + is_slice.range(), + )); + } + } + } + _ => { + // If it's some other checkable data type, it's a violation + checker.diagnostics.push(Diagnostic::new( + InvalidIndexType { + value_type: value_type.to_string(), + index_type: index_type.to_string(), + is_slice: false, + }, + index.range(), + )); + } + } +} + +/// An expression that can be checked for type compatibility. +/// +/// These are generally "literal" type expressions in that we know their concrete type +/// without additional analysis; opposed to expressions like a function call where we +/// cannot determine what type it may return. +#[derive(Debug)] +enum CheckableExprType<'a> { + Constant(&'a Constant), + JoinedStr, + List, + ListComp, + SetComp, + DictComp, + Set, + Dict, + Tuple, + Slice, +} + +impl fmt::Display for CheckableExprType<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Constant(constant) => f.write_str(constant_type_name(constant)), + Self::JoinedStr => f.write_str("str"), + Self::List => f.write_str("list"), + Self::SetComp => f.write_str("set comprehension"), + Self::ListComp => f.write_str("list comprehension"), + Self::DictComp => f.write_str("dict comprehension"), + Self::Set => f.write_str("set"), + Self::Slice => f.write_str("slice"), + Self::Dict => f.write_str("dict"), + Self::Tuple => f.write_str("tuple"), + } + } +} + +impl<'a> CheckableExprType<'a> { + fn try_from(expr: &'a Expr) -> Option { + match expr { + Expr::Constant(ExprConstant { value, .. }) => Some(Self::Constant(value)), + Expr::JoinedStr(_) => Some(Self::JoinedStr), + Expr::List(_) => Some(Self::List), + Expr::ListComp(_) => Some(Self::ListComp), + Expr::SetComp(_) => Some(Self::SetComp), + Expr::DictComp(_) => Some(Self::DictComp), + Expr::Set(_) => Some(Self::Set), + Expr::Dict(_) => Some(Self::Dict), + Expr::Tuple(_) => Some(Self::Tuple), + Expr::Slice(_) => Some(Self::Slice), + _ => None, + } + } +} + +fn constant_type_name(constant: &Constant) -> &'static str { + match constant { + Constant::None => "None", + Constant::Bool(_) => "bool", + Constant::Str(_) => "str", + Constant::Bytes(_) => "bytes", + Constant::Int(_) => "int", + Constant::Tuple(_) => "tuple", + Constant::Float(_) => "float", + Constant::Complex { .. } => "complex", + Constant::Ellipsis => "ellipsis", + } +} diff --git a/crates/ruff/src/rules/ruff/rules/mod.rs b/crates/ruff/src/rules/ruff/rules/mod.rs index e6654f1204..e4d39feefd 100644 --- a/crates/ruff/src/rules/ruff/rules/mod.rs +++ b/crates/ruff/src/rules/ruff/rules/mod.rs @@ -4,6 +4,7 @@ pub(crate) use collection_literal_concatenation::*; pub(crate) use explicit_f_string_type_conversion::*; pub(crate) use function_call_in_dataclass_default::*; pub(crate) use implicit_optional::*; +pub(crate) use invalid_index_type::*; pub(crate) use invalid_pyproject_toml::*; pub(crate) use mutable_class_default::*; pub(crate) use mutable_dataclass_default::*; @@ -22,6 +23,7 @@ mod explicit_f_string_type_conversion; mod function_call_in_dataclass_default; mod helpers; mod implicit_optional; +mod invalid_index_type; mod invalid_pyproject_toml; mod mutable_class_default; mod mutable_dataclass_default; diff --git a/crates/ruff/src/rules/ruff/rules/unnecessary_iterable_allocation_for_first_element.rs b/crates/ruff/src/rules/ruff/rules/unnecessary_iterable_allocation_for_first_element.rs index 62f2cca624..17fa0922e5 100644 --- a/crates/ruff/src/rules/ruff/rules/unnecessary_iterable_allocation_for_first_element.rs +++ b/crates/ruff/src/rules/ruff/rules/unnecessary_iterable_allocation_for_first_element.rs @@ -1,6 +1,6 @@ use num_bigint::BigInt; use num_traits::{One, Zero}; -use rustpython_parser::ast::{self, Comprehension, Constant, Expr}; +use rustpython_parser::ast::{self, Comprehension, Constant, Expr, ExprSubscript}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -78,17 +78,14 @@ impl AlwaysAutofixableViolation for UnnecessaryIterableAllocationForFirstElement /// RUF015 pub(crate) fn unnecessary_iterable_allocation_for_first_element( checker: &mut Checker, - subscript: &Expr, + subscript: &ExprSubscript, ) { - let Expr::Subscript(ast::ExprSubscript { + let ast::ExprSubscript { value, slice, range, .. - }) = subscript - else { - return; - }; + } = subscript; let Some(subscript_kind) = classify_subscript(slice) else { return; diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF016_RUF016.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF016_RUF016.py.snap new file mode 100644 index 0000000000..bf56f0d36c --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF016_RUF016.py.snap @@ -0,0 +1,379 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF016.py:20:13: RUF016 Indexed access to type `str` uses type `str` instead of an integer or slice. + | +19 | # Should emit for invalid access on strings +20 | var = "abc"["x"] + | ^^^ RUF016 +21 | var = f"abc"["x"] + | + +RUF016.py:21:14: RUF016 Indexed access to type `str` uses type `str` instead of an integer or slice. + | +19 | # Should emit for invalid access on strings +20 | var = "abc"["x"] +21 | var = f"abc"["x"] + | ^^^ RUF016 +22 | +23 | # Should emit for invalid access on bytes + | + +RUF016.py:24:14: RUF016 Indexed access to type `bytes` uses type `str` instead of an integer or slice. + | +23 | # Should emit for invalid access on bytes +24 | var = b"abc"["x"] + | ^^^ RUF016 +25 | +26 | # Should emit for invalid access on lists and tuples + | + +RUF016.py:27:17: RUF016 Indexed access to type `list` uses type `str` instead of an integer or slice. + | +26 | # Should emit for invalid access on lists and tuples +27 | var = [1, 2, 3]["x"] + | ^^^ RUF016 +28 | var = (1, 2, 3)["x"] + | + +RUF016.py:28:17: RUF016 Indexed access to type `tuple` uses type `str` instead of an integer or slice. + | +26 | # Should emit for invalid access on lists and tuples +27 | var = [1, 2, 3]["x"] +28 | var = (1, 2, 3)["x"] + | ^^^ RUF016 +29 | +30 | # Should emit for invalid access on list comprehensions + | + +RUF016.py:31:30: RUF016 Indexed access to type `list comprehension` uses type `str` instead of an integer or slice. + | +30 | # Should emit for invalid access on list comprehensions +31 | var = [x for x in range(10)]["x"] + | ^^^ RUF016 +32 | +33 | # Should emit for invalid access using tuple + | + +RUF016.py:34:13: RUF016 Indexed access to type `str` uses type `tuple` instead of an integer or slice. + | +33 | # Should emit for invalid access using tuple +34 | var = "abc"[1, 2] + | ^^^^ RUF016 +35 | +36 | # Should emit for invalid access using string + | + +RUF016.py:37:14: RUF016 Indexed access to type `list` uses type `str` instead of an integer or slice. + | +36 | # Should emit for invalid access using string +37 | var = [1, 2]["x"] + | ^^^ RUF016 +38 | +39 | # Should emit for invalid access using float + | + +RUF016.py:40:14: RUF016 Indexed access to type `list` uses type `float` instead of an integer or slice. + | +39 | # Should emit for invalid access using float +40 | var = [1, 2][0.25] + | ^^^^ RUF016 +41 | +42 | # Should emit for invalid access using dict + | + +RUF016.py:43:14: RUF016 Indexed access to type `list` uses type `dict` instead of an integer or slice. + | +42 | # Should emit for invalid access using dict +43 | var = [1, 2][{"x": "y"}] + | ^^^^^^^^^^ RUF016 +44 | +45 | # Should emit for invalid access using dict comp + | + +RUF016.py:46:14: RUF016 Indexed access to type `list` uses type `dict comprehension` instead of an integer or slice. + | +45 | # Should emit for invalid access using dict comp +46 | var = [1, 2][{x: "y" for x in range(2)}] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF016 +47 | +48 | # Should emit for invalid access using list + | + +RUF016.py:49:14: RUF016 Indexed access to type `list` uses type `tuple` instead of an integer or slice. + | +48 | # Should emit for invalid access using list +49 | var = [1, 2][2, 3] + | ^^^^ RUF016 +50 | +51 | # Should emit for invalid access using list comp + | + +RUF016.py:52:14: RUF016 Indexed access to type `list` uses type `list comprehension` instead of an integer or slice. + | +51 | # Should emit for invalid access using list comp +52 | var = [1, 2][[x for x in range(2)]] + | ^^^^^^^^^^^^^^^^^^^^^ RUF016 +53 | +54 | # Should emit on invalid access using set + | + +RUF016.py:55:14: RUF016 Indexed access to type `list` uses type `set` instead of an integer or slice. + | +54 | # Should emit on invalid access using set +55 | var = [1, 2][{"x", "y"}] + | ^^^^^^^^^^ RUF016 +56 | +57 | # Should emit on invalid access using set comp + | + +RUF016.py:58:14: RUF016 Indexed access to type `list` uses type `set comprehension` instead of an integer or slice. + | +57 | # Should emit on invalid access using set comp +58 | var = [1, 2][{x for x in range(2)}] + | ^^^^^^^^^^^^^^^^^^^^^ RUF016 +59 | +60 | # Should emit on invalid access using bytes + | + +RUF016.py:61:14: RUF016 Indexed access to type `list` uses type `bytes` instead of an integer or slice. + | +60 | # Should emit on invalid access using bytes +61 | var = [1, 2][b"x"] + | ^^^^ RUF016 +62 | +63 | # Should emit for non-integer slice start + | + +RUF016.py:64:17: RUF016 Slice in indexed access to type `list` uses type `str` instead of an integer. + | +63 | # Should emit for non-integer slice start +64 | var = [1, 2, 3]["x":2] + | ^^^ RUF016 +65 | var = [1, 2, 3][f"x":2] +66 | var = [1, 2, 3][1.2:2] + | + +RUF016.py:65:17: RUF016 Slice in indexed access to type `list` uses type `str` instead of an integer. + | +63 | # Should emit for non-integer slice start +64 | var = [1, 2, 3]["x":2] +65 | var = [1, 2, 3][f"x":2] + | ^^^^ RUF016 +66 | var = [1, 2, 3][1.2:2] +67 | var = [1, 2, 3][{"x"}:2] + | + +RUF016.py:66:17: RUF016 Slice in indexed access to type `list` uses type `float` instead of an integer. + | +64 | var = [1, 2, 3]["x":2] +65 | var = [1, 2, 3][f"x":2] +66 | var = [1, 2, 3][1.2:2] + | ^^^ RUF016 +67 | var = [1, 2, 3][{"x"}:2] +68 | var = [1, 2, 3][{x for x in range(2)}:2] + | + +RUF016.py:67:17: RUF016 Slice in indexed access to type `list` uses type `set` instead of an integer. + | +65 | var = [1, 2, 3][f"x":2] +66 | var = [1, 2, 3][1.2:2] +67 | var = [1, 2, 3][{"x"}:2] + | ^^^^^ RUF016 +68 | var = [1, 2, 3][{x for x in range(2)}:2] +69 | var = [1, 2, 3][{"x": x for x in range(2)}:2] + | + +RUF016.py:68:17: RUF016 Slice in indexed access to type `list` uses type `set comprehension` instead of an integer. + | +66 | var = [1, 2, 3][1.2:2] +67 | var = [1, 2, 3][{"x"}:2] +68 | var = [1, 2, 3][{x for x in range(2)}:2] + | ^^^^^^^^^^^^^^^^^^^^^ RUF016 +69 | var = [1, 2, 3][{"x": x for x in range(2)}:2] +70 | var = [1, 2, 3][[x for x in range(2)]:2] + | + +RUF016.py:69:17: RUF016 Slice in indexed access to type `list` uses type `dict comprehension` instead of an integer. + | +67 | var = [1, 2, 3][{"x"}:2] +68 | var = [1, 2, 3][{x for x in range(2)}:2] +69 | var = [1, 2, 3][{"x": x for x in range(2)}:2] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF016 +70 | var = [1, 2, 3][[x for x in range(2)]:2] + | + +RUF016.py:70:17: RUF016 Slice in indexed access to type `list` uses type `list comprehension` instead of an integer. + | +68 | var = [1, 2, 3][{x for x in range(2)}:2] +69 | var = [1, 2, 3][{"x": x for x in range(2)}:2] +70 | var = [1, 2, 3][[x for x in range(2)]:2] + | ^^^^^^^^^^^^^^^^^^^^^ RUF016 +71 | +72 | # Should emit for non-integer slice end + | + +RUF016.py:73:19: RUF016 Slice in indexed access to type `list` uses type `str` instead of an integer. + | +72 | # Should emit for non-integer slice end +73 | var = [1, 2, 3][0:"x"] + | ^^^ RUF016 +74 | var = [1, 2, 3][0:f"x"] +75 | var = [1, 2, 3][0:1.2] + | + +RUF016.py:74:19: RUF016 Slice in indexed access to type `list` uses type `str` instead of an integer. + | +72 | # Should emit for non-integer slice end +73 | var = [1, 2, 3][0:"x"] +74 | var = [1, 2, 3][0:f"x"] + | ^^^^ RUF016 +75 | var = [1, 2, 3][0:1.2] +76 | var = [1, 2, 3][0:{"x"}] + | + +RUF016.py:75:19: RUF016 Slice in indexed access to type `list` uses type `float` instead of an integer. + | +73 | var = [1, 2, 3][0:"x"] +74 | var = [1, 2, 3][0:f"x"] +75 | var = [1, 2, 3][0:1.2] + | ^^^ RUF016 +76 | var = [1, 2, 3][0:{"x"}] +77 | var = [1, 2, 3][0:{x for x in range(2)}] + | + +RUF016.py:76:19: RUF016 Slice in indexed access to type `list` uses type `set` instead of an integer. + | +74 | var = [1, 2, 3][0:f"x"] +75 | var = [1, 2, 3][0:1.2] +76 | var = [1, 2, 3][0:{"x"}] + | ^^^^^ RUF016 +77 | var = [1, 2, 3][0:{x for x in range(2)}] +78 | var = [1, 2, 3][0:{"x": x for x in range(2)}] + | + +RUF016.py:77:19: RUF016 Slice in indexed access to type `list` uses type `set comprehension` instead of an integer. + | +75 | var = [1, 2, 3][0:1.2] +76 | var = [1, 2, 3][0:{"x"}] +77 | var = [1, 2, 3][0:{x for x in range(2)}] + | ^^^^^^^^^^^^^^^^^^^^^ RUF016 +78 | var = [1, 2, 3][0:{"x": x for x in range(2)}] +79 | var = [1, 2, 3][0:[x for x in range(2)]] + | + +RUF016.py:78:19: RUF016 Slice in indexed access to type `list` uses type `dict comprehension` instead of an integer. + | +76 | var = [1, 2, 3][0:{"x"}] +77 | var = [1, 2, 3][0:{x for x in range(2)}] +78 | var = [1, 2, 3][0:{"x": x for x in range(2)}] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF016 +79 | var = [1, 2, 3][0:[x for x in range(2)]] + | + +RUF016.py:79:19: RUF016 Slice in indexed access to type `list` uses type `list comprehension` instead of an integer. + | +77 | var = [1, 2, 3][0:{x for x in range(2)}] +78 | var = [1, 2, 3][0:{"x": x for x in range(2)}] +79 | var = [1, 2, 3][0:[x for x in range(2)]] + | ^^^^^^^^^^^^^^^^^^^^^ RUF016 +80 | +81 | # Should emit for non-integer slice step + | + +RUF016.py:82:21: RUF016 Slice in indexed access to type `list` uses type `str` instead of an integer. + | +81 | # Should emit for non-integer slice step +82 | var = [1, 2, 3][0:1:"x"] + | ^^^ RUF016 +83 | var = [1, 2, 3][0:1:f"x"] +84 | var = [1, 2, 3][0:1:1.2] + | + +RUF016.py:83:21: RUF016 Slice in indexed access to type `list` uses type `str` instead of an integer. + | +81 | # Should emit for non-integer slice step +82 | var = [1, 2, 3][0:1:"x"] +83 | var = [1, 2, 3][0:1:f"x"] + | ^^^^ RUF016 +84 | var = [1, 2, 3][0:1:1.2] +85 | var = [1, 2, 3][0:1:{"x"}] + | + +RUF016.py:84:21: RUF016 Slice in indexed access to type `list` uses type `float` instead of an integer. + | +82 | var = [1, 2, 3][0:1:"x"] +83 | var = [1, 2, 3][0:1:f"x"] +84 | var = [1, 2, 3][0:1:1.2] + | ^^^ RUF016 +85 | var = [1, 2, 3][0:1:{"x"}] +86 | var = [1, 2, 3][0:1:{x for x in range(2)}] + | + +RUF016.py:85:21: RUF016 Slice in indexed access to type `list` uses type `set` instead of an integer. + | +83 | var = [1, 2, 3][0:1:f"x"] +84 | var = [1, 2, 3][0:1:1.2] +85 | var = [1, 2, 3][0:1:{"x"}] + | ^^^^^ RUF016 +86 | var = [1, 2, 3][0:1:{x for x in range(2)}] +87 | var = [1, 2, 3][0:1:{"x": x for x in range(2)}] + | + +RUF016.py:86:21: RUF016 Slice in indexed access to type `list` uses type `set comprehension` instead of an integer. + | +84 | var = [1, 2, 3][0:1:1.2] +85 | var = [1, 2, 3][0:1:{"x"}] +86 | var = [1, 2, 3][0:1:{x for x in range(2)}] + | ^^^^^^^^^^^^^^^^^^^^^ RUF016 +87 | var = [1, 2, 3][0:1:{"x": x for x in range(2)}] +88 | var = [1, 2, 3][0:1:[x for x in range(2)]] + | + +RUF016.py:87:21: RUF016 Slice in indexed access to type `list` uses type `dict comprehension` instead of an integer. + | +85 | var = [1, 2, 3][0:1:{"x"}] +86 | var = [1, 2, 3][0:1:{x for x in range(2)}] +87 | var = [1, 2, 3][0:1:{"x": x for x in range(2)}] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF016 +88 | var = [1, 2, 3][0:1:[x for x in range(2)]] + | + +RUF016.py:88:21: RUF016 Slice in indexed access to type `list` uses type `list comprehension` instead of an integer. + | +86 | var = [1, 2, 3][0:1:{x for x in range(2)}] +87 | var = [1, 2, 3][0:1:{"x": x for x in range(2)}] +88 | var = [1, 2, 3][0:1:[x for x in range(2)]] + | ^^^^^^^^^^^^^^^^^^^^^ RUF016 +89 | +90 | # Should emit for non-integer slice start and end; should emit twice with specific ranges + | + +RUF016.py:91:17: RUF016 Slice in indexed access to type `list` uses type `str` instead of an integer. + | +90 | # Should emit for non-integer slice start and end; should emit twice with specific ranges +91 | var = [1, 2, 3]["x":"y"] + | ^^^ RUF016 +92 | +93 | # Should emit once for repeated invalid access + | + +RUF016.py:91:21: RUF016 Slice in indexed access to type `list` uses type `str` instead of an integer. + | +90 | # Should emit for non-integer slice start and end; should emit twice with specific ranges +91 | var = [1, 2, 3]["x":"y"] + | ^^^ RUF016 +92 | +93 | # Should emit once for repeated invalid access + | + +RUF016.py:94:17: RUF016 Indexed access to type `list` uses type `str` instead of an integer or slice. + | +93 | # Should emit once for repeated invalid access +94 | var = [1, 2, 3]["x"]["y"]["z"] + | ^^^ RUF016 +95 | +96 | # Cannot emit on invalid access using variable in index + | + + diff --git a/ruff.schema.json b/ruff.schema.json index ae611fd379..2fc94daff7 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2405,6 +2405,7 @@ "RUF013", "RUF014", "RUF015", + "RUF016", "RUF1", "RUF10", "RUF100", From 33a91773f7a8c60c2f1cdb84127feabad8c6d84f Mon Sep 17 00:00:00 2001 From: Zanie Date: Wed, 12 Jul 2023 01:26:37 -0500 Subject: [PATCH 427/447] Use permalinks in ecosystem diff references (#5704) Closes https://github.com/astral-sh/ruff/issues/5702 --- scripts/check_ecosystem.py | 52 +++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/scripts/check_ecosystem.py b/scripts/check_ecosystem.py index 59ebb9a056..ed7c876811 100755 --- a/scripts/check_ecosystem.py +++ b/scripts/check_ecosystem.py @@ -45,11 +45,11 @@ class Repository(NamedTuple): """Shallow clone this repository to a temporary directory.""" if checkout_dir.exists(): logger.debug(f"Reusing {self.org}:{self.repo}") - yield Path(checkout_dir) + yield await self._get_commit(checkout_dir) return logger.debug(f"Cloning {self.org}:{self.repo}") - git_command = [ + git_clone_command = [ "git", "clone", "--config", @@ -60,39 +60,50 @@ class Repository(NamedTuple): "--no-tags", ] if self.ref: - git_command.extend(["--branch", self.ref]) + git_clone_command.extend(["--branch", self.ref]) - git_command.extend( + git_clone_command.extend( [ f"https://github.com/{self.org}/{self.repo}", checkout_dir, ], ) - process = await create_subprocess_exec( - *git_command, + git_clone_process = await create_subprocess_exec( + *git_clone_command, env={"GIT_TERMINAL_PROMPT": "0"}, ) - status_code = await process.wait() + status_code = await git_clone_process.wait() logger.debug( f"Finished cloning {self.org}/{self.repo} with status {status_code}", ) + yield await self._get_commit(checkout_dir) - yield Path(checkout_dir) - - def url_for(self: Self, path: str, lnum: int | None = None) -> str: - """Return the GitHub URL for the given path and line number, if given.""" + def url_for(self: Self, commit_sha: str, path: str, lnum: int | None = None) -> str: + """ + Return the GitHub URL for the given commit, path, and line number, if given. + """ # Default to main branch - url = ( - f"https://github.com/{self.org}/{self.repo}" - f"/blob/{self.ref or 'main'}/{path}" - ) + url = f"https://github.com/{self.org}/{self.repo}/blob/{commit_sha}/{path}" if lnum: url += f"#L{lnum}" return url + async def _get_commit(self: Self, checkout_dir: Path) -> str: + """Return the commit sha for the repository in the checkout directory.""" + git_sha_process = await create_subprocess_exec( + *["git", "rev-parse", "HEAD"], + cwd=checkout_dir, + stdout=PIPE, + ) + git_sha_stdout, _ = await git_sha_process.communicate() + assert ( + await git_sha_process.wait() == 0 + ), f"Failed to retrieve commit sha at {checkout_dir}" + return git_sha_stdout.decode().strip() + REPOSITORIES: list[Repository] = [ Repository("apache", "airflow", "main", select="ALL"), @@ -169,6 +180,7 @@ class Diff(NamedTuple): removed: set[str] added: set[str] + source_sha: str def __bool__(self: Self) -> bool: """Return true if this diff is non-empty.""" @@ -203,13 +215,13 @@ async def compare( assert ":" not in repo.org assert ":" not in repo.repo checkout_dir = Path(checkout_parent).joinpath(f"{repo.org}:{repo.repo}") - async with repo.clone(checkout_dir) as path: + async with repo.clone(checkout_dir) as checkout_sha: try: async with asyncio.TaskGroup() as tg: check1 = tg.create_task( check( ruff=ruff1, - path=path, + path=checkout_dir, name=f"{repo.org}/{repo.repo}", select=repo.select, ignore=repo.ignore, @@ -220,7 +232,7 @@ async def compare( check2 = tg.create_task( check( ruff=ruff2, - path=path, + path=checkout_dir, name=f"{repo.org}/{repo.repo}", select=repo.select, ignore=repo.ignore, @@ -237,7 +249,7 @@ async def compare( elif line.startswith("+ "): added.add(line[2:]) - return Diff(removed, added) + return Diff(removed, added, checkout_sha) def read_projects_jsonl(projects_jsonl: Path) -> dict[tuple[str, str], Repository]: @@ -379,7 +391,7 @@ async def main( continue pre, inner, path, lnum, post = match.groups() - url = repo.url_for(path, int(lnum)) + url = repo.url_for(diff.source_sha, path, int(lnum)) print(f"{pre} {inner} {post}") print("
") From 5665968b4249784734fda04fdd2a8e9272b8cee9 Mon Sep 17 00:00:00 2001 From: Zanie Date: Wed, 12 Jul 2023 08:56:22 -0500 Subject: [PATCH 428/447] Bump static Python versions in CI from 3.7 to 3.11 (#5700) Python 3.7 is EOL and we should use the latest stable version for builds. Related to https://github.com/astral-sh/ruff-lsp/pull/189 --------- Co-authored-by: konsti --- .github/workflows/ci.yaml | 6 +++--- .github/workflows/flake8-to-ruff.yaml | 2 +- .github/workflows/release.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index af305c0d67..48f532299a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ env: CARGO_TERM_COLOR: always RUSTUP_MAX_RETRIES: 10 PACKAGE_NAME: ruff - PYTHON_VERSION: "3.7" # to build abi3 wheels + PYTHON_VERSION: "3.11" # to build abi3 wheels jobs: cargo-fmt: @@ -142,7 +142,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: ${{ env.PYTHON_VERSION }} - uses: actions/download-artifact@v3 name: Download Ruff binary @@ -226,7 +226,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: ${{ env.PYTHON_VERSION }} - name: "Install Rust toolchain" run: rustup show - uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/flake8-to-ruff.yaml b/.github/workflows/flake8-to-ruff.yaml index 68582b1472..44e41b7d29 100644 --- a/.github/workflows/flake8-to-ruff.yaml +++ b/.github/workflows/flake8-to-ruff.yaml @@ -9,7 +9,7 @@ concurrency: env: PACKAGE_NAME: flake8-to-ruff CRATE_NAME: flake8_to_ruff - PYTHON_VERSION: "3.7" # to build abi3 wheels + PYTHON_VERSION: "3.11" CARGO_INCREMENTAL: 0 CARGO_NET_RETRY: 10 CARGO_TERM_COLOR: always diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index dfd6fbbd47..3dd0b1cf20 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -20,7 +20,7 @@ concurrency: env: PACKAGE_NAME: ruff - PYTHON_VERSION: "3.7" # to build abi3 wheels + PYTHON_VERSION: "3.11" CARGO_INCREMENTAL: 0 CARGO_NET_RETRY: 10 CARGO_TERM_COLOR: always From f0aa6bd4d36d3b8793bea405541e91d525c0e9fb Mon Sep 17 00:00:00 2001 From: konsti Date: Wed, 12 Jul 2023 16:18:22 +0200 Subject: [PATCH 429/447] Document ruff_dev and format_dev (#5648) ## Summary Document all `ruff_dev` subcommands and document the `format_dev` flags in the formatter readme. CC @zanieb please flag everything that isn't clear or missing ## Test Plan n/a --- CONTRIBUTING.md | 131 ++++++++++++++++++++ crates/ruff_dev/src/generate_all.rs | 2 +- crates/ruff_dev/src/generate_options.rs | 4 +- crates/ruff_dev/src/generate_rules_table.rs | 2 + crates/ruff_python_formatter/README.md | 8 ++ 5 files changed, 145 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8c51d5f6b3..08aee28a3a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -550,3 +550,134 @@ cargo instruments -t time --bench linter --profile release-debug -p ruff_benchma - You may want to pass an additional filter to run a single test file Otherwise, follow the instructions from the linux section. + +## `cargo dev` + +`cargo dev` is a shortcut for `cargo run --package ruff_dev --bin ruff_dev`. You can run some useful +utils with it: + +- `cargo dev print-ast `: Print the AST of a python file using the + [RustPython parser](https://github.com/astral-sh/RustPython-Parser/tree/main/parser) that is + mainly used in Ruff. For `if True: pass # comment`, you can see the syntax tree, the byte offsets + for start and stop of each node and also how the `:` token, the comment and whitespace are not + represented anymore: + +```text +[ + If( + StmtIf { + range: 0..13, + test: Constant( + ExprConstant { + range: 3..7, + value: Bool( + true, + ), + kind: None, + }, + ), + body: [ + Pass( + StmtPass { + range: 9..13, + }, + ), + ], + orelse: [], + }, + ), +] +``` + +- `cargo dev print-tokens `: Print the tokens that the AST is built upon. Again for + `if True: pass # comment`: + +```text +0 If 2 +3 True 7 +7 Colon 8 +9 Pass 13 +14 Comment( + "# comment", +) 23 +23 Newline 24 +``` + +- `cargo dev print-cst `: Print the CST of a python file using + [LibCST](https://github.com/Instagram/LibCST), which is used in addition to the RustPython parser + in Ruff. E.g. for `if True: pass # comment` everything including the whitespace is represented: + +```text +Module { + body: [ + Compound( + If( + If { + test: Name( + Name { + value: "True", + lpar: [], + rpar: [], + }, + ), + body: SimpleStatementSuite( + SimpleStatementSuite { + body: [ + Pass( + Pass { + semicolon: None, + }, + ), + ], + leading_whitespace: SimpleWhitespace( + " ", + ), + trailing_whitespace: TrailingWhitespace { + whitespace: SimpleWhitespace( + " ", + ), + comment: Some( + Comment( + "# comment", + ), + ), + newline: Newline( + None, + Real, + ), + }, + }, + ), + orelse: None, + leading_lines: [], + whitespace_before_test: SimpleWhitespace( + " ", + ), + whitespace_after_test: SimpleWhitespace( + "", + ), + is_elif: false, + }, + ), + ), + ], + header: [], + footer: [], + default_indent: " ", + default_newline: "\n", + has_trailing_newline: true, + encoding: "utf-8", +} +``` + +- `cargo dev generate-all`: Update `ruff.schema.json`, `docs/configuration.md` and `docs/rules`. + You can also set `RUFF_UPDATE_SCHEMA=1` to update `ruff.schema.json` during `cargo test`. +- `cargo dev generate-cli-help`, `cargo dev generate-docs` and `cargo dev generate-json-schema`: + Update just `docs/configuration.md`, `docs/rules` and `ruff.schema.json` respectively. +- `cargo dev generate-options`: Generate a markdown-compatible table of all `pyproject.toml` + options. Used for +- `cargo dev generate-rules-table`: Generate a markdown-compatible table of all rules. Used for +- `cargo dev round-trip `: Read a Python file or Jupyter Notebook, + parse it, serialize the parsed representation and write it back. Used to check how good our + representation is so that fixes don't rewrite irrelevant parts of a file. +- `cargo dev format_dev`: See ruff_python_formatter README.md diff --git a/crates/ruff_dev/src/generate_all.rs b/crates/ruff_dev/src/generate_all.rs index 3eb1b0adfa..6b8212ce64 100644 --- a/crates/ruff_dev/src/generate_all.rs +++ b/crates/ruff_dev/src/generate_all.rs @@ -21,7 +21,7 @@ pub(crate) enum Mode { /// Don't write to the file, check if the file is up-to-date and error if not. Check, - /// Write the generated help to stdout (rather than to `docs/configuration.md`). + /// Write the generated help to stdout. DryRun, } diff --git a/crates/ruff_dev/src/generate_options.rs b/crates/ruff_dev/src/generate_options.rs index 7737f2097f..c2e58bb836 100644 --- a/crates/ruff_dev/src/generate_options.rs +++ b/crates/ruff_dev/src/generate_options.rs @@ -1,4 +1,6 @@ -//! Generate a Markdown-compatible listing of configuration options. +//! Generate a Markdown-compatible listing of configuration options for `pyproject.toml`. +//! +//! Used for . use itertools::Itertools; use ruff::settings::options::Options; diff --git a/crates/ruff_dev/src/generate_rules_table.rs b/crates/ruff_dev/src/generate_rules_table.rs index a92b56cdd1..5a77acee2d 100644 --- a/crates/ruff_dev/src/generate_rules_table.rs +++ b/crates/ruff_dev/src/generate_rules_table.rs @@ -1,4 +1,6 @@ //! Generate a Markdown-compatible table of supported lint rules. +//! +//! Used for . use itertools::Itertools; use strum::IntoEnumIterator; diff --git a/crates/ruff_python_formatter/README.md b/crates/ruff_python_formatter/README.md index 9c2d918e19..f4d63100e4 100644 --- a/crates/ruff_python_formatter/README.md +++ b/crates/ruff_python_formatter/README.md @@ -276,6 +276,14 @@ python scripts/check_ecosystem.py --checkouts target/checkouts --projects github cargo run --bin ruff_dev -- format-dev --stability-check --multi-project target/checkouts ``` +Compared to `ruff check`, `cargo run --bin ruff_dev -- format-dev` has 4 additional options: + +- `--write`: Format the files and write them back to disk +- `--stability-check`: Format twice (but don't write to disk) and check for differences and crashes +- `--multi-project`: Treat every subdirectory as a separate project. Useful for ecosystem checks. +- `--error-file`: Use together with `--multi-project`, this writes all errors (but not status + messages) to a file. + ## The orphan rules and trait structure For the formatter, we would like to implement `Format` from the rust_formatter crate for all AST From 653429bef9d212f0d4dc22ca8f7d9eae1d432f81 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 12 Jul 2023 18:21:28 +0200 Subject: [PATCH 430/447] Handle right parens in join comma builder (#5711) --- crates/ruff_python_formatter/src/builders.rs | 67 +++++++---- .../src/expression/expr_call.rs | 57 +++++++-- .../src/expression/expr_dict.rs | 2 +- .../src/expression/expr_list.rs | 8 +- .../src/expression/expr_tuple.rs | 20 ++-- .../src/statement/stmt_class_def.rs | 3 +- .../src/statement/stmt_delete.rs | 8 +- .../src/statement/stmt_import_from.rs | 4 +- .../src/statement/stmt_with.rs | 7 +- ...aneous__long_strings_flag_disabled.py.snap | 110 +++++------------- ...black_compatibility@py_38__pep_572.py.snap | 9 +- 11 files changed, 159 insertions(+), 136 deletions(-) diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 368c651c6d..5ff1569170 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -1,9 +1,12 @@ +use ruff_text_size::{TextRange, TextSize}; +use rustpython_parser::ast::Ranged; + +use ruff_formatter::{format_args, write, Argument, Arguments}; + use crate::context::NodeLevel; use crate::prelude::*; -use crate::trivia::{first_non_trivia_token, lines_after, skip_trailing_trivia, Token, TokenKind}; -use ruff_formatter::{format_args, write, Argument, Arguments}; -use ruff_text_size::TextSize; -use rustpython_parser::ast::Ranged; +use crate::trivia::{lines_after, skip_trailing_trivia, SimpleTokenizer, Token, TokenKind}; +use crate::MagicTrailingComma; /// Adds parentheses and indents `content` if it doesn't fit on a line. pub(crate) fn parenthesize_if_expands<'ast, T>(content: &T) -> ParenthesizeIfExpands<'_, 'ast> @@ -53,7 +56,10 @@ pub(crate) trait PyFormatterExtensions<'ast, 'buf> { /// A builder that separates each element by a `,` and a [`soft_line_break_or_space`]. /// It emits a trailing `,` that is only shown if the enclosing group expands. It forces the enclosing /// group to expand if the last item has a trailing `comma` and the magical comma option is enabled. - fn join_comma_separated<'fmt>(&'fmt mut self) -> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf>; + fn join_comma_separated<'fmt>( + &'fmt mut self, + sequence_end: TextSize, + ) -> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf>; } impl<'buf, 'ast> PyFormatterExtensions<'ast, 'buf> for PyFormatter<'ast, 'buf> { @@ -61,8 +67,11 @@ impl<'buf, 'ast> PyFormatterExtensions<'ast, 'buf> for PyFormatter<'ast, 'buf> { JoinNodesBuilder::new(self, level) } - fn join_comma_separated<'fmt>(&'fmt mut self) -> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { - JoinCommaSeparatedBuilder::new(self) + fn join_comma_separated<'fmt>( + &'fmt mut self, + sequence_end: TextSize, + ) -> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { + JoinCommaSeparatedBuilder::new(self, sequence_end) } } @@ -194,18 +203,20 @@ pub(crate) struct JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { result: FormatResult<()>, fmt: &'fmt mut PyFormatter<'ast, 'buf>, end_of_last_entry: Option, + sequence_end: TextSize, /// 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> { - fn new(f: &'fmt mut PyFormatter<'ast, 'buf>) -> Self { + fn new(f: &'fmt mut PyFormatter<'ast, 'buf>, sequence_end: TextSize) -> Self { Self { fmt: f, result: Ok(()), end_of_last_entry: None, len: 0, + sequence_end, } } @@ -236,7 +247,7 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { where T: Ranged, F: Format>, - I: Iterator, + I: IntoIterator, { for (node, content) in entries { self.entry(&node, &content); @@ -248,7 +259,7 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { pub(crate) fn nodes<'a, T, I>(&mut self, entries: I) -> &mut Self where T: Ranged + AsFormat> + 'a, - I: Iterator, + I: IntoIterator, { for node in entries { self.entry(node, &node.format()); @@ -260,14 +271,26 @@ 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.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().source()), - Some(Token { - kind: TokenKind::Comma, - .. - }) - ); + let magic_trailing_comma = match self.fmt.options().magic_trailing_comma() { + MagicTrailingComma::Respect => { + let first_token = SimpleTokenizer::new( + self.fmt.context().source(), + TextRange::new(last_end, self.sequence_end), + ) + .skip_trivia() + // Skip over any closing parentheses belonging to the expression + .find(|token| token.kind() != TokenKind::RParen); + + matches!( + first_token, + Some(Token { + kind: TokenKind::Comma, + .. + }) + ) + } + MagicTrailingComma::Ignore => false, + }; // 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. @@ -287,13 +310,15 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { #[cfg(test)] mod tests { + use rustpython_parser::ast::ModModule; + use rustpython_parser::Parse; + + use ruff_formatter::format; + use crate::comments::Comments; use crate::context::{NodeLevel, PyFormatContext}; use crate::prelude::*; use crate::PyFormatOptions; - use ruff_formatter::format; - use rustpython_parser::ast::ModModule; - use rustpython_parser::Parse; fn format_ranged(level: NodeLevel) -> String { let source = r#" diff --git a/crates/ruff_python_formatter/src/expression/expr_call.rs b/crates/ruff_python_formatter/src/expression/expr_call.rs index 070027fc21..b1dd972df2 100644 --- a/crates/ruff_python_formatter/src/expression/expr_call.rs +++ b/crates/ruff_python_formatter/src/expression/expr_call.rs @@ -5,10 +5,12 @@ use crate::expression::parentheses::{ default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, Parenthesize, }; +use crate::trivia::{SimpleTokenizer, TokenKind}; use crate::{AsFormat, FormatNodeRule, PyFormatter}; use ruff_formatter::prelude::{format_with, group, text}; use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::ExprCall; +use ruff_text_size::{TextRange, TextSize}; +use rustpython_parser::ast::{Expr, ExprCall, Ranged}; #[derive(Default)] pub struct FormatExprCall; @@ -43,15 +45,25 @@ impl FormatNodeRule for FormatExprCall { ); } - let all_args = format_with(|f| { - f.join_comma_separated() - .entries( - // We have the parentheses from the call so the arguments never need any - args.iter() - .map(|arg| (arg, arg.format().with_options(Parenthesize::Never))), - ) - .nodes(keywords.iter()) - .finish() + let all_args = format_with(|f: &mut PyFormatter| { + let source = f.context().source(); + let mut joiner = f.join_comma_separated(item.end()); + match args.as_slice() { + [argument] if keywords.is_empty() => { + let parentheses = + if is_single_argument_parenthesized(argument, item.end(), source) { + Parenthesize::Always + } else { + Parenthesize::Never + }; + joiner.entry(argument, &argument.format().with_options(parentheses)); + } + arguments => { + joiner.nodes(arguments).nodes(keywords.iter()); + } + } + + joiner.finish() }); write!( @@ -97,3 +109,28 @@ impl NeedsParentheses for ExprCall { } } } + +fn is_single_argument_parenthesized(argument: &Expr, call_end: TextSize, source: &str) -> bool { + let mut has_seen_r_paren = false; + + for token in + SimpleTokenizer::new(source, TextRange::new(argument.end(), call_end)).skip_trivia() + { + match token.kind() { + TokenKind::RParen => { + if has_seen_r_paren { + return true; + } + has_seen_r_paren = true; + } + // Skip over any trailing comma + TokenKind::Comma => continue, + _ => { + // Passed the arguments + break; + } + } + } + + false +} diff --git a/crates/ruff_python_formatter/src/expression/expr_dict.rs b/crates/ruff_python_formatter/src/expression/expr_dict.rs index 1f9fcca568..5a00386779 100644 --- a/crates/ruff_python_formatter/src/expression/expr_dict.rs +++ b/crates/ruff_python_formatter/src/expression/expr_dict.rs @@ -77,7 +77,7 @@ impl FormatNodeRule for FormatExprDict { } let format_pairs = format_with(|f| { - let mut joiner = f.join_comma_separated(); + let mut joiner = f.join_comma_separated(item.end()); for (key, value) in keys.iter().zip(values) { let key_value_pair = KeyValuePair { key, value }; diff --git a/crates/ruff_python_formatter/src/expression/expr_list.rs b/crates/ruff_python_formatter/src/expression/expr_list.rs index c090182762..b35abdaa43 100644 --- a/crates/ruff_python_formatter/src/expression/expr_list.rs +++ b/crates/ruff_python_formatter/src/expression/expr_list.rs @@ -6,7 +6,7 @@ use crate::expression::parentheses::{ use crate::prelude::*; use crate::FormatNodeRule; use ruff_formatter::{format_args, write}; -use rustpython_parser::ast::ExprList; +use rustpython_parser::ast::{ExprList, Ranged}; #[derive(Default)] pub struct FormatExprList; @@ -53,7 +53,11 @@ impl FormatNodeRule for FormatExprList { "A non-empty expression list has dangling comments" ); - let items = format_with(|f| f.join_comma_separated().nodes(elts.iter()).finish()); + let items = format_with(|f| { + f.join_comma_separated(item.end()) + .nodes(elts.iter()) + .finish() + }); parenthesized("[", &items, "]").fmt(f) } diff --git a/crates/ruff_python_formatter/src/expression/expr_tuple.rs b/crates/ruff_python_formatter/src/expression/expr_tuple.rs index 78ad8279d6..2fcad27387 100644 --- a/crates/ruff_python_formatter/src/expression/expr_tuple.rs +++ b/crates/ruff_python_formatter/src/expression/expr_tuple.rs @@ -108,14 +108,14 @@ impl FormatNodeRule for FormatExprTuple { // // Unlike other expression parentheses, tuple parentheses are part of the range of the // tuple itself. - elts if is_parenthesized(*range, elts, f.context().source()) + _ if is_parenthesized(*range, elts, f.context().source()) && self.parentheses != TupleParentheses::StripInsideForLoop => { - parenthesized("(", &ExprSequence::new(elts), ")").fmt(f) + parenthesized("(", &ExprSequence::new(item), ")").fmt(f) } - elts => match self.parentheses { - TupleParentheses::Subscript => group(&ExprSequence::new(elts)).fmt(f), - _ => parenthesize_if_expands(&ExprSequence::new(elts)).fmt(f), + _ => match self.parentheses { + TupleParentheses::Subscript => group(&ExprSequence::new(item)).fmt(f), + _ => parenthesize_if_expands(&ExprSequence::new(item)).fmt(f), }, } } @@ -128,18 +128,20 @@ impl FormatNodeRule for FormatExprTuple { #[derive(Debug)] struct ExprSequence<'a> { - elts: &'a [Expr], + tuple: &'a ExprTuple, } impl<'a> ExprSequence<'a> { - const fn new(elts: &'a [Expr]) -> Self { - Self { elts } + const fn new(expr: &'a ExprTuple) -> Self { + Self { tuple: expr } } } impl Format> for ExprSequence<'_> { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { - f.join_comma_separated().nodes(self.elts.iter()).finish() + f.join_comma_separated(self.tuple.end()) + .nodes(&self.tuple.elts) + .finish() } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs index 2388323ae1..ab6d4be00e 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs @@ -76,12 +76,13 @@ impl Format> for FormatInheritanceClause<'_> { bases, keywords, name, + body, .. } = self.class_definition; let source = f.context().source(); - let mut joiner = f.join_comma_separated(); + let mut joiner = f.join_comma_separated(body.first().unwrap().start()); if let Some((first, rest)) = bases.split_first() { // Manually handle parentheses for the first expression because the logic in `FormatExpr` diff --git a/crates/ruff_python_formatter/src/statement/stmt_delete.rs b/crates/ruff_python_formatter/src/statement/stmt_delete.rs index c3ba7814e8..3b5d3225fc 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_delete.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_delete.rs @@ -4,7 +4,7 @@ use crate::expression::parentheses::Parenthesize; use crate::{AsFormat, FormatNodeRule, PyFormatter}; use ruff_formatter::prelude::{block_indent, format_with, space, text}; use ruff_formatter::{write, Buffer, Format, FormatResult}; -use rustpython_parser::ast::StmtDelete; +use rustpython_parser::ast::{Ranged, StmtDelete}; #[derive(Default)] pub struct FormatStmtDelete; @@ -35,7 +35,11 @@ impl FormatNodeRule for FormatStmtDelete { write!(f, [single.format().with_options(Parenthesize::IfBreaks)]) } targets => { - let item = format_with(|f| f.join_comma_separated().nodes(targets.iter()).finish()); + let item = format_with(|f| { + f.join_comma_separated(item.end()) + .nodes(targets.iter()) + .finish() + }); parenthesize_if_expands(&item).fmt(f) } } 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 353f920e2b..6611899906 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_import_from.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_import_from.rs @@ -2,7 +2,7 @@ use crate::builders::{parenthesize_if_expands, 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; +use rustpython_parser::ast::{Ranged, StmtImportFrom}; #[derive(Default)] pub struct FormatStmtImportFrom; @@ -39,7 +39,7 @@ impl FormatNodeRule for FormatStmtImportFrom { } } let names = format_with(|f| { - f.join_comma_separated() + f.join_comma_separated(item.end()) .entries(names.iter().map(|name| (name, name.format()))) .finish() }); diff --git a/crates/ruff_python_formatter/src/statement/stmt_with.rs b/crates/ruff_python_formatter/src/statement/stmt_with.rs index 03932ff2c1..8474d70240 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_with.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_with.rs @@ -68,8 +68,11 @@ impl Format> for AnyStatementWith<'_> { let comments = f.context().comments().clone(); let dangling_comments = comments.dangling_comments(self); - let joined_items = - format_with(|f| f.join_comma_separated().nodes(self.items().iter()).finish()); + let joined_items = format_with(|f| { + f.join_comma_separated(self.body().first().unwrap().start()) + .nodes(self.items().iter()) + .finish() + }); if self.is_async() { write!(f, [text("async"), space()])?; 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 908e9c1cc9..6355a154f5 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 @@ -315,30 +315,7 @@ long_unmergable_string_with_pragma = ( bad_split_func1( "But what should happen when code has already " -@@ -96,15 +96,13 @@ - ) - - bad_split_func3( -- ( -- "But what should happen when code has already " -- r"been formatted but in the wrong way? Like " -- "with a space at the end instead of the " -- r"beginning. Or what about when it is split too " -- r"soon? In the case of a split that is too " -- "short, black will try to honer the custom " -- "split." -- ), -+ "But what should happen when code has already " -+ r"been formatted but in the wrong way? Like " -+ "with a space at the end instead of the " -+ r"beginning. Or what about when it is split too " -+ r"soon? In the case of a split that is too " -+ "short, black will try to honer the custom " -+ "split.", - xxx, - yyy, - zzz, -@@ -143,9 +141,9 @@ +@@ -143,9 +143,9 @@ ) ) @@ -350,7 +327,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,25 +163,13 @@ +@@ -165,25 +165,13 @@ triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" @@ -380,53 +357,18 @@ 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, " -@@ -212,29 +198,25 @@ - ) - +@@ -221,8 +209,8 @@ func_with_bad_comma( -- ( -- "This is a really long string argument to a function that has a trailing comma" -- " 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." - ) - - func_with_bad_comma( -- ( -- "This is a really long string argument to a function that has a trailing comma" + ( + "This is a really long string argument to a function that has a trailing comma" - " 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( -- ("short string that should have parens stripped"), x, y, z -+ "short string that should have parens stripped", x, y, z - ) - - func_with_bad_parens_that_wont_fit_in_one_line( -- x, y, ("short string that should have parens stripped"), z -+ x, y, "short string that should have parens stripped", z - ) - - func_with_bad_parens( -- ("short string that should have parens stripped"), -+ "short string that should have parens stripped", - x, - y, - z, -@@ -243,7 +225,7 @@ - func_with_bad_parens( - x, - y, -- ("short string that should have parens stripped"), -+ "short string that should have parens stripped", - z, - ) - -@@ -271,10 +253,10 @@ +@@ -271,10 +259,10 @@ def foo(): @@ -542,13 +484,15 @@ bad_split_func2( ) bad_split_func3( - "But what should happen when code has already " - r"been formatted but in the wrong way? Like " - "with a space at the end instead of the " - r"beginning. Or what about when it is split too " - r"soon? In the case of a split that is too " - "short, black will try to honer the custom " - "split.", + ( + "But what should happen when code has already " + r"been formatted but in the wrong way? Like " + "with a space at the end instead of the " + r"beginning. Or what about when it is split too " + r"soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." + ), xxx, yyy, zzz, @@ -644,25 +588,29 @@ 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." + ( + "This is a really long string argument to a function that has a trailing comma" + " 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 + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." # comment after comma + ), ) func_with_bad_parens_that_wont_fit_in_one_line( - "short string that should have parens stripped", x, y, z + ("short string that should have parens stripped"), x, y, z ) func_with_bad_parens_that_wont_fit_in_one_line( - x, y, "short string that should have parens stripped", z + x, y, ("short string that should have parens stripped"), z ) func_with_bad_parens( - "short string that should have parens stripped", + ("short string that should have parens stripped"), x, y, z, @@ -671,7 +619,7 @@ func_with_bad_parens( func_with_bad_parens( x, y, - "short string that should have parens stripped", + ("short string that should have parens stripped"), z, ) 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 cb2a031c4e..c411a16a6e 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 @@ -83,7 +83,7 @@ while x := f(x): x = (y := 0) (z := (y := (x := 0))) (info := (name, phone, *rest)) -@@ -31,17 +31,17 @@ +@@ -31,9 +31,9 @@ len(lines := f.readlines()) foo(x := 3, cat="vector") foo(cat=(category := "vector")) @@ -95,9 +95,8 @@ while x := f(x): return env_base if self._is_special and (ans := self._check_nans(context=context)): return ans - foo(b := 2, a=1) --foo((b := 2), a=1) -+foo(b := 2, a=1) +@@ -41,7 +41,7 @@ + foo((b := 2), a=1) foo(c=(b := 2), a=1) -while x := f(x): @@ -151,7 +150,7 @@ if (env_base := os.environ.get("PYTHONUSERBASE", None)): if self._is_special and (ans := self._check_nans(context=context)): return ans foo(b := 2, a=1) -foo(b := 2, a=1) +foo((b := 2), a=1) foo(c=(b := 2), a=1) while (x := f(x)): From 0ead9a16ac306bd17ab5951c0e4240c1ed71a208 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 12 Jul 2023 12:39:56 -0400 Subject: [PATCH 431/447] Bump version to 0.0.278 (#5714) --- 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 | 6 +++--- pyproject.toml | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d64986ffc2..12414ea02e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -734,7 +734,7 @@ dependencies = [ [[package]] name = "flake8-to-ruff" -version = "0.0.277" +version = "0.0.278" dependencies = [ "anyhow", "clap", @@ -1835,7 +1835,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.0.277" +version = "0.0.278" dependencies = [ "annotate-snippets 0.9.1", "anyhow", @@ -1933,7 +1933,7 @@ dependencies = [ [[package]] name = "ruff_cli" -version = "0.0.277" +version = "0.0.278" dependencies = [ "annotate-snippets 0.9.1", "anyhow", diff --git a/README.md b/README.md index d154cebbbe..7b1087920d 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,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.277 + rev: v0.0.278 hooks: - id: ruff ``` diff --git a/crates/flake8_to_ruff/Cargo.toml b/crates/flake8_to_ruff/Cargo.toml index 1324d76596..012975a2b6 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.277" +version = "0.0.278" description = """ Convert Flake8 configuration files to Ruff configuration files. """ diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 09a8c50c71..eb3e3f2455 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.0.277" +version = "0.0.278" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_cli/Cargo.toml b/crates/ruff_cli/Cargo.toml index 86f37c1a0a..ca58bdcb42 100644 --- a/crates/ruff_cli/Cargo.toml +++ b/crates/ruff_cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_cli" -version = "0.0.277" +version = "0.0.278" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/docs/tutorial.md b/docs/tutorial.md index 404ddc26a9..d8e87368a1 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.277 + rev: v0.0.278 hooks: - id: ruff ``` diff --git a/docs/usage.md b/docs/usage.md index 2e1d4d77d2..6dfcbedde1 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.277 + rev: v0.0.278 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.277 + rev: v0.0.278 hooks: - id: ruff args: [ --fix, --exit-non-zero-on-fix ] @@ -43,7 +43,7 @@ Or, to run the hook on Jupyter Notebooks too: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.0.277 + rev: v0.0.278 hooks: - id: ruff types_or: [python, pyi, jupyter] diff --git a/pyproject.toml b/pyproject.toml index 789be5574e..498c120a36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.0.277" +version = "0.0.278" 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 c029c8b37a0f98f21eafda91896a8681ba5a38eb Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 12 Jul 2023 14:22:29 -0400 Subject: [PATCH 432/447] Run release testing on PR, not push (#5718) ## Summary This job runs whenever I put up a PR to bump the version, which is really useful. But then it also runs again when I merge, and then _that_ job tends to get cancelled immediately, because I run the _actual_ release job, which triggers the cancel-concurrent-runs flow. (See, e.g., https://github.com/astral-sh/ruff/actions/runs/5534191373.) I think it makes sense to run these on PR (when editing `pyproject.toml` and friends), but not again on merge. --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 3dd0b1cf20..83c1679cb2 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -9,7 +9,7 @@ on: sha: description: "Optionally, the full sha of the commit to be released" type: string - push: + pull_request: paths: # When we change pyproject.toml, we want to ensure that the maturin builds still work - pyproject.toml From 6ce252f0eda86952293158e318bf39562c460b47 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 12 Jul 2023 17:08:22 -0400 Subject: [PATCH 433/447] Tweak hierarchy of benchmark docs (#5720) ## Summary Before: Screen Shot 2023-07-12 at 4 33 23 PM After: Screen Shot 2023-07-12 at 4 33 32 PM --- CONTRIBUTING.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 08aee28a3a..87cf63b9e6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -438,7 +438,7 @@ Benchmark 1: find . -type f -name "*.py" | xargs -P 0 pyupgrade --py311-plus Range (min … max): 29.813 s … 30.356 s 10 runs ``` -## Microbenchmarks +### Microbenchmarks The `ruff_benchmark` crate benchmarks the linter and the formatter on individual files. @@ -448,7 +448,7 @@ You can run the benchmarks with cargo benchmark ``` -### Benchmark driven Development +#### Benchmark driven Development Ruff uses [Criterion.rs](https://bheisler.github.io/criterion.rs/book/) for benchmarks. You can use `--save-baseline=` to store an initial baseline benchmark (e.g. on `main`) and then use @@ -463,7 +463,7 @@ cargo benchmark --save-baseline=main cargo benchmark --baseline=main ``` -### PR Summary +#### PR Summary You can use `--save-baseline` and `critcmp` to get a pretty comparison between two recordings. This is useful to illustrate the improvements of a PR. @@ -484,21 +484,21 @@ You must install [`critcmp`](https://github.com/BurntSushi/critcmp) for the comp cargo install critcmp ``` -### Tips +#### Tips - Use `cargo benchmark ` to only run specific benchmarks. For example: `cargo benchmark linter/pydantic` to only run the pydantic tests. - Use `cargo benchmark --quiet` for a more cleaned up output (without statistical relevance) - Use `cargo benchmark --quick` to get faster results (more prone to noise) -## Profiling Projects +### Profiling Projects You can either use the microbenchmarks from above or a project directory for benchmarking. There are a lot of profiling tools out there, [The Rust Performance Book](https://nnethercote.github.io/perf-book/profiling.html) lists some examples. -### Linux +#### Linux Install `perf` and build `ruff_benchmark` with the `release-debug` profile and then run it with perf @@ -531,7 +531,7 @@ An alternative is to convert the perf data to `flamegraph.svg` using flamegraph --perfdata perf.data ``` -### Mac +#### Mac Install [`cargo-instruments`](https://crates.io/crates/cargo-instruments): From 6dbc6d2e59e5cd101a94a03c948ad921ebac205d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 12 Jul 2023 17:09:27 -0400 Subject: [PATCH 434/447] Use shared `Cursor` across crates (#5715) ## Summary We have two `Cursor` implementations. This PR moves the implementation from the formatter into `ruff_python_whitespace` (kind of a poorly-named crate now) and uses it for both use-cases. --- crates/ruff/src/checkers/ast/mod.rs | 30 ++--- crates/ruff_python_ast/src/identifier.rs | 134 ++++++-------------- crates/ruff_python_formatter/Cargo.toml | 1 - crates/ruff_python_formatter/src/trivia.rs | 103 +-------------- crates/ruff_python_whitespace/src/cursor.rs | 103 +++++++++++++++ crates/ruff_python_whitespace/src/lib.rs | 2 + 6 files changed, 163 insertions(+), 210 deletions(-) create mode 100644 crates/ruff_python_whitespace/src/cursor.rs diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index aff756000b..9fac7dde22 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -18,7 +18,7 @@ use ruff_python_ast::str::trailing_quote; use ruff_python_ast::types::Node; use ruff_python_ast::typing::{parse_type_annotation, AnnotationKind}; use ruff_python_ast::visitor::{walk_except_handler, walk_pattern, Visitor}; -use ruff_python_ast::{cast, helpers, identifier, str, visitor}; +use ruff_python_ast::{cast, helpers, str, visitor}; use ruff_python_semantic::analyze::{branch_detection, typing, visibility}; use ruff_python_semantic::{ Binding, BindingFlags, BindingId, BindingKind, ContextualizedDefinition, Exceptions, @@ -251,9 +251,8 @@ where // Pre-visit. match stmt { Stmt::Global(ast::StmtGlobal { names, range: _ }) => { - let ranges: Vec = identifier::names(stmt, self.locator).collect(); if !self.semantic.scope_id.is_global() { - for (name, range) in names.iter().zip(ranges.iter()) { + for name in names { if let Some(binding_id) = self.semantic.global_scope().get(name) { // Mark the binding in the global scope as "rebound" in the current scope. self.semantic @@ -262,7 +261,7 @@ where // Add a binding to the current scope. let binding_id = self.semantic.push_binding( - *range, + name.range(), BindingKind::Global, BindingFlags::GLOBAL, ); @@ -272,21 +271,19 @@ where } if self.enabled(Rule::AmbiguousVariableName) { - self.diagnostics - .extend(names.iter().zip(ranges.iter()).filter_map(|(name, range)| { - pycodestyle::rules::ambiguous_variable_name(name, *range) - })); + self.diagnostics.extend(names.iter().filter_map(|name| { + pycodestyle::rules::ambiguous_variable_name(name, name.range()) + })); } } Stmt::Nonlocal(ast::StmtNonlocal { names, range: _ }) => { - let ranges: Vec = identifier::names(stmt, self.locator).collect(); if !self.semantic.scope_id.is_global() { - for (name, range) in names.iter().zip(ranges.iter()) { + for name in names { if let Some((scope_id, binding_id)) = self.semantic.nonlocal(name) { // Mark the binding as "used". self.semantic.add_local_reference( binding_id, - *range, + name.range(), ExecutionContext::Runtime, ); @@ -297,7 +294,7 @@ where // Add a binding to the current scope. let binding_id = self.semantic.push_binding( - *range, + name.range(), BindingKind::Nonlocal(scope_id), BindingFlags::NONLOCAL, ); @@ -309,17 +306,16 @@ where pylint::rules::NonlocalWithoutBinding { name: name.to_string(), }, - *range, + name.range(), )); } } } } if self.enabled(Rule::AmbiguousVariableName) { - self.diagnostics - .extend(names.iter().zip(ranges.iter()).filter_map(|(name, range)| { - pycodestyle::rules::ambiguous_variable_name(name, *range) - })); + self.diagnostics.extend(names.iter().filter_map(|name| { + pycodestyle::rules::ambiguous_variable_name(name, name.range()) + })); } } Stmt::Break(_) => { diff --git a/crates/ruff_python_ast/src/identifier.rs b/crates/ruff_python_ast/src/identifier.rs index d8ddc8606c..fb801f098e 100644 --- a/crates/ruff_python_ast/src/identifier.rs +++ b/crates/ruff_python_ast/src/identifier.rs @@ -1,29 +1,20 @@ //! Extract [`TextRange`] information from AST nodes. //! -//! In the `RustPython` AST, each node has a `range` field that contains the -//! start and end byte offsets of the node. However, attributes on those -//! nodes may not have their own ranges. In particular, identifiers are -//! not given their own ranges, unless they're part of a name expression. -//! //! For example, given: //! ```python -//! def f(): +//! try: +//! ... +//! except Exception as e: //! ... //! ``` //! -//! The statement defining `f` has a range, but the identifier `f` does not. -//! -//! This module assists with extracting [`TextRange`] ranges from AST nodes -//! via manual lexical analysis. - -use std::ops::{Add, Sub}; -use std::str::Chars; +//! This module can be used to identify the [`TextRange`] of the `except` token. use ruff_text_size::{TextLen, TextRange, TextSize}; use rustpython_ast::{Alias, Arg, ArgWithDefault, Pattern}; use rustpython_parser::ast::{self, ExceptHandler, Ranged, Stmt}; -use ruff_python_whitespace::is_python_whitespace; +use ruff_python_whitespace::{is_python_whitespace, Cursor}; use crate::source_code::Locator; @@ -163,20 +154,6 @@ impl TryIdentifier for ExceptHandler { } } -/// Return the [`TextRange`] for every name in a [`Stmt`]. -/// -/// Intended to be used for `global` and `nonlocal` statements. -/// -/// For example, return the ranges of `x` and `y` in: -/// ```python -/// global x, y -/// ``` -pub fn names<'a>(stmt: &Stmt, locator: &'a Locator<'a>) -> impl Iterator + 'a { - // Given `global x, y`, the first identifier is `global`, and the remaining identifiers are - // the names. - IdentifierTokenizer::new(locator.contents(), stmt.range()).skip(1) -} - /// Return the [`TextRange`] of the `except` token in an [`ExceptHandler`]. pub fn except(handler: &ExceptHandler, locator: &Locator) -> TextRange { IdentifierTokenizer::new(locator.contents(), handler.range()) @@ -248,13 +225,15 @@ impl<'a> IdentifierTokenizer<'a> { } fn next_token(&mut self) -> Option { - while let Some(c) = self.cursor.bump() { + while let Some(c) = { + self.offset += self.cursor.token_len(); + self.cursor.start_token(); + self.cursor.bump() + } { match c { c if is_python_identifier_start(c) => { - let start = self.offset.add(self.cursor.offset()).sub(c.text_len()); self.cursor.eat_while(is_python_identifier_continue); - let end = self.offset.add(self.cursor.offset()); - return Some(TextRange::new(start, end)); + return Some(TextRange::at(self.offset, self.cursor.token_len())); } c if is_python_whitespace(c) => { @@ -295,73 +274,15 @@ impl Iterator for IdentifierTokenizer<'_> { } } -const EOF_CHAR: char = '\0'; - -#[derive(Debug, Clone)] -struct Cursor<'a> { - chars: Chars<'a>, - offset: TextSize, -} - -impl<'a> Cursor<'a> { - fn new(source: &'a str) -> Self { - Self { - chars: source.chars(), - offset: TextSize::from(0), - } - } - - const fn offset(&self) -> TextSize { - self.offset - } - - /// Peeks the next character from the input stream without consuming it. - /// Returns [`EOF_CHAR`] if the file is at the end of the file. - fn first(&self) -> char { - self.chars.clone().next().unwrap_or(EOF_CHAR) - } - - /// Returns `true` if the file is at the end of the file. - fn is_eof(&self) -> bool { - self.chars.as_str().is_empty() - } - - /// Consumes the next character. - fn bump(&mut self) -> Option { - if let Some(char) = self.chars.next() { - self.offset += char.text_len(); - Some(char) - } else { - None - } - } - - /// Eats the next character if it matches the given character. - fn eat_char(&mut self, c: char) -> bool { - if self.first() == c { - self.bump(); - true - } else { - false - } - } - - /// Eats symbols while predicate returns true or until the end of file is reached. - fn eat_while(&mut self, mut predicate: impl FnMut(char) -> bool) { - while predicate(self.first()) && !self.is_eof() { - self.bump(); - } - } -} - #[cfg(test)] mod tests { use anyhow::Result; use ruff_text_size::{TextRange, TextSize}; - use rustpython_ast::Stmt; + use rustpython_ast::{Ranged, Stmt}; use rustpython_parser::Parse; use crate::identifier; + use crate::identifier::IdentifierTokenizer; use crate::source_code::Locator; #[test] @@ -383,4 +304,33 @@ else: ); Ok(()) } + + #[test] + fn extract_global_names() -> Result<()> { + let contents = r#"global X,Y, Z"#.trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + + let mut names = IdentifierTokenizer::new(locator.contents(), stmt.range()); + + let range = names.next_token().unwrap(); + assert_eq!(&contents[range], "global"); + assert_eq!(range, TextRange::new(TextSize::from(0), TextSize::from(6))); + + let range = names.next_token().unwrap(); + assert_eq!(&contents[range], "X"); + assert_eq!(range, TextRange::new(TextSize::from(7), TextSize::from(8))); + + let range = names.next_token().unwrap(); + assert_eq!(&contents[range], "Y"); + assert_eq!(range, TextRange::new(TextSize::from(9), TextSize::from(10))); + + let range = names.next_token().unwrap(); + assert_eq!(&contents[range], "Z"); + assert_eq!( + range, + TextRange::new(TextSize::from(12), TextSize::from(13)) + ); + Ok(()) + } } diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index dac4a95d80..381c3ec6c9 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -44,7 +44,6 @@ path = "tests/fixtures.rs" test = true required-features = [ "serde" ] - [features] serde = ["dep:serde", "ruff_formatter/serde"] default = ["serde"] diff --git a/crates/ruff_python_formatter/src/trivia.rs b/crates/ruff_python_formatter/src/trivia.rs index 4d8a1fa8cd..b6432e5336 100644 --- a/crates/ruff_python_formatter/src/trivia.rs +++ b/crates/ruff_python_formatter/src/trivia.rs @@ -1,9 +1,8 @@ -use std::str::Chars; - -use ruff_python_whitespace::is_python_whitespace; use ruff_text_size::{TextLen, TextRange, TextSize}; use unic_ucd_ident::{is_xid_continue, is_xid_start}; +use ruff_python_whitespace::{is_python_whitespace, Cursor}; + /// Searches for the first non-trivia character in `range`. /// /// The search skips over any whitespace and comments. @@ -402,9 +401,7 @@ impl<'a> SimpleTokenizer<'a> { // Skip the test whether there's a preceding comment if it has been performed before. if !self.back_line_has_no_comment { - let rest = self.cursor.chars.as_str(); - - for (back_index, c) in rest.chars().rev().enumerate() { + for (back_index, c) in self.cursor.chars().rev().enumerate() { match c { '#' => { // Potentially a comment @@ -515,100 +512,6 @@ impl DoubleEndedIterator for SimpleTokenizer<'_> { } } -const EOF_CHAR: char = '\0'; - -#[derive(Debug, Clone)] -struct Cursor<'a> { - chars: Chars<'a>, - source_length: TextSize, -} - -impl<'a> Cursor<'a> { - fn new(source: &'a str) -> Self { - Self { - source_length: source.text_len(), - chars: source.chars(), - } - } - - /// Peeks the next character from the input stream without consuming it. - /// Returns [`EOF_CHAR`] if the file is at the end of the file. - fn first(&self) -> char { - self.chars.clone().next().unwrap_or(EOF_CHAR) - } - - /// Peeks the next character from the input stream without consuming it. - /// Returns [`EOF_CHAR`] if the file is at the end of the file. - fn last(&self) -> char { - self.chars.clone().next_back().unwrap_or(EOF_CHAR) - } - - // SAFETY: THe `source.text_len` call in `new` would panic if the string length is larger than a `u32`. - #[allow(clippy::cast_possible_truncation)] - fn text_len(&self) -> TextSize { - TextSize::new(self.chars.as_str().len() as u32) - } - - fn token_len(&self) -> TextSize { - self.source_length - self.text_len() - } - - fn start_token(&mut self) { - self.source_length = self.text_len(); - } - - /// Returns `true` if the file is at the end of the file. - fn is_eof(&self) -> bool { - self.chars.as_str().is_empty() - } - - /// Consumes the next character - fn bump(&mut self) -> Option { - self.chars.next() - } - - /// Consumes the next character from the back - fn bump_back(&mut self) -> Option { - self.chars.next_back() - } - - fn eat_char(&mut self, c: char) -> bool { - if self.first() == c { - self.bump(); - true - } else { - false - } - } - - fn eat_char_back(&mut self, c: char) -> bool { - if self.last() == c { - self.bump_back(); - true - } else { - false - } - } - - /// Eats symbols while predicate returns true or until the end of file is reached. - fn eat_while(&mut self, mut predicate: impl FnMut(char) -> bool) { - // It was tried making optimized version of this for eg. line comments, but - // LLVM can inline all of this and compile it down to fast iteration over bytes. - while predicate(self.first()) && !self.is_eof() { - self.bump(); - } - } - - /// Eats symbols from the back while predicate returns true or until the beginning of file is reached. - fn eat_back_while(&mut self, mut predicate: impl FnMut(char) -> bool) { - // It was tried making optimized version of this for eg. line comments, but - // LLVM can inline all of this and compile it down to fast iteration over bytes. - while predicate(self.last()) && !self.is_eof() { - self.bump_back(); - } - } -} - #[cfg(test)] mod tests { use insta::assert_debug_snapshot; diff --git a/crates/ruff_python_whitespace/src/cursor.rs b/crates/ruff_python_whitespace/src/cursor.rs new file mode 100644 index 0000000000..43a750cb4f --- /dev/null +++ b/crates/ruff_python_whitespace/src/cursor.rs @@ -0,0 +1,103 @@ +use std::str::Chars; + +use ruff_text_size::{TextLen, TextSize}; + +pub const EOF_CHAR: char = '\0'; + +/// A [`Cursor`] over a string. +#[derive(Debug, Clone)] +pub struct Cursor<'a> { + chars: Chars<'a>, + source_length: TextSize, +} + +impl<'a> Cursor<'a> { + pub fn new(source: &'a str) -> Self { + Self { + source_length: source.text_len(), + chars: source.chars(), + } + } + + /// Return the remaining input as a string slice. + pub fn chars(&self) -> Chars<'a> { + self.chars.clone() + } + + /// Peeks the next character from the input stream without consuming it. + /// Returns [`EOF_CHAR`] if the file is at the end of the file. + pub fn first(&self) -> char { + self.chars.clone().next().unwrap_or(EOF_CHAR) + } + + /// Peeks the next character from the input stream without consuming it. + /// Returns [`EOF_CHAR`] if the file is at the end of the file. + pub fn last(&self) -> char { + self.chars.clone().next_back().unwrap_or(EOF_CHAR) + } + + // SAFETY: THe `source.text_len` call in `new` would panic if the string length is larger than a `u32`. + #[allow(clippy::cast_possible_truncation)] + pub fn text_len(&self) -> TextSize { + TextSize::new(self.chars.as_str().len() as u32) + } + + pub fn token_len(&self) -> TextSize { + self.source_length - self.text_len() + } + + pub fn start_token(&mut self) { + self.source_length = self.text_len(); + } + + /// Returns `true` if the file is at the end of the file. + pub fn is_eof(&self) -> bool { + self.chars.as_str().is_empty() + } + + /// Consumes the next character + pub fn bump(&mut self) -> Option { + self.chars.next() + } + + /// Consumes the next character from the back + pub fn bump_back(&mut self) -> Option { + self.chars.next_back() + } + + pub fn eat_char(&mut self, c: char) -> bool { + if self.first() == c { + self.bump(); + true + } else { + false + } + } + + pub fn eat_char_back(&mut self, c: char) -> bool { + if self.last() == c { + self.bump_back(); + true + } else { + false + } + } + + /// Eats symbols while predicate returns true or until the end of file is reached. + pub fn eat_while(&mut self, mut predicate: impl FnMut(char) -> bool) { + // It was tried making optimized version of this for eg. line comments, but + // LLVM can inline all of this and compile it down to fast iteration over bytes. + while predicate(self.first()) && !self.is_eof() { + self.bump(); + } + } + + /// Eats symbols from the back while predicate returns true or until the beginning of file is reached. + pub fn eat_back_while(&mut self, mut predicate: impl FnMut(char) -> bool) { + // It was tried making optimized version of this for eg. line comments, but + // LLVM can inline all of this and compile it down to fast iteration over bytes. + while predicate(self.last()) && !self.is_eof() { + self.bump_back(); + } + } +} diff --git a/crates/ruff_python_whitespace/src/lib.rs b/crates/ruff_python_whitespace/src/lib.rs index 36d3ddee97..b8c95e351c 100644 --- a/crates/ruff_python_whitespace/src/lib.rs +++ b/crates/ruff_python_whitespace/src/lib.rs @@ -1,5 +1,7 @@ +mod cursor; mod newlines; mod whitespace; +pub use cursor::*; pub use newlines::*; pub use whitespace::*; From c87faca8846658cd0c62c89d2bd785fb7ead7a13 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 12 Jul 2023 17:22:09 -0400 Subject: [PATCH 435/447] Use `Cursor` for shebang parsing (#5716) ## Summary Better to leverage the shared functionality we get from `Cursor`. It's also a little bit faster, which is very cool. --- crates/ruff/src/checkers/physical_lines.rs | 2 +- crates/ruff/src/comments/mod.rs | 1 + crates/ruff/src/comments/shebang.rs | 67 +++++++++++++++ ..._shebang__tests__shebang_end_of_line.snap} | 2 +- ...hebang__tests__shebang_leading_space.snap} | 2 +- ...ments__shebang__tests__shebang_match.snap} | 2 +- ...s__shebang__tests__shebang_non_match.snap} | 2 +- crates/ruff/src/lib.rs | 1 + .../src/rules/flake8_executable/helpers.rs | 84 +------------------ .../rules/shebang_newline.rs | 2 +- .../rules/shebang_not_executable.rs | 2 +- .../flake8_executable/rules/shebang_python.rs | 2 +- .../rules/shebang_whitespace.rs | 5 +- 13 files changed, 82 insertions(+), 92 deletions(-) create mode 100644 crates/ruff/src/comments/mod.rs create mode 100644 crates/ruff/src/comments/shebang.rs rename crates/ruff/src/{rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_end_of_line.snap => comments/snapshots/ruff__comments__shebang__tests__shebang_end_of_line.snap} (52%) rename crates/ruff/src/{rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_leading_space.snap => comments/snapshots/ruff__comments__shebang__tests__shebang_leading_space.snap} (72%) rename crates/ruff/src/{rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_match.snap => comments/snapshots/ruff__comments__shebang__tests__shebang_match.snap} (72%) rename crates/ruff/src/{rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_non_match.snap => comments/snapshots/ruff__comments__shebang__tests__shebang_non_match.snap} (52%) diff --git a/crates/ruff/src/checkers/physical_lines.rs b/crates/ruff/src/checkers/physical_lines.rs index 007f02a003..92524ef2f0 100644 --- a/crates/ruff/src/checkers/physical_lines.rs +++ b/crates/ruff/src/checkers/physical_lines.rs @@ -7,9 +7,9 @@ use ruff_diagnostics::Diagnostic; use ruff_python_ast::source_code::{Indexer, Locator, Stylist}; use ruff_python_whitespace::UniversalNewlines; +use crate::comments::shebang::ShebangDirective; use crate::registry::Rule; use crate::rules::flake8_copyright::rules::missing_copyright_notice; -use crate::rules::flake8_executable::helpers::ShebangDirective; use crate::rules::flake8_executable::rules::{ shebang_missing, shebang_newline, shebang_not_executable, shebang_python, shebang_whitespace, }; diff --git a/crates/ruff/src/comments/mod.rs b/crates/ruff/src/comments/mod.rs new file mode 100644 index 0000000000..4b88bac2ed --- /dev/null +++ b/crates/ruff/src/comments/mod.rs @@ -0,0 +1 @@ +pub(crate) mod shebang; diff --git a/crates/ruff/src/comments/shebang.rs b/crates/ruff/src/comments/shebang.rs new file mode 100644 index 0000000000..a9a6bb13b1 --- /dev/null +++ b/crates/ruff/src/comments/shebang.rs @@ -0,0 +1,67 @@ +use ruff_python_whitespace::{is_python_whitespace, Cursor}; +use ruff_text_size::{TextLen, TextSize}; + +/// A shebang directive (e.g., `#!/usr/bin/env python3`). +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct ShebangDirective<'a> { + /// The offset of the directive contents (e.g., `/usr/bin/env python3`) from the start of the + /// line. + pub(crate) offset: TextSize, + /// The contents of the directive (e.g., `"/usr/bin/env python3"`). + pub(crate) contents: &'a str, +} + +impl<'a> ShebangDirective<'a> { + /// Parse a shebang directive from a line, or return `None` if the line does not contain a + /// shebang directive. + pub(crate) fn try_extract(line: &'a str) -> Option { + let mut cursor = Cursor::new(line); + + // Trim whitespace. + cursor.eat_while(is_python_whitespace); + + // Trim the `#!` prefix. + if !cursor.eat_char('#') { + return None; + } + if !cursor.eat_char('!') { + return None; + } + + Some(Self { + offset: line.text_len() - cursor.text_len(), + contents: cursor.chars().as_str(), + }) + } +} + +#[cfg(test)] +mod tests { + use insta::assert_debug_snapshot; + + use super::ShebangDirective; + + #[test] + fn shebang_non_match() { + let source = "not a match"; + assert_debug_snapshot!(ShebangDirective::try_extract(source)); + } + + #[test] + fn shebang_end_of_line() { + let source = "print('test') #!/usr/bin/python"; + assert_debug_snapshot!(ShebangDirective::try_extract(source)); + } + + #[test] + fn shebang_match() { + let source = "#!/usr/bin/env python"; + assert_debug_snapshot!(ShebangDirective::try_extract(source)); + } + + #[test] + fn shebang_leading_space() { + let source = " #!/usr/bin/env python"; + assert_debug_snapshot!(ShebangDirective::try_extract(source)); + } +} diff --git a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_end_of_line.snap b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_end_of_line.snap similarity index 52% rename from crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_end_of_line.snap rename to crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_end_of_line.snap index e5550fcc2c..87d72c9d88 100644 --- a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_end_of_line.snap +++ b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_end_of_line.snap @@ -1,5 +1,5 @@ --- -source: crates/ruff/src/rules/flake8_executable/helpers.rs +source: crates/ruff/src/comments/shebang.rs expression: "ShebangDirective::try_extract(source)" --- None diff --git a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_leading_space.snap b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_leading_space.snap similarity index 72% rename from crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_leading_space.snap rename to crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_leading_space.snap index abb2535298..8ea8bfcfca 100644 --- a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_leading_space.snap +++ b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_leading_space.snap @@ -1,5 +1,5 @@ --- -source: crates/ruff/src/rules/flake8_executable/helpers.rs +source: crates/ruff/src/comments/shebang.rs expression: "ShebangDirective::try_extract(source)" --- Some( diff --git a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_match.snap b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_match.snap similarity index 72% rename from crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_match.snap rename to crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_match.snap index 05f3d5fe3b..c0ec6ca308 100644 --- a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_match.snap +++ b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_match.snap @@ -1,5 +1,5 @@ --- -source: crates/ruff/src/rules/flake8_executable/helpers.rs +source: crates/ruff/src/comments/shebang.rs expression: "ShebangDirective::try_extract(source)" --- Some( diff --git a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_non_match.snap b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_non_match.snap similarity index 52% rename from crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_non_match.snap rename to crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_non_match.snap index e5550fcc2c..87d72c9d88 100644 --- a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_non_match.snap +++ b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_non_match.snap @@ -1,5 +1,5 @@ --- -source: crates/ruff/src/rules/flake8_executable/helpers.rs +source: crates/ruff/src/comments/shebang.rs expression: "ShebangDirective::try_extract(source)" --- None diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs index b224744c31..0f65d8ffbf 100644 --- a/crates/ruff/src/lib.rs +++ b/crates/ruff/src/lib.rs @@ -14,6 +14,7 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION"); mod autofix; mod checkers; mod codes; +mod comments; mod cst; pub mod directives; mod doc_lines; diff --git a/crates/ruff/src/rules/flake8_executable/helpers.rs b/crates/ruff/src/rules/flake8_executable/helpers.rs index 7f23613bc5..be200dfb2c 100644 --- a/crates/ruff/src/rules/flake8_executable/helpers.rs +++ b/crates/ruff/src/rules/flake8_executable/helpers.rs @@ -1,92 +1,12 @@ -#[cfg(target_family = "unix")] +#![cfg(target_family = "unix")] + use std::os::unix::fs::PermissionsExt; -#[cfg(target_family = "unix")] use std::path::Path; -#[cfg(target_family = "unix")] use anyhow::Result; -use ruff_text_size::{TextLen, TextSize}; -/// A shebang directive (e.g., `#!/usr/bin/env python3`). -#[derive(Debug, PartialEq, Eq)] -pub(crate) struct ShebangDirective<'a> { - /// The offset of the directive contents (e.g., `/usr/bin/env python3`) from the start of the - /// line. - pub(crate) offset: TextSize, - /// The contents of the directive (e.g., `"/usr/bin/env python3"`). - pub(crate) contents: &'a str, -} - -impl<'a> ShebangDirective<'a> { - /// Parse a shebang directive from a line, or return `None` if the line does not contain a - /// shebang directive. - pub(crate) fn try_extract(line: &'a str) -> Option { - // Trim whitespace. - let directive = Self::lex_whitespace(line); - - // Trim the `#!` prefix. - let directive = Self::lex_char(directive, '#')?; - let directive = Self::lex_char(directive, '!')?; - - Some(Self { - offset: line.text_len() - directive.text_len(), - contents: directive, - }) - } - - /// Lex optional leading whitespace. - #[inline] - fn lex_whitespace(line: &str) -> &str { - line.trim_start() - } - - /// Lex a specific character, or return `None` if the character is not the first character in - /// the line. - #[inline] - fn lex_char(line: &str, c: char) -> Option<&str> { - let mut chars = line.chars(); - if chars.next() == Some(c) { - Some(chars.as_str()) - } else { - None - } - } -} - -#[cfg(target_family = "unix")] pub(super) fn is_executable(filepath: &Path) -> Result { let metadata = filepath.metadata()?; let permissions = metadata.permissions(); Ok(permissions.mode() & 0o111 != 0) } - -#[cfg(test)] -mod tests { - use insta::assert_debug_snapshot; - - use crate::rules::flake8_executable::helpers::ShebangDirective; - - #[test] - fn shebang_non_match() { - let source = "not a match"; - assert_debug_snapshot!(ShebangDirective::try_extract(source)); - } - - #[test] - fn shebang_end_of_line() { - let source = "print('test') #!/usr/bin/python"; - assert_debug_snapshot!(ShebangDirective::try_extract(source)); - } - - #[test] - fn shebang_match() { - let source = "#!/usr/bin/env python"; - assert_debug_snapshot!(ShebangDirective::try_extract(source)); - } - - #[test] - fn shebang_leading_space() { - let source = " #!/usr/bin/env python"; - assert_debug_snapshot!(ShebangDirective::try_extract(source)); - } -} diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs index d1ae29ee77..626e29020a 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs @@ -3,7 +3,7 @@ use ruff_text_size::{TextLen, TextRange}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use crate::rules::flake8_executable::helpers::ShebangDirective; +use crate::comments::shebang::ShebangDirective; /// ## What it does /// Checks for a shebang directive that is not at the beginning of the file. diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs index b5b37037fc..56ec4b3249 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs @@ -7,10 +7,10 @@ use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use crate::comments::shebang::ShebangDirective; use crate::registry::AsRule; #[cfg(target_family = "unix")] use crate::rules::flake8_executable::helpers::is_executable; -use crate::rules::flake8_executable::helpers::ShebangDirective; /// ## What it does /// Checks for a shebang directive in a file that is not executable. diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs index 0552de6f05..832533d8ca 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs @@ -3,7 +3,7 @@ use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use crate::rules::flake8_executable::helpers::ShebangDirective; +use crate::comments::shebang::ShebangDirective; /// ## What it does /// Checks for a shebang directive in `.py` files that does not contain `python`. diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs index b0394344a8..3b8c990897 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs @@ -1,10 +1,11 @@ -use ruff_text_size::{TextRange, TextSize}; use std::ops::Sub; +use ruff_text_size::{TextRange, TextSize}; + use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use crate::rules::flake8_executable::helpers::ShebangDirective; +use crate::comments::shebang::ShebangDirective; /// ## What it does /// Checks for whitespace before a shebang directive. From 2b03bd18f4fc8090fb2342befaa6e6c4bc0c12b4 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Thu, 13 Jul 2023 02:32:34 +0100 Subject: [PATCH 436/447] Implement Pylint `consider-using-in` (#5193) ## Summary Implement Pylint rule [`consider-using-in` (`R1714`)](https://pylint.pycqa.org/en/latest/user_guide/messages/refactor/consider-using-in.html) as `repeated-equality-comparison-target` (`PLR1714`). This rule checks for expressions that can be re-written as a membership test for better readability and performance. For example, ```python foo == "bar" or foo == "baz" or foo == "qux" ``` should be rewritten as ```python foo in {"bar", "baz", "qux"} ``` Related to #970. Includes documentation. ### Implementation quirks The implementation does not work with Yoda conditions (e.g., `"a" == foo` instead of `foo == "a"`). The Pylint version does. I couldn't find a way of supporting Yoda-style conditions without it being inefficient, so didn't (I don't think people write Yoda conditions any way). ## Test Plan Added fixture. `cargo test` --- .../repeated_equality_comparison_target.py | 34 ++++ crates/ruff/src/checkers/ast/mod.rs | 15 +- crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/pylint/mod.rs | 4 + crates/ruff/src/rules/pylint/rules/mod.rs | 2 + .../repeated_equality_comparison_target.rs | 151 ++++++++++++++++++ ...epeated_equality_comparison_target.py.snap | 53 ++++++ .../rules/ruff/rules/invalid_index_type.rs | 5 +- ruff.schema.json | 1 + 9 files changed, 260 insertions(+), 6 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/pylint/repeated_equality_comparison_target.py create mode 100644 crates/ruff/src/rules/pylint/rules/repeated_equality_comparison_target.rs create mode 100644 crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLR1714_repeated_equality_comparison_target.py.snap diff --git a/crates/ruff/resources/test/fixtures/pylint/repeated_equality_comparison_target.py b/crates/ruff/resources/test/fixtures/pylint/repeated_equality_comparison_target.py new file mode 100644 index 0000000000..f82e19a761 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pylint/repeated_equality_comparison_target.py @@ -0,0 +1,34 @@ +# Errors. +foo == "a" or foo == "b" + +foo != "a" and foo != "b" + +foo == "a" or foo == "b" or foo == "c" + +foo != "a" and foo != "b" and foo != "c" + +foo == a or foo == "b" or foo == 3 # Mixed types. + +# False negatives (the current implementation doesn't support Yoda conditions). +"a" == foo or "b" == foo or "c" == foo + +"a" != foo and "b" != foo and "c" != foo + +"a" == foo or foo == "b" or "c" == foo + +# OK +foo == "a" and foo == "b" and foo == "c" # `and` mixed with `==`. + +foo != "a" or foo != "b" or foo != "c" # `or` mixed with `!=`. + +foo == a or foo == b() or foo == c # Call expression. + +foo != a or foo() != b or foo != c # Call expression. + +foo in {"a", "b", "c"} # Uses membership test already. + +foo not in {"a", "b", "c"} # Uses membership test already. + +foo == "a" # Single comparison. + +foo != "a" # Single comparison. diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 9fac7dde22..2a92755808 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -3484,11 +3484,13 @@ where } } } - Expr::BoolOp(ast::ExprBoolOp { - op, - values, - range: _, - }) => { + Expr::BoolOp( + bool_op @ ast::ExprBoolOp { + op, + values, + range: _, + }, + ) => { if self.enabled(Rule::RepeatedIsinstanceCalls) { pylint::rules::repeated_isinstance_calls(self, expr, *op, values); } @@ -3513,6 +3515,9 @@ where if self.enabled(Rule::ExprAndFalse) { flake8_simplify::rules::expr_and_false(self, expr); } + if self.enabled(Rule::RepeatedEqualityComparisonTarget) { + pylint::rules::repeated_equality_comparison_target(self, bool_op); + } } _ => {} }; diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 704deb1437..27d20325d7 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -209,6 +209,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "R0915") => (RuleGroup::Unspecified, rules::pylint::rules::TooManyStatements), (Pylint, "R1701") => (RuleGroup::Unspecified, rules::pylint::rules::RepeatedIsinstanceCalls), (Pylint, "R1711") => (RuleGroup::Unspecified, rules::pylint::rules::UselessReturn), + (Pylint, "R1714") => (RuleGroup::Unspecified, rules::pylint::rules::RepeatedEqualityComparisonTarget), (Pylint, "R1722") => (RuleGroup::Unspecified, rules::pylint::rules::SysExitAlias), (Pylint, "R2004") => (RuleGroup::Unspecified, rules::pylint::rules::MagicValueComparison), (Pylint, "R5501") => (RuleGroup::Unspecified, rules::pylint::rules::CollapsibleElseIf), diff --git a/crates/ruff/src/rules/pylint/mod.rs b/crates/ruff/src/rules/pylint/mod.rs index f6f5330d06..c182a14b23 100644 --- a/crates/ruff/src/rules/pylint/mod.rs +++ b/crates/ruff/src/rules/pylint/mod.rs @@ -112,6 +112,10 @@ mod tests { )] #[test_case(Rule::YieldInInit, Path::new("yield_in_init.py"))] #[test_case(Rule::NestedMinMax, Path::new("nested_min_max.py"))] + #[test_case( + Rule::RepeatedEqualityComparisonTarget, + Path::new("repeated_equality_comparison_target.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/pylint/rules/mod.rs b/crates/ruff/src/rules/pylint/rules/mod.rs index 551abb1ada..829c9e6916 100644 --- a/crates/ruff/src/rules/pylint/rules/mod.rs +++ b/crates/ruff/src/rules/pylint/rules/mod.rs @@ -29,6 +29,7 @@ pub(crate) use nested_min_max::*; pub(crate) use nonlocal_without_binding::*; pub(crate) use property_with_parameters::*; pub(crate) use redefined_loop_name::*; +pub(crate) use repeated_equality_comparison_target::*; pub(crate) use repeated_isinstance_calls::*; pub(crate) use return_in_init::*; pub(crate) use single_string_slots::*; @@ -79,6 +80,7 @@ mod nested_min_max; mod nonlocal_without_binding; mod property_with_parameters; mod redefined_loop_name; +mod repeated_equality_comparison_target; mod repeated_isinstance_calls; mod return_in_init; mod single_string_slots; diff --git a/crates/ruff/src/rules/pylint/rules/repeated_equality_comparison_target.rs b/crates/ruff/src/rules/pylint/rules/repeated_equality_comparison_target.rs new file mode 100644 index 0000000000..78608cd005 --- /dev/null +++ b/crates/ruff/src/rules/pylint/rules/repeated_equality_comparison_target.rs @@ -0,0 +1,151 @@ +use std::hash::BuildHasherDefault; +use std::ops::Deref; + +use itertools::{any, Itertools}; +use rustc_hash::FxHashMap; +use rustpython_parser::ast::{BoolOp, CmpOp, Expr, ExprBoolOp, ExprCompare, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::hashable::HashableExpr; +use ruff_python_ast::source_code::Locator; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for repeated equality comparisons that can rewritten as a membership +/// test. +/// +/// ## Why is this bad? +/// To check if a variable is equal to one of many values, it is common to +/// write a series of equality comparisons (e.g., +/// `foo == "bar" or foo == "baz"). +/// +/// Instead, prefer to combine the values into a collection and use the `in` +/// operator to check for membership, which is more performant and succinct. +/// If the items are hashable, use a `set` for efficiency; otherwise, use a +/// `tuple`. +/// +/// ## Example +/// ```python +/// foo == "bar" or foo == "baz" or foo == "qux" +/// ``` +/// +/// Use instead: +/// ```python +/// foo in {"bar", "baz", "qux"} +/// ``` +/// +/// ## References +/// - [Python documentation: Comparisons](https://docs.python.org/3/reference/expressions.html#comparisons) +/// - [Python documentation: Membership test operations](https://docs.python.org/3/reference/expressions.html#membership-test-operations) +/// - [Python documentation: `set`](https://docs.python.org/3/library/stdtypes.html#set) +#[violation] +pub struct RepeatedEqualityComparisonTarget { + expr: String, +} + +impl Violation for RepeatedEqualityComparisonTarget { + #[derive_message_formats] + fn message(&self) -> String { + let RepeatedEqualityComparisonTarget { expr } = self; + format!( + "Consider merging multiple comparisons: `{expr}`. Use a `set` if the elements are hashable." + ) + } +} + +/// PLR1714 +pub(crate) fn repeated_equality_comparison_target(checker: &mut Checker, bool_op: &ExprBoolOp) { + if bool_op + .values + .iter() + .any(|value| !is_allowed_value(bool_op.op, value)) + { + return; + } + + let mut left_to_comparators: FxHashMap)> = + FxHashMap::with_capacity_and_hasher(bool_op.values.len(), BuildHasherDefault::default()); + for value in &bool_op.values { + if let Expr::Compare(ExprCompare { + left, comparators, .. + }) = value + { + let (count, matches) = left_to_comparators + .entry(left.deref().into()) + .or_insert_with(|| (0, Vec::new())); + *count += 1; + matches.extend(comparators); + } + } + + for (left, (count, comparators)) in left_to_comparators { + if count > 1 { + checker.diagnostics.push(Diagnostic::new( + RepeatedEqualityComparisonTarget { + expr: merged_membership_test( + left.as_expr(), + bool_op.op, + &comparators, + checker.locator, + ), + }, + bool_op.range(), + )); + } + } +} + +/// Return `true` if the given expression is compatible with a membership test. +/// E.g., `==` operators can be joined with `or` and `!=` operators can be +/// joined with `and`. +fn is_allowed_value(bool_op: BoolOp, value: &Expr) -> bool { + let Expr::Compare(ExprCompare { + left, + ops, + comparators, + .. + }) = value + else { + return false; + }; + + ops.iter().all(|op| { + if match bool_op { + BoolOp::Or => !matches!(op, CmpOp::Eq), + BoolOp::And => !matches!(op, CmpOp::NotEq), + } { + return false; + } + + if left.is_call_expr() { + return false; + } + + if any(comparators.iter(), Expr::is_call_expr) { + return false; + } + + true + }) +} + +/// Generate a string like `obj in (a, b, c)` or `obj not in (a, b, c)`. +fn merged_membership_test( + left: &Expr, + op: BoolOp, + comparators: &[&Expr], + locator: &Locator, +) -> String { + let op = match op { + BoolOp::Or => "in", + BoolOp::And => "not in", + }; + let left = locator.slice(left.range()); + let members = comparators + .iter() + .map(|comparator| locator.slice(comparator.range())) + .join(", "); + format!("{left} {op} ({members})",) +} diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLR1714_repeated_equality_comparison_target.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLR1714_repeated_equality_comparison_target.py.snap new file mode 100644 index 0000000000..9fa0ddc667 --- /dev/null +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLR1714_repeated_equality_comparison_target.py.snap @@ -0,0 +1,53 @@ +--- +source: crates/ruff/src/rules/pylint/mod.rs +--- +repeated_equality_comparison_target.py:2:1: PLR1714 Consider merging multiple comparisons: `foo in ("a", "b")`. Use a `set` if the elements are hashable. + | +1 | # Errors. +2 | foo == "a" or foo == "b" + | ^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714 +3 | +4 | foo != "a" and foo != "b" + | + +repeated_equality_comparison_target.py:4:1: PLR1714 Consider merging multiple comparisons: `foo not in ("a", "b")`. Use a `set` if the elements are hashable. + | +2 | foo == "a" or foo == "b" +3 | +4 | foo != "a" and foo != "b" + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714 +5 | +6 | foo == "a" or foo == "b" or foo == "c" + | + +repeated_equality_comparison_target.py:6:1: PLR1714 Consider merging multiple comparisons: `foo in ("a", "b", "c")`. Use a `set` if the elements are hashable. + | +4 | foo != "a" and foo != "b" +5 | +6 | foo == "a" or foo == "b" or foo == "c" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714 +7 | +8 | foo != "a" and foo != "b" and foo != "c" + | + +repeated_equality_comparison_target.py:8:1: PLR1714 Consider merging multiple comparisons: `foo not in ("a", "b", "c")`. Use a `set` if the elements are hashable. + | + 6 | foo == "a" or foo == "b" or foo == "c" + 7 | + 8 | foo != "a" and foo != "b" and foo != "c" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714 + 9 | +10 | foo == a or foo == "b" or foo == 3 # Mixed types. + | + +repeated_equality_comparison_target.py:10:1: PLR1714 Consider merging multiple comparisons: `foo in (a, "b", 3)`. Use a `set` if the elements are hashable. + | + 8 | foo != "a" and foo != "b" and foo != "c" + 9 | +10 | foo == a or foo == "b" or foo == 3 # Mixed types. + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714 +11 | +12 | # False negatives (the current implementation doesn't support Yoda conditions). + | + + diff --git a/crates/ruff/src/rules/ruff/rules/invalid_index_type.rs b/crates/ruff/src/rules/ruff/rules/invalid_index_type.rs index 399a77dbab..b8e832a62e 100644 --- a/crates/ruff/src/rules/ruff/rules/invalid_index_type.rs +++ b/crates/ruff/src/rules/ruff/rules/invalid_index_type.rs @@ -74,7 +74,10 @@ pub(crate) fn invalid_index_type(checker: &mut Checker, expr: &ExprSubscript) { // The value types supported by this rule should always be checkable let Some(value_type) = CheckableExprType::try_from(value) else { - debug_assert!(false, "Index value must be a checkable type to generate a violation message."); + debug_assert!( + false, + "Index value must be a checkable type to generate a violation message." + ); return; }; diff --git a/ruff.schema.json b/ruff.schema.json index 2fc94daff7..696a7d9cca 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2222,6 +2222,7 @@ "PLR1701", "PLR171", "PLR1711", + "PLR1714", "PLR172", "PLR1722", "PLR2", From 19f475ae1f36c387231ff3a00bff7236215bc913 Mon Sep 17 00:00:00 2001 From: Justin Prieto Date: Wed, 12 Jul 2023 22:50:00 -0400 Subject: [PATCH 437/447] [`flake8-pyi`] Implement PYI036 (#5668) ## Summary Implements PYI036 from `flake8-pyi`. See [original code](https://github.com/PyCQA/flake8-pyi/blob/main/pyi.py#L1585) ## Test Plan - Updated snapshots - Checked against manual runs of flake8 ref: #848 --- .../test/fixtures/flake8_pyi/PYI036.py | 75 ++++ .../test/fixtures/flake8_pyi/PYI036.pyi | 75 ++++ crates/ruff/src/checkers/ast/mod.rs | 8 + crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/flake8_pyi/mod.rs | 2 + .../flake8_pyi/rules/exit_annotations.rs | 351 ++++++++++++++++++ crates/ruff/src/rules/flake8_pyi/rules/mod.rs | 2 + ...__flake8_pyi__tests__PYI036_PYI036.py.snap | 4 + ..._flake8_pyi__tests__PYI036_PYI036.pyi.snap | 171 +++++++++ ruff.schema.json | 1 + 10 files changed, 690 insertions(+) create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI036.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI036.pyi create mode 100644 crates/ruff/src/rules/flake8_pyi/rules/exit_annotations.rs create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI036_PYI036.py.snap create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI036_PYI036.pyi.snap diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI036.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI036.py new file mode 100644 index 0000000000..57f71dc39e --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI036.py @@ -0,0 +1,75 @@ +import builtins +import types +import typing +from collections.abc import Awaitable +from types import TracebackType +from typing import Any, Type + +import _typeshed +import typing_extensions +from _typeshed import Unused + +class GoodOne: + def __exit__(self, *args: object) -> None: ... + async def __aexit__(self, *args) -> str: ... + +class GoodTwo: + def __exit__(self, typ: type[builtins.BaseException] | None, *args: builtins.object) -> bool | None: ... + async def __aexit__(self, /, typ: Type[BaseException] | None, *args: object, **kwargs) -> bool: ... + +class GoodThree: + def __exit__(self, __typ: typing.Type[BaseException] | None, exc: BaseException | None, *args: object) -> None: ... + async def __aexit__(self, typ: typing_extensions.Type[BaseException] | None, __exc: BaseException | None, *args: object) -> None: ... + +class GoodFour: + def __exit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ... + async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: types.TracebackType | None, *args: list[None]) -> None: ... + +class GoodFive: + def __exit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, weird_extra_arg: int = ..., *args: int, **kwargs: str) -> None: ... + async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> Awaitable[None]: ... + +class GoodSix: + def __exit__(self, typ: object, exc: builtins.object, tb: object) -> None: ... + async def __aexit__(self, typ: object, exc: object, tb: builtins.object) -> None: ... + +class GoodSeven: + def __exit__(self, *args: Unused) -> bool: ... + async def __aexit__(self, typ: Type[BaseException] | None, *args: _typeshed.Unused) -> Awaitable[None]: ... + +class GoodEight: + def __exit__(self, __typ: typing.Type[BaseException] | None, exc: BaseException | None, *args: _typeshed.Unused) -> bool: ... + async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ... + +class GoodNine: + def __exit__(self, __typ: typing.Union[typing.Type[BaseException] , None], exc: typing.Union[BaseException , None], *args: _typeshed.Unused) -> bool: ... + async def __aexit__(self, typ: typing.Union[typing.Type[BaseException], None], exc: typing.Union[BaseException , None], tb: typing.Union[TracebackType , None], weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ... + +class GoodTen: + def __exit__(self, __typ: typing.Optional[typing.Type[BaseException]], exc: typing.Optional[BaseException], *args: _typeshed.Unused) -> bool: ... + async def __aexit__(self, typ: typing.Optional[typing.Type[BaseException]], exc: typing.Optional[BaseException], tb: typing.Optional[TracebackType], weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ... + + +class BadOne: + def __exit__(self, *args: Any) -> None: ... # PYI036: Bad star-args annotation + async def __aexit__(self) -> None: ... # PYI036: Missing args + +class BadTwo: + def __exit__(self, typ, exc, tb, weird_extra_arg) -> None: ... # PYI036: Extra arg must have default + async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ...# PYI036: Extra arg must have default + +class BadThree: + def __exit__(self, typ: type[BaseException], exc: BaseException | None, tb: TracebackType | None) -> None: ... # PYI036: First arg has bad annotation + async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException, __tb: TracebackType) -> bool | None: ... # PYI036: Second arg has bad annotation + +class BadFour: + def __exit__(self, typ: typing.Optional[type[BaseException]], exc: typing.Union[BaseException, None], tb: TracebackType) -> None: ... # PYI036: Third arg has bad annotation + async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException | None, __tb: typing.Union[TracebackType, None, int]) -> bool | None: ... # PYI036: Third arg has bad annotation + +class BadFive: + def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation + async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation + +class BadSix: + def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default + async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ... # PYI036: kwargs must have default diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI036.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI036.pyi new file mode 100644 index 0000000000..a49791aa1b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI036.pyi @@ -0,0 +1,75 @@ +import builtins +import types +import typing +from collections.abc import Awaitable +from types import TracebackType +from typing import Any, Type + +import _typeshed +import typing_extensions +from _typeshed import Unused + +class GoodOne: + def __exit__(self, *args: object) -> None: ... + async def __aexit__(self, *args) -> str: ... + +class GoodTwo: + def __exit__(self, typ: type[builtins.BaseException] | None, *args: builtins.object) -> bool | None: ... + async def __aexit__(self, /, typ: Type[BaseException] | None, *args: object, **kwargs) -> bool: ... + +class GoodThree: + def __exit__(self, __typ: typing.Type[BaseException] | None, exc: BaseException | None, *args: object) -> None: ... + async def __aexit__(self, typ: typing_extensions.Type[BaseException] | None, __exc: BaseException | None, *args: object) -> None: ... + +class GoodFour: + def __exit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ... + async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: types.TracebackType | None, *args: list[None]) -> None: ... + +class GoodFive: + def __exit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, weird_extra_arg: int = ..., *args: int, **kwargs: str) -> None: ... + async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> Awaitable[None]: ... + +class GoodSix: + def __exit__(self, typ: object, exc: builtins.object, tb: object) -> None: ... + async def __aexit__(self, typ: object, exc: object, tb: builtins.object) -> None: ... + +class GoodSeven: + def __exit__(self, *args: Unused) -> bool: ... + async def __aexit__(self, typ: Type[BaseException] | None, *args: _typeshed.Unused) -> Awaitable[None]: ... + +class GoodEight: + def __exit__(self, __typ: typing.Type[BaseException] | None, exc: BaseException | None, *args: _typeshed.Unused) -> bool: ... + async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ... + +class GoodNine: + def __exit__(self, __typ: typing.Union[typing.Type[BaseException] , None], exc: typing.Union[BaseException , None], *args: _typeshed.Unused) -> bool: ... + async def __aexit__(self, typ: typing.Union[typing.Type[BaseException], None], exc: typing.Union[BaseException , None], tb: typing.Union[TracebackType , None], weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ... + +class GoodTen: + def __exit__(self, __typ: typing.Optional[typing.Type[BaseException]], exc: typing.Optional[BaseException], *args: _typeshed.Unused) -> bool: ... + async def __aexit__(self, typ: typing.Optional[typing.Type[BaseException]], exc: typing.Optional[BaseException], tb: typing.Optional[TracebackType], weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ... + + +class BadOne: + def __exit__(self, *args: Any) -> None: ... # PYI036: Bad star-args annotation + async def __aexit__(self) -> None: ... # PYI036: Missing args + +class BadTwo: + def __exit__(self, typ, exc, tb, weird_extra_arg) -> None: ... # PYI036: Extra arg must have default + async def __aexit__(self, typ, exc, tb, *, weird_extra_arg1, weird_extra_arg2) -> None: ...# PYI036: kwargs must have default + +class BadThree: + def __exit__(self, typ: type[BaseException], exc: BaseException | None, tb: TracebackType | None) -> None: ... # PYI036: First arg has bad annotation + async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException, __tb: TracebackType) -> bool | None: ... # PYI036: Second arg has bad annotation + +class BadFour: + def __exit__(self, typ: typing.Optional[type[BaseException]], exc: typing.Union[BaseException, None], tb: TracebackType) -> None: ... # PYI036: Third arg has bad annotation + async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException | None, __tb: typing.Union[TracebackType, None, int]) -> bool | None: ... # PYI036: Third arg has bad annotation + +class BadFive: + def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation + async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation + +class BadSix: + def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default + async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ... # PYI036: kwargs must have default diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 2a92755808..930bcc35c7 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -435,6 +435,14 @@ where if self.enabled(Rule::NoReturnArgumentAnnotationInStub) { flake8_pyi::rules::no_return_argument_annotation(self, args); } + if self.enabled(Rule::BadExitAnnotation) { + flake8_pyi::rules::bad_exit_annotation( + self, + stmt.is_async_function_def_stmt(), + name, + args, + ); + } } if self.enabled(Rule::DunderFunctionName) { if let Some(diagnostic) = pep8_naming::rules::dunder_function_name( diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 27d20325d7..b8b95250ad 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -638,6 +638,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Pyi, "033") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::TypeCommentInStub), (Flake8Pyi, "034") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::NonSelfReturnType), (Flake8Pyi, "035") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnassignedSpecialVariableInStub), + (Flake8Pyi, "036") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::BadExitAnnotation), (Flake8Pyi, "042") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::SnakeCaseTypeAlias), (Flake8Pyi, "043") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::TSuffixedTypeAlias), (Flake8Pyi, "044") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::FutureAnnotationsInStub), diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index 8dd20e1f11..6b8e6e4264 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -19,6 +19,8 @@ mod tests { #[test_case(Rule::ArgumentDefaultInStub, Path::new("PYI014.pyi"))] #[test_case(Rule::AssignmentDefaultInStub, Path::new("PYI015.py"))] #[test_case(Rule::AssignmentDefaultInStub, Path::new("PYI015.pyi"))] + #[test_case(Rule::BadExitAnnotation, Path::new("PYI036.py"))] + #[test_case(Rule::BadExitAnnotation, Path::new("PYI036.pyi"))] #[test_case(Rule::BadVersionInfoComparison, Path::new("PYI006.py"))] #[test_case(Rule::BadVersionInfoComparison, Path::new("PYI006.pyi"))] #[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.py"))] diff --git a/crates/ruff/src/rules/flake8_pyi/rules/exit_annotations.rs b/crates/ruff/src/rules/flake8_pyi/rules/exit_annotations.rs new file mode 100644 index 0000000000..69887d75e3 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/exit_annotations.rs @@ -0,0 +1,351 @@ +use std::fmt::{Display, Formatter}; + +use rustpython_parser::ast::{ + ArgWithDefault, Arguments, Expr, ExprBinOp, ExprSubscript, ExprTuple, Identifier, Operator, + Ranged, +}; +use smallvec::SmallVec; + +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_none; +use ruff_python_semantic::SemanticModel; + +use crate::checkers::ast::Checker; +use crate::registry::AsRule; + +/// ## What it does +/// Checks for incorrect function signatures on `__exit__` and `__aexit__` +/// methods. +/// +/// ## Why is this bad? +/// Improperly-annotated `__exit__` and `__aexit__` methods can cause +/// unexpected behavior when interacting with type checkers. +/// +/// ## Example +/// ```python +/// class Foo: +/// def __exit__(self, typ, exc, tb, extra_arg) -> None: +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// class Foo: +/// def __exit__( +/// self, +/// typ: type[BaseException] | None, +/// exc: BaseException | None, +/// tb: TracebackType | None, +/// extra_arg: int = 0, +/// ) -> None: +/// ... +/// ``` +#[violation] +pub struct BadExitAnnotation { + func_kind: FuncKind, + error_kind: ErrorKind, +} + +impl Violation for BadExitAnnotation { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + let method_name = self.func_kind.to_string(); + match self.error_kind { + ErrorKind::StarArgsNotAnnotated => format!("Star-args in `{method_name}` should be annotated with `object`"), + ErrorKind::MissingArgs => format!("If there are no star-args, `{method_name}` should have at least 3 non-keyword-only args (excluding `self`)"), + ErrorKind::ArgsAfterFirstFourMustHaveDefault => format!("All arguments after the first four in `{method_name}` must have a default value"), + ErrorKind::AllKwargsMustHaveDefault => format!("All keyword-only arguments in `{method_name}` must have a default value"), + ErrorKind::FirstArgBadAnnotation => format!("The first argument in `{method_name}` should be annotated with `object` or `type[BaseException] | None`"), + ErrorKind::SecondArgBadAnnotation => format!("The second argument in `{method_name}` should be annotated with `object` or `BaseException | None`"), + ErrorKind::ThirdArgBadAnnotation => format!("The third argument in `{method_name}` should be annotated with `object` or `types.TracebackType | None`"), + } + } + + fn autofix_title(&self) -> Option { + if matches!(self.error_kind, ErrorKind::StarArgsNotAnnotated) { + Some("Annotate star-args with `object`".to_string()) + } else { + None + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum FuncKind { + Sync, + Async, +} + +impl Display for FuncKind { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + FuncKind::Sync => write!(f, "__exit__"), + FuncKind::Async => write!(f, "__aexit__"), + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum ErrorKind { + StarArgsNotAnnotated, + MissingArgs, + FirstArgBadAnnotation, + SecondArgBadAnnotation, + ThirdArgBadAnnotation, + ArgsAfterFirstFourMustHaveDefault, + AllKwargsMustHaveDefault, +} + +/// PYI036 +pub(crate) fn bad_exit_annotation( + checker: &mut Checker, + is_async: bool, + name: &Identifier, + args: &Arguments, +) { + let func_kind = match name.as_str() { + "__exit__" if !is_async => FuncKind::Sync, + "__aexit__" if is_async => FuncKind::Async, + _ => return, + }; + + let positional_args = args + .args + .iter() + .chain(args.posonlyargs.iter()) + .collect::>(); + + // If there are less than three positional arguments, at least one of them must be a star-arg, + // and it must be annotated with `object`. + if positional_args.len() < 4 { + check_short_args_list(checker, args, func_kind); + } + + // Every positional argument (beyond the first four) must have a default. + for arg_with_default in positional_args + .iter() + .skip(4) + .filter(|arg_with_default| arg_with_default.default.is_none()) + { + checker.diagnostics.push(Diagnostic::new( + BadExitAnnotation { + func_kind, + error_kind: ErrorKind::ArgsAfterFirstFourMustHaveDefault, + }, + arg_with_default.range(), + )); + } + + // ...as should all keyword-only arguments. + for arg_with_default in args.kwonlyargs.iter().filter(|arg| arg.default.is_none()) { + checker.diagnostics.push(Diagnostic::new( + BadExitAnnotation { + func_kind, + error_kind: ErrorKind::AllKwargsMustHaveDefault, + }, + arg_with_default.range(), + )); + } + + check_positional_args(checker, &positional_args, func_kind); +} + +/// Determine whether a "short" argument list (i.e., an argument list with less than four elements) +/// contains a star-args argument annotated with `object`. If not, report an error. +fn check_short_args_list(checker: &mut Checker, args: &Arguments, func_kind: FuncKind) { + if let Some(varargs) = &args.vararg { + if let Some(annotation) = varargs + .annotation + .as_ref() + .filter(|ann| !is_object_or_unused(ann, checker.semantic())) + { + let mut diagnostic = Diagnostic::new( + BadExitAnnotation { + func_kind, + error_kind: ErrorKind::StarArgsNotAnnotated, + }, + annotation.range(), + ); + + if checker.patch(diagnostic.kind.rule()) { + if checker.semantic().is_builtin("object") { + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( + "object".to_string(), + annotation.range(), + ))); + } + } + + checker.diagnostics.push(diagnostic); + } + } else { + checker.diagnostics.push(Diagnostic::new( + BadExitAnnotation { + func_kind, + error_kind: ErrorKind::MissingArgs, + }, + args.range(), + )); + } +} + +/// Determines whether the positional arguments of an `__exit__` or `__aexit__` method are +/// annotated correctly. +fn check_positional_args( + checker: &mut Checker, + positional_args: &[&ArgWithDefault], + kind: FuncKind, +) { + // For each argument, define the predicate against which to check the annotation. + type AnnotationValidator = fn(&Expr, &SemanticModel) -> bool; + + let validations: [(ErrorKind, AnnotationValidator); 3] = [ + (ErrorKind::FirstArgBadAnnotation, is_base_exception_type), + (ErrorKind::SecondArgBadAnnotation, is_base_exception), + (ErrorKind::ThirdArgBadAnnotation, is_traceback_type), + ]; + + for (arg, (error_info, predicate)) in positional_args + .iter() + .skip(1) + .take(3) + .zip(validations.into_iter()) + { + let Some(annotation) = arg.def.annotation.as_ref() else { + continue; + }; + + if is_object_or_unused(annotation, checker.semantic()) { + continue; + } + + // If there's an annotation that's not `object` or `Unused`, check that the annotated type + // matches the predicate. + if non_none_annotation_element(annotation, checker.semantic()) + .map_or(false, |elem| predicate(elem, checker.semantic())) + { + continue; + } + + checker.diagnostics.push(Diagnostic::new( + BadExitAnnotation { + func_kind: kind, + error_kind: error_info, + }, + annotation.range(), + )); + } +} + +/// Return the non-`None` annotation element of a PEP 604-style union or `Optional` annotation. +fn non_none_annotation_element<'a>( + annotation: &'a Expr, + model: &SemanticModel, +) -> Option<&'a Expr> { + // E.g., `typing.Union` or `typing.Optional` + if let Expr::Subscript(ExprSubscript { value, slice, .. }) = annotation { + if model.match_typing_expr(value, "Optional") { + return if is_const_none(slice) { + None + } else { + Some(slice) + }; + } + + if !model.match_typing_expr(value, "Union") { + return None; + } + + let Expr::Tuple(ExprTuple { elts, .. }) = slice.as_ref() else { + return None; + }; + + let [left, right] = elts.as_slice() else { + return None; + }; + + return match (is_const_none(left), is_const_none(right)) { + (false, true) => Some(left), + (true, false) => Some(right), + (true, true) => None, + (false, false) => None, + }; + } + + // PEP 604-style union (e.g., `int | None`) + if let Expr::BinOp(ExprBinOp { + op: Operator::BitOr, + left, + right, + .. + }) = annotation + { + if !is_const_none(left) { + return Some(left); + } + + if !is_const_none(right) { + return Some(right); + } + + return None; + } + + None +} + +/// Return `true` if the [`Expr`] is the `object` builtin or the `_typeshed.Unused` type. +fn is_object_or_unused(expr: &Expr, model: &SemanticModel) -> bool { + model + .resolve_call_path(expr) + .as_ref() + .map_or(false, |call_path| { + matches!( + call_path.as_slice(), + ["" | "builtins", "object"] | ["_typeshed", "Unused"] + ) + }) +} + +/// Return `true` if the [`Expr`] is `BaseException`. +fn is_base_exception(expr: &Expr, model: &SemanticModel) -> bool { + model + .resolve_call_path(expr) + .as_ref() + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["" | "builtins", "BaseException"]) + }) +} + +/// Return `true` if the [`Expr`] is the `types.TracebackType` type. +fn is_traceback_type(expr: &Expr, model: &SemanticModel) -> bool { + model + .resolve_call_path(expr) + .as_ref() + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["types", "TracebackType"]) + }) +} + +/// Return `true` if the [`Expr`] is, e.g., `Type[BaseException]`. +fn is_base_exception_type(expr: &Expr, model: &SemanticModel) -> bool { + let Expr::Subscript(ExprSubscript { value, slice, .. }) = expr else { + return false; + }; + + if model.match_typing_expr(value, "Type") + || model + .resolve_call_path(value) + .as_ref() + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["" | "builtins", "type"]) + }) + { + is_base_exception(slice, model) + } else { + false + } +} diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index ce083d31c3..5fda64cb32 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -5,6 +5,7 @@ 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::*; +pub(crate) use exit_annotations::*; pub(crate) use future_annotations_in_stub::*; pub(crate) use iter_method_return_iterable::*; pub(crate) use no_return_argument_annotation::*; @@ -33,6 +34,7 @@ mod complex_if_statement_in_stub; mod docstring_in_stubs; mod duplicate_union_member; mod ellipsis_in_non_empty_class_body; +mod exit_annotations; mod future_annotations_in_stub; mod iter_method_return_iterable; mod no_return_argument_annotation; diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI036_PYI036.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI036_PYI036.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI036_PYI036.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__PYI036_PYI036.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI036_PYI036.pyi.snap new file mode 100644 index 0000000000..2c3a0dc7b6 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI036_PYI036.pyi.snap @@ -0,0 +1,171 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI036.pyi:54:31: PYI036 [*] Star-args in `__exit__` should be annotated with `object` + | +53 | class BadOne: +54 | def __exit__(self, *args: Any) -> None: ... # PYI036: Bad star-args annotation + | ^^^ PYI036 +55 | async def __aexit__(self) -> None: ... # PYI036: Missing args + | + = help: Annotate star-args with `object` + +ℹ Fix +51 51 | +52 52 | +53 53 | class BadOne: +54 |- def __exit__(self, *args: Any) -> None: ... # PYI036: Bad star-args annotation + 54 |+ def __exit__(self, *args: object) -> None: ... # PYI036: Bad star-args annotation +55 55 | async def __aexit__(self) -> None: ... # PYI036: Missing args +56 56 | +57 57 | class BadTwo: + +PYI036.pyi:55:24: PYI036 If there are no star-args, `__aexit__` should have at least 3 non-keyword-only args (excluding `self`) + | +53 | class BadOne: +54 | def __exit__(self, *args: Any) -> None: ... # PYI036: Bad star-args annotation +55 | async def __aexit__(self) -> None: ... # PYI036: Missing args + | ^^^^^^ PYI036 +56 | +57 | class BadTwo: + | + +PYI036.pyi:58:38: PYI036 All arguments after the first four in `__exit__` must have a default value + | +57 | class BadTwo: +58 | def __exit__(self, typ, exc, tb, weird_extra_arg) -> None: ... # PYI036: Extra arg must have default + | ^^^^^^^^^^^^^^^ PYI036 +59 | async def __aexit__(self, typ, exc, tb, *, weird_extra_arg1, weird_extra_arg2) -> None: ...# PYI036: kwargs must have default + | + +PYI036.pyi:59:48: PYI036 All keyword-only arguments in `__aexit__` must have a default value + | +57 | class BadTwo: +58 | def __exit__(self, typ, exc, tb, weird_extra_arg) -> None: ... # PYI036: Extra arg must have default +59 | async def __aexit__(self, typ, exc, tb, *, weird_extra_arg1, weird_extra_arg2) -> None: ...# PYI036: kwargs must have default + | ^^^^^^^^^^^^^^^^ PYI036 +60 | +61 | class BadThree: + | + +PYI036.pyi:59:66: PYI036 All keyword-only arguments in `__aexit__` must have a default value + | +57 | class BadTwo: +58 | def __exit__(self, typ, exc, tb, weird_extra_arg) -> None: ... # PYI036: Extra arg must have default +59 | async def __aexit__(self, typ, exc, tb, *, weird_extra_arg1, weird_extra_arg2) -> None: ...# PYI036: kwargs must have default + | ^^^^^^^^^^^^^^^^ PYI036 +60 | +61 | class BadThree: + | + +PYI036.pyi:62:29: PYI036 The first argument in `__exit__` should be annotated with `object` or `type[BaseException] | None` + | +61 | class BadThree: +62 | def __exit__(self, typ: type[BaseException], exc: BaseException | None, tb: TracebackType | None) -> None: ... # PYI036: First arg has bad annotation + | ^^^^^^^^^^^^^^^^^^^ PYI036 +63 | async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException, __tb: TracebackType) -> bool | None: ... # PYI036: Second arg has bad annotation + | + +PYI036.pyi:63:73: PYI036 The second argument in `__aexit__` should be annotated with `object` or `BaseException | None` + | +61 | class BadThree: +62 | def __exit__(self, typ: type[BaseException], exc: BaseException | None, tb: TracebackType | None) -> None: ... # PYI036: First arg has bad annotation +63 | async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException, __tb: TracebackType) -> bool | None: ... # PYI036: Second arg has bad annotation + | ^^^^^^^^^^^^^ PYI036 +64 | +65 | class BadFour: + | + +PYI036.pyi:63:94: PYI036 The third argument in `__aexit__` should be annotated with `object` or `types.TracebackType | None` + | +61 | class BadThree: +62 | def __exit__(self, typ: type[BaseException], exc: BaseException | None, tb: TracebackType | None) -> None: ... # PYI036: First arg has bad annotation +63 | async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException, __tb: TracebackType) -> bool | None: ... # PYI036: Second arg has bad annotation + | ^^^^^^^^^^^^^ PYI036 +64 | +65 | class BadFour: + | + +PYI036.pyi:66:111: PYI036 The third argument in `__exit__` should be annotated with `object` or `types.TracebackType | None` + | +65 | class BadFour: +66 | def __exit__(self, typ: typing.Optional[type[BaseException]], exc: typing.Union[BaseException, None], tb: TracebackType) -> None: ... # PYI036: Third arg has bad annotation + | ^^^^^^^^^^^^^ PYI036 +67 | async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException | None, __tb: typing.Union[TracebackType, None, int]) -> bool | None: ... # PYI036: Third arg has bad annotation + | + +PYI036.pyi:67:101: PYI036 The third argument in `__aexit__` should be annotated with `object` or `types.TracebackType | None` + | +65 | class BadFour: +66 | def __exit__(self, typ: typing.Optional[type[BaseException]], exc: typing.Union[BaseException, None], tb: TracebackType) -> None: ... # PYI036: Third arg has bad annotation +67 | async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException | None, __tb: typing.Union[TracebackType, None, int]) -> bool | None: ... # PYI036: Third arg has bad annotation + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI036 +68 | +69 | class BadFive: + | + +PYI036.pyi:70:29: PYI036 The first argument in `__exit__` should be annotated with `object` or `type[BaseException] | None` + | +69 | class BadFive: +70 | def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation + | ^^^^^^^^^^^^^^^^^^^^ PYI036 +71 | async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation + | + +PYI036.pyi:70:58: PYI036 [*] Star-args in `__exit__` should be annotated with `object` + | +69 | class BadFive: +70 | def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation + | ^^^^^^^^^ PYI036 +71 | async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation + | + = help: Annotate star-args with `object` + +ℹ Fix +67 67 | async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException | None, __tb: typing.Union[TracebackType, None, int]) -> bool | None: ... # PYI036: Third arg has bad annotation +68 68 | +69 69 | class BadFive: +70 |- def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation + 70 |+ def __exit__(self, typ: BaseException | None, *args: object) -> bool: ... # PYI036: Bad star-args annotation +71 71 | async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation +72 72 | +73 73 | class BadSix: + +PYI036.pyi:71:74: PYI036 [*] Star-args in `__aexit__` should be annotated with `object` + | +69 | class BadFive: +70 | def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation +71 | async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation + | ^^^ PYI036 +72 | +73 | class BadSix: + | + = help: Annotate star-args with `object` + +ℹ Fix +68 68 | +69 69 | class BadFive: +70 70 | def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation +71 |- async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation + 71 |+ async def __aexit__(self, /, typ: type[BaseException] | None, *args: object) -> Awaitable[None]: ... # PYI036: Bad star-args annotation +72 72 | +73 73 | class BadSix: +74 74 | def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default + +PYI036.pyi:74:38: PYI036 All arguments after the first four in `__exit__` must have a default value + | +73 | class BadSix: +74 | def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default + | ^^^^^^^^^^^^^^^ PYI036 +75 | async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ... # PYI036: kwargs must have default + | + +PYI036.pyi:75:48: PYI036 All keyword-only arguments in `__aexit__` must have a default value + | +73 | class BadSix: +74 | def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default +75 | async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ... # PYI036: kwargs must have default + | ^^^^^^^^^^^^^^^ PYI036 + | + + diff --git a/ruff.schema.json b/ruff.schema.json index 696a7d9cca..ac8dfa68b5 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2355,6 +2355,7 @@ "PYI033", "PYI034", "PYI035", + "PYI036", "PYI04", "PYI042", "PYI043", From 34b79ead3de3e2f48fbe89cb235cd70ea59cf2ef Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 12 Jul 2023 23:50:16 -0400 Subject: [PATCH 438/447] Use Locator-based replacement rather than Generator for UP007 (#5723) ## Summary Locator-based replacement is generally preferable as we get verbatim fixes. --- .../pyupgrade/rules/use_pep604_annotation.rs | 47 +++++++------------ 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs index 4be7de1fe3..819d018c47 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs @@ -1,8 +1,9 @@ -use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Constant, Expr, Operator, Ranged}; +use itertools::Itertools; +use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::source_code::Locator; use ruff_python_semantic::analyze::typing::Pep604Operator; use crate::checkers::ast::Checker; @@ -48,32 +49,6 @@ impl Violation for NonPEP604Annotation { } } -fn optional(expr: &Expr) -> Expr { - Expr::BinOp(ast::ExprBinOp { - left: Box::new(expr.clone()), - op: Operator::BitOr, - right: Box::new(Expr::Constant(ast::ExprConstant { - value: Constant::None, - kind: None, - range: TextRange::default(), - })), - range: TextRange::default(), - }) -} - -fn union(elts: &[Expr]) -> Expr { - if elts.len() == 1 { - elts[0].clone() - } else { - Expr::BinOp(ast::ExprBinOp { - left: Box::new(union(&elts[..elts.len() - 1])), - op: Operator::BitOr, - right: Box::new(elts[elts.len() - 1].clone()), - range: TextRange::default(), - }) - } -} - /// UP007 pub(crate) fn use_pep604_annotation( checker: &mut Checker, @@ -89,7 +64,7 @@ pub(crate) fn use_pep604_annotation( let mut diagnostic = Diagnostic::new(NonPEP604Annotation, expr.range()); if fixable && checker.patch(diagnostic.kind.rule()) { diagnostic.set_fix(Fix::manual(Edit::range_replacement( - checker.generator().expr(&optional(slice)), + optional(slice, checker.locator), expr.range(), ))); } @@ -104,14 +79,14 @@ pub(crate) fn use_pep604_annotation( } Expr::Tuple(ast::ExprTuple { elts, .. }) => { diagnostic.set_fix(Fix::manual(Edit::range_replacement( - checker.generator().expr(&union(elts)), + union(elts, checker.locator), expr.range(), ))); } _ => { // Single argument. diagnostic.set_fix(Fix::manual(Edit::range_replacement( - checker.generator().expr(slice), + checker.locator.slice(slice.range()).to_string(), expr.range(), ))); } @@ -121,3 +96,13 @@ pub(crate) fn use_pep604_annotation( } } } + +fn optional(expr: &Expr, locator: &Locator) -> String { + format!("{} | None", locator.slice(expr.range())) +} + +fn union(elts: &[Expr], locator: &Locator) -> String { + elts.iter() + .map(|expr| locator.slice(expr.range())) + .join(" | ") +} From 30702c297752ef702ee5ed268952bc0056e233dc Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 13 Jul 2023 00:11:32 -0400 Subject: [PATCH 439/447] Flatten nested tuples when fixing UP007 violations (#5724) ## Summary Also upgrading these to "Suggested" from "Manual" (they should've always been "Suggested", I think), and adding some more test cases. --- .../test/fixtures/pyupgrade/UP007.py | 8 + .../pyupgrade/rules/use_pep604_annotation.rs | 24 ++- ...ff__rules__pyupgrade__tests__UP007.py.snap | 184 +++++++++++------- ...tests__future_annotations_pep_604_p37.snap | 2 +- ...sts__future_annotations_pep_604_py310.snap | 4 +- 5 files changed, 139 insertions(+), 83 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pyupgrade/UP007.py b/crates/ruff/resources/test/fixtures/pyupgrade/UP007.py index 56591e565c..287652b030 100644 --- a/crates/ruff/resources/test/fixtures/pyupgrade/UP007.py +++ b/crates/ruff/resources/test/fixtures/pyupgrade/UP007.py @@ -27,6 +27,14 @@ def f(x: typing.Union[(str, int), float]) -> None: ... +def f(x: typing.Union[(int,)]) -> None: + ... + + +def f(x: typing.Union[()]) -> None: + ... + + def f(x: "Union[str, int, Union[float, bytes]]") -> None: ... diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs index 819d018c47..b1515b410e 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs @@ -1,3 +1,4 @@ +use itertools::Either::{Left, Right}; use itertools::Itertools; use rustpython_parser::ast::{self, Expr, Ranged}; @@ -63,7 +64,7 @@ pub(crate) fn use_pep604_annotation( Pep604Operator::Optional => { let mut diagnostic = Diagnostic::new(NonPEP604Annotation, expr.range()); if fixable && checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Fix::manual(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( optional(slice, checker.locator), expr.range(), ))); @@ -78,14 +79,14 @@ pub(crate) fn use_pep604_annotation( // Invalid type annotation. } Expr::Tuple(ast::ExprTuple { elts, .. }) => { - diagnostic.set_fix(Fix::manual(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( union(elts, checker.locator), expr.range(), ))); } _ => { // Single argument. - diagnostic.set_fix(Fix::manual(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.locator.slice(slice.range()).to_string(), expr.range(), ))); @@ -97,12 +98,23 @@ pub(crate) fn use_pep604_annotation( } } +/// Format the expression as a PEP 604-style optional. fn optional(expr: &Expr, locator: &Locator) -> String { format!("{} | None", locator.slice(expr.range())) } +/// Format the expressions as a PEP 604-style union. fn union(elts: &[Expr], locator: &Locator) -> String { - elts.iter() - .map(|expr| locator.slice(expr.range())) - .join(" | ") + let mut elts = elts + .iter() + .flat_map(|expr| match expr { + Expr::Tuple(ast::ExprTuple { elts, .. }) => Left(elts.iter()), + _ => Right(std::iter::once(expr)), + }) + .peekable(); + if elts.peek().is_none() { + "()".to_string() + } else { + elts.map(|expr| locator.slice(expr.range())).join(" | ") + } } diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP007.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP007.py.snap index cd61499b3c..dc112cd0e0 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP007.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP007.py.snap @@ -9,7 +9,7 @@ UP007.py:6:10: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Possible fix +ℹ Suggested fix 3 3 | from typing import Union 4 4 | 5 5 | @@ -27,7 +27,7 @@ UP007.py:10:10: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Possible fix +ℹ Suggested fix 7 7 | ... 8 8 | 9 9 | @@ -45,7 +45,7 @@ UP007.py:14:10: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Possible fix +ℹ Suggested fix 11 11 | ... 12 12 | 13 13 | @@ -63,7 +63,7 @@ UP007.py:14:26: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Possible fix +ℹ Suggested fix 11 11 | ... 12 12 | 13 13 | @@ -81,7 +81,7 @@ UP007.py:18:10: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Possible fix +ℹ Suggested fix 15 15 | ... 16 16 | 17 17 | @@ -99,7 +99,7 @@ UP007.py:22:10: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Possible fix +ℹ Suggested fix 19 19 | ... 20 20 | 21 21 | @@ -117,127 +117,163 @@ UP007.py:26:10: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Possible fix +ℹ Suggested fix 23 23 | ... 24 24 | 25 25 | 26 |-def f(x: typing.Union[(str, int), float]) -> None: - 26 |+def f(x: (str, int) | float) -> None: + 26 |+def f(x: str | int | float) -> None: 27 27 | ... 28 28 | 29 29 | -UP007.py:30:11: UP007 [*] Use `X | Y` for type annotations +UP007.py:30:10: UP007 [*] Use `X | Y` for type annotations | -30 | def f(x: "Union[str, int, Union[float, bytes]]") -> None: - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP007 +30 | def f(x: typing.Union[(int,)]) -> None: + | ^^^^^^^^^^^^^^^^^^^^ UP007 31 | ... | = help: Convert to `X | Y` -ℹ Possible fix +ℹ Suggested fix 27 27 | ... 28 28 | 29 29 | -30 |-def f(x: "Union[str, int, Union[float, bytes]]") -> None: - 30 |+def f(x: "str | int | Union[float, bytes]") -> None: +30 |-def f(x: typing.Union[(int,)]) -> None: + 30 |+def f(x: int) -> None: 31 31 | ... 32 32 | 33 33 | -UP007.py:30:27: UP007 [*] Use `X | Y` for type annotations +UP007.py:34:10: UP007 [*] Use `X | Y` for type annotations | -30 | def f(x: "Union[str, int, Union[float, bytes]]") -> None: - | ^^^^^^^^^^^^^^^^^^^ UP007 -31 | ... - | - = help: Convert to `X | Y` - -ℹ Possible fix -27 27 | ... -28 28 | -29 29 | -30 |-def f(x: "Union[str, int, Union[float, bytes]]") -> None: - 30 |+def f(x: "Union[str, int, float | bytes]") -> None: -31 31 | ... -32 32 | -33 33 | - -UP007.py:34:11: UP007 [*] Use `X | Y` for type annotations - | -34 | def f(x: "typing.Union[str, int]") -> None: - | ^^^^^^^^^^^^^^^^^^^^^^ UP007 +34 | def f(x: typing.Union[()]) -> None: + | ^^^^^^^^^^^^^^^^ UP007 35 | ... | = help: Convert to `X | Y` -ℹ Possible fix +ℹ Suggested fix 31 31 | ... 32 32 | 33 33 | -34 |-def f(x: "typing.Union[str, int]") -> None: - 34 |+def f(x: "str | int") -> None: +34 |-def f(x: typing.Union[()]) -> None: + 34 |+def f(x: ()) -> None: 35 35 | ... 36 36 | 37 37 | -UP007.py:47:8: UP007 [*] Use `X | Y` for type annotations +UP007.py:38:11: UP007 [*] Use `X | Y` for type annotations | -46 | def f() -> None: -47 | x: Optional[str] - | ^^^^^^^^^^^^^ UP007 -48 | x = Optional[str] +38 | def f(x: "Union[str, int, Union[float, bytes]]") -> None: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP007 +39 | ... | = help: Convert to `X | Y` -ℹ Possible fix +ℹ Suggested fix +35 35 | ... +36 36 | +37 37 | +38 |-def f(x: "Union[str, int, Union[float, bytes]]") -> None: + 38 |+def f(x: "str | int | Union[float, bytes]") -> None: +39 39 | ... +40 40 | +41 41 | + +UP007.py:38:27: UP007 [*] Use `X | Y` for type annotations + | +38 | def f(x: "Union[str, int, Union[float, bytes]]") -> None: + | ^^^^^^^^^^^^^^^^^^^ UP007 +39 | ... + | + = help: Convert to `X | Y` + +ℹ Suggested fix +35 35 | ... +36 36 | +37 37 | +38 |-def f(x: "Union[str, int, Union[float, bytes]]") -> None: + 38 |+def f(x: "Union[str, int, float | bytes]") -> None: +39 39 | ... +40 40 | +41 41 | + +UP007.py:42:11: UP007 [*] Use `X | Y` for type annotations + | +42 | def f(x: "typing.Union[str, int]") -> None: + | ^^^^^^^^^^^^^^^^^^^^^^ UP007 +43 | ... + | + = help: Convert to `X | Y` + +ℹ Suggested fix +39 39 | ... +40 40 | +41 41 | +42 |-def f(x: "typing.Union[str, int]") -> None: + 42 |+def f(x: "str | int") -> None: +43 43 | ... 44 44 | 45 45 | -46 46 | def f() -> None: -47 |- x: Optional[str] - 47 |+ x: str | None -48 48 | x = Optional[str] -49 49 | -50 50 | x = Union[str, int] -UP007.py:48:9: UP007 Use `X | Y` for type annotations +UP007.py:55:8: UP007 [*] Use `X | Y` for type annotations | -46 | def f() -> None: -47 | x: Optional[str] -48 | x = Optional[str] +54 | def f() -> None: +55 | x: Optional[str] + | ^^^^^^^^^^^^^ UP007 +56 | x = Optional[str] + | + = help: Convert to `X | Y` + +ℹ Suggested fix +52 52 | +53 53 | +54 54 | def f() -> None: +55 |- x: Optional[str] + 55 |+ x: str | None +56 56 | x = Optional[str] +57 57 | +58 58 | x = Union[str, int] + +UP007.py:56:9: UP007 Use `X | Y` for type annotations + | +54 | def f() -> None: +55 | x: Optional[str] +56 | x = Optional[str] | ^^^^^^^^^^^^^ UP007 -49 | -50 | x = Union[str, int] +57 | +58 | x = Union[str, int] | = help: Convert to `X | Y` -UP007.py:50:9: UP007 Use `X | Y` for type annotations +UP007.py:58:9: UP007 Use `X | Y` for type annotations | -48 | x = Optional[str] -49 | -50 | x = Union[str, int] +56 | x = Optional[str] +57 | +58 | x = Union[str, int] | ^^^^^^^^^^^^^^^ UP007 -51 | x = Union["str", "int"] -52 | x: Union[str, int] +59 | x = Union["str", "int"] +60 | x: Union[str, int] | = help: Convert to `X | Y` -UP007.py:52:8: UP007 [*] Use `X | Y` for type annotations +UP007.py:60:8: UP007 [*] Use `X | Y` for type annotations | -50 | x = Union[str, int] -51 | x = Union["str", "int"] -52 | x: Union[str, int] +58 | x = Union[str, int] +59 | x = Union["str", "int"] +60 | x: Union[str, int] | ^^^^^^^^^^^^^^^ UP007 -53 | x: Union["str", "int"] +61 | x: Union["str", "int"] | = help: Convert to `X | Y` -ℹ Possible fix -49 49 | -50 50 | x = Union[str, int] -51 51 | x = Union["str", "int"] -52 |- x: Union[str, int] - 52 |+ x: str | int -53 53 | x: Union["str", "int"] +ℹ Suggested fix +57 57 | +58 58 | x = Union[str, int] +59 59 | x = Union["str", "int"] +60 |- x: Union[str, int] + 60 |+ x: str | int +61 61 | x: Union["str", "int"] diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_pep_604_p37.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_pep_604_p37.snap index b20911af4e..a5a07f6b84 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_pep_604_p37.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_pep_604_p37.snap @@ -10,7 +10,7 @@ future_annotations.py:40:4: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Possible fix +ℹ Suggested fix 37 37 | return y 38 38 | 39 39 | diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_pep_604_py310.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_pep_604_py310.snap index b025a5bc9b..db003ea51f 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_pep_604_py310.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_pep_604_py310.snap @@ -10,7 +10,7 @@ future_annotations.py:40:4: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Possible fix +ℹ Suggested fix 37 37 | return y 38 38 | 39 39 | @@ -28,7 +28,7 @@ future_annotations.py:42:21: UP007 [*] Use `X | Y` for type annotations | = help: Convert to `X | Y` -ℹ Possible fix +ℹ Suggested fix 39 39 | 40 40 | x: Optional[int] = None 41 41 | From 067b2a6ce615e4a365b53499730b4867906d89cf Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 13 Jul 2023 08:57:29 +0200 Subject: [PATCH 440/447] Pass parent to `NeedsParentheses` (#5708) --- .../src/expression/expr_attribute.rs | 59 ++--- .../src/expression/expr_await.rs | 13 +- .../src/expression/expr_bin_op.rs | 13 +- .../src/expression/expr_bool_op.rs | 12 +- .../src/expression/expr_call.rs | 45 ++-- .../src/expression/expr_compare.rs | 12 +- .../src/expression/expr_constant.rs | 66 +++--- .../src/expression/expr_dict.rs | 17 +- .../src/expression/expr_dict_comp.rs | 16 +- .../src/expression/expr_formatted_value.rs | 13 +- .../src/expression/expr_generator_exp.rs | 16 +- .../src/expression/expr_if_exp.rs | 12 +- .../src/expression/expr_joined_str.rs | 13 +- .../src/expression/expr_lambda.rs | 13 +- .../src/expression/expr_list.rs | 17 +- .../src/expression/expr_list_comp.rs | 17 +- .../src/expression/expr_name.rs | 13 +- .../src/expression/expr_named_expr.rs | 20 +- .../src/expression/expr_set.rs | 17 +- .../src/expression/expr_set_comp.rs | 16 +- .../src/expression/expr_slice.rs | 14 +- .../src/expression/expr_starred.rs | 13 +- .../src/expression/expr_subscript.rs | 16 +- .../src/expression/expr_tuple.rs | 29 ++- .../src/expression/expr_unary_op.rs | 24 +- .../src/expression/expr_yield.rs | 13 +- .../src/expression/expr_yield_from.rs | 13 +- .../src/expression/mod.rs | 205 +++++++++++------- .../src/expression/parentheses.rs | 107 +++------ .../src/expression/string.rs | 68 ++---- .../src/other/decorator.rs | 3 +- .../other/except_handler_except_handler.rs | 6 +- .../src/other/with_item.rs | 7 +- .../src/statement/stmt_assign.rs | 10 +- .../src/statement/stmt_async_function_def.rs | 6 +- .../src/statement/stmt_aug_assign.rs | 3 +- .../src/statement/stmt_class_def.rs | 11 +- .../src/statement/stmt_delete.rs | 12 +- .../src/statement/stmt_expr.rs | 17 +- .../src/statement/stmt_for.rs | 5 +- .../src/statement/stmt_function_def.rs | 12 +- .../src/statement/stmt_if.rs | 3 +- .../src/statement/stmt_raise.rs | 10 +- .../src/statement/stmt_return.rs | 5 +- .../src/statement/stmt_while.rs | 3 +- ...aneous__long_strings_flag_disabled.py.snap | 15 +- ...patibility@simple_cases__comments4.py.snap | 17 +- ...atibility@simple_cases__expression.py.snap | 21 +- ...patibility@simple_cases__fmtonoff5.py.snap | 22 +- .../format@expression__binary.py.snap | 6 +- .../format@expression__string.py.snap | 30 ++- .../snapshots/format@statement__for.py.snap | 6 +- .../snapshots/format@statement__raise.py.snap | 4 +- .../snapshots/format@statement__while.py.snap | 4 +- .../snapshots/format@statement__with.py.snap | 8 +- 55 files changed, 562 insertions(+), 606 deletions(-) diff --git a/crates/ruff_python_formatter/src/expression/expr_attribute.rs b/crates/ruff_python_formatter/src/expression/expr_attribute.rs index 2fd19d768f..e5a8602caa 100644 --- a/crates/ruff_python_formatter/src/expression/expr_attribute.rs +++ b/crates/ruff_python_formatter/src/expression/expr_attribute.rs @@ -1,11 +1,12 @@ -use crate::comments::{leading_comments, trailing_comments, Comments}; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use rustpython_parser::ast::{Constant, Expr, ExprAttribute, ExprConstant}; + +use ruff_formatter::write; +use ruff_python_ast::node::AnyNodeRef; + +use crate::comments::{leading_comments, trailing_comments}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses, Parentheses}; use crate::prelude::*; use crate::FormatNodeRule; -use ruff_formatter::write; -use rustpython_parser::ast::{Constant, Expr, ExprAttribute, ExprConstant}; #[derive(Default)] pub struct FormatExprAttribute; @@ -35,7 +36,7 @@ impl FormatNodeRule for FormatExprAttribute { dangling_comments.split_at(leading_attribute_comments_start); if needs_parentheses { - value.format().with_options(Parenthesize::Always).fmt(f)?; + value.format().with_options(Parentheses::Always).fmt(f)?; } else if let Expr::Attribute(expr_attribute) = value.as_ref() { // We're in a attribute chain (`a.b.c`). The outermost node adds parentheses if // required, the inner ones don't need them so we skip the `Expr` formatting that @@ -71,41 +72,23 @@ impl FormatNodeRule for FormatExprAttribute { } } -/// Checks if there are any own line comments in an attribute chain (a.b.c). This method is -/// recursive up to the innermost expression that the attribute chain starts behind. -fn has_breaking_comments_attribute_chain( - expr_attribute: &ExprAttribute, - comments: &Comments, -) -> bool { - if comments - .dangling_comments(expr_attribute) - .iter() - .any(|comment| comment.line_position().is_own_line()) - || comments.has_trailing_own_line_comments(expr_attribute) - { - return true; - } - - if let Expr::Attribute(inner) = expr_attribute.value.as_ref() { - return has_breaking_comments_attribute_chain(inner, comments); - } - - return comments.has_trailing_own_line_comments(expr_attribute.value.as_ref()); -} - impl NeedsParentheses for ExprAttribute { fn needs_parentheses( &self, - parenthesize: Parenthesize, + parent: AnyNodeRef, context: &PyFormatContext, - ) -> Parentheses { - if has_breaking_comments_attribute_chain(self, context.comments()) { - return Parentheses::Always; - } - - match default_expression_needs_parentheses(self.into(), parenthesize, context) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, + ) -> OptionalParentheses { + // Checks if there are any own line comments in an attribute chain (a.b.c). + if context + .comments() + .dangling_comments(self) + .iter() + .any(|comment| comment.line_position().is_own_line()) + || context.comments().has_trailing_own_line_comments(self) + { + OptionalParentheses::Always + } else { + self.value.needs_parentheses(parent, context) } } } diff --git a/crates/ruff_python_formatter/src/expression/expr_await.rs b/crates/ruff_python_formatter/src/expression/expr_await.rs index 6e275fd9a1..2f02c352ed 100644 --- a/crates/ruff_python_formatter/src/expression/expr_await.rs +++ b/crates/ruff_python_formatter/src/expression/expr_await.rs @@ -1,10 +1,9 @@ use crate::context::PyFormatContext; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::{AsFormat, FormatNodeRule, PyFormatter}; use ruff_formatter::prelude::{space, text}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprAwait; #[derive(Default)] @@ -20,9 +19,9 @@ impl FormatNodeRule for FormatExprAwait { impl NeedsParentheses for ExprAwait { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, context) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } diff --git a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs index f6f051066a..20d708e819 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs @@ -1,13 +1,12 @@ use crate::comments::{trailing_comments, trailing_node_comments}; use crate::expression::parentheses::{ - default_expression_needs_parentheses, in_parentheses_only_group, is_expression_parenthesized, - NeedsParentheses, Parenthesize, + in_parentheses_only_group, is_expression_parenthesized, NeedsParentheses, OptionalParentheses, }; use crate::expression::Parentheses; use crate::prelude::*; use crate::FormatNodeRule; use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions}; -use ruff_python_ast::node::AstNode; +use ruff_python_ast::node::{AnyNodeRef, AstNode}; use rustpython_parser::ast::{ Constant, Expr, ExprAttribute, ExprBinOp, ExprConstant, ExprUnaryOp, Operator, UnaryOp, }; @@ -175,9 +174,9 @@ impl FormatRule> for FormatOperator { impl NeedsParentheses for ExprBinOp { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, context) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } 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 9ba2f9a2b2..f18abeb18c 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bool_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bool_op.rs @@ -1,10 +1,10 @@ use crate::comments::leading_comments; use crate::expression::parentheses::{ - default_expression_needs_parentheses, in_parentheses_only_group, NeedsParentheses, Parentheses, - Parenthesize, + in_parentheses_only_group, NeedsParentheses, OptionalParentheses, Parentheses, }; use crate::prelude::*; use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::{BoolOp, ExprBoolOp}; #[derive(Default)] @@ -70,10 +70,10 @@ impl FormatNodeRule for FormatExprBoolOp { impl NeedsParentheses for ExprBoolOp { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, context) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } diff --git a/crates/ruff_python_formatter/src/expression/expr_call.rs b/crates/ruff_python_formatter/src/expression/expr_call.rs index b1dd972df2..c46aa374f7 100644 --- a/crates/ruff_python_formatter/src/expression/expr_call.rs +++ b/crates/ruff_python_formatter/src/expression/expr_call.rs @@ -1,17 +1,18 @@ -use crate::builders::PyFormatterExtensions; -use crate::comments::dangling_comments; -use crate::context::PyFormatContext; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, - Parenthesize, -}; -use crate::trivia::{SimpleTokenizer, TokenKind}; -use crate::{AsFormat, FormatNodeRule, PyFormatter}; -use ruff_formatter::prelude::{format_with, group, text}; -use ruff_formatter::{write, Buffer, FormatResult}; use ruff_text_size::{TextRange, TextSize}; use rustpython_parser::ast::{Expr, ExprCall, Ranged}; +use ruff_formatter::write; +use ruff_python_ast::node::AnyNodeRef; + +use crate::comments::dangling_comments; + +use crate::expression::parentheses::{ + parenthesized, NeedsParentheses, OptionalParentheses, Parentheses, +}; +use crate::prelude::*; +use crate::trivia::{SimpleTokenizer, TokenKind}; +use crate::FormatNodeRule; + #[derive(Default)] pub struct FormatExprCall; @@ -52,14 +53,21 @@ impl FormatNodeRule for FormatExprCall { [argument] if keywords.is_empty() => { let parentheses = if is_single_argument_parenthesized(argument, item.end(), source) { - Parenthesize::Always + Parentheses::Always } else { - Parenthesize::Never + Parentheses::Never }; joiner.entry(argument, &argument.format().with_options(parentheses)); } arguments => { - joiner.nodes(arguments).nodes(keywords.iter()); + joiner + .entries( + // We have the parentheses from the call so the arguments never need any + arguments + .iter() + .map(|arg| (arg, arg.format().with_options(Parentheses::Preserve))), + ) + .nodes(keywords.iter()); } } @@ -100,13 +108,10 @@ impl FormatNodeRule for FormatExprCall { impl NeedsParentheses for ExprCall { fn needs_parentheses( &self, - parenthesize: Parenthesize, + parent: AnyNodeRef, context: &PyFormatContext, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, context) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, - } + ) -> OptionalParentheses { + self.func.needs_parentheses(parent, context) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_compare.rs b/crates/ruff_python_formatter/src/expression/expr_compare.rs index d03094c7de..770c750162 100644 --- a/crates/ruff_python_formatter/src/expression/expr_compare.rs +++ b/crates/ruff_python_formatter/src/expression/expr_compare.rs @@ -1,11 +1,11 @@ use crate::comments::leading_comments; use crate::expression::parentheses::{ - default_expression_needs_parentheses, in_parentheses_only_group, NeedsParentheses, Parentheses, - Parenthesize, + in_parentheses_only_group, NeedsParentheses, OptionalParentheses, Parentheses, }; use crate::prelude::*; use crate::FormatNodeRule; use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::{CmpOp, ExprCompare}; #[derive(Default)] @@ -73,10 +73,10 @@ impl FormatNodeRule for FormatExprCompare { impl NeedsParentheses for ExprCompare { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, context) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } diff --git a/crates/ruff_python_formatter/src/expression/expr_constant.rs b/crates/ruff_python_formatter/src/expression/expr_constant.rs index b657b16c28..a1e3ef58e3 100644 --- a/crates/ruff_python_formatter/src/expression/expr_constant.rs +++ b/crates/ruff_python_formatter/src/expression/expr_constant.rs @@ -1,25 +1,17 @@ -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; -use crate::expression::string::{FormatString, StringLayout}; +use ruff_text_size::{TextLen, TextRange}; +use rustpython_parser::ast::{Constant, ExprConstant, Ranged}; + +use ruff_formatter::write; +use ruff_python_ast::node::AnyNodeRef; +use ruff_python_ast::str::is_implicit_concatenation; + +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; +use crate::expression::string::{FormatString, StringPrefix, StringQuotes}; use crate::prelude::*; use crate::{not_yet_implemented_custom_text, verbatim_text, FormatNodeRule}; -use ruff_formatter::{write, FormatRuleWithOptions}; -use rustpython_parser::ast::{Constant, ExprConstant}; #[derive(Default)] -pub struct FormatExprConstant { - string_layout: StringLayout, -} - -impl FormatRuleWithOptions> for FormatExprConstant { - type Options = StringLayout; - - fn with_options(mut self, options: Self::Options) -> Self { - self.string_layout = options; - self - } -} +pub struct FormatExprConstant; impl FormatNodeRule for FormatExprConstant { fn fmt_fields(&self, item: &ExprConstant, f: &mut PyFormatter) -> FormatResult<()> { @@ -39,7 +31,7 @@ impl FormatNodeRule for FormatExprConstant { Constant::Int(_) | Constant::Float(_) | Constant::Complex { .. } => { write!(f, [verbatim_text(item)]) } - Constant::Str(_) => FormatString::new(item, self.string_layout).fmt(f), + Constant::Str(_) => FormatString::new(item).fmt(f), Constant::Bytes(_) => { not_yet_implemented_custom_text(r#"b"NOT_YET_IMPLEMENTED_BYTE_STRING""#).fmt(f) } @@ -61,20 +53,32 @@ impl FormatNodeRule for FormatExprConstant { impl NeedsParentheses for ExprConstant { fn needs_parentheses( &self, - parenthesize: Parenthesize, + _parent: AnyNodeRef, context: &PyFormatContext, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, context) { - Parentheses::Optional if self.value.is_str() && parenthesize.is_if_breaks() => { - // Custom handling that only adds parentheses for implicit concatenated strings. - if parenthesize.is_if_breaks() { - Parentheses::Custom - } else { - Parentheses::Optional - } + ) -> OptionalParentheses { + if self.value.is_str() { + let contents = context.locator().slice(self.range()); + // Don't wrap triple quoted strings + if is_multiline_string(self, context.source()) || !is_implicit_concatenation(contents) { + OptionalParentheses::Never + } else { + OptionalParentheses::Multiline } - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, + } else { + OptionalParentheses::Never } } } + +pub(super) fn is_multiline_string(constant: &ExprConstant, source: &str) -> bool { + if constant.value.is_str() { + let contents = &source[constant.range()]; + let prefix = StringPrefix::parse(contents); + let quotes = + StringQuotes::parse(&contents[TextRange::new(prefix.text_len(), contents.text_len())]); + + quotes.map_or(false, StringQuotes::is_triple) && contents.contains(['\n', '\r']) + } else { + false + } +} diff --git a/crates/ruff_python_formatter/src/expression/expr_dict.rs b/crates/ruff_python_formatter/src/expression/expr_dict.rs index 5a00386779..1e8b73e022 100644 --- a/crates/ruff_python_formatter/src/expression/expr_dict.rs +++ b/crates/ruff_python_formatter/src/expression/expr_dict.rs @@ -1,11 +1,9 @@ use crate::comments::{dangling_node_comments, leading_comments}; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, - Parenthesize, -}; +use crate::expression::parentheses::{parenthesized, NeedsParentheses, OptionalParentheses}; use crate::prelude::*; use crate::FormatNodeRule; use ruff_formatter::{format_args, write}; +use ruff_python_ast::node::AnyNodeRef; use ruff_text_size::TextRange; use rustpython_parser::ast::Ranged; use rustpython_parser::ast::{Expr, ExprDict}; @@ -99,12 +97,9 @@ impl FormatNodeRule for FormatExprDict { impl NeedsParentheses for ExprDict { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, context) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, - } + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Never } } diff --git a/crates/ruff_python_formatter/src/expression/expr_dict_comp.rs b/crates/ruff_python_formatter/src/expression/expr_dict_comp.rs index 12af04579a..322dfea898 100644 --- a/crates/ruff_python_formatter/src/expression/expr_dict_comp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_dict_comp.rs @@ -1,9 +1,8 @@ use crate::context::PyFormatContext; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprDictComp; #[derive(Default)] @@ -23,12 +22,9 @@ impl FormatNodeRule for FormatExprDictComp { impl NeedsParentheses for ExprDictComp { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, context) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, - } + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Never } } diff --git a/crates/ruff_python_formatter/src/expression/expr_formatted_value.rs b/crates/ruff_python_formatter/src/expression/expr_formatted_value.rs index 7cc71b967d..d2f224efd5 100644 --- a/crates/ruff_python_formatter/src/expression/expr_formatted_value.rs +++ b/crates/ruff_python_formatter/src/expression/expr_formatted_value.rs @@ -1,9 +1,8 @@ use crate::context::PyFormatContext; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprFormattedValue; #[derive(Default)] @@ -18,9 +17,9 @@ impl FormatNodeRule for FormatExprFormattedValue { impl NeedsParentheses for ExprFormattedValue { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, context) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } 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 2f91b0c518..b9e8b2ed65 100644 --- a/crates/ruff_python_formatter/src/expression/expr_generator_exp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_generator_exp.rs @@ -1,9 +1,8 @@ use crate::context::PyFormatContext; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprGeneratorExp; #[derive(Default)] @@ -23,12 +22,9 @@ impl FormatNodeRule for FormatExprGeneratorExp { impl NeedsParentheses for ExprGeneratorExp { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, context) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, - } + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Never } } diff --git a/crates/ruff_python_formatter/src/expression/expr_if_exp.rs b/crates/ruff_python_formatter/src/expression/expr_if_exp.rs index 85bcff304c..5eb09d3b50 100644 --- a/crates/ruff_python_formatter/src/expression/expr_if_exp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_if_exp.rs @@ -1,11 +1,11 @@ use crate::comments::leading_comments; use crate::expression::parentheses::{ - default_expression_needs_parentheses, in_parentheses_only_group, NeedsParentheses, Parentheses, - Parenthesize, + in_parentheses_only_group, NeedsParentheses, OptionalParentheses, }; use crate::prelude::*; use crate::FormatNodeRule; use ruff_formatter::{format_args, write}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprIfExp; #[derive(Default)] @@ -46,9 +46,9 @@ impl FormatNodeRule for FormatExprIfExp { impl NeedsParentheses for ExprIfExp { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, context) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } diff --git a/crates/ruff_python_formatter/src/expression/expr_joined_str.rs b/crates/ruff_python_formatter/src/expression/expr_joined_str.rs index bd4bcf6fc9..f6a9c4168c 100644 --- a/crates/ruff_python_formatter/src/expression/expr_joined_str.rs +++ b/crates/ruff_python_formatter/src/expression/expr_joined_str.rs @@ -1,9 +1,8 @@ use crate::context::PyFormatContext; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprJoinedStr; #[derive(Default)] @@ -18,9 +17,9 @@ impl FormatNodeRule for FormatExprJoinedStr { impl NeedsParentheses for ExprJoinedStr { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, context) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } diff --git a/crates/ruff_python_formatter/src/expression/expr_lambda.rs b/crates/ruff_python_formatter/src/expression/expr_lambda.rs index 8f2f81e5b3..e631e80061 100644 --- a/crates/ruff_python_formatter/src/expression/expr_lambda.rs +++ b/crates/ruff_python_formatter/src/expression/expr_lambda.rs @@ -1,9 +1,8 @@ use crate::context::PyFormatContext; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprLambda; #[derive(Default)] @@ -23,9 +22,9 @@ impl FormatNodeRule for FormatExprLambda { impl NeedsParentheses for ExprLambda { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, context) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } diff --git a/crates/ruff_python_formatter/src/expression/expr_list.rs b/crates/ruff_python_formatter/src/expression/expr_list.rs index b35abdaa43..b28085fc00 100644 --- a/crates/ruff_python_formatter/src/expression/expr_list.rs +++ b/crates/ruff_python_formatter/src/expression/expr_list.rs @@ -1,11 +1,9 @@ use crate::comments::{dangling_comments, CommentLinePosition}; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, - Parenthesize, -}; +use crate::expression::parentheses::{parenthesized, NeedsParentheses, OptionalParentheses}; use crate::prelude::*; use crate::FormatNodeRule; use ruff_formatter::{format_args, write}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::{ExprList, Ranged}; #[derive(Default)] @@ -71,12 +69,9 @@ impl FormatNodeRule for FormatExprList { impl NeedsParentheses for ExprList { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, context) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, - } + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Never } } 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 a911ecf266..764ff399b1 100644 --- a/crates/ruff_python_formatter/src/expression/expr_list_comp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_list_comp.rs @@ -1,12 +1,10 @@ use crate::context::PyFormatContext; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, - Parenthesize, -}; +use crate::expression::parentheses::{parenthesized, NeedsParentheses, OptionalParentheses}; use crate::prelude::*; use crate::AsFormat; use crate::{FormatNodeRule, PyFormatter}; use ruff_formatter::{format_args, write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprListComp; #[derive(Default)] @@ -44,12 +42,9 @@ impl FormatNodeRule for FormatExprListComp { impl NeedsParentheses for ExprListComp { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, context) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, - } + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Never } } diff --git a/crates/ruff_python_formatter/src/expression/expr_name.rs b/crates/ruff_python_formatter/src/expression/expr_name.rs index ca07981ea1..b3963adb42 100644 --- a/crates/ruff_python_formatter/src/expression/expr_name.rs +++ b/crates/ruff_python_formatter/src/expression/expr_name.rs @@ -1,9 +1,8 @@ -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::prelude::*; use crate::FormatNodeRule; use ruff_formatter::{write, FormatContext}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprName; #[derive(Default)] @@ -28,10 +27,10 @@ impl FormatNodeRule for FormatExprName { impl NeedsParentheses for ExprName { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, context) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Never } } diff --git a/crates/ruff_python_formatter/src/expression/expr_named_expr.rs b/crates/ruff_python_formatter/src/expression/expr_named_expr.rs index e1ba5c1789..4a009b8746 100644 --- a/crates/ruff_python_formatter/src/expression/expr_named_expr.rs +++ b/crates/ruff_python_formatter/src/expression/expr_named_expr.rs @@ -1,10 +1,9 @@ use crate::context::PyFormatContext; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::{AsFormat, FormatNodeRule, PyFormatter}; use ruff_formatter::prelude::{space, text}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprNamedExpr; #[derive(Default)] @@ -33,14 +32,11 @@ impl FormatNodeRule for FormatExprNamedExpr { impl NeedsParentheses for ExprNamedExpr { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, context) { - // Unlike tuples, named expression parentheses are not part of the range even when - // mandatory. See [PEP 572](https://peps.python.org/pep-0572/) for details. - Parentheses::Optional => Parentheses::Always, - parentheses => parentheses, - } + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + // Unlike tuples, named expression parentheses are not part of the range even when + // mandatory. See [PEP 572](https://peps.python.org/pep-0572/) for details. + OptionalParentheses::Always } } diff --git a/crates/ruff_python_formatter/src/expression/expr_set.rs b/crates/ruff_python_formatter/src/expression/expr_set.rs index 3af7376461..83ff228a83 100644 --- a/crates/ruff_python_formatter/src/expression/expr_set.rs +++ b/crates/ruff_python_formatter/src/expression/expr_set.rs @@ -1,10 +1,8 @@ -use crate::expression::parentheses::{ - default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, - Parenthesize, -}; +use crate::expression::parentheses::{parenthesized, NeedsParentheses, OptionalParentheses}; use crate::prelude::*; use crate::FormatNodeRule; use ruff_formatter::format_args; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprSet; #[derive(Default)] @@ -29,12 +27,9 @@ impl FormatNodeRule for FormatExprSet { impl NeedsParentheses for ExprSet { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, context) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, - } + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Never } } diff --git a/crates/ruff_python_formatter/src/expression/expr_set_comp.rs b/crates/ruff_python_formatter/src/expression/expr_set_comp.rs index e661b13386..9588dee66f 100644 --- a/crates/ruff_python_formatter/src/expression/expr_set_comp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_set_comp.rs @@ -1,9 +1,8 @@ use crate::context::PyFormatContext; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprSetComp; #[derive(Default)] @@ -23,12 +22,9 @@ impl FormatNodeRule for FormatExprSetComp { impl NeedsParentheses for ExprSetComp { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, context) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, - } + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Never } } diff --git a/crates/ruff_python_formatter/src/expression/expr_slice.rs b/crates/ruff_python_formatter/src/expression/expr_slice.rs index 1dcf71099b..0d9dd7445f 100644 --- a/crates/ruff_python_formatter/src/expression/expr_slice.rs +++ b/crates/ruff_python_formatter/src/expression/expr_slice.rs @@ -1,14 +1,12 @@ use crate::comments::{dangling_comments, SourceComment}; use crate::context::PyFormatContext; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::trivia::Token; use crate::trivia::{first_non_trivia_token, TokenKind}; use crate::{AsFormat, FormatNodeRule, PyFormatter}; use ruff_formatter::prelude::{hard_line_break, line_suffix_boundary, space, text}; use ruff_formatter::{write, Buffer, Format, FormatError, FormatResult}; -use ruff_python_ast::node::AstNode; +use ruff_python_ast::node::{AnyNodeRef, AstNode}; use ruff_text_size::TextRange; use rustpython_parser::ast::ExprSlice; use rustpython_parser::ast::{Expr, Ranged}; @@ -262,9 +260,9 @@ fn leading_comments_spacing( impl NeedsParentheses for ExprSlice { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, context) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } diff --git a/crates/ruff_python_formatter/src/expression/expr_starred.rs b/crates/ruff_python_formatter/src/expression/expr_starred.rs index e45e445676..6027ef8430 100644 --- a/crates/ruff_python_formatter/src/expression/expr_starred.rs +++ b/crates/ruff_python_formatter/src/expression/expr_starred.rs @@ -1,11 +1,10 @@ use rustpython_parser::ast::ExprStarred; use ruff_formatter::write; +use ruff_python_ast::node::AnyNodeRef; use crate::context::PyFormatContext; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::prelude::*; use crate::FormatNodeRule; @@ -33,9 +32,9 @@ impl FormatNodeRule for FormatExprStarred { impl NeedsParentheses for ExprStarred { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, context) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } diff --git a/crates/ruff_python_formatter/src/expression/expr_subscript.rs b/crates/ruff_python_formatter/src/expression/expr_subscript.rs index 2a5e90c444..a03cc7ec89 100644 --- a/crates/ruff_python_formatter/src/expression/expr_subscript.rs +++ b/crates/ruff_python_formatter/src/expression/expr_subscript.rs @@ -1,15 +1,14 @@ use rustpython_parser::ast::{Expr, ExprSubscript}; use ruff_formatter::{format_args, write}; -use ruff_python_ast::node::AstNode; +use ruff_python_ast::node::{AnyNodeRef, AstNode}; use crate::comments::trailing_comments; use crate::context::NodeLevel; use crate::context::PyFormatContext; use crate::expression::expr_tuple::TupleParentheses; use crate::expression::parentheses::{ - default_expression_needs_parentheses, in_parentheses_only_group, NeedsParentheses, Parentheses, - Parenthesize, + in_parentheses_only_group, NeedsParentheses, OptionalParentheses, }; use crate::prelude::*; use crate::FormatNodeRule; @@ -87,12 +86,11 @@ impl FormatNodeRule for FormatExprSubscript { impl NeedsParentheses for ExprSubscript { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, context) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + { + OptionalParentheses::Never } } } diff --git a/crates/ruff_python_formatter/src/expression/expr_tuple.rs b/crates/ruff_python_formatter/src/expression/expr_tuple.rs index 2fcad27387..e0e5689a9c 100644 --- a/crates/ruff_python_formatter/src/expression/expr_tuple.rs +++ b/crates/ruff_python_formatter/src/expression/expr_tuple.rs @@ -1,15 +1,17 @@ -use crate::builders::parenthesize_if_expands; -use crate::comments::{dangling_comments, CommentLinePosition}; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, parenthesized, NeedsParentheses, Parentheses, - Parenthesize, -}; -use crate::prelude::*; -use ruff_formatter::{format_args, write, FormatRuleWithOptions}; use ruff_text_size::TextRange; use rustpython_parser::ast::ExprTuple; use rustpython_parser::ast::{Expr, Ranged}; +use ruff_formatter::{format_args, write, FormatRuleWithOptions}; +use ruff_python_ast::node::AnyNodeRef; + +use crate::builders::parenthesize_if_expands; +use crate::comments::{dangling_comments, CommentLinePosition}; +use crate::expression::parentheses::{ + parenthesized, NeedsParentheses, OptionalParentheses, Parentheses, +}; +use crate::prelude::*; + #[derive(Eq, PartialEq, Debug, Default)] pub enum TupleParentheses { /// Effectively `None` in `Option` @@ -148,13 +150,10 @@ impl Format> for ExprSequence<'_> { impl NeedsParentheses for ExprTuple { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, context) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, - } + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Never } } diff --git a/crates/ruff_python_formatter/src/expression/expr_unary_op.rs b/crates/ruff_python_formatter/src/expression/expr_unary_op.rs index 8e2655d40b..97462c4d7f 100644 --- a/crates/ruff_python_formatter/src/expression/expr_unary_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_unary_op.rs @@ -1,12 +1,11 @@ use crate::comments::trailing_comments; use crate::context::PyFormatContext; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::trivia::{SimpleTokenizer, TokenKind}; use crate::{AsFormat, FormatNodeRule, PyFormatter}; use ruff_formatter::prelude::{hard_line_break, space, text}; use ruff_formatter::{Format, FormatContext, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use ruff_text_size::{TextLen, TextRange}; use rustpython_parser::ast::UnaryOp; use rustpython_parser::ast::{ExprUnaryOp, Ranged}; @@ -70,19 +69,14 @@ impl FormatNodeRule for FormatExprUnaryOp { impl NeedsParentheses for ExprUnaryOp { fn needs_parentheses( &self, - parenthesize: Parenthesize, + _parent: AnyNodeRef, context: &PyFormatContext, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, context) { - Parentheses::Optional => { - // We preserve the parentheses of the operand. It should not be necessary to break this expression. - if is_operand_parenthesized(self, context.source()) { - Parentheses::Never - } else { - Parentheses::Optional - } - } - parentheses => parentheses, + ) -> OptionalParentheses { + // We preserve the parentheses of the operand. It should not be necessary to break this expression. + if is_operand_parenthesized(self, context.source()) { + OptionalParentheses::Never + } else { + OptionalParentheses::Multiline } } } diff --git a/crates/ruff_python_formatter/src/expression/expr_yield.rs b/crates/ruff_python_formatter/src/expression/expr_yield.rs index a31c56bfbb..20fabe7d05 100644 --- a/crates/ruff_python_formatter/src/expression/expr_yield.rs +++ b/crates/ruff_python_formatter/src/expression/expr_yield.rs @@ -1,9 +1,8 @@ use crate::context::PyFormatContext; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprYield; #[derive(Default)] @@ -18,9 +17,9 @@ impl FormatNodeRule for FormatExprYield { impl NeedsParentheses for ExprYield { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, context) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } diff --git a/crates/ruff_python_formatter/src/expression/expr_yield_from.rs b/crates/ruff_python_formatter/src/expression/expr_yield_from.rs index ae122ded3a..3f2f6b6842 100644 --- a/crates/ruff_python_formatter/src/expression/expr_yield_from.rs +++ b/crates/ruff_python_formatter/src/expression/expr_yield_from.rs @@ -1,9 +1,8 @@ use crate::context::PyFormatContext; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprYieldFrom; #[derive(Default)] @@ -18,9 +17,9 @@ impl FormatNodeRule for FormatExprYieldFrom { impl NeedsParentheses for ExprYieldFrom { fn needs_parentheses( &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, context) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index cfe701d35a..121610b81c 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -1,19 +1,19 @@ -use rustpython_parser::ast; -use rustpython_parser::ast::{Expr, Operator}; use std::cmp::Ordering; -use crate::builders::parenthesize_if_expands; +use rustpython_parser::ast; +use rustpython_parser::ast::{Expr, Operator}; + use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions}; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::visitor::preorder::{walk_expr, PreorderVisitor}; +use crate::builders::parenthesize_if_expands; use crate::context::NodeLevel; use crate::expression::expr_tuple::TupleParentheses; use crate::expression::parentheses::{ is_expression_parenthesized, optional_parentheses, parenthesized, NeedsParentheses, - Parentheses, Parenthesize, + OptionalParentheses, Parentheses, Parenthesize, }; -use crate::expression::string::StringLayout; use crate::prelude::*; pub(crate) mod expr_attribute; @@ -46,25 +46,25 @@ pub(crate) mod expr_yield_from; pub(crate) mod parentheses; pub(crate) mod string; -#[derive(Default)] +#[derive(Copy, Clone, PartialEq, Eq, Default)] pub struct FormatExpr { - parenthesize: Parenthesize, + parentheses: Parentheses, } impl FormatRuleWithOptions> for FormatExpr { - type Options = Parenthesize; + type Options = Parentheses; fn with_options(mut self, options: Self::Options) -> Self { - self.parenthesize = options; + self.parentheses = options; self } } impl FormatRule> for FormatExpr { - fn fmt(&self, item: &Expr, f: &mut PyFormatter) -> FormatResult<()> { - let parentheses = item.needs_parentheses(self.parenthesize, f.context()); + fn fmt(&self, expression: &Expr, f: &mut PyFormatter) -> FormatResult<()> { + let parentheses = self.parentheses; - let format_expr = format_with(|f| match item { + let format_expr = format_with(|f| match expression { Expr::BoolOp(expr) => expr.format().with_options(Some(parentheses)).fmt(f), Expr::NamedExpr(expr) => expr.format().fmt(f), Expr::BinOp(expr) => expr.format().with_options(Some(parentheses)).fmt(f), @@ -84,10 +84,7 @@ impl FormatRule> for FormatExpr { Expr::Call(expr) => expr.format().fmt(f), Expr::FormattedValue(expr) => expr.format().fmt(f), Expr::JoinedStr(expr) => expr.format().fmt(f), - Expr::Constant(expr) => expr - .format() - .with_options(StringLayout::Default(Some(parentheses))) - .fmt(f), + Expr::Constant(expr) => expr.format().fmt(f), Expr::Attribute(expr) => expr.format().fmt(f), Expr::Subscript(expr) => expr.format().fmt(f), Expr::Starred(expr) => expr.format().fmt(f), @@ -100,74 +97,136 @@ impl FormatRule> for FormatExpr { Expr::Slice(expr) => expr.format().fmt(f), }); - let result = match parentheses { - Parentheses::Always => parenthesized("(", &format_expr, ")").fmt(f), - // Add optional parentheses. Ignore if the item renders parentheses itself. - Parentheses::Optional => { - if can_omit_optional_parentheses(item, f.context()) { - optional_parentheses(&format_expr).fmt(f) - } else { - parenthesize_if_expands(&format_expr).fmt(f) - } - } - Parentheses::Custom | Parentheses::Never => { - let saved_level = f.context().node_level(); - - let new_level = match saved_level { - NodeLevel::TopLevel | NodeLevel::CompoundStatement => { - NodeLevel::Expression(None) - } - level @ (NodeLevel::Expression(_) | NodeLevel::ParenthesizedExpression) => { - level - } - }; - - f.context_mut().set_node_level(new_level); - - let result = Format::fmt(&format_expr, f); - f.context_mut().set_node_level(saved_level); - result + let parenthesize = match parentheses { + Parentheses::Preserve => { + is_expression_parenthesized(AnyNodeRef::from(expression), f.context().source()) } + Parentheses::Always => true, + Parentheses::Never => false, }; - result + if parenthesize { + parenthesized("(", &format_expr, ")").fmt(f) + } else { + let saved_level = match f.context().node_level() { + saved_level @ (NodeLevel::TopLevel | NodeLevel::CompoundStatement) => { + f.context_mut().set_node_level(NodeLevel::Expression(None)); + Some(saved_level) + } + NodeLevel::Expression(_) | NodeLevel::ParenthesizedExpression => None, + }; + + let result = Format::fmt(&format_expr, f); + + if let Some(saved_level) = saved_level { + f.context_mut().set_node_level(saved_level); + } + + result + } + } +} + +/// Wraps an expression in an optional parentheses except if its [`NeedsParentheses::needs_parentheses`] implementation +/// indicates that it is okay to omit the parentheses. For example, parentheses can always be omitted for lists, +/// because they already bring their own parentheses. +pub(crate) fn maybe_parenthesize_expression<'a, T>( + expression: &'a Expr, + parent: T, + parenthesize: Parenthesize, +) -> MaybeParenthesizeExpression<'a> +where + T: Into>, +{ + MaybeParenthesizeExpression { + expression, + parent: parent.into(), + parenthesize, + } +} + +pub(crate) struct MaybeParenthesizeExpression<'a> { + expression: &'a Expr, + parent: AnyNodeRef<'a>, + parenthesize: Parenthesize, +} + +impl Format> for MaybeParenthesizeExpression<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let MaybeParenthesizeExpression { + expression, + parent, + parenthesize, + } = self; + + let parenthesize = match parenthesize { + Parenthesize::Optional => { + is_expression_parenthesized(AnyNodeRef::from(*expression), f.context().source()) + } + Parenthesize::IfBreaks => false, + }; + + let parentheses = + if parenthesize || f.context().comments().has_leading_comments(*expression) { + OptionalParentheses::Always + } else { + expression.needs_parentheses(*parent, f.context()) + }; + + match parentheses { + OptionalParentheses::Multiline => { + if can_omit_optional_parentheses(expression, f.context()) { + optional_parentheses(&expression.format().with_options(Parentheses::Never)) + .fmt(f) + } else { + parenthesize_if_expands(&expression.format().with_options(Parentheses::Never)) + .fmt(f) + } + } + OptionalParentheses::Always => { + expression.format().with_options(Parentheses::Always).fmt(f) + } + OptionalParentheses::Never => { + expression.format().with_options(Parentheses::Never).fmt(f) + } + } } } impl NeedsParentheses for Expr { fn needs_parentheses( &self, - parenthesize: Parenthesize, + parent: AnyNodeRef, context: &PyFormatContext, - ) -> Parentheses { + ) -> OptionalParentheses { match self { - Expr::BoolOp(expr) => expr.needs_parentheses(parenthesize, context), - Expr::NamedExpr(expr) => expr.needs_parentheses(parenthesize, context), - Expr::BinOp(expr) => expr.needs_parentheses(parenthesize, context), - Expr::UnaryOp(expr) => expr.needs_parentheses(parenthesize, context), - Expr::Lambda(expr) => expr.needs_parentheses(parenthesize, context), - Expr::IfExp(expr) => expr.needs_parentheses(parenthesize, context), - Expr::Dict(expr) => expr.needs_parentheses(parenthesize, context), - Expr::Set(expr) => expr.needs_parentheses(parenthesize, context), - Expr::ListComp(expr) => expr.needs_parentheses(parenthesize, context), - Expr::SetComp(expr) => expr.needs_parentheses(parenthesize, context), - Expr::DictComp(expr) => expr.needs_parentheses(parenthesize, context), - Expr::GeneratorExp(expr) => expr.needs_parentheses(parenthesize, context), - Expr::Await(expr) => expr.needs_parentheses(parenthesize, context), - Expr::Yield(expr) => expr.needs_parentheses(parenthesize, context), - Expr::YieldFrom(expr) => expr.needs_parentheses(parenthesize, context), - Expr::Compare(expr) => expr.needs_parentheses(parenthesize, context), - Expr::Call(expr) => expr.needs_parentheses(parenthesize, context), - Expr::FormattedValue(expr) => expr.needs_parentheses(parenthesize, context), - Expr::JoinedStr(expr) => expr.needs_parentheses(parenthesize, context), - Expr::Constant(expr) => expr.needs_parentheses(parenthesize, context), - Expr::Attribute(expr) => expr.needs_parentheses(parenthesize, context), - Expr::Subscript(expr) => expr.needs_parentheses(parenthesize, context), - Expr::Starred(expr) => expr.needs_parentheses(parenthesize, context), - Expr::Name(expr) => expr.needs_parentheses(parenthesize, context), - Expr::List(expr) => expr.needs_parentheses(parenthesize, context), - Expr::Tuple(expr) => expr.needs_parentheses(parenthesize, context), - Expr::Slice(expr) => expr.needs_parentheses(parenthesize, context), + Expr::BoolOp(expr) => expr.needs_parentheses(parent, context), + Expr::NamedExpr(expr) => expr.needs_parentheses(parent, context), + Expr::BinOp(expr) => expr.needs_parentheses(parent, context), + Expr::UnaryOp(expr) => expr.needs_parentheses(parent, context), + Expr::Lambda(expr) => expr.needs_parentheses(parent, context), + Expr::IfExp(expr) => expr.needs_parentheses(parent, context), + Expr::Dict(expr) => expr.needs_parentheses(parent, context), + Expr::Set(expr) => expr.needs_parentheses(parent, context), + Expr::ListComp(expr) => expr.needs_parentheses(parent, context), + Expr::SetComp(expr) => expr.needs_parentheses(parent, context), + Expr::DictComp(expr) => expr.needs_parentheses(parent, context), + Expr::GeneratorExp(expr) => expr.needs_parentheses(parent, context), + Expr::Await(expr) => expr.needs_parentheses(parent, context), + Expr::Yield(expr) => expr.needs_parentheses(parent, context), + Expr::YieldFrom(expr) => expr.needs_parentheses(parent, context), + Expr::Compare(expr) => expr.needs_parentheses(parent, context), + Expr::Call(expr) => expr.needs_parentheses(parent, context), + Expr::FormattedValue(expr) => expr.needs_parentheses(parent, context), + Expr::JoinedStr(expr) => expr.needs_parentheses(parent, context), + Expr::Constant(expr) => expr.needs_parentheses(parent, context), + Expr::Attribute(expr) => expr.needs_parentheses(parent, context), + Expr::Subscript(expr) => expr.needs_parentheses(parent, context), + Expr::Starred(expr) => expr.needs_parentheses(parent, context), + Expr::Name(expr) => expr.needs_parentheses(parent, context), + Expr::List(expr) => expr.needs_parentheses(parent, context), + Expr::Tuple(expr) => expr.needs_parentheses(parent, context), + Expr::Slice(expr) => expr.needs_parentheses(parent, context), } } } diff --git a/crates/ruff_python_formatter/src/expression/parentheses.rs b/crates/ruff_python_formatter/src/expression/parentheses.rs index 2d387d5f46..80405ebcf2 100644 --- a/crates/ruff_python_formatter/src/expression/parentheses.rs +++ b/crates/ruff_python_formatter/src/expression/parentheses.rs @@ -6,101 +6,50 @@ use ruff_formatter::{format_args, Argument, Arguments}; use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::Ranged; -pub(crate) trait NeedsParentheses { - fn needs_parentheses( - &self, - parenthesize: Parenthesize, - context: &PyFormatContext, - ) -> Parentheses; +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub(crate) enum OptionalParentheses { + /// Add parentheses if the expression expands over multiple lines + Multiline, + + /// Always set parentheses regardless if the expression breaks or if they were + /// present in the source. + Always, + + /// Never add parentheses + Never, } -pub(super) fn default_expression_needs_parentheses( - node: AnyNodeRef, - parenthesize: Parenthesize, - context: &PyFormatContext, -) -> Parentheses { - debug_assert!( - node.is_expression(), - "Should only be called for expressions" - ); - - #[allow(clippy::if_same_then_else)] - if parenthesize.is_always() { - Parentheses::Always - } else if parenthesize.is_never() { - Parentheses::Never - } - // `Optional` or `Preserve` and expression has parentheses in source code. - else if !parenthesize.is_if_breaks() && is_expression_parenthesized(node, context.source()) { - Parentheses::Always - } - // `Optional` or `IfBreaks`: Add parentheses if the expression doesn't fit on a line but enforce - // parentheses if the expression has leading comments - else if !parenthesize.is_preserve() { - if context.comments().has_leading_comments(node) { - Parentheses::Always - } else { - Parentheses::Optional - } - } else { - //`Preserve` and expression has no parentheses in the source code - Parentheses::Never - } +pub(crate) trait NeedsParentheses { + /// Determines if this object needs optional parentheses or if it is safe to omit the parentheses. + fn needs_parentheses( + &self, + parent: AnyNodeRef, + context: &PyFormatContext, + ) -> OptionalParentheses; } /// Configures if the expression should be parenthesized. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub enum Parenthesize { - /// Parenthesize the expression if it has parenthesis in the source. - #[default] - Preserve, - +#[derive(Copy, Clone, Debug, PartialEq)] +pub(crate) enum Parenthesize { /// Parenthesizes the expression if it doesn't fit on a line OR if the expression is parenthesized in the source code. Optional, /// Parenthesizes the expression only if it doesn't fit on a line. IfBreaks, - - /// Always adds parentheses - Always, - - /// Never adds parentheses. Parentheses are handled by the caller. - Never, -} - -impl Parenthesize { - pub(crate) const fn is_always(self) -> bool { - matches!(self, Parenthesize::Always) - } - - pub(crate) const fn is_never(self) -> bool { - matches!(self, Parenthesize::Never) - } - - pub(crate) const fn is_if_breaks(self) -> bool { - matches!(self, Parenthesize::IfBreaks) - } - - pub(crate) const fn is_preserve(self) -> bool { - matches!(self, Parenthesize::Preserve) - } } /// Whether it is necessary to add parentheses around an expression. /// This is different from [`Parenthesize`] in that it is the resolved representation: It takes into account /// whether there are parentheses in the source code or not. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] pub enum Parentheses { + #[default] + Preserve, + /// Always set parentheses regardless if the expression breaks or if they were /// present in the source. Always, - /// Only add parentheses when necessary because the expression breaks over multiple lines. - Optional, - - /// Custom handling by the node's formatter implementation - Custom, - /// Never add parentheses Never, } @@ -182,20 +131,20 @@ impl<'ast> Format> for FormatParenthesized<'_, 'ast> { /// a parentheses (`()`, `[]`, `{}`). pub(crate) fn optional_parentheses<'content, 'ast, Content>( content: &'content Content, -) -> OptionalParentheses<'content, 'ast> +) -> FormatOptionalParentheses<'content, 'ast> where Content: Format>, { - OptionalParentheses { + FormatOptionalParentheses { content: Argument::new(content), } } -pub(crate) struct OptionalParentheses<'content, 'ast> { +pub(crate) struct FormatOptionalParentheses<'content, 'ast> { content: Argument<'content, PyFormatContext<'ast>>, } -impl<'ast> Format> for OptionalParentheses<'_, 'ast> { +impl<'ast> Format> for FormatOptionalParentheses<'_, 'ast> { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { let saved_level = f.context().node_level(); diff --git a/crates/ruff_python_formatter/src/expression/string.rs b/crates/ruff_python_formatter/src/expression/string.rs index 34ffebae7d..fab95539f2 100644 --- a/crates/ruff_python_formatter/src/expression/string.rs +++ b/crates/ruff_python_formatter/src/expression/string.rs @@ -1,41 +1,27 @@ -use crate::builders::parenthesize_if_expands; -use crate::comments::{leading_comments, trailing_comments}; -use crate::expression::parentheses::Parentheses; -use crate::prelude::*; -use crate::QuoteStyle; +use std::borrow::Cow; + use bitflags::bitflags; -use ruff_formatter::{format_args, write, FormatError}; -use ruff_python_ast::str::is_implicit_concatenation; use ruff_text_size::{TextLen, TextRange, TextSize}; use rustpython_parser::ast::{ExprConstant, Ranged}; use rustpython_parser::lexer::{lex_starts_at, LexicalError, LexicalErrorType}; use rustpython_parser::{Mode, Tok}; -use std::borrow::Cow; -#[derive(Copy, Clone, Debug)] -pub enum StringLayout { - Default(Option), +use ruff_formatter::{format_args, write, FormatError}; +use ruff_python_ast::str::is_implicit_concatenation; - /// Enforces that implicit continuation strings are printed on a single line even if they exceed - /// the configured line width. - Flat, -} - -impl Default for StringLayout { - fn default() -> Self { - Self::Default(None) - } -} +use crate::comments::{leading_comments, trailing_comments}; +use crate::expression::parentheses::in_parentheses_only_group; +use crate::prelude::*; +use crate::QuoteStyle; pub(super) struct FormatString<'a> { constant: &'a ExprConstant, - layout: StringLayout, } impl<'a> FormatString<'a> { - pub(super) fn new(constant: &'a ExprConstant, layout: StringLayout) -> Self { + pub(super) fn new(constant: &'a ExprConstant) -> Self { debug_assert!(constant.value.is_str()); - Self { constant, layout } + Self { constant } } } @@ -45,13 +31,7 @@ impl<'a> Format> for FormatString<'a> { let string_content = f.context().locator().slice(string_range); if is_implicit_concatenation(string_content) { - let format_continuation = FormatStringContinuation::new(self.constant, self.layout); - - if let StringLayout::Default(Some(Parentheses::Custom)) = self.layout { - parenthesize_if_expands(&format_continuation).fmt(f) - } else { - format_continuation.fmt(f) - } + in_parentheses_only_group(&FormatStringContinuation::new(self.constant)).fmt(f) } else { FormatStringPart::new(string_range).fmt(f) } @@ -60,13 +40,12 @@ impl<'a> Format> for FormatString<'a> { struct FormatStringContinuation<'a> { constant: &'a ExprConstant, - layout: StringLayout, } impl<'a> FormatStringContinuation<'a> { - fn new(constant: &'a ExprConstant, layout: StringLayout) -> Self { + fn new(constant: &'a ExprConstant) -> Self { debug_assert!(constant.value.is_str()); - Self { constant, layout } + Self { constant } } } @@ -85,12 +64,7 @@ impl Format> for FormatStringContinuation<'_> { // because this is a black preview style. let lexer = lex_starts_at(string_content, Mode::Expression, string_range.start()); - let separator = format_with(|f| match self.layout { - StringLayout::Default(_) => soft_line_break_or_space().fmt(f), - StringLayout::Flat => space().fmt(f), - }); - - let mut joiner = f.join_with(separator); + let mut joiner = f.join_with(soft_line_break_or_space()); for token in lexer { let (token, token_range) = match token { @@ -220,7 +194,7 @@ impl Format> for FormatStringPart { bitflags! { #[derive(Copy, Clone, Debug)] - struct StringPrefix: u8 { + pub(super) struct StringPrefix: u8 { const UNICODE = 0b0000_0001; /// `r"test"` const RAW = 0b0000_0010; @@ -232,7 +206,7 @@ bitflags! { } impl StringPrefix { - fn parse(input: &str) -> StringPrefix { + pub(super) fn parse(input: &str) -> StringPrefix { let chars = input.chars(); let mut prefix = StringPrefix::empty(); @@ -257,7 +231,7 @@ impl StringPrefix { prefix } - const fn text_len(self) -> TextSize { + pub(super) const fn text_len(self) -> TextSize { TextSize::new(self.bits().count_ones()) } } @@ -383,13 +357,13 @@ fn preferred_quotes( } #[derive(Copy, Clone, Debug)] -struct StringQuotes { +pub(super) struct StringQuotes { triple: bool, style: QuoteStyle, } impl StringQuotes { - fn parse(input: &str) -> Option { + pub(super) fn parse(input: &str) -> Option { let mut chars = input.chars(); let quote_char = chars.next()?; @@ -400,6 +374,10 @@ impl StringQuotes { Some(Self { triple, style }) } + pub(super) const fn is_triple(self) -> bool { + self.triple + } + const fn text_len(self) -> TextSize { if self.triple { TextSize::new(3) diff --git a/crates/ruff_python_formatter/src/other/decorator.rs b/crates/ruff_python_formatter/src/other/decorator.rs index 3ebdca4e70..88b1a92d65 100644 --- a/crates/ruff_python_formatter/src/other/decorator.rs +++ b/crates/ruff_python_formatter/src/other/decorator.rs @@ -1,3 +1,4 @@ +use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; use crate::FormatNodeRule; @@ -18,7 +19,7 @@ impl FormatNodeRule for FormatDecorator { f, [ text("@"), - expression.format().with_options(Parenthesize::Optional) + maybe_parenthesize_expression(expression, item, Parenthesize::Optional) ] ) } diff --git a/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs b/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs index 66ab84be59..a66fd39963 100644 --- a/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs +++ b/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs @@ -1,4 +1,5 @@ use crate::comments::trailing_comments; +use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; use crate::{FormatNodeRule, PyFormatter}; @@ -60,7 +61,10 @@ impl FormatNodeRule for FormatExceptHandlerExceptHan if let Some(type_) = type_ { write!( f, - [space(), type_.format().with_options(Parenthesize::IfBreaks)] + [ + space(), + maybe_parenthesize_expression(type_, item, Parenthesize::IfBreaks) + ] )?; if let Some(name) = name { write!(f, [space(), text("as"), space(), name.format()])?; diff --git a/crates/ruff_python_formatter/src/other/with_item.rs b/crates/ruff_python_formatter/src/other/with_item.rs index 0c3f696e27..1004639f15 100644 --- a/crates/ruff_python_formatter/src/other/with_item.rs +++ b/crates/ruff_python_formatter/src/other/with_item.rs @@ -1,3 +1,4 @@ +use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; use crate::{FormatNodeRule, PyFormatter}; @@ -18,7 +19,11 @@ impl FormatNodeRule for FormatWithItem { let inner = format_with(|f| { write!( f, - [context_expr.format().with_options(Parenthesize::IfBreaks)] + [maybe_parenthesize_expression( + context_expr, + item, + Parenthesize::IfBreaks + )] )?; if let Some(optional_vars) = optional_vars { write!(f, [space(), text("as"), space(), optional_vars.format()])?; diff --git a/crates/ruff_python_formatter/src/statement/stmt_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_assign.rs index e8f1175a3e..36a53891b5 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_assign.rs @@ -2,6 +2,7 @@ use rustpython_parser::ast::StmtAssign; use ruff_formatter::write; +use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; use crate::FormatNodeRule; @@ -26,6 +27,13 @@ impl FormatNodeRule for FormatStmtAssign { write!(f, [target.format(), space(), text("="), space()])?; } - write!(f, [value.format().with_options(Parenthesize::IfBreaks)]) + write!( + f, + [maybe_parenthesize_expression( + value, + item, + Parenthesize::IfBreaks + )] + ) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_async_function_def.rs b/crates/ruff_python_formatter/src/statement/stmt_async_function_def.rs index a0e28aa381..1a001d91b7 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_async_function_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_async_function_def.rs @@ -1,7 +1,9 @@ +use rustpython_parser::ast::StmtAsyncFunctionDef; + +use ruff_python_ast::function::AnyFunctionDefinition; + use crate::prelude::*; use crate::FormatNodeRule; -use ruff_python_ast::function::AnyFunctionDefinition; -use rustpython_parser::ast::StmtAsyncFunctionDef; #[derive(Default)] pub struct FormatStmtAsyncFunctionDef; diff --git a/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs index c09c58ed62..6682ef547b 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs @@ -1,3 +1,4 @@ +use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::{AsFormat, FormatNodeRule, PyFormatter}; use ruff_formatter::prelude::{space, text}; @@ -23,7 +24,7 @@ impl FormatNodeRule for FormatStmtAugAssign { op.format(), text("="), space(), - value.format().with_options(Parenthesize::IfBreaks) + maybe_parenthesize_expression(value, item, Parenthesize::IfBreaks) ] ) } diff --git a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs index ab6d4be00e..bd903d8b39 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs @@ -1,5 +1,6 @@ use crate::comments::trailing_comments; -use crate::expression::parentheses::Parenthesize; + +use crate::expression::parentheses::Parentheses; use crate::prelude::*; use crate::trivia::{SimpleTokenizer, TokenKind}; use ruff_formatter::{format_args, write}; @@ -100,13 +101,13 @@ impl Format> for FormatInheritanceClause<'_> { .count(); // Ignore the first parentheses count - let parenthesize = if left_paren_count > 1 { - Parenthesize::Always + let parentheses = if left_paren_count > 1 { + Parentheses::Always } else { - Parenthesize::Never + Parentheses::Never }; - joiner.entry(first, &first.format().with_options(parenthesize)); + joiner.entry(first, &first.format().with_options(parentheses)); joiner.nodes(rest.iter()); } diff --git a/crates/ruff_python_formatter/src/statement/stmt_delete.rs b/crates/ruff_python_formatter/src/statement/stmt_delete.rs index 3b5d3225fc..a61e86c028 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_delete.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_delete.rs @@ -1,7 +1,8 @@ use crate::builders::{parenthesize_if_expands, PyFormatterExtensions}; use crate::comments::dangling_node_comments; +use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; -use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use crate::{FormatNodeRule, PyFormatter}; use ruff_formatter::prelude::{block_indent, format_with, space, text}; use ruff_formatter::{write, Buffer, Format, FormatResult}; use rustpython_parser::ast::{Ranged, StmtDelete}; @@ -32,7 +33,14 @@ impl FormatNodeRule for FormatStmtDelete { ) } [single] => { - write!(f, [single.format().with_options(Parenthesize::IfBreaks)]) + write!( + f, + [maybe_parenthesize_expression( + single, + item, + Parenthesize::IfBreaks + )] + ) } targets => { let item = format_with(|f| { diff --git a/crates/ruff_python_formatter/src/statement/stmt_expr.rs b/crates/ruff_python_formatter/src/statement/stmt_expr.rs index 4e415f77cf..0eb13b2493 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_expr.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_expr.rs @@ -1,8 +1,9 @@ -use crate::expression::parentheses::{is_expression_parenthesized, Parenthesize}; -use crate::expression::string::StringLayout; +use rustpython_parser::ast::StmtExpr; + +use crate::expression::maybe_parenthesize_expression; +use crate::expression::parentheses::Parenthesize; use crate::prelude::*; use crate::FormatNodeRule; -use rustpython_parser::ast::StmtExpr; #[derive(Default)] pub struct FormatStmtExpr; @@ -11,14 +12,6 @@ impl FormatNodeRule for FormatStmtExpr { fn fmt_fields(&self, item: &StmtExpr, f: &mut PyFormatter) -> FormatResult<()> { let StmtExpr { value, .. } = item; - if let Some(constant) = value.as_constant_expr() { - if constant.value.is_str() - && !is_expression_parenthesized(value.as_ref().into(), f.context().source()) - { - return constant.format().with_options(StringLayout::Flat).fmt(f); - } - } - - value.format().with_options(Parenthesize::Optional).fmt(f) + maybe_parenthesize_expression(value, item, Parenthesize::Optional).fmt(f) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_for.rs b/crates/ruff_python_formatter/src/statement/stmt_for.rs index 528e06f152..b03283cabc 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_for.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_for.rs @@ -1,5 +1,6 @@ use crate::comments::{leading_alternate_branch_comments, trailing_comments}; use crate::expression::expr_tuple::TupleParentheses; +use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; use crate::{FormatNodeRule, PyFormatter}; @@ -17,7 +18,7 @@ impl Format> for ExprTupleWithoutParentheses<'_> { .format() .with_options(TupleParentheses::StripInsideForLoop) .fmt(f), - other => other.format().with_options(Parenthesize::IfBreaks).fmt(f), + other => maybe_parenthesize_expression(other, self.0, Parenthesize::IfBreaks).fmt(f), } } } @@ -54,7 +55,7 @@ impl FormatNodeRule for FormatStmtFor { space(), text("in"), space(), - iter.format().with_options(Parenthesize::IfBreaks), + maybe_parenthesize_expression(iter, item, Parenthesize::IfBreaks), text(":"), trailing_comments(trailing_condition_comments), block_indent(&body.format()) diff --git a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs index a0079b4975..69f370e7c1 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs @@ -1,12 +1,14 @@ +use rustpython_parser::ast::{Ranged, StmtFunctionDef}; + +use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule}; +use ruff_python_ast::function::AnyFunctionDefinition; + use crate::comments::{leading_comments, trailing_comments}; use crate::context::NodeLevel; -use crate::expression::parentheses::{optional_parentheses, Parenthesize}; +use crate::expression::parentheses::{optional_parentheses, Parentheses}; use crate::prelude::*; use crate::trivia::{lines_after, skip_trailing_trivia}; use crate::FormatNodeRule; -use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule}; -use ruff_python_ast::function::AnyFunctionDefinition; -use rustpython_parser::ast::{Ranged, StmtFunctionDef}; #[derive(Default)] pub struct FormatStmtFunctionDef; @@ -98,7 +100,7 @@ impl FormatRule, PyFormatContext<'_>> for FormatAnyFun text("->"), space(), optional_parentheses( - &return_annotation.format().with_options(Parenthesize::Never) + &return_annotation.format().with_options(Parentheses::Never) ) ] )?; diff --git a/crates/ruff_python_formatter/src/statement/stmt_if.rs b/crates/ruff_python_formatter/src/statement/stmt_if.rs index 5611a66ac3..ab249f228e 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_if.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_if.rs @@ -1,4 +1,5 @@ use crate::comments::{leading_alternate_branch_comments, trailing_comments, SourceComment}; +use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; use crate::FormatNodeRule; @@ -48,7 +49,7 @@ impl FormatNodeRule for FormatStmtIf { [ text(current.keyword()), space(), - test.format().with_options(Parenthesize::IfBreaks), + maybe_parenthesize_expression(test, current_statement, Parenthesize::IfBreaks), text(":"), trailing_comments(if_trailing_comments), block_indent(&body.format()) diff --git a/crates/ruff_python_formatter/src/statement/stmt_raise.rs b/crates/ruff_python_formatter/src/statement/stmt_raise.rs index 5159d3ab35..78c8591b5a 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_raise.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_raise.rs @@ -1,8 +1,9 @@ use crate::expression::parentheses::Parenthesize; -use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use crate::{FormatNodeRule, PyFormatter}; use ruff_formatter::prelude::{space, text}; use ruff_formatter::{write, Buffer, Format, FormatResult}; +use crate::expression::maybe_parenthesize_expression; use rustpython_parser::ast::StmtRaise; #[derive(Default)] @@ -21,7 +22,10 @@ impl FormatNodeRule for FormatStmtRaise { if let Some(value) = exc { write!( f, - [space(), value.format().with_options(Parenthesize::Optional)] + [ + space(), + maybe_parenthesize_expression(value, item, Parenthesize::Optional) + ] )?; } @@ -32,7 +36,7 @@ impl FormatNodeRule for FormatStmtRaise { space(), text("from"), space(), - value.format().with_options(Parenthesize::Optional) + maybe_parenthesize_expression(value, item, Parenthesize::Optional) ] )?; } diff --git a/crates/ruff_python_formatter/src/statement/stmt_return.rs b/crates/ruff_python_formatter/src/statement/stmt_return.rs index db9b28b0d4..c298379c47 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_return.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_return.rs @@ -1,5 +1,6 @@ +use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; -use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use crate::{FormatNodeRule, PyFormatter}; use ruff_formatter::prelude::{space, text}; use ruff_formatter::{write, Buffer, Format, FormatResult}; use rustpython_parser::ast::StmtReturn; @@ -16,7 +17,7 @@ impl FormatNodeRule for FormatStmtReturn { [ text("return"), space(), - value.format().with_options(Parenthesize::IfBreaks) + maybe_parenthesize_expression(value, item, Parenthesize::IfBreaks) ] ) } else { diff --git a/crates/ruff_python_formatter/src/statement/stmt_while.rs b/crates/ruff_python_formatter/src/statement/stmt_while.rs index 758f1d840e..d89e02fb85 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_while.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_while.rs @@ -1,4 +1,5 @@ use crate::comments::{leading_alternate_branch_comments, trailing_comments}; +use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; use crate::FormatNodeRule; @@ -33,7 +34,7 @@ impl FormatNodeRule for FormatStmtWhile { [ text("while"), space(), - test.format().with_options(Parenthesize::IfBreaks), + maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), text(":"), trailing_comments(trailing_condition_comments), block_indent(&body.format()) 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 6355a154f5..5aca69b3a6 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 @@ -304,17 +304,6 @@ long_unmergable_string_with_pragma = ( ```diff --- Black +++ Ruff -@@ -70,8 +70,8 @@ - bad_split3 = ( - "What if we have inline comments on " # First Comment - "each line of a bad split? In that " # Second Comment -- "case, we should just leave it alone." # Third Comment --) -+ "case, we should just leave it alone." -+) # Third Comment - - bad_split_func1( - "But what should happen when code has already " @@ -143,9 +143,9 @@ ) ) @@ -458,8 +447,8 @@ bad_split2 = ( bad_split3 = ( "What if we have inline comments on " # First Comment "each line of a bad split? In that " # Second Comment - "case, we should just leave it alone." -) # Third Comment + "case, we should just leave it alone." # Third Comment +) bad_split_func1( "But what should happen when code has already " 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 7a1948330f..b7752423a8 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 @@ -122,11 +122,10 @@ def foo3(list_a, list_b): - .order_by(User.created_at.desc()) - .with_for_update(key_share=True) - .all() -- ) -+ filter -+ )(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))).order_by( -+ User.created_at.desc() -+ ).with_for_update(key_share=True).all() ++ filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))).order_by( ++ User.created_at.desc() ++ ).with_for_update(key_share=True).all() + ) return results @@ -224,10 +223,10 @@ def foo(list_a, list_b): db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) ).filter(User.xyz.is_(None)). # Another comment about the filtering on is_quux goes here. - filter - )(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))).order_by( - User.created_at.desc() - ).with_for_update(key_share=True).all() + filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))).order_by( + User.created_at.desc() + ).with_for_update(key_share=True).all() + ) return results 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 4f4798dc42..ad164bd6c6 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 @@ -398,10 +398,11 @@ last_call() (*starred,) { "id": "1", -@@ -208,24 +208,14 @@ +@@ -207,25 +207,15 @@ + ) 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( @@ -409,7 +410,7 @@ last_call() - ) - .order_by(models.Customer.id.asc()) - .all() --) + ) -result = ( - session.query(models.Customer.id) - .filter( @@ -446,7 +447,7 @@ last_call() async def f(): -@@ -248,18 +238,22 @@ +@@ -248,18 +238,20 @@ print(*[] or [1]) @@ -471,13 +472,11 @@ last_call() for y in (): ... -for z in (i for i in (1, 2, 3)): -+for ( -+ z -+) in (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []): ++for z in (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []): ... for i in call(): ... -@@ -328,13 +322,18 @@ +@@ -328,13 +320,18 @@ ): return True if ( @@ -499,7 +498,7 @@ last_call() ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n ): return True -@@ -342,7 +341,8 @@ +@@ -342,7 +339,8 @@ ~aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e @@ -767,9 +766,7 @@ for (x,) in (1,), (2,), (3,): ... for y in (): ... -for ( - z -) in (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key 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__fmtonoff5.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff5.py.snap index 9cd5e7ed31..99e98844ff 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 @@ -96,18 +96,21 @@ elif unformatted: ```diff --- Black +++ Ruff -@@ -5,8 +5,8 @@ +@@ -3,10 +3,9 @@ + entry_points={ + # fmt: off "console_scripts": [ - "foo-bar" - "=foo.bar.:main", +- "foo-bar" +- "=foo.bar.:main", - # fmt: on - ] # Includes an formatted indentation. ++ "foo-bar" "=foo.bar.:main", + # fmt: on + ] # Includes an formatted indentation. }, ) -@@ -27,7 +27,7 @@ +@@ -27,7 +26,7 @@ # Regression test for https://github.com/psf/black/issues/3026. def test_func(): # yapf: disable @@ -116,7 +119,7 @@ elif unformatted: return True # yapf: enable elif b: -@@ -39,10 +39,10 @@ +@@ -39,10 +38,10 @@ # Regression test for https://github.com/psf/black/issues/2567. if True: # fmt: off @@ -131,7 +134,7 @@ elif unformatted: else: print("This will be formatted") -@@ -52,14 +52,12 @@ +@@ -52,14 +51,12 @@ async def call(param): if param: # fmt: off @@ -149,7 +152,7 @@ elif unformatted: print("This will be formatted") -@@ -68,13 +66,13 @@ +@@ -68,13 +65,13 @@ class Named(t.Protocol): # fmt: off @property @@ -165,7 +168,7 @@ elif unformatted: # fmt: on -@@ -82,6 +80,6 @@ +@@ -82,6 +79,6 @@ if x: return x # fmt: off @@ -183,8 +186,7 @@ setup( entry_points={ # fmt: off "console_scripts": [ - "foo-bar" - "=foo.bar.:main", + "foo-bar" "=foo.bar.:main", # fmt: on ] # Includes an formatted indentation. }, 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 9dea01bb94..890bfd41cb 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 @@ -476,9 +476,9 @@ if ( # Unstable formatting in https://github.com/realtyem/synapse-unraid/blob/unraid_develop/synapse/handlers/presence.py -for user_id in ( - set(target_user_ids) - {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} -): +for user_id in set( + target_user_ids +) - {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set}: updates.append(UserPresenceState.default(user_id)) # Keeps parenthesized left hand sides diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap index 53afe8b2a3..72128eca3a 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap @@ -203,7 +203,20 @@ String \"\"\" "Let's" "start" "with" "a" "simple" "example" -"Let's" "start" "with" "a" "simple" "example" "now repeat after me:" "I am confident" "I am confident" "I am confident" "I am confident" "I am confident" +( + "Let's" + "start" + "with" + "a" + "simple" + "example" + "now repeat after me:" + "I am confident" + "I am confident" + "I am confident" + "I am confident" + "I am confident" +) ( "Let's" @@ -351,7 +364,20 @@ String \"\"\" "Let's" 'start' 'with' 'a' 'simple' 'example' -"Let's" 'start' 'with' 'a' 'simple' 'example' 'now repeat after me:' 'I am confident' 'I am confident' 'I am confident' 'I am confident' 'I am confident' +( + "Let's" + 'start' + 'with' + 'a' + 'simple' + 'example' + 'now repeat after me:' + 'I am confident' + 'I am confident' + 'I am confident' + 'I am confident' + 'I am confident' +) ( "Let's" diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__for.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__for.py.snap index b350ab75f4..637372d81f 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__for.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__for.py.snap @@ -55,11 +55,7 @@ else: # trailing else comment # trailing else body comment -for ( - aVeryLongNameThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn -) in ( - anotherVeryLongNameThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn -): # trailing comment +for aVeryLongNameThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn in anotherVeryLongNameThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn: # trailing comment pass else: diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__raise.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__raise.py.snap index f9e1808758..a90849f43b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__raise.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__raise.py.snap @@ -105,9 +105,7 @@ raise a from OsError( ) # some comment -raise a from ( - aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa -) # some comment +raise a from aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa # some comment # some comment raise OsError( diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__while.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__while.py.snap index 9014bcdd53..fdc99df9a6 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__while.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__while.py.snap @@ -51,9 +51,7 @@ else: # trailing else comment # trailing else body comment -while ( - aVeryLongConditionThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn -): # trailing comment +while aVeryLongConditionThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn: # trailing comment pass else: 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 9f1f6eded7..2d133fe87b 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 @@ -82,18 +82,14 @@ with ( with ( - ( - a # a # comma - ), + a, # a # comma b, # c ): # colon ... with ( - ( - a # a # as - ) as b, # b # comma + a as b, # a # as # b # comma c, # c ): # colon ... # body From e9771c9c63bfea30bfd18d85c718b3038f8fb192 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 13 Jul 2023 13:26:47 +0530 Subject: [PATCH 441/447] Ignore Jupyter Notebooks for `--add-noqa` (#5727) --- crates/ruff_cli/src/commands/add_noqa.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_cli/src/commands/add_noqa.rs b/crates/ruff_cli/src/commands/add_noqa.rs index 0ee7027f97..06cc9b13f1 100644 --- a/crates/ruff_cli/src/commands/add_noqa.rs +++ b/crates/ruff_cli/src/commands/add_noqa.rs @@ -9,7 +9,7 @@ use rayon::prelude::*; use ruff::linter::add_noqa_to_path; use ruff::resolver::PyprojectConfig; use ruff::{packaging, resolver, warn_user_once}; -use ruff_python_stdlib::path::is_project_toml; +use ruff_python_stdlib::path::{is_jupyter_notebook, is_project_toml}; use crate::args::Overrides; @@ -47,7 +47,7 @@ pub(crate) fn add_noqa( .flatten() .filter_map(|entry| { let path = entry.path(); - if is_project_toml(path) { + if is_project_toml(path) || is_jupyter_notebook(path) { return None; } let package = path From 68e0f97354db845b5d51d54d47c76dc1231b4a52 Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 13 Jul 2023 11:27:25 +0200 Subject: [PATCH 442/447] Formatter: Better f-string dummy (#5730) ## Summary The previous dummy was causing instabilities since it turned a string into a variable. E.g. ```python script_header_dict[ "slurm_partition_line" ] = f"#SBATCH --partition {resources.queue_name}" ``` has an instability as ```python - script_header_dict["slurm_partition_line"] = ( - NOT_YET_IMPLEMENTED_ExprJoinedStr - ) + script_header_dict[ + "slurm_partition_line" + ] = NOT_YET_IMPLEMENTED_ExprJoinedStr ``` ## Test Plan The instability is gone, otherwise it's still a dummy --- .../src/expression/expr_joined_str.rs | 11 +++-- ...ility@miscellaneous__debug_visitor.py.snap | 32 +++++++++----- ...ng_preview_no_string_normalization.py.snap | 4 +- ...aneous__long_strings_flag_disabled.py.snap | 12 ++--- ...ility@miscellaneous__string_quotes.py.snap | 44 +++++++++---------- ...y@py_310__pattern_matching_generic.py.snap | 20 ++++----- ...ty@simple_cases__docstring_preview.py.snap | 16 +++---- ...mpatibility@simple_cases__fmtonoff.py.snap | 4 +- ...ompatibility@simple_cases__fstring.py.snap | 40 ++++++++--------- ...mpatibility@simple_cases__function.py.snap | 4 +- ...move_newline_after_code_block_open.py.snap | 12 ++--- ...bility@simple_cases__remove_parens.py.snap | 4 +- ...lity@simple_cases__string_prefixes.py.snap | 36 +++++++-------- 13 files changed, 127 insertions(+), 112 deletions(-) diff --git a/crates/ruff_python_formatter/src/expression/expr_joined_str.rs b/crates/ruff_python_formatter/src/expression/expr_joined_str.rs index f6a9c4168c..cf54ebffcb 100644 --- a/crates/ruff_python_formatter/src/expression/expr_joined_str.rs +++ b/crates/ruff_python_formatter/src/expression/expr_joined_str.rs @@ -1,6 +1,6 @@ use crate::context::PyFormatContext; use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; +use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprJoinedStr; @@ -9,8 +9,13 @@ use rustpython_parser::ast::ExprJoinedStr; pub struct FormatExprJoinedStr; impl FormatNodeRule for FormatExprJoinedStr { - fn fmt_fields(&self, item: &ExprJoinedStr, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + fn fmt_fields(&self, _item: &ExprJoinedStr, f: &mut PyFormatter) -> FormatResult<()> { + write!( + f, + [not_yet_implemented_custom_text( + r#"f"NOT_YET_IMPLEMENTED_ExprJoinedStr""# + )] + ) } } diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap index 784fe8d950..3dba6a699c 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap @@ -44,7 +44,7 @@ class DebugVisitor(Visitor[T]): ```diff --- Black +++ Ruff -@@ -3,24 +3,24 @@ +@@ -3,24 +3,29 @@ tree_depth: int = 0 def visit_default(self, node: LN) -> Iterator[T]: @@ -53,7 +53,7 @@ class DebugVisitor(Visitor[T]): if isinstance(node, Node): _type = type_repr(node.type) - out(f'{indent}{_type}', fg='yellow') -+ out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="yellow") ++ out(f"NOT_YET_IMPLEMENTED_ExprJoinedStr", fg="yellow") self.tree_depth += 1 for child in node.children: - yield from self.visit(child) @@ -61,18 +61,23 @@ class DebugVisitor(Visitor[T]): self.tree_depth -= 1 - out(f'{indent}/{_type}', fg='yellow', bold=False) -+ out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="yellow", bold=False) ++ out(f"NOT_YET_IMPLEMENTED_ExprJoinedStr", fg="yellow", bold=False) else: _type = token.tok_name.get(node.type, str(node.type)) - out(f'{indent}{_type}', fg='blue', nl=False) -+ out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="blue", nl=False) ++ out(f"NOT_YET_IMPLEMENTED_ExprJoinedStr", fg="blue", nl=False) if node.prefix: # We don't have to handle prefixes for `Node` objects since # that delegates to the first child anyway. - out(f' {node.prefix!r}', fg='green', bold=False, nl=False) - out(f' {node.value!r}', fg='blue', bold=False) -+ out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="green", bold=False, nl=False) -+ out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="blue", bold=False) ++ out( ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr", ++ fg="green", ++ bold=False, ++ nl=False, ++ ) ++ out(f"NOT_YET_IMPLEMENTED_ExprJoinedStr", fg="blue", bold=False) @classmethod def show(cls, code: str) -> None: @@ -89,21 +94,26 @@ class DebugVisitor(Visitor[T]): indent = " " * (2 * self.tree_depth) if isinstance(node, Node): _type = type_repr(node.type) - out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="yellow") + out(f"NOT_YET_IMPLEMENTED_ExprJoinedStr", fg="yellow") self.tree_depth += 1 for child in node.children: NOT_YET_IMPLEMENTED_ExprYieldFrom self.tree_depth -= 1 - out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="yellow", bold=False) + out(f"NOT_YET_IMPLEMENTED_ExprJoinedStr", fg="yellow", bold=False) else: _type = token.tok_name.get(node.type, str(node.type)) - out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="blue", nl=False) + out(f"NOT_YET_IMPLEMENTED_ExprJoinedStr", fg="blue", nl=False) if node.prefix: # We don't have to handle prefixes for `Node` objects since # that delegates to the first child anyway. - out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="green", bold=False, nl=False) - out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="blue", bold=False) + out( + f"NOT_YET_IMPLEMENTED_ExprJoinedStr", + fg="green", + bold=False, + nl=False, + ) + out(f"NOT_YET_IMPLEMENTED_ExprJoinedStr", fg="blue", bold=False) @classmethod def show(cls, code: str) -> None: diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_preview_no_string_normalization.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_preview_no_string_normalization.py.snap index 52233c973b..0a617572c9 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_preview_no_string_normalization.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_preview_no_string_normalization.py.snap @@ -27,7 +27,7 @@ def do_not_touch_this_prefix3(): def do_not_touch_this_prefix2(): - FR'There was a bug where docstring prefixes would be normalized even with -S.' -+ NOT_YET_IMPLEMENTED_ExprJoinedStr ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def do_not_touch_this_prefix3(): @@ -43,7 +43,7 @@ def do_not_touch_this_prefix(): def do_not_touch_this_prefix2(): - NOT_YET_IMPLEMENTED_ExprJoinedStr + f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def do_not_touch_this_prefix3(): 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 5aca69b3a6..fe648573cc 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 @@ -309,10 +309,10 @@ long_unmergable_string_with_pragma = ( ) -fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." -+fstring = NOT_YET_IMPLEMENTED_ExprJoinedStr ++fstring = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" -fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." -+fstring_with_no_fexprs = NOT_YET_IMPLEMENTED_ExprJoinedStr ++fstring_with_no_fexprs = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" 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. @@ -366,7 +366,7 @@ long_unmergable_string_with_pragma = ( -x = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four." -+x = NOT_YET_IMPLEMENTED_ExprJoinedStr ++x = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" long_unmergable_string_with_pragma = ( "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore @@ -520,9 +520,9 @@ old_fmt_string3 = ( ) ) -fstring = NOT_YET_IMPLEMENTED_ExprJoinedStr +fstring = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" -fstring_with_no_fexprs = NOT_YET_IMPLEMENTED_ExprJoinedStr +fstring_with_no_fexprs = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" 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. @@ -639,7 +639,7 @@ def foo(): NOT_YET_IMPLEMENTED_ExprYield -x = NOT_YET_IMPLEMENTED_ExprJoinedStr +x = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" long_unmergable_string_with_pragma = ( "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__string_quotes.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__string_quotes.py.snap index 9fa5227c0a..f2d2835ccc 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__string_quotes.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__string_quotes.py.snap @@ -77,10 +77,10 @@ f"\"{a}\"{'hello' * b}\"{c}\"" -f"""This is a triple-quoted {f}-string""" -f'MOAR {" ".join([])}' -f"MOAR {' '.join([])}" -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" r"raw string ftw" -r"Date d\'expiration:(.*)" +r"Date d'expiration:(.*)" @@ -89,7 +89,7 @@ f"\"{a}\"{'hello' * b}\"{c}\"" -rf"{yay}" -"\nThe \"quick\"\nbrown fox\njumps over\nthe 'lazy' dog.\n" +r'Not-so-tricky "quote' -+NOT_YET_IMPLEMENTED_ExprJoinedStr ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +"\n\ +The \"quick\"\n\ +brown fox\n\ @@ -107,10 +107,10 @@ f"\"{a}\"{'hello' * b}\"{c}\"" -f"{{y * \" \"}} '{z}'" -f'\'{z}\' {y * " "}' -f"{y * x} '{z}'" -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" "'{z}' {y * \" \"}" "{y * x} '{z}'" @@ -118,8 +118,8 @@ f"\"{a}\"{'hello' * b}\"{c}\"" # expressions. xref: https://github.com/psf/black/issues/2348 -f"\"{b}\"{' ' * (long-len(b)+1)}: \"{sts}\",\n" -f"\"{a}\"{'hello' * b}\"{c}\"" -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ``` ## Ruff Output @@ -142,15 +142,15 @@ f"\"{a}\"{'hello' * b}\"{c}\"" """Here's a " """ """Just a normal triple quote""" -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" r"raw string ftw" r"Date d'expiration:(.*)" r'Tricky "quote' r'Not-so-tricky "quote' -NOT_YET_IMPLEMENTED_ExprJoinedStr +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" "\n\ The \"quick\"\n\ brown fox\n\ @@ -171,17 +171,17 @@ re.compile(r'[\\"]') '\\""' "\\''" "Lots of \\\\\\\\'quotes'" -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" "'{z}' {y * \" \"}" "{y * x} '{z}'" # We must bail out if changing the quotes would introduce backslashes in f-string # expressions. xref: https://github.com/psf/black/issues/2348 -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ``` ## Black Output 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 7c2a145131..6e8d106770 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 @@ -124,13 +124,13 @@ with match() as match: match = a with match() as match: - match = f"{match}" -+ match = NOT_YET_IMPLEMENTED_ExprJoinedStr ++ match = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" re.match() match = a with match() as match: - match = f"{match}" -+ match = NOT_YET_IMPLEMENTED_ExprJoinedStr ++ match = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: @@ -157,7 +157,7 @@ with match() as match: match = a with match() as match: - match = f"{match}" -+ match = NOT_YET_IMPLEMENTED_ExprJoinedStr ++ match = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def test_patma_139(self): x = False @@ -192,13 +192,13 @@ with match() as match: match = a with match() as match: - match = f"{match}" -+ match = NOT_YET_IMPLEMENTED_ExprJoinedStr ++ match = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" re.match() match = a with match() as match: - match = f"{match}" -+ match = NOT_YET_IMPLEMENTED_ExprJoinedStr ++ match = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ``` ## Ruff Output @@ -207,12 +207,12 @@ with match() as match: re.match() match = a with match() as match: - match = NOT_YET_IMPLEMENTED_ExprJoinedStr + match = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" re.match() match = a with match() as match: - match = NOT_YET_IMPLEMENTED_ExprJoinedStr + match = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: @@ -245,7 +245,7 @@ def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: re.match() match = a with match() as match: - match = NOT_YET_IMPLEMENTED_ExprJoinedStr + match = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def test_patma_139(self): x = False @@ -297,12 +297,12 @@ def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) - re.match() match = a with match() as match: - match = NOT_YET_IMPLEMENTED_ExprJoinedStr + match = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" re.match() match = a with match() as match: - match = NOT_YET_IMPLEMENTED_ExprJoinedStr + match = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring_preview.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring_preview.py.snap index 3101537b9d..d297799051 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring_preview.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring_preview.py.snap @@ -71,7 +71,7 @@ def single_quote_docstring_over_line_limit2(): def docstring_almost_at_line_limit_with_prefix(): - f"""long docstring................................................................""" -+ NOT_YET_IMPLEMENTED_ExprJoinedStr ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def mulitline_docstring_almost_at_line_limit(): @@ -83,7 +83,7 @@ def single_quote_docstring_over_line_limit2(): - - .................................................................................. - """ -+ NOT_YET_IMPLEMENTED_ExprJoinedStr ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def docstring_at_line_limit(): @@ -92,7 +92,7 @@ def single_quote_docstring_over_line_limit2(): def docstring_at_line_limit_with_prefix(): - f"""long docstring...............................................................""" -+ NOT_YET_IMPLEMENTED_ExprJoinedStr ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def multiline_docstring_at_line_limit(): @@ -103,7 +103,7 @@ def single_quote_docstring_over_line_limit2(): - f"""first line---------------------------------------------------------------------- - - second line----------------------------------------------------------------------""" -+ NOT_YET_IMPLEMENTED_ExprJoinedStr ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def single_quote_docstring_over_line_limit(): @@ -118,7 +118,7 @@ def docstring_almost_at_line_limit(): def docstring_almost_at_line_limit_with_prefix(): - NOT_YET_IMPLEMENTED_ExprJoinedStr + f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def mulitline_docstring_almost_at_line_limit(): @@ -129,7 +129,7 @@ def mulitline_docstring_almost_at_line_limit(): def mulitline_docstring_almost_at_line_limit_with_prefix(): - NOT_YET_IMPLEMENTED_ExprJoinedStr + f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def docstring_at_line_limit(): @@ -137,7 +137,7 @@ def docstring_at_line_limit(): def docstring_at_line_limit_with_prefix(): - NOT_YET_IMPLEMENTED_ExprJoinedStr + f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def multiline_docstring_at_line_limit(): @@ -147,7 +147,7 @@ def multiline_docstring_at_line_limit(): def multiline_docstring_at_line_limit_with_prefix(): - NOT_YET_IMPLEMENTED_ExprJoinedStr + f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def single_quote_docstring_over_line_limit(): 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 218da54c3e..2aa1ef102d 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 @@ -207,7 +207,7 @@ d={'a':1, +from third_party import X, Y, Z # fmt: on -f"trigger 3.6 mode" -+NOT_YET_IMPLEMENTED_ExprJoinedStr ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" # Comment 1 # Comment 2 @@ -407,7 +407,7 @@ from library import some_connection, some_decorator # fmt: off from third_party import X, Y, Z # fmt: on -NOT_YET_IMPLEMENTED_ExprJoinedStr +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" # Comment 1 # Comment 2 diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fstring.py.snap index aa8a125ac7..7282dc5589 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fstring.py.snap @@ -35,31 +35,31 @@ but none started with prefix {parentdir_prefix}" -f'Hello \'{tricky + "example"}\'' -f"Tried directories {str(rootdirs)} \ -but none started with prefix {parentdir_prefix}" -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ``` ## Ruff Output ```py -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ``` ## Black Output 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 169478f47f..219714a500 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 @@ -113,7 +113,7 @@ def __await__(): return (yield) from library import some_connection, some_decorator - -f"trigger 3.6 mode" -+NOT_YET_IMPLEMENTED_ExprJoinedStr ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def func_no_args(): @@ -188,7 +188,7 @@ import sys from third_party import X, Y, Z from library import some_connection, some_decorator -NOT_YET_IMPLEMENTED_ExprJoinedStr +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def func_no_args(): 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 c97f2a11d6..a47f6ddb56 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 @@ -125,18 +125,18 @@ with open("/path/to/file.txt", mode="r") as read_file: for i in range(5): - print(f"{i}) The line above me should be removed!") -+ print(NOT_YET_IMPLEMENTED_ExprJoinedStr) ++ print(f"NOT_YET_IMPLEMENTED_ExprJoinedStr") for i in range(5): - print(f"{i}) The lines above me should be removed!") -+ print(NOT_YET_IMPLEMENTED_ExprJoinedStr) ++ print(f"NOT_YET_IMPLEMENTED_ExprJoinedStr") for i in range(5): for j in range(7): - print(f"{i}) The lines above me should be removed!") -+ print(NOT_YET_IMPLEMENTED_ExprJoinedStr) ++ print(f"NOT_YET_IMPLEMENTED_ExprJoinedStr") if random.randint(0, 3) == 0: @@ -174,16 +174,16 @@ class Foo: for i in range(5): - print(NOT_YET_IMPLEMENTED_ExprJoinedStr) + print(f"NOT_YET_IMPLEMENTED_ExprJoinedStr") for i in range(5): - print(NOT_YET_IMPLEMENTED_ExprJoinedStr) + print(f"NOT_YET_IMPLEMENTED_ExprJoinedStr") for i in range(5): for j in range(7): - print(NOT_YET_IMPLEMENTED_ExprJoinedStr) + print(f"NOT_YET_IMPLEMENTED_ExprJoinedStr") if random.randint(0, 3) == 0: diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_parens.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_parens.py.snap index 2ba7a34a5a..b7afd62f41 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_parens.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_parens.py.snap @@ -74,7 +74,7 @@ def example8(): - data = ( - f"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - ).encode() -+ data = (NOT_YET_IMPLEMENTED_ExprJoinedStr).encode() ++ data = (f"NOT_YET_IMPLEMENTED_ExprJoinedStr").encode() except Exception as e: pass @@ -151,7 +151,7 @@ async def show_status(): while True: try: if report_host: - data = (NOT_YET_IMPLEMENTED_ExprJoinedStr).encode() + data = (f"NOT_YET_IMPLEMENTED_ExprJoinedStr").encode() except Exception as e: pass diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__string_prefixes.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__string_prefixes.py.snap index 93c5a7f651..5d5f5028ad 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__string_prefixes.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__string_prefixes.py.snap @@ -38,7 +38,7 @@ def docstring_multiline(): name = "Łukasz" -(f"hello {name}", f"hello {name}") -(b"", b"") -+(NOT_YET_IMPLEMENTED_ExprJoinedStr, NOT_YET_IMPLEMENTED_ExprJoinedStr) ++(f"NOT_YET_IMPLEMENTED_ExprJoinedStr", f"NOT_YET_IMPLEMENTED_ExprJoinedStr") +(b"NOT_YET_IMPLEMENTED_BYTE_STRING", b"NOT_YET_IMPLEMENTED_BYTE_STRING") ("", "") (r"", R"") @@ -46,14 +46,14 @@ def docstring_multiline(): -(rf"", rf"", Rf"", Rf"", rf"", rf"", Rf"", Rf"") -(rb"", rb"", Rb"", Rb"", rb"", rb"", Rb"", Rb"") +( -+ NOT_YET_IMPLEMENTED_ExprJoinedStr, -+ NOT_YET_IMPLEMENTED_ExprJoinedStr, -+ NOT_YET_IMPLEMENTED_ExprJoinedStr, -+ NOT_YET_IMPLEMENTED_ExprJoinedStr, -+ NOT_YET_IMPLEMENTED_ExprJoinedStr, -+ NOT_YET_IMPLEMENTED_ExprJoinedStr, -+ NOT_YET_IMPLEMENTED_ExprJoinedStr, -+ NOT_YET_IMPLEMENTED_ExprJoinedStr, ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr", ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr", ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr", ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr", ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr", ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr", ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr", ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr", +) +( + b"NOT_YET_IMPLEMENTED_BYTE_STRING", @@ -76,20 +76,20 @@ def docstring_multiline(): #!/usr/bin/env python3 name = "Łukasz" -(NOT_YET_IMPLEMENTED_ExprJoinedStr, NOT_YET_IMPLEMENTED_ExprJoinedStr) +(f"NOT_YET_IMPLEMENTED_ExprJoinedStr", f"NOT_YET_IMPLEMENTED_ExprJoinedStr") (b"NOT_YET_IMPLEMENTED_BYTE_STRING", b"NOT_YET_IMPLEMENTED_BYTE_STRING") ("", "") (r"", R"") ( - NOT_YET_IMPLEMENTED_ExprJoinedStr, - NOT_YET_IMPLEMENTED_ExprJoinedStr, - NOT_YET_IMPLEMENTED_ExprJoinedStr, - NOT_YET_IMPLEMENTED_ExprJoinedStr, - NOT_YET_IMPLEMENTED_ExprJoinedStr, - NOT_YET_IMPLEMENTED_ExprJoinedStr, - NOT_YET_IMPLEMENTED_ExprJoinedStr, - NOT_YET_IMPLEMENTED_ExprJoinedStr, + f"NOT_YET_IMPLEMENTED_ExprJoinedStr", + f"NOT_YET_IMPLEMENTED_ExprJoinedStr", + f"NOT_YET_IMPLEMENTED_ExprJoinedStr", + f"NOT_YET_IMPLEMENTED_ExprJoinedStr", + f"NOT_YET_IMPLEMENTED_ExprJoinedStr", + f"NOT_YET_IMPLEMENTED_ExprJoinedStr", + f"NOT_YET_IMPLEMENTED_ExprJoinedStr", + f"NOT_YET_IMPLEMENTED_ExprJoinedStr", ) ( b"NOT_YET_IMPLEMENTED_BYTE_STRING", From b1781abffb1d603529b1d8713d4306b1ad8ced08 Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 13 Jul 2023 12:42:09 +0200 Subject: [PATCH 443/447] Link issue tracker in contributing docs (#5688) ## Summary This adds links to issue categories that are good for people looking to implement something and a link to the contributing guide feedback issue (https://github.com/astral-sh/ruff/issues/5684) --------- Co-authored-by: Zanie --- CONTRIBUTING.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 87cf63b9e6..6864a7ecd1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,9 @@ For small changes (e.g., bug fixes), feel free to submit a PR. For larger changes (e.g., new lint rules, new functionality, new configuration options), consider creating an [**issue**](https://github.com/astral-sh/ruff/issues) outlining your proposed change. You can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5) to discuss your idea with the -community. +community. We have labeled [beginner-friendly tasks in the issue tracker](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) +as well as [bugs](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Abug) and +[improvements that are ready for contributions](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Aaccepted). If you're looking for a place to start, we recommend implementing a new lint rule (see: [_Adding a new lint rule_](#example-adding-a-new-lint-rule), which will allow you to learn from and @@ -34,6 +36,8 @@ As a concrete example: consider taking on one of the rules from the [`flake8-pyi plugin, and looking to the originating [Python source](https://github.com/PyCQA/flake8-pyi) for guidance. +If you have suggestions on how we might improve the contributing documentation, [let us know](https://github.com/astral-sh/ruff/discussions/5693)! + ### Prerequisites Ruff is written in Rust. You'll need to install the From 549173b3959e5becbe723bc8a85078a9ea604cc4 Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 13 Jul 2023 12:51:25 +0200 Subject: [PATCH 444/447] Fix `StmtAnnAssign` formatting by mirroring `StmtAssign` (#5732) ## Summary `StmtAnnAssign` would not insert parentheses when breaking the same way `StmtAssign` does, causing unstable formatting and likely some syntax errors. ## Test Plan I added a regression test. --- .../fixtures/ruff/statement/ann_assign.py | 10 +++++--- .../fixtures/ruff/statement/aug_assign.py | 5 ++++ .../src/statement/stmt_ann_assign.rs | 19 ++++++++++++-- .../format@statement__ann_assign.py.snap | 25 ++++++++++++------- .../format@statement__aug_assign.py.snap | 25 +++++++++++++++++++ 5 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/aug_assign.py create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@statement__aug_assign.py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/ann_assign.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/ann_assign.py index 6c9d3155f6..b965199c98 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/ann_assign.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/ann_assign.py @@ -1,5 +1,7 @@ -tree_depth += 1 +# Regression test: Don't forget the parentheses in the value when breaking +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: int = a + 1 * a -greeting += "This is very long, formal greeting for whomever is name here. Dear %s, it will break the line" % len( - name -) + +# Regression test: Don't forget the parentheses in the annotation when breaking +class DefaultRunner: + task_runner_cls: TaskRunnerProtocol | typing.Callable[[], typing.Any] = DefaultTaskRunner diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/aug_assign.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/aug_assign.py new file mode 100644 index 0000000000..6c9d3155f6 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/aug_assign.py @@ -0,0 +1,5 @@ +tree_depth += 1 + +greeting += "This is very long, formal greeting for whomever is name here. Dear %s, it will break the line" % len( + name +) diff --git a/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs index c9d5cf2fdf..418b4d52c1 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs @@ -1,3 +1,5 @@ +use crate::expression::maybe_parenthesize_expression; +use crate::expression::parentheses::Parenthesize; use crate::prelude::*; use crate::FormatNodeRule; use ruff_formatter::write; @@ -18,11 +20,24 @@ impl FormatNodeRule for FormatStmtAnnAssign { write!( f, - [target.format(), text(":"), space(), annotation.format()] + [ + target.format(), + text(":"), + space(), + maybe_parenthesize_expression(annotation, item, Parenthesize::IfBreaks) + ] )?; if let Some(value) = value { - write!(f, [space(), text("="), space(), value.format()])?; + write!( + f, + [ + space(), + text("="), + space(), + maybe_parenthesize_expression(value, item, Parenthesize::IfBreaks) + ] + )?; } Ok(()) diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap index 9385c1b1ba..52d136a3be 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap @@ -4,21 +4,28 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/ --- ## Input ```py -tree_depth += 1 +# Regression test: Don't forget the parentheses in the value when breaking +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: int = a + 1 * a -greeting += "This is very long, formal greeting for whomever is name here. Dear %s, it will break the line" % len( - name -) + +# Regression test: Don't forget the parentheses in the annotation when breaking +class DefaultRunner: + task_runner_cls: TaskRunnerProtocol | typing.Callable[[], typing.Any] = DefaultTaskRunner ``` ## Output ```py -tree_depth += 1 - -greeting += ( - "This is very long, formal greeting for whomever is name here. Dear %s, it will break the line" - % len(name) +# Regression test: Don't forget the parentheses in the value when breaking +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: int = ( + a + 1 * a ) + + +# Regression test: Don't forget the parentheses in the annotation when breaking +class DefaultRunner: + task_runner_cls: ( + TaskRunnerProtocol | typing.Callable[[], typing.Any] + ) = DefaultTaskRunner ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__aug_assign.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__aug_assign.py.snap new file mode 100644 index 0000000000..715c6cd154 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__aug_assign.py.snap @@ -0,0 +1,25 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/aug_assign.py +--- +## Input +```py +tree_depth += 1 + +greeting += "This is very long, formal greeting for whomever is name here. Dear %s, it will break the line" % len( + name +) +``` + +## Output +```py +tree_depth += 1 + +greeting += ( + "This is very long, formal greeting for whomever is name here. Dear %s, it will break the line" + % len(name) +) +``` + + + From 932c9a47893c5d599e605112c732be737c3bf6f7 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 13 Jul 2023 07:34:04 -0400 Subject: [PATCH 445/447] Extend PEP 604 rewrites to support some quoted annotations (#5725) ## Summary Python doesn't allow `"Foo" | None` if the annotation will be evaluated at runtime (see the comments in the PR, or the semantic model documentation for more on what this means and when it is true), but it _does_ allow it if the annotation is typing-only. This, for example, is invalid, as Python will evaluate `"Foo" | None` at runtime in order to populate the function's `__annotations__`: ```python def f(x: "Foo" | None): ... ``` This, however, is valid: ```python def f(): x: "Foo" | None ``` As is this: ```python from __future__ import annotations def f(x: "Foo" | None): ... ``` Closes #5706. --- .../pyupgrade/rules/use_pep604_annotation.rs | 1 + ...ff__rules__pyupgrade__tests__UP007.py.snap | 16 +++++++++++ .../src/analyze/typing.rs | 28 ++++++++++++++----- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs index b1515b410e..2f2fea0424 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs @@ -60,6 +60,7 @@ pub(crate) fn use_pep604_annotation( // Avoid fixing forward references, or types not in an annotation. let fixable = checker.semantic().in_type_definition() && !checker.semantic().in_complex_string_type_definition(); + match operator { Pep604Operator::Optional => { let mut diagnostic = Diagnostic::new(NonPEP604Annotation, expr.range()); diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP007.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP007.py.snap index dc112cd0e0..250930ff96 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP007.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP007.py.snap @@ -276,4 +276,20 @@ UP007.py:60:8: UP007 [*] Use `X | Y` for type annotations 60 |+ x: str | int 61 61 | x: Union["str", "int"] +UP007.py:61:8: UP007 [*] Use `X | Y` for type annotations + | +59 | x = Union["str", "int"] +60 | x: Union[str, int] +61 | x: Union["str", "int"] + | ^^^^^^^^^^^^^^^^^^^ UP007 + | + = help: Convert to `X | Y` + +ℹ Suggested fix +58 58 | x = Union[str, int] +59 59 | x = Union["str", "int"] +60 60 | x: Union[str, int] +61 |- x: Union["str", "int"] + 61 |+ x: "str" | "int" + diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 4da230cd70..39ea75af86 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -127,22 +127,36 @@ pub fn to_pep604_operator( slice: &Expr, semantic: &SemanticModel, ) -> Option { - /// Returns `true` if any argument in the slice is a string. - fn any_arg_is_str(slice: &Expr) -> bool { + /// Returns `true` if any argument in the slice is a quoted annotation). + fn quoted_annotation(slice: &Expr) -> bool { match slice { Expr::Constant(ast::ExprConstant { value: Constant::Str(_), .. }) => true, - Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().any(any_arg_is_str), + Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().any(quoted_annotation), _ => false, } } - // If any of the _arguments_ are forward references, we can't use PEP 604. - // Ex) `Union["str", "int"]` can't be converted to `"str" | "int"`. - if any_arg_is_str(slice) { - return None; + // If the slice is a forward reference (e.g., `Optional["Foo"]`), it can only be rewritten + // if we're in a typing-only context. + // + // This, for example, is invalid, as Python will evaluate `"Foo" | None` at runtime in order to + // populate the function's `__annotations__`: + // ```python + // def f(x: "Foo" | None): ... + // ``` + // + // This, however, is valid: + // ```python + // def f(): + // x: "Foo" | None + // ``` + if quoted_annotation(slice) { + if semantic.execution_context().is_runtime() { + return None; + } } semantic From 8420008e79d538b1af39b3f39f69856044d31a57 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Thu, 13 Jul 2023 12:36:07 +0100 Subject: [PATCH 446/447] Avoid checking `EXE001` and `EXE002` on WSL (#5735) ## Summary Do not raise `EXE001` and `EXE002` if WSL is detected. Uses the [`wsl`](https://crates.io/crates/wsl) crate. Closes #5445. ## Test Plan `cargo test` I don't use Windows, so was unable to test on a WSL environment. It would be good if someone who runs Windows could check the functionality. --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + crates/ruff/Cargo.toml | 1 + .../src/rules/flake8_executable/rules/shebang_missing.rs | 7 +++++++ .../flake8_executable/rules/shebang_not_executable.rs | 7 +++++++ 5 files changed, 23 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 12414ea02e..f3ce5f85ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1899,6 +1899,7 @@ dependencies = [ "typed-arena", "unicode-width", "unicode_names2", + "wsl", ] [[package]] @@ -3345,6 +3346,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "wsl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dab7ac864710bdea6594becbea5b5050333cf34fefb0dc319567eb347950d4" + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index bec1ee357d..b9f44b1438 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ syn = { version = "2.0.15" } test-case = { version = "3.0.0" } thiserror = { version = "1.0.43" } toml = { version = "0.7.2" } +wsl = { version = "0.1.0" } # v1.0.1 libcst = { git = "https://github.com/Instagram/LibCST.git", rev = "3cacca1a1029f05707e50703b49fe3dd860aa839", default-features = false } diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index eb3e3f2455..b659730d8a 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -78,6 +78,7 @@ toml = { workspace = true } typed-arena = { version = "2.0.2" } unicode-width = { version = "0.1.10" } unicode_names2 = { version = "0.6.0", git = "https://github.com/youknowone/unicode_names2.git", rev = "4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" } +wsl = { version = "0.1.0" } [dev-dependencies] insta = { workspace = true } diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_missing.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_missing.rs index 7cf9daed31..2f1e47f015 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_missing.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_missing.rs @@ -2,6 +2,8 @@ use std::path::Path; +use wsl; + use ruff_text_size::TextRange; use ruff_diagnostics::{Diagnostic, Violation}; @@ -42,6 +44,11 @@ impl Violation for ShebangMissingExecutableFile { /// EXE002 #[cfg(target_family = "unix")] pub(crate) fn shebang_missing(filepath: &Path) -> Option { + // WSL supports Windows file systems, which do not have executable bits. + // Instead, everything is executable. Therefore, we skip this rule on WSL. + if wsl::is_wsl() { + return None; + } if let Ok(true) = is_executable(filepath) { let diagnostic = Diagnostic::new(ShebangMissingExecutableFile, TextRange::default()); return Some(diagnostic); diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs index 56ec4b3249..a1400f0b67 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs @@ -2,6 +2,8 @@ use std::path::Path; +use wsl; + use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::{Diagnostic, Violation}; @@ -48,6 +50,11 @@ pub(crate) fn shebang_not_executable( range: TextRange, shebang: &ShebangDirective, ) -> Option { + // WSL supports Windows file systems, which do not have executable bits. + // Instead, everything is executable. Therefore, we skip this rule on WSL. + if wsl::is_wsl() { + return None; + } let ShebangDirective { offset, contents } = shebang; if let Ok(false) = is_executable(filepath) { From f44acc047aa9a6958a07253b9014c49aba9081d0 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 13 Jul 2023 18:19:27 +0530 Subject: [PATCH 447/447] Check for `Any` in other types for `ANN401` (#5601) ## Summary Check for `Any` in other types for `ANN401`. This reuses the logic from `implicit-optional` rule to resolve the type to `Any`. Following types are supported: * `Union[Any, ...]` * `Any | ...` * `Optional[Any]` * `Annotated[, ...]` * Forward references i.e., `"Any | ..."` ## Test Plan Added test cases for various combinations. fixes: #5458 --- .../flake8_annotations/annotation_presence.py | 26 +- .../flake8_annotations/rules/definition.rs | 78 +++-- ...__flake8_annotations__tests__defaults.snap | 56 +++ crates/ruff/src/rules/ruff/mod.rs | 1 + .../src/rules/ruff/rules/implicit_optional.rs | 272 +-------------- ..._ruff__tests__PY39_RUF013_RUF013_0.py.snap | 36 -- ...ules__ruff__tests__RUF013_RUF013_0.py.snap | 36 -- crates/ruff/src/rules/ruff/typing.rs | 330 ++++++++++++++++++ scripts/pyproject.toml | 1 + 9 files changed, 458 insertions(+), 378 deletions(-) create mode 100644 crates/ruff/src/rules/ruff/typing.rs diff --git a/crates/ruff/resources/test/fixtures/flake8_annotations/annotation_presence.py b/crates/ruff/resources/test/fixtures/flake8_annotations/annotation_presence.py index d37f178bbb..7778c85072 100644 --- a/crates/ruff/resources/test/fixtures/flake8_annotations/annotation_presence.py +++ b/crates/ruff/resources/test/fixtures/flake8_annotations/annotation_presence.py @@ -1,4 +1,4 @@ -from typing import Any, Type +from typing import Annotated, Any, Optional, Type, Union from typing_extensions import override # Error @@ -95,27 +95,27 @@ class Foo: def foo(self: "Foo", a: int, *params: str, **options: Any) -> int: pass - # ANN401 + # OK @override def foo(self: "Foo", a: Any, *params: str, **options: str) -> int: pass - # ANN401 + # OK @override def foo(self: "Foo", a: int, *params: str, **options: str) -> Any: pass - # ANN401 + # OK @override def foo(self: "Foo", a: int, *params: Any, **options: Any) -> int: pass - # ANN401 + # OK @override def foo(self: "Foo", a: int, *params: Any, **options: str) -> int: pass - # ANN401 + # OK @override def foo(self: "Foo", a: int, *params: str, **options: Any) -> int: pass @@ -137,3 +137,17 @@ class Foo: # OK def f(*args: *tuple[int]) -> None: ... +def f(a: object) -> None: ... +def f(a: str | bytes) -> None: ... +def f(a: Union[str, bytes]) -> None: ... +def f(a: Optional[str]) -> None: ... +def f(a: Annotated[str, ...]) -> None: ... +def f(a: "Union[str, bytes]") -> None: ... + +# ANN401 +def f(a: Any | int) -> None: ... +def f(a: int | Any) -> None: ... +def f(a: Union[str, bytes, Any]) -> None: ... +def f(a: Optional[Any]) -> None: ... +def f(a: Annotated[Any, ...]) -> None: ... +def f(a: "Union[str, bytes, Any]") -> None: ... diff --git a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs index 47232b9e04..f672afd9b7 100644 --- a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{ArgWithDefault, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, ArgWithDefault, Constant, Expr, Ranged, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -6,12 +6,14 @@ use ruff_python_ast::cast; use ruff_python_ast::helpers::ReturnStatementVisitor; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::statement_visitor::StatementVisitor; +use ruff_python_ast::typing::parse_type_annotation; use ruff_python_semantic::analyze::visibility; -use ruff_python_semantic::{Definition, Member, MemberKind, SemanticModel}; +use ruff_python_semantic::{Definition, Member, MemberKind}; use ruff_python_stdlib::typing::simple_magic_return_type; use crate::checkers::ast::Checker; use crate::registry::{AsRule, Rule}; +use crate::rules::ruff::typing::type_hint_resolves_to_any; use super::super::fixes; use super::super::helpers::match_function_def; @@ -432,20 +434,46 @@ fn is_none_returning(body: &[Stmt]) -> bool { /// ANN401 fn check_dynamically_typed( + checker: &Checker, annotation: &Expr, func: F, diagnostics: &mut Vec, - is_overridden: bool, - semantic: &SemanticModel, ) where F: FnOnce() -> String, { - if !is_overridden && semantic.match_typing_expr(annotation, "Any") { - diagnostics.push(Diagnostic::new( - AnyType { name: func() }, - annotation.range(), - )); - }; + if let Expr::Constant(ast::ExprConstant { + range, + value: Constant::Str(string), + .. + }) = annotation + { + // Quoted annotations + if let Ok((parsed_annotation, _)) = parse_type_annotation(string, *range, checker.locator) { + if type_hint_resolves_to_any( + &parsed_annotation, + checker.semantic(), + checker.locator, + checker.settings.target_version.minor(), + ) { + diagnostics.push(Diagnostic::new( + AnyType { name: func() }, + annotation.range(), + )); + } + } + } else { + if type_hint_resolves_to_any( + annotation, + checker.semantic(), + checker.locator, + checker.settings.target_version.minor(), + ) { + diagnostics.push(Diagnostic::new( + AnyType { name: func() }, + annotation.range(), + )); + } + } } /// Generate flake8-annotation checks for a given `Definition`. @@ -500,13 +528,12 @@ pub(crate) fn definition( // ANN401 for dynamically typed arguments if let Some(annotation) = &def.annotation { has_any_typed_arg = true; - if checker.enabled(Rule::AnyType) { + if checker.enabled(Rule::AnyType) && !is_overridden { check_dynamically_typed( + checker, annotation, || def.arg.to_string(), &mut diagnostics, - is_overridden, - checker.semantic(), ); } } else { @@ -530,15 +557,9 @@ pub(crate) fn definition( if let Some(expr) = &arg.annotation { has_any_typed_arg = true; if !checker.settings.flake8_annotations.allow_star_arg_any { - if checker.enabled(Rule::AnyType) { + if checker.enabled(Rule::AnyType) && !is_overridden { let name = &arg.arg; - check_dynamically_typed( - expr, - || format!("*{name}"), - &mut diagnostics, - is_overridden, - checker.semantic(), - ); + check_dynamically_typed(checker, expr, || format!("*{name}"), &mut diagnostics); } } } else { @@ -562,14 +583,13 @@ pub(crate) fn definition( if let Some(expr) = &arg.annotation { has_any_typed_arg = true; if !checker.settings.flake8_annotations.allow_star_arg_any { - if checker.enabled(Rule::AnyType) { + if checker.enabled(Rule::AnyType) && !is_overridden { let name = &arg.arg; check_dynamically_typed( + checker, expr, || format!("**{name}"), &mut diagnostics, - is_overridden, - checker.semantic(), ); } } @@ -629,14 +649,8 @@ pub(crate) fn definition( // ANN201, ANN202, ANN401 if let Some(expr) = &returns { has_typed_return = true; - if checker.enabled(Rule::AnyType) { - check_dynamically_typed( - expr, - || name.to_string(), - &mut diagnostics, - is_overridden, - checker.semantic(), - ); + if checker.enabled(Rule::AnyType) && !is_overridden { + check_dynamically_typed(checker, expr, || name.to_string(), &mut diagnostics); } } else if !( // Allow omission of return annotation if the function only returns `None` diff --git a/crates/ruff/src/rules/flake8_annotations/snapshots/ruff__rules__flake8_annotations__tests__defaults.snap b/crates/ruff/src/rules/flake8_annotations/snapshots/ruff__rules__flake8_annotations__tests__defaults.snap index 0eba6a0469..7cd87414d4 100644 --- a/crates/ruff/src/rules/flake8_annotations/snapshots/ruff__rules__flake8_annotations__tests__defaults.snap +++ b/crates/ruff/src/rules/flake8_annotations/snapshots/ruff__rules__flake8_annotations__tests__defaults.snap @@ -186,4 +186,60 @@ annotation_presence.py:134:13: ANN101 Missing type annotation for `self` in meth 135 | pass | +annotation_presence.py:148:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a` + | +147 | # ANN401 +148 | def f(a: Any | int) -> None: ... + | ^^^^^^^^^ ANN401 +149 | def f(a: int | Any) -> None: ... +150 | def f(a: Union[str, bytes, Any]) -> None: ... + | + +annotation_presence.py:149:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a` + | +147 | # ANN401 +148 | def f(a: Any | int) -> None: ... +149 | def f(a: int | Any) -> None: ... + | ^^^^^^^^^ ANN401 +150 | def f(a: Union[str, bytes, Any]) -> None: ... +151 | def f(a: Optional[Any]) -> None: ... + | + +annotation_presence.py:150:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a` + | +148 | def f(a: Any | int) -> None: ... +149 | def f(a: int | Any) -> None: ... +150 | def f(a: Union[str, bytes, Any]) -> None: ... + | ^^^^^^^^^^^^^^^^^^^^^^ ANN401 +151 | def f(a: Optional[Any]) -> None: ... +152 | def f(a: Annotated[Any, ...]) -> None: ... + | + +annotation_presence.py:151:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a` + | +149 | def f(a: int | Any) -> None: ... +150 | def f(a: Union[str, bytes, Any]) -> None: ... +151 | def f(a: Optional[Any]) -> None: ... + | ^^^^^^^^^^^^^ ANN401 +152 | def f(a: Annotated[Any, ...]) -> None: ... +153 | def f(a: "Union[str, bytes, Any]") -> None: ... + | + +annotation_presence.py:152:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a` + | +150 | def f(a: Union[str, bytes, Any]) -> None: ... +151 | def f(a: Optional[Any]) -> None: ... +152 | def f(a: Annotated[Any, ...]) -> None: ... + | ^^^^^^^^^^^^^^^^^^^ ANN401 +153 | def f(a: "Union[str, bytes, Any]") -> None: ... + | + +annotation_presence.py:153:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a` + | +151 | def f(a: Optional[Any]) -> None: ... +152 | def f(a: Annotated[Any, ...]) -> None: ... +153 | def f(a: "Union[str, bytes, Any]") -> None: ... + | ^^^^^^^^^^^^^^^^^^^^^^^^ ANN401 + | + diff --git a/crates/ruff/src/rules/ruff/mod.rs b/crates/ruff/src/rules/ruff/mod.rs index 0c9aded910..79062bc839 100644 --- a/crates/ruff/src/rules/ruff/mod.rs +++ b/crates/ruff/src/rules/ruff/mod.rs @@ -1,6 +1,7 @@ //! Ruff-specific rules. pub(crate) mod rules; +pub(crate) mod typing; #[cfg(test)] mod tests { diff --git a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs index 64fb85c073..d9f03e2699 100644 --- a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs +++ b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs @@ -6,18 +6,16 @@ use rustpython_parser::ast::{self, ArgWithDefault, Arguments, Constant, Expr, Op use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::CallPath; use ruff_python_ast::helpers::is_const_none; -use ruff_python_ast::source_code::Locator; use ruff_python_ast::typing::parse_type_annotation; -use ruff_python_semantic::SemanticModel; -use ruff_python_stdlib::sys::is_known_standard_library; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; use crate::registry::AsRule; use crate::settings::types::PythonVersion; +use super::super::typing::type_hint_explicitly_allows_none; + /// ## What it does /// Checks for the use of implicit `Optional` in type annotations when the /// default parameter value is `None`. @@ -121,231 +119,6 @@ impl From for ConversionType { } } -/// Custom iterator to collect all the `|` separated expressions in a PEP 604 -/// union type. -struct PEP604UnionIterator<'a> { - stack: Vec<&'a Expr>, -} - -impl<'a> PEP604UnionIterator<'a> { - fn new(expr: &'a Expr) -> Self { - Self { stack: vec![expr] } - } -} - -impl<'a> Iterator for PEP604UnionIterator<'a> { - type Item = &'a Expr; - - fn next(&mut self) -> Option { - while let Some(expr) = self.stack.pop() { - match expr { - Expr::BinOp(ast::ExprBinOp { - left, - op: Operator::BitOr, - right, - .. - }) => { - self.stack.push(left); - self.stack.push(right); - } - _ => return Some(expr), - } - } - None - } -} - -/// Returns `true` if the given call path is a known type. -/// -/// A known type is either a builtin type, any object from the standard library, -/// or a type from the `typing_extensions` module. -fn is_known_type(call_path: &CallPath, target_version: PythonVersion) -> bool { - match call_path.as_slice() { - ["" | "typing_extensions", ..] => true, - [module, ..] => is_known_standard_library(target_version.minor(), module), - _ => false, - } -} - -#[derive(Debug)] -enum TypingTarget<'a> { - None, - Any, - Object, - Optional, - ForwardReference(Expr), - Union(Vec<&'a Expr>), - Literal(Vec<&'a Expr>), - Annotated(&'a Expr), -} - -impl<'a> TypingTarget<'a> { - fn try_from_expr( - expr: &'a Expr, - semantic: &SemanticModel, - locator: &Locator, - target_version: PythonVersion, - ) -> Option { - match expr { - Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { - if semantic.match_typing_expr(value, "Optional") { - return Some(TypingTarget::Optional); - } - let Expr::Tuple(ast::ExprTuple { elts: elements, .. }) = slice.as_ref() else { - return None; - }; - if semantic.match_typing_expr(value, "Literal") { - Some(TypingTarget::Literal(elements.iter().collect())) - } else if semantic.match_typing_expr(value, "Union") { - Some(TypingTarget::Union(elements.iter().collect())) - } else if semantic.match_typing_expr(value, "Annotated") { - elements.first().map(TypingTarget::Annotated) - } else { - semantic.resolve_call_path(value).map_or( - // If we can't resolve the call path, it must be defined - // in the same file, so we assume it's `Any` as it could - // be a type alias. - Some(TypingTarget::Any), - |call_path| { - if is_known_type(&call_path, target_version) { - None - } else { - // If it's not a known type, we assume it's `Any`. - Some(TypingTarget::Any) - } - }, - ) - } - } - Expr::BinOp(..) => Some(TypingTarget::Union( - PEP604UnionIterator::new(expr).collect(), - )), - Expr::Constant(ast::ExprConstant { - value: Constant::None, - .. - }) => Some(TypingTarget::None), - Expr::Constant(ast::ExprConstant { - value: Constant::Str(string), - range, - .. - }) => parse_type_annotation(string, *range, locator) - // In case of a parse error, we return `Any` to avoid false positives. - .map_or(Some(TypingTarget::Any), |(expr, _)| { - Some(TypingTarget::ForwardReference(expr)) - }), - _ => semantic.resolve_call_path(expr).map_or( - // If we can't resolve the call path, it must be defined in the - // same file, so we assume it's `Any` as it could be a type alias. - Some(TypingTarget::Any), - |call_path| { - if semantic.match_typing_call_path(&call_path, "Any") { - Some(TypingTarget::Any) - } else if matches!(call_path.as_slice(), ["" | "builtins", "object"]) { - Some(TypingTarget::Object) - } else if !is_known_type(&call_path, target_version) { - // If it's not a known type, we assume it's `Any`. - Some(TypingTarget::Any) - } else { - None - } - }, - ), - } - } - - /// Check if the [`TypingTarget`] explicitly allows `None`. - fn contains_none( - &self, - semantic: &SemanticModel, - locator: &Locator, - target_version: PythonVersion, - ) -> bool { - match self { - TypingTarget::None - | TypingTarget::Optional - | 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 { - 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) - } - _ => false, - } - }), - TypingTarget::Union(elements) => elements.iter().any(|element| { - 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 { - 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 { - return false; - }; - new_target.contains_none(semantic, locator, target_version) - } - } - } -} - -/// Check if the given annotation [`Expr`] explicitly allows `None`. -/// -/// This function will return `None` if the annotation explicitly allows `None` -/// otherwise it will return the annotation itself. If it's a `Annotated` type, -/// then the inner type will be checked. -/// -/// This function assumes that the annotation is a valid typing annotation expression. -fn type_hint_explicitly_allows_none<'a>( - annotation: &'a Expr, - semantic: &SemanticModel, - locator: &Locator, - target_version: PythonVersion, -) -> Option<&'a Expr> { - let Some(target) = TypingTarget::try_from_expr(annotation, semantic, locator, target_version) - else { - return Some(annotation); - }; - match target { - // Short circuit on top level `None`, `Any` or `Optional` - TypingTarget::None | TypingTarget::Optional | TypingTarget::Any => None, - // Top-level `Annotated` node should check for the inner type and - // return the inner type if it doesn't allow `None`. If `Annotated` - // is found nested inside another type, then the outer type should - // be returned. - TypingTarget::Annotated(expr) => { - type_hint_explicitly_allows_none(expr, semantic, locator, target_version) - } - _ => { - if target.contains_none(semantic, locator, target_version) { - None - } else { - Some(annotation) - } - } - } -} - /// Generate a [`Fix`] for the given [`Expr`] as per the [`ConversionType`]. fn generate_fix(checker: &Checker, conversion_type: ConversionType, expr: &Expr) -> Result { match conversion_type { @@ -423,7 +196,7 @@ pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) { &annotation, checker.semantic(), checker.locator, - checker.settings.target_version, + checker.settings.target_version.minor(), ) else { continue; }; @@ -444,7 +217,7 @@ pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) { annotation, checker.semantic(), checker.locator, - checker.settings.target_version, + checker.settings.target_version.minor(), ) else { continue; }; @@ -459,40 +232,3 @@ pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) { } } } - -#[cfg(test)] -mod tests { - use ruff_python_ast::call_path::CallPath; - - use crate::settings::types::PythonVersion; - - use super::is_known_type; - - #[test] - fn test_is_known_type() { - assert!(is_known_type( - &CallPath::from_slice(&["", "int"]), - PythonVersion::Py311 - )); - assert!(is_known_type( - &CallPath::from_slice(&["builtins", "int"]), - PythonVersion::Py311 - )); - assert!(is_known_type( - &CallPath::from_slice(&["typing", "Optional"]), - PythonVersion::Py311 - )); - assert!(is_known_type( - &CallPath::from_slice(&["typing_extensions", "Literal"]), - PythonVersion::Py311 - )); - assert!(is_known_type( - &CallPath::from_slice(&["zoneinfo", "ZoneInfo"]), - PythonVersion::Py311 - )); - assert!(!is_known_type( - &CallPath::from_slice(&["zoneinfo", "ZoneInfo"]), - PythonVersion::Py38 - )); - } -} diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap index 9ee735a80b..7d4b9a9f00 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap @@ -37,42 +37,6 @@ RUF013_0.py:25:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 27 27 | 28 28 | -RUF013_0.py:29:12: RUF013 [*] PEP 484 prohibits implicit `Optional` - | -29 | def f(arg: typing.List[str] = None): # RUF013 - | ^^^^^^^^^^^^^^^^ RUF013 -30 | pass - | - = help: Convert to `Optional[T]` - -ℹ Suggested fix -26 26 | pass -27 27 | -28 28 | -29 |-def f(arg: typing.List[str] = None): # RUF013 - 29 |+def f(arg: Optional[typing.List[str]] = None): # RUF013 -30 30 | pass -31 31 | -32 32 | - -RUF013_0.py:33:12: RUF013 [*] PEP 484 prohibits implicit `Optional` - | -33 | def f(arg: Tuple[str] = None): # RUF013 - | ^^^^^^^^^^ RUF013 -34 | pass - | - = help: Convert to `Optional[T]` - -ℹ Suggested fix -30 30 | pass -31 31 | -32 32 | -33 |-def f(arg: Tuple[str] = None): # RUF013 - 33 |+def f(arg: Optional[Tuple[str]] = None): # RUF013 -34 34 | pass -35 35 | -36 36 | - RUF013_0.py:67:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | 67 | def f(arg: Union = None): # RUF013 diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap index 2f21fcd56b..341aecce5e 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap @@ -37,42 +37,6 @@ RUF013_0.py:25:12: RUF013 [*] PEP 484 prohibits implicit `Optional` 27 27 | 28 28 | -RUF013_0.py:29:12: RUF013 [*] PEP 484 prohibits implicit `Optional` - | -29 | def f(arg: typing.List[str] = None): # RUF013 - | ^^^^^^^^^^^^^^^^ RUF013 -30 | pass - | - = help: Convert to `T | None` - -ℹ Suggested fix -26 26 | pass -27 27 | -28 28 | -29 |-def f(arg: typing.List[str] = None): # RUF013 - 29 |+def f(arg: typing.List[str] | None = None): # RUF013 -30 30 | pass -31 31 | -32 32 | - -RUF013_0.py:33:12: RUF013 [*] PEP 484 prohibits implicit `Optional` - | -33 | def f(arg: Tuple[str] = None): # RUF013 - | ^^^^^^^^^^ RUF013 -34 | pass - | - = help: Convert to `T | None` - -ℹ Suggested fix -30 30 | pass -31 31 | -32 32 | -33 |-def f(arg: Tuple[str] = None): # RUF013 - 33 |+def f(arg: Tuple[str] | None = None): # RUF013 -34 34 | pass -35 35 | -36 36 | - RUF013_0.py:67:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | 67 | def f(arg: Union = None): # RUF013 diff --git a/crates/ruff/src/rules/ruff/typing.rs b/crates/ruff/src/rules/ruff/typing.rs new file mode 100644 index 0000000000..127c213fef --- /dev/null +++ b/crates/ruff/src/rules/ruff/typing.rs @@ -0,0 +1,330 @@ +use rustpython_parser::ast::{self, Constant, Expr, Operator}; + +use ruff_python_ast::call_path::CallPath; +use ruff_python_ast::source_code::Locator; +use ruff_python_ast::typing::parse_type_annotation; +use ruff_python_semantic::SemanticModel; +use ruff_python_stdlib::sys::is_known_standard_library; + +/// Custom iterator to collect all the `|` separated expressions in a PEP 604 +/// union type. +struct PEP604UnionIterator<'a> { + stack: Vec<&'a Expr>, +} + +impl<'a> PEP604UnionIterator<'a> { + fn new(expr: &'a Expr) -> Self { + Self { stack: vec![expr] } + } +} + +impl<'a> Iterator for PEP604UnionIterator<'a> { + type Item = &'a Expr; + + fn next(&mut self) -> Option { + while let Some(expr) = self.stack.pop() { + match expr { + Expr::BinOp(ast::ExprBinOp { + left, + op: Operator::BitOr, + right, + .. + }) => { + self.stack.push(left); + self.stack.push(right); + } + _ => return Some(expr), + } + } + None + } +} + +/// Returns `true` if the given call path is a known type. +/// +/// A known type is either a builtin type, any object from the standard library, +/// or a type from the `typing_extensions` module. +fn is_known_type(call_path: &CallPath, minor_version: u32) -> bool { + match call_path.as_slice() { + ["" | "typing_extensions", ..] => true, + [module, ..] => is_known_standard_library(minor_version, module), + _ => false, + } +} + +#[derive(Debug)] +enum TypingTarget<'a> { + /// Literal `None` type. + None, + + /// A `typing.Any` type. + Any, + + /// Literal `object` type. + Object, + + /// Forward reference to a type e.g., `"List[str]"`. + ForwardReference(Expr), + + /// A `typing.Union` type or `|` separated types e.g., `Union[int, str]` + /// or `int | str`. + Union(Vec<&'a Expr>), + + /// A `typing.Literal` type e.g., `Literal[1, 2, 3]`. + Literal(Vec<&'a Expr>), + + /// A `typing.Optional` type e.g., `Optional[int]`. + Optional(&'a Expr), + + /// A `typing.Annotated` type e.g., `Annotated[int, ...]`. + Annotated(&'a Expr), + + /// Special type used to represent an unknown type (and not a typing target) + /// which could be a type alias. + Unknown, + + /// Special type used to represent a known type (and not a typing target). + /// A known type is either a builtin type, any object from the standard + /// library, or a type from the `typing_extensions` module. + Known, +} + +impl<'a> TypingTarget<'a> { + fn try_from_expr( + expr: &'a Expr, + semantic: &SemanticModel, + locator: &Locator, + minor_version: u32, + ) -> Option { + match expr { + Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { + if semantic.match_typing_expr(value, "Optional") { + return Some(TypingTarget::Optional(slice.as_ref())); + } + let Expr::Tuple(ast::ExprTuple { elts: elements, .. }) = slice.as_ref() else { + return None; + }; + if semantic.match_typing_expr(value, "Literal") { + Some(TypingTarget::Literal(elements.iter().collect())) + } else if semantic.match_typing_expr(value, "Union") { + Some(TypingTarget::Union(elements.iter().collect())) + } else if semantic.match_typing_expr(value, "Annotated") { + elements.first().map(TypingTarget::Annotated) + } else { + semantic.resolve_call_path(value).map_or( + // If we can't resolve the call path, it must be defined + // in the same file and could be a type alias. + Some(TypingTarget::Unknown), + |call_path| { + if is_known_type(&call_path, minor_version) { + Some(TypingTarget::Known) + } else { + Some(TypingTarget::Unknown) + } + }, + ) + } + } + Expr::BinOp(..) => Some(TypingTarget::Union( + PEP604UnionIterator::new(expr).collect(), + )), + Expr::Constant(ast::ExprConstant { + value: Constant::None, + .. + }) => Some(TypingTarget::None), + Expr::Constant(ast::ExprConstant { + value: Constant::Str(string), + range, + .. + }) => parse_type_annotation(string, *range, locator) + .map_or(None, |(expr, _)| Some(TypingTarget::ForwardReference(expr))), + _ => semantic.resolve_call_path(expr).map_or( + // If we can't resolve the call path, it must be defined in the + // same file, so we assume it's `Any` as it could be a type alias. + Some(TypingTarget::Unknown), + |call_path| { + if semantic.match_typing_call_path(&call_path, "Any") { + Some(TypingTarget::Any) + } else if matches!(call_path.as_slice(), ["" | "builtins", "object"]) { + Some(TypingTarget::Object) + } else if !is_known_type(&call_path, minor_version) { + // If it's not a known type, we assume it's `Any`. + Some(TypingTarget::Unknown) + } else { + Some(TypingTarget::Known) + } + }, + ), + } + } + + /// Check if the [`TypingTarget`] explicitly allows `None`. + fn contains_none( + &self, + semantic: &SemanticModel, + locator: &Locator, + minor_version: u32, + ) -> bool { + match self { + TypingTarget::None + | TypingTarget::Optional(_) + | TypingTarget::Any + | TypingTarget::Object + | TypingTarget::Unknown => true, + TypingTarget::Known => false, + TypingTarget::Literal(elements) => elements.iter().any(|element| { + // Literal can only contain `None`, a literal value, other `Literal` + // or an enum value. + match TypingTarget::try_from_expr(element, semantic, locator, minor_version) { + None | Some(TypingTarget::None) => true, + Some(new_target @ TypingTarget::Literal(_)) => { + new_target.contains_none(semantic, locator, minor_version) + } + _ => false, + } + }), + TypingTarget::Union(elements) => elements.iter().any(|element| { + TypingTarget::try_from_expr(element, semantic, locator, minor_version) + .map_or(true, |new_target| { + new_target.contains_none(semantic, locator, minor_version) + }) + }), + TypingTarget::Annotated(element) => { + TypingTarget::try_from_expr(element, semantic, locator, minor_version) + .map_or(true, |new_target| { + new_target.contains_none(semantic, locator, minor_version) + }) + } + TypingTarget::ForwardReference(expr) => { + TypingTarget::try_from_expr(expr, semantic, locator, minor_version) + .map_or(true, |new_target| { + new_target.contains_none(semantic, locator, minor_version) + }) + } + } + } + + /// Check if the [`TypingTarget`] explicitly allows `Any`. + fn contains_any( + &self, + semantic: &SemanticModel, + locator: &Locator, + minor_version: u32, + ) -> bool { + match self { + TypingTarget::Any => true, + // `Literal` cannot contain `Any` as it's a dynamic value. + TypingTarget::Literal(_) + | TypingTarget::None + | TypingTarget::Object + | TypingTarget::Known + | TypingTarget::Unknown => false, + TypingTarget::Union(elements) => elements.iter().any(|element| { + TypingTarget::try_from_expr(element, semantic, locator, minor_version) + .map_or(true, |new_target| { + new_target.contains_any(semantic, locator, minor_version) + }) + }), + TypingTarget::Annotated(element) | TypingTarget::Optional(element) => { + TypingTarget::try_from_expr(element, semantic, locator, minor_version) + .map_or(true, |new_target| { + new_target.contains_any(semantic, locator, minor_version) + }) + } + TypingTarget::ForwardReference(expr) => { + TypingTarget::try_from_expr(expr, semantic, locator, minor_version) + .map_or(true, |new_target| { + new_target.contains_any(semantic, locator, minor_version) + }) + } + } + } +} + +/// Check if the given annotation [`Expr`] explicitly allows `None`. +/// +/// This function will return `None` if the annotation explicitly allows `None` +/// otherwise it will return the annotation itself. If it's a `Annotated` type, +/// then the inner type will be checked. +/// +/// This function assumes that the annotation is a valid typing annotation expression. +pub(crate) fn type_hint_explicitly_allows_none<'a>( + annotation: &'a Expr, + semantic: &SemanticModel, + locator: &Locator, + minor_version: u32, +) -> Option<&'a Expr> { + match TypingTarget::try_from_expr(annotation, semantic, locator, minor_version) { + None | + // Short circuit on top level `None`, `Any` or `Optional` + Some(TypingTarget::None | TypingTarget::Optional(_) | TypingTarget::Any) => None, + // Top-level `Annotated` node should check for the inner type and + // return the inner type if it doesn't allow `None`. If `Annotated` + // is found nested inside another type, then the outer type should + // be returned. + Some(TypingTarget::Annotated(expr)) => { + type_hint_explicitly_allows_none(expr, semantic, locator, minor_version) + } + Some(target) => { + if target.contains_none(semantic, locator, minor_version) { + None + } else { + Some(annotation) + } + } + } +} + +/// Check if the given annotation [`Expr`] resolves to `Any`. +/// +/// This function assumes that the annotation is a valid typing annotation expression. +pub(crate) fn type_hint_resolves_to_any( + annotation: &Expr, + semantic: &SemanticModel, + locator: &Locator, + minor_version: u32, +) -> bool { + match TypingTarget::try_from_expr(annotation, semantic, locator, minor_version) { + None | + // Short circuit on top level `Any` + Some(TypingTarget::Any) => true, + // Top-level `Annotated` node should check if the inner type resolves + // to `Any`. + Some(TypingTarget::Annotated(expr)) => { + type_hint_resolves_to_any(expr, semantic, locator, minor_version) + } + Some(target) => target.contains_any(semantic, locator, minor_version), + } +} + +#[cfg(test)] +mod tests { + use ruff_python_ast::call_path::CallPath; + + use super::is_known_type; + + #[test] + fn test_is_known_type() { + assert!(is_known_type(&CallPath::from_slice(&["", "int"]), 11)); + assert!(is_known_type( + &CallPath::from_slice(&["builtins", "int"]), + 11 + )); + assert!(is_known_type( + &CallPath::from_slice(&["typing", "Optional"]), + 11 + )); + assert!(is_known_type( + &CallPath::from_slice(&["typing_extensions", "Literal"]), + 11 + )); + assert!(is_known_type( + &CallPath::from_slice(&["zoneinfo", "ZoneInfo"]), + 11 + )); + assert!(!is_known_type( + &CallPath::from_slice(&["zoneinfo", "ZoneInfo"]), + 8 + )); + } +} diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml index bdb5a08d5e..1a3a318a1a 100644 --- a/scripts/pyproject.toml +++ b/scripts/pyproject.toml @@ -19,6 +19,7 @@ ignore = [ "T", # flake8-print "FBT", # flake8-boolean-trap "PERF", # perflint + "ANN401", ] [tool.ruff.isort]