Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B004.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,21 @@ def __init__(self): self.__call__ = None

assert hasattr(A(), "__call__")
assert callable(A()) is False

# https://github.com/astral-sh/ruff/issues/20440
def test_invalid_hasattr_calls():
hasattr(0, "__call__", 0) # 3 args - invalid
hasattr(0, "__call__", x=0) # keyword arg - invalid
hasattr(0, "__call__", 0, x=0) # 3 args + keyword - invalid
hasattr() # no args - invalid
hasattr(0) # 1 arg - invalid
hasattr(*(), "__call__", "extra") # unpacking - invalid
hasattr(*()) # unpacking - invalid

def test_invalid_getattr_calls():
getattr(0, "__call__", None, "extra") # 4 args - invalid
getattr(0, "__call__", default=None) # keyword arg - invalid
getattr() # no args - invalid
getattr(0) # 1 arg - invalid
getattr(*(), "__call__", None, "extra") # unpacking - invalid
getattr(*()) # unpacking - invalid
4 changes: 3 additions & 1 deletion crates/ruff_linter/src/checkers/ast/analyze/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -717,7 +717,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
flake8_bugbear::rules::re_sub_positional_args(checker, call);
}
if checker.is_rule_enabled(Rule::UnreliableCallableCheck) {
flake8_bugbear::rules::unreliable_callable_check(checker, expr, func, args);
flake8_bugbear::rules::unreliable_callable_check(
checker, expr, func, args, keywords,
);
}
if checker.is_rule_enabled(Rule::StripWithMultiCharacters) {
flake8_bugbear::rules::strip_with_multi_characters(checker, expr, func, args);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,11 @@ pub(crate) fn unreliable_callable_check(
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[ast::Keyword],
) {
if !keywords.is_empty() {
return;
}
let [obj, attr, ..] = args else {
return;
};
Expand All @@ -103,7 +107,21 @@ pub(crate) fn unreliable_callable_check(
let Some(builtins_function) = checker.semantic().resolve_builtin_symbol(func) else {
return;
};
if !matches!(builtins_function, "hasattr" | "getattr") {

// Validate function arguments based on function name
let valid_args = match builtins_function {
"hasattr" => {
// hasattr should have exactly 2 positional arguments and no keywords
args.len() == 2
}
"getattr" => {
// getattr should have 2 or 3 positional arguments and no keywords
args.len() == 2 || args.len() == 3
}
_ => return,
};

if !valid_args {
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,6 @@ help: Replace with `callable()`
- assert hasattr(A(), "__call__")
53 + assert callable(A())
54 | assert callable(A()) is False
55 |
56 | # https://github.com/astral-sh/ruff/issues/20440
note: This is an unsafe fix and may change runtime behavior