[pylint] Implement Pylint bad-format-character (E1300) (#6171)

## Summary

Relates to #970.

Add new `bad-format-character` Pylint rule.

I had to make a change in `crates/ruff_python_literal/src/format.rs` to
get a more detailed error in case the format character is not correct. I
chose to do this since most of the format spec parsing functions are
private. It would have required me reimplementing most of the parsing
logic just to know if the format char was correct.

This PR also doesn't reflect current Pylint functionality in two ways.

It supports new format strings correctly, Pylint as of now doesn't. See
pylint-dev/pylint#6085.

In case there are multiple adjacent string literals delimited by
whitespace the index of the wrong format char will relative to the
single string. Pylint will instead reported it relative to the
concatenated string.

Given this:
```
"%s" "%z" % ("hello", "world")
```

Ruff will report this:
```Unsupported format character 'z' (0x7a) at index 1```

Pylint instead:
```Unsupported format character 'z' (0x7a) at index 3```

I believe it's more sensible to report the index relative to the
individual string.

## Test Plan

Added new snapshot and a small test in
`crates/ruff_python_literal/src/format.rs`.

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
Silvano Cerza 2023-08-02 23:32:43 +02:00 committed by GitHub
parent 5b2e973fa5
commit 82410524d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 221 additions and 5 deletions

View file

@ -180,6 +180,7 @@ impl FormatParse for FormatType {
Some('g') => (Some(Self::GeneralFormat(Case::Lower)), chars.as_str()),
Some('G') => (Some(Self::GeneralFormat(Case::Upper)), chars.as_str()),
Some('%') => (Some(Self::Percentage), chars.as_str()),
Some(_) => (None, chars.as_str()),
_ => (None, text),
}
}
@ -283,10 +284,20 @@ impl FormatSpec {
let (width, text) = parse_number(text)?;
let (grouping_option, text) = FormatGrouping::parse(text);
let (precision, text) = parse_precision(text)?;
let (format_type, text) = FormatType::parse(text);
if !text.is_empty() {
return Err(FormatSpecError::InvalidFormatSpecifier);
}
let (format_type, _text) = if text.is_empty() {
(None, text)
} else {
// If there's any remaining text, we should yield a valid format type and consume it
// all.
let (format_type, text) = FormatType::parse(text);
if format_type.is_none() {
return Err(FormatSpecError::InvalidFormatType);
}
if !text.is_empty() {
return Err(FormatSpecError::InvalidFormatSpecifier);
}
(format_type, text)
};
if zero && fill.is_none() {
fill.replace('0');
@ -724,6 +735,7 @@ pub enum FormatSpecError {
DecimalDigitsTooMany,
PrecisionTooBig,
InvalidFormatSpecifier,
InvalidFormatType,
UnspecifiedFormat(char, char),
UnknownFormatCode(char, &'static str),
PrecisionNotAllowed,
@ -1275,6 +1287,10 @@ mod tests {
FormatSpec::parse("d "),
Err(FormatSpecError::InvalidFormatSpecifier)
);
assert_eq!(
FormatSpec::parse("z"),
Err(FormatSpecError::InvalidFormatType)
);
}
#[test]