[`flake8-simplify`] Implement fix for `maxsplit` without separator (`SIM905`) (#19851)

**Stacked on top of #19849; diff will include that PR until it is
merged.**

---

<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

As part of #19849, I noticed this fix could be implemented.

## Test Plan

Tests added based on CPython behaviour.
This commit is contained in:
Frazer McLean 2025-08-15 21:18:06 +02:00 committed by GitHub
parent 2dc2f68b0f
commit 2e1d6623cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 1650 additions and 6 deletions

View File

@ -166,3 +166,7 @@ r"""first
print("S\x1cP\x1dL\x1eI\x1fT".split())
print("\x1c\x1d\x1e\x1f>".split(maxsplit=0))
print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
# leading/trailing whitespace should not count towards maxsplit
" a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
" a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]

View File

@ -230,3 +230,8 @@ pub(crate) const fn is_add_future_annotations_imports_enabled(settings: &LinterS
pub(crate) const fn is_trailing_comma_type_params_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19851
pub(crate) const fn is_maxsplit_without_separator_fix_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@ -60,6 +60,7 @@ mod tests {
}
#[test_case(Rule::MultipleWithStatements, Path::new("SIM117.py"))]
#[test_case(Rule::SplitStaticString, Path::new("SIM905.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@ -9,6 +9,8 @@ use ruff_python_ast::{
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::preview::is_maxsplit_without_separator_fix_enabled;
use crate::settings::LinterSettings;
use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
/// ## What it does
@ -84,7 +86,9 @@ pub(crate) fn split_static_string(
let sep_arg = arguments.find_argument_value("sep", 0);
let split_replacement = if let Some(sep) = sep_arg {
match sep {
Expr::NoneLiteral(_) => split_default(str_value, maxsplit_value, direction),
Expr::NoneLiteral(_) => {
split_default(str_value, maxsplit_value, direction, checker.settings())
}
Expr::StringLiteral(sep_value) => {
let sep_value_str = sep_value.value.to_str();
Some(split_sep(
@ -100,7 +104,7 @@ pub(crate) fn split_static_string(
}
}
} else {
split_default(str_value, maxsplit_value, direction)
split_default(str_value, maxsplit_value, direction, checker.settings())
};
let mut diagnostic = checker.report_diagnostic(SplitStaticString, call.range());
@ -174,6 +178,7 @@ fn split_default(
str_value: &StringLiteralValue,
max_split: i32,
direction: Direction,
settings: &LinterSettings,
) -> Option<Expr> {
// From the Python documentation:
// > If sep is not specified or is None, a different splitting algorithm is applied: runs of
@ -185,10 +190,31 @@ fn split_default(
let string_val = str_value.to_str();
match max_split.cmp(&0) {
Ordering::Greater => {
// Autofix for `maxsplit` without separator not yet implemented, as
// `split_whitespace().remainder()` is not stable:
// https://doc.rust-lang.org/std/str/struct.SplitWhitespace.html#method.remainder
None
if !is_maxsplit_without_separator_fix_enabled(settings) {
return None;
}
let Ok(max_split) = usize::try_from(max_split) else {
return None;
};
let list_items: Vec<&str> = if direction == Direction::Left {
string_val
.trim_start_matches(py_unicode_is_whitespace)
.splitn(max_split + 1, py_unicode_is_whitespace)
.filter(|s| !s.is_empty())
.collect()
} else {
let mut items: Vec<&str> = string_val
.trim_end_matches(py_unicode_is_whitespace)
.rsplitn(max_split + 1, py_unicode_is_whitespace)
.filter(|s| !s.is_empty())
.collect();
items.reverse();
items
};
Some(construct_replacement(
&list_items,
str_value.first_literal_flags(),
))
}
Ordering::Equal => {
// Behavior for maxsplit = 0 when sep is None:

View File

@ -1439,6 +1439,7 @@ help: Replace with list literal
166 |+print(["S", "P", "L", "I", "T"])
167 167 | print("\x1c\x1d\x1e\x1f>".split(maxsplit=0))
168 168 | print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
169 169 |
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:167:7
@ -1458,6 +1459,8 @@ help: Replace with list literal
167 |-print("\x1c\x1d\x1e\x1f>".split(maxsplit=0))
167 |+print([">"])
168 168 | print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
169 169 |
170 170 | # leading/trailing whitespace should not count towards maxsplit
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:168:7
@ -1466,6 +1469,8 @@ SIM905 [*] Consider using a list literal instead of `str.split`
167 | print("\x1c\x1d\x1e\x1f>".split(maxsplit=0))
168 | print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
169 |
170 | # leading/trailing whitespace should not count towards maxsplit
|
help: Replace with list literal
@ -1475,3 +1480,26 @@ help: Replace with list literal
167 167 | print("\x1c\x1d\x1e\x1f>".split(maxsplit=0))
168 |-print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
168 |+print(["<"])
169 169 |
170 170 | # leading/trailing whitespace should not count towards maxsplit
171 171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
SIM905 Consider using a list literal instead of `str.split`
--> SIM905.py:171:1
|
170 | # leading/trailing whitespace should not count towards maxsplit
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
|
help: Replace with list literal
SIM905 Consider using a list literal instead of `str.split`
--> SIM905.py:172:1
|
170 | # leading/trailing whitespace should not count towards maxsplit
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Replace with list literal