diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/missing_maxsplit_arg.py b/crates/ruff_linter/resources/test/fixtures/pylint/missing_maxsplit_arg.py new file mode 100644 index 00000000000000..f24e23fdbd7f59 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pylint/missing_maxsplit_arg.py @@ -0,0 +1,184 @@ +SEQ = "1,2,3" + +class Foo(str): + class_str = "1,2,3" + + def split(self, sep=None, maxsplit=-1) -> list[str]: + return super().split(sep, maxsplit) + +class Bar(): + split = "1,2,3" + +# Errors +## Test split called directly on string literal +"1,2,3".split(",")[0] # [missing-maxsplit-arg] +"1,2,3".split(",")[-1] # [missing-maxsplit-arg] +"1,2,3".rsplit(",")[0] # [missing-maxsplit-arg] +"1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg] + +## Test split called on string variable +SEQ.split(",")[0] # [missing-maxsplit-arg] +SEQ.split(",")[-1] # [missing-maxsplit-arg] +SEQ.rsplit(",")[0] # [missing-maxsplit-arg] +SEQ.rsplit(",")[-1] # [missing-maxsplit-arg] + +## Test split called on class attribute +Foo.class_str.split(",")[0] # [missing-maxsplit-arg] +Foo.class_str.split(",")[-1] # [missing-maxsplit-arg] +Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg] +Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg] + +## Test split called on sliced string +"1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg] +"1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg] +SEQ[:3].split(",")[0] # [missing-maxsplit-arg] +Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg] +"1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg] +SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg] +Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg] + +## Test sep given as named argument +"1,2,3".split(sep=",")[0] # [missing-maxsplit-arg] +"1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg] +"1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg] +"1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg] + +## Special cases +"1,2,3".split("\n")[0] # [missing-maxsplit-arg] +"1,2,3".split("split")[-1] # [missing-maxsplit-arg] +"1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg] + +## Test class attribute named split +Bar.split.split(",")[0] # [missing-maxsplit-arg] +Bar.split.split(",")[-1] # [missing-maxsplit-arg] +Bar.split.rsplit(",")[0] # [missing-maxsplit-arg] +Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg] + +## Test unpacked dict literal kwargs +"1,2,3".split(**{"sep": ","})[0] # [missing-maxsplit-arg] + + +# OK +## Test not accessing the first or last element +### Test split called directly on string literal +"1,2,3".split(",")[1] +"1,2,3".split(",")[-2] +"1,2,3".rsplit(",")[1] +"1,2,3".rsplit(",")[-2] + +### Test split called on string variable +SEQ.split(",")[1] +SEQ.split(",")[-2] +SEQ.rsplit(",")[1] +SEQ.rsplit(",")[-2] + +### Test split called on class attribute +Foo.class_str.split(",")[1] +Foo.class_str.split(",")[-2] +Foo.class_str.rsplit(",")[1] +Foo.class_str.rsplit(",")[-2] + +### Test split called on sliced string +"1,2,3"[::-1].split(",")[1] +SEQ[:3].split(",")[1] +Foo.class_str[1:3].split(",")[-2] +"1,2,3"[::-1].rsplit(",")[1] +SEQ[:3].rsplit(",")[1] +Foo.class_str[1:3].rsplit(",")[-2] + +### Test sep given as named argument +"1,2,3".split(sep=",")[1] +"1,2,3".split(sep=",")[-2] +"1,2,3".rsplit(sep=",")[1] +"1,2,3".rsplit(sep=",")[-2] + +## Test varying maxsplit argument +### str.split() tests +"1,2,3".split(sep=",", maxsplit=1)[-1] +"1,2,3".split(sep=",", maxsplit=1)[0] +"1,2,3".split(sep=",", maxsplit=2)[-1] +"1,2,3".split(sep=",", maxsplit=2)[0] +"1,2,3".split(sep=",", maxsplit=2)[1] + +### str.rsplit() tests +"1,2,3".rsplit(sep=",", maxsplit=1)[-1] +"1,2,3".rsplit(sep=",", maxsplit=1)[0] +"1,2,3".rsplit(sep=",", maxsplit=2)[-1] +"1,2,3".rsplit(sep=",", maxsplit=2)[0] +"1,2,3".rsplit(sep=",", maxsplit=2)[1] + +## Test user-defined split +Foo("1,2,3").split(",")[0] +Foo("1,2,3").split(",")[-1] +Foo("1,2,3").rsplit(",")[0] +Foo("1,2,3").rsplit(",")[-1] + +## Test split called on sliced list +["1", "2", "3"][::-1].split(",")[0] + +## Test class attribute named split +Bar.split[0] +Bar.split[-1] +Bar.split[0] +Bar.split[-1] + +## Test unpacked dict literal kwargs +"1,2,3".split(",", **{"maxsplit": 1})[0] +"1,2,3".split(**{"sep": ",", "maxsplit": 1})[0] + + +# TODO + +## Test variable split result index +## TODO: These require the ability to resolve a variable name to a value +# Errors +result_index = 0 +"1,2,3".split(",")[result_index] # TODO: [missing-maxsplit-arg] +result_index = -1 +"1,2,3".split(",")[result_index] # TODO: [missing-maxsplit-arg] +# OK +result_index = 1 +"1,2,3".split(",")[result_index] +result_index = -2 +"1,2,3".split(",")[result_index] + + +## Test split result index modified in loop +## TODO: These require the ability to recognize being in a loop where: +## - the result of split called on a string is indexed by a variable +## - the variable index above is modified +# OK +result_index = 0 +for j in range(3): + print(SEQ.split(",")[result_index]) + result_index = result_index + 1 + + +## Test accessor +## TODO: These require the ability to get the return type of a method +## (possibly via `typing::is_string`) +class Baz(): + def __init__(self): + self.my_str = "1,2,3" + + def get_string(self) -> str: + return self.my_str + +# Errors +Baz().get_string().split(",")[0] # TODO: [missing-maxsplit-arg] +Baz().get_string().split(",")[-1] # TODO: [missing-maxsplit-arg] +# OK +Baz().get_string().split(",")[1] +Baz().get_string().split(",")[-2] + + +## Test unpacked dict instance kwargs +## TODO: These require the ability to resolve a dict variable name to a value +# Errors +kwargs_without_maxsplit = {"seq": ","} +"1,2,3".split(**kwargs_without_maxsplit)[0] # TODO: [missing-maxsplit-arg] +# OK +kwargs_with_maxsplit = {"maxsplit": 1} +"1,2,3".split(",", **kwargs_with_maxsplit)[0] # TODO: false positive +kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1} +"1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index e6de35139836be..e29b4db602863f 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -176,6 +176,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { if checker.enabled(Rule::Airflow3Removal) { airflow::rules::airflow_3_removal_expr(checker, expr); } + if checker.enabled(Rule::MissingMaxsplitArg) { + pylint::rules::missing_maxsplit_arg(checker, value, slice, expr); + } pandas_vet::rules::subscript(checker, value, expr); } Expr::Tuple(ast::ExprTuple { diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 4051d69b2e8de0..69ac2201ce5405 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -198,6 +198,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "C0132") => (RuleGroup::Stable, rules::pylint::rules::TypeParamNameMismatch), (Pylint, "C0205") => (RuleGroup::Stable, rules::pylint::rules::SingleStringSlots), (Pylint, "C0206") => (RuleGroup::Stable, rules::pylint::rules::DictIndexMissingItems), + (Pylint, "C0207") => (RuleGroup::Preview, rules::pylint::rules::MissingMaxsplitArg), (Pylint, "C0208") => (RuleGroup::Stable, rules::pylint::rules::IterationOverSet), (Pylint, "C0414") => (RuleGroup::Stable, rules::pylint::rules::UselessImportAlias), (Pylint, "C0415") => (RuleGroup::Preview, rules::pylint::rules::ImportOutsideTopLevel), diff --git a/crates/ruff_linter/src/rules/pylint/mod.rs b/crates/ruff_linter/src/rules/pylint/mod.rs index 67dbdd06839381..12ee8ed2526f1a 100644 --- a/crates/ruff_linter/src/rules/pylint/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/mod.rs @@ -231,6 +231,7 @@ mod tests { Path::new("bad_staticmethod_argument.py") )] #[test_case(Rule::LenTest, Path::new("len_as_condition.py"))] + #[test_case(Rule::MissingMaxsplitArg, Path::new("missing_maxsplit_arg.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/pylint/rules/missing_maxsplit_arg.rs b/crates/ruff_linter/src/rules/pylint/rules/missing_maxsplit_arg.rs new file mode 100644 index 00000000000000..daa9e57e00d969 --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/rules/missing_maxsplit_arg.rs @@ -0,0 +1,134 @@ +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{ + DictItem, Expr, ExprAttribute, ExprCall, ExprDict, ExprNumberLiteral, ExprStringLiteral, + ExprSubscript, ExprUnaryOp, Keyword, Number, UnaryOp, +}; +use ruff_python_semantic::{SemanticModel, analyze::typing}; +use ruff_text_size::Ranged; + +use crate::Violation; +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for access to the first or last element of `str.split()` without +/// `maxsplit=1` +/// +/// ## Why is this bad? +/// Calling `str.split()` without `maxsplit` set splits on every delimiter in the +/// string. When accessing only the first or last element of the result, it +/// would be more efficient to only split once. +/// +/// ## Example +/// ```python +/// url = "www.example.com" +/// prefix = url.split(".")[0] +/// ``` +/// +/// Use instead: +/// ```python +/// url = "www.example.com" +/// prefix = url.split(".", maxsplit=1)[0] +/// ``` + +#[derive(ViolationMetadata)] +pub(crate) struct MissingMaxsplitArg; + +impl Violation for MissingMaxsplitArg { + #[derive_message_formats] + fn message(&self) -> String { + "Accessing only the first or last element of `str.split()` without setting `maxsplit=1`" + .to_string() + } +} + +fn is_string(expr: &Expr, semantic: &SemanticModel) -> bool { + if let Expr::Name(name) = expr { + semantic + .only_binding(name) + .is_some_and(|binding_id| typing::is_string(semantic.binding(binding_id), semantic)) + } else if let Some(binding_id) = semantic.lookup_attribute(expr) { + typing::is_string(semantic.binding(binding_id), semantic) + } else { + expr.is_string_literal_expr() + } +} + +/// PLC0207 +pub(crate) fn missing_maxsplit_arg(checker: &Checker, value: &Expr, slice: &Expr, expr: &Expr) { + // Check the sliced expression is a function + let Expr::Call(ExprCall { + func, arguments, .. + }) = value + else { + return; + }; + + // Check the slice index is either 0 or -1 (first or last value) + let index = match slice { + Expr::NumberLiteral(ExprNumberLiteral { + value: Number::Int(number_value), + .. + }) => number_value.as_i64(), + Expr::UnaryOp(ExprUnaryOp { + op: UnaryOp::USub, + operand, + .. + }) => match operand.as_ref() { + Expr::NumberLiteral(ExprNumberLiteral { + value: Number::Int(number_value), + .. + }) => number_value.as_i64().map(|number| -number), + _ => return, + }, + _ => return, + }; + + if !matches!(index, Some(0 | -1)) { + return; + } + + let Expr::Attribute(ExprAttribute { attr, value, .. }) = func.as_ref() else { + return; + }; + + // Check the function is "split" or "rsplit" + let attr = attr.as_str(); + if !matches!(attr, "split" | "rsplit") { + return; + } + + let mut target_instance = value; + // a subscripted value could technically be subscripted further ad infinitum, so we + // recurse into the subscript expressions until we find the value being subscripted + while let Expr::Subscript(ExprSubscript { value, .. }) = target_instance.as_ref() { + target_instance = value; + } + + // Check the function is called on a string + if !is_string(target_instance, checker.semantic()) { + return; + } + + // Check the function does not have maxsplit set + if arguments.find_argument_value("maxsplit", 1).is_some() { + return; + } + + // Check maxsplit kwarg not set via unpacked dict literal + for keyword in &*arguments.keywords { + let Keyword { value, .. } = keyword; + + if let Expr::Dict(ExprDict { items, .. }) = value { + for item in items { + let DictItem { key, .. } = item; + if let Some(Expr::StringLiteral(ExprStringLiteral { value, .. })) = key { + if value.to_str() == "maxsplit" { + return; + } + } + } + } + } + + checker.report_diagnostic(MissingMaxsplitArg, expr.range()); +} diff --git a/crates/ruff_linter/src/rules/pylint/rules/mod.rs b/crates/ruff_linter/src/rules/pylint/rules/mod.rs index 3ddc469656fc4d..891691f21b7ec7 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/mod.rs @@ -46,6 +46,7 @@ pub(crate) use logging::*; pub(crate) use magic_value_comparison::*; pub(crate) use manual_import_from::*; pub(crate) use misplaced_bare_raise::*; +pub(crate) use missing_maxsplit_arg::*; pub(crate) use modified_iterating_set::*; pub(crate) use named_expr_without_context::*; pub(crate) use nan_comparison::*; @@ -155,6 +156,7 @@ mod logging; mod magic_value_comparison; mod manual_import_from; mod misplaced_bare_raise; +mod missing_maxsplit_arg; mod modified_iterating_set; mod named_expr_without_context; mod nan_comparison; diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC0207_missing_maxsplit_arg.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC0207_missing_maxsplit_arg.py.snap new file mode 100644 index 00000000000000..97c8800eebf657 --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC0207_missing_maxsplit_arg.py.snap @@ -0,0 +1,324 @@ +--- +source: crates/ruff_linter/src/rules/pylint/mod.rs +--- +missing_maxsplit_arg.py:14:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +12 | # Errors +13 | ## Test split called directly on string literal +14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^ PLC0207 +15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg] +16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:15:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +13 | ## Test split called directly on string literal +14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg] +15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg] +17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:16:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg] +15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg] +16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:17:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg] +16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg] +17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +18 | +19 | ## Test split called on string variable + | + +missing_maxsplit_arg.py:20:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +19 | ## Test split called on string variable +20 | SEQ.split(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^ PLC0207 +21 | SEQ.split(",")[-1] # [missing-maxsplit-arg] +22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:21:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +19 | ## Test split called on string variable +20 | SEQ.split(",")[0] # [missing-maxsplit-arg] +21 | SEQ.split(",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^ PLC0207 +22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg] +23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:22:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +20 | SEQ.split(",")[0] # [missing-maxsplit-arg] +21 | SEQ.split(",")[-1] # [missing-maxsplit-arg] +22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^ PLC0207 +23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:23:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +21 | SEQ.split(",")[-1] # [missing-maxsplit-arg] +22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg] +23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^ PLC0207 +24 | +25 | ## Test split called on class attribute + | + +missing_maxsplit_arg.py:26:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +25 | ## Test split called on class attribute +26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg] +28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:27:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +25 | ## Test split called on class attribute +26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg] +27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg] +29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:28:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg] +27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg] +28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:29:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg] +28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg] +29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +30 | +31 | ## Test split called on sliced string + | + +missing_maxsplit_arg.py:32:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +31 | ## Test split called on sliced string +32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg] +34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:33:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +31 | ## Test split called on sliced string +32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg] +33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg] +35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:34:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg] +33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg] +34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^ PLC0207 +35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg] +36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:35:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg] +34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg] +35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg] +37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:36:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg] +35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg] +36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg] +38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:37:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg] +36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg] +37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:38:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg] +37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg] +38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +39 | +40 | ## Test sep given as named argument + | + +missing_maxsplit_arg.py:41:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +40 | ## Test sep given as named argument +41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg] +43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:42:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +40 | ## Test sep given as named argument +41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg] +42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg] +44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:43:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg] +42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg] +43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:44:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg] +43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg] +44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +45 | +46 | ## Special cases + | + +missing_maxsplit_arg.py:47:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +46 | ## Special cases +47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +48 | "1,2,3".split("split")[-1] # [missing-maxsplit-arg] +49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:48:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +46 | ## Special cases +47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg] +48 | "1,2,3".split("split")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:49:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg] +48 | "1,2,3".split("split")[-1] # [missing-maxsplit-arg] +49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +50 | +51 | ## Test class attribute named split + | + +missing_maxsplit_arg.py:52:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +51 | ## Test class attribute named split +52 | Bar.split.split(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg] +54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:53:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +51 | ## Test class attribute named split +52 | Bar.split.split(",")[0] # [missing-maxsplit-arg] +53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg] +55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:54:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +52 | Bar.split.split(",")[0] # [missing-maxsplit-arg] +53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg] +54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:55:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg] +54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg] +55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +56 | +57 | ## Test unpacked dict literal kwargs + | + +missing_maxsplit_arg.py:58:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +57 | ## Test unpacked dict literal kwargs +58 | "1,2,3".split(**{"sep": ","})[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 + | + +missing_maxsplit_arg.py:179:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +177 | # Errors +178 | kwargs_without_maxsplit = {"seq": ","} +179 | "1,2,3".split(**kwargs_without_maxsplit)[0] # TODO: [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +180 | # OK +181 | kwargs_with_maxsplit = {"maxsplit": 1} + | + +missing_maxsplit_arg.py:182:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +180 | # OK +181 | kwargs_with_maxsplit = {"maxsplit": 1} +182 | "1,2,3".split(",", **kwargs_with_maxsplit)[0] # TODO: false positive + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +183 | kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1} +184 | "1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive + | + +missing_maxsplit_arg.py:184:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1` + | +182 | "1,2,3".split(",", **kwargs_with_maxsplit)[0] # TODO: false positive +183 | kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1} +184 | "1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 + | diff --git a/ruff.schema.json b/ruff.schema.json index 37fc8102c7eb68..7bf97547310a7c 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3588,6 +3588,7 @@ "PLC020", "PLC0205", "PLC0206", + "PLC0207", "PLC0208", "PLC04", "PLC041",