[ruff] Implement falsy-dict-get-fallback (RUF056) (#15160)

Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
Arnav Gupta 2024-12-31 05:40:51 -05:00 committed by GitHub
parent 68d2466832
commit 3c9021ffcb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 897 additions and 88 deletions

View file

@ -0,0 +1,171 @@
# Correct
my_dict = {"key": "value", "other_key": "other_value"}
# Using dict.get without a fallback
value = my_dict.get("key")
# Using dict.get with a truthy fallback
value = my_dict.get("key", "default")
value = my_dict.get("key", 42)
value = my_dict.get("key", [1, 2, 3])
value = my_dict.get("key", {"nested": "dict"})
value = my_dict.get("key", set([1, 2]))
value = my_dict.get("key", True)
value = my_dict.get("key", "Non-empty string")
value = my_dict.get("key", 3.14)
value = my_dict.get("key", (1, 2)) # Tuples are truthy
# Chained get calls with truthy fallbacks
value1 = my_dict.get("key1", {'k': 'v'}).get("subkey")
value2 = my_dict.get("key2", [1, 2, 3]).append(4)
# Valid
# Using dict.get with a falsy fallback: False
value = my_dict.get("key", False)
# Using dict.get with a falsy fallback: empty string
value = my_dict.get("key", "")
# Using dict.get with a falsy fallback: empty list
value = my_dict.get("key", [])
# Using dict.get with a falsy fallback: empty dict
value = my_dict.get("key", {})
# Using dict.get with a falsy fallback: empty set
value = my_dict.get("key", set())
# Using dict.get with a falsy fallback: zero integer
value = my_dict.get("key", 0)
# Using dict.get with a falsy fallback: zero float
value = my_dict.get("key", 0.0)
# Using dict.get with a falsy fallback: None
value = my_dict.get("key", None)
# Using dict.get with a falsy fallback via function call
value = my_dict.get("key", list())
value = my_dict.get("key", dict())
value = my_dict.get("key", set())
# Reassigning with falsy fallback
def get_value(d):
return d.get("key", False)
# Multiple dict.get calls with mixed fallbacks
value1 = my_dict.get("key1", "default")
value2 = my_dict.get("key2", 0)
value3 = my_dict.get("key3", "another default")
# Using dict.get in a class with falsy fallback
class MyClass:
def method(self):
return self.data.get("key", {})
# Using dict.get in a nested function with falsy fallback
def outer():
def inner():
return my_dict.get("key", "")
return inner()
# Using dict.get with variable fallback that is falsy
falsy_value = None
value = my_dict.get("key", falsy_value)
# Using dict.get with variable fallback that is truthy
truthy_value = "exists"
value = my_dict.get("key", truthy_value)
# Using dict.get with complex expressions as fallback
value = my_dict.get("key", 0 or "default")
value = my_dict.get("key", [] if condition else {})
# testing dict.get call using kwargs
value = my_dict.get(key="key", default=False)
value = my_dict.get(default=[], key="key")
# Edge Cases
dicts = [my_dict, my_dict, my_dict]
# Falsy fallback in a lambda
get_fallback = lambda d: d.get("key", False)
# Falsy fallback in a list comprehension
results = [d.get("key", "") for d in dicts]
# Falsy fallback in a generator expression
results = (d.get("key", None) for d in dicts)
# Falsy fallback in a ternary expression
value = my_dict.get("key", 0) if True else "default"
# Falsy fallback with inline comment
value = my_dict.get("key", # comment1
[] # comment2
) # comment3
# Invalid
# Invalid falsy fallbacks are when the call to dict.get is used in a boolean context
# dict.get in ternary expression
value = "not found" if my_dict.get("key", False) else "default" # [RUF056]
# dict.get in an if statement
if my_dict.get("key", False): # [RUF056]
pass
# dict.get in compound if statement
if True and my_dict.get("key", False): # [RUF056]
pass
if my_dict.get("key", False) or True: # [RUF056]
pass
# dict.get in an assert statement
assert my_dict.get("key", False) # [RUF056]
# dict.get in a while statement
while my_dict.get("key", False): # [RUF056]
pass
# dict.get in unary not expression
value = not my_dict.get("key", False) # [RUF056]
# testing all falsy fallbacks
value = not my_dict.get("key", False) # [RUF056]
value = not my_dict.get("key", []) # [RUF056]
value = not my_dict.get("key", list()) # [RUF056]
value = not my_dict.get("key", {}) # [RUF056]
value = not my_dict.get("key", dict()) # [RUF056]
value = not my_dict.get("key", set()) # [RUF056]
value = not my_dict.get("key", None) # [RUF056]
value = not my_dict.get("key", 0) # [RUF056]
value = not my_dict.get("key", 0.0) # [RUF056]
value = not my_dict.get("key", "") # [RUF056]
# testing dict.get call using kwargs
value = not my_dict.get(key="key", default=False) # [RUF056]
value = not my_dict.get(default=[], key="key") # [RUF056]
# testing invalid dict.get call with inline comment
value = not my_dict.get("key", # comment1
[] # comment2
) # [RUF056]
# testing invalid dict.get call with kwargs and inline comment
value = not my_dict.get(key="key", # comment1
default=False # comment2
) # [RUF056]
value = not my_dict.get(default=[], # comment1
key="key" # comment2
) # [RUF056]
# testing invalid dict.get calls
value = not my_dict.get(key="key", other="something", default=False)
value = not my_dict.get(default=False, other="something", key="test")

View file

@ -1111,6 +1111,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::PytestRaisesAmbiguousPattern) { if checker.enabled(Rule::PytestRaisesAmbiguousPattern) {
ruff::rules::pytest_raises_ambiguous_pattern(checker, call); ruff::rules::pytest_raises_ambiguous_pattern(checker, call);
} }
if checker.enabled(Rule::FalsyDictGetFallback) {
ruff::rules::falsy_dict_get_fallback(checker, expr);
}
} }
Expr::Dict(dict) => { Expr::Dict(dict) => {
if checker.any_enabled(&[ if checker.any_enabled(&[

View file

@ -991,6 +991,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "051") => (RuleGroup::Preview, rules::ruff::rules::IfKeyInDictDel), (Ruff, "051") => (RuleGroup::Preview, rules::ruff::rules::IfKeyInDictDel),
(Ruff, "052") => (RuleGroup::Preview, rules::ruff::rules::UsedDummyVariable), (Ruff, "052") => (RuleGroup::Preview, rules::ruff::rules::UsedDummyVariable),
(Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression), (Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression),
(Ruff, "056") => (RuleGroup::Preview, rules::ruff::rules::FalsyDictGetFallback),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA), (Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA), (Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),

View file

@ -293,10 +293,10 @@ impl Renamer {
let qualified_name = semantic.resolve_qualified_name(func)?; let qualified_name = semantic.resolve_qualified_name(func)?;
let name_argument = match qualified_name.segments() { let name_argument = match qualified_name.segments() {
["collections", "namedtuple"] => arguments.find_argument("typename", 0), ["collections", "namedtuple"] => arguments.find_argument_value("typename", 0),
["typing" | "typing_extensions", "TypeVar" | "ParamSpec" | "TypeVarTuple" | "NewType" | "TypeAliasType"] => { ["typing" | "typing_extensions", "TypeVar" | "ParamSpec" | "TypeVarTuple" | "NewType" | "TypeAliasType"] => {
arguments.find_argument("name", 0) arguments.find_argument_value("name", 0)
} }
["enum", "Enum" | "IntEnum" | "StrEnum" | "ReprEnum" | "Flag" | "IntFlag"] ["enum", "Enum" | "IntEnum" | "StrEnum" | "ReprEnum" | "Flag" | "IntFlag"]

View file

@ -62,7 +62,7 @@ pub(crate) fn async_zero_sleep(checker: &mut Checker, call: &ExprCall) {
return; return;
} }
let Some(arg) = call.arguments.find_argument("seconds", 0) else { let Some(arg) = call.arguments.find_argument_value("seconds", 0) else {
return; return;
}; };

View file

@ -148,7 +148,7 @@ pub(crate) fn blocking_process_invocation(checker: &mut Checker, call: &ast::Exp
} }
fn is_p_wait(call: &ast::ExprCall, semantic: &SemanticModel) -> bool { fn is_p_wait(call: &ast::ExprCall, semantic: &SemanticModel) -> bool {
let Some(arg) = call.arguments.find_argument("mode", 0) else { let Some(arg) = call.arguments.find_argument_value("mode", 0) else {
return true; return true;
}; };

View file

@ -67,7 +67,7 @@ pub(crate) fn long_sleep_not_forever(checker: &mut Checker, call: &ExprCall) {
return; return;
} }
let Some(arg) = call.arguments.find_argument("seconds", 0) else { let Some(arg) = call.arguments.find_argument_value("seconds", 0) else {
return; return;
}; };

View file

@ -71,7 +71,7 @@ pub(crate) fn bad_file_permissions(checker: &mut Checker, call: &ast::ExprCall)
.resolve_qualified_name(&call.func) .resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["os", "chmod"])) .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["os", "chmod"]))
{ {
if let Some(mode_arg) = call.arguments.find_argument("mode", 1) { if let Some(mode_arg) = call.arguments.find_argument_value("mode", 1) {
match parse_mask(mode_arg, checker.semantic()) { match parse_mask(mode_arg, checker.semantic()) {
// The mask couldn't be determined (e.g., it's dynamic). // The mask couldn't be determined (e.g., it's dynamic).
Ok(None) => {} Ok(None) => {}

View file

@ -62,7 +62,7 @@ pub(crate) fn django_extra(checker: &mut Checker, call: &ast::ExprCall) {
fn is_call_insecure(call: &ast::ExprCall) -> bool { fn is_call_insecure(call: &ast::ExprCall) -> bool {
for (argument_name, position) in [("select", 0), ("where", 1), ("tables", 3)] { for (argument_name, position) in [("select", 0), ("where", 1), ("tables", 3)] {
if let Some(argument) = call.arguments.find_argument(argument_name, position) { if let Some(argument) = call.arguments.find_argument_value(argument_name, position) {
match argument_name { match argument_name {
"select" => match argument { "select" => match argument {
Expr::Dict(dict) => { Expr::Dict(dict) => {

View file

@ -52,7 +52,7 @@ pub(crate) fn django_raw_sql(checker: &mut Checker, call: &ast::ExprCall) {
{ {
if !call if !call
.arguments .arguments
.find_argument("sql", 0) .find_argument_value("sql", 0)
.is_some_and(Expr::is_string_literal_expr) .is_some_and(Expr::is_string_literal_expr)
{ {
checker checker

View file

@ -115,7 +115,7 @@ fn detect_insecure_hashlib_calls(
match hashlib_call { match hashlib_call {
HashlibCall::New => { HashlibCall::New => {
let Some(name_arg) = call.arguments.find_argument("name", 0) else { let Some(name_arg) = call.arguments.find_argument_value("name", 0) else {
return; return;
}; };
let Some(hash_func_name) = string_literal(name_arg) else { let Some(hash_func_name) = string_literal(name_arg) else {
@ -159,7 +159,7 @@ fn detect_insecure_crypt_calls(checker: &mut Checker, call: &ast::ExprCall) {
_ => None, _ => None,
}) })
.and_then(|(argument_name, position)| { .and_then(|(argument_name, position)| {
call.arguments.find_argument(argument_name, position) call.arguments.find_argument_value(argument_name, position)
}) })
else { else {
return; return;

View file

@ -53,7 +53,7 @@ pub(crate) fn ssh_no_host_key_verification(checker: &mut Checker, call: &ExprCal
return; return;
} }
let Some(policy_argument) = call.arguments.find_argument("policy", 0) else { let Some(policy_argument) = call.arguments.find_argument_value("policy", 0) else {
return; return;
}; };

View file

@ -906,7 +906,7 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
["six", "moves", "urllib", "request", "Request"] => { ["six", "moves", "urllib", "request", "Request"] => {
// If the `url` argument is a string literal or an f-string, allow `http` and `https` schemes. // If the `url` argument is a string literal or an f-string, allow `http` and `https` schemes.
if call.arguments.args.iter().all(|arg| !arg.is_starred_expr()) && call.arguments.keywords.iter().all(|keyword| keyword.arg.is_some()) { if call.arguments.args.iter().all(|arg| !arg.is_starred_expr()) && call.arguments.keywords.iter().all(|keyword| keyword.arg.is_some()) {
if call.arguments.find_argument("url", 0).and_then(leading_chars).is_some_and(has_http_prefix) { if call.arguments.find_argument_value("url", 0).and_then(leading_chars).is_some_and(has_http_prefix) {
return None; return None;
} }
} }
@ -916,11 +916,11 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
["urllib", "request", "urlopen" | "urlretrieve" ] | ["urllib", "request", "urlopen" | "urlretrieve" ] |
["six", "moves", "urllib", "request", "urlopen" | "urlretrieve" ] => { ["six", "moves", "urllib", "request", "urlopen" | "urlretrieve" ] => {
if call.arguments.args.iter().all(|arg| !arg.is_starred_expr()) && call.arguments.keywords.iter().all(|keyword| keyword.arg.is_some()) { if call.arguments.args.iter().all(|arg| !arg.is_starred_expr()) && call.arguments.keywords.iter().all(|keyword| keyword.arg.is_some()) {
match call.arguments.find_argument("url", 0) { match call.arguments.find_argument_value("url", 0) {
// If the `url` argument is a `urllib.request.Request` object, allow `http` and `https` schemes. // If the `url` argument is a `urllib.request.Request` object, allow `http` and `https` schemes.
Some(Expr::Call(ExprCall { func, arguments, .. })) => { Some(Expr::Call(ExprCall { func, arguments, .. })) => {
if checker.semantic().resolve_qualified_name(func.as_ref()).is_some_and(|name| name.segments() == ["urllib", "request", "Request"]) { if checker.semantic().resolve_qualified_name(func.as_ref()).is_some_and(|name| name.segments() == ["urllib", "request", "Request"]) {
if arguments.find_argument("url", 0).and_then(leading_chars).is_some_and(has_http_prefix) { if arguments.find_argument_value("url", 0).and_then(leading_chars).is_some_and(has_http_prefix) {
return None; return None;
} }
} }

View file

@ -65,7 +65,7 @@ pub(crate) fn unsafe_yaml_load(checker: &mut Checker, call: &ast::ExprCall) {
.resolve_qualified_name(&call.func) .resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["yaml", "load"])) .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["yaml", "load"]))
{ {
if let Some(loader_arg) = call.arguments.find_argument("Loader", 1) { if let Some(loader_arg) = call.arguments.find_argument_value("Loader", 1) {
if !checker if !checker
.semantic() .semantic()
.resolve_qualified_name(loader_arg) .resolve_qualified_name(loader_arg)

View file

@ -114,7 +114,7 @@ fn extract_cryptographic_key(
Some((CryptographicKey::Rsa { key_size }, range)) Some((CryptographicKey::Rsa { key_size }, range))
} }
"ec" => { "ec" => {
let argument = call.arguments.find_argument("curve", 0)?; let argument = call.arguments.find_argument_value("curve", 0)?;
let ExprAttribute { attr, value, .. } = argument.as_attribute_expr()?; let ExprAttribute { attr, value, .. } = argument.as_attribute_expr()?;
let qualified_name = checker.semantic().resolve_qualified_name(value)?; let qualified_name = checker.semantic().resolve_qualified_name(value)?;
if matches!( if matches!(
@ -150,7 +150,7 @@ fn extract_cryptographic_key(
} }
fn extract_int_argument(call: &ExprCall, name: &str, position: usize) -> Option<(u16, TextRange)> { fn extract_int_argument(call: &ExprCall, name: &str, position: usize) -> Option<(u16, TextRange)> {
let argument = call.arguments.find_argument(name, position)?; let argument = call.arguments.find_argument_value(name, position)?;
let Expr::NumberLiteral(ast::ExprNumberLiteral { let Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(i), value: ast::Number::Int(i),
.. ..

View file

@ -60,7 +60,10 @@ pub(crate) fn no_explicit_stacklevel(checker: &mut Checker, call: &ast::ExprCall
return; return;
} }
if call.arguments.find_argument("stacklevel", 2).is_some() if call
.arguments
.find_argument_value("stacklevel", 2)
.is_some()
|| call || call
.arguments .arguments
.args .args

View file

@ -91,7 +91,7 @@ pub(crate) fn call_datetime_fromtimestamp(checker: &mut Checker, call: &ast::Exp
return; return;
} }
let antipattern = match call.arguments.find_argument("tz", 1) { let antipattern = match call.arguments.find_argument_value("tz", 1) {
Some(ast::Expr::NoneLiteral(_)) => DatetimeModuleAntipattern::NonePassedToTzArgument, Some(ast::Expr::NoneLiteral(_)) => DatetimeModuleAntipattern::NonePassedToTzArgument,
Some(_) => return, Some(_) => return,
None => DatetimeModuleAntipattern::NoTzArgumentPassed, None => DatetimeModuleAntipattern::NoTzArgumentPassed,

View file

@ -86,7 +86,7 @@ pub(crate) fn call_datetime_now_without_tzinfo(checker: &mut Checker, call: &ast
return; return;
} }
let antipattern = match call.arguments.find_argument("tz", 0) { let antipattern = match call.arguments.find_argument_value("tz", 0) {
Some(ast::Expr::NoneLiteral(_)) => DatetimeModuleAntipattern::NonePassedToTzArgument, Some(ast::Expr::NoneLiteral(_)) => DatetimeModuleAntipattern::NonePassedToTzArgument,
Some(_) => return, Some(_) => return,
None => DatetimeModuleAntipattern::NoTzArgumentPassed, None => DatetimeModuleAntipattern::NoTzArgumentPassed,

View file

@ -80,7 +80,7 @@ pub(crate) fn call_datetime_without_tzinfo(checker: &mut Checker, call: &ast::Ex
return; return;
} }
let antipattern = match call.arguments.find_argument("tzinfo", 7) { let antipattern = match call.arguments.find_argument_value("tzinfo", 7) {
Some(ast::Expr::NoneLiteral(_)) => DatetimeModuleAntipattern::NonePassedToTzArgument, Some(ast::Expr::NoneLiteral(_)) => DatetimeModuleAntipattern::NonePassedToTzArgument,
Some(_) => return, Some(_) => return,
None => DatetimeModuleAntipattern::NoTzArgumentPassed, None => DatetimeModuleAntipattern::NoTzArgumentPassed,

View file

@ -59,7 +59,7 @@ pub(crate) fn locals_in_render_function(checker: &mut Checker, call: &ast::ExprC
return; return;
} }
if let Some(argument) = call.arguments.find_argument("context", 2) { if let Some(argument) = call.arguments.find_argument_value("context", 2) {
if is_locals_call(argument, checker.semantic()) { if is_locals_call(argument, checker.semantic()) {
checker.diagnostics.push(Diagnostic::new( checker.diagnostics.push(Diagnostic::new(
DjangoLocalsInRenderFunction, DjangoLocalsInRenderFunction,

View file

@ -62,7 +62,8 @@ pub(crate) fn invalid_get_logger_argument(checker: &mut Checker, call: &ast::Exp
return; return;
} }
let Some(Expr::Name(expr @ ast::ExprName { id, .. })) = call.arguments.find_argument("name", 0) let Some(Expr::Name(expr @ ast::ExprName { id, .. })) =
call.arguments.find_argument_value("name", 0)
else { else {
return; return;
}; };

View file

@ -181,7 +181,7 @@ pub(crate) fn logging_call(checker: &mut Checker, call: &ast::ExprCall) {
// G001 - G004 // G001 - G004
let msg_pos = usize::from(matches!(logging_call_type, LoggingCallType::LogCall)); let msg_pos = usize::from(matches!(logging_call_type, LoggingCallType::LogCall));
if let Some(format_arg) = call.arguments.find_argument("msg", msg_pos) { if let Some(format_arg) = call.arguments.find_argument_value("msg", msg_pos) {
check_msg(checker, format_arg); check_msg(checker, format_arg);
} }

View file

@ -61,8 +61,8 @@ pub(crate) fn fail_call(checker: &mut Checker, call: &ast::ExprCall) {
// `pytest.fail(msg="...")` (deprecated in pytest 7.0) // `pytest.fail(msg="...")` (deprecated in pytest 7.0)
if call if call
.arguments .arguments
.find_argument("reason", 0) .find_argument_value("reason", 0)
.or_else(|| call.arguments.find_argument("msg", 0)) .or_else(|| call.arguments.find_argument_value("msg", 0))
.map_or(true, is_empty_or_null_string) .map_or(true, is_empty_or_null_string)
{ {
checker checker

View file

@ -864,12 +864,12 @@ pub(crate) fn parametrize(checker: &mut Checker, call: &ExprCall) {
if checker.enabled(Rule::PytestParametrizeNamesWrongType) { if checker.enabled(Rule::PytestParametrizeNamesWrongType) {
let names = if checker.settings.preview.is_enabled() { let names = if checker.settings.preview.is_enabled() {
call.arguments.find_argument("argnames", 0) call.arguments.find_argument_value("argnames", 0)
} else { } else {
call.arguments.find_positional(0) call.arguments.find_positional(0)
}; };
let values = if checker.settings.preview.is_enabled() { let values = if checker.settings.preview.is_enabled() {
call.arguments.find_argument("argvalues", 1) call.arguments.find_argument_value("argvalues", 1)
} else { } else {
call.arguments.find_positional(1) call.arguments.find_positional(1)
}; };
@ -879,12 +879,12 @@ pub(crate) fn parametrize(checker: &mut Checker, call: &ExprCall) {
} }
if checker.enabled(Rule::PytestParametrizeValuesWrongType) { if checker.enabled(Rule::PytestParametrizeValuesWrongType) {
let names = if checker.settings.preview.is_enabled() { let names = if checker.settings.preview.is_enabled() {
call.arguments.find_argument("argnames", 0) call.arguments.find_argument_value("argnames", 0)
} else { } else {
call.arguments.find_positional(0) call.arguments.find_positional(0)
}; };
let values = if checker.settings.preview.is_enabled() { let values = if checker.settings.preview.is_enabled() {
call.arguments.find_argument("argvalues", 1) call.arguments.find_argument_value("argvalues", 1)
} else { } else {
call.arguments.find_positional(1) call.arguments.find_positional(1)
}; };
@ -894,7 +894,7 @@ pub(crate) fn parametrize(checker: &mut Checker, call: &ExprCall) {
} }
if checker.enabled(Rule::PytestDuplicateParametrizeTestCases) { if checker.enabled(Rule::PytestDuplicateParametrizeTestCases) {
if let Some(values) = if checker.settings.preview.is_enabled() { if let Some(values) = if checker.settings.preview.is_enabled() {
call.arguments.find_argument("argvalues", 1) call.arguments.find_argument_value("argvalues", 1)
} else { } else {
call.arguments.find_positional(1) call.arguments.find_positional(1)
} { } {

View file

@ -84,7 +84,7 @@ fn check_patch_call(call: &ast::ExprCall, index: usize) -> Option<Diagnostic> {
range: _, range: _,
} = call } = call
.arguments .arguments
.find_argument("new", index)? .find_argument_value("new", index)?
.as_lambda_expr()?; .as_lambda_expr()?;
// Walk the lambda body. If the lambda uses the arguments, then it's valid. // Walk the lambda body. If the lambda uses the arguments, then it's valid.

View file

@ -177,7 +177,7 @@ pub(crate) fn raises_call(checker: &mut Checker, call: &ast::ExprCall) {
} }
if checker.enabled(Rule::PytestRaisesTooBroad) { if checker.enabled(Rule::PytestRaisesTooBroad) {
if let Some(exception) = call.arguments.find_argument("expected_exception", 0) { if let Some(exception) = call.arguments.find_argument_value("expected_exception", 0) {
if call if call
.arguments .arguments
.find_keyword("match") .find_keyword("match")

View file

@ -67,7 +67,7 @@ pub(crate) fn split_static_string(
) { ) {
let ExprCall { arguments, .. } = call; let ExprCall { arguments, .. } = call;
let maxsplit_arg = arguments.find_argument("maxsplit", 1); let maxsplit_arg = arguments.find_argument_value("maxsplit", 1);
let Some(maxsplit_value) = get_maxsplit_value(maxsplit_arg) else { let Some(maxsplit_value) = get_maxsplit_value(maxsplit_arg) else {
return; return;
}; };
@ -79,7 +79,7 @@ pub(crate) fn split_static_string(
Direction::Right Direction::Right
}; };
let sep_arg = arguments.find_argument("sep", 0); let sep_arg = arguments.find_argument_value("sep", 0);
let split_replacement = if let Some(sep) = sep_arg { let split_replacement = if let Some(sep) = sep_arg {
match sep { match sep {
Expr::NoneLiteral(_) => split_default(str_value, maxsplit_value), Expr::NoneLiteral(_) => split_default(str_value, maxsplit_value),

View file

@ -89,7 +89,7 @@ pub(crate) fn invalid_pathlib_with_suffix(checker: &mut Checker, call: &ast::Exp
return; return;
} }
let Some(ast::Expr::StringLiteral(string)) = arguments.find_argument("suffix", 0) else { let Some(ast::Expr::StringLiteral(string)) = arguments.find_argument_value("suffix", 0) else {
return; return;
}; };

View file

@ -82,7 +82,7 @@ pub(crate) fn os_sep_split(checker: &mut Checker, call: &ast::ExprCall) {
return; return;
} }
let Some(sep) = call.arguments.find_argument("sep", 0) else { let Some(sep) = call.arguments.find_argument_value("sep", 0) else {
return; return;
}; };

View file

@ -115,7 +115,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &mut Checker, call: &ExprCall) {
// ``` // ```
if call if call
.arguments .arguments
.find_argument("closefd", 6) .find_argument_value("closefd", 6)
.is_some_and(|expr| { .is_some_and(|expr| {
!matches!( !matches!(
expr, expr,
@ -124,7 +124,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &mut Checker, call: &ExprCall) {
}) })
|| call || call
.arguments .arguments
.find_argument("opener", 7) .find_argument_value("opener", 7)
.is_some_and(|expr| !expr.is_none_literal_expr()) .is_some_and(|expr| !expr.is_none_literal_expr())
|| call || call
.arguments .arguments

View file

@ -121,8 +121,8 @@ pub(super) fn is_django_model_import(name: &str, stmt: &Stmt, semantic: &Semanti
// Match against, e.g., `apps.get_model("zerver", "Attachment")`. // Match against, e.g., `apps.get_model("zerver", "Attachment")`.
if let Some(unqualified_name) = UnqualifiedName::from_expr(func.as_ref()) { if let Some(unqualified_name) = UnqualifiedName::from_expr(func.as_ref()) {
if matches!(unqualified_name.segments(), [.., "get_model"]) { if matches!(unqualified_name.segments(), [.., "get_model"]) {
if let Some(argument) = if let Some(argument) = arguments
arguments.find_argument("model_name", arguments.args.len().saturating_sub(1)) .find_argument_value("model_name", arguments.args.len().saturating_sub(1))
{ {
if let Some(string_literal) = argument.as_string_literal_expr() { if let Some(string_literal) = argument.as_string_literal_expr() {
if string_literal.value.to_str() == name { if string_literal.value.to_str() == name {
@ -141,7 +141,7 @@ pub(super) fn is_django_model_import(name: &str, stmt: &Stmt, semantic: &Semanti
qualified_name.segments(), qualified_name.segments(),
["django", "utils", "module_loading", "import_string"] ["django", "utils", "module_loading", "import_string"]
) { ) {
if let Some(argument) = arguments.find_argument("dotted_path", 0) { if let Some(argument) = arguments.find_argument_value("dotted_path", 0) {
if let Some(string_literal) = argument.as_string_literal_expr() { if let Some(string_literal) = argument.as_string_literal_expr() {
if let Some((.., model)) = string_literal.value.to_str().rsplit_once('.') { if let Some((.., model)) = string_literal.value.to_str().rsplit_once('.') {
if model == name { if model == name {

View file

@ -10,7 +10,7 @@ use crate::settings::LinterSettings;
/// Returns the value of the `name` parameter to, e.g., a `TypeVar` constructor. /// Returns the value of the `name` parameter to, e.g., a `TypeVar` constructor.
pub(super) fn type_param_name(arguments: &Arguments) -> Option<&str> { pub(super) fn type_param_name(arguments: &Arguments) -> Option<&str> {
// Handle both `TypeVar("T")` and `TypeVar(name="T")`. // Handle both `TypeVar("T")` and `TypeVar(name="T")`.
let name_param = arguments.find_argument("name", 0)?; let name_param = arguments.find_argument_value("name", 0)?;
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = &name_param { if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = &name_param {
Some(value.to_str()) Some(value.to_str())
} else { } else {

View file

@ -107,7 +107,7 @@ fn is_open(func: &Expr, semantic: &SemanticModel) -> Option<Kind> {
/// Returns the mode argument, if present. /// Returns the mode argument, if present.
fn extract_mode(call: &ast::ExprCall, kind: Kind) -> Option<&Expr> { fn extract_mode(call: &ast::ExprCall, kind: Kind) -> Option<&Expr> {
match kind { match kind {
Kind::Builtin => call.arguments.find_argument("mode", 1), Kind::Builtin => call.arguments.find_argument_value("mode", 1),
Kind::Pathlib => call.arguments.find_argument("mode", 0), Kind::Pathlib => call.arguments.find_argument_value("mode", 0),
} }
} }

View file

@ -63,7 +63,7 @@ pub(crate) fn invalid_envvar_default(checker: &mut Checker, call: &ast::ExprCall
}) })
{ {
// Find the `default` argument, if it exists. // Find the `default` argument, if it exists.
let Some(expr) = call.arguments.find_argument("default", 1) else { let Some(expr) = call.arguments.find_argument_value("default", 1) else {
return; return;
}; };

View file

@ -47,7 +47,7 @@ pub(crate) fn invalid_envvar_value(checker: &mut Checker, call: &ast::ExprCall)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["os", "getenv"])) .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["os", "getenv"]))
{ {
// Find the `key` argument, if it exists. // Find the `key` argument, if it exists.
let Some(expr) = call.arguments.find_argument("key", 0) else { let Some(expr) = call.arguments.find_argument_value("key", 0) else {
return; return;
}; };

View file

@ -152,15 +152,18 @@ fn enumerate_items<'a>(
}; };
// If the `enumerate` call has a non-zero `start`, don't omit. // If the `enumerate` call has a non-zero `start`, don't omit.
if !arguments.find_argument("start", 1).map_or(true, |expr| { if !arguments
matches!( .find_argument_value("start", 1)
expr, .map_or(true, |expr| {
Expr::NumberLiteral(ast::ExprNumberLiteral { matches!(
value: Number::Int(Int::ZERO), expr,
.. Expr::NumberLiteral(ast::ExprNumberLiteral {
}) value: Number::Int(Int::ZERO),
) ..
}) { })
)
})
{
return None; return None;
} }

View file

@ -203,19 +203,19 @@ fn is_violation(call: &ast::ExprCall, qualified_name: &Callee) -> bool {
match qualified_name { match qualified_name {
Callee::Qualified(qualified_name) => match qualified_name.segments() { Callee::Qualified(qualified_name) => match qualified_name.segments() {
["" | "codecs" | "_io", "open"] => { ["" | "codecs" | "_io", "open"] => {
if let Some(mode_arg) = call.arguments.find_argument("mode", 1) { if let Some(mode_arg) = call.arguments.find_argument_value("mode", 1) {
if is_binary_mode(mode_arg).unwrap_or(true) { if is_binary_mode(mode_arg).unwrap_or(true) {
// binary mode or unknown mode is no violation // binary mode or unknown mode is no violation
return false; return false;
} }
} }
// else mode not specified, defaults to text mode // else mode not specified, defaults to text mode
call.arguments.find_argument("encoding", 3).is_none() call.arguments.find_argument_value("encoding", 3).is_none()
} }
["tempfile", tempfile_class @ ("TemporaryFile" | "NamedTemporaryFile" | "SpooledTemporaryFile")] => ["tempfile", tempfile_class @ ("TemporaryFile" | "NamedTemporaryFile" | "SpooledTemporaryFile")] =>
{ {
let mode_pos = usize::from(*tempfile_class == "SpooledTemporaryFile"); let mode_pos = usize::from(*tempfile_class == "SpooledTemporaryFile");
if let Some(mode_arg) = call.arguments.find_argument("mode", mode_pos) { if let Some(mode_arg) = call.arguments.find_argument_value("mode", mode_pos) {
if is_binary_mode(mode_arg).unwrap_or(true) { if is_binary_mode(mode_arg).unwrap_or(true) {
// binary mode or unknown mode is no violation // binary mode or unknown mode is no violation
return false; return false;
@ -225,27 +225,27 @@ fn is_violation(call: &ast::ExprCall, qualified_name: &Callee) -> bool {
return false; return false;
} }
call.arguments call.arguments
.find_argument("encoding", mode_pos + 2) .find_argument_value("encoding", mode_pos + 2)
.is_none() .is_none()
} }
["io" | "_io", "TextIOWrapper"] => { ["io" | "_io", "TextIOWrapper"] => {
call.arguments.find_argument("encoding", 1).is_none() call.arguments.find_argument_value("encoding", 1).is_none()
} }
_ => false, _ => false,
}, },
Callee::Pathlib(attr) => match *attr { Callee::Pathlib(attr) => match *attr {
"open" => { "open" => {
if let Some(mode_arg) = call.arguments.find_argument("mode", 0) { if let Some(mode_arg) = call.arguments.find_argument_value("mode", 0) {
if is_binary_mode(mode_arg).unwrap_or(true) { if is_binary_mode(mode_arg).unwrap_or(true) {
// binary mode or unknown mode is no violation // binary mode or unknown mode is no violation
return false; return false;
} }
} }
// else mode not specified, defaults to text mode // else mode not specified, defaults to text mode
call.arguments.find_argument("encoding", 2).is_none() call.arguments.find_argument_value("encoding", 2).is_none()
} }
"read_text" => call.arguments.find_argument("encoding", 0).is_none(), "read_text" => call.arguments.find_argument_value("encoding", 0).is_none(),
"write_text" => call.arguments.find_argument("encoding", 1).is_none(), "write_text" => call.arguments.find_argument_value("encoding", 1).is_none(),
_ => false, _ => false,
}, },
} }

View file

@ -71,7 +71,7 @@ pub(crate) fn redundant_open_modes(checker: &mut Checker, call: &ast::ExprCall)
return; return;
} }
let Some(mode_param) = call.arguments.find_argument("mode", 1) else { let Some(mode_param) = call.arguments.find_argument_value("mode", 1) else {
return; return;
}; };
let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = &mode_param else { let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = &mode_param else {

View file

@ -178,7 +178,7 @@ pub(crate) fn unnecessary_enumerate(checker: &mut Checker, stmt_for: &ast::StmtF
{ {
// If the `start` argument is set to something other than the `range` default, // If the `start` argument is set to something other than the `range` default,
// there's no clear fix. // there's no clear fix.
let start = arguments.find_argument("start", 1); let start = arguments.find_argument_value("start", 1);
if start.map_or(true, |start| { if start.map_or(true, |start| {
matches!( matches!(
start, start,

View file

@ -115,8 +115,8 @@ pub(crate) fn unnecessary_from_float(checker: &mut Checker, call: &ExprCall) {
}; };
let Some(value) = (match method_name { let Some(value) = (match method_name {
MethodName::FromFloat => call.arguments.find_argument("f", 0), MethodName::FromFloat => call.arguments.find_argument_value("f", 0),
MethodName::FromDecimal => call.arguments.find_argument("dec", 0), MethodName::FromDecimal => call.arguments.find_argument_value("dec", 0),
}) else { }) else {
return; return;
}; };

View file

@ -67,7 +67,7 @@ pub(crate) fn verbose_decimal_constructor(checker: &mut Checker, call: &ast::Exp
} }
// Decimal accepts arguments of the form: `Decimal(value='0', context=None)` // Decimal accepts arguments of the form: `Decimal(value='0', context=None)`
let Some(value) = call.arguments.find_argument("value", 0) else { let Some(value) = call.arguments.find_argument_value("value", 0) else {
return; return;
}; };

View file

@ -72,6 +72,7 @@ mod tests {
#[test_case(Rule::UnnecessaryNestedLiteral, Path::new("RUF041.py"))] #[test_case(Rule::UnnecessaryNestedLiteral, Path::new("RUF041.py"))]
#[test_case(Rule::UnnecessaryNestedLiteral, Path::new("RUF041.pyi"))] #[test_case(Rule::UnnecessaryNestedLiteral, Path::new("RUF041.pyi"))]
#[test_case(Rule::IfKeyInDictDel, Path::new("RUF051.py"))] #[test_case(Rule::IfKeyInDictDel, Path::new("RUF051.py"))]
#[test_case(Rule::FalsyDictGetFallback, Path::new("RUF056.py"))]
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"))] #[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> { fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());

View file

@ -0,0 +1,120 @@
use crate::checkers::ast::Checker;
use crate::fix::edits::{remove_argument, Parentheses};
use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{helpers::Truthiness, Expr, ExprAttribute, ExprName};
use ruff_python_semantic::analyze::typing;
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
/// ## What it does
/// Checks for `dict.get(key, falsy_value)` calls in boolean test positions.
///
/// ## Why is this bad?
/// The default fallback `None` is already falsy.
///
/// ## Example
///
/// ```python
/// if dict.get(key, False):
/// ...
/// ```
///
/// Use instead:
///
/// ```python
/// if dict.get(key):
/// ...
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as safe, unless the `dict.get()` call contains comments between arguments.
#[derive(ViolationMetadata)]
pub(crate) struct FalsyDictGetFallback;
impl AlwaysFixableViolation for FalsyDictGetFallback {
#[derive_message_formats]
fn message(&self) -> String {
"Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.".to_string()
}
fn fix_title(&self) -> String {
"Remove falsy fallback from `dict.get()`".to_string()
}
}
pub(crate) fn falsy_dict_get_fallback(checker: &mut Checker, expr: &Expr) {
let semantic = checker.semantic();
// Check if we are in a boolean test
if !semantic.in_boolean_test() {
return;
}
// Check if the expression is a call
let Expr::Call(call) = expr else {
return;
};
// Check if the function being called is an attribute (e.g. `dict.get`)
let Expr::Attribute(ExprAttribute { value, attr, .. }) = &*call.func else {
return;
};
// Ensure the method called is `get`
if attr != "get" {
return;
}
// Check if the object is a dictionary using the semantic model
if !value
.as_name_expr()
.is_some_and(|name| is_known_to_be_of_type_dict(semantic, name))
{
return;
}
// Get the fallback argument
let Some(fallback_arg) = call.arguments.find_argument("default", 1) else {
return;
};
// Check if the fallback is a falsy value
if Truthiness::from_expr(fallback_arg.value(), |id| semantic.has_builtin_binding(id))
.into_bool()
!= Some(false)
{
return;
}
let mut diagnostic = Diagnostic::new(FalsyDictGetFallback, fallback_arg.range());
let comment_ranges = checker.comment_ranges();
// Determine applicability based on the presence of comments
let applicability = if comment_ranges.intersects(call.arguments.range()) {
Applicability::Unsafe
} else {
Applicability::Safe
};
diagnostic.try_set_fix(|| {
remove_argument(
&fallback_arg,
&call.arguments,
Parentheses::Preserve,
checker.locator().contents(),
)
.map(|edit| Fix::applicable_edit(edit, applicability))
});
checker.diagnostics.push(diagnostic);
}
fn is_known_to_be_of_type_dict(semantic: &SemanticModel, expr: &ExprName) -> bool {
let Some(binding) = semantic.only_binding(expr).map(|id| semantic.binding(id)) else {
return false;
};
typing::is_dict(binding, semantic)
}

View file

@ -103,7 +103,7 @@ fn is_dunder_version_split_dot(expr: &ast::Expr) -> bool {
} }
let Some(ast::Expr::StringLiteral(ast::ExprStringLiteral { value, range: _ })) = let Some(ast::Expr::StringLiteral(ast::ExprStringLiteral { value, range: _ })) =
arguments.find_argument("sep", 0) arguments.find_argument_value("sep", 0)
else { else {
return false; return false;
}; };

View file

@ -6,6 +6,7 @@ pub(crate) use collection_literal_concatenation::*;
pub(crate) use decimal_from_float_literal::*; pub(crate) use decimal_from_float_literal::*;
pub(crate) use default_factory_kwarg::*; pub(crate) use default_factory_kwarg::*;
pub(crate) use explicit_f_string_type_conversion::*; pub(crate) use explicit_f_string_type_conversion::*;
pub(crate) use falsy_dict_get_fallback::*;
pub(crate) use function_call_in_dataclass_default::*; pub(crate) use function_call_in_dataclass_default::*;
pub(crate) use if_key_in_dict_del::*; pub(crate) use if_key_in_dict_del::*;
pub(crate) use implicit_optional::*; pub(crate) use implicit_optional::*;
@ -54,6 +55,7 @@ mod confusables;
mod decimal_from_float_literal; mod decimal_from_float_literal;
mod default_factory_kwarg; mod default_factory_kwarg;
mod explicit_f_string_type_conversion; mod explicit_f_string_type_conversion;
mod falsy_dict_get_fallback;
mod function_call_in_dataclass_default; mod function_call_in_dataclass_default;
mod helpers; mod helpers;
mod if_key_in_dict_del; mod if_key_in_dict_del;

View file

@ -128,7 +128,7 @@ fn convert_to_reduce(iterable: &Expr, call: &ast::ExprCall, checker: &Checker) -
/// Returns `true` if the `start` argument to a `sum()` call is an empty list. /// Returns `true` if the `start` argument to a `sum()` call is an empty list.
fn start_is_empty_list(arguments: &Arguments, semantic: &SemanticModel) -> bool { fn start_is_empty_list(arguments: &Arguments, semantic: &SemanticModel) -> bool {
let Some(start_arg) = arguments.find_argument("start", 1) else { let Some(start_arg) = arguments.find_argument_value("start", 1) else {
return false; return false;
}; };

View file

@ -175,8 +175,8 @@ fn round_applicability(checker: &Checker, arguments: &Arguments) -> Option<Appli
return None; return None;
} }
let number = arguments.find_argument("number", 0)?; let number = arguments.find_argument_value("number", 0)?;
let ndigits = arguments.find_argument("ndigits", 1); let ndigits = arguments.find_argument_value("ndigits", 1);
let number_kind = match number { let number_kind = match number {
Expr::Name(name) => { Expr::Name(name) => {

View file

@ -173,14 +173,14 @@ impl<'a> ReFunc<'a> {
// have already been filtered out from the `pattern` // have already been filtered out from the `pattern`
("split", 2) => Some(ReFunc { ("split", 2) => Some(ReFunc {
kind: ReFuncKind::Split, kind: ReFuncKind::Split,
pattern: call.arguments.find_argument("pattern", 0)?, pattern: call.arguments.find_argument_value("pattern", 0)?,
string: call.arguments.find_argument("string", 1)?, string: call.arguments.find_argument_value("string", 1)?,
}), }),
// `sub` is only safe to fix if `repl` is a string. `re.sub` also // `sub` is only safe to fix if `repl` is a string. `re.sub` also
// allows it to be a function, which will *not* work in the str // allows it to be a function, which will *not* work in the str
// version // version
("sub", 3) => { ("sub", 3) => {
let repl = call.arguments.find_argument("repl", 1)?; let repl = call.arguments.find_argument_value("repl", 1)?;
let lit = resolve_string_literal(repl, semantic)?; let lit = resolve_string_literal(repl, semantic)?;
let mut fixable = true; let mut fixable = true;
for (c, next) in lit.value.chars().tuple_windows() { for (c, next) in lit.value.chars().tuple_windows() {
@ -207,24 +207,24 @@ impl<'a> ReFunc<'a> {
kind: ReFuncKind::Sub { kind: ReFuncKind::Sub {
repl: fixable.then_some(repl), repl: fixable.then_some(repl),
}, },
pattern: call.arguments.find_argument("pattern", 0)?, pattern: call.arguments.find_argument_value("pattern", 0)?,
string: call.arguments.find_argument("string", 2)?, string: call.arguments.find_argument_value("string", 2)?,
}) })
} }
("match", 2) if in_if_context => Some(ReFunc { ("match", 2) if in_if_context => Some(ReFunc {
kind: ReFuncKind::Match, kind: ReFuncKind::Match,
pattern: call.arguments.find_argument("pattern", 0)?, pattern: call.arguments.find_argument_value("pattern", 0)?,
string: call.arguments.find_argument("string", 1)?, string: call.arguments.find_argument_value("string", 1)?,
}), }),
("search", 2) if in_if_context => Some(ReFunc { ("search", 2) if in_if_context => Some(ReFunc {
kind: ReFuncKind::Search, kind: ReFuncKind::Search,
pattern: call.arguments.find_argument("pattern", 0)?, pattern: call.arguments.find_argument_value("pattern", 0)?,
string: call.arguments.find_argument("string", 1)?, string: call.arguments.find_argument_value("string", 1)?,
}), }),
("fullmatch", 2) if in_if_context => Some(ReFunc { ("fullmatch", 2) if in_if_context => Some(ReFunc {
kind: ReFuncKind::Fullmatch, kind: ReFuncKind::Fullmatch,
pattern: call.arguments.find_argument("pattern", 0)?, pattern: call.arguments.find_argument_value("pattern", 0)?,
string: call.arguments.find_argument("string", 1)?, string: call.arguments.find_argument_value("string", 1)?,
}), }),
_ => None, _ => None,
} }

View file

@ -0,0 +1,485 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
snapshot_kind: text
---
RUF056.py:117:43: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
116 | # dict.get in ternary expression
117 | value = "not found" if my_dict.get("key", False) else "default" # [RUF056]
| ^^^^^ RUF056
118 |
119 | # dict.get in an if statement
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
114 114 | # Invalid falsy fallbacks are when the call to dict.get is used in a boolean context
115 115 |
116 116 | # dict.get in ternary expression
117 |-value = "not found" if my_dict.get("key", False) else "default" # [RUF056]
117 |+value = "not found" if my_dict.get("key") else "default" # [RUF056]
118 118 |
119 119 | # dict.get in an if statement
120 120 | if my_dict.get("key", False): # [RUF056]
RUF056.py:120:23: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
119 | # dict.get in an if statement
120 | if my_dict.get("key", False): # [RUF056]
| ^^^^^ RUF056
121 | pass
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
117 117 | value = "not found" if my_dict.get("key", False) else "default" # [RUF056]
118 118 |
119 119 | # dict.get in an if statement
120 |-if my_dict.get("key", False): # [RUF056]
120 |+if my_dict.get("key"): # [RUF056]
121 121 | pass
122 122 |
123 123 | # dict.get in compound if statement
RUF056.py:124:32: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
123 | # dict.get in compound if statement
124 | if True and my_dict.get("key", False): # [RUF056]
| ^^^^^ RUF056
125 | pass
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
121 121 | pass
122 122 |
123 123 | # dict.get in compound if statement
124 |-if True and my_dict.get("key", False): # [RUF056]
124 |+if True and my_dict.get("key"): # [RUF056]
125 125 | pass
126 126 |
127 127 | if my_dict.get("key", False) or True: # [RUF056]
RUF056.py:127:23: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
125 | pass
126 |
127 | if my_dict.get("key", False) or True: # [RUF056]
| ^^^^^ RUF056
128 | pass
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
124 124 | if True and my_dict.get("key", False): # [RUF056]
125 125 | pass
126 126 |
127 |-if my_dict.get("key", False) or True: # [RUF056]
127 |+if my_dict.get("key") or True: # [RUF056]
128 128 | pass
129 129 |
130 130 | # dict.get in an assert statement
RUF056.py:131:27: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
130 | # dict.get in an assert statement
131 | assert my_dict.get("key", False) # [RUF056]
| ^^^^^ RUF056
132 |
133 | # dict.get in a while statement
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
128 128 | pass
129 129 |
130 130 | # dict.get in an assert statement
131 |-assert my_dict.get("key", False) # [RUF056]
131 |+assert my_dict.get("key") # [RUF056]
132 132 |
133 133 | # dict.get in a while statement
134 134 | while my_dict.get("key", False): # [RUF056]
RUF056.py:134:26: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
133 | # dict.get in a while statement
134 | while my_dict.get("key", False): # [RUF056]
| ^^^^^ RUF056
135 | pass
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
131 131 | assert my_dict.get("key", False) # [RUF056]
132 132 |
133 133 | # dict.get in a while statement
134 |-while my_dict.get("key", False): # [RUF056]
134 |+while my_dict.get("key"): # [RUF056]
135 135 | pass
136 136 |
137 137 | # dict.get in unary not expression
RUF056.py:138:32: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
137 | # dict.get in unary not expression
138 | value = not my_dict.get("key", False) # [RUF056]
| ^^^^^ RUF056
139 |
140 | # testing all falsy fallbacks
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
135 135 | pass
136 136 |
137 137 | # dict.get in unary not expression
138 |-value = not my_dict.get("key", False) # [RUF056]
138 |+value = not my_dict.get("key") # [RUF056]
139 139 |
140 140 | # testing all falsy fallbacks
141 141 | value = not my_dict.get("key", False) # [RUF056]
RUF056.py:141:32: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
140 | # testing all falsy fallbacks
141 | value = not my_dict.get("key", False) # [RUF056]
| ^^^^^ RUF056
142 | value = not my_dict.get("key", []) # [RUF056]
143 | value = not my_dict.get("key", list()) # [RUF056]
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
138 138 | value = not my_dict.get("key", False) # [RUF056]
139 139 |
140 140 | # testing all falsy fallbacks
141 |-value = not my_dict.get("key", False) # [RUF056]
141 |+value = not my_dict.get("key") # [RUF056]
142 142 | value = not my_dict.get("key", []) # [RUF056]
143 143 | value = not my_dict.get("key", list()) # [RUF056]
144 144 | value = not my_dict.get("key", {}) # [RUF056]
RUF056.py:142:32: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
140 | # testing all falsy fallbacks
141 | value = not my_dict.get("key", False) # [RUF056]
142 | value = not my_dict.get("key", []) # [RUF056]
| ^^ RUF056
143 | value = not my_dict.get("key", list()) # [RUF056]
144 | value = not my_dict.get("key", {}) # [RUF056]
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
139 139 |
140 140 | # testing all falsy fallbacks
141 141 | value = not my_dict.get("key", False) # [RUF056]
142 |-value = not my_dict.get("key", []) # [RUF056]
142 |+value = not my_dict.get("key") # [RUF056]
143 143 | value = not my_dict.get("key", list()) # [RUF056]
144 144 | value = not my_dict.get("key", {}) # [RUF056]
145 145 | value = not my_dict.get("key", dict()) # [RUF056]
RUF056.py:143:32: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
141 | value = not my_dict.get("key", False) # [RUF056]
142 | value = not my_dict.get("key", []) # [RUF056]
143 | value = not my_dict.get("key", list()) # [RUF056]
| ^^^^^^ RUF056
144 | value = not my_dict.get("key", {}) # [RUF056]
145 | value = not my_dict.get("key", dict()) # [RUF056]
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
140 140 | # testing all falsy fallbacks
141 141 | value = not my_dict.get("key", False) # [RUF056]
142 142 | value = not my_dict.get("key", []) # [RUF056]
143 |-value = not my_dict.get("key", list()) # [RUF056]
143 |+value = not my_dict.get("key") # [RUF056]
144 144 | value = not my_dict.get("key", {}) # [RUF056]
145 145 | value = not my_dict.get("key", dict()) # [RUF056]
146 146 | value = not my_dict.get("key", set()) # [RUF056]
RUF056.py:144:32: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
142 | value = not my_dict.get("key", []) # [RUF056]
143 | value = not my_dict.get("key", list()) # [RUF056]
144 | value = not my_dict.get("key", {}) # [RUF056]
| ^^ RUF056
145 | value = not my_dict.get("key", dict()) # [RUF056]
146 | value = not my_dict.get("key", set()) # [RUF056]
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
141 141 | value = not my_dict.get("key", False) # [RUF056]
142 142 | value = not my_dict.get("key", []) # [RUF056]
143 143 | value = not my_dict.get("key", list()) # [RUF056]
144 |-value = not my_dict.get("key", {}) # [RUF056]
144 |+value = not my_dict.get("key") # [RUF056]
145 145 | value = not my_dict.get("key", dict()) # [RUF056]
146 146 | value = not my_dict.get("key", set()) # [RUF056]
147 147 | value = not my_dict.get("key", None) # [RUF056]
RUF056.py:145:32: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
143 | value = not my_dict.get("key", list()) # [RUF056]
144 | value = not my_dict.get("key", {}) # [RUF056]
145 | value = not my_dict.get("key", dict()) # [RUF056]
| ^^^^^^ RUF056
146 | value = not my_dict.get("key", set()) # [RUF056]
147 | value = not my_dict.get("key", None) # [RUF056]
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
142 142 | value = not my_dict.get("key", []) # [RUF056]
143 143 | value = not my_dict.get("key", list()) # [RUF056]
144 144 | value = not my_dict.get("key", {}) # [RUF056]
145 |-value = not my_dict.get("key", dict()) # [RUF056]
145 |+value = not my_dict.get("key") # [RUF056]
146 146 | value = not my_dict.get("key", set()) # [RUF056]
147 147 | value = not my_dict.get("key", None) # [RUF056]
148 148 | value = not my_dict.get("key", 0) # [RUF056]
RUF056.py:146:32: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
144 | value = not my_dict.get("key", {}) # [RUF056]
145 | value = not my_dict.get("key", dict()) # [RUF056]
146 | value = not my_dict.get("key", set()) # [RUF056]
| ^^^^^ RUF056
147 | value = not my_dict.get("key", None) # [RUF056]
148 | value = not my_dict.get("key", 0) # [RUF056]
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
143 143 | value = not my_dict.get("key", list()) # [RUF056]
144 144 | value = not my_dict.get("key", {}) # [RUF056]
145 145 | value = not my_dict.get("key", dict()) # [RUF056]
146 |-value = not my_dict.get("key", set()) # [RUF056]
146 |+value = not my_dict.get("key") # [RUF056]
147 147 | value = not my_dict.get("key", None) # [RUF056]
148 148 | value = not my_dict.get("key", 0) # [RUF056]
149 149 | value = not my_dict.get("key", 0.0) # [RUF056]
RUF056.py:147:32: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
145 | value = not my_dict.get("key", dict()) # [RUF056]
146 | value = not my_dict.get("key", set()) # [RUF056]
147 | value = not my_dict.get("key", None) # [RUF056]
| ^^^^ RUF056
148 | value = not my_dict.get("key", 0) # [RUF056]
149 | value = not my_dict.get("key", 0.0) # [RUF056]
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
144 144 | value = not my_dict.get("key", {}) # [RUF056]
145 145 | value = not my_dict.get("key", dict()) # [RUF056]
146 146 | value = not my_dict.get("key", set()) # [RUF056]
147 |-value = not my_dict.get("key", None) # [RUF056]
147 |+value = not my_dict.get("key") # [RUF056]
148 148 | value = not my_dict.get("key", 0) # [RUF056]
149 149 | value = not my_dict.get("key", 0.0) # [RUF056]
150 150 | value = not my_dict.get("key", "") # [RUF056]
RUF056.py:148:32: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
146 | value = not my_dict.get("key", set()) # [RUF056]
147 | value = not my_dict.get("key", None) # [RUF056]
148 | value = not my_dict.get("key", 0) # [RUF056]
| ^ RUF056
149 | value = not my_dict.get("key", 0.0) # [RUF056]
150 | value = not my_dict.get("key", "") # [RUF056]
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
145 145 | value = not my_dict.get("key", dict()) # [RUF056]
146 146 | value = not my_dict.get("key", set()) # [RUF056]
147 147 | value = not my_dict.get("key", None) # [RUF056]
148 |-value = not my_dict.get("key", 0) # [RUF056]
148 |+value = not my_dict.get("key") # [RUF056]
149 149 | value = not my_dict.get("key", 0.0) # [RUF056]
150 150 | value = not my_dict.get("key", "") # [RUF056]
151 151 |
RUF056.py:149:32: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
147 | value = not my_dict.get("key", None) # [RUF056]
148 | value = not my_dict.get("key", 0) # [RUF056]
149 | value = not my_dict.get("key", 0.0) # [RUF056]
| ^^^ RUF056
150 | value = not my_dict.get("key", "") # [RUF056]
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
146 146 | value = not my_dict.get("key", set()) # [RUF056]
147 147 | value = not my_dict.get("key", None) # [RUF056]
148 148 | value = not my_dict.get("key", 0) # [RUF056]
149 |-value = not my_dict.get("key", 0.0) # [RUF056]
149 |+value = not my_dict.get("key") # [RUF056]
150 150 | value = not my_dict.get("key", "") # [RUF056]
151 151 |
152 152 | # testing dict.get call using kwargs
RUF056.py:150:32: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
148 | value = not my_dict.get("key", 0) # [RUF056]
149 | value = not my_dict.get("key", 0.0) # [RUF056]
150 | value = not my_dict.get("key", "") # [RUF056]
| ^^ RUF056
151 |
152 | # testing dict.get call using kwargs
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
147 147 | value = not my_dict.get("key", None) # [RUF056]
148 148 | value = not my_dict.get("key", 0) # [RUF056]
149 149 | value = not my_dict.get("key", 0.0) # [RUF056]
150 |-value = not my_dict.get("key", "") # [RUF056]
150 |+value = not my_dict.get("key") # [RUF056]
151 151 |
152 152 | # testing dict.get call using kwargs
153 153 | value = not my_dict.get(key="key", default=False) # [RUF056]
RUF056.py:153:36: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
152 | # testing dict.get call using kwargs
153 | value = not my_dict.get(key="key", default=False) # [RUF056]
| ^^^^^^^^^^^^^ RUF056
154 | value = not my_dict.get(default=[], key="key") # [RUF056]
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
150 150 | value = not my_dict.get("key", "") # [RUF056]
151 151 |
152 152 | # testing dict.get call using kwargs
153 |-value = not my_dict.get(key="key", default=False) # [RUF056]
153 |+value = not my_dict.get(key="key") # [RUF056]
154 154 | value = not my_dict.get(default=[], key="key") # [RUF056]
155 155 |
156 156 | # testing invalid dict.get call with inline comment
RUF056.py:154:25: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
152 | # testing dict.get call using kwargs
153 | value = not my_dict.get(key="key", default=False) # [RUF056]
154 | value = not my_dict.get(default=[], key="key") # [RUF056]
| ^^^^^^^^^^ RUF056
155 |
156 | # testing invalid dict.get call with inline comment
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
151 151 |
152 152 | # testing dict.get call using kwargs
153 153 | value = not my_dict.get(key="key", default=False) # [RUF056]
154 |-value = not my_dict.get(default=[], key="key") # [RUF056]
154 |+value = not my_dict.get(key="key") # [RUF056]
155 155 |
156 156 | # testing invalid dict.get call with inline comment
157 157 | value = not my_dict.get("key", # comment1
RUF056.py:158:22: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
156 | # testing invalid dict.get call with inline comment
157 | value = not my_dict.get("key", # comment1
158 | [] # comment2
| ^^ RUF056
159 | ) # [RUF056]
|
= help: Remove falsy fallback from `dict.get()`
Unsafe fix
154 154 | value = not my_dict.get(default=[], key="key") # [RUF056]
155 155 |
156 156 | # testing invalid dict.get call with inline comment
157 |-value = not my_dict.get("key", # comment1
158 |- [] # comment2
157 |+value = not my_dict.get("key" # comment2
159 158 | ) # [RUF056]
160 159 |
161 160 | # testing invalid dict.get call with kwargs and inline comment
RUF056.py:163:25: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
161 | # testing invalid dict.get call with kwargs and inline comment
162 | value = not my_dict.get(key="key", # comment1
163 | default=False # comment2
| ^^^^^^^^^^^^^ RUF056
164 | ) # [RUF056]
165 | value = not my_dict.get(default=[], # comment1
|
= help: Remove falsy fallback from `dict.get()`
Unsafe fix
159 159 | ) # [RUF056]
160 160 |
161 161 | # testing invalid dict.get call with kwargs and inline comment
162 |-value = not my_dict.get(key="key", # comment1
163 |- default=False # comment2
162 |+value = not my_dict.get(key="key" # comment2
164 163 | ) # [RUF056]
165 164 | value = not my_dict.get(default=[], # comment1
166 165 | key="key" # comment2
RUF056.py:165:25: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
163 | default=False # comment2
164 | ) # [RUF056]
165 | value = not my_dict.get(default=[], # comment1
| ^^^^^^^^^^ RUF056
166 | key="key" # comment2
167 | ) # [RUF056]
|
= help: Remove falsy fallback from `dict.get()`
Unsafe fix
162 162 | value = not my_dict.get(key="key", # comment1
163 163 | default=False # comment2
164 164 | ) # [RUF056]
165 |-value = not my_dict.get(default=[], # comment1
165 |+value = not my_dict.get(# comment1
166 166 | key="key" # comment2
167 167 | ) # [RUF056]
168 168 |
RUF056.py:170:55: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
169 | # testing invalid dict.get calls
170 | value = not my_dict.get(key="key", other="something", default=False)
| ^^^^^^^^^^^^^ RUF056
171 | value = not my_dict.get(default=False, other="something", key="test")
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
167 167 | ) # [RUF056]
168 168 |
169 169 | # testing invalid dict.get calls
170 |-value = not my_dict.get(key="key", other="something", default=False)
170 |+value = not my_dict.get(key="key", other="something")
171 171 | value = not my_dict.get(default=False, other="something", key="test")
RUF056.py:171:25: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
|
169 | # testing invalid dict.get calls
170 | value = not my_dict.get(key="key", other="something", default=False)
171 | value = not my_dict.get(default=False, other="something", key="test")
| ^^^^^^^^^^^^^ RUF056
|
= help: Remove falsy fallback from `dict.get()`
Safe fix
168 168 |
169 169 | # testing invalid dict.get calls
170 170 | value = not my_dict.get(key="key", other="something", default=False)
171 |-value = not my_dict.get(default=False, other="something", key="test")
171 |+value = not my_dict.get(other="something", key="test")

View file

@ -3780,6 +3780,15 @@ pub enum ArgOrKeyword<'a> {
Keyword(&'a Keyword), Keyword(&'a Keyword),
} }
impl<'a> ArgOrKeyword<'a> {
pub const fn value(&self) -> &'a Expr {
match self {
ArgOrKeyword::Arg(argument) => argument,
ArgOrKeyword::Keyword(keyword) => &keyword.value,
}
}
}
impl<'a> From<&'a Expr> for ArgOrKeyword<'a> { impl<'a> From<&'a Expr> for ArgOrKeyword<'a> {
fn from(arg: &'a Expr) -> Self { fn from(arg: &'a Expr) -> Self {
Self::Arg(arg) Self::Arg(arg)
@ -3828,15 +3837,24 @@ impl Arguments {
.nth(position) .nth(position)
} }
/// Return the argument with the given name or at the given position, or `None` if no such /// Return the value for the argument with the given name or at the given position, or `None` if no such
/// argument exists. Used to retrieve arguments that can be provided _either_ as keyword or /// argument exists. Used to retrieve argument values that can be provided _either_ as keyword or
/// positional arguments. /// positional arguments.
pub fn find_argument(&self, name: &str, position: usize) -> Option<&Expr> { pub fn find_argument_value(&self, name: &str, position: usize) -> Option<&Expr> {
self.find_keyword(name) self.find_keyword(name)
.map(|keyword| &keyword.value) .map(|keyword| &keyword.value)
.or_else(|| self.find_positional(position)) .or_else(|| self.find_positional(position))
} }
/// Return the the argument with the given name or at the given position, or `None` if no such
/// argument exists. Used to retrieve arguments that can be provided _either_ as keyword or
/// positional arguments.
pub fn find_argument(&self, name: &str, position: usize) -> Option<ArgOrKeyword> {
self.find_keyword(name)
.map(ArgOrKeyword::from)
.or_else(|| self.find_positional(position).map(ArgOrKeyword::from))
}
/// Return the positional and keyword arguments in the order of declaration. /// Return the positional and keyword arguments in the order of declaration.
/// ///
/// Positional arguments are generally before keyword arguments, but star arguments are an /// Positional arguments are generally before keyword arguments, but star arguments are an

1
ruff.schema.json generated
View file

@ -3865,6 +3865,7 @@
"RUF051", "RUF051",
"RUF052", "RUF052",
"RUF055", "RUF055",
"RUF056",
"RUF1", "RUF1",
"RUF10", "RUF10",
"RUF100", "RUF100",