Skip to content

Commit 27902b7

Browse files
authored
[pylint] Implement invalid-index-returned (PLE0305) (#10962)
Add pylint rule invalid-index-returned (PLE0305) See #970 for rules Test Plan: `cargo test`
1 parent 97acf1d commit 27902b7

File tree

8 files changed

+234
-0
lines changed

8 files changed

+234
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# These testcases should raise errors
2+
3+
4+
class Bool:
5+
"""pylint would not raise, but ruff does - see explanation in the docs"""
6+
7+
def __index__(self):
8+
return True # [invalid-index-return]
9+
10+
11+
class Float:
12+
def __index__(self):
13+
return 3.05 # [invalid-index-return]
14+
15+
16+
class Dict:
17+
def __index__(self):
18+
return {"1": "1"} # [invalid-index-return]
19+
20+
21+
class Str:
22+
def __index__(self):
23+
return "ruff" # [invalid-index-return]
24+
25+
26+
class IndexNoReturn:
27+
def __index__(self):
28+
print("ruff") # [invalid-index-return]
29+
30+
31+
# TODO: Once Ruff has better type checking
32+
def return_index():
33+
return "3"
34+
35+
36+
class ComplexReturn:
37+
def __index__(self):
38+
return return_index() # [invalid-index-return]
39+
40+
41+
# These testcases should NOT raise errors
42+
43+
44+
class Index:
45+
def __index__(self):
46+
return 0
47+
48+
49+
class Index2:
50+
def __index__(self):
51+
x = 1
52+
return x
53+
54+
55+
class Index3:
56+
def __index__(self):
57+
...
58+
59+
60+
class Index4:
61+
def __index__(self):
62+
pass
63+
64+
65+
class Index5:
66+
def __index__(self):
67+
raise NotImplementedError
68+
69+
70+
class Index6:
71+
def __index__(self):
72+
print("raise some error")
73+
raise NotImplementedError

crates/ruff_linter/src/checkers/ast/analyze/statement.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
103103
if checker.enabled(Rule::InvalidBytesReturnType) {
104104
pylint::rules::invalid_bytes_return(checker, function_def);
105105
}
106+
if checker.enabled(Rule::InvalidIndexReturnType) {
107+
pylint::rules::invalid_index_return(checker, function_def);
108+
}
106109
if checker.enabled(Rule::InvalidHashReturnType) {
107110
pylint::rules::invalid_hash_return(checker, function_def);
108111
}

crates/ruff_linter/src/codes.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
243243
(Pylint, "E0302") => (RuleGroup::Stable, rules::pylint::rules::UnexpectedSpecialMethodSignature),
244244
(Pylint, "E0303") => (RuleGroup::Preview, rules::pylint::rules::InvalidLengthReturnType),
245245
(Pylint, "E0304") => (RuleGroup::Preview, rules::pylint::rules::InvalidBoolReturnType),
246+
(Pylint, "E0305") => (RuleGroup::Preview, rules::pylint::rules::InvalidIndexReturnType),
246247
(Pylint, "E0307") => (RuleGroup::Stable, rules::pylint::rules::InvalidStrReturnType),
247248
(Pylint, "E0308") => (RuleGroup::Preview, rules::pylint::rules::InvalidBytesReturnType),
248249
(Pylint, "E0309") => (RuleGroup::Preview, rules::pylint::rules::InvalidHashReturnType),

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ mod tests {
8181
Rule::InvalidBytesReturnType,
8282
Path::new("invalid_return_type_bytes.py")
8383
)]
84+
#[test_case(
85+
Rule::InvalidIndexReturnType,
86+
Path::new("invalid_return_type_index.py")
87+
)]
8488
#[test_case(Rule::InvalidHashReturnType, Path::new("invalid_return_type_hash.py"))]
8589
#[test_case(
8690
Rule::InvalidLengthReturnType,
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use ruff_diagnostics::{Diagnostic, Violation};
2+
use ruff_macros::{derive_message_formats, violation};
3+
use ruff_python_ast::helpers::ReturnStatementVisitor;
4+
use ruff_python_ast::identifier::Identifier;
5+
use ruff_python_ast::visitor::Visitor;
6+
use ruff_python_ast::{self as ast};
7+
use ruff_python_semantic::analyze::function_type::is_stub;
8+
use ruff_python_semantic::analyze::terminal::Terminal;
9+
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
10+
use ruff_text_size::Ranged;
11+
12+
use crate::checkers::ast::Checker;
13+
14+
/// ## What it does
15+
/// Checks for `__index__` implementations that return a type other than `integer`.
16+
///
17+
/// ## Why is this bad?
18+
/// The `__index__` method should return an `integer`. Returning a different
19+
/// type may cause unexpected behavior.
20+
///
21+
/// Note: `bool` is a subclass of `int`, so it's technically valid for `__index__` to
22+
/// return `True` or `False`. However, a DeprecationWarning (`DeprecationWarning:
23+
/// __index__ returned non-int (type bool)`) for such cases was already introduced,
24+
/// thus this is a conscious difference between the original pylint rule and the
25+
/// current ruff implementation.
26+
///
27+
/// ## Example
28+
/// ```python
29+
/// class Foo:
30+
/// def __index__(self):
31+
/// return "2"
32+
/// ```
33+
///
34+
/// Use instead:
35+
/// ```python
36+
/// class Foo:
37+
/// def __index__(self):
38+
/// return 2
39+
/// ```
40+
///
41+
///
42+
/// ## References
43+
/// - [Python documentation: The `__index__` method](https://docs.python.org/3/reference/datamodel.html#object.__index__)
44+
#[violation]
45+
pub struct InvalidIndexReturnType;
46+
47+
impl Violation for InvalidIndexReturnType {
48+
#[derive_message_formats]
49+
fn message(&self) -> String {
50+
format!("`__index__` does not return an integer")
51+
}
52+
}
53+
54+
/// E0305
55+
pub(crate) fn invalid_index_return(checker: &mut Checker, function_def: &ast::StmtFunctionDef) {
56+
if function_def.name.as_str() != "__index__" {
57+
return;
58+
}
59+
60+
if !checker.semantic().current_scope().kind.is_class() {
61+
return;
62+
}
63+
64+
if is_stub(function_def, checker.semantic()) {
65+
return;
66+
}
67+
68+
// Determine the terminal behavior (i.e., implicit return, no return, etc.).
69+
let terminal = Terminal::from_function(function_def);
70+
71+
// If every control flow path raises an exception, ignore the function.
72+
if terminal == Terminal::Raise {
73+
return;
74+
}
75+
76+
// If there are no return statements, add a diagnostic.
77+
if terminal == Terminal::Implicit {
78+
checker.diagnostics.push(Diagnostic::new(
79+
InvalidIndexReturnType,
80+
function_def.identifier(),
81+
));
82+
return;
83+
}
84+
85+
let returns = {
86+
let mut visitor = ReturnStatementVisitor::default();
87+
visitor.visit_body(&function_def.body);
88+
visitor.returns
89+
};
90+
91+
for stmt in returns {
92+
if let Some(value) = stmt.value.as_deref() {
93+
if !matches!(
94+
ResolvedPythonType::from(value),
95+
ResolvedPythonType::Unknown
96+
| ResolvedPythonType::Atom(PythonType::Number(NumberLike::Integer))
97+
) {
98+
checker
99+
.diagnostics
100+
.push(Diagnostic::new(InvalidIndexReturnType, value.range()));
101+
}
102+
} else {
103+
// Disallow implicit `None`.
104+
checker
105+
.diagnostics
106+
.push(Diagnostic::new(InvalidIndexReturnType, stmt.range()));
107+
}
108+
}
109+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub(crate) use invalid_bytes_return::*;
3232
pub(crate) use invalid_envvar_default::*;
3333
pub(crate) use invalid_envvar_value::*;
3434
pub(crate) use invalid_hash_return::*;
35+
pub(crate) use invalid_index_return::*;
3536
pub(crate) use invalid_length_return::*;
3637
pub(crate) use invalid_str_return::*;
3738
pub(crate) use invalid_string_characters::*;
@@ -133,6 +134,7 @@ mod invalid_bytes_return;
133134
mod invalid_envvar_default;
134135
mod invalid_envvar_value;
135136
mod invalid_hash_return;
137+
mod invalid_index_return;
136138
mod invalid_length_return;
137139
mod invalid_str_return;
138140
mod invalid_string_characters;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
source: crates/ruff_linter/src/rules/pylint/mod.rs
3+
---
4+
invalid_return_type_index.py:8:16: PLE0305 `__index__` does not return an integer
5+
|
6+
7 | def __index__(self):
7+
8 | return True # [invalid-index-return]
8+
| ^^^^ PLE0305
9+
|
10+
11+
invalid_return_type_index.py:13:16: PLE0305 `__index__` does not return an integer
12+
|
13+
11 | class Float:
14+
12 | def __index__(self):
15+
13 | return 3.05 # [invalid-index-return]
16+
| ^^^^ PLE0305
17+
|
18+
19+
invalid_return_type_index.py:18:16: PLE0305 `__index__` does not return an integer
20+
|
21+
16 | class Dict:
22+
17 | def __index__(self):
23+
18 | return {"1": "1"} # [invalid-index-return]
24+
| ^^^^^^^^^^ PLE0305
25+
|
26+
27+
invalid_return_type_index.py:23:16: PLE0305 `__index__` does not return an integer
28+
|
29+
21 | class Str:
30+
22 | def __index__(self):
31+
23 | return "ruff" # [invalid-index-return]
32+
| ^^^^^^ PLE0305
33+
|
34+
35+
invalid_return_type_index.py:27:9: PLE0305 `__index__` does not return an integer
36+
|
37+
26 | class IndexNoReturn:
38+
27 | def __index__(self):
39+
| ^^^^^^^^^ PLE0305
40+
28 | print("ruff") # [invalid-index-return]
41+
|

ruff.schema.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)