Skip to content

Commit eec7743

Browse files
committed
[flake8_gettext]: Make INTxxx rules also trigger on aliased imports
1 parent 28ab61d commit eec7743

12 files changed

+871
-8
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,49 @@
11
_(f"{'value'}")
2+
gettext(f"{'value'}")
3+
ngettext(f"{'value'}", f"{'values'}", 2)
4+
5+
_gettext(f"{'value'}") # no lint
6+
gettext_fn(f"{'value'}") # no lint
7+
8+
9+
# https://github.com/astral-sh/ruff/issues/19028 - import aliases
10+
import gettext as gettext_mod
11+
from gettext import (
12+
gettext as gettext_fn,
13+
ngettext as ngettext_fn,
14+
)
15+
16+
name = "Guido"
17+
18+
gettext_mod.gettext(f"Hello, {name}!")
19+
gettext_mod.ngettext(f"Hello, {name}!", f"Hello, {name}s!", 2)
20+
gettext_fn(f"Hello, {name}!")
21+
ngettext_fn(f"Hello, {name}!", f"Hello, {name}s!", 2)
22+
23+
24+
# https://github.com/astral-sh/ruff/issues/19028 - deferred translations
25+
def _(message): return message
26+
27+
animals = [_(f"{'mollusk'}"),
28+
_(f"{'albatross'}"),
29+
_(f"{'rat'}"),
30+
_(f"{'penguin'}"),
31+
_(f"{'python'}"),]
32+
33+
del _
34+
35+
for a in animals:
36+
print(_(a))
37+
print(_(f"{a}"))
38+
39+
40+
# https://github.com/astral-sh/ruff/issues/19028
41+
# - installed translations/i18n functions dynamically assigned to builtins
42+
import builtins
43+
44+
gettext_mod.install("domain")
45+
builtins.__dict__["gettext"] = gettext_fn
46+
47+
builtins._(f"{'value'}")
48+
builtins.gettext(f"{'value'}")
49+
builtins.ngettext(f"{'value'}", f"{'values'}", 2)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,49 @@
11
_("{}".format("line"))
2+
gettext("{}".format("line"))
3+
ngettext("{}".format("line"), "{}".format("lines"), 2)
4+
5+
_gettext("{}".format("line")) # no lint
6+
gettext_fn("{}".format("line")) # no lint
7+
8+
9+
# https://github.com/astral-sh/ruff/issues/19028
10+
import gettext as gettext_mod
11+
from gettext import (
12+
gettext as gettext_fn,
13+
ngettext as ngettext_fn,
14+
)
15+
16+
name = "Guido"
17+
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)
22+
23+
24+
# https://github.com/astral-sh/ruff/issues/19028 - deferred translations
25+
def _(message): return message
26+
27+
animals = [_("{}".format("mollusk")),
28+
_("{}".format("albatross")),
29+
_("{}".format("rat")),
30+
_("{}".format("penguin")),
31+
_("{}".format("python")),]
32+
33+
del _
34+
35+
for a in animals:
36+
print(_(a))
37+
print(_("{}".format(a)))
38+
39+
40+
# https://github.com/astral-sh/ruff/issues/19028
41+
# - installed translations/i18n functions dynamically assigned to builtins
42+
import builtins
43+
44+
gettext_mod.install("domain")
45+
builtins.__dict__["gettext"] = gettext_fn
46+
47+
builtins._("{}".format("line"))
48+
builtins.gettext("{}".format("line"))
49+
builtins.ngettext("{}".format("line"), "{}".format("lines"), 2)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,49 @@
11
_("%s" % "line")
2+
gettext("%s" % "line")
3+
ngettext("%s" % "line", "%s" % "lines", 2)
4+
5+
_gettext("%s" % "line") # no lint
6+
gettext_fn("%s" % "line") # no lint
7+
8+
9+
# https://github.com/astral-sh/ruff/issues/19028
10+
import gettext as gettext_mod
11+
from gettext import (
12+
gettext as gettext_fn,
13+
ngettext as ngettext_fn,
14+
)
15+
16+
name = "Guido"
17+
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)
22+
23+
24+
# https://github.com/astral-sh/ruff/issues/19028 - deferred translations
25+
def _(message): return message
26+
27+
animals = [_("%s" %"mollusk"),
28+
_("%s" %"albatross"),
29+
_("%s" %"rat"),
30+
_("%s" %"penguin"),
31+
_("%s" %"python"),]
32+
33+
del _
34+
35+
for a in animals:
36+
print(_(a))
37+
print(_("%s" % a))
38+
39+
40+
# https://github.com/astral-sh/ruff/issues/19028
41+
# - installed translations/i18n functions dynamically assigned to builtins
42+
import builtins
43+
44+
gettext_mod.install("domain")
45+
builtins.__dict__["gettext"] = gettext_fn
46+
47+
builtins._("%s" % "line")
48+
builtins.gettext("%s" % "line")
49+
builtins.ngettext("%s" % "line", "%s" % "lines", 2)

crates/ruff_linter/src/checkers/ast/analyze/expression.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
10111011
Rule::FormatInGetTextFuncCall,
10121012
Rule::PrintfInGetTextFuncCall,
10131013
]) && flake8_gettext::is_gettext_func_call(
1014+
checker,
10141015
func,
10151016
&checker.settings().flake8_gettext.functions_names,
10161017
) {

crates/ruff_linter/src/preview.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,8 @@ pub(crate) const fn is_safe_super_call_with_parameters_fix_enabled(
111111
) -> bool {
112112
settings.preview.is_enabled()
113113
}
114+
115+
// https://github.com/astral-sh/ruff/pull/19045
116+
pub(crate) const fn is_extended_i18n_function_matching_enabled(settings: &LinterSettings) -> bool {
117+
settings.preview.is_enabled()
118+
}

crates/ruff_linter/src/rules/flake8_gettext/mod.rs

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,50 @@
11
//! Rules from [flake8-gettext](https://pypi.org/project/flake8-gettext/).
2+
use crate::checkers::ast::Checker;
3+
use crate::preview::is_extended_i18n_function_matching_enabled;
24
use ruff_python_ast::name::Name;
35
use ruff_python_ast::{self as ast, Expr};
6+
use ruff_python_semantic::Modules;
47

58
pub(crate) mod rules;
69
pub mod settings;
710

811
/// Returns true if the [`Expr`] is an internationalization function call.
9-
pub(crate) fn is_gettext_func_call(func: &Expr, functions_names: &[Name]) -> bool {
10-
if let Expr::Name(ast::ExprName { id, .. }) = func {
11-
functions_names.contains(id)
12-
} else {
13-
false
12+
pub(crate) fn is_gettext_func_call(
13+
checker: &Checker,
14+
func: &Expr,
15+
functions_names: &[Name],
16+
) -> bool {
17+
if func
18+
.as_name_expr()
19+
.map(ast::ExprName::id)
20+
.is_some_and(|id| functions_names.contains(id))
21+
{
22+
return true;
1423
}
24+
25+
if !is_extended_i18n_function_matching_enabled(checker.settings()) {
26+
return false;
27+
}
28+
29+
let semantic = checker.semantic();
30+
31+
let Some(qualified_name) = semantic.resolve_qualified_name(func) else {
32+
return false;
33+
};
34+
35+
if semantic.seen_module(Modules::BUILTINS)
36+
&& matches!(
37+
qualified_name.segments(),
38+
["" | "builtins", id] if functions_names.contains(&Name::new(id)),
39+
)
40+
{
41+
return true;
42+
}
43+
44+
matches!(
45+
qualified_name.segments(),
46+
["gettext", "gettext" | "ngettext"]
47+
)
1548
}
1649

1750
#[cfg(test)]
@@ -22,6 +55,7 @@ mod tests {
2255
use test_case::test_case;
2356

2457
use crate::registry::Rule;
58+
use crate::settings::types::PreviewMode;
2559
use crate::test::test_path;
2660
use crate::{assert_diagnostics, settings};
2761

@@ -37,4 +71,20 @@ mod tests {
3771
assert_diagnostics!(snapshot, diagnostics);
3872
Ok(())
3973
}
74+
75+
#[test_case(Rule::FStringInGetTextFuncCall, Path::new("INT001.py"))]
76+
#[test_case(Rule::FormatInGetTextFuncCall, Path::new("INT002.py"))]
77+
#[test_case(Rule::PrintfInGetTextFuncCall, Path::new("INT003.py"))]
78+
fn rules_preview(rule_code: Rule, path: &Path) -> Result<()> {
79+
let snapshot = format!("preview__{}_{}", rule_code.name(), path.to_string_lossy());
80+
let diagnostics = test_path(
81+
Path::new("flake8_gettext").join(path).as_path(),
82+
&settings::LinterSettings {
83+
preview: PreviewMode::Enabled,
84+
..settings::LinterSettings::for_rule(rule_code)
85+
},
86+
)?;
87+
assert_diagnostics!(snapshot, diagnostics);
88+
Ok(())
89+
}
4090
}
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,84 @@
11
---
22
source: crates/ruff_linter/src/rules/flake8_gettext/mod.rs
3-
snapshot_kind: text
43
---
54
INT001.py:1:3: INT001 f-string is resolved before function call; consider `_("string %s") % arg`
65
|
76
1 | _(f"{'value'}")
87
| ^^^^^^^^^^^^ INT001
8+
2 | gettext(f"{'value'}")
9+
3 | ngettext(f"{'value'}", f"{'values'}", 2)
910
|
11+
12+
INT001.py:2:9: INT001 f-string is resolved before function call; consider `_("string %s") % arg`
13+
|
14+
1 | _(f"{'value'}")
15+
2 | gettext(f"{'value'}")
16+
| ^^^^^^^^^^^^ INT001
17+
3 | ngettext(f"{'value'}", f"{'values'}", 2)
18+
|
19+
20+
INT001.py:3:10: INT001 f-string is resolved before function call; consider `_("string %s") % arg`
21+
|
22+
1 | _(f"{'value'}")
23+
2 | gettext(f"{'value'}")
24+
3 | ngettext(f"{'value'}", f"{'values'}", 2)
25+
| ^^^^^^^^^^^^ INT001
26+
4 |
27+
5 | _gettext(f"{'value'}") # no lint
28+
|
29+
30+
INT001.py:27:14: INT001 f-string is resolved before function call; consider `_("string %s") % arg`
31+
|
32+
25 | def _(message): return message
33+
26 |
34+
27 | animals = [_(f"{'mollusk'}"),
35+
| ^^^^^^^^^^^^^^ INT001
36+
28 | _(f"{'albatross'}"),
37+
29 | _(f"{'rat'}"),
38+
|
39+
40+
INT001.py:28:14: INT001 f-string is resolved before function call; consider `_("string %s") % arg`
41+
|
42+
27 | animals = [_(f"{'mollusk'}"),
43+
28 | _(f"{'albatross'}"),
44+
| ^^^^^^^^^^^^^^^^ INT001
45+
29 | _(f"{'rat'}"),
46+
30 | _(f"{'penguin'}"),
47+
|
48+
49+
INT001.py:29:14: INT001 f-string is resolved before function call; consider `_("string %s") % arg`
50+
|
51+
27 | animals = [_(f"{'mollusk'}"),
52+
28 | _(f"{'albatross'}"),
53+
29 | _(f"{'rat'}"),
54+
| ^^^^^^^^^^ INT001
55+
30 | _(f"{'penguin'}"),
56+
31 | _(f"{'python'}"),]
57+
|
58+
59+
INT001.py:30:14: INT001 f-string is resolved before function call; consider `_("string %s") % arg`
60+
|
61+
28 | _(f"{'albatross'}"),
62+
29 | _(f"{'rat'}"),
63+
30 | _(f"{'penguin'}"),
64+
| ^^^^^^^^^^^^^^ INT001
65+
31 | _(f"{'python'}"),]
66+
|
67+
68+
INT001.py:31:14: INT001 f-string is resolved before function call; consider `_("string %s") % arg`
69+
|
70+
29 | _(f"{'rat'}"),
71+
30 | _(f"{'penguin'}"),
72+
31 | _(f"{'python'}"),]
73+
| ^^^^^^^^^^^^^ INT001
74+
32 |
75+
33 | del _
76+
|
77+
78+
INT001.py:37:13: INT001 f-string is resolved before function call; consider `_("string %s") % arg`
79+
|
80+
35 | for a in animals:
81+
36 | print(_(a))
82+
37 | print(_(f"{a}"))
83+
| ^^^^^^ INT001
84+
|

0 commit comments

Comments
 (0)