Move find_keyword helpers onto Arguments struct (#6280)

## Summary

Similar to #6279, moving some helpers onto the struct in the name of
reducing the number of random undiscoverable utilities we have in
`helpers.rs`.

Most of the churn is migrating rules to take `ast::ExprCall` instead of
the spread call arguments.

## Test Plan

`cargo test`
This commit is contained in:
Charlie Marsh 2023-08-02 13:54:48 -04:00 committed by GitHub
parent 041946fb64
commit fd40864924
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 289 additions and 468 deletions

View file

@ -429,7 +429,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pyupgrade::rules::unnecessary_encode_utf8(checker, call);
}
if checker.enabled(Rule::RedundantOpenModes) {
pyupgrade::rules::redundant_open_modes(checker, expr);
pyupgrade::rules::redundant_open_modes(checker, call);
}
if checker.enabled(Rule::NativeLiterals) {
pyupgrade::rules::native_literals(checker, expr, func, args, keywords);
@ -438,7 +438,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pyupgrade::rules::open_alias(checker, expr, func);
}
if checker.enabled(Rule::ReplaceUniversalNewlines) {
pyupgrade::rules::replace_universal_newlines(checker, func, keywords);
pyupgrade::rules::replace_universal_newlines(checker, call);
}
if checker.enabled(Rule::ReplaceStdoutStderr) {
pyupgrade::rules::replace_stdout_stderr(checker, call);
@ -461,7 +461,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_async::rules::blocking_os_call(checker, expr);
}
if checker.any_enabled(&[Rule::Print, Rule::PPrint]) {
flake8_print::rules::print_call(checker, func, keywords);
flake8_print::rules::print_call(checker, call);
}
if checker.any_enabled(&[
Rule::SuspiciousPickleUsage,
@ -513,13 +513,11 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
if checker.enabled(Rule::ZipWithoutExplicitStrict) {
if checker.settings.target_version >= PythonVersion::Py310 {
flake8_bugbear::rules::zip_without_explicit_strict(
checker, expr, func, args, keywords,
);
flake8_bugbear::rules::zip_without_explicit_strict(checker, call);
}
}
if checker.enabled(Rule::NoExplicitStacklevel) {
flake8_bugbear::rules::no_explicit_stacklevel(checker, func, keywords);
flake8_bugbear::rules::no_explicit_stacklevel(checker, call);
}
if checker.enabled(Rule::UnnecessaryDictKwargs) {
flake8_pie::rules::unnecessary_dict_kwargs(checker, expr, keywords);
@ -531,19 +529,19 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_bandit::rules::bad_file_permissions(checker, call);
}
if checker.enabled(Rule::RequestWithNoCertValidation) {
flake8_bandit::rules::request_with_no_cert_validation(checker, func, keywords);
flake8_bandit::rules::request_with_no_cert_validation(checker, call);
}
if checker.enabled(Rule::UnsafeYAMLLoad) {
flake8_bandit::rules::unsafe_yaml_load(checker, call);
}
if checker.enabled(Rule::SnmpInsecureVersion) {
flake8_bandit::rules::snmp_insecure_version(checker, func, keywords);
flake8_bandit::rules::snmp_insecure_version(checker, call);
}
if checker.enabled(Rule::SnmpWeakCryptography) {
flake8_bandit::rules::snmp_weak_cryptography(checker, call);
}
if checker.enabled(Rule::Jinja2AutoescapeFalse) {
flake8_bandit::rules::jinja2_autoescape_false(checker, func, keywords);
flake8_bandit::rules::jinja2_autoescape_false(checker, call);
}
if checker.enabled(Rule::HardcodedPasswordFuncArg) {
flake8_bandit::rules::hardcoded_password_func_arg(checker, keywords);
@ -555,13 +553,13 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_bandit::rules::hashlib_insecure_hash_functions(checker, call);
}
if checker.enabled(Rule::RequestWithoutTimeout) {
flake8_bandit::rules::request_without_timeout(checker, func, keywords);
flake8_bandit::rules::request_without_timeout(checker, call);
}
if checker.enabled(Rule::ParamikoCall) {
flake8_bandit::rules::paramiko_call(checker, func);
}
if checker.enabled(Rule::LoggingConfigInsecureListen) {
flake8_bandit::rules::logging_config_insecure_listen(checker, func, keywords);
flake8_bandit::rules::logging_config_insecure_listen(checker, call);
}
if checker.any_enabled(&[
Rule::SubprocessWithoutShellEqualsTrue,
@ -572,7 +570,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Rule::StartProcessWithPartialPath,
Rule::UnixCommandWildcardInjection,
]) {
flake8_bandit::rules::shell_injection(checker, func, args, keywords);
flake8_bandit::rules::shell_injection(checker, call);
}
if checker.enabled(Rule::UnnecessaryGeneratorList) {
flake8_comprehensions::rules::unnecessary_generator_list(
@ -679,19 +677,13 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
pandas_vet::rules::call(checker, func);
if checker.enabled(Rule::PandasUseOfDotReadTable) {
pandas_vet::rules::use_of_read_table(checker, func, keywords);
pandas_vet::rules::use_of_read_table(checker, call);
}
if checker.enabled(Rule::PandasUseOfPdMerge) {
pandas_vet::rules::use_of_pd_merge(checker, func);
}
if checker.enabled(Rule::CallDatetimeWithoutTzinfo) {
flake8_datetimez::rules::call_datetime_without_tzinfo(
checker,
func,
args,
keywords,
expr.range(),
);
flake8_datetimez::rules::call_datetime_without_tzinfo(checker, call);
}
if checker.enabled(Rule::CallDatetimeToday) {
flake8_datetimez::rules::call_datetime_today(checker, func, expr.range());
@ -707,30 +699,13 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
);
}
if checker.enabled(Rule::CallDatetimeNowWithoutTzinfo) {
flake8_datetimez::rules::call_datetime_now_without_tzinfo(
checker,
func,
args,
keywords,
expr.range(),
);
flake8_datetimez::rules::call_datetime_now_without_tzinfo(checker, call);
}
if checker.enabled(Rule::CallDatetimeFromtimestamp) {
flake8_datetimez::rules::call_datetime_fromtimestamp(
checker,
func,
args,
keywords,
expr.range(),
);
flake8_datetimez::rules::call_datetime_fromtimestamp(checker, call);
}
if checker.enabled(Rule::CallDatetimeStrptimeWithoutZone) {
flake8_datetimez::rules::call_datetime_strptime_without_zone(
checker,
func,
args,
expr.range(),
);
flake8_datetimez::rules::call_datetime_strptime_without_zone(checker, call);
}
if checker.enabled(Rule::CallDateToday) {
flake8_datetimez::rules::call_date_today(checker, func, expr.range());
@ -754,10 +729,10 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pylint::rules::bad_str_strip_call(checker, func, args);
}
if checker.enabled(Rule::InvalidEnvvarDefault) {
pylint::rules::invalid_envvar_default(checker, func, args, keywords);
pylint::rules::invalid_envvar_default(checker, call);
}
if checker.enabled(Rule::InvalidEnvvarValue) {
pylint::rules::invalid_envvar_value(checker, func, args, keywords);
pylint::rules::invalid_envvar_value(checker, call);
}
if checker.enabled(Rule::NestedMinMax) {
pylint::rules::nested_min_max(checker, expr, func, args, keywords);
@ -775,13 +750,13 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::SubprocessPopenPreexecFn) {
pylint::rules::subprocess_popen_preexec_fn(checker, func, keywords);
pylint::rules::subprocess_popen_preexec_fn(checker, call);
}
if checker.any_enabled(&[
Rule::PytestRaisesWithoutException,
Rule::PytestRaisesTooBroad,
]) {
flake8_pytest_style::rules::raises_call(checker, func, args, keywords);
flake8_pytest_style::rules::raises_call(checker, call);
}
if checker.enabled(Rule::PytestFailWithoutMessage) {
flake8_pytest_style::rules::fail_call(checker, call);
@ -855,7 +830,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_use_pathlib::rules::path_constructor_current_directory(checker, expr, func);
}
if checker.enabled(Rule::OsSepSplit) {
flake8_use_pathlib::rules::os_sep_split(checker, func, args, keywords);
flake8_use_pathlib::rules::os_sep_split(checker, call);
}
if checker.enabled(Rule::NumpyLegacyRandom) {
numpy::rules::legacy_random(checker, func);
@ -876,7 +851,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pylint::rules::logging_call(checker, call);
}
if checker.enabled(Rule::DjangoLocalsInRenderFunction) {
flake8_django::rules::locals_in_render_function(checker, func, args, keywords);
flake8_django::rules::locals_in_render_function(checker, call);
}
if checker.is_stub && checker.enabled(Rule::UnsupportedMethodCallOnAll) {
flake8_pyi::rules::unsupported_method_call_on_all(checker, func);

View file

@ -1,10 +1,8 @@
use ruff_python_ast as ast;
use ruff_python_ast::{Arguments, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use ruff_python_ast as ast;
use ruff_python_ast::Constant;
use ruff_python_ast::{Expr, Ranged};
use crate::checkers::ast::Checker;
@ -61,9 +59,7 @@ pub(crate) fn variable_name_task_id(
// If the value is not a call, we can't do anything.
let Expr::Call(ast::ExprCall {
func,
arguments: Arguments { keywords, .. },
..
func, arguments, ..
}) = value
else {
return None;
@ -79,7 +75,7 @@ pub(crate) fn variable_name_task_id(
}
// If the call doesn't have a `task_id` keyword argument, we can't do anything.
let keyword = find_keyword(keywords, "task_id")?;
let keyword = arguments.find_keyword("task_id")?;
// If the keyword argument is not a string, we can't do anything.
let task_id = match &keyword.value {

View file

@ -1,8 +1,6 @@
use ruff_python_ast::helpers::find_keyword;
use ruff_python_ast::{self as ast, Constant, Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Constant, Expr, Ranged};
use crate::checkers::ast::Checker;
@ -59,13 +57,13 @@ impl Violation for Jinja2AutoescapeFalse {
}
/// S701
pub(crate) fn jinja2_autoescape_false(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) {
pub(crate) fn jinja2_autoescape_false(checker: &mut Checker, call: &ast::ExprCall) {
if checker
.semantic()
.resolve_call_path(func)
.resolve_call_path(&call.func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["jinja2", "Environment"]))
{
if let Some(keyword) = find_keyword(keywords, "autoescape") {
if let Some(keyword) = call.arguments.find_keyword("autoescape") {
match &keyword.value {
Expr::Constant(ast::ExprConstant {
value: Constant::Bool(true),
@ -89,7 +87,7 @@ pub(crate) fn jinja2_autoescape_false(checker: &mut Checker, func: &Expr, keywor
} else {
checker.diagnostics.push(Diagnostic::new(
Jinja2AutoescapeFalse { value: false },
func.range(),
call.func.range(),
));
}
}

View file

@ -1,8 +1,6 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use ruff_python_ast::{self as ast, Ranged};
use crate::checkers::ast::Checker;
@ -35,22 +33,19 @@ impl Violation for LoggingConfigInsecureListen {
}
/// S612
pub(crate) fn logging_config_insecure_listen(
checker: &mut Checker,
func: &Expr,
keywords: &[Keyword],
) {
pub(crate) fn logging_config_insecure_listen(checker: &mut Checker, call: &ast::ExprCall) {
if checker
.semantic()
.resolve_call_path(func)
.resolve_call_path(&call.func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["logging", "config", "listen"]))
{
if find_keyword(keywords, "verify").is_some() {
if call.arguments.find_keyword("verify").is_some() {
return;
}
checker
.diagnostics
.push(Diagnostic::new(LoggingConfigInsecureListen, func.range()));
checker.diagnostics.push(Diagnostic::new(
LoggingConfigInsecureListen,
call.func.range(),
));
}
}

View file

@ -1,8 +1,7 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{find_keyword, is_const_false};
use ruff_python_ast::helpers::is_const_false;
use ruff_python_ast::{self as ast, Ranged};
use crate::checkers::ast::Checker;
@ -46,14 +45,10 @@ impl Violation for RequestWithNoCertValidation {
}
/// S501
pub(crate) fn request_with_no_cert_validation(
checker: &mut Checker,
func: &Expr,
keywords: &[Keyword],
) {
pub(crate) fn request_with_no_cert_validation(checker: &mut Checker, call: &ast::ExprCall) {
if let Some(target) = checker
.semantic()
.resolve_call_path(func)
.resolve_call_path(&call.func)
.and_then(|call_path| match call_path.as_slice() {
["requests", "get" | "options" | "head" | "post" | "put" | "patch" | "delete"] => {
Some("requests")
@ -63,7 +58,7 @@ pub(crate) fn request_with_no_cert_validation(
_ => None,
})
{
if let Some(keyword) = find_keyword(keywords, "verify") {
if let Some(keyword) = call.arguments.find_keyword("verify") {
if is_const_false(&keyword.value) {
checker.diagnostics.push(Diagnostic::new(
RequestWithNoCertValidation {

View file

@ -1,8 +1,7 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{find_keyword, is_const_none};
use ruff_python_ast::helpers::is_const_none;
use ruff_python_ast::{self as ast, Ranged};
use crate::checkers::ast::Checker;
@ -49,10 +48,10 @@ impl Violation for RequestWithoutTimeout {
}
/// S113
pub(crate) fn request_without_timeout(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) {
pub(crate) fn request_without_timeout(checker: &mut Checker, call: &ast::ExprCall) {
if checker
.semantic()
.resolve_call_path(func)
.resolve_call_path(&call.func)
.is_some_and(|call_path| {
matches!(
call_path.as_slice(),
@ -63,7 +62,7 @@ pub(crate) fn request_without_timeout(checker: &mut Checker, func: &Expr, keywor
)
})
{
if let Some(keyword) = find_keyword(keywords, "timeout") {
if let Some(keyword) = call.arguments.find_keyword("timeout") {
if is_const_none(&keyword.value) {
checker.diagnostics.push(Diagnostic::new(
RequestWithoutTimeout { implicit: false },
@ -73,7 +72,7 @@ pub(crate) fn request_without_timeout(checker: &mut Checker, func: &Expr, keywor
} else {
checker.diagnostics.push(Diagnostic::new(
RequestWithoutTimeout { implicit: true },
func.range(),
call.func.range(),
));
}
}

View file

@ -1,10 +1,9 @@
//! Checks relating to shell injection.
use ruff_python_ast::{self as ast, Constant, Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{find_keyword, Truthiness};
use ruff_python_ast::helpers::Truthiness;
use ruff_python_ast::{self as ast, Arguments, Constant, Expr, Keyword, Ranged};
use ruff_python_semantic::SemanticModel;
use crate::{
@ -144,17 +143,12 @@ impl Violation for UnixCommandWildcardInjection {
}
/// S602, S603, S604, S605, S606, S607, S609
pub(crate) fn shell_injection(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
let call_kind = get_call_kind(func, checker.semantic());
let shell_keyword = find_shell_keyword(keywords, checker.semantic());
pub(crate) fn shell_injection(checker: &mut Checker, call: &ast::ExprCall) {
let call_kind = get_call_kind(&call.func, checker.semantic());
let shell_keyword = find_shell_keyword(&call.arguments, checker.semantic());
if matches!(call_kind, Some(CallKind::Subprocess)) {
if let Some(arg) = args.first() {
if let Some(arg) = call.arguments.args.first() {
match shell_keyword {
// S602
Some(ShellKeyword {
@ -209,7 +203,7 @@ pub(crate) fn shell_injection(
// S605
if checker.enabled(Rule::StartProcessWithAShell) {
if matches!(call_kind, Some(CallKind::Shell)) {
if let Some(arg) = args.first() {
if let Some(arg) = call.arguments.args.first() {
checker.diagnostics.push(Diagnostic::new(
StartProcessWithAShell {
seems_safe: shell_call_seems_safe(arg),
@ -225,14 +219,14 @@ pub(crate) fn shell_injection(
if matches!(call_kind, Some(CallKind::NoShell)) {
checker
.diagnostics
.push(Diagnostic::new(StartProcessWithNoShell, func.range()));
.push(Diagnostic::new(StartProcessWithNoShell, call.func.range()));
}
}
// S607
if checker.enabled(Rule::StartProcessWithPartialPath) {
if call_kind.is_some() {
if let Some(arg) = args.first() {
if let Some(arg) = call.arguments.args.first() {
if is_partial_path(arg) {
checker
.diagnostics
@ -256,11 +250,12 @@ pub(crate) fn shell_injection(
)
)
{
if let Some(arg) = args.first() {
if let Some(arg) = call.arguments.args.first() {
if is_wildcard_command(arg) {
checker
.diagnostics
.push(Diagnostic::new(UnixCommandWildcardInjection, func.range()));
checker.diagnostics.push(Diagnostic::new(
UnixCommandWildcardInjection,
call.func.range(),
));
}
}
}
@ -317,10 +312,10 @@ struct ShellKeyword<'a> {
/// Return the `shell` keyword argument to the given function call, if any.
fn find_shell_keyword<'a>(
keywords: &'a [Keyword],
arguments: &'a Arguments,
semantic: &SemanticModel,
) -> Option<ShellKeyword<'a>> {
find_keyword(keywords, "shell").map(|keyword| ShellKeyword {
arguments.find_keyword("shell").map(|keyword| ShellKeyword {
truthiness: Truthiness::from_expr(&keyword.value, |id| semantic.is_builtin(id)),
keyword,
})

View file

@ -1,9 +1,8 @@
use num_traits::{One, Zero};
use ruff_python_ast::{self as ast, Constant, Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use ruff_python_ast::{self as ast, Constant, Expr, Ranged};
use crate::checkers::ast::Checker;
@ -43,15 +42,15 @@ impl Violation for SnmpInsecureVersion {
}
/// S508
pub(crate) fn snmp_insecure_version(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) {
pub(crate) fn snmp_insecure_version(checker: &mut Checker, call: &ast::ExprCall) {
if checker
.semantic()
.resolve_call_path(func)
.resolve_call_path(&call.func)
.is_some_and(|call_path| {
matches!(call_path.as_slice(), ["pysnmp", "hlapi", "CommunityData"])
})
{
if let Some(keyword) = find_keyword(keywords, "mpModel") {
if let Some(keyword) = call.arguments.find_keyword("mpModel") {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Int(value),
..

View file

@ -1,8 +1,7 @@
use ruff_python_ast::{self as ast, Arguments, Expr, Ranged, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{find_keyword, is_const_true};
use ruff_python_ast::helpers::is_const_true;
use ruff_python_ast::{self as ast, Expr, Ranged, Stmt};
use ruff_python_semantic::analyze::logging;
use crate::checkers::ast::Checker;
@ -95,9 +94,7 @@ pub(crate) fn blind_except(
if body.iter().any(|stmt| {
if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt {
if let Expr::Call(ast::ExprCall {
func,
arguments: Arguments { keywords, .. },
..
func, arguments, ..
}) = value.as_ref()
{
if logging::is_logger_candidate(
@ -111,7 +108,7 @@ pub(crate) fn blind_except(
return true;
}
if attr == "error" {
if let Some(keyword) = find_keyword(keywords, "exc_info") {
if let Some(keyword) = arguments.find_keyword("exc_info") {
if is_const_true(&keyword.value) {
return true;
}

View file

@ -1,10 +1,8 @@
use std::fmt;
use ruff_python_ast::{self as ast, Arguments, Expr, Ranged, WithItem};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use ruff_python_ast::{self as ast, Expr, Ranged, WithItem};
use crate::checkers::ast::Checker;
@ -81,27 +79,24 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem])
for item in items {
let Expr::Call(ast::ExprCall {
func,
arguments:
Arguments {
args,
keywords,
range: _,
},
arguments,
range: _,
}) = &item.context_expr
else {
return;
};
if args.len() != 1 {
return;
}
if item.optional_vars.is_some() {
return;
}
let [arg] = arguments.args.as_slice() else {
return;
};
let Some(exception) = checker
.semantic()
.resolve_call_path(args.first().unwrap())
.resolve_call_path(arg)
.and_then(|call_path| match call_path.as_slice() {
["", "Exception"] => Some(ExceptionKind::Exception),
["", "BaseException"] => Some(ExceptionKind::BaseException),
@ -118,7 +113,7 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem])
.semantic()
.resolve_call_path(func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["pytest", "raises"]))
&& find_keyword(keywords, "match").is_none()
&& arguments.find_keyword("match").is_none()
{
AssertionKind::PytestRaises
} else {

View file

@ -1,8 +1,6 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use ruff_python_ast::{self as ast, Ranged};
use crate::checkers::ast::Checker;
@ -38,20 +36,20 @@ impl Violation for NoExplicitStacklevel {
}
/// B028
pub(crate) fn no_explicit_stacklevel(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) {
pub(crate) fn no_explicit_stacklevel(checker: &mut Checker, call: &ast::ExprCall) {
if !checker
.semantic()
.resolve_call_path(func)
.resolve_call_path(&call.func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["warnings", "warn"]))
{
return;
}
if find_keyword(keywords, "stacklevel").is_some() {
if call.arguments.find_keyword("stacklevel").is_some() {
return;
}
checker
.diagnostics
.push(Diagnostic::new(NoExplicitStacklevel, func.range()));
.push(Diagnostic::new(NoExplicitStacklevel, call.func.range()));
}

View file

@ -1,8 +1,7 @@
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{find_keyword, is_const_none};
use ruff_python_ast::helpers::is_const_none;
use ruff_python_ast::{self as ast, Arguments, Expr, Ranged};
use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
@ -41,24 +40,20 @@ impl Violation for ZipWithoutExplicitStrict {
}
/// B905
pub(crate) fn zip_without_explicit_strict(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
kwargs: &[Keyword],
) {
if let Expr::Name(ast::ExprName { id, .. }) = func {
pub(crate) fn zip_without_explicit_strict(checker: &mut Checker, call: &ast::ExprCall) {
if let Expr::Name(ast::ExprName { id, .. }) = call.func.as_ref() {
if id == "zip"
&& checker.semantic().is_builtin("zip")
&& find_keyword(kwargs, "strict").is_none()
&& !args
&& call.arguments.find_keyword("strict").is_none()
&& !call
.arguments
.args
.iter()
.any(|arg| is_infinite_iterator(arg, checker.semantic()))
{
checker
.diagnostics
.push(Diagnostic::new(ZipWithoutExplicitStrict, expr.range()));
.push(Diagnostic::new(ZipWithoutExplicitStrict, call.range()));
}
}
}

View file

@ -1,11 +1,10 @@
use ruff_python_ast::{Expr, Keyword};
use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{has_non_none_keyword, is_const_none};
use ruff_python_ast::helpers::is_const_none;
use ruff_python_ast::{self as ast, Ranged};
use crate::checkers::ast::Checker;
use crate::rules::flake8_datetimez::rules::helpers::has_non_none_keyword;
use super::helpers;
@ -22,16 +21,10 @@ impl Violation for CallDatetimeFromtimestamp {
}
/// DTZ006
pub(crate) fn call_datetime_fromtimestamp(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
location: TextRange,
) {
pub(crate) fn call_datetime_fromtimestamp(checker: &mut Checker, call: &ast::ExprCall) {
if !checker
.semantic()
.resolve_call_path(func)
.resolve_call_path(&call.func)
.is_some_and(|call_path| {
matches!(
call_path.as_slice(),
@ -47,25 +40,25 @@ pub(crate) fn call_datetime_fromtimestamp(
}
// no args / no args unqualified
if args.len() < 2 && keywords.is_empty() {
if call.arguments.args.len() < 2 && call.arguments.keywords.is_empty() {
checker
.diagnostics
.push(Diagnostic::new(CallDatetimeFromtimestamp, location));
.push(Diagnostic::new(CallDatetimeFromtimestamp, call.range()));
return;
}
// none args
if args.len() > 1 && is_const_none(&args[1]) {
if call.arguments.args.len() > 1 && is_const_none(&call.arguments.args[1]) {
checker
.diagnostics
.push(Diagnostic::new(CallDatetimeFromtimestamp, location));
.push(Diagnostic::new(CallDatetimeFromtimestamp, call.range()));
return;
}
// wrong keywords / none keyword
if !keywords.is_empty() && !has_non_none_keyword(keywords, "tz") {
if !call.arguments.keywords.is_empty() && !has_non_none_keyword(&call.arguments, "tz") {
checker
.diagnostics
.push(Diagnostic::new(CallDatetimeFromtimestamp, location));
.push(Diagnostic::new(CallDatetimeFromtimestamp, call.range()));
}
}

View file

@ -1,11 +1,10 @@
use ruff_python_ast::{Expr, Keyword};
use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{has_non_none_keyword, is_const_none};
use ruff_python_ast::helpers::is_const_none;
use ruff_python_ast::{self as ast, Ranged};
use crate::checkers::ast::Checker;
use crate::rules::flake8_datetimez::rules::helpers::has_non_none_keyword;
use super::helpers;
@ -20,16 +19,10 @@ impl Violation for CallDatetimeNowWithoutTzinfo {
}
/// DTZ005
pub(crate) fn call_datetime_now_without_tzinfo(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
location: TextRange,
) {
pub(crate) fn call_datetime_now_without_tzinfo(checker: &mut Checker, call: &ast::ExprCall) {
if !checker
.semantic()
.resolve_call_path(func)
.resolve_call_path(&call.func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["datetime", "datetime", "now"]))
{
return;
@ -40,25 +33,25 @@ pub(crate) fn call_datetime_now_without_tzinfo(
}
// no args / no args unqualified
if args.is_empty() && keywords.is_empty() {
if call.arguments.args.is_empty() && call.arguments.keywords.is_empty() {
checker
.diagnostics
.push(Diagnostic::new(CallDatetimeNowWithoutTzinfo, location));
.push(Diagnostic::new(CallDatetimeNowWithoutTzinfo, call.range()));
return;
}
// none args
if !args.is_empty() && is_const_none(&args[0]) {
if call.arguments.args.first().is_some_and(is_const_none) {
checker
.diagnostics
.push(Diagnostic::new(CallDatetimeNowWithoutTzinfo, location));
.push(Diagnostic::new(CallDatetimeNowWithoutTzinfo, call.range()));
return;
}
// wrong keywords / none keyword
if !keywords.is_empty() && !has_non_none_keyword(keywords, "tz") {
if !call.arguments.keywords.is_empty() && !has_non_none_keyword(&call.arguments, "tz") {
checker
.diagnostics
.push(Diagnostic::new(CallDatetimeNowWithoutTzinfo, location));
.push(Diagnostic::new(CallDatetimeNowWithoutTzinfo, call.range()));
}
}

View file

@ -1,11 +1,9 @@
use ruff_python_ast::{self as ast, Arguments, Constant, Expr};
use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::has_non_none_keyword;
use ruff_python_ast::{self as ast, Constant, Expr, Ranged};
use crate::checkers::ast::Checker;
use crate::rules::flake8_datetimez::rules::helpers::has_non_none_keyword;
#[violation]
pub struct CallDatetimeStrptimeWithoutZone;
@ -21,15 +19,10 @@ impl Violation for CallDatetimeStrptimeWithoutZone {
}
/// DTZ007
pub(crate) fn call_datetime_strptime_without_zone(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
location: TextRange,
) {
pub(crate) fn call_datetime_strptime_without_zone(checker: &mut Checker, call: &ast::ExprCall) {
if !checker
.semantic()
.resolve_call_path(func)
.resolve_call_path(&call.func)
.is_some_and(|call_path| {
matches!(call_path.as_slice(), ["datetime", "datetime", "strptime"])
})
@ -42,7 +35,7 @@ pub(crate) fn call_datetime_strptime_without_zone(
value: Constant::Str(format),
kind: None,
range: _,
})) = args.get(1).as_ref()
})) = call.arguments.args.get(1).as_ref()
{
if format.contains("%z") {
return;
@ -53,17 +46,14 @@ pub(crate) fn call_datetime_strptime_without_zone(
checker.semantic().expr_grandparent(),
checker.semantic().expr_parent(),
) else {
checker
.diagnostics
.push(Diagnostic::new(CallDatetimeStrptimeWithoutZone, location));
checker.diagnostics.push(Diagnostic::new(
CallDatetimeStrptimeWithoutZone,
call.range(),
));
return;
};
if let Expr::Call(ast::ExprCall {
arguments: Arguments { keywords, .. },
..
}) = grandparent
{
if let Expr::Call(ast::ExprCall { arguments, .. }) = grandparent {
if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = parent {
let attr = attr.as_str();
// Ex) `datetime.strptime(...).astimezone()`
@ -73,14 +63,15 @@ pub(crate) fn call_datetime_strptime_without_zone(
// Ex) `datetime.strptime(...).replace(tzinfo=UTC)`
if attr == "replace" {
if has_non_none_keyword(keywords, "tzinfo") {
if has_non_none_keyword(arguments, "tzinfo") {
return;
}
}
}
}
checker
.diagnostics
.push(Diagnostic::new(CallDatetimeStrptimeWithoutZone, location));
checker.diagnostics.push(Diagnostic::new(
CallDatetimeStrptimeWithoutZone,
call.range(),
));
}

View file

@ -1,11 +1,10 @@
use ruff_python_ast::{Expr, Keyword};
use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{has_non_none_keyword, is_const_none};
use ruff_python_ast::helpers::is_const_none;
use ruff_python_ast::{self as ast, Ranged};
use crate::checkers::ast::Checker;
use crate::rules::flake8_datetimez::rules::helpers::has_non_none_keyword;
use super::helpers;
@ -44,16 +43,10 @@ impl Violation for CallDatetimeWithoutTzinfo {
}
}
pub(crate) fn call_datetime_without_tzinfo(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
location: TextRange,
) {
pub(crate) fn call_datetime_without_tzinfo(checker: &mut Checker, call: &ast::ExprCall) {
if !checker
.semantic()
.resolve_call_path(func)
.resolve_call_path(&call.func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["datetime", "datetime"]))
{
return;
@ -64,17 +57,17 @@ pub(crate) fn call_datetime_without_tzinfo(
}
// No positional arg: keyword is missing or constant None.
if args.len() < 8 && !has_non_none_keyword(keywords, "tzinfo") {
if call.arguments.args.len() < 8 && !has_non_none_keyword(&call.arguments, "tzinfo") {
checker
.diagnostics
.push(Diagnostic::new(CallDatetimeWithoutTzinfo, location));
.push(Diagnostic::new(CallDatetimeWithoutTzinfo, call.range()));
return;
}
// Positional arg: is constant None.
if args.len() >= 8 && is_const_none(&args[7]) {
if call.arguments.args.get(7).is_some_and(is_const_none) {
checker
.diagnostics
.push(Diagnostic::new(CallDatetimeWithoutTzinfo, location));
.push(Diagnostic::new(CallDatetimeWithoutTzinfo, call.range()));
}
}

View file

@ -1,11 +1,19 @@
use ruff_python_ast::{Expr, ExprAttribute};
use ruff_python_ast::helpers::is_const_none;
use ruff_python_ast::{Arguments, Expr, ExprAttribute};
use crate::checkers::ast::Checker;
/// Check if the parent expression is a call to `astimezone`. This assumes that
/// the current expression is a `datetime.datetime` object.
pub(crate) fn parent_expr_is_astimezone(checker: &Checker) -> bool {
pub(super) fn parent_expr_is_astimezone(checker: &Checker) -> bool {
checker.semantic().expr_parent().is_some_and( |parent| {
matches!(parent, Expr::Attribute(ExprAttribute { attr, .. }) if attr.as_str() == "astimezone")
})
}
/// Return `true` if a keyword argument is present with a non-`None` value.
pub(super) fn has_non_none_keyword(arguments: &Arguments, keyword: &str) -> bool {
arguments
.find_keyword(keyword)
.is_some_and(|keyword| !is_const_none(&keyword.value))
}

View file

@ -1,8 +1,6 @@
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use ruff_python_ast::{self as ast, Expr, Ranged};
use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
@ -45,38 +43,23 @@ impl Violation for DjangoLocalsInRenderFunction {
}
/// DJ003
pub(crate) fn locals_in_render_function(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
pub(crate) fn locals_in_render_function(checker: &mut Checker, call: &ast::ExprCall) {
if !checker
.semantic()
.resolve_call_path(func)
.resolve_call_path(&call.func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["django", "shortcuts", "render"]))
{
return;
}
let locals = if args.len() >= 3 {
if !is_locals_call(&args[2], checker.semantic()) {
return;
if let Some(argument) = call.arguments.find_argument("context", 2) {
if is_locals_call(argument, checker.semantic()) {
checker.diagnostics.push(Diagnostic::new(
DjangoLocalsInRenderFunction,
argument.range(),
));
}
&args[2]
} else if let Some(keyword) = find_keyword(keywords, "context") {
if !is_locals_call(&keyword.value, checker.semantic()) {
return;
}
&keyword.value
} else {
return;
};
checker.diagnostics.push(Diagnostic::new(
DjangoLocalsInRenderFunction,
locals.range(),
));
}
}
fn is_locals_call(expr: &Expr, semantic: &SemanticModel) -> bool {

View file

@ -1,8 +1,7 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{find_keyword, is_const_none};
use ruff_python_ast::helpers::is_const_none;
use ruff_python_ast::{self as ast, Ranged};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@ -76,16 +75,16 @@ impl Violation for PPrint {
}
/// T201, T203
pub(crate) fn print_call(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) {
pub(crate) fn print_call(checker: &mut Checker, call: &ast::ExprCall) {
let diagnostic = {
let call_path = checker.semantic().resolve_call_path(func);
let call_path = checker.semantic().resolve_call_path(&call.func);
if call_path
.as_ref()
.is_some_and(|call_path| matches!(call_path.as_slice(), ["", "print"]))
{
// If the print call has a `file=` argument (that isn't `None`, `"sys.stdout"`,
// or `"sys.stderr"`), don't trigger T201.
if let Some(keyword) = find_keyword(keywords, "file") {
if let Some(keyword) = call.arguments.find_keyword("file") {
if !is_const_none(&keyword.value) {
if checker.semantic().resolve_call_path(&keyword.value).map_or(
true,
@ -98,12 +97,12 @@ pub(crate) fn print_call(checker: &mut Checker, func: &Expr, keywords: &[Keyword
}
}
}
Diagnostic::new(Print, func.range())
Diagnostic::new(Print, call.func.range())
} else if call_path
.as_ref()
.is_some_and(|call_path| matches!(call_path.as_slice(), ["pprint", "pprint"]))
{
Diagnostic::new(PPrint, func.range())
Diagnostic::new(PPrint, call.func.range())
} else {
return;
}

View file

@ -4,7 +4,7 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Violation};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::collect_call_path;
use ruff_python_ast::helpers::{find_keyword, includes_arg_name};
use ruff_python_ast::helpers::includes_arg_name;
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
@ -506,7 +506,7 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &D
}
if checker.enabled(Rule::PytestExtraneousScopeFunction) {
if let Some(keyword) = find_keyword(&arguments.keywords, "scope") {
if let Some(keyword) = arguments.find_keyword("scope") {
if keyword_is_literal(keyword, "function") {
let mut diagnostic =
Diagnostic::new(PytestExtraneousScopeFunction, keyword.range());

View file

@ -1,10 +1,9 @@
use ruff_python_ast::helpers::{find_keyword, is_compound_statement};
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged, Stmt, WithItem};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::format_call_path;
use ruff_python_ast::call_path::from_qualified_name;
use ruff_python_ast::helpers::is_compound_statement;
use ruff_python_ast::{self as ast, Expr, Ranged, Stmt, WithItem};
use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
@ -91,19 +90,20 @@ const fn is_non_trivial_with_body(body: &[Stmt]) -> bool {
}
}
pub(crate) fn raises_call(checker: &mut Checker, func: &Expr, args: &[Expr], keywords: &[Keyword]) {
if is_pytest_raises(func, checker.semantic()) {
pub(crate) fn raises_call(checker: &mut Checker, call: &ast::ExprCall) {
if is_pytest_raises(&call.func, checker.semantic()) {
if checker.enabled(Rule::PytestRaisesWithoutException) {
if args.is_empty() && keywords.is_empty() {
checker
.diagnostics
.push(Diagnostic::new(PytestRaisesWithoutException, func.range()));
if call.arguments.is_empty() {
checker.diagnostics.push(Diagnostic::new(
PytestRaisesWithoutException,
call.func.range(),
));
}
}
if checker.enabled(Rule::PytestRaisesTooBroad) {
let match_keyword = find_keyword(keywords, "match");
if let Some(exception) = args.first() {
let match_keyword = call.arguments.find_keyword("match");
if let Some(exception) = call.arguments.args.first() {
if let Some(match_keyword) = match_keyword {
if is_empty_or_null_string(&match_keyword.value) {
exception_needs_match(checker, exception);

View file

@ -1,7 +1,6 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use ruff_python_ast::{Expr, ExprAttribute, Keyword, Ranged};
use ruff_python_ast::{self as ast, Expr, ExprAttribute, Ranged};
use crate::checkers::ast::Checker;
@ -52,13 +51,8 @@ impl Violation for OsSepSplit {
}
/// PTH206
pub(crate) fn os_sep_split(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
let Expr::Attribute(ExprAttribute { attr, .. }) = func else {
pub(crate) fn os_sep_split(checker: &mut Checker, call: &ast::ExprCall) {
let Expr::Attribute(ExprAttribute { attr, .. }) = call.func.as_ref() else {
return;
};
@ -66,19 +60,8 @@ pub(crate) fn os_sep_split(
return;
};
let sep = if !args.is_empty() {
// `.split(os.sep)`
let [arg] = args else {
return;
};
arg
} else if !keywords.is_empty() {
// `.split(sep=os.sep)`
let Some(keyword) = find_keyword(keywords, "sep") else {
return;
};
&keyword.value
} else {
// Match `.split(os.sep)` or `.split(sep=os.sep)`
let Some(sep) = call.arguments.find_argument("sep", 0) else {
return;
};

View file

@ -1,9 +1,7 @@
use ruff_python_ast as ast;
use ruff_python_ast::{Constant, Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use ruff_python_ast as ast;
use ruff_python_ast::{Constant, Expr, Ranged};
use crate::checkers::ast::Checker;
@ -46,21 +44,24 @@ impl Violation for PandasUseOfDotReadTable {
}
/// PD012
pub(crate) fn use_of_read_table(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) {
pub(crate) fn use_of_read_table(checker: &mut Checker, call: &ast::ExprCall) {
if checker
.semantic()
.resolve_call_path(func)
.resolve_call_path(&call.func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["pandas", "read_table"]))
{
if let Some(Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
..
})) = find_keyword(keywords, "sep").map(|keyword| &keyword.value)
})) = call
.arguments
.find_keyword("sep")
.map(|keyword| &keyword.value)
{
if value.as_str() == "," {
checker
.diagnostics
.push(Diagnostic::new(PandasUseOfDotReadTable, func.range()));
.push(Diagnostic::new(PandasUseOfDotReadTable, call.func.range()));
}
}
}

View file

@ -1,20 +1,16 @@
use std::fmt;
use ruff_python_ast as ast;
use ruff_python_ast::helpers::find_keyword;
use ruff_python_ast::{CmpOp, Constant, Expr, Keyword};
use ruff_python_ast::{Arguments, CmpOp, Constant, Expr};
use ruff_python_semantic::analyze::function_type;
use ruff_python_semantic::{ScopeKind, SemanticModel};
use crate::settings::Settings;
/// Returns the value of the `name` parameter to, e.g., a `TypeVar` constructor.
pub(super) fn type_param_name<'a>(args: &'a [Expr], keywords: &'a [Keyword]) -> Option<&'a str> {
pub(super) fn type_param_name(arguments: &Arguments) -> Option<&str> {
// Handle both `TypeVar("T")` and `TypeVar(name="T")`.
let name_param = find_keyword(keywords, "name")
.map(|keyword| &keyword.value)
.or_else(|| args.get(0))?;
let name_param = arguments.find_argument("name", 0)?;
if let Expr::Constant(ast::ExprConstant {
value: Constant::Str(name),
..

View file

@ -1,8 +1,6 @@
use ruff_python_ast::{self as ast, Constant, Expr, Keyword, Operator, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use ruff_python_ast::{self as ast, Constant, Expr, Operator, Ranged};
use crate::checkers::ast::Checker;
@ -78,22 +76,14 @@ fn is_valid_default(expr: &Expr) -> bool {
}
/// PLW1508
pub(crate) fn invalid_envvar_default(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
pub(crate) fn invalid_envvar_default(checker: &mut Checker, call: &ast::ExprCall) {
if checker
.semantic()
.resolve_call_path(func)
.resolve_call_path(&call.func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["os", "getenv"]))
{
// Find the `default` argument, if it exists.
let Some(expr) = args
.get(1)
.or_else(|| find_keyword(keywords, "default").map(|keyword| &keyword.value))
else {
let Some(expr) = call.arguments.find_argument("default", 1) else {
return;
};

View file

@ -1,8 +1,6 @@
use ruff_python_ast::{self as ast, Constant, Expr, Keyword, Operator, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use ruff_python_ast::{self as ast, Constant, Expr, Operator, Ranged};
use crate::checkers::ast::Checker;
@ -75,22 +73,14 @@ fn is_valid_key(expr: &Expr) -> bool {
}
/// PLE1507
pub(crate) fn invalid_envvar_value(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
pub(crate) fn invalid_envvar_value(checker: &mut Checker, call: &ast::ExprCall) {
if checker
.semantic()
.resolve_call_path(func)
.resolve_call_path(&call.func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["os", "getenv"]))
{
// Find the `key` argument, if it exists.
let Some(expr) = args
.get(0)
.or_else(|| find_keyword(keywords, "key").map(|keyword| &keyword.value))
else {
let Some(expr) = call.arguments.find_argument("key", 0) else {
return;
};

View file

@ -1,7 +1,7 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{find_keyword, is_const_none};
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_python_ast::helpers::is_const_none;
use ruff_python_ast::{self as ast, Ranged};
use crate::checkers::ast::Checker;
@ -48,14 +48,16 @@ impl Violation for SubprocessPopenPreexecFn {
}
/// PLW1509
pub(crate) fn subprocess_popen_preexec_fn(checker: &mut Checker, func: &Expr, kwargs: &[Keyword]) {
pub(crate) fn subprocess_popen_preexec_fn(checker: &mut Checker, call: &ast::ExprCall) {
if checker
.semantic()
.resolve_call_path(func)
.resolve_call_path(&call.func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["subprocess", "Popen"]))
{
if let Some(keyword) =
find_keyword(kwargs, "preexec_fn").filter(|keyword| !is_const_none(&keyword.value))
if let Some(keyword) = call
.arguments
.find_keyword("preexec_fn")
.filter(|keyword| !is_const_none(&keyword.value))
{
checker
.diagnostics

View file

@ -1,10 +1,9 @@
use std::fmt;
use ruff_python_ast::{self as ast, Arguments, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{find_keyword, is_const_true};
use ruff_python_ast::helpers::is_const_true;
use ruff_python_ast::{self as ast, Expr, Ranged};
use crate::checkers::ast::Checker;
use crate::rules::pylint::helpers::type_param_name;
@ -75,19 +74,22 @@ impl Violation for TypeBivariance {
/// PLC0131
pub(crate) fn type_bivariance(checker: &mut Checker, value: &Expr) {
let Expr::Call(ast::ExprCall {
func,
arguments: Arguments { args, keywords, .. },
..
func, arguments, ..
}) = value
else {
return;
};
let Some(covariant) = find_keyword(keywords, "covariant").map(|keyword| &keyword.value) else {
let Some(covariant) = arguments
.find_keyword("covariant")
.map(|keyword| &keyword.value)
else {
return;
};
let Some(contravariant) = find_keyword(keywords, "contravariant").map(|keyword| &keyword.value)
let Some(contravariant) = arguments
.find_keyword("contravariant")
.map(|keyword| &keyword.value)
else {
return;
};
@ -118,7 +120,7 @@ pub(crate) fn type_bivariance(checker: &mut Checker, value: &Expr) {
checker.diagnostics.push(Diagnostic::new(
TypeBivariance {
kind,
param_name: type_param_name(args, keywords).map(ToString::to_string),
param_name: type_param_name(arguments).map(ToString::to_string),
},
func.range(),
));

View file

@ -1,10 +1,9 @@
use std::fmt;
use ruff_python_ast::{self as ast, Arguments, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{find_keyword, is_const_true};
use ruff_python_ast::helpers::is_const_true;
use ruff_python_ast::{self as ast, Expr, Ranged};
use crate::checkers::ast::Checker;
use crate::rules::pylint::helpers::type_param_name;
@ -66,21 +65,23 @@ impl Violation for TypeNameIncorrectVariance {
/// PLC0105
pub(crate) fn type_name_incorrect_variance(checker: &mut Checker, value: &Expr) {
let Expr::Call(ast::ExprCall {
func,
arguments: Arguments { args, keywords, .. },
..
func, arguments, ..
}) = value
else {
return;
};
let Some(param_name) = type_param_name(args, keywords) else {
let Some(param_name) = type_param_name(arguments) else {
return;
};
let covariant = find_keyword(keywords, "covariant").map(|keyword| &keyword.value);
let covariant = arguments
.find_keyword("covariant")
.map(|keyword| &keyword.value);
let contravariant = find_keyword(keywords, "contravariant").map(|keyword| &keyword.value);
let contravariant = arguments
.find_keyword("contravariant")
.map(|keyword| &keyword.value);
if !mismatch(param_name, covariant, contravariant) {
return;

View file

@ -1,9 +1,8 @@
use std::fmt;
use ruff_python_ast::{self as ast, Arguments, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Ranged};
use crate::checkers::ast::Checker;
use crate::rules::pylint::helpers::type_param_name;
@ -68,15 +67,13 @@ pub(crate) fn type_param_name_mismatch(checker: &mut Checker, value: &Expr, targ
};
let Expr::Call(ast::ExprCall {
func,
arguments: Arguments { args, keywords, .. },
..
func, arguments, ..
}) = value
else {
return;
};
let Some(param_name) = type_param_name(args, keywords) else {
let Some(param_name) = type_param_name(arguments) else {
return;
};

View file

@ -1,16 +1,16 @@
use anyhow::{bail, Result};
use log::debug;
use ruff_python_ast::{
self as ast, Arguments, Constant, Expr, ExprContext, Identifier, Keyword, Ranged, Stmt,
};
use ruff_text_size::TextRange;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{find_keyword, is_dunder};
use ruff_python_ast::helpers::is_dunder;
use ruff_python_ast::{
self as ast, Arguments, Constant, Expr, ExprContext, Identifier, Keyword, Ranged, Stmt,
};
use ruff_python_codegen::Generator;
use ruff_python_semantic::SemanticModel;
use ruff_python_stdlib::identifiers::is_identifier;
use ruff_text_size::TextRange;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@ -70,14 +70,14 @@ fn match_typed_dict_assign<'a>(
targets: &'a [Expr],
value: &'a Expr,
semantic: &SemanticModel,
) -> Option<(&'a str, &'a [Expr], &'a [Keyword], &'a Expr)> {
) -> Option<(&'a str, &'a Arguments, &'a Expr)> {
let target = targets.get(0)?;
let Expr::Name(ast::ExprName { id: class_name, .. }) = target else {
return None;
};
let Expr::Call(ast::ExprCall {
func,
arguments: Arguments { args, keywords, .. },
arguments,
range: _,
}) = value
else {
@ -86,7 +86,7 @@ fn match_typed_dict_assign<'a>(
if !semantic.match_typing_expr(func, "TypedDict") {
return None;
}
Some((class_name, args, keywords, func))
Some((class_name, arguments, func))
}
/// Generate a [`Stmt::AnnAssign`] representing the provided property
@ -201,17 +201,14 @@ fn properties_from_keywords(keywords: &[Keyword]) -> Result<Vec<Stmt>> {
.collect()
}
fn match_properties_and_total<'a>(
args: &'a [Expr],
keywords: &'a [Keyword],
) -> Result<(Vec<Stmt>, Option<&'a Keyword>)> {
fn match_properties_and_total(arguments: &Arguments) -> Result<(Vec<Stmt>, Option<&Keyword>)> {
// We don't have to manage the hybrid case because it's not possible to have a
// dict and keywords. For example, the following is illegal:
// ```
// MyType = TypedDict('MyType', {'a': int, 'b': str}, a=int, b=str)
// ```
if let Some(dict) = args.get(1) {
let total = find_keyword(keywords, "total");
if let Some(dict) = arguments.args.get(1) {
let total = arguments.find_keyword("total");
match dict {
Expr::Dict(ast::ExprDict {
keys,
@ -225,8 +222,8 @@ fn match_properties_and_total<'a>(
}) => Ok((properties_from_dict_call(func, keywords)?, total)),
_ => bail!("Expected `arg` to be `Expr::Dict` or `Expr::Call`"),
}
} else if !keywords.is_empty() {
Ok((properties_from_keywords(keywords)?, None))
} else if !arguments.keywords.is_empty() {
Ok((properties_from_keywords(&arguments.keywords)?, None))
} else {
let node = Stmt::Pass(ast::StmtPass {
range: TextRange::default(),
@ -262,13 +259,13 @@ pub(crate) fn convert_typed_dict_functional_to_class(
targets: &[Expr],
value: &Expr,
) {
let Some((class_name, args, keywords, base_class)) =
let Some((class_name, arguments, base_class)) =
match_typed_dict_assign(targets, value, checker.semantic())
else {
return;
};
let (body, total_keyword) = match match_properties_and_total(args, keywords) {
let (body, total_keyword) = match match_properties_and_total(arguments) {
Ok((body, total_keyword)) => (body, total_keyword),
Err(err) => {
debug!("Skipping ineligible `TypedDict` \"{class_name}\": {err}");

View file

@ -1,15 +1,14 @@
use std::str::FromStr;
use anyhow::{anyhow, Result};
use ruff_python_ast::{self as ast, Arguments, Constant, Expr, Keyword, Ranged};
use ruff_python_parser::{lexer, Mode};
use ruff_text_size::TextSize;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use ruff_python_ast::{self as ast, Constant, Expr, Ranged};
use ruff_python_parser::{lexer, Mode};
use ruff_python_semantic::SemanticModel;
use ruff_source_file::Locator;
use ruff_text_size::TextSize;
use crate::checkers::ast::Checker;
use crate::registry::Rule;
@ -64,14 +63,15 @@ impl AlwaysAutofixableViolation for RedundantOpenModes {
}
/// UP015
pub(crate) fn redundant_open_modes(checker: &mut Checker, expr: &Expr) {
let Some((mode_param, keywords)) = match_open(expr, checker.semantic()) else {
pub(crate) fn redundant_open_modes(checker: &mut Checker, call: &ast::ExprCall) {
if !is_open_builtin(call.func.as_ref(), checker.semantic()) {
return;
};
match mode_param {
}
match call.arguments.find_argument("mode", 1) {
None => {
if !keywords.is_empty() {
if let Some(keyword) = find_keyword(keywords, MODE_KEYWORD_ARGUMENT) {
if !call.arguments.is_empty() {
if let Some(keyword) = call.arguments.find_keyword(MODE_KEYWORD_ARGUMENT) {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Str(mode_param_value),
..
@ -79,7 +79,7 @@ pub(crate) fn redundant_open_modes(checker: &mut Checker, expr: &Expr) {
{
if let Ok(mode) = OpenMode::from_str(mode_param_value.as_str()) {
checker.diagnostics.push(create_check(
expr,
call,
&keyword.value,
mode.replacement_value(),
checker.locator(),
@ -98,7 +98,7 @@ pub(crate) fn redundant_open_modes(checker: &mut Checker, expr: &Expr) {
{
if let Ok(mode) = OpenMode::from_str(value.as_str()) {
checker.diagnostics.push(create_check(
expr,
call,
mode_param,
mode.replacement_value(),
checker.locator(),
@ -113,29 +113,12 @@ pub(crate) fn redundant_open_modes(checker: &mut Checker, expr: &Expr) {
const OPEN_FUNC_NAME: &str = "open";
const MODE_KEYWORD_ARGUMENT: &str = "mode";
fn match_open<'a>(
expr: &'a Expr,
model: &SemanticModel,
) -> Option<(Option<&'a Expr>, &'a [Keyword])> {
let ast::ExprCall {
func,
arguments:
Arguments {
args,
keywords,
range: _,
},
range: _,
} = expr.as_call_expr()?;
let ast::ExprName { id, .. } = func.as_name_expr()?;
if id.as_str() == OPEN_FUNC_NAME && model.is_builtin(id) {
// Return the "open mode" parameter and keywords.
Some((args.get(1), keywords))
} else {
None
}
/// Returns `true` if the given `call` is a call to the `open` builtin.
fn is_open_builtin(func: &Expr, model: &SemanticModel) -> bool {
let Some(ast::ExprName { id, .. }) = func.as_name_expr() else {
return false;
};
id.as_str() == OPEN_FUNC_NAME && model.is_builtin(id)
}
#[derive(Debug, Copy, Clone)]
@ -180,8 +163,8 @@ impl OpenMode {
}
}
fn create_check(
expr: &Expr,
fn create_check<T: Ranged>(
expr: &T,
mode_param: &Expr,
replacement_value: Option<&str>,
locator: &Locator,
@ -208,7 +191,11 @@ fn create_check(
diagnostic
}
fn create_remove_param_fix(locator: &Locator, expr: &Expr, mode_param: &Expr) -> Result<Edit> {
fn create_remove_param_fix<T: Ranged>(
locator: &Locator,
expr: &T,
mode_param: &Expr,
) -> Result<Edit> {
let content = locator.slice(expr.range());
// Find the last comma before mode_param and create a deletion fix
// starting from the comma and ending after mode_param.

View file

@ -2,7 +2,6 @@ use anyhow::Result;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use ruff_python_ast::{self as ast, Keyword, Ranged};
use ruff_source_file::Locator;
@ -82,10 +81,10 @@ pub(crate) fn replace_stdout_stderr(checker: &mut Checker, call: &ast::ExprCall)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["subprocess", "run"]))
{
// Find `stdout` and `stderr` kwargs.
let Some(stdout) = find_keyword(&call.arguments.keywords, "stdout") else {
let Some(stdout) = call.arguments.find_keyword("stdout") else {
return;
};
let Some(stderr) = find_keyword(&call.arguments.keywords, "stderr") else {
let Some(stderr) = call.arguments.find_keyword("stderr") else {
return;
};

View file

@ -1,9 +1,7 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_text_size::{TextLen, TextRange};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use ruff_python_ast::{self as ast, Ranged};
use ruff_text_size::{TextLen, TextRange};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@ -50,13 +48,13 @@ impl AlwaysAutofixableViolation for ReplaceUniversalNewlines {
}
/// UP021
pub(crate) fn replace_universal_newlines(checker: &mut Checker, func: &Expr, kwargs: &[Keyword]) {
pub(crate) fn replace_universal_newlines(checker: &mut Checker, call: &ast::ExprCall) {
if checker
.semantic()
.resolve_call_path(func)
.resolve_call_path(&call.func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["subprocess", "run"]))
{
let Some(kwarg) = find_keyword(kwargs, "universal_newlines") else {
let Some(kwarg) = call.arguments.find_keyword("universal_newlines") else {
return;
};
let range = TextRange::at(kwarg.start(), "universal_newlines".text_len());

View file

@ -9,8 +9,8 @@ use ruff_text_size::TextRange;
use crate::call_path::CallPath;
use crate::statement_visitor::{walk_body, walk_stmt, StatementVisitor};
use crate::{
self as ast, Arguments, Constant, ExceptHandler, Expr, Keyword, MatchCase, Parameters, Pattern,
Ranged, Stmt, TypeParam,
self as ast, Arguments, Constant, ExceptHandler, Expr, MatchCase, Parameters, Pattern, Ranged,
Stmt, TypeParam,
};
/// Return `true` if the `Stmt` is a compound statement (as opposed to a simple statement).
@ -655,17 +655,6 @@ pub fn is_constant_non_singleton(expr: &Expr) -> bool {
is_constant(expr) && !is_singleton(expr)
}
/// Return the [`Keyword`] with the given name, if it's present in the list of
/// [`Keyword`] arguments.
///
/// TODO(charlie): Make this an associated function on [`Arguments`].
pub fn find_keyword<'a>(keywords: &'a [Keyword], keyword_name: &str) -> Option<&'a Keyword> {
keywords.iter().find(|keyword| {
let Keyword { arg, .. } = keyword;
arg.as_ref().is_some_and(|arg| arg == keyword_name)
})
}
/// Return `true` if an [`Expr`] is `None`.
pub const fn is_const_none(expr: &Expr) -> bool {
matches!(
@ -702,14 +691,6 @@ pub const fn is_const_false(expr: &Expr) -> bool {
)
}
/// Return `true` if a keyword argument is present with a non-`None` value.
pub fn has_non_none_keyword(keywords: &[Keyword], keyword: &str) -> bool {
find_keyword(keywords, keyword).is_some_and(|keyword| {
let Keyword { value, .. } = keyword;
!is_const_none(value)
})
}
/// Extract the names of all handled exceptions.
pub fn extract_handled_exceptions(handlers: &[ExceptHandler]) -> Vec<&Expr> {
let mut handled_exceptions = Vec::new();