Skip to content

Commit

Permalink
Implement flake8-gettext (#3785)
Browse files Browse the repository at this point in the history
  • Loading branch information
leiserfg authored Mar 28, 2023
1 parent 515e436 commit 224e85c
Show file tree
Hide file tree
Showing 22 changed files with 384 additions and 20 deletions.
9 changes: 9 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,15 @@ are:
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""

- flake8-gettext, licensed as follows:
"""
BSD Zero Clause License

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""

- flake8-implicit-str-concat, licensed as follows:
"""
The MIT License (MIT)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ quality tools, including:
- [flake8-eradicate](https://pypi.org/project/flake8-eradicate/)
- [flake8-errmsg](https://pypi.org/project/flake8-errmsg/)
- [flake8-executable](https://pypi.org/project/flake8-executable/)
- [flake8-gettext](https://pypi.org/project/flake8-gettext/)
- [flake8-implicit-str-concat](https://pypi.org/project/flake8-implicit-str-concat/)
- [flake8-import-conventions](https://github.com/joaopalmeiro/flake8-import-conventions)
- [flake8-logging-format](https://pypi.org/project/flake8-logging-format/)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
_(f"{'value'}")
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
_("{}".format("line"))
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
_("%s" % "line")
34 changes: 29 additions & 5 deletions crates/ruff/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ use crate::registry::{AsRule, Rule};
use crate::rules::{
flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, flake8_boolean_trap,
flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger,
flake8_django, flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions,
flake8_logging_format, flake8_pie, flake8_print, flake8_pyi, flake8_pytest_style, flake8_raise,
flake8_return, flake8_self, flake8_simplify, flake8_tidy_imports, flake8_type_checking,
flake8_unused_arguments, flake8_use_pathlib, mccabe, numpy, pandas_vet, pep8_naming,
pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, tryceratops,
flake8_django, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat,
flake8_import_conventions, flake8_logging_format, flake8_pie, flake8_print, flake8_pyi,
flake8_pytest_style, flake8_raise, flake8_return, flake8_self, flake8_simplify,
flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, flake8_use_pathlib, mccabe,
numpy, pandas_vet, pep8_naming, pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint,
pyupgrade, ruff, tryceratops,
};
use crate::settings::types::PythonVersion;
use crate::settings::{flags, Settings};
Expand Down Expand Up @@ -2890,6 +2891,29 @@ where
}
}

// flake8-gettext
if self.settings.rules.any_enabled(&[
Rule::FStringInGetTextFuncCall,
Rule::FormatInGetTextFuncCall,
Rule::PrintfInGetTextFuncCall,
]) && flake8_gettext::rules::is_gettext_func_call(
func,
&self.settings.flake8_gettext.functions_names,
) {
if self.settings.rules.enabled(Rule::FStringInGetTextFuncCall) {
self.diagnostics
.extend(flake8_gettext::rules::f_string_in_gettext_func_call(args));
}
if self.settings.rules.enabled(Rule::FormatInGetTextFuncCall) {
self.diagnostics
.extend(flake8_gettext::rules::format_in_gettext_func_call(args));
}
if self.settings.rules.enabled(Rule::PrintfInGetTextFuncCall) {
self.diagnostics
.extend(flake8_gettext::rules::printf_in_gettext_func_call(args));
}
}

// flake8-simplify
if self
.settings
Expand Down
5 changes: 5 additions & 0 deletions crates/ruff/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
(Flake8Return, "507") => Rule::SuperfluousElseContinue,
(Flake8Return, "508") => Rule::SuperfluousElseBreak,

// flake8-gettext
(Flake8GetText, "001") => Rule::FStringInGetTextFuncCall,
(Flake8GetText, "002") => Rule::FormatInGetTextFuncCall,
(Flake8GetText, "003") => Rule::PrintfInGetTextFuncCall,

// flake8-implicit-str-concat
(Flake8ImplicitStrConcat, "001") => Rule::SingleLineImplicitStringConcatenation,
(Flake8ImplicitStrConcat, "002") => Rule::MultiLineImplicitStringConcatenation,
Expand Down
7 changes: 7 additions & 0 deletions crates/ruff/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,10 @@ ruff_macros::register_rules!(
rules::flake8_raise::rules::UnnecessaryParenOnRaiseException,
// flake8-self
rules::flake8_self::rules::PrivateMemberAccess,
// flake8-gettext
rules::flake8_gettext::rules::FStringInGetTextFuncCall,
rules::flake8_gettext::rules::FormatInGetTextFuncCall,
rules::flake8_gettext::rules::PrintfInGetTextFuncCall,
// numpy
rules::numpy::rules::NumpyDeprecatedTypeAlias,
rules::numpy::rules::NumpyLegacyRandom,
Expand Down Expand Up @@ -778,6 +782,9 @@ pub enum Linter {
/// [flake8-type-checking](https://pypi.org/project/flake8-type-checking/)
#[prefix = "TCH"]
Flake8TypeChecking,
/// [flake8-gettext](https://pypi.org/project/flake8-gettext/)
#[prefix = "INT"]
Flake8GetText,
/// [flake8-unused-arguments](https://pypi.org/project/flake8-unused-arguments/)
#[prefix = "ARG"]
Flake8UnusedArguments,
Expand Down
29 changes: 29 additions & 0 deletions crates/ruff/src/rules/flake8_gettext/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//! Rules from [flake8-gettext](https://pypi.org/project/flake8-gettext/).
pub(crate) mod rules;
pub mod settings;

#[cfg(test)]
mod tests {
use std::path::Path;

use anyhow::Result;
use insta::assert_yaml_snapshot;
use test_case::test_case;

use crate::registry::Rule;
use crate::settings;
use crate::test::test_path;

#[test_case(Rule::FStringInGetTextFuncCall,Path::new("INT001.py"); "INT001")]
#[test_case(Rule::FormatInGetTextFuncCall, Path::new("INT002.py"); "INT002")]
#[test_case(Rule::PrintfInGetTextFuncCall, Path::new("INT003.py"); "INT003")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_gettext").join(path).as_path(),
&settings::Settings::for_rule(rule_code),
)?;
assert_yaml_snapshot!(snapshot, diagnostics);
Ok(())
}
}
97 changes: 97 additions & 0 deletions crates/ruff/src/rules/flake8_gettext/rules.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use rustpython_parser::ast::{Constant, Expr, ExprKind, Operator};

use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::Range;

#[violation]
pub struct FStringInGetTextFuncCall;

impl Violation for FStringInGetTextFuncCall {
#[derive_message_formats]
fn message(&self) -> String {
format!("f-string is resolved before function call; consider `_(\"string %s\") % arg`")
}
}

#[violation]
pub struct FormatInGetTextFuncCall;

impl Violation for FormatInGetTextFuncCall {
#[derive_message_formats]
fn message(&self) -> String {
format!("`format` method argument is resolved before function call; consider `_(\"string %s\") % arg`")
}
}
#[violation]
pub struct PrintfInGetTextFuncCall;

impl Violation for PrintfInGetTextFuncCall {
#[derive_message_formats]
fn message(&self) -> String {
format!("printf-style format is resolved before function call; consider `_(\"string %s\") % arg`")
}
}

/// Returns true if the [`Expr`] is an internationalization function call.
pub fn is_gettext_func_call(func: &Expr, functions_names: &[String]) -> bool {
if let ExprKind::Name { id, .. } = &func.node {
functions_names.contains(id)
} else {
false
}
}

/// INT001
pub fn f_string_in_gettext_func_call(args: &[Expr]) -> Option<Diagnostic> {
if let Some(first) = args.first() {
if matches!(first.node, ExprKind::JoinedStr { .. }) {
return Some(Diagnostic::new(
FStringInGetTextFuncCall {},
Range::from(first),
));
}
}
None
}

/// INT002
pub fn format_in_gettext_func_call(args: &[Expr]) -> Option<Diagnostic> {
if let Some(first) = args.first() {
if let ExprKind::Call { func, .. } = &first.node {
if let ExprKind::Attribute { attr, .. } = &func.node {
if attr == "format" {
return Some(Diagnostic::new(
FormatInGetTextFuncCall {},
Range::from(first),
));
}
}
}
}
None
}

/// INT003
pub fn printf_in_gettext_func_call(args: &[Expr]) -> Option<Diagnostic> {
if let Some(first) = args.first() {
if let ExprKind::BinOp {
op: Operator::Mod { .. },
left,
..
} = &first.node
{
if let ExprKind::Constant {
value: Constant::Str(_),
..
} = left.node
{
return Some(Diagnostic::new(
PrintfInGetTextFuncCall {},
Range::from(first),
));
}
}
}
None
}
73 changes: 73 additions & 0 deletions crates/ruff/src/rules/flake8_gettext/settings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use ruff_macros::{CacheKey, ConfigurationOptions};

#[derive(
Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions, JsonSchema,
)]
#[serde(
deny_unknown_fields,
rename_all = "kebab-case",
rename = "Flake8GetTextOptions"
)]
pub struct Options {
#[option(
default = r#"["_", "gettext", "ngettext"]"#,
value_type = "list[str]",
example = r#"function-names = ["_", "gettext", "ngettext", "ugettetxt"]"#
)]
/// The function names to consider as internationalization calls.
pub function_names: Option<Vec<String>>,
#[option(
default = r#"[]"#,
value_type = "list[str]",
example = r#"extend-function-names = ["ugettetxt"]"#
)]
#[serde(default)]
/// Additional function names to consider as internationalization calls, in addition to those
/// included in `function-names`.
pub extend_function_names: Vec<String>,
}

#[derive(Debug, CacheKey)]
pub struct Settings {
pub functions_names: Vec<String>,
}

fn default_func_names() -> Vec<String> {
["_", "gettext", "ngettext"]
.iter()
.map(std::string::ToString::to_string)
.collect()
}

impl Default for Settings {
fn default() -> Self {
Self {
functions_names: default_func_names(),
}
}
}

impl From<Options> for Settings {
fn from(options: Options) -> Self {
Self {
functions_names: options
.function_names
.unwrap_or_else(default_func_names)
.into_iter()
.chain(options.extend_function_names)
.collect(),
}
}
}

impl From<Settings> for Options {
fn from(settings: Settings) -> Self {
Self {
function_names: Some(settings.functions_names),
extend_function_names: vec![],
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
source: crates/ruff/src/rules/flake8_gettext/mod.rs
expression: diagnostics
---
- kind:
name: FStringInGetTextFuncCall
body: "f-string is resolved before function call; consider `_(\"string %s\") % arg`"
suggestion: ~
fixable: false
location:
row: 1
column: 2
end_location:
row: 1
column: 14
fix:
edits: []
parent: ~

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
source: crates/ruff/src/rules/flake8_gettext/mod.rs
expression: diagnostics
---
- kind:
name: FormatInGetTextFuncCall
body: "`format` method argument is resolved before function call; consider `_(\"string %s\") % arg`"
suggestion: ~
fixable: false
location:
row: 1
column: 2
end_location:
row: 1
column: 21
fix:
edits: []
parent: ~

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
source: crates/ruff/src/rules/flake8_gettext/mod.rs
expression: diagnostics
---
- kind:
name: PrintfInGetTextFuncCall
body: "printf-style format is resolved before function call; consider `_(\"string %s\") % arg`"
suggestion: ~
fixable: false
location:
row: 1
column: 2
end_location:
row: 1
column: 15
fix:
edits: []
parent: ~

1 change: 1 addition & 0 deletions crates/ruff/src/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod flake8_debugger;
pub mod flake8_django;
pub mod flake8_errmsg;
pub mod flake8_executable;
pub mod flake8_gettext;
pub mod flake8_implicit_str_concat;
pub mod flake8_import_conventions;
pub mod flake8_logging_format;
Expand Down
Loading

0 comments on commit 224e85c

Please sign in to comment.