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 4b2a194abb1552..31e6366a639ee0 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 @@ -16,8 +16,10 @@ "{:*^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) +"{0:.{prec}g}".format(1.23, prec=15) # OK +"{0:.{foo}x{bar}y{foobar}g}".format(...) # OK (all nested replacements are consumed without considering in between chars) +"{0:.{foo}{bar}{foobar}y}".format(...) # [bad-format-character] (check value after replacements) ## f-strings 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 f34f9cc8763582..63cc95513cc0d3 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 @@ -51,14 +51,24 @@ bad_string_format_character.py:15:1: PLE1300 Unsupported format character 'y' 17 | "{:*^30s}".format("centered") # OK | -bad_string_format_character.py:20:1: PLE1300 Unsupported format character 'y' +bad_string_format_character.py:19:1: PLE1300 Unsupported format character 'y' | +17 | "{:*^30s}".format("centered") # OK 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) +19 | "{:{s:y}}".format("hello", s="s") # [bad-format-character] (nested replacement format spec checked) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLE1300 -21 | -22 | ## f-strings +20 | "{0:.{prec}g}".format(1.23, prec=15) # OK +21 | "{0:.{foo}x{bar}y{foobar}g}".format(...) # OK (all nested replacements are consumed without considering in between chars) + | + +bad_string_format_character.py:22:1: PLE1300 Unsupported format character 'y' + | +20 | "{0:.{prec}g}".format(1.23, prec=15) # OK +21 | "{0:.{foo}x{bar}y{foobar}g}".format(...) # OK (all nested replacements are consumed without considering in between chars) +22 | "{0:.{foo}{bar}{foobar}y}".format(...) # [bad-format-character] (check value after replacements) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLE1300 +23 | +24 | ## f-strings | diff --git a/crates/ruff_python_literal/src/format.rs b/crates/ruff_python_literal/src/format.rs index 2e2364e2186352..dcb0d2cb105535 100644 --- a/crates/ruff_python_literal/src/format.rs +++ b/crates/ruff_python_literal/src/format.rs @@ -318,9 +318,9 @@ fn parse_precision(text: &str) -> Result<(Option, &str), FormatSpecError> }) } -/// Parses a format part within a format spec +/// Parses a placeholder within a format spec fn parse_nested_placeholder<'a>( - parts: &mut Vec, + placeholders: &mut Vec, text: &'a str, ) -> Result<&'a str, FormatSpecError> { match FormatString::parse_spec(text, AllowPlaceholderNesting::No) { @@ -328,16 +328,38 @@ fn parse_nested_placeholder<'a>( Err(FormatParseError::MissingStartBracket) => Ok(text), Err(err) => Err(FormatSpecError::InvalidPlaceholder(err)), Ok((format_part, text)) => { - parts.push(format_part); + placeholders.push(format_part); Ok(text) } } } +/// Parse and consume all placeholders in a format spec +/// This will also consume any intermediate characters such as `x` and `y` in `"x{foo}y{bar}z"` +fn consume_all_placeholders<'a>( + placeholders: &mut Vec, + text: &'a str, +) -> Result<&'a str, FormatSpecError> { + let mut chars = text.chars(); + let mut text = text; + let mut placeholder_count = placeholders.len(); + + while chars.clone().contains(&'{') { + text = parse_nested_placeholder(placeholders, chars.as_str())?; + chars = text.chars(); + // If we did not parse a placeholder, consume a character + if placeholder_count == placeholders.len() { + chars.next(); + } else { + placeholder_count = placeholders.len(); + } + } + Ok(text) +} + impl FormatSpec { pub fn parse(text: &str) -> Result { let mut replacements = vec![]; - // get_integer in CPython let text = parse_nested_placeholder(&mut replacements, text)?; let (conversion, text) = FormatConversion::parse(text); let text = parse_nested_placeholder(&mut replacements, text)?; @@ -354,7 +376,7 @@ impl FormatSpec { let (grouping_option, text) = FormatGrouping::parse(text); let text = parse_nested_placeholder(&mut replacements, text)?; let (precision, text) = parse_precision(text)?; - let text = parse_nested_placeholder(&mut replacements, text)?; + let text = consume_all_placeholders(&mut replacements, text)?; let (format_type, _text) = if text.is_empty() { (None, text)