mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-26 20:10:09 +00:00
Pyupgrade: Printf string formatting (#1803)
This commit is contained in:
parent
465943adf7
commit
80295f335b
22 changed files with 1249 additions and 36 deletions
97
Cargo.lock
generated
97
Cargo.lock
generated
|
@ -1871,9 +1871,9 @@ dependencies = [
|
||||||
"ropey",
|
"ropey",
|
||||||
"ruff_macros",
|
"ruff_macros",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"rustpython-ast",
|
"rustpython-ast 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c)",
|
||||||
"rustpython-common",
|
"rustpython-common 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c)",
|
||||||
"rustpython-parser",
|
"rustpython-parser 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c)",
|
||||||
"schemars",
|
"schemars",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -1939,9 +1939,9 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"ruff",
|
"ruff",
|
||||||
"ruff_cli",
|
"ruff_cli",
|
||||||
"rustpython-ast",
|
"rustpython-ast 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa)",
|
||||||
"rustpython-common",
|
"rustpython-common 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa)",
|
||||||
"rustpython-parser",
|
"rustpython-parser 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa)",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"strum",
|
"strum",
|
||||||
|
@ -2002,14 +2002,49 @@ dependencies = [
|
||||||
"webpki",
|
"webpki",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustpython-ast"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c#62aa942bf506ea3d41ed0503b947b84141fdaa3c"
|
||||||
|
dependencies = [
|
||||||
|
"num-bigint",
|
||||||
|
"rustpython-common 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c)",
|
||||||
|
"rustpython-compiler-core 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustpython-ast"
|
name = "rustpython-ast"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa#ff90fe52eea578c8ebdd9d95e078cc041a5959fa"
|
source = "git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa#ff90fe52eea578c8ebdd9d95e078cc041a5959fa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-bigint",
|
"num-bigint",
|
||||||
"rustpython-common",
|
"rustpython-common 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa)",
|
||||||
"rustpython-compiler-core",
|
"rustpython-compiler-core 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustpython-common"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c#62aa942bf506ea3d41ed0503b947b84141fdaa3c"
|
||||||
|
dependencies = [
|
||||||
|
"ascii",
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"hexf-parse",
|
||||||
|
"itertools",
|
||||||
|
"lexical-parse-float",
|
||||||
|
"libc",
|
||||||
|
"lock_api",
|
||||||
|
"num-bigint",
|
||||||
|
"num-complex",
|
||||||
|
"num-traits",
|
||||||
|
"once_cell",
|
||||||
|
"radium",
|
||||||
|
"rand",
|
||||||
|
"siphasher",
|
||||||
|
"unic-ucd-category",
|
||||||
|
"volatile",
|
||||||
|
"widestring",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2037,6 +2072,23 @@ dependencies = [
|
||||||
"widestring",
|
"widestring",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustpython-compiler-core"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c#62aa942bf506ea3d41ed0503b947b84141fdaa3c"
|
||||||
|
dependencies = [
|
||||||
|
"bincode",
|
||||||
|
"bitflags",
|
||||||
|
"bstr 0.2.17",
|
||||||
|
"itertools",
|
||||||
|
"lz4_flex",
|
||||||
|
"num-bigint",
|
||||||
|
"num-complex",
|
||||||
|
"num_enum",
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustpython-compiler-core"
|
name = "rustpython-compiler-core"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -2054,6 +2106,31 @@ dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustpython-parser"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c#62aa942bf506ea3d41ed0503b947b84141fdaa3c"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"anyhow",
|
||||||
|
"itertools",
|
||||||
|
"lalrpop",
|
||||||
|
"lalrpop-util",
|
||||||
|
"log",
|
||||||
|
"num-bigint",
|
||||||
|
"num-traits",
|
||||||
|
"phf 0.10.1",
|
||||||
|
"phf_codegen 0.10.0",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustpython-ast 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c)",
|
||||||
|
"rustpython-compiler-core 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c)",
|
||||||
|
"thiserror",
|
||||||
|
"tiny-keccak",
|
||||||
|
"unic-emoji-char",
|
||||||
|
"unic-ucd-ident",
|
||||||
|
"unicode_names2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustpython-parser"
|
name = "rustpython-parser"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -2070,8 +2147,8 @@ dependencies = [
|
||||||
"phf 0.10.1",
|
"phf 0.10.1",
|
||||||
"phf_codegen 0.10.0",
|
"phf_codegen 0.10.0",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"rustpython-ast",
|
"rustpython-ast 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa)",
|
||||||
"rustpython-compiler-core",
|
"rustpython-compiler-core 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa)",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tiny-keccak",
|
"tiny-keccak",
|
||||||
"unic-emoji-char",
|
"unic-emoji-char",
|
||||||
|
|
|
@ -49,9 +49,9 @@ regex = { version = "1.6.0" }
|
||||||
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
|
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
|
||||||
ruff_macros = { version = "0.0.228", path = "ruff_macros" }
|
ruff_macros = { version = "0.0.228", path = "ruff_macros" }
|
||||||
rustc-hash = { version = "1.1.0" }
|
rustc-hash = { version = "1.1.0" }
|
||||||
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "ff90fe52eea578c8ebdd9d95e078cc041a5959fa" }
|
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "62aa942bf506ea3d41ed0503b947b84141fdaa3c" }
|
||||||
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "ff90fe52eea578c8ebdd9d95e078cc041a5959fa" }
|
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "62aa942bf506ea3d41ed0503b947b84141fdaa3c" }
|
||||||
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "ff90fe52eea578c8ebdd9d95e078cc041a5959fa" }
|
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "62aa942bf506ea3d41ed0503b947b84141fdaa3c" }
|
||||||
schemars = { version = "0.8.11" }
|
schemars = { version = "0.8.11" }
|
||||||
semver = { version = "1.0.16" }
|
semver = { version = "1.0.16" }
|
||||||
serde = { version = "1.0.147", features = ["derive"] }
|
serde = { version = "1.0.147", features = ["derive"] }
|
||||||
|
|
|
@ -727,6 +727,7 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
|
||||||
| UP028 | rewrite-yield-from | Replace `yield` over `for` loop with `yield from` | 🛠 |
|
| UP028 | rewrite-yield-from | Replace `yield` over `for` loop with `yield from` | 🛠 |
|
||||||
| UP029 | unnecessary-builtin-import | Unnecessary builtin import: `{import}` | 🛠 |
|
| UP029 | unnecessary-builtin-import | Unnecessary builtin import: `{import}` | 🛠 |
|
||||||
| UP030 | format-literals | Use implicit references for positional format fields | 🛠 |
|
| UP030 | format-literals | Use implicit references for positional format fields | 🛠 |
|
||||||
|
| UP031 | printf-string-formatting | Use format specifiers instead of percent format | 🛠 |
|
||||||
| UP032 | f-string | Use f-string instead of `format` call | 🛠 |
|
| UP032 | f-string | Use f-string instead of `format` call | 🛠 |
|
||||||
| UP033 | functools-cache | Use `@functools.cache` instead of `@functools.lru_cache(maxsize=None)` | 🛠 |
|
| UP033 | functools-cache | Use `@functools.cache` instead of `@functools.lru_cache(maxsize=None)` | 🛠 |
|
||||||
| UP034 | extraneous-parentheses | Avoid extraneous parentheses | 🛠 |
|
| UP034 | extraneous-parentheses | Avoid extraneous parentheses | 🛠 |
|
||||||
|
|
69
resources/test/fixtures/pyupgrade/UP031_0.py
vendored
Normal file
69
resources/test/fixtures/pyupgrade/UP031_0.py
vendored
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
a, b, x, y = 1, 2, 3, 4
|
||||||
|
|
||||||
|
# UP031
|
||||||
|
print('%s %s' % (a, b))
|
||||||
|
|
||||||
|
print('%s%s' % (a, b))
|
||||||
|
|
||||||
|
print("trivial" % ())
|
||||||
|
|
||||||
|
print("%s" % ("simple",))
|
||||||
|
|
||||||
|
print("%s" % ("%s" % ("nested",),))
|
||||||
|
|
||||||
|
print("%s%% percent" % (15,))
|
||||||
|
|
||||||
|
print("%f" % (15,))
|
||||||
|
|
||||||
|
print("%.f" % (15,))
|
||||||
|
|
||||||
|
print("%.3f" % (15,))
|
||||||
|
|
||||||
|
print("%3f" % (15,))
|
||||||
|
|
||||||
|
print("%-5f" % (5,))
|
||||||
|
|
||||||
|
print("%9f" % (5,))
|
||||||
|
|
||||||
|
print("%#o" % (123,))
|
||||||
|
|
||||||
|
print("brace {} %s" % (1,))
|
||||||
|
|
||||||
|
print(
|
||||||
|
"%s" % (
|
||||||
|
"trailing comma",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
print("foo %s " % (x,))
|
||||||
|
|
||||||
|
print("%(k)s" % {"k": "v"})
|
||||||
|
|
||||||
|
print("%(k)s" % {
|
||||||
|
"k": "v",
|
||||||
|
"i": "j"
|
||||||
|
})
|
||||||
|
|
||||||
|
print("%(to_list)s" % {"to_list": []})
|
||||||
|
|
||||||
|
print("%(k)s" % {"k": "v", "i": 1, "j": []})
|
||||||
|
|
||||||
|
print("%(ab)s" % {"a" "b": 1})
|
||||||
|
|
||||||
|
print("%(a)s" % {"a" : 1})
|
||||||
|
|
||||||
|
print((
|
||||||
|
"foo %s "
|
||||||
|
"bar %s" % (x, y)
|
||||||
|
))
|
||||||
|
|
||||||
|
print(
|
||||||
|
"foo %(foo)s "
|
||||||
|
"bar %(bar)s" % {"foo": x, "bar": y}
|
||||||
|
)
|
||||||
|
|
||||||
|
print("%s \N{snowman}" % (a,))
|
||||||
|
|
||||||
|
print("%(foo)s \N{snowman}" % {"foo": 1})
|
||||||
|
|
||||||
|
print(("foo %s " "bar %s") % (x, y))
|
59
resources/test/fixtures/pyupgrade/UP031_1.py
vendored
Normal file
59
resources/test/fixtures/pyupgrade/UP031_1.py
vendored
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
# OK
|
||||||
|
"%s" % unknown_type
|
||||||
|
|
||||||
|
b"%s" % (b"bytestring",)
|
||||||
|
|
||||||
|
"%*s" % (5, "hi")
|
||||||
|
|
||||||
|
"%d" % (flt,)
|
||||||
|
|
||||||
|
"%c" % (some_string,)
|
||||||
|
|
||||||
|
"%4%" % ()
|
||||||
|
|
||||||
|
"%.2r" % (1.25)
|
||||||
|
|
||||||
|
i % 3
|
||||||
|
|
||||||
|
"%.*s" % (5, "hi")
|
||||||
|
|
||||||
|
"%i" % (flt,)
|
||||||
|
|
||||||
|
"%()s" % {"": "empty"}
|
||||||
|
|
||||||
|
"%s" % {"k": "v"}
|
||||||
|
|
||||||
|
"%(1)s" % {"1": "bar"}
|
||||||
|
|
||||||
|
"%(a)s" % {"a": 1, "a": 2}
|
||||||
|
|
||||||
|
pytest.param('"%8s" % (None,)', id="unsafe width-string conversion"),
|
||||||
|
|
||||||
|
"%()s" % {"": "bar"}
|
||||||
|
|
||||||
|
"%(1)s" % {1: 2, "1": 2}
|
||||||
|
|
||||||
|
"%(and)s" % {"and": 2}
|
||||||
|
|
||||||
|
# OK (arguably false negatives)
|
||||||
|
(
|
||||||
|
"foo %s "
|
||||||
|
"bar %s"
|
||||||
|
) % (x, y)
|
||||||
|
|
||||||
|
(
|
||||||
|
"foo %(foo)s "
|
||||||
|
"bar %(bar)s"
|
||||||
|
) % {"foo": x, "bar": y}
|
||||||
|
|
||||||
|
(
|
||||||
|
"""foo %s"""
|
||||||
|
% (x,)
|
||||||
|
)
|
||||||
|
|
||||||
|
(
|
||||||
|
"""
|
||||||
|
foo %s
|
||||||
|
"""
|
||||||
|
% (x,)
|
||||||
|
)
|
|
@ -1780,6 +1780,7 @@
|
||||||
"UP029",
|
"UP029",
|
||||||
"UP03",
|
"UP03",
|
||||||
"UP030",
|
"UP030",
|
||||||
|
"UP031",
|
||||||
"UP032",
|
"UP032",
|
||||||
"UP033",
|
"UP033",
|
||||||
"UP034",
|
"UP034",
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -13,6 +13,7 @@ Please use `python -m pip install .` instead.
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
"abc".isidentifier()
|
||||||
|
|
||||||
# The below code will never execute, however GitHub is particularly
|
# The below code will never execute, however GitHub is particularly
|
||||||
# picky about where it finds Python packaging metadata.
|
# picky about where it finds Python packaging metadata.
|
||||||
|
|
|
@ -2627,7 +2627,7 @@ where
|
||||||
.enabled(&Rule::PercentFormatUnsupportedFormatCharacter)
|
.enabled(&Rule::PercentFormatUnsupportedFormatCharacter)
|
||||||
{
|
{
|
||||||
let location = Range::from_located(expr);
|
let location = Range::from_located(expr);
|
||||||
match pyflakes::cformat::CFormatSummary::try_from(value.as_ref()) {
|
match pyflakes::cformat::CFormatSummary::try_from(value.as_str()) {
|
||||||
Err(CFormatError {
|
Err(CFormatError {
|
||||||
typ: CFormatErrorType::UnsupportedFormatChar(c),
|
typ: CFormatErrorType::UnsupportedFormatChar(c),
|
||||||
..
|
..
|
||||||
|
@ -2722,6 +2722,10 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.settings.rules.enabled(&Rule::PrintfStringFormatting) {
|
||||||
|
pyupgrade::rules::printf_string_formatting(self, expr, left, right);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ExprKind::BinOp {
|
ExprKind::BinOp {
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
use once_cell::sync::Lazy;
|
/// Returns `true` if a string is a valid Python identifier (e.g., variable
|
||||||
use regex::Regex;
|
/// name).
|
||||||
|
pub fn is_identifier(s: &str) -> bool {
|
||||||
|
// Is the first character a letter or underscore?
|
||||||
|
if !s
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.map_or(false, |c| c.is_alphabetic() || c == '_')
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
pub static IDENTIFIER_REGEX: Lazy<Regex> =
|
// Are the rest of the characters letters, digits, or underscores?
|
||||||
Lazy::new(|| Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*$").unwrap());
|
s.chars().skip(1).all(|c| c.is_alphanumeric() || c == '_')
|
||||||
|
}
|
||||||
|
|
|
@ -252,6 +252,7 @@ ruff_macros::define_rule_mapping!(
|
||||||
UP028 => violations::RewriteYieldFrom,
|
UP028 => violations::RewriteYieldFrom,
|
||||||
UP029 => violations::UnnecessaryBuiltinImport,
|
UP029 => violations::UnnecessaryBuiltinImport,
|
||||||
UP030 => violations::FormatLiterals,
|
UP030 => violations::FormatLiterals,
|
||||||
|
UP031 => violations::PrintfStringFormatting,
|
||||||
UP032 => violations::FString,
|
UP032 => violations::FString,
|
||||||
UP033 => violations::FunctoolsCache,
|
UP033 => violations::FunctoolsCache,
|
||||||
UP034 => violations::ExtraneousParentheses,
|
UP034 => violations::ExtraneousParentheses,
|
||||||
|
|
|
@ -3,7 +3,7 @@ use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Location};
|
||||||
use crate::ast::types::Range;
|
use crate::ast::types::Range;
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
use crate::fix::Fix;
|
use crate::fix::Fix;
|
||||||
use crate::python::identifiers::IDENTIFIER_REGEX;
|
use crate::python::identifiers::is_identifier;
|
||||||
use crate::python::keyword::KWLIST;
|
use crate::python::keyword::KWLIST;
|
||||||
use crate::registry::Diagnostic;
|
use crate::registry::Diagnostic;
|
||||||
use crate::source_code::Generator;
|
use crate::source_code::Generator;
|
||||||
|
@ -38,7 +38,7 @@ pub fn getattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, ar
|
||||||
} = &arg.node else {
|
} = &arg.node else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if !IDENTIFIER_REGEX.is_match(value) {
|
if !is_identifier(value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if KWLIST.contains(&value.as_str()) {
|
if KWLIST.contains(&value.as_str()) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Location, Stmt, Stmt
|
||||||
use crate::ast::types::Range;
|
use crate::ast::types::Range;
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
use crate::fix::Fix;
|
use crate::fix::Fix;
|
||||||
use crate::python::identifiers::IDENTIFIER_REGEX;
|
use crate::python::identifiers::is_identifier;
|
||||||
use crate::python::keyword::KWLIST;
|
use crate::python::keyword::KWLIST;
|
||||||
use crate::registry::Diagnostic;
|
use crate::registry::Diagnostic;
|
||||||
use crate::source_code::{Generator, Stylist};
|
use crate::source_code::{Generator, Stylist};
|
||||||
|
@ -49,7 +49,7 @@ pub fn setattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, ar
|
||||||
} = &name.node else {
|
} = &name.node else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if !IDENTIFIER_REGEX.is_match(name) {
|
if !is_identifier(name) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if KWLIST.contains(&name.as_str()) {
|
if KWLIST.contains(&name.as_str()) {
|
||||||
|
|
|
@ -4,7 +4,7 @@ use std::str::FromStr;
|
||||||
|
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
use rustpython_common::cformat::{
|
use rustpython_common::cformat::{
|
||||||
CFormatError, CFormatPart, CFormatQuantity, CFormatSpec, CFormatString,
|
CFormatError, CFormatPart, CFormatPrecision, CFormatQuantity, CFormatSpec, CFormatString,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) struct CFormatSummary {
|
pub(crate) struct CFormatSummary {
|
||||||
|
@ -13,12 +13,8 @@ pub(crate) struct CFormatSummary {
|
||||||
pub keywords: FxHashSet<String>,
|
pub keywords: FxHashSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for CFormatSummary {
|
impl From<&CFormatString> for CFormatSummary {
|
||||||
type Error = CFormatError;
|
fn from(format_string: &CFormatString) -> Self {
|
||||||
|
|
||||||
fn try_from(literal: &str) -> Result<Self, Self::Error> {
|
|
||||||
let format_string = CFormatString::from_str(literal)?;
|
|
||||||
|
|
||||||
let mut starred = false;
|
let mut starred = false;
|
||||||
let mut num_positional = 0;
|
let mut num_positional = 0;
|
||||||
let mut keywords = FxHashSet::default();
|
let mut keywords = FxHashSet::default();
|
||||||
|
@ -45,17 +41,26 @@ impl TryFrom<&str> for CFormatSummary {
|
||||||
num_positional += 1;
|
num_positional += 1;
|
||||||
starred = true;
|
starred = true;
|
||||||
}
|
}
|
||||||
if precision == &Some(CFormatQuantity::FromValuesTuple) {
|
if precision == &Some(CFormatPrecision::Quantity(CFormatQuantity::FromValuesTuple)) {
|
||||||
num_positional += 1;
|
num_positional += 1;
|
||||||
starred = true;
|
starred = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(CFormatSummary {
|
Self {
|
||||||
starred,
|
starred,
|
||||||
num_positional,
|
num_positional,
|
||||||
keywords,
|
keywords,
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for CFormatSummary {
|
||||||
|
type Error = CFormatError;
|
||||||
|
|
||||||
|
fn try_from(literal: &str) -> Result<Self, Self::Error> {
|
||||||
|
let format_string = CFormatString::from_str(literal)?;
|
||||||
|
Ok(Self::from(&format_string))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
21
src/rules/pyupgrade/helpers.rs
Normal file
21
src/rules/pyupgrade/helpers.rs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use regex::{Captures, Regex};
|
||||||
|
|
||||||
|
static CURLY_ESCAPE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\\N\{[^}]+})|([{}])").unwrap());
|
||||||
|
|
||||||
|
pub fn curly_escape(text: &str) -> String {
|
||||||
|
// We don't support emojis right now.
|
||||||
|
CURLY_ESCAPE
|
||||||
|
.replace_all(text, |caps: &Captures| {
|
||||||
|
if let Some(match_) = caps.get(1) {
|
||||||
|
match_.as_str().to_string()
|
||||||
|
} else {
|
||||||
|
if &caps[2] == "{" {
|
||||||
|
"{{".to_string()
|
||||||
|
} else {
|
||||||
|
"}}".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.to_string()
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
//! Rules from [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/).
|
//! Rules from [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/).
|
||||||
mod fixes;
|
mod fixes;
|
||||||
|
mod helpers;
|
||||||
pub(crate) mod rules;
|
pub(crate) mod rules;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
pub(crate) mod types;
|
pub(crate) mod types;
|
||||||
|
@ -52,6 +53,8 @@ mod tests {
|
||||||
#[test_case(Rule::UnnecessaryBuiltinImport, Path::new("UP029.py"); "UP029")]
|
#[test_case(Rule::UnnecessaryBuiltinImport, Path::new("UP029.py"); "UP029")]
|
||||||
#[test_case(Rule::FormatLiterals, Path::new("UP030_0.py"); "UP030_0")]
|
#[test_case(Rule::FormatLiterals, Path::new("UP030_0.py"); "UP030_0")]
|
||||||
#[test_case(Rule::FormatLiterals, Path::new("UP030_1.py"); "UP030_1")]
|
#[test_case(Rule::FormatLiterals, Path::new("UP030_1.py"); "UP030_1")]
|
||||||
|
#[test_case(Rule::PrintfStringFormatting, Path::new("UP031_0.py"); "UP031_0")]
|
||||||
|
#[test_case(Rule::PrintfStringFormatting, Path::new("UP031_1.py"); "UP031_1")]
|
||||||
#[test_case(Rule::FString, Path::new("UP032.py"); "UP032")]
|
#[test_case(Rule::FString, Path::new("UP032.py"); "UP032")]
|
||||||
#[test_case(Rule::FunctoolsCache, Path::new("UP033.py"); "UP033")]
|
#[test_case(Rule::FunctoolsCache, Path::new("UP033.py"); "UP033")]
|
||||||
#[test_case(Rule::ExtraneousParentheses, Path::new("UP034.py"); "UP034")]
|
#[test_case(Rule::ExtraneousParentheses, Path::new("UP034.py"); "UP034")]
|
||||||
|
|
|
@ -6,7 +6,7 @@ use crate::ast::helpers::{create_expr, create_stmt, unparse_stmt};
|
||||||
use crate::ast::types::Range;
|
use crate::ast::types::Range;
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
use crate::fix::Fix;
|
use crate::fix::Fix;
|
||||||
use crate::python::identifiers::IDENTIFIER_REGEX;
|
use crate::python::identifiers::is_identifier;
|
||||||
use crate::python::keyword::KWLIST;
|
use crate::python::keyword::KWLIST;
|
||||||
use crate::registry::Diagnostic;
|
use crate::registry::Diagnostic;
|
||||||
use crate::source_code::Stylist;
|
use crate::source_code::Stylist;
|
||||||
|
@ -104,7 +104,7 @@ fn create_properties_from_args(args: &[Expr], defaults: &[Expr]) -> Result<Vec<S
|
||||||
} = &field_name.node else {
|
} = &field_name.node else {
|
||||||
bail!("Expected `field_name` to be `Constant::Str`")
|
bail!("Expected `field_name` to be `Constant::Str`")
|
||||||
};
|
};
|
||||||
if !IDENTIFIER_REGEX.is_match(property) || KWLIST.contains(&property.as_str()) {
|
if !is_identifier(property) || KWLIST.contains(&property.as_str()) {
|
||||||
bail!("Invalid property name: {}", property)
|
bail!("Invalid property name: {}", property)
|
||||||
}
|
}
|
||||||
Ok(create_property_assignment_stmt(
|
Ok(create_property_assignment_stmt(
|
||||||
|
|
|
@ -6,7 +6,7 @@ use crate::ast::helpers::{create_expr, create_stmt, unparse_stmt};
|
||||||
use crate::ast::types::Range;
|
use crate::ast::types::Range;
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
use crate::fix::Fix;
|
use crate::fix::Fix;
|
||||||
use crate::python::identifiers::IDENTIFIER_REGEX;
|
use crate::python::identifiers::is_identifier;
|
||||||
use crate::python::keyword::KWLIST;
|
use crate::python::keyword::KWLIST;
|
||||||
use crate::registry::Diagnostic;
|
use crate::registry::Diagnostic;
|
||||||
use crate::source_code::Stylist;
|
use crate::source_code::Stylist;
|
||||||
|
@ -86,7 +86,7 @@ fn properties_from_dict_literal(keys: &[Expr], values: &[Expr]) -> Result<Vec<St
|
||||||
value: Constant::Str(property),
|
value: Constant::Str(property),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
if IDENTIFIER_REGEX.is_match(property) && !KWLIST.contains(&property.as_str()) {
|
if is_identifier(property) && !KWLIST.contains(&property.as_str()) {
|
||||||
Ok(create_property_assignment_stmt(property, &value.node))
|
Ok(create_property_assignment_stmt(property, &value.node))
|
||||||
} else {
|
} else {
|
||||||
bail!("Property name is not valid identifier: {}", property)
|
bail!("Property name is not valid identifier: {}", property)
|
||||||
|
|
|
@ -11,6 +11,7 @@ pub(crate) use native_literals::native_literals;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
pub(crate) use open_alias::open_alias;
|
pub(crate) use open_alias::open_alias;
|
||||||
pub(crate) use os_error_alias::os_error_alias;
|
pub(crate) use os_error_alias::os_error_alias;
|
||||||
|
pub(crate) use printf_string_formatting::printf_string_formatting;
|
||||||
pub(crate) use redundant_open_modes::redundant_open_modes;
|
pub(crate) use redundant_open_modes::redundant_open_modes;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
pub(crate) use remove_six_compat::remove_six_compat;
|
pub(crate) use remove_six_compat::remove_six_compat;
|
||||||
|
@ -52,6 +53,7 @@ mod lru_cache_without_parameters;
|
||||||
mod native_literals;
|
mod native_literals;
|
||||||
mod open_alias;
|
mod open_alias;
|
||||||
mod os_error_alias;
|
mod os_error_alias;
|
||||||
|
mod printf_string_formatting;
|
||||||
mod redundant_open_modes;
|
mod redundant_open_modes;
|
||||||
mod remove_six_compat;
|
mod remove_six_compat;
|
||||||
mod replace_stdout_stderr;
|
mod replace_stdout_stderr;
|
||||||
|
|
458
src/rules/pyupgrade/rules/printf_string_formatting.rs
Normal file
458
src/rules/pyupgrade/rules/printf_string_formatting.rs
Normal file
|
@ -0,0 +1,458 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use rustpython_ast::Location;
|
||||||
|
use rustpython_common::cformat::{
|
||||||
|
CConversionFlags, CFormatPart, CFormatPrecision, CFormatQuantity, CFormatString,
|
||||||
|
};
|
||||||
|
use rustpython_parser::ast::{Constant, Expr, ExprKind};
|
||||||
|
use rustpython_parser::lexer;
|
||||||
|
use rustpython_parser::lexer::Tok;
|
||||||
|
|
||||||
|
use crate::ast::types::Range;
|
||||||
|
use crate::ast::whitespace::indentation;
|
||||||
|
use crate::checkers::ast::Checker;
|
||||||
|
use crate::fix::Fix;
|
||||||
|
use crate::python::identifiers::is_identifier;
|
||||||
|
use crate::python::keyword::KWLIST;
|
||||||
|
use crate::registry::{Diagnostic, Rule};
|
||||||
|
use crate::rules::pydocstyle::helpers::{leading_quote, trailing_quote};
|
||||||
|
use crate::rules::pyupgrade::helpers::curly_escape;
|
||||||
|
use crate::violations;
|
||||||
|
|
||||||
|
fn simplify_conversion_flag(flags: CConversionFlags) -> String {
|
||||||
|
let mut flag_string = String::new();
|
||||||
|
if flags.contains(CConversionFlags::LEFT_ADJUST) {
|
||||||
|
flag_string.push('<');
|
||||||
|
}
|
||||||
|
if flags.contains(CConversionFlags::SIGN_CHAR) {
|
||||||
|
flag_string.push('+');
|
||||||
|
}
|
||||||
|
if flags.contains(CConversionFlags::ALTERNATE_FORM) {
|
||||||
|
flag_string.push('#');
|
||||||
|
}
|
||||||
|
if flags.contains(CConversionFlags::BLANK_SIGN) {
|
||||||
|
if !flags.contains(CConversionFlags::SIGN_CHAR) {
|
||||||
|
flag_string.push(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if flags.contains(CConversionFlags::ZERO_PAD) {
|
||||||
|
if !flags.contains(CConversionFlags::LEFT_ADJUST) {
|
||||||
|
flag_string.push('0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flag_string
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a [`PercentFormat`] struct into a `String`.
|
||||||
|
fn handle_part(part: &CFormatPart<String>) -> String {
|
||||||
|
match part {
|
||||||
|
CFormatPart::Literal(item) => curly_escape(item),
|
||||||
|
CFormatPart::Spec(spec) => {
|
||||||
|
let mut format_string = String::new();
|
||||||
|
|
||||||
|
// TODO(charlie): What case is this?
|
||||||
|
if spec.format_char == '%' {
|
||||||
|
format_string.push('%');
|
||||||
|
return format_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
format_string.push('{');
|
||||||
|
|
||||||
|
// Ex) `{foo}`
|
||||||
|
if let Some(key_item) = &spec.mapping_key {
|
||||||
|
format_string.push_str(key_item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !spec.flags.is_empty()
|
||||||
|
|| spec.min_field_width.is_some()
|
||||||
|
|| spec.precision.is_some()
|
||||||
|
|| (spec.format_char != 's' && spec.format_char != 'r' && spec.format_char != 'a')
|
||||||
|
{
|
||||||
|
format_string.push(':');
|
||||||
|
|
||||||
|
if !spec.flags.is_empty() {
|
||||||
|
format_string.push_str(&simplify_conversion_flag(spec.flags));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(width) = &spec.min_field_width {
|
||||||
|
let amount = match width {
|
||||||
|
CFormatQuantity::Amount(amount) => amount,
|
||||||
|
CFormatQuantity::FromValuesTuple => {
|
||||||
|
unreachable!("FromValuesTuple is unsupported")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
format_string.push_str(&amount.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(precision) = &spec.precision {
|
||||||
|
match precision {
|
||||||
|
CFormatPrecision::Quantity(quantity) => match quantity {
|
||||||
|
CFormatQuantity::Amount(amount) => {
|
||||||
|
format_string.push('.');
|
||||||
|
format_string.push_str(&amount.to_string());
|
||||||
|
}
|
||||||
|
CFormatQuantity::FromValuesTuple => {
|
||||||
|
unreachable!("Width should be a usize")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CFormatPrecision::Dot => {
|
||||||
|
format_string.push('.');
|
||||||
|
format_string.push('0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if spec.format_char != 's' && spec.format_char != 'r' && spec.format_char != 'a' {
|
||||||
|
format_string.push(spec.format_char);
|
||||||
|
}
|
||||||
|
if spec.format_char == 'r' || spec.format_char == 'a' {
|
||||||
|
format_string.push('!');
|
||||||
|
format_string.push(spec.format_char);
|
||||||
|
}
|
||||||
|
format_string.push('}');
|
||||||
|
format_string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a [`CFormatString`] into a `String`.
|
||||||
|
fn percent_to_format(format_string: &CFormatString) -> String {
|
||||||
|
let mut contents = String::new();
|
||||||
|
for (.., format_part) in format_string.iter() {
|
||||||
|
contents.push_str(&handle_part(format_part));
|
||||||
|
}
|
||||||
|
contents
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If a tuple has one argument, remove the comma; otherwise, return it as-is.
|
||||||
|
fn clean_params_tuple(checker: &mut Checker, right: &Expr) -> String {
|
||||||
|
let mut contents = checker
|
||||||
|
.locator
|
||||||
|
.slice_source_code_range(&Range::from_located(right))
|
||||||
|
.to_string();
|
||||||
|
if let ExprKind::Tuple { elts, .. } = &right.node {
|
||||||
|
if elts.len() == 1 {
|
||||||
|
if right.location.row() == right.end_location.unwrap().row() {
|
||||||
|
for (i, character) in contents.chars().rev().enumerate() {
|
||||||
|
if character == ',' {
|
||||||
|
let correct_index = contents.len() - i - 1;
|
||||||
|
contents.remove(correct_index);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contents
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a dictionary to a function call while preserving as much styling as
|
||||||
|
/// possible.
|
||||||
|
fn clean_params_dictionary(checker: &mut Checker, right: &Expr) -> Option<String> {
|
||||||
|
let is_multi_line = right.location.row() < right.end_location.unwrap().row();
|
||||||
|
let mut contents = String::new();
|
||||||
|
if let ExprKind::Dict { keys, values } = &right.node {
|
||||||
|
let mut arguments: Vec<String> = vec![];
|
||||||
|
let mut seen: Vec<&str> = vec![];
|
||||||
|
let mut indent = None;
|
||||||
|
for (key, value) in keys.iter().zip(values.iter()) {
|
||||||
|
if let ExprKind::Constant {
|
||||||
|
value: Constant::Str(key_string),
|
||||||
|
..
|
||||||
|
} = &key.node
|
||||||
|
{
|
||||||
|
// If the dictionary key is not a valid variable name, abort.
|
||||||
|
if !is_identifier(key_string) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// If the key is a Python keyword, abort.
|
||||||
|
if KWLIST.contains(&key_string.as_str()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// If there are multiple entries of the same key, abort.
|
||||||
|
if seen.contains(&key_string.as_str()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
seen.push(key_string);
|
||||||
|
let mut contents = String::new();
|
||||||
|
if is_multi_line {
|
||||||
|
if indent.is_none() {
|
||||||
|
indent = indentation(checker.locator, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let value_string = checker
|
||||||
|
.locator
|
||||||
|
.slice_source_code_range(&Range::from_located(value));
|
||||||
|
contents.push_str(key_string);
|
||||||
|
contents.push('=');
|
||||||
|
contents.push_str(&value_string);
|
||||||
|
arguments.push(contents);
|
||||||
|
} else {
|
||||||
|
// If there are any non-string keys, abort.
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we couldn't parse out key values, abort.
|
||||||
|
if arguments.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
contents.push('(');
|
||||||
|
if is_multi_line {
|
||||||
|
let Some(indent) = indent else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
for item in &arguments {
|
||||||
|
contents.push('\n');
|
||||||
|
contents.push_str(&indent);
|
||||||
|
contents.push_str(item);
|
||||||
|
contents.push(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
contents.push('\n');
|
||||||
|
|
||||||
|
// For the ending parentheses, go back one indent.
|
||||||
|
let default_indent: &str = checker.stylist.indentation();
|
||||||
|
if let Some(ident) = indent.strip_prefix(default_indent) {
|
||||||
|
contents.push_str(ident);
|
||||||
|
} else {
|
||||||
|
contents.push_str(&indent);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
contents.push_str(&arguments.join(", "));
|
||||||
|
}
|
||||||
|
contents.push(')');
|
||||||
|
}
|
||||||
|
Some(contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the sequence of [`PercentFormatPart`] indicate that an
|
||||||
|
/// [`Expr`] can be converted.
|
||||||
|
fn convertible(format_string: &CFormatString, params: &Expr) -> bool {
|
||||||
|
for (.., format_part) in format_string.iter() {
|
||||||
|
let CFormatPart::Spec(ref fmt) = format_part else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// These require out-of-order parameter consumption.
|
||||||
|
if matches!(fmt.min_field_width, Some(CFormatQuantity::FromValuesTuple)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if matches!(
|
||||||
|
fmt.precision,
|
||||||
|
Some(CFormatPrecision::Quantity(CFormatQuantity::FromValuesTuple))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// These conversions require modification of parameters.
|
||||||
|
if fmt.format_char == 'd'
|
||||||
|
|| fmt.format_char == 'i'
|
||||||
|
|| fmt.format_char == 'u'
|
||||||
|
|| fmt.format_char == 'c'
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No equivalent in format.
|
||||||
|
if fmt.mapping_key.as_ref().map_or(false, String::is_empty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_nontrivial =
|
||||||
|
!fmt.flags.is_empty() || fmt.min_field_width.is_some() || fmt.precision.is_some();
|
||||||
|
|
||||||
|
// Conversion is subject to modifiers.
|
||||||
|
if is_nontrivial && fmt.format_char == '%' {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No equivalent in `format`.
|
||||||
|
if is_nontrivial && (fmt.format_char == 'a' || fmt.format_char == 'r') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "%s" with None and width is not supported.
|
||||||
|
if fmt.min_field_width.is_some() && fmt.format_char == 's' {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All dict substitutions must be named.
|
||||||
|
if let ExprKind::Dict { .. } = ¶ms.node {
|
||||||
|
if fmt.mapping_key.is_none() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// UP031
|
||||||
|
pub(crate) fn printf_string_formatting(
|
||||||
|
checker: &mut Checker,
|
||||||
|
expr: &Expr,
|
||||||
|
left: &Expr,
|
||||||
|
right: &Expr,
|
||||||
|
) {
|
||||||
|
// If the modulo symbol is on a separate line, abort.
|
||||||
|
if right.location.row() != left.end_location.unwrap().row() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab each string segment (in case there's an implicit concatenation).
|
||||||
|
let mut strings: Vec<(Location, Location)> = vec![];
|
||||||
|
let mut extension = None;
|
||||||
|
for (start, tok, end) in lexer::make_tokenizer_located(
|
||||||
|
&checker
|
||||||
|
.locator
|
||||||
|
.slice_source_code_range(&Range::from_located(expr)),
|
||||||
|
expr.location,
|
||||||
|
)
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
if matches!(tok, Tok::String { .. }) {
|
||||||
|
strings.push((start, end));
|
||||||
|
} else if matches!(tok, Tok::Rpar) {
|
||||||
|
// If we hit a right paren, we have to preserve it.
|
||||||
|
extension = Some((start, end));
|
||||||
|
} else if matches!(tok, Tok::Percent) {
|
||||||
|
// Break as soon as we find the modulo symbol.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no string segments, abort.
|
||||||
|
if strings.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse each string segment.
|
||||||
|
let mut format_strings = vec![];
|
||||||
|
for (start, end) in &strings {
|
||||||
|
let string = checker
|
||||||
|
.locator
|
||||||
|
.slice_source_code_range(&Range::new(*start, *end));
|
||||||
|
let (Some(leader), Some(trailer)) = (leading_quote(&string), trailing_quote(&string)) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let string = &string[leader.len()..string.len() - trailer.len()];
|
||||||
|
|
||||||
|
// Parse the format string (e.g. `"%s"`) into a list of `PercentFormat`.
|
||||||
|
let Ok(format_string) = CFormatString::from_str(string) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !convertible(&format_string, right) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let format_string = percent_to_format(&format_string);
|
||||||
|
format_strings.push(format!("{leader}{format_string}{trailer}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the parameters.
|
||||||
|
let params_string = match right.node {
|
||||||
|
ExprKind::Tuple { .. } => clean_params_tuple(checker, right),
|
||||||
|
ExprKind::Dict { .. } => {
|
||||||
|
if let Some(params_string) = clean_params_dictionary(checker, right) {
|
||||||
|
params_string
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reconstruct the string.
|
||||||
|
let mut contents = String::new();
|
||||||
|
let mut prev = None;
|
||||||
|
for ((start, end), format_string) in strings.iter().zip(format_strings) {
|
||||||
|
// Add the content before the string segment.
|
||||||
|
match prev {
|
||||||
|
None => {
|
||||||
|
contents.push_str(
|
||||||
|
&checker
|
||||||
|
.locator
|
||||||
|
.slice_source_code_range(&Range::new(expr.location, *start)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some(prev) => {
|
||||||
|
contents.push_str(
|
||||||
|
&checker
|
||||||
|
.locator
|
||||||
|
.slice_source_code_range(&Range::new(prev, *start)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add the string itself.
|
||||||
|
contents.push_str(&format_string);
|
||||||
|
prev = Some(*end);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((.., end)) = extension {
|
||||||
|
contents.push_str(
|
||||||
|
&checker
|
||||||
|
.locator
|
||||||
|
.slice_source_code_range(&Range::new(prev.unwrap(), end)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the `.format` call.
|
||||||
|
contents.push_str(&format!(".format{params_string}"));
|
||||||
|
|
||||||
|
let mut diagnostic = Diagnostic::new(
|
||||||
|
violations::PrintfStringFormatting,
|
||||||
|
Range::from_located(expr),
|
||||||
|
);
|
||||||
|
if checker.patch(&Rule::PrintfStringFormatting) {
|
||||||
|
diagnostic.amend(Fix::replacement(
|
||||||
|
contents,
|
||||||
|
expr.location,
|
||||||
|
expr.end_location.unwrap(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
checker.diagnostics.push(diagnostic);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use test_case::test_case;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test_case("\"%s\"", "\"{}\""; "simple string")]
|
||||||
|
#[test_case("\"%%%s\"", "\"%{}\""; "three percents")]
|
||||||
|
#[test_case("\"%(foo)s\"", "\"{foo}\""; "word in string")]
|
||||||
|
#[test_case("\"%2f\"", "\"{:2f}\""; "formatting in string")]
|
||||||
|
#[test_case("\"%r\"", "\"{!r}\""; "format an r")]
|
||||||
|
#[test_case("\"%a\"", "\"{!a}\""; "format an a")]
|
||||||
|
fn test_percent_to_format(sample: &str, expected: &str) {
|
||||||
|
let format_string = CFormatString::from_str(sample).unwrap();
|
||||||
|
let actual = percent_to_format(&format_string);
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preserve_blanks() {
|
||||||
|
assert_eq!(
|
||||||
|
simplify_conversion_flag(CConversionFlags::empty()),
|
||||||
|
String::new()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preserve_space() {
|
||||||
|
assert_eq!(
|
||||||
|
simplify_conversion_flag(CConversionFlags::BLANK_SIGN),
|
||||||
|
" ".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complex_format() {
|
||||||
|
assert_eq!(
|
||||||
|
simplify_conversion_flag(CConversionFlags::all()),
|
||||||
|
"<+#".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,481 @@
|
||||||
|
---
|
||||||
|
source: src/rules/pyupgrade/mod.rs
|
||||||
|
expression: diagnostics
|
||||||
|
---
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 4
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 4
|
||||||
|
column: 22
|
||||||
|
fix:
|
||||||
|
content: "'{} {}'.format(a, b)"
|
||||||
|
location:
|
||||||
|
row: 4
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 4
|
||||||
|
column: 22
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 6
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 6
|
||||||
|
column: 21
|
||||||
|
fix:
|
||||||
|
content: "'{}{}'.format(a, b)"
|
||||||
|
location:
|
||||||
|
row: 6
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 6
|
||||||
|
column: 21
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 8
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 8
|
||||||
|
column: 20
|
||||||
|
fix:
|
||||||
|
content: "\"trivial\".format()"
|
||||||
|
location:
|
||||||
|
row: 8
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 8
|
||||||
|
column: 20
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 10
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 10
|
||||||
|
column: 24
|
||||||
|
fix:
|
||||||
|
content: "\"{}\".format(\"simple\")"
|
||||||
|
location:
|
||||||
|
row: 10
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 10
|
||||||
|
column: 24
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 12
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 12
|
||||||
|
column: 34
|
||||||
|
fix:
|
||||||
|
content: "\"{}\".format(\"%s\" % (\"nested\",))"
|
||||||
|
location:
|
||||||
|
row: 12
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 12
|
||||||
|
column: 34
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 12
|
||||||
|
column: 14
|
||||||
|
end_location:
|
||||||
|
row: 12
|
||||||
|
column: 32
|
||||||
|
fix:
|
||||||
|
content: "\"{}\".format(\"nested\")"
|
||||||
|
location:
|
||||||
|
row: 12
|
||||||
|
column: 14
|
||||||
|
end_location:
|
||||||
|
row: 12
|
||||||
|
column: 32
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 14
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 14
|
||||||
|
column: 28
|
||||||
|
fix:
|
||||||
|
content: "\"{}% percent\".format(15)"
|
||||||
|
location:
|
||||||
|
row: 14
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 14
|
||||||
|
column: 28
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 16
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 16
|
||||||
|
column: 18
|
||||||
|
fix:
|
||||||
|
content: "\"{:f}\".format(15)"
|
||||||
|
location:
|
||||||
|
row: 16
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 16
|
||||||
|
column: 18
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 18
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 18
|
||||||
|
column: 19
|
||||||
|
fix:
|
||||||
|
content: "\"{:.0f}\".format(15)"
|
||||||
|
location:
|
||||||
|
row: 18
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 18
|
||||||
|
column: 19
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 20
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 20
|
||||||
|
column: 20
|
||||||
|
fix:
|
||||||
|
content: "\"{:.3f}\".format(15)"
|
||||||
|
location:
|
||||||
|
row: 20
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 20
|
||||||
|
column: 20
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 22
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 22
|
||||||
|
column: 19
|
||||||
|
fix:
|
||||||
|
content: "\"{:3f}\".format(15)"
|
||||||
|
location:
|
||||||
|
row: 22
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 22
|
||||||
|
column: 19
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 24
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 24
|
||||||
|
column: 19
|
||||||
|
fix:
|
||||||
|
content: "\"{:<5f}\".format(5)"
|
||||||
|
location:
|
||||||
|
row: 24
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 24
|
||||||
|
column: 19
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 26
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 26
|
||||||
|
column: 18
|
||||||
|
fix:
|
||||||
|
content: "\"{:9f}\".format(5)"
|
||||||
|
location:
|
||||||
|
row: 26
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 26
|
||||||
|
column: 18
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 28
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 28
|
||||||
|
column: 20
|
||||||
|
fix:
|
||||||
|
content: "\"{:#o}\".format(123)"
|
||||||
|
location:
|
||||||
|
row: 28
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 28
|
||||||
|
column: 20
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 30
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 30
|
||||||
|
column: 26
|
||||||
|
fix:
|
||||||
|
content: "\"brace {{}} {}\".format(1)"
|
||||||
|
location:
|
||||||
|
row: 30
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 30
|
||||||
|
column: 26
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 33
|
||||||
|
column: 2
|
||||||
|
end_location:
|
||||||
|
row: 35
|
||||||
|
column: 9
|
||||||
|
fix:
|
||||||
|
content: "\"{}\".format(\n \"trailing comma\",\n )"
|
||||||
|
location:
|
||||||
|
row: 33
|
||||||
|
column: 2
|
||||||
|
end_location:
|
||||||
|
row: 35
|
||||||
|
column: 9
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 38
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 38
|
||||||
|
column: 22
|
||||||
|
fix:
|
||||||
|
content: "\"foo {} \".format(x)"
|
||||||
|
location:
|
||||||
|
row: 38
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 38
|
||||||
|
column: 22
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 40
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 40
|
||||||
|
column: 26
|
||||||
|
fix:
|
||||||
|
content: "\"{k}\".format(k=\"v\")"
|
||||||
|
location:
|
||||||
|
row: 40
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 40
|
||||||
|
column: 26
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 42
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 45
|
||||||
|
column: 1
|
||||||
|
fix:
|
||||||
|
content: "\"{k}\".format(\n k=\"v\",\n i=\"j\",\n)"
|
||||||
|
location:
|
||||||
|
row: 42
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 45
|
||||||
|
column: 1
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 47
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 47
|
||||||
|
column: 37
|
||||||
|
fix:
|
||||||
|
content: "\"{to_list}\".format(to_list=[])"
|
||||||
|
location:
|
||||||
|
row: 47
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 47
|
||||||
|
column: 37
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 49
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 49
|
||||||
|
column: 43
|
||||||
|
fix:
|
||||||
|
content: "\"{k}\".format(k=\"v\", i=1, j=[])"
|
||||||
|
location:
|
||||||
|
row: 49
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 49
|
||||||
|
column: 43
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 51
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 51
|
||||||
|
column: 29
|
||||||
|
fix:
|
||||||
|
content: "\"{ab}\".format(ab=1)"
|
||||||
|
location:
|
||||||
|
row: 51
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 51
|
||||||
|
column: 29
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 53
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 53
|
||||||
|
column: 27
|
||||||
|
fix:
|
||||||
|
content: "\"{a}\".format(a=1)"
|
||||||
|
location:
|
||||||
|
row: 53
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 53
|
||||||
|
column: 27
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 56
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 57
|
||||||
|
column: 21
|
||||||
|
fix:
|
||||||
|
content: "\"foo {} \"\n \"bar {}\".format(x, y)"
|
||||||
|
location:
|
||||||
|
row: 56
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 57
|
||||||
|
column: 21
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 61
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 62
|
||||||
|
column: 40
|
||||||
|
fix:
|
||||||
|
content: "\"foo {foo} \"\n \"bar {bar}\".format(foo=x, bar=y)"
|
||||||
|
location:
|
||||||
|
row: 61
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 62
|
||||||
|
column: 40
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 65
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 65
|
||||||
|
column: 29
|
||||||
|
fix:
|
||||||
|
content: "\"{} \\N{snowman}\".format(a)"
|
||||||
|
location:
|
||||||
|
row: 65
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 65
|
||||||
|
column: 29
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 67
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 67
|
||||||
|
column: 40
|
||||||
|
fix:
|
||||||
|
content: "\"{foo} \\N{snowman}\".format(foo=1)"
|
||||||
|
location:
|
||||||
|
row: 67
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 67
|
||||||
|
column: 40
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
PrintfStringFormatting: ~
|
||||||
|
location:
|
||||||
|
row: 69
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 69
|
||||||
|
column: 35
|
||||||
|
fix:
|
||||||
|
content: "(\"foo {} \" \"bar {}\").format(x, y)"
|
||||||
|
location:
|
||||||
|
row: 69
|
||||||
|
column: 6
|
||||||
|
end_location:
|
||||||
|
row: 69
|
||||||
|
column: 35
|
||||||
|
parent: ~
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
source: src/rules/pyupgrade/mod.rs
|
||||||
|
expression: diagnostics
|
||||||
|
---
|
||||||
|
[]
|
||||||
|
|
|
@ -3015,6 +3015,20 @@ impl AlwaysAutofixableViolation for ReplaceUniversalNewlines {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
define_violation!(
|
||||||
|
pub struct PrintfStringFormatting;
|
||||||
|
);
|
||||||
|
impl AlwaysAutofixableViolation for PrintfStringFormatting {
|
||||||
|
#[derive_message_formats]
|
||||||
|
fn message(&self) -> String {
|
||||||
|
format!("Use format specifiers instead of percent format")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn autofix_title(&self) -> String {
|
||||||
|
"Replace with format specifiers".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
define_violation!(
|
define_violation!(
|
||||||
pub struct ReplaceStdoutStderr;
|
pub struct ReplaceStdoutStderr;
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue