Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement "native literals" check from pyupgrade #1350

Merged
merged 2 commits into from Dec 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Expand Up @@ -629,6 +629,7 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
| UP015 | RedundantOpenModes | Unnecessary open mode parameters | 🛠 |
| UP016 | RemoveSixCompat | Unnecessary `six` compatibility usage | 🛠 |
| UP017 | DatetimeTimezoneUTC | Use `datetime.UTC` alias | 🛠 |
| UP018 | NativeLiterals | Unnecessary call to `str` and `bytes` | 🛠 |

### pep8-naming (N)

Expand Down Expand Up @@ -1232,7 +1233,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
Expand Down
25 changes: 25 additions & 0 deletions 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""")
3 changes: 3 additions & 0 deletions src/checkers/ast.rs
Expand Up @@ -1644,6 +1644,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) {
Expand Down
9 changes: 8 additions & 1 deletion src/checks.rs
Expand Up @@ -226,6 +226,7 @@ pub enum CheckCode {
UP015,
UP016,
UP017,
UP018,
// pydocstyle
D100,
D101,
Expand Down Expand Up @@ -829,6 +830,7 @@ pub enum CheckKind {
RedundantOpenModes,
RemoveSixCompat,
DatetimeTimezoneUTC,
NativeLiterals,
// pydocstyle
BlankLineAfterLastSection(String),
BlankLineAfterSection(String),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -2956,6 +2962,7 @@ impl CheckKind {
| CheckKind::ConvertNamedTupleFunctionalToClass(..)
| CheckKind::ConvertTypedDictFunctionalToClass(..)
| CheckKind::DashedUnderlineAfterSection(..)
| CheckKind::DatetimeTimezoneUTC
| CheckKind::DeprecatedUnittestAlias(..)
| CheckKind::DoNotAssertFalse
| CheckKind::DoNotAssignLambda
Expand All @@ -2969,6 +2976,7 @@ impl CheckKind {
| CheckKind::KeyInDict(..)
| CheckKind::MisplacedComparisonConstant(..)
| CheckKind::MissingReturnTypeSpecialMethod(..)
| CheckKind::NativeLiterals
| CheckKind::NewLineAfterLastParagraph
| CheckKind::NewLineAfterSectionName(..)
| CheckKind::NoBlankLineAfterFunction(..)
Expand All @@ -2991,7 +2999,6 @@ impl CheckKind {
| CheckKind::RedundantOpenModes
| CheckKind::RedundantTupleInExceptionHandler(..)
| CheckKind::RemoveSixCompat
| CheckKind::DatetimeTimezoneUTC
| CheckKind::SectionNameEndsInColon(..)
| CheckKind::SectionNotOverIndented(..)
| CheckKind::SectionUnderlineAfterName(..)
Expand Down
9 changes: 9 additions & 0 deletions src/checks_gen.rs
Expand Up @@ -513,6 +513,7 @@ pub enum CheckCodePrefix {
UP015,
UP016,
UP017,
UP018,
W,
W2,
W29,
Expand Down Expand Up @@ -2088,6 +2089,7 @@ impl CheckCodePrefix {
CheckCode::UP015,
CheckCode::UP016,
CheckCode::UP017,
CheckCode::UP018,
]
}
CheckCodePrefix::U0 => {
Expand All @@ -2114,6 +2116,7 @@ impl CheckCodePrefix {
CheckCode::UP015,
CheckCode::UP016,
CheckCode::UP017,
CheckCode::UP018,
]
}
CheckCodePrefix::U00 => {
Expand Down Expand Up @@ -2222,6 +2225,7 @@ impl CheckCodePrefix {
CheckCode::UP015,
CheckCode::UP016,
CheckCode::UP017,
CheckCode::UP018,
]
}
CheckCodePrefix::U010 => {
Expand Down Expand Up @@ -2313,6 +2317,7 @@ impl CheckCodePrefix {
CheckCode::UP015,
CheckCode::UP016,
CheckCode::UP017,
CheckCode::UP018,
],
CheckCodePrefix::UP0 => vec![
CheckCode::UP001,
Expand All @@ -2331,6 +2336,7 @@ impl CheckCodePrefix {
CheckCode::UP015,
CheckCode::UP016,
CheckCode::UP017,
CheckCode::UP018,
],
CheckCodePrefix::UP00 => vec![
CheckCode::UP001,
Expand Down Expand Up @@ -2359,6 +2365,7 @@ impl CheckCodePrefix {
CheckCode::UP015,
CheckCode::UP016,
CheckCode::UP017,
CheckCode::UP018,
],
CheckCodePrefix::UP010 => vec![CheckCode::UP010],
CheckCodePrefix::UP011 => vec![CheckCode::UP011],
Expand All @@ -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],
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/pyupgrade/mod.rs
Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions src/pyupgrade/plugins/mod.rs
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
73 changes: 73 additions & 0 deletions 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")
&& keywords.is_empty()
&& args.len() <= 1
&& checker.is_builtin(id)
{
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);
}
}
@@ -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