From 3b36a7281c3f5f6352e5773d60426982959cf30a Mon Sep 17 00:00:00 2001 From: Reiner Gerecke Date: Fri, 23 Dec 2022 12:05:16 +0100 Subject: [PATCH] Implement "native literals" check from pyupgrade --- README.md | 2 +- resources/test/fixtures/pyupgrade/UP018.py | 25 +++++ src/checkers/ast.rs | 3 + src/checks.rs | 6 ++ src/checks_gen.rs | 9 ++ src/pyupgrade/mod.rs | 1 + src/pyupgrade/plugins/mod.rs | 2 + src/pyupgrade/plugins/native_literals.rs | 73 ++++++++++++++ ...uff__pyupgrade__tests__UP018_UP018.py.snap | 95 +++++++++++++++++++ 9 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 resources/test/fixtures/pyupgrade/UP018.py create mode 100644 src/pyupgrade/plugins/native_literals.rs create mode 100644 src/pyupgrade/snapshots/ruff__pyupgrade__tests__UP018_UP018.py.snap diff --git a/README.md b/README.md index 5c0f8102adcb98..929098b8983a0c 100644 --- a/README.md +++ b/README.md @@ -1232,7 +1232,7 @@ natively, including: - [`pep8-naming`](https://pypi.org/project/pep8-naming/) - [`pydocstyle`](https://pypi.org/project/pydocstyle/) - [`pygrep-hooks`](https://github.com/pre-commit/pygrep-hooks) (3/10) -- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (17/33) +- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (18/33) - [`yesqa`](https://github.com/asottile/yesqa) Note that, in some cases, Ruff uses different error code prefixes than would be found in the diff --git a/resources/test/fixtures/pyupgrade/UP018.py b/resources/test/fixtures/pyupgrade/UP018.py new file mode 100644 index 00000000000000..c000447c827b91 --- /dev/null +++ b/resources/test/fixtures/pyupgrade/UP018.py @@ -0,0 +1,25 @@ +# These remain unchanged +str(1) +str(*a) +str("foo", *a) +str(**k) +str("foo", **k) +str("foo", encoding="UTF-8") +str("foo" + "bar") +bytes("foo", encoding="UTF-8") +bytes(*a) +bytes("foo", *a) +bytes("foo", **a) +bytes(b"foo" + b"bar") + +# These become string or byte literals +str() +str("foo") +str(""" +foo""") +bytes() +bytes(b"foo") +bytes(b""" +foo""") diff --git a/src/checkers/ast.rs b/src/checkers/ast.rs index bda2295c44afc2..ddf0ba4328f003 100644 --- a/src/checkers/ast.rs +++ b/src/checkers/ast.rs @@ -1650,6 +1650,9 @@ where if self.settings.enabled.contains(&CheckCode::UP016) { pyupgrade::plugins::remove_six_compat(self, expr); } + if self.settings.enabled.contains(&CheckCode::UP018) { + pyupgrade::plugins::native_literals(self, expr, func, args, keywords); + } // flake8-super if self.settings.enabled.contains(&CheckCode::UP008) { diff --git a/src/checks.rs b/src/checks.rs index 95560a14121423..4bfa57e2487d44 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -226,6 +226,7 @@ pub enum CheckCode { UP015, UP016, UP017, + UP018, // pydocstyle D100, D101, @@ -829,6 +830,7 @@ pub enum CheckKind { RedundantOpenModes, RemoveSixCompat, DatetimeTimezoneUTC, + NativeLiterals, // pydocstyle BlankLineAfterLastSection(String), BlankLineAfterSection(String), @@ -1203,6 +1205,7 @@ impl CheckCode { CheckCode::UP015 => CheckKind::RedundantOpenModes, CheckCode::UP016 => CheckKind::RemoveSixCompat, CheckCode::UP017 => CheckKind::DatetimeTimezoneUTC, + CheckCode::UP018 => CheckKind::NativeLiterals, // pydocstyle CheckCode::D100 => CheckKind::PublicModule, CheckCode::D101 => CheckKind::PublicClass, @@ -1621,6 +1624,7 @@ impl CheckCode { CheckCode::UP015 => CheckCategory::Pyupgrade, CheckCode::UP016 => CheckCategory::Pyupgrade, CheckCode::UP017 => CheckCategory::Pyupgrade, + CheckCode::UP018 => CheckCategory::Pyupgrade, CheckCode::W292 => CheckCategory::Pycodestyle, CheckCode::W605 => CheckCategory::Pycodestyle, CheckCode::YTT101 => CheckCategory::Flake82020, @@ -1832,6 +1836,7 @@ impl CheckKind { CheckKind::RedundantOpenModes => &CheckCode::UP015, CheckKind::RemoveSixCompat => &CheckCode::UP016, CheckKind::DatetimeTimezoneUTC => &CheckCode::UP017, + CheckKind::NativeLiterals => &CheckCode::UP018, // pydocstyle CheckKind::BlankLineAfterLastSection(_) => &CheckCode::D413, CheckKind::BlankLineAfterSection(_) => &CheckCode::D410, @@ -2555,6 +2560,7 @@ impl CheckKind { CheckKind::RedundantOpenModes => "Unnecessary open mode parameters".to_string(), CheckKind::RemoveSixCompat => "Unnecessary `six` compatibility usage".to_string(), CheckKind::DatetimeTimezoneUTC => "Use `datetime.UTC` alias".to_string(), + CheckKind::NativeLiterals => "Unnecessary call to `str` and `bytes`".to_string(), CheckKind::ConvertTypedDictFunctionalToClass(name) => { format!("Convert `{name}` from `TypedDict` functional to class syntax") } diff --git a/src/checks_gen.rs b/src/checks_gen.rs index cd5083ec07e382..8f3011c0836361 100644 --- a/src/checks_gen.rs +++ b/src/checks_gen.rs @@ -513,6 +513,7 @@ pub enum CheckCodePrefix { UP015, UP016, UP017, + UP018, W, W2, W29, @@ -2088,6 +2089,7 @@ impl CheckCodePrefix { CheckCode::UP015, CheckCode::UP016, CheckCode::UP017, + CheckCode::UP018, ] } CheckCodePrefix::U0 => { @@ -2114,6 +2116,7 @@ impl CheckCodePrefix { CheckCode::UP015, CheckCode::UP016, CheckCode::UP017, + CheckCode::UP018, ] } CheckCodePrefix::U00 => { @@ -2222,6 +2225,7 @@ impl CheckCodePrefix { CheckCode::UP015, CheckCode::UP016, CheckCode::UP017, + CheckCode::UP018, ] } CheckCodePrefix::U010 => { @@ -2313,6 +2317,7 @@ impl CheckCodePrefix { CheckCode::UP015, CheckCode::UP016, CheckCode::UP017, + CheckCode::UP018, ], CheckCodePrefix::UP0 => vec![ CheckCode::UP001, @@ -2331,6 +2336,7 @@ impl CheckCodePrefix { CheckCode::UP015, CheckCode::UP016, CheckCode::UP017, + CheckCode::UP018, ], CheckCodePrefix::UP00 => vec![ CheckCode::UP001, @@ -2359,6 +2365,7 @@ impl CheckCodePrefix { CheckCode::UP015, CheckCode::UP016, CheckCode::UP017, + CheckCode::UP018, ], CheckCodePrefix::UP010 => vec![CheckCode::UP010], CheckCodePrefix::UP011 => vec![CheckCode::UP011], @@ -2368,6 +2375,7 @@ impl CheckCodePrefix { CheckCodePrefix::UP015 => vec![CheckCode::UP015], CheckCodePrefix::UP016 => vec![CheckCode::UP016], CheckCodePrefix::UP017 => vec![CheckCode::UP017], + CheckCodePrefix::UP018 => vec![CheckCode::UP018], CheckCodePrefix::W => vec![CheckCode::W292, CheckCode::W605], CheckCodePrefix::W2 => vec![CheckCode::W292], CheckCodePrefix::W29 => vec![CheckCode::W292], @@ -2923,6 +2931,7 @@ impl CheckCodePrefix { CheckCodePrefix::UP015 => SuffixLength::Three, CheckCodePrefix::UP016 => SuffixLength::Three, CheckCodePrefix::UP017 => SuffixLength::Three, + CheckCodePrefix::UP018 => SuffixLength::Three, CheckCodePrefix::W => SuffixLength::Zero, CheckCodePrefix::W2 => SuffixLength::One, CheckCodePrefix::W29 => SuffixLength::Two, diff --git a/src/pyupgrade/mod.rs b/src/pyupgrade/mod.rs index 2ff84fc5622661..aabb47074f2e29 100644 --- a/src/pyupgrade/mod.rs +++ b/src/pyupgrade/mod.rs @@ -37,6 +37,7 @@ mod tests { #[test_case(CheckCode::UP014, Path::new("UP014.py"); "UP014")] #[test_case(CheckCode::UP015, Path::new("UP015.py"); "UP015")] #[test_case(CheckCode::UP016, Path::new("UP016.py"); "UP016")] + #[test_case(CheckCode::UP018, Path::new("UP018.py"); "UP018")] fn checks(check_code: CheckCode, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy()); let mut checks = test_path( diff --git a/src/pyupgrade/plugins/mod.rs b/src/pyupgrade/plugins/mod.rs index e89570085b2d07..8dfd47a2ea4db3 100644 --- a/src/pyupgrade/plugins/mod.rs +++ b/src/pyupgrade/plugins/mod.rs @@ -2,6 +2,7 @@ pub use convert_named_tuple_functional_to_class::convert_named_tuple_functional_ pub use convert_typed_dict_functional_to_class::convert_typed_dict_functional_to_class; pub use datetime_utc_alias::datetime_utc_alias; pub use deprecated_unittest_alias::deprecated_unittest_alias; +pub use native_literals::native_literals; pub use redundant_open_modes::redundant_open_modes; pub use remove_six_compat::remove_six_compat; pub use super_call_with_parameters::super_call_with_parameters; @@ -18,6 +19,7 @@ mod convert_named_tuple_functional_to_class; mod convert_typed_dict_functional_to_class; mod datetime_utc_alias; mod deprecated_unittest_alias; +mod native_literals; mod redundant_open_modes; mod remove_six_compat; mod super_call_with_parameters; diff --git a/src/pyupgrade/plugins/native_literals.rs b/src/pyupgrade/plugins/native_literals.rs new file mode 100644 index 00000000000000..0b3c6b5ae0adcf --- /dev/null +++ b/src/pyupgrade/plugins/native_literals.rs @@ -0,0 +1,73 @@ +use rustpython_ast::{Constant, Expr, ExprKind, Keyword}; +use rustpython_parser::lexer; +use rustpython_parser::lexer::Tok; + +use crate::ast::types::Range; +use crate::autofix::Fix; +use crate::checkers::ast::Checker; +use crate::checks::{Check, CheckCode, CheckKind}; + +/// UP018 +pub fn native_literals( + checker: &mut Checker, + expr: &Expr, + func: &Expr, + args: &[Expr], + keywords: &[Keyword], +) { + let ExprKind::Name { id, .. } = &func.node else { return; }; + + if (id == "str" || id == "bytes") + && checker.is_builtin(id) + && keywords.is_empty() + && args.len() <= 1 + { + let Some(arg) = args.get(0) else { + let mut check = Check::new(CheckKind::NativeLiterals, Range::from_located(expr)); + if checker.patch(&CheckCode::UP018) { + check.amend(Fix::replacement( + format!("{}\"\"", if id == "bytes" { "b" } else { "" }), + expr.location, + expr.end_location.unwrap(), + )); + } + checker.add_check(check); + return; + }; + + if !matches!( + &arg.node, + ExprKind::Constant { + value: Constant::Str(_) | Constant::Bytes(_), + .. + } + ) { + return; + } + + // rust-python merges adjacent string/bytes literals into one node, but we can't + // safely remove the outer call in this situation. We're following pyupgrade + // here and skip. + let arg_code = checker + .locator + .slice_source_code_range(&Range::from_located(arg)); + if lexer::make_tokenizer(&arg_code) + .flatten() + .filter(|(_, tok, _)| matches!(tok, Tok::String { .. } | Tok::Bytes { .. })) + .count() + > 1 + { + return; + } + + let mut check = Check::new(CheckKind::NativeLiterals, Range::from_located(expr)); + if checker.patch(&CheckCode::UP018) { + check.amend(Fix::replacement( + arg_code.to_string(), + expr.location, + expr.end_location.unwrap(), + )); + } + checker.add_check(check); + } +} diff --git a/src/pyupgrade/snapshots/ruff__pyupgrade__tests__UP018_UP018.py.snap b/src/pyupgrade/snapshots/ruff__pyupgrade__tests__UP018_UP018.py.snap new file mode 100644 index 00000000000000..732032d1903c79 --- /dev/null +++ b/src/pyupgrade/snapshots/ruff__pyupgrade__tests__UP018_UP018.py.snap @@ -0,0 +1,95 @@ +--- +source: src/pyupgrade/mod.rs +expression: checks +--- +- kind: NativeLiterals + location: + row: 18 + column: 0 + end_location: + row: 18 + column: 5 + fix: + content: "\"\"" + location: + row: 18 + column: 0 + end_location: + row: 18 + column: 5 +- kind: NativeLiterals + location: + row: 19 + column: 0 + end_location: + row: 19 + column: 10 + fix: + content: "\"foo\"" + location: + row: 19 + column: 0 + end_location: + row: 19 + column: 10 +- kind: NativeLiterals + location: + row: 20 + column: 0 + end_location: + row: 21 + column: 7 + fix: + content: "\"\"\"\nfoo\"\"\"" + location: + row: 20 + column: 0 + end_location: + row: 21 + column: 7 +- kind: NativeLiterals + location: + row: 22 + column: 0 + end_location: + row: 22 + column: 7 + fix: + content: "b\"\"" + location: + row: 22 + column: 0 + end_location: + row: 22 + column: 7 +- kind: NativeLiterals + location: + row: 23 + column: 0 + end_location: + row: 23 + column: 13 + fix: + content: "b\"foo\"" + location: + row: 23 + column: 0 + end_location: + row: 23 + column: 13 +- kind: NativeLiterals + location: + row: 24 + column: 0 + end_location: + row: 25 + column: 7 + fix: + content: "b\"\"\"\nfoo\"\"\"" + location: + row: 24 + column: 0 + end_location: + row: 25 + column: 7 +