Skip to content

Commit fdaf7bf

Browse files
committed
[pylint] Also report when the object isn't a literal (PLE1310)
1 parent 0186a72 commit fdaf7bf

4 files changed

Lines changed: 250 additions & 2 deletions

File tree

crates/ruff_linter/src/rules/pylint/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ mod tests {
441441
Path::new("repeated_equality_comparison.py")
442442
)]
443443
#[test_case(Rule::InvalidEnvvarDefault, Path::new("invalid_envvar_default.py"))]
444+
#[test_case(Rule::BadStrStripCall, Path::new("bad_str_strip_call.py"))]
444445
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
445446
let snapshot = format!(
446447
"preview__{}_{}",

crates/ruff_linter/src/rules/pylint/rules/bad_str_strip_call.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use rustc_hash::FxHashSet;
55

66
use ruff_diagnostics::{Diagnostic, Violation};
77
use ruff_macros::{derive_message_formats, ViolationMetadata};
8+
use ruff_python_semantic::analyze::typing;
9+
use ruff_python_semantic::SemanticModel;
810
use ruff_text_size::Ranged;
911

1012
use crate::checkers::ast::Checker;
@@ -79,10 +81,20 @@ pub(crate) enum ValueKind {
7981
}
8082

8183
impl ValueKind {
82-
fn from(expr: &Expr) -> Option<Self> {
84+
fn from(expr: &Expr, semantic: &SemanticModel) -> Option<Self> {
8385
match expr {
8486
Expr::StringLiteral(_) => Some(Self::String),
8587
Expr::BytesLiteral(_) => Some(Self::Bytes),
88+
Expr::Name(name) => {
89+
let binding_id = semantic.only_binding(name)?;
90+
let binding = semantic.binding(binding_id);
91+
92+
match () {
93+
() if typing::is_string(binding, semantic) => Some(Self::String),
94+
() if typing::is_bytes(binding, semantic) => Some(Self::Bytes),
95+
_ => None,
96+
}
97+
}
8698
_ => None,
8799
}
88100
}
@@ -188,7 +200,15 @@ pub(crate) fn bad_str_strip_call(checker: &mut Checker, call: &ast::ExprCall) {
188200
return;
189201
};
190202

191-
let Some(value_kind) = ValueKind::from(value.as_ref()) else {
203+
let value = value.as_ref();
204+
205+
if checker.settings.preview.is_disabled()
206+
&& !matches!(value, Expr::StringLiteral(_) | Expr::BytesLiteral(_))
207+
{
208+
return;
209+
}
210+
211+
let Some(value_kind) = ValueKind::from(value, checker.semantic()) else {
192212
return;
193213
};
194214

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
---
2+
source: crates/ruff_linter/src/rules/pylint/mod.rs
3+
---
4+
bad_str_strip_call.py:2:21: PLE1310 String `strip` call contains duplicate character 'l'
5+
|
6+
1 | # PLE1310
7+
2 | "Hello World".strip("Hello")
8+
| ^^^^^^^ PLE1310
9+
3 |
10+
4 | # PLE1310
11+
|
12+
13+
bad_str_strip_call.py:5:21: PLE1310 String `strip` call contains duplicate character 'l'
14+
|
15+
4 | # PLE1310
16+
5 | "Hello World".strip("Hello")
17+
| ^^^^^^^ PLE1310
18+
6 |
19+
7 | # PLE1310
20+
|
21+
22+
bad_str_strip_call.py:8:21: PLE1310 String `strip` call contains duplicate character 'l'
23+
|
24+
7 | # PLE1310
25+
8 | "Hello World".strip(u"Hello")
26+
| ^^^^^^^^ PLE1310
27+
9 |
28+
10 | # PLE1310
29+
|
30+
31+
bad_str_strip_call.py:11:21: PLE1310 String `strip` call contains duplicate character 'l'
32+
|
33+
10 | # PLE1310
34+
11 | "Hello World".strip(r"Hello")
35+
| ^^^^^^^^ PLE1310
36+
12 |
37+
13 | # PLE1310
38+
|
39+
40+
bad_str_strip_call.py:14:21: PLE1310 String `strip` call contains duplicate character 'l'
41+
|
42+
13 | # PLE1310
43+
14 | "Hello World".strip("Hello\t")
44+
| ^^^^^^^^^ PLE1310
45+
15 |
46+
16 | # PLE1310
47+
|
48+
49+
bad_str_strip_call.py:17:21: PLE1310 String `strip` call contains duplicate character 'l'
50+
|
51+
16 | # PLE1310
52+
17 | "Hello World".strip(r"Hello\t")
53+
| ^^^^^^^^^^ PLE1310
54+
18 |
55+
19 | # PLE1310
56+
|
57+
58+
bad_str_strip_call.py:20:21: PLE1310 String `strip` call contains duplicate character 'l'
59+
|
60+
19 | # PLE1310
61+
20 | "Hello World".strip("Hello\\")
62+
| ^^^^^^^^^ PLE1310
63+
21 |
64+
22 | # PLE1310
65+
|
66+
67+
bad_str_strip_call.py:23:21: PLE1310 String `strip` call contains duplicate character 'l'
68+
|
69+
22 | # PLE1310
70+
23 | "Hello World".strip(r"Hello\\")
71+
| ^^^^^^^^^^ PLE1310
72+
24 |
73+
25 | # PLE1310
74+
|
75+
76+
bad_str_strip_call.py:26:21: PLE1310 String `strip` call contains duplicate character '🤣'
77+
|
78+
25 | # PLE1310
79+
26 | "Hello World".strip("🤣🤣🤣🤣🙃👀😀")
80+
| ^^^^^^^^^^^^^^^^ PLE1310
81+
27 |
82+
28 | # PLE1310
83+
|
84+
85+
bad_str_strip_call.py:30:5: PLE1310 String `strip` call contains duplicate character 'e'
86+
|
87+
28 | # PLE1310
88+
29 | "Hello World".strip(
89+
30 | / """
90+
31 | | there are a lot of characters to strip
91+
32 | | """
92+
| |___^ PLE1310
93+
33 | )
94+
|
95+
96+
bad_str_strip_call.py:36:21: PLE1310 String `strip` call contains duplicate character ' '
97+
|
98+
35 | # PLE1310
99+
36 | "Hello World".strip("can we get a long " \
100+
| _____________________^
101+
37 | | "string of characters to strip " \
102+
38 | | "please?")
103+
| |_____________________________^ PLE1310
104+
39 |
105+
40 | # PLE1310
106+
|
107+
108+
bad_str_strip_call.py:42:5: PLE1310 String `strip` call contains duplicate character ' '
109+
|
110+
40 | # PLE1310
111+
41 | "Hello World".strip(
112+
42 | / "can we get a long "
113+
43 | | "string of characters to strip "
114+
44 | | "please?"
115+
| |_____________^ PLE1310
116+
45 | )
117+
|
118+
119+
bad_str_strip_call.py:49:5: PLE1310 String `strip` call contains duplicate character ' '
120+
|
121+
47 | # PLE1310
122+
48 | "Hello World".strip(
123+
49 | / "can \t we get a long"
124+
50 | | "string \t of characters to strip"
125+
51 | | "please?"
126+
| |_____________^ PLE1310
127+
52 | )
128+
|
129+
130+
bad_str_strip_call.py:61:11: PLE1310 String `strip` call contains duplicate character 't'
131+
|
132+
60 | # PLE1310
133+
61 | u''.strip('http://')
134+
| ^^^^^^^^^ PLE1310
135+
62 |
136+
63 | # PLE1310
137+
|
138+
139+
bad_str_strip_call.py:64:12: PLE1310 String `lstrip` call contains duplicate character 't' (did you mean `removeprefix`?)
140+
|
141+
63 | # PLE1310
142+
64 | u''.lstrip('http://')
143+
| ^^^^^^^^^ PLE1310
144+
65 |
145+
66 | # PLE1310
146+
|
147+
148+
bad_str_strip_call.py:67:12: PLE1310 String `rstrip` call contains duplicate character 't' (did you mean `removesuffix`?)
149+
|
150+
66 | # PLE1310
151+
67 | b''.rstrip(b'http://')
152+
| ^^^^^^^^^^ PLE1310
153+
68 |
154+
69 | # OK
155+
|
156+
157+
bad_str_strip_call.py:79:10: PLE1310 String `strip` call contains duplicate character '\\'
158+
|
159+
78 | # Errors: Multiple backslashes
160+
79 | ''.strip('\\b\\x09')
161+
| ^^^^^^^^^^ PLE1310
162+
80 | ''.strip(r'\b\x09')
163+
81 | ''.strip('\\\x5C')
164+
|
165+
166+
bad_str_strip_call.py:80:10: PLE1310 String `strip` call contains duplicate character '\\'
167+
|
168+
78 | # Errors: Multiple backslashes
169+
79 | ''.strip('\\b\\x09')
170+
80 | ''.strip(r'\b\x09')
171+
| ^^^^^^^^^ PLE1310
172+
81 | ''.strip('\\\x5C')
173+
|
174+
175+
bad_str_strip_call.py:81:10: PLE1310 String `strip` call contains duplicate character '\\'
176+
|
177+
79 | ''.strip('\\b\\x09')
178+
80 | ''.strip(r'\b\x09')
179+
81 | ''.strip('\\\x5C')
180+
| ^^^^^^^^ PLE1310
181+
82 |
182+
83 | # OK: Different types
183+
|
184+
185+
bad_str_strip_call.py:98:12: PLE1310 String `rstrip` call contains duplicate character '/' (did you mean `removesuffix`?)
186+
|
187+
97 | # False negative
188+
98 | foo.rstrip("//")
189+
| ^^^^ PLE1310
190+
99 | bar.lstrip(b"//")
191+
|
192+
193+
bad_str_strip_call.py:99:12: PLE1310 String `lstrip` call contains duplicate character '/' (did you mean `removeprefix`?)
194+
|
195+
97 | # False negative
196+
98 | foo.rstrip("//")
197+
99 | bar.lstrip(b"//")
198+
| ^^^^^ PLE1310
199+
100 |
200+
101 | # OK: Not `.[lr]?strip`
201+
|

crates/ruff_python_semantic/src/analyze/typing.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,22 @@ impl BuiltinTypeChecker for FloatChecker {
740740
const EXPR_TYPE: PythonType = PythonType::Number(NumberLike::Float);
741741
}
742742

743+
struct StringChecker;
744+
745+
impl BuiltinTypeChecker for StringChecker {
746+
const BUILTIN_TYPE_NAME: &'static str = "str";
747+
const TYPING_NAME: Option<&'static str> = None;
748+
const EXPR_TYPE: PythonType = PythonType::String;
749+
}
750+
751+
struct BytesChecker;
752+
753+
impl BuiltinTypeChecker for BytesChecker {
754+
const BUILTIN_TYPE_NAME: &'static str = "bytes";
755+
const TYPING_NAME: Option<&'static str> = None;
756+
const EXPR_TYPE: PythonType = PythonType::String;
757+
}
758+
743759
pub struct IoBaseChecker;
744760

745761
impl TypeChecker for IoBaseChecker {
@@ -959,6 +975,16 @@ pub fn is_float(binding: &Binding, semantic: &SemanticModel) -> bool {
959975
check_type::<FloatChecker>(binding, semantic)
960976
}
961977

978+
/// Test whether the given binding can be considered an instance of `str`.
979+
pub fn is_string(binding: &Binding, semantic: &SemanticModel) -> bool {
980+
check_type::<StringChecker>(binding, semantic)
981+
}
982+
983+
/// Test whether the given binding can be considered an instance of `bytes`.
984+
pub fn is_bytes(binding: &Binding, semantic: &SemanticModel) -> bool {
985+
check_type::<BytesChecker>(binding, semantic)
986+
}
987+
962988
/// Test whether the given binding can be considered a set.
963989
///
964990
/// For this, we check what value might be associated with it through it's initialization and

0 commit comments

Comments
 (0)