Skip to content

Commit 33529c0

Browse files
Allow NoReturn-like functions for __str__, __len__, etc. (#11017)
## Summary If the method always raises, we shouldn't raise a diagnostic for "returning a value of the wrong type". Closes #11016.
1 parent e751b4e commit 33529c0

11 files changed

+103
-57
lines changed

crates/ruff_linter/resources/test/fixtures/pylint/invalid_return_type_bytes.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,6 @@ def __bytes__(self):
2121
print("ruff") # [invalid-bytes-return]
2222

2323

24-
class BytesWrongRaise:
25-
def __bytes__(self):
26-
print("raise some error")
27-
raise NotImplementedError # [invalid-bytes-return]
28-
29-
3024
# TODO: Once Ruff has better type checking
3125
def return_bytes():
3226
return "some string"
@@ -63,3 +57,9 @@ def __bytes__(self):
6357
class Bytes5:
6458
def __bytes__(self):
6559
raise NotImplementedError
60+
61+
62+
class Bytes6:
63+
def __bytes__(self):
64+
print("raise some error")
65+
raise NotImplementedError

crates/ruff_linter/resources/test/fixtures/pylint/invalid_return_type_length.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,6 @@ def __len__(self):
2626
return -42 # [invalid-length-return]
2727

2828

29-
class LengthWrongRaise:
30-
def __len__(self):
31-
print("raise some error")
32-
raise NotImplementedError # [invalid-length-return]
33-
34-
3529
# TODO: Once Ruff has better type checking
3630
def return_int():
3731
return "3"
@@ -68,3 +62,9 @@ def __len__(self):
6862
class Length5:
6963
def __len__(self):
7064
raise NotImplementedError
65+
66+
67+
class Length6:
68+
def __len__(self):
69+
print("raise some error")
70+
raise NotImplementedError

crates/ruff_linter/resources/test/fixtures/pylint/invalid_return_type_str.py

+11
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,14 @@ def __str__(self):
4747

4848
class Str3:
4949
def __str__(self): ...
50+
51+
52+
class Str4:
53+
def __str__(self):
54+
raise RuntimeError("__str__ not allowed")
55+
56+
57+
class Str5:
58+
def __str__(self): # PLE0307 (returns None if x <= 0)
59+
if x > 0:
60+
raise RuntimeError("__str__ not allowed")

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

+18-7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use ruff_python_ast::identifier::Identifier;
55
use ruff_python_ast::visitor::Visitor;
66
use ruff_python_ast::{self as ast};
77
use ruff_python_semantic::analyze::function_type::is_stub;
8+
use ruff_python_semantic::analyze::terminal::Terminal;
89
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
910
use ruff_text_size::Ranged;
1011

@@ -43,7 +44,7 @@ impl Violation for InvalidBoolReturnType {
4344
}
4445
}
4546

46-
/// E0307
47+
/// PLE0304
4748
pub(crate) fn invalid_bool_return(checker: &mut Checker, function_def: &ast::StmtFunctionDef) {
4849
if function_def.name.as_str() != "__bool__" {
4950
return;
@@ -57,19 +58,29 @@ pub(crate) fn invalid_bool_return(checker: &mut Checker, function_def: &ast::Stm
5758
return;
5859
}
5960

60-
let returns = {
61-
let mut visitor = ReturnStatementVisitor::default();
62-
visitor.visit_body(&function_def.body);
63-
visitor.returns
64-
};
61+
// Determine the terminal behavior (i.e., implicit return, no return, etc.).
62+
let terminal = Terminal::from_function(function_def);
63+
64+
// If every control flow path raises an exception, ignore the function.
65+
if terminal == Terminal::Raise {
66+
return;
67+
}
6568

66-
if returns.is_empty() {
69+
// If there are no return statements, add a diagnostic.
70+
if terminal == Terminal::Implicit {
6771
checker.diagnostics.push(Diagnostic::new(
6872
InvalidBoolReturnType,
6973
function_def.identifier(),
7074
));
75+
return;
7176
}
7277

78+
let returns = {
79+
let mut visitor = ReturnStatementVisitor::default();
80+
visitor.visit_body(&function_def.body);
81+
visitor.returns
82+
};
83+
7384
for stmt in returns {
7485
if let Some(value) = stmt.value.as_deref() {
7586
if !matches!(

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

+18-7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use ruff_python_ast::identifier::Identifier;
55
use ruff_python_ast::visitor::Visitor;
66
use ruff_python_ast::{self as ast};
77
use ruff_python_semantic::analyze::function_type::is_stub;
8+
use ruff_python_semantic::analyze::terminal::Terminal;
89
use ruff_python_semantic::analyze::type_inference::{PythonType, ResolvedPythonType};
910
use ruff_text_size::Ranged;
1011

@@ -43,7 +44,7 @@ impl Violation for InvalidBytesReturnType {
4344
}
4445
}
4546

46-
/// E0308
47+
/// PLE0308
4748
pub(crate) fn invalid_bytes_return(checker: &mut Checker, function_def: &ast::StmtFunctionDef) {
4849
if function_def.name.as_str() != "__bytes__" {
4950
return;
@@ -57,19 +58,29 @@ pub(crate) fn invalid_bytes_return(checker: &mut Checker, function_def: &ast::St
5758
return;
5859
}
5960

60-
let returns = {
61-
let mut visitor = ReturnStatementVisitor::default();
62-
visitor.visit_body(&function_def.body);
63-
visitor.returns
64-
};
61+
// Determine the terminal behavior (i.e., implicit return, no return, etc.).
62+
let terminal = Terminal::from_function(function_def);
63+
64+
// If every control flow path raises an exception, ignore the function.
65+
if terminal == Terminal::Raise {
66+
return;
67+
}
6568

66-
if returns.is_empty() {
69+
// If there are no return statements, add a diagnostic.
70+
if terminal == Terminal::Implicit {
6771
checker.diagnostics.push(Diagnostic::new(
6872
InvalidBytesReturnType,
6973
function_def.identifier(),
7074
));
75+
return;
7176
}
7277

78+
let returns = {
79+
let mut visitor = ReturnStatementVisitor::default();
80+
visitor.visit_body(&function_def.body);
81+
visitor.returns
82+
};
83+
7384
for stmt in returns {
7485
if let Some(value) = stmt.value.as_deref() {
7586
if !matches!(

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

+17-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use ruff_python_ast::identifier::Identifier;
55
use ruff_python_ast::visitor::Visitor;
66
use ruff_python_ast::{self as ast, Expr};
77
use ruff_python_semantic::analyze::function_type::is_stub;
8+
use ruff_python_semantic::analyze::terminal::Terminal;
89
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
910
use ruff_text_size::Ranged;
1011

@@ -63,19 +64,29 @@ pub(crate) fn invalid_length_return(checker: &mut Checker, function_def: &ast::S
6364
return;
6465
}
6566

66-
let returns = {
67-
let mut visitor = ReturnStatementVisitor::default();
68-
visitor.visit_body(&function_def.body);
69-
visitor.returns
70-
};
67+
// Determine the terminal behavior (i.e., implicit return, no return, etc.).
68+
let terminal = Terminal::from_function(function_def);
69+
70+
// If every control flow path raises an exception, ignore the function.
71+
if terminal == Terminal::Raise {
72+
return;
73+
}
7174

72-
if returns.is_empty() {
75+
// If there are no return statements, add a diagnostic.
76+
if terminal == Terminal::Implicit {
7377
checker.diagnostics.push(Diagnostic::new(
7478
InvalidLengthReturnType,
7579
function_def.identifier(),
7680
));
81+
return;
7782
}
7883

84+
let returns = {
85+
let mut visitor = ReturnStatementVisitor::default();
86+
visitor.visit_body(&function_def.body);
87+
visitor.returns
88+
};
89+
7990
for stmt in returns {
8091
if let Some(value) = stmt.value.as_deref() {
8192
if is_negative_integer(value)

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

+17-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use ruff_python_ast::identifier::Identifier;
55
use ruff_python_ast::visitor::Visitor;
66
use ruff_python_ast::{self as ast};
77
use ruff_python_semantic::analyze::function_type::is_stub;
8+
use ruff_python_semantic::analyze::terminal::Terminal;
89
use ruff_python_semantic::analyze::type_inference::{PythonType, ResolvedPythonType};
910
use ruff_text_size::Ranged;
1011

@@ -57,19 +58,29 @@ pub(crate) fn invalid_str_return(checker: &mut Checker, function_def: &ast::Stmt
5758
return;
5859
}
5960

60-
let returns = {
61-
let mut visitor = ReturnStatementVisitor::default();
62-
visitor.visit_body(&function_def.body);
63-
visitor.returns
64-
};
61+
// Determine the terminal behavior (i.e., implicit return, no return, etc.).
62+
let terminal = Terminal::from_function(function_def);
63+
64+
// If every control flow path raises an exception, ignore the function.
65+
if terminal == Terminal::Raise {
66+
return;
67+
}
6568

66-
if returns.is_empty() {
69+
// If there are no return statements, add a diagnostic.
70+
if terminal == Terminal::Implicit {
6771
checker.diagnostics.push(Diagnostic::new(
6872
InvalidStrReturnType,
6973
function_def.identifier(),
7074
));
75+
return;
7176
}
7277

78+
let returns = {
79+
let mut visitor = ReturnStatementVisitor::default();
80+
visitor.visit_body(&function_def.body);
81+
visitor.returns
82+
};
83+
7384
for stmt in returns {
7485
if let Some(value) = stmt.value.as_deref() {
7586
if !matches!(

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ impl Violation for NonSlotAssignment {
5959
}
6060
}
6161

62-
/// E0237
62+
/// PLE0237
6363
pub(crate) fn non_slot_assignment(checker: &mut Checker, class_def: &ast::StmtClassDef) {
6464
let semantic = checker.semantic();
6565

crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0303_invalid_return_type_length.py.snap

-9
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,3 @@ invalid_return_type_length.py:26:16: PLE0303 `__len__` does not return a non-neg
4040
26 | return -42 # [invalid-length-return]
4141
| ^^^ PLE0303
4242
|
43-
44-
invalid_return_type_length.py:30:9: PLE0303 `__len__` does not return a non-negative integer
45-
|
46-
29 | class LengthWrongRaise:
47-
30 | def __len__(self):
48-
| ^^^^^^^ PLE0303
49-
31 | print("raise some error")
50-
32 | raise NotImplementedError # [invalid-length-return]
51-
|

crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0307_invalid_return_type_str.py.snap

+9
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,12 @@ invalid_return_type_str.py:21:16: PLE0307 `__str__` does not return `str`
3232
21 | return False
3333
| ^^^^^ PLE0307
3434
|
35+
36+
invalid_return_type_str.py:58:9: PLE0307 `__str__` does not return `str`
37+
|
38+
57 | class Str5:
39+
58 | def __str__(self): # PLE0307 (returns None if x <= 0)
40+
| ^^^^^^^ PLE0307
41+
59 | if x > 0:
42+
60 | raise RuntimeError("__str__ not allowed")
43+
|

crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0308_invalid_return_type_bytes.py.snap

-9
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,3 @@ invalid_return_type_bytes.py:20:9: PLE0308 `__bytes__` does not return `bytes`
3232
| ^^^^^^^^^ PLE0308
3333
21 | print("ruff") # [invalid-bytes-return]
3434
|
35-
36-
invalid_return_type_bytes.py:25:9: PLE0308 `__bytes__` does not return `bytes`
37-
|
38-
24 | class BytesWrongRaise:
39-
25 | def __bytes__(self):
40-
| ^^^^^^^^^ PLE0308
41-
26 | print("raise some error")
42-
27 | raise NotImplementedError # [invalid-bytes-return]
43-
|

0 commit comments

Comments
 (0)