diff --git a/Cargo.toml b/Cargo.toml index b2afea8f85..2c4cc88edb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,7 @@ wasm-bindgen = { version = "0.2.83" } is_executable = "1.0.1" [dev-dependencies] -insta = { version = "1.19.1", features = ["yaml", "redactions"] } +insta = { version = "1.19.0", features = ["yaml", "redactions"] } test-case = { version = "2.2.2" } wasm-bindgen-test = { version = "0.3.33" } diff --git a/README.md b/README.md index 29c348b609..7e94906b0c 100644 --- a/README.md +++ b/README.md @@ -850,6 +850,7 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/) on PyPI. | UP033 | functools-cache | Use `@functools.cache` instead of `@functools.lru_cache(maxsize=None)` | 🛠 | | UP034 | extraneous-parentheses | Avoid extraneous parentheses | 🛠 | | UP035 | import-replacements | Import from `{module}` instead: {names} | 🛠 | +| UP036 | outdated-version-block | Version block is outdated for minimum Python version | 🛠 | ### flake8-2020 (YTT) diff --git a/foo.py b/foo.py new file mode 100644 index 0000000000..a4c70ac96d --- /dev/null +++ b/foo.py @@ -0,0 +1,4 @@ +import sys + +expected_error = \ + [] diff --git a/resources/test/fixtures/pyupgrade/UP036_0.py b/resources/test/fixtures/pyupgrade/UP036_0.py new file mode 100644 index 0000000000..d223d58c68 --- /dev/null +++ b/resources/test/fixtures/pyupgrade/UP036_0.py @@ -0,0 +1,180 @@ +import sys + +if sys.version_info < (3,0): + print("py2") +else: + print("py3") + +if sys.version_info < (3,0): + if True: + print("py2!") + else: + print("???") +else: + print("py3") + +if sys.version_info < (3,0): print("PY2!") +else: print("PY3!") + +if True: + if sys.version_info < (3,0): + print("PY2") + else: + print("PY3") + +if sys.version_info < (3,0): print(1 if True else 3) +else: + print("py3") + +if sys.version_info < (3,0): + def f(): + print("py2") +else: + def f(): + print("py3") + print("This the next") + +if sys.version_info > (3,0): + print("py3") +else: + print("py2") + + +x = 1 + +if sys.version_info > (3,0): + print("py3") +else: + print("py2") + # ohai + +x = 1 + +if sys.version_info > (3,0): print("py3") +else: print("py2") + +if sys.version_info > (3,): + print("py3") +else: + print("py2") + +if True: + if sys.version_info > (3,): + print("py3") + else: + print("py2") + +if sys.version_info < (3,): + print("py2") +else: + print("py3") + +def f(): + if sys.version_info < (3,0): + try: + yield + finally: + pass + else: + yield + + +class C: + def g(): + pass + + if sys.version_info < (3,0): + def f(py2): + pass + else: + def f(py3): + pass + + def h(): + pass + +if True: + if sys.version_info < (3,0): + 2 + else: + 3 + + # comment + +if sys.version_info < (3,0): + def f(): + print("py2") + def g(): + print("py2") +else: + def f(): + print("py3") + def g(): + print("py3") + +if True: + if sys.version_info > (3,): + print(3) + # comment + print(2+3) + +if True: + if sys.version_info > (3,): print(3) + +if True: + if sys.version_info > (3,): + print(3) + + +if True: + if sys.version_info <= (3, 0): + expected_error = [] + else: + expected_error = [ +":1:5: Generator expression must be parenthesized", +"max(1 for i in range(10), key=lambda x: x+1)", +" ^", + ] + + +if sys.version_info <= (3, 0): + expected_error = [] +else: + expected_error = [ +":1:5: Generator expression must be parenthesized", +"max(1 for i in range(10), key=lambda x: x+1)", +" ^", + ] + + +if sys.version_info > (3,0): + """this +is valid""" + + """the indentation on + this line is significant""" + + "this is" \ + "allowed too" + + ("so is" + "this for some reason") + +if sys.version_info > (3, 0): expected_error = \ + [] + +if sys.version_info > (3, 0): expected_error = [] + +if sys.version_info > (3, 0): \ + expected_error = [] + +if True: + if sys.version_info > (3, 0): expected_error = \ + [] + +if True: + if sys.version_info > (3, 0): expected_error = [] + +if True: + if sys.version_info > (3, 0): \ + expected_error = [] diff --git a/resources/test/fixtures/pyupgrade/UP036_1.py b/resources/test/fixtures/pyupgrade/UP036_1.py new file mode 100644 index 0000000000..108ea43c19 --- /dev/null +++ b/resources/test/fixtures/pyupgrade/UP036_1.py @@ -0,0 +1,76 @@ +import sys + +if sys.version_info == 2: + 2 +else: + 3 + +if sys.version_info < (3,): + 2 +else: + 3 + +if sys.version_info < (3,0): + 2 +else: + 3 + +if sys.version_info == 3: + 3 +else: + 2 + +if sys.version_info > (3,): + 3 +else: + 2 + +if sys.version_info >= (3,): + 3 +else: + 2 + +from sys import version_info + +if version_info > (3,): + 3 +else: + 2 + +if True: + print(1) +elif sys.version_info < (3,0): + print(2) +else: + print(3) + +if True: + print(1) +elif sys.version_info > (3,): + print(3) +else: + print(2) + +if True: + print(1) +elif sys.version_info > (3,): + print(3) + +def f(): + if True: + print(1) + elif sys.version_info > (3,): + print(3) + +if True: + print(1) +elif sys.version_info < (3,0): + print(2) +else: + print(3) + +def f(): + if True: + print(1) + elif sys.version_info > (3,): + print(3) diff --git a/resources/test/fixtures/pyupgrade/UP036_2.py b/resources/test/fixtures/pyupgrade/UP036_2.py new file mode 100644 index 0000000000..9c588d790e --- /dev/null +++ b/resources/test/fixtures/pyupgrade/UP036_2.py @@ -0,0 +1,62 @@ +import sys +from sys import version_info + +if sys.version_info > (3, 5): + 3+6 +else: + 3-5 + +if version_info > (3, 5): + 3+6 +else: + 3-5 + +if sys.version_info >= (3,6): + 3+6 +else: + 3-5 + +if version_info >= (3,6): + 3+6 +else: + 3-5 + +if sys.version_info < (3,6): + 3-5 +else: + 3+6 + +if sys.version_info <= (3,5): + 3-5 +else: + 3+6 + +if sys.version_info <= (3, 5): + 3-5 +else: + 3+6 + +if sys.version_info >= (3, 5): + pass + +if sys.version_info < (3,0): + pass + +if True: + if sys.version_info < (3,0): + pass + +if sys.version_info < (3,0): + pass +elif False: + pass + +if sys.version_info > (3,): + pass +elif False: + pass + +if sys.version_info[0] > "2": + 3 +else: + 2 diff --git a/resources/test/fixtures/pyupgrade/UP036_3.py b/resources/test/fixtures/pyupgrade/UP036_3.py new file mode 100644 index 0000000000..287d6ad3b4 --- /dev/null +++ b/resources/test/fixtures/pyupgrade/UP036_3.py @@ -0,0 +1,24 @@ +import sys + +if sys.version_info < (3,0): + print("py2") + for item in range(10): + print(f"PY2-{item}") +else : + print("py3") + for item in range(10): + print(f"PY3-{item}") + +if False: + if sys.version_info < (3,0): + print("py2") + for item in range(10): + print(f"PY2-{item}") + else : + print("py3") + for item in range(10): + print(f"PY3-{item}") + + +if sys.version_info < (3,0): print("PY2!") +else : print("PY3!") diff --git a/resources/test/fixtures/pyupgrade/UP036_4.py b/resources/test/fixtures/pyupgrade/UP036_4.py new file mode 100644 index 0000000000..e3eead1251 --- /dev/null +++ b/resources/test/fixtures/pyupgrade/UP036_4.py @@ -0,0 +1,45 @@ +import sys + +if True: + if sys.version_info < (3, 3): + cmd = [sys.executable, "-m", "test.regrtest"] + + +if True: + if foo: + pass + elif sys.version_info < (3, 3): + cmd = [sys.executable, "-m", "test.regrtest"] + +if True: + if foo: + pass + elif sys.version_info < (3, 3): + cmd = [sys.executable, "-m", "test.regrtest"] + elif foo: + cmd = [sys.executable, "-m", "test", "-j0"] + + if foo: + pass + elif sys.version_info < (3, 3): + cmd = [sys.executable, "-m", "test.regrtest"] + + if sys.version_info < (3, 3): + cmd = [sys.executable, "-m", "test.regrtest"] + + if foo: + pass + elif sys.version_info < (3, 3): + cmd = [sys.executable, "-m", "test.regrtest"] + else: + cmd = [sys.executable, "-m", "test", "-j0"] + + if sys.version_info < (3, 3): + cmd = [sys.executable, "-m", "test.regrtest"] + else: + cmd = [sys.executable, "-m", "test", "-j0"] + + if sys.version_info < (3, 3): + cmd = [sys.executable, "-m", "test.regrtest"] + elif foo: + cmd = [sys.executable, "-m", "test", "-j0"] diff --git a/ruff.schema.json b/ruff.schema.json index 11755b354b..ed9569277c 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1905,6 +1905,7 @@ "UP033", "UP034", "UP035", + "UP036", "W", "W2", "W29", diff --git a/setup.py b/setup.py index aeef6f2ca3..a8d82c4a0a 100644 --- a/setup.py +++ b/setup.py @@ -22,3 +22,5 @@ sys.exit(1) # To be removed once GitHub catches up. setup(name="ruff", install_requires=[]) +if True: a = 1; \ + b = 2 diff --git a/src/checkers/ast.rs b/src/checkers/ast.rs index d112e85676..a80f8790e2 100644 --- a/src/checkers/ast.rs +++ b/src/checkers/ast.rs @@ -1500,6 +1500,9 @@ where self.current_stmt_parent().map(Into::into), ); } + if self.settings.rules.enabled(&Rule::OutdatedVersionBlock) { + pyupgrade::rules::outdated_version_block(self, stmt, test, body, orelse); + } } StmtKind::Assert { test, msg } => { if self.settings.rules.enabled(&Rule::AssertTuple) { diff --git a/src/registry.rs b/src/registry.rs index 3f656eec12..3fdf74c4dd 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -256,6 +256,7 @@ ruff_macros::define_rule_mapping!( UP033 => violations::FunctoolsCache, UP034 => violations::ExtraneousParentheses, UP035 => rules::pyupgrade::rules::ImportReplacements, + UP036 => rules::pyupgrade::rules::OutdatedVersionBlock, // pydocstyle D100 => violations::PublicModule, D101 => violations::PublicClass, diff --git a/src/rules/pyupgrade/fixes.rs b/src/rules/pyupgrade/fixes.rs index 4546efdbd6..90a1bc444d 100644 --- a/src/rules/pyupgrade/fixes.rs +++ b/src/rules/pyupgrade/fixes.rs @@ -1,5 +1,7 @@ +use anyhow::{bail, Result}; use libcst_native::{ - Codegen, CodegenState, Expression, ParenthesizableWhitespace, SmallStatement, Statement, + Codegen, CodegenState, CompoundStatement, Expression, ParenthesizableWhitespace, + SmallStatement, Statement, Suite, }; use rustpython_ast::{Expr, Keyword, Location}; use rustpython_parser::lexer; @@ -7,9 +9,47 @@ use rustpython_parser::lexer::Tok; use crate::ast::types::Range; use crate::autofix::helpers::remove_argument; +use crate::cst::matchers::match_module; use crate::fix::Fix; use crate::source_code::{Locator, Stylist}; +/// Safely adjust the indentation of the indented block at [`Range`]. +pub fn adjust_indentation( + range: Range, + indentation: &str, + locator: &Locator, + stylist: &Stylist, +) -> Result { + let contents = locator.slice_source_code_range(&range); + + let module_text = format!("def f():{}{contents}", stylist.line_ending().as_str()); + + let mut tree = match_module(&module_text)?; + + let [Statement::Compound(CompoundStatement::FunctionDef(embedding))] = &mut *tree.body else { + bail!("Expected statement to be embedded in a function definition") + }; + + let Suite::IndentedBlock(indented_block) = &mut embedding.body else { + bail!("Expected indented block") + }; + indented_block.indent = Some(indentation); + + let mut state = CodegenState { + default_newline: stylist.line_ending(), + default_indent: stylist.indentation(), + ..Default::default() + }; + indented_block.codegen(&mut state); + + let module_text = state.to_string(); + let module_text = module_text + .strip_prefix(stylist.line_ending().as_str()) + .unwrap() + .to_string(); + Ok(module_text) +} + /// Generate a fix to remove a base from a `ClassDef` statement. pub fn remove_class_def_base( locator: &Locator, diff --git a/src/rules/pyupgrade/mod.rs b/src/rules/pyupgrade/mod.rs index 33de7d5eb3..db906786cb 100644 --- a/src/rules/pyupgrade/mod.rs +++ b/src/rules/pyupgrade/mod.rs @@ -58,6 +58,11 @@ mod tests { #[test_case(Rule::FunctoolsCache, Path::new("UP033.py"); "UP033")] #[test_case(Rule::ExtraneousParentheses, Path::new("UP034.py"); "UP034")] #[test_case(Rule::ImportReplacements, Path::new("UP035.py"); "UP035")] + #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_0.py"); "UP036_0")] + #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_1.py"); "UP036_1")] + #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_2.py"); "UP036_2")] + #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_3.py"); "UP036_3")] + #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_4.py"); "UP036_4")] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/src/rules/pyupgrade/rules/mod.rs b/src/rules/pyupgrade/rules/mod.rs index a49cd9c975..c58ec44a3d 100644 --- a/src/rules/pyupgrade/rules/mod.rs +++ b/src/rules/pyupgrade/rules/mod.rs @@ -11,6 +11,7 @@ pub(crate) use lru_cache_without_parameters::lru_cache_without_parameters; pub(crate) use native_literals::native_literals; pub(crate) use open_alias::open_alias; pub(crate) use os_error_alias::os_error_alias; +pub(crate) use outdated_version_block::{outdated_version_block, OutdatedVersionBlock}; pub(crate) use printf_string_formatting::printf_string_formatting; pub(crate) use redundant_open_modes::redundant_open_modes; pub(crate) use replace_stdout_stderr::replace_stdout_stderr; @@ -46,6 +47,7 @@ mod lru_cache_without_parameters; mod native_literals; mod open_alias; mod os_error_alias; +mod outdated_version_block; mod printf_string_formatting; mod redundant_open_modes; mod replace_stdout_stderr; diff --git a/src/rules/pyupgrade/rules/outdated_version_block.rs b/src/rules/pyupgrade/rules/outdated_version_block.rs new file mode 100644 index 0000000000..e9bac98a4e --- /dev/null +++ b/src/rules/pyupgrade/rules/outdated_version_block.rs @@ -0,0 +1,442 @@ +use std::cmp::Ordering; + +use log::error; +use num_bigint::{BigInt, Sign}; +use rustpython_ast::Location; +use rustpython_parser::ast::{Cmpop, Constant, Expr, ExprKind, Located, Stmt}; +use rustpython_parser::lexer; +use rustpython_parser::lexer::Tok; + +use ruff_macros::derive_message_formats; + +use crate::ast::types::{Range, RefEquality}; +use crate::ast::whitespace::indentation; +use crate::autofix::helpers::delete_stmt; +use crate::checkers::ast::Checker; +use crate::define_violation; +use crate::fix::Fix; +use crate::registry::Diagnostic; +use crate::rules::pyupgrade::fixes::adjust_indentation; +use crate::settings::types::PythonVersion; +use crate::source_code::Locator; +use crate::violation::AlwaysAutofixableViolation; + +define_violation!( + pub struct OutdatedVersionBlock; +); +impl AlwaysAutofixableViolation for OutdatedVersionBlock { + #[derive_message_formats] + fn message(&self) -> String { + format!("Version block is outdated for minimum Python version") + } + + fn autofix_title(&self) -> String { + "Remove outdated version block".to_string() + } +} + +#[derive(Debug)] +struct BlockMetadata { + /// The first non-whitespace token in the block. + starter: Tok, + /// The location of the first `elif` token, if any. + elif: Option, + /// The location of the `else` token, if any. + else_: Option, +} + +impl BlockMetadata { + fn new(starter: Tok, elif: Option, else_: Option) -> Self { + Self { + starter, + elif, + else_, + } + } +} + +fn metadata(locator: &Locator, located: &Located) -> Option { + indentation(locator, located)?; + + // Start the selection at the start-of-line. This ensures consistent indentation in the + // token stream, in the event that the entire block is indented. + let text = locator.slice_source_code_range(&Range::new( + Location::new(located.location.row(), 0), + located.end_location.unwrap(), + )); + + let mut starter: Option = None; + let mut elif = None; + let mut else_ = None; + + for (start, tok, _) in + lexer::make_tokenizer_located(text, Location::new(located.location.row(), 0)) + .flatten() + .filter(|(_, tok, _)| { + !matches!( + tok, + Tok::Indent + | Tok::Dedent + | Tok::NonLogicalNewline + | Tok::Newline + | Tok::Comment(..) + ) + }) + { + if starter.is_none() { + starter = Some(tok.clone()); + } else { + if matches!(tok, Tok::Elif) && elif.is_none() { + elif = Some(start); + } + if matches!(tok, Tok::Else) && else_.is_none() { + else_ = Some(start); + } + } + if starter.is_some() && elif.is_some() && else_.is_some() { + break; + } + } + Some(BlockMetadata::new(starter.unwrap(), elif, else_)) +} + +/// Converts a `BigInt` to a `u32`, if the number is negative, it will return 0 +fn bigint_to_u32(number: &BigInt) -> u32 { + let the_number = number.to_u32_digits(); + match the_number.0 { + Sign::Minus | Sign::NoSign => 0, + Sign::Plus => *the_number.1.first().unwrap(), + } +} + +/// Gets the version from the tuple +fn extract_version(elts: &[Expr]) -> Vec { + let mut version: Vec = vec![]; + for elt in elts { + if let ExprKind::Constant { + value: Constant::Int(item), + .. + } = &elt.node + { + let number = bigint_to_u32(item); + version.push(number); + } else { + return version; + } + } + version +} + +/// Returns true if the `if_version` is less than the `PythonVersion` +fn compare_version(if_version: &[u32], py_version: PythonVersion, or_equal: bool) -> bool { + let mut if_version_iter = if_version.iter(); + if let Some(if_major) = if_version_iter.next() { + let (py_major, py_minor) = py_version.as_tuple(); + match if_major.cmp(&py_major) { + Ordering::Less => true, + Ordering::Equal => { + if let Some(if_minor) = if_version_iter.next() { + // Check the if_minor number (the minor version). + if or_equal { + *if_minor <= py_minor + } else { + *if_minor < py_minor + } + } else { + // Assume Python 3.0. + true + } + } + Ordering::Greater => false, + } + } else { + false + } +} + +/// Convert a [`StmtKind::If`], retaining the `else`. +fn fix_py2_block( + checker: &mut Checker, + stmt: &Stmt, + body: &[Stmt], + orelse: &[Stmt], + block: &BlockMetadata, +) -> Option { + if orelse.is_empty() { + // 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 deleted: Vec<&Stmt> = checker + .deletions + .iter() + .map(std::convert::Into::into) + .collect(); + let defined_by = checker.current_stmt(); + let defined_in = checker.current_stmt_parent(); + return match delete_stmt( + defined_by.into(), + if block.starter == Tok::If { + defined_in.map(std::convert::Into::into) + } else { + None + }, + &deleted, + checker.locator, + checker.indexer, + checker.stylist, + ) { + Ok(fix) => { + checker.deletions.insert(RefEquality(defined_by.into())); + Some(fix) + } + Err(err) => { + error!("Failed to remove block: {}", err); + None + } + }; + } + + // If we only have an `if` and an `else`, dedent the `else` block. + if block.starter == Tok::If && block.elif.is_none() { + let start = orelse.first().unwrap(); + let end = orelse.last().unwrap(); + + if indentation(checker.locator, start).is_none() { + // Inline `else` block (e.g., `else: x = 1`). + Some(Fix::replacement( + checker + .locator + .slice_source_code_range(&Range::new(start.location, end.end_location.unwrap())) + .to_string(), + stmt.location, + stmt.end_location.unwrap(), + )) + } else { + indentation(checker.locator, stmt) + .and_then(|indentation| { + adjust_indentation( + Range::new( + Location::new(start.location.row(), 0), + end.end_location.unwrap(), + ), + indentation, + checker.locator, + checker.stylist, + ) + .ok() + }) + .map(|contents| { + Fix::replacement( + contents, + Location::new(stmt.location.row(), 0), + stmt.end_location.unwrap(), + ) + }) + } + } else { + let mut end_location = orelse.last().unwrap().location; + if block.starter == Tok::If && block.elif.is_some() { + // Turn the `elif` into an `if`. + end_location = block.elif.unwrap(); + end_location.go_right(); + end_location.go_right(); + } else if block.starter == Tok::Elif { + if let Some(elif) = block.elif { + end_location = elif; + } else if let Some(else_) = block.else_ { + end_location = else_; + } else { + end_location = body.last().unwrap().end_location.unwrap(); + } + } + Some(Fix::deletion(stmt.location, end_location)) + } +} + +/// Convert a [`StmtKind::If`], removing the `else` block. +fn fix_py3_block( + checker: &mut Checker, + stmt: &Stmt, + test: &Expr, + body: &[Stmt], + block: &BlockMetadata, +) -> Option { + match block.starter { + Tok::If => { + // If the first statement is an if, use the body of this statement, and ignore the rest. + let start = body.first().unwrap(); + let end = body.last().unwrap(); + + if indentation(checker.locator, start).is_none() { + // Inline `if` block (e.g., `if ...: x = 1`). + Some(Fix::replacement( + checker + .locator + .slice_source_code_range(&Range::new( + start.location, + end.end_location.unwrap(), + )) + .to_string(), + stmt.location, + stmt.end_location.unwrap(), + )) + } else { + indentation(checker.locator, stmt) + .and_then(|indentation| { + adjust_indentation( + Range::new( + Location::new(start.location.row(), 0), + end.end_location.unwrap(), + ), + indentation, + checker.locator, + checker.stylist, + ) + .ok() + }) + .map(|contents| { + Fix::replacement( + contents, + Location::new(stmt.location.row(), 0), + stmt.end_location.unwrap(), + ) + }) + } + } + Tok::Elif => { + // Replace the `elif` with an `else, preserve the body of the elif, and remove the rest. + let end = body.last().unwrap(); + let text = checker.locator.slice_source_code_range(&Range::new( + test.end_location.unwrap(), + end.end_location.unwrap(), + )); + Some(Fix::replacement( + format!("else{text}"), + stmt.location, + stmt.end_location.unwrap(), + )) + } + _ => None, + } +} + +/// UP036 +pub fn outdated_version_block( + checker: &mut Checker, + stmt: &Stmt, + test: &Expr, + body: &[Stmt], + orelse: &[Stmt], +) { + let ExprKind::Compare { + left, + ops, + comparators, + } = &test.node else { + return; + }; + + if !checker.resolve_call_path(left).map_or(false, |call_path| { + call_path.as_slice() == ["sys", "version_info"] + }) { + return; + } + + if ops.len() == 1 && comparators.len() == 1 { + let comparison = &comparators[0].node; + let op = &ops[0]; + match comparison { + ExprKind::Tuple { 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) { + let mut diagnostic = + Diagnostic::new(OutdatedVersionBlock, Range::from_located(stmt)); + if checker.patch(diagnostic.kind.rule()) { + if let Some(block) = metadata(checker.locator, stmt) { + if let Some(fix) = + fix_py2_block(checker, stmt, body, orelse, &block) + { + diagnostic.amend(fix); + } + } + } + checker.diagnostics.push(diagnostic); + } + } else if op == &Cmpop::Gt || op == &Cmpop::GtE { + if compare_version(&version, target, op == &Cmpop::GtE) { + let mut diagnostic = + Diagnostic::new(OutdatedVersionBlock, Range::from_located(stmt)); + if checker.patch(diagnostic.kind.rule()) { + if let Some(block) = metadata(checker.locator, stmt) { + if let Some(fix) = fix_py3_block(checker, stmt, test, body, &block) + { + diagnostic.amend(fix); + } + } + } + checker.diagnostics.push(diagnostic); + } + } + } + ExprKind::Constant { + value: Constant::Int(number), + .. + } => { + let version_number = bigint_to_u32(number); + if version_number == 2 && op == &Cmpop::Eq { + let mut diagnostic = + Diagnostic::new(OutdatedVersionBlock, Range::from_located(stmt)); + if checker.patch(diagnostic.kind.rule()) { + if let Some(block) = metadata(checker.locator, stmt) { + if let Some(fix) = fix_py2_block(checker, stmt, body, orelse, &block) { + diagnostic.amend(fix); + } + } + } + checker.diagnostics.push(diagnostic); + } else if version_number == 3 && op == &Cmpop::Eq { + let mut diagnostic = + Diagnostic::new(OutdatedVersionBlock, Range::from_located(stmt)); + if checker.patch(diagnostic.kind.rule()) { + if let Some(block) = metadata(checker.locator, stmt) { + if let Some(fix) = fix_py3_block(checker, stmt, test, body, &block) { + diagnostic.amend(fix); + } + } + } + checker.diagnostics.push(diagnostic); + } + } + _ => (), + } + } +} + +#[cfg(test)] +mod tests { + use test_case::test_case; + + use super::*; + + #[test_case(PythonVersion::Py37, &[2], true, true; "compare-2.0")] + #[test_case(PythonVersion::Py37, &[2, 0], true, true; "compare-2.0-whole")] + #[test_case(PythonVersion::Py37, &[3], true, true; "compare-3.0")] + #[test_case(PythonVersion::Py37, &[3, 0], true, true; "compare-3.0-whole")] + #[test_case(PythonVersion::Py37, &[3, 1], true, true; "compare-3.1")] + #[test_case(PythonVersion::Py37, &[3, 5], true, true; "compare-3.5")] + #[test_case(PythonVersion::Py37, &[3, 7], true, true; "compare-3.7")] + #[test_case(PythonVersion::Py37, &[3, 7], false, false; "compare-3.7-not-equal")] + #[test_case(PythonVersion::Py37, &[3, 8], false , false; "compare-3.8")] + #[test_case(PythonVersion::Py310, &[3,9], true, true; "compare-3.9")] + #[test_case(PythonVersion::Py310, &[3, 11], true, false; "compare-3.11")] + fn test_compare_version( + version: PythonVersion, + version_vec: &[u32], + or_equal: bool, + expected: bool, + ) { + assert_eq!(compare_version(version_vec, version, or_equal), expected); + } +} diff --git a/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP036_UP036_0.py.snap b/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP036_UP036_0.py.snap new file mode 100644 index 0000000000..0eae75df5a --- /dev/null +++ b/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP036_UP036_0.py.snap @@ -0,0 +1,535 @@ +--- +source: src/rules/pyupgrade/mod.rs +expression: diagnostics +--- +- kind: + OutdatedVersionBlock: ~ + location: + row: 3 + column: 0 + end_location: + row: 6 + column: 16 + fix: + content: + - "print(\"py3\")" + location: + row: 3 + column: 0 + end_location: + row: 6 + column: 16 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 8 + column: 0 + end_location: + row: 14 + column: 16 + fix: + content: + - "print(\"py3\")" + location: + row: 8 + column: 0 + end_location: + row: 14 + column: 16 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 16 + column: 0 + end_location: + row: 17 + column: 19 + fix: + content: + - "print(\"PY3!\")" + location: + row: 16 + column: 0 + end_location: + row: 17 + column: 19 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 20 + column: 4 + end_location: + row: 23 + column: 20 + fix: + content: + - " print(\"PY3\")" + location: + row: 20 + column: 0 + end_location: + row: 23 + column: 20 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 25 + column: 0 + end_location: + row: 27 + column: 16 + fix: + content: + - "print(\"py3\")" + location: + row: 25 + column: 0 + end_location: + row: 27 + column: 16 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 29 + column: 0 + end_location: + row: 35 + column: 30 + fix: + content: + - "def f():" + - " print(\"py3\")" + - " print(\"This the next\")" + location: + row: 29 + column: 0 + end_location: + row: 35 + column: 30 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 37 + column: 0 + end_location: + row: 40 + column: 16 + fix: + content: + - "print(\"py3\")" + location: + row: 37 + column: 0 + end_location: + row: 40 + column: 16 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 45 + column: 0 + end_location: + row: 48 + column: 16 + fix: + content: + - "print(\"py3\")" + location: + row: 45 + column: 0 + end_location: + row: 48 + column: 16 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 53 + column: 0 + end_location: + row: 54 + column: 18 + fix: + content: + - "print(\"py3\")" + location: + row: 53 + column: 0 + end_location: + row: 54 + column: 18 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 56 + column: 0 + end_location: + row: 59 + column: 16 + fix: + content: + - "print(\"py3\")" + location: + row: 56 + column: 0 + end_location: + row: 59 + column: 16 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 62 + column: 4 + end_location: + row: 65 + column: 20 + fix: + content: + - " print(\"py3\")" + location: + row: 62 + column: 0 + end_location: + row: 65 + column: 20 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 67 + column: 0 + end_location: + row: 70 + column: 16 + fix: + content: + - "print(\"py3\")" + location: + row: 67 + column: 0 + end_location: + row: 70 + column: 16 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 73 + column: 4 + end_location: + row: 79 + column: 13 + fix: + content: + - " yield" + location: + row: 73 + column: 0 + end_location: + row: 79 + column: 13 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 86 + column: 4 + end_location: + row: 91 + column: 16 + fix: + content: + - " def f(py3):" + - " pass" + location: + row: 86 + column: 0 + end_location: + row: 91 + column: 16 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 97 + column: 4 + end_location: + row: 100 + column: 9 + fix: + content: + - " 3" + location: + row: 97 + column: 0 + end_location: + row: 100 + column: 9 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 104 + column: 0 + end_location: + row: 113 + column: 20 + fix: + content: + - "def f():" + - " print(\"py3\")" + - "def g():" + - " print(\"py3\")" + location: + row: 104 + column: 0 + end_location: + row: 113 + column: 20 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 116 + column: 4 + end_location: + row: 117 + column: 16 + fix: + content: + - " print(3)" + location: + row: 116 + column: 0 + end_location: + row: 117 + column: 16 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 122 + column: 4 + end_location: + row: 122 + column: 40 + fix: + content: + - print(3) + location: + row: 122 + column: 4 + end_location: + row: 122 + column: 40 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 125 + column: 4 + end_location: + row: 126 + column: 16 + fix: + content: + - " print(3)" + location: + row: 125 + column: 0 + end_location: + row: 126 + column: 16 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 130 + column: 4 + end_location: + row: 137 + column: 9 + fix: + content: + - " expected_error = [" + - "\":1:5: Generator expression must be parenthesized\"," + - "\"max(1 for i in range(10), key=lambda x: x+1)\"," + - "\" ^\"," + - " ]" + location: + row: 130 + column: 0 + end_location: + row: 137 + column: 9 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 140 + column: 0 + end_location: + row: 147 + column: 5 + fix: + content: + - "expected_error = [" + - "\":1:5: Generator expression must be parenthesized\"," + - "\"max(1 for i in range(10), key=lambda x: x+1)\"," + - "\" ^\"," + - "]" + location: + row: 140 + column: 0 + end_location: + row: 147 + column: 5 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 150 + column: 0 + end_location: + row: 161 + column: 28 + fix: + content: + - "\"\"\"this" + - "is valid\"\"\"" + - "" + - "\"\"\"the indentation on" + - " this line is significant\"\"\"" + - "" + - "\"this is\" \\" + - " \"allowed too\"" + - "" + - "(\"so is\"" + - " \"this for some reason\")" + location: + row: 150 + column: 0 + end_location: + row: 161 + column: 28 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 163 + column: 0 + end_location: + row: 164 + column: 6 + fix: + content: + - "expected_error = \\" + - " []" + location: + row: 163 + column: 0 + end_location: + row: 164 + column: 6 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 166 + column: 0 + end_location: + row: 166 + column: 49 + fix: + content: + - "expected_error = []" + location: + row: 166 + column: 0 + end_location: + row: 166 + column: 49 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 168 + column: 0 + end_location: + row: 169 + column: 23 + fix: + content: + - "expected_error = []" + location: + row: 168 + column: 0 + end_location: + row: 169 + column: 23 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 172 + column: 4 + end_location: + row: 173 + column: 6 + fix: + content: + - "expected_error = \\" + - " []" + location: + row: 172 + column: 4 + end_location: + row: 173 + column: 6 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 176 + column: 4 + end_location: + row: 176 + column: 53 + fix: + content: + - "expected_error = []" + location: + row: 176 + column: 4 + end_location: + row: 176 + column: 53 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 179 + column: 4 + end_location: + row: 180 + column: 23 + fix: + content: + - " expected_error = []" + location: + row: 179 + column: 0 + end_location: + row: 180 + column: 23 + parent: ~ + diff --git a/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP036_UP036_1.py.snap b/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP036_UP036_1.py.snap new file mode 100644 index 0000000000..a3476a6186 --- /dev/null +++ b/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP036_UP036_1.py.snap @@ -0,0 +1,243 @@ +--- +source: src/rules/pyupgrade/mod.rs +expression: diagnostics +--- +- kind: + OutdatedVersionBlock: ~ + location: + row: 3 + column: 0 + end_location: + row: 6 + column: 5 + fix: + content: + - "3" + location: + row: 3 + column: 0 + end_location: + row: 6 + column: 5 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 8 + column: 0 + end_location: + row: 11 + column: 5 + fix: + content: + - "3" + location: + row: 8 + column: 0 + end_location: + row: 11 + column: 5 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 13 + column: 0 + end_location: + row: 16 + column: 5 + fix: + content: + - "3" + location: + row: 13 + column: 0 + end_location: + row: 16 + column: 5 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 18 + column: 0 + end_location: + row: 21 + column: 5 + fix: + content: + - "3" + location: + row: 18 + column: 0 + end_location: + row: 21 + column: 5 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 23 + column: 0 + end_location: + row: 26 + column: 5 + fix: + content: + - "3" + location: + row: 23 + column: 0 + end_location: + row: 26 + column: 5 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 28 + column: 0 + end_location: + row: 31 + column: 5 + fix: + content: + - "3" + location: + row: 28 + column: 0 + end_location: + row: 31 + column: 5 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 35 + column: 0 + end_location: + row: 38 + column: 5 + fix: + content: + - "3" + location: + row: 35 + column: 0 + end_location: + row: 38 + column: 5 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 42 + column: 0 + end_location: + row: 45 + column: 12 + fix: + content: + - "" + location: + row: 42 + column: 0 + end_location: + row: 44 + column: 0 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 49 + column: 0 + end_location: + row: 52 + column: 12 + fix: + content: + - "else:" + - " print(3)" + location: + row: 49 + column: 0 + end_location: + row: 52 + column: 12 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 56 + column: 0 + end_location: + row: 57 + column: 12 + fix: + content: + - "else:" + - " print(3)" + location: + row: 56 + column: 0 + end_location: + row: 57 + column: 12 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 62 + column: 4 + end_location: + row: 63 + column: 16 + fix: + content: + - "else:" + - " print(3)" + location: + row: 62 + column: 4 + end_location: + row: 63 + column: 16 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 67 + column: 0 + end_location: + row: 70 + column: 12 + fix: + content: + - "" + location: + row: 67 + column: 0 + end_location: + row: 69 + column: 0 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 75 + column: 4 + end_location: + row: 76 + column: 16 + fix: + content: + - "else:" + - " print(3)" + location: + row: 75 + column: 4 + end_location: + row: 76 + column: 16 + parent: ~ + diff --git a/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP036_UP036_2.py.snap b/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP036_UP036_2.py.snap new file mode 100644 index 0000000000..a492598723 --- /dev/null +++ b/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP036_UP036_2.py.snap @@ -0,0 +1,221 @@ +--- +source: src/rules/pyupgrade/mod.rs +expression: diagnostics +--- +- kind: + OutdatedVersionBlock: ~ + location: + row: 4 + column: 0 + end_location: + row: 7 + column: 7 + fix: + content: + - 3+6 + location: + row: 4 + column: 0 + end_location: + row: 7 + column: 7 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 9 + column: 0 + end_location: + row: 12 + column: 7 + fix: + content: + - 3+6 + location: + row: 9 + column: 0 + end_location: + row: 12 + column: 7 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 14 + column: 0 + end_location: + row: 17 + column: 7 + fix: + content: + - 3+6 + location: + row: 14 + column: 0 + end_location: + row: 17 + column: 7 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 19 + column: 0 + end_location: + row: 22 + column: 7 + fix: + content: + - 3+6 + location: + row: 19 + column: 0 + end_location: + row: 22 + column: 7 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 24 + column: 0 + end_location: + row: 27 + column: 7 + fix: + content: + - 3+6 + location: + row: 24 + column: 0 + end_location: + row: 27 + column: 7 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 29 + column: 0 + end_location: + row: 32 + column: 7 + fix: + content: + - 3+6 + location: + row: 29 + column: 0 + end_location: + row: 32 + column: 7 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 34 + column: 0 + end_location: + row: 37 + column: 7 + fix: + content: + - 3+6 + location: + row: 34 + column: 0 + end_location: + row: 37 + column: 7 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 39 + column: 0 + end_location: + row: 40 + column: 8 + fix: + content: + - pass + location: + row: 39 + column: 0 + end_location: + row: 40 + column: 8 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 42 + column: 0 + end_location: + row: 43 + column: 8 + fix: + content: + - "" + location: + row: 42 + column: 0 + end_location: + row: 44 + column: 0 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 46 + column: 4 + end_location: + row: 47 + column: 12 + fix: + content: + - pass + location: + row: 46 + column: 4 + end_location: + row: 47 + column: 12 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 49 + column: 0 + end_location: + row: 52 + column: 8 + fix: + content: + - "" + location: + row: 49 + column: 0 + end_location: + row: 51 + column: 2 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 54 + column: 0 + end_location: + row: 57 + column: 8 + fix: + content: + - pass + location: + row: 54 + column: 0 + end_location: + row: 57 + column: 8 + parent: ~ + diff --git a/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP036_UP036_3.py.snap b/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP036_UP036_3.py.snap new file mode 100644 index 0000000000..82a0607c0b --- /dev/null +++ b/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP036_UP036_3.py.snap @@ -0,0 +1,63 @@ +--- +source: src/rules/pyupgrade/mod.rs +expression: diagnostics +--- +- kind: + OutdatedVersionBlock: ~ + location: + row: 3 + column: 0 + end_location: + row: 10 + column: 28 + fix: + content: + - "print(\"py3\")" + - "for item in range(10):" + - " print(f\"PY3-{item}\")" + location: + row: 3 + column: 0 + end_location: + row: 10 + column: 28 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 13 + column: 4 + end_location: + row: 20 + column: 32 + fix: + content: + - " print(\"py3\")" + - " for item in range(10):" + - " print(f\"PY3-{item}\")" + location: + row: 13 + column: 0 + end_location: + row: 20 + column: 32 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 23 + column: 0 + end_location: + row: 24 + column: 50 + fix: + content: + - "print(\"PY3!\")" + location: + row: 23 + column: 0 + end_location: + row: 24 + column: 50 + parent: ~ + diff --git a/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP036_UP036_4.py.snap b/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP036_UP036_4.py.snap new file mode 100644 index 0000000000..bcdf8a5bed --- /dev/null +++ b/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP036_UP036_4.py.snap @@ -0,0 +1,149 @@ +--- +source: src/rules/pyupgrade/mod.rs +expression: diagnostics +--- +- kind: + OutdatedVersionBlock: ~ + location: + row: 4 + column: 4 + end_location: + row: 5 + column: 53 + fix: + content: + - pass + location: + row: 4 + column: 4 + end_location: + row: 5 + column: 53 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 11 + column: 4 + end_location: + row: 12 + column: 53 + fix: + content: + - "" + location: + row: 11 + column: 0 + end_location: + row: 13 + column: 0 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 17 + column: 4 + end_location: + row: 20 + column: 51 + fix: + content: + - "" + location: + row: 17 + column: 4 + end_location: + row: 19 + column: 4 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 24 + column: 4 + end_location: + row: 25 + column: 53 + fix: + content: + - "" + location: + row: 24 + column: 0 + end_location: + row: 26 + column: 0 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 27 + column: 4 + end_location: + row: 28 + column: 53 + fix: + content: + - "" + location: + row: 27 + column: 0 + end_location: + row: 29 + column: 0 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 32 + column: 4 + end_location: + row: 35 + column: 51 + fix: + content: + - "" + location: + row: 32 + column: 4 + end_location: + row: 34 + column: 4 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 37 + column: 4 + end_location: + row: 40 + column: 51 + fix: + content: + - " cmd = [sys.executable, \"-m\", \"test\", \"-j0\"]" + location: + row: 37 + column: 0 + end_location: + row: 40 + column: 51 + parent: ~ +- kind: + OutdatedVersionBlock: ~ + location: + row: 42 + column: 4 + end_location: + row: 45 + column: 51 + fix: + content: + - "" + location: + row: 42 + column: 4 + end_location: + row: 44 + column: 6 + parent: ~ + diff --git a/src/settings/types.rs b/src/settings/types.rs index ab4682d2c6..324adf3450 100644 --- a/src/settings/types.rs +++ b/src/settings/types.rs @@ -49,6 +49,18 @@ impl FromStr for PythonVersion { } } +impl PythonVersion { + pub fn as_tuple(&self) -> (u32, u32) { + match self { + PythonVersion::Py37 => (3, 7), + PythonVersion::Py38 => (3, 8), + PythonVersion::Py39 => (3, 9), + PythonVersion::Py310 => (3, 10), + PythonVersion::Py311 => (3, 11), + } + } +} + #[derive(Debug, Clone, Hash, PartialEq, PartialOrd, Eq, Ord)] pub enum FilePattern { Builtin(&'static str),