diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 53081e3681840..fa5efbf290b9d 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -1039,13 +1039,13 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { &checker.settings().flake8_gettext.functions_names, ) { if checker.is_rule_enabled(Rule::FStringInGetTextFuncCall) { - flake8_gettext::rules::f_string_in_gettext_func_call(checker, args); + flake8_gettext::rules::f_string_in_gettext_func_call(checker, func, args); } if checker.is_rule_enabled(Rule::FormatInGetTextFuncCall) { - flake8_gettext::rules::format_in_gettext_func_call(checker, args); + flake8_gettext::rules::format_in_gettext_func_call(checker, func, args); } if checker.is_rule_enabled(Rule::PrintfInGetTextFuncCall) { - flake8_gettext::rules::printf_in_gettext_func_call(checker, args); + flake8_gettext::rules::printf_in_gettext_func_call(checker, func, args); } } if checker.is_rule_enabled(Rule::UncapitalizedEnvironmentVariables) { diff --git a/crates/ruff_linter/src/rules/flake8_gettext/helpers.rs b/crates/ruff_linter/src/rules/flake8_gettext/helpers.rs new file mode 100644 index 0000000000000..32ec548a22492 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_gettext/helpers.rs @@ -0,0 +1,21 @@ +use crate::checkers::ast::Checker; +use ruff_python_ast::Expr; + +/// Returns true if the function call is ngettext +pub(crate) fn is_ngettext_call(checker: &Checker, func: &Expr) -> bool { + let semantic = checker.semantic(); + + // Check if it's a direct name reference to ngettext + if let Some(name) = func.as_name_expr() { + if name.id == "ngettext" { + return true; + } + } + + // Check if it's a qualified name ending with ngettext + if let Some(qualified_name) = semantic.resolve_qualified_name(func) { + return matches!(qualified_name.segments(), [.., "ngettext"]); + } + + false +} diff --git a/crates/ruff_linter/src/rules/flake8_gettext/mod.rs b/crates/ruff_linter/src/rules/flake8_gettext/mod.rs index d4c5f7fb72d48..682e1a5f0b5d9 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_gettext/mod.rs @@ -5,6 +5,7 @@ use ruff_python_ast::name::Name; use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::Modules; +pub(crate) mod helpers; pub(crate) mod rules; pub mod settings; diff --git a/crates/ruff_linter/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs b/crates/ruff_linter/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs index 2cc9ca51a1e9e..9b7ad313af548 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs +++ b/crates/ruff_linter/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs @@ -5,6 +5,7 @@ use ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; +use crate::rules::flake8_gettext::helpers; /// ## What it does /// Checks for f-strings in `gettext` function calls. @@ -52,10 +53,20 @@ impl Violation for FStringInGetTextFuncCall { } /// INT001 -pub(crate) fn f_string_in_gettext_func_call(checker: &Checker, args: &[Expr]) { +pub(crate) fn f_string_in_gettext_func_call(checker: &Checker, func: &Expr, args: &[Expr]) { + // Check first argument (singular) if let Some(first) = args.first() { if first.is_f_string_expr() { checker.report_diagnostic(FStringInGetTextFuncCall {}, first.range()); } } + + // Check second argument (plural) for ngettext calls + if helpers::is_ngettext_call(checker, func) { + if let Some(second) = args.get(1) { + if second.is_f_string_expr() { + checker.report_diagnostic(FStringInGetTextFuncCall {}, second.range()); + } + } + } } diff --git a/crates/ruff_linter/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs b/crates/ruff_linter/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs index ad584804d0d57..58a8d1673846a 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs +++ b/crates/ruff_linter/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs @@ -5,6 +5,7 @@ use ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; +use crate::rules::flake8_gettext::helpers; /// ## What it does /// Checks for `str.format` calls in `gettext` function calls. @@ -52,7 +53,8 @@ impl Violation for FormatInGetTextFuncCall { } /// INT002 -pub(crate) fn format_in_gettext_func_call(checker: &Checker, args: &[Expr]) { +pub(crate) fn format_in_gettext_func_call(checker: &Checker, func: &Expr, args: &[Expr]) { + // Check first argument (singular) if let Some(first) = args.first() { if let Expr::Call(ast::ExprCall { func, .. }) = &first { if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() { @@ -62,4 +64,17 @@ pub(crate) fn format_in_gettext_func_call(checker: &Checker, args: &[Expr]) { } } } + + // Check second argument (plural) for ngettext calls + if helpers::is_ngettext_call(checker, func) { + if let Some(second) = args.get(1) { + if let Expr::Call(ast::ExprCall { func, .. }) = &second { + if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() { + if attr == "format" { + checker.report_diagnostic(FormatInGetTextFuncCall {}, second.range()); + } + } + } + } + } } diff --git a/crates/ruff_linter/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs b/crates/ruff_linter/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs index 22172a80058cd..a0b1df4c7f944 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs +++ b/crates/ruff_linter/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs @@ -4,6 +4,7 @@ use ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; +use crate::rules::flake8_gettext::helpers; /// ## What it does /// Checks for printf-style formatted strings in `gettext` function calls. @@ -52,7 +53,8 @@ impl Violation for PrintfInGetTextFuncCall { } /// INT003 -pub(crate) fn printf_in_gettext_func_call(checker: &Checker, args: &[Expr]) { +pub(crate) fn printf_in_gettext_func_call(checker: &Checker, func: &Expr, args: &[Expr]) { + // Check first argument (singular) if let Some(first) = args.first() { if let Expr::BinOp(ast::ExprBinOp { op: Operator::Mod, @@ -65,4 +67,20 @@ pub(crate) fn printf_in_gettext_func_call(checker: &Checker, args: &[Expr]) { } } } + + // Check second argument (plural) for ngettext calls + if helpers::is_ngettext_call(checker, func) { + if let Some(second) = args.get(1) { + if let Expr::BinOp(ast::ExprBinOp { + op: Operator::Mod, + left, + .. + }) = &second + { + if left.is_string_literal_expr() { + checker.report_diagnostic(PrintfInGetTextFuncCall {}, second.range()); + } + } + } + } } diff --git a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__f-string-in-get-text-func-call_INT001.py.snap b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__f-string-in-get-text-func-call_INT001.py.snap index 0285458057920..78ae142b86051 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__f-string-in-get-text-func-call_INT001.py.snap +++ b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__f-string-in-get-text-func-call_INT001.py.snap @@ -30,6 +30,16 @@ INT001 f-string is resolved before function call; consider `_("string %s") % arg 9 | _gettext(f"{'value'}") # no lint | +INT001 f-string is resolved before function call; consider `_("string %s") % arg` + --> INT001.py:7:24 + | +6 | gettext(f"{'value'}") +7 | ngettext(f"{'value'}", f"{'values'}", 2) + | ^^^^^^^^^^^^^ +8 | +9 | _gettext(f"{'value'}") # no lint + | + INT001 f-string is resolved before function call; consider `_("string %s") % arg` --> INT001.py:31:14 | diff --git a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__format-in-get-text-func-call_INT002.py.snap b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__format-in-get-text-func-call_INT002.py.snap index b1edf104de3c9..f7736c7237d65 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__format-in-get-text-func-call_INT002.py.snap +++ b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__format-in-get-text-func-call_INT002.py.snap @@ -30,6 +30,17 @@ INT002 `format` method argument is resolved before function call; consider `_("s 5 | _gettext("{}".format("line")) # no lint | +INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` + --> INT002.py:3:31 + | +1 | _("{}".format("line")) +2 | gettext("{}".format("line")) +3 | ngettext("{}".format("line"), "{}".format("lines"), 2) + | ^^^^^^^^^^^^^^^^^^^^ +4 | +5 | _gettext("{}".format("line")) # no lint + | + INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` --> INT002.py:27:14 | diff --git a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__f-string-in-get-text-func-call_INT001.py.snap b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__f-string-in-get-text-func-call_INT001.py.snap index 9cfb1b5242d73..a2940e9042e45 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__f-string-in-get-text-func-call_INT001.py.snap +++ b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__f-string-in-get-text-func-call_INT001.py.snap @@ -30,6 +30,16 @@ INT001 f-string is resolved before function call; consider `_("string %s") % arg 9 | _gettext(f"{'value'}") # no lint | +INT001 f-string is resolved before function call; consider `_("string %s") % arg` + --> INT001.py:7:24 + | +6 | gettext(f"{'value'}") +7 | ngettext(f"{'value'}", f"{'values'}", 2) + | ^^^^^^^^^^^^^ +8 | +9 | _gettext(f"{'value'}") # no lint + | + INT001 f-string is resolved before function call; consider `_("string %s") % arg` --> INT001.py:22:21 | @@ -51,6 +61,16 @@ INT001 f-string is resolved before function call; consider `_("string %s") % arg 25 | ngettext_fn(f"Hello, {name}!", f"Hello, {name}s!", 2) | +INT001 f-string is resolved before function call; consider `_("string %s") % arg` + --> INT001.py:23:41 + | +22 | gettext_mod.gettext(f"Hello, {name}!") +23 | gettext_mod.ngettext(f"Hello, {name}!", f"Hello, {name}s!", 2) + | ^^^^^^^^^^^^^^^^^^ +24 | gettext_fn(f"Hello, {name}!") +25 | ngettext_fn(f"Hello, {name}!", f"Hello, {name}s!", 2) + | + INT001 f-string is resolved before function call; consider `_("string %s") % arg` --> INT001.py:24:12 | @@ -70,6 +90,15 @@ INT001 f-string is resolved before function call; consider `_("string %s") % arg | ^^^^^^^^^^^^^^^^^ | +INT001 f-string is resolved before function call; consider `_("string %s") % arg` + --> INT001.py:25:32 + | +23 | gettext_mod.ngettext(f"Hello, {name}!", f"Hello, {name}s!", 2) +24 | gettext_fn(f"Hello, {name}!") +25 | ngettext_fn(f"Hello, {name}!", f"Hello, {name}s!", 2) + | ^^^^^^^^^^^^^^^^^^ + | + INT001 f-string is resolved before function call; consider `_("string %s") % arg` --> INT001.py:31:14 | @@ -160,3 +189,12 @@ INT001 f-string is resolved before function call; consider `_("string %s") % arg 53 | builtins.ngettext(f"{'value'}", f"{'values'}", 2) | ^^^^^^^^^^^^ | + +INT001 f-string is resolved before function call; consider `_("string %s") % arg` + --> INT001.py:53:33 + | +51 | builtins._(f"{'value'}") +52 | builtins.gettext(f"{'value'}") +53 | builtins.ngettext(f"{'value'}", f"{'values'}", 2) + | ^^^^^^^^^^^^^ + | diff --git a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__format-in-get-text-func-call_INT002.py.snap b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__format-in-get-text-func-call_INT002.py.snap index b368f71065204..9e0ff38e50115 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__format-in-get-text-func-call_INT002.py.snap +++ b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__format-in-get-text-func-call_INT002.py.snap @@ -30,6 +30,17 @@ INT002 `format` method argument is resolved before function call; consider `_("s 5 | _gettext("{}".format("line")) # no lint | +INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` + --> INT002.py:3:31 + | +1 | _("{}".format("line")) +2 | gettext("{}".format("line")) +3 | ngettext("{}".format("line"), "{}".format("lines"), 2) + | ^^^^^^^^^^^^^^^^^^^^ +4 | +5 | _gettext("{}".format("line")) # no lint + | + INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` --> INT002.py:18:21 | @@ -51,6 +62,16 @@ INT002 `format` method argument is resolved before function call; consider `_("s 21 | ngettext_fn("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) | +INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` + --> INT002.py:19:49 + | +18 | gettext_mod.gettext("Hello, {}!".format(name)) +19 | gettext_mod.ngettext("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ +20 | gettext_fn("Hello, {}!".format(name)) +21 | ngettext_fn("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) + | + INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` --> INT002.py:20:12 | @@ -70,6 +91,15 @@ INT002 `format` method argument is resolved before function call; consider `_("s | ^^^^^^^^^^^^^^^^^^^^^^^^^ | +INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` + --> INT002.py:21:40 + | +19 | gettext_mod.ngettext("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) +20 | gettext_fn("Hello, {}!".format(name)) +21 | ngettext_fn("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` --> INT002.py:27:14 | @@ -160,3 +190,12 @@ INT002 `format` method argument is resolved before function call; consider `_("s 49 | builtins.ngettext("{}".format("line"), "{}".format("lines"), 2) | ^^^^^^^^^^^^^^^^^^^ | + +INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` + --> INT002.py:49:40 + | +47 | builtins._("{}".format("line")) +48 | builtins.gettext("{}".format("line")) +49 | builtins.ngettext("{}".format("line"), "{}".format("lines"), 2) + | ^^^^^^^^^^^^^^^^^^^^ + | diff --git a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__printf-in-get-text-func-call_INT003.py.snap b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__printf-in-get-text-func-call_INT003.py.snap index 496422c556a34..f3d70b57c1dec 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__printf-in-get-text-func-call_INT003.py.snap +++ b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__printf-in-get-text-func-call_INT003.py.snap @@ -30,6 +30,17 @@ INT003 printf-style format is resolved before function call; consider `_("string 5 | _gettext("%s" % "line") # no lint | +INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` + --> INT003.py:3:25 + | +1 | _("%s" % "line") +2 | gettext("%s" % "line") +3 | ngettext("%s" % "line", "%s" % "lines", 2) + | ^^^^^^^^^^^^^^ +4 | +5 | _gettext("%s" % "line") # no lint + | + INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` --> INT003.py:18:21 | @@ -51,6 +62,16 @@ INT003 printf-style format is resolved before function call; consider `_("string 21 | ngettext_fn("Hello, %s!" % name, "Hello, %ss!" % name, 2) | +INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` + --> INT003.py:19:43 + | +18 | gettext_mod.gettext("Hello, %s!" % name) +19 | gettext_mod.ngettext("Hello, %s!" % name, "Hello, %ss!" % name, 2) + | ^^^^^^^^^^^^^^^^^^^^ +20 | gettext_fn("Hello, %s!" % name) +21 | ngettext_fn("Hello, %s!" % name, "Hello, %ss!" % name, 2) + | + INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` --> INT003.py:20:12 | @@ -70,6 +91,15 @@ INT003 printf-style format is resolved before function call; consider `_("string | ^^^^^^^^^^^^^^^^^^^ | +INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` + --> INT003.py:21:34 + | +19 | gettext_mod.ngettext("Hello, %s!" % name, "Hello, %ss!" % name, 2) +20 | gettext_fn("Hello, %s!" % name) +21 | ngettext_fn("Hello, %s!" % name, "Hello, %ss!" % name, 2) + | ^^^^^^^^^^^^^^^^^^^^ + | + INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` --> INT003.py:27:14 | @@ -160,3 +190,12 @@ INT003 printf-style format is resolved before function call; consider `_("string 49 | builtins.ngettext("%s" % "line", "%s" % "lines", 2) | ^^^^^^^^^^^^^ | + +INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` + --> INT003.py:49:34 + | +47 | builtins._("%s" % "line") +48 | builtins.gettext("%s" % "line") +49 | builtins.ngettext("%s" % "line", "%s" % "lines", 2) + | ^^^^^^^^^^^^^^ + | diff --git a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__printf-in-get-text-func-call_INT003.py.snap b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__printf-in-get-text-func-call_INT003.py.snap index 91260001b788a..18b456a31cdff 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__printf-in-get-text-func-call_INT003.py.snap +++ b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__printf-in-get-text-func-call_INT003.py.snap @@ -30,6 +30,17 @@ INT003 printf-style format is resolved before function call; consider `_("string 5 | _gettext("%s" % "line") # no lint | +INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` + --> INT003.py:3:25 + | +1 | _("%s" % "line") +2 | gettext("%s" % "line") +3 | ngettext("%s" % "line", "%s" % "lines", 2) + | ^^^^^^^^^^^^^^ +4 | +5 | _gettext("%s" % "line") # no lint + | + INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` --> INT003.py:27:14 |