diff --git a/crates/ruff/resources/test/fixtures/pylint/bad_string_format_character.py b/crates/ruff/resources/test/fixtures/pylint/bad_string_format_character.py index 4d668418d4a74b..4b2a194abb1552 100644 --- a/crates/ruff/resources/test/fixtures/pylint/bad_string_format_character.py +++ b/crates/ruff/resources/test/fixtures/pylint/bad_string_format_character.py @@ -14,7 +14,10 @@ "{:s} {:y}".format("hello", "world") # [bad-format-character] -"{:*^30s}".format("centered") +"{:*^30s}".format("centered") # OK +"{:{s}}".format("hello", s="s") # OK (nested replacement value not checked) + +"{:{s:y}}".format("hello", s="s") # [bad-format-character] (nested replacement format spec checked) ## f-strings diff --git a/crates/ruff/src/rules/pylint/rules/bad_string_format_character.rs b/crates/ruff/src/rules/pylint/rules/bad_string_format_character.rs index 6639fb95ce6064..24d69c79ad3f07 100644 --- a/crates/ruff/src/rules/pylint/rules/bad_string_format_character.rs +++ b/crates/ruff/src/rules/pylint/rules/bad_string_format_character.rs @@ -49,16 +49,39 @@ pub(crate) fn call(checker: &mut Checker, string: &str, range: TextRange) { continue; }; - if let Err(FormatSpecError::InvalidFormatType) = FormatSpec::parse(format_spec) { - checker.diagnostics.push(Diagnostic::new( - BadStringFormatCharacter { - // The format type character is always the last one. - // More info in the official spec: - // https://docs.python.org/3/library/string.html#format-specification-mini-language - format_char: format_spec.chars().last().unwrap(), - }, - range, - )); + match FormatSpec::parse(format_spec) { + Err(FormatSpecError::InvalidFormatType) => { + checker.diagnostics.push(Diagnostic::new( + BadStringFormatCharacter { + // The format type character is always the last one. + // More info in the official spec: + // https://docs.python.org/3/library/string.html#format-specification-mini-language + format_char: format_spec.chars().last().unwrap(), + }, + range, + )); + } + Err(_) => {} + Ok(format_spec) => { + for replacement in format_spec.replacements { + let FormatPart::Field { format_spec, .. } = replacement else { + continue; + }; + if let Err(FormatSpecError::InvalidFormatType) = + FormatSpec::parse(&format_spec) + { + checker.diagnostics.push(Diagnostic::new( + BadStringFormatCharacter { + // The format type character is always the last one. + // More info in the official spec: + // https://docs.python.org/3/library/string.html#format-specification-mini-language + format_char: format_spec.chars().last().unwrap(), + }, + range, + )); + } + } + } } } } diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE1300_bad_string_format_character.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE1300_bad_string_format_character.py.snap index 375d02e9e99125..f34f9cc8763582 100644 --- a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE1300_bad_string_format_character.py.snap +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE1300_bad_string_format_character.py.snap @@ -48,7 +48,17 @@ bad_string_format_character.py:15:1: PLE1300 Unsupported format character 'y' 15 | "{:s} {:y}".format("hello", "world") # [bad-format-character] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLE1300 16 | -17 | "{:*^30s}".format("centered") +17 | "{:*^30s}".format("centered") # OK + | + +bad_string_format_character.py:20:1: PLE1300 Unsupported format character 'y' + | +18 | "{:{s}}".format("hello", s="s") # OK (nested replacement value not checked) +19 | +20 | "{:{s:y}}".format("hello", s="s") # [bad-format-character] (nested replacement format spec checked) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLE1300 +21 | +22 | ## f-strings | diff --git a/crates/ruff_python_literal/src/format.rs b/crates/ruff_python_literal/src/format.rs index 5d0a5f219eee16..4a3c515be7d5bf 100644 --- a/crates/ruff_python_literal/src/format.rs +++ b/crates/ruff_python_literal/src/format.rs @@ -237,7 +237,7 @@ pub struct FormatSpec { // Ex) `f` in `'{:+f}'` format_type: Option, // Ex) `x` and `y` in `'{:*{x},{y}b}'` - replacements: Vec, + pub replacements: Vec, } fn get_num_digits(text: &str) -> usize {