diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/non_ascii_name.py b/crates/ruff_linter/resources/test/fixtures/pylint/non_ascii_name.py new file mode 100644 index 0000000000000..0487770133434 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pylint/non_ascii_name.py @@ -0,0 +1,31 @@ +ápple_count: int = 1 # C2401 +ápple_count += 2 # C2401 +ápple_count = 3 # C2401 + +(ápple_count for ápple_count in y) + + +def func(ápple_count): + global ápple_count + nonlocal ápple_count + + +def ápple_count(): + pass + + +match ápple_count: + case ápple_count: + pass + +ápple_count: int + +try: + 1/0 +except ápple_count: + pass + +# OK +print(ápple_count) +ápple_count == 3 +apple_count = 4 diff --git a/crates/ruff_linter/src/checkers/ast/analyze/bindings.rs b/crates/ruff_linter/src/checkers/ast/analyze/bindings.rs index e7a1f64baed75..0fbc85f5552fa 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/bindings.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/bindings.rs @@ -10,6 +10,7 @@ pub(crate) fn bindings(checker: &mut Checker) { if !checker.any_enabled(&[ Rule::InvalidAllFormat, Rule::InvalidAllObject, + Rule::NonAsciiName, Rule::UnaliasedCollectionsAbcSetImport, Rule::UnconventionalImportAlias, Rule::UnusedVariable, @@ -49,6 +50,11 @@ pub(crate) fn bindings(checker: &mut Checker) { checker.diagnostics.push(diagnostic); } } + if checker.enabled(Rule::NonAsciiName) { + if let Some(diagnostic) = pylint::rules::non_ascii_name(binding, checker.locator) { + checker.diagnostics.push(diagnostic); + } + } if checker.enabled(Rule::UnconventionalImportAlias) { if let Some(diagnostic) = flake8_import_conventions::rules::unconventional_import_alias( checker, diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 86e9ae80666e4..29e5a72173440 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -1325,6 +1325,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { } } Stmt::Assign(assign @ ast::StmtAssign { targets, value, .. }) => { + checker.enabled(Rule::NonAsciiName); if checker.enabled(Rule::LambdaAssignment) { if let [target] = &targets[..] { pycodestyle::rules::lambda_assignment(checker, target, value, None, stmt); diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 8e5b87074c027..1c836a092be94 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -211,6 +211,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "C0205") => (RuleGroup::Stable, rules::pylint::rules::SingleStringSlots), (Pylint, "C0208") => (RuleGroup::Stable, rules::pylint::rules::IterationOverSet), (Pylint, "C0414") => (RuleGroup::Stable, rules::pylint::rules::UselessImportAlias), + (Pylint, "C2401") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiName), (Pylint, "C2403") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiImportName), #[allow(deprecated)] (Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString), diff --git a/crates/ruff_linter/src/rules/pylint/mod.rs b/crates/ruff_linter/src/rules/pylint/mod.rs index a1a6c3c591aa4..4b3b5afa335e3 100644 --- a/crates/ruff_linter/src/rules/pylint/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/mod.rs @@ -141,6 +141,7 @@ mod tests { #[test_case(Rule::GlobalAtModuleLevel, Path::new("global_at_module_level.py"))] #[test_case(Rule::UnnecessaryLambda, Path::new("unnecessary_lambda.py"))] #[test_case(Rule::NonAsciiImportName, Path::new("non_ascii_module_import.py"))] + #[test_case(Rule::NonAsciiName, Path::new("non_ascii_name.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/pylint/rules/mod.rs b/crates/ruff_linter/src/rules/pylint/rules/mod.rs index 0b8e3f20f3313..ae3676ba3fc5d 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/mod.rs @@ -35,6 +35,7 @@ pub(crate) use named_expr_without_context::*; pub(crate) use nested_min_max::*; pub(crate) use no_self_use::*; pub(crate) use non_ascii_module_import::*; +pub(crate) use non_ascii_name::*; pub(crate) use nonlocal_without_binding::*; pub(crate) use property_with_parameters::*; pub(crate) use redefined_loop_name::*; @@ -102,6 +103,7 @@ mod named_expr_without_context; mod nested_min_max; mod no_self_use; mod non_ascii_module_import; +mod non_ascii_name; mod nonlocal_without_binding; mod property_with_parameters; mod redefined_loop_name; diff --git a/crates/ruff_linter/src/rules/pylint/rules/non_ascii_module_import.rs b/crates/ruff_linter/src/rules/pylint/rules/non_ascii_module_import.rs index f1fc168e16117..577a4b69f19f1 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/non_ascii_module_import.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/non_ascii_module_import.rs @@ -10,8 +10,8 @@ use crate::checkers::ast::Checker; /// Checks for the use of non-ASCII characters in import statements. /// /// ## Why is this bad? -/// Pylint discourages the use of non-ASCII characters in symbol names as -/// they can cause confusion and compatibility issues. +/// The use of non-ASCII characters in import statements can cause confusion +/// and compatibility issues (see: [PEP 672]). /// /// ## Example /// ```python @@ -28,8 +28,7 @@ use crate::checkers::ast::Checker; /// import bár as bar /// ``` /// -/// ## References -/// - [PEP 672](https://peps.python.org/pep-0672/) +/// [PEP 672]: https://peps.python.org/pep-0672/ #[violation] pub struct NonAsciiImportName { name: String, diff --git a/crates/ruff_linter/src/rules/pylint/rules/non_ascii_name.rs b/crates/ruff_linter/src/rules/pylint/rules/non_ascii_name.rs new file mode 100644 index 0000000000000..c3326b9f5f63c --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/rules/non_ascii_name.rs @@ -0,0 +1,116 @@ +use std::fmt; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_semantic::{Binding, BindingKind}; +use ruff_source_file::Locator; +use ruff_text_size::Ranged; + +/// ## What it does +/// Checks for the use of non-ASCII characters in variable names. +/// +/// ## Why is this bad? +/// The use of non-ASCII characters in variable names can cause confusion +/// and compatibility issues (see: [PEP 672]). +/// +/// ## Example +/// ```python +/// ápple_count: int +/// ``` +/// +/// Use instead: +/// ```python +/// apple_count: int +/// ``` +/// +/// [PEP 672]: https://peps.python.org/pep-0672/ +#[violation] +pub struct NonAsciiName { + name: String, + kind: Kind, +} + +impl Violation for NonAsciiName { + #[derive_message_formats] + fn message(&self) -> String { + let Self { name, kind } = self; + format!("{kind} name `{name}` contains a non-ASCII character, consider renaming it") + } +} + +/// PLC2401 +pub(crate) fn non_ascii_name(binding: &Binding, locator: &Locator) -> Option { + let name = binding.name(locator); + if name.is_ascii() { + return None; + } + + let kind = match binding.kind { + BindingKind::Annotation => Kind::Annotation, + BindingKind::Argument => Kind::Argument, + BindingKind::NamedExprAssignment => Kind::NamedExprAssignment, + BindingKind::UnpackedAssignment => Kind::UnpackedAssignment, + BindingKind::Assignment => Kind::Assignment, + BindingKind::TypeParam => Kind::TypeParam, + BindingKind::LoopVar => Kind::LoopVar, + BindingKind::Global => Kind::Global, + BindingKind::Nonlocal(_) => Kind::Nonlocal, + BindingKind::ClassDefinition(_) => Kind::ClassDefinition, + BindingKind::FunctionDefinition(_) => Kind::FunctionDefinition, + BindingKind::BoundException => Kind::BoundException, + + BindingKind::Builtin + | BindingKind::Export(_) + | BindingKind::FutureImport + | BindingKind::Import(_) + | BindingKind::FromImport(_) + | BindingKind::SubmoduleImport(_) + | BindingKind::Deletion + | BindingKind::UnboundException(_) => { + return None; + } + }; + + Some(Diagnostic::new( + NonAsciiName { + name: name.to_string(), + kind, + }, + binding.range(), + )) +} + +#[derive(Debug, PartialEq, Eq)] +enum Kind { + Annotation, + Argument, + NamedExprAssignment, + UnpackedAssignment, + Assignment, + TypeParam, + LoopVar, + Global, + Nonlocal, + ClassDefinition, + FunctionDefinition, + BoundException, +} + +impl fmt::Display for Kind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Kind::Annotation => f.write_str("Annotation"), + Kind::Argument => f.write_str("Argument"), + Kind::NamedExprAssignment => f.write_str("Variable"), + Kind::UnpackedAssignment => f.write_str("Variable"), + Kind::Assignment => f.write_str("Variable"), + Kind::TypeParam => f.write_str("Type parameter"), + Kind::LoopVar => f.write_str("Variable"), + Kind::Global => f.write_str("Global"), + Kind::Nonlocal => f.write_str("Nonlocal"), + Kind::ClassDefinition => f.write_str("Class"), + Kind::FunctionDefinition => f.write_str("Function"), + Kind::BoundException => f.write_str("Exception"), + } + } +} diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2401_non_ascii_name.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2401_non_ascii_name.py.snap new file mode 100644 index 0000000000000..1ef5432f9575f --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2401_non_ascii_name.py.snap @@ -0,0 +1,79 @@ +--- +source: crates/ruff_linter/src/rules/pylint/mod.rs +--- +non_ascii_name.py:1:1: PLC2401 Variable name `ápple_count` contains a non-ASCII character, consider renaming it + | +1 | ápple_count: int = 1 # C2401 + | ^^^^^^^^^^^ PLC2401 +2 | ápple_count += 2 # C2401 +3 | ápple_count = 3 # C2401 + | + +non_ascii_name.py:2:1: PLC2401 Variable name `ápple_count` contains a non-ASCII character, consider renaming it + | +1 | ápple_count: int = 1 # C2401 +2 | ápple_count += 2 # C2401 + | ^^^^^^^^^^^ PLC2401 +3 | ápple_count = 3 # C2401 + | + +non_ascii_name.py:3:1: PLC2401 Variable name `ápple_count` contains a non-ASCII character, consider renaming it + | +1 | ápple_count: int = 1 # C2401 +2 | ápple_count += 2 # C2401 +3 | ápple_count = 3 # C2401 + | ^^^^^^^^^^^ PLC2401 +4 | +5 | (ápple_count for ápple_count in y) + | + +non_ascii_name.py:5:18: PLC2401 Variable name `ápple_count` contains a non-ASCII character, consider renaming it + | +3 | ápple_count = 3 # C2401 +4 | +5 | (ápple_count for ápple_count in y) + | ^^^^^^^^^^^ PLC2401 + | + +non_ascii_name.py:8:10: PLC2401 Argument name `ápple_count` contains a non-ASCII character, consider renaming it + | + 8 | def func(ápple_count): + | ^^^^^^^^^^^ PLC2401 + 9 | global ápple_count +10 | nonlocal ápple_count + | + +non_ascii_name.py:9:12: PLC2401 Global name `ápple_count` contains a non-ASCII character, consider renaming it + | + 8 | def func(ápple_count): + 9 | global ápple_count + | ^^^^^^^^^^^ PLC2401 +10 | nonlocal ápple_count + | + +non_ascii_name.py:13:5: PLC2401 Function name `ápple_count` contains a non-ASCII character, consider renaming it + | +13 | def ápple_count(): + | ^^^^^^^^^^^ PLC2401 +14 | pass + | + +non_ascii_name.py:18:10: PLC2401 Variable name `ápple_count` contains a non-ASCII character, consider renaming it + | +17 | match ápple_count: +18 | case ápple_count: + | ^^^^^^^^^^^ PLC2401 +19 | pass + | + +non_ascii_name.py:21:1: PLC2401 Annotation name `ápple_count` contains a non-ASCII character, consider renaming it + | +19 | pass +20 | +21 | ápple_count: int + | ^^^^^^^^^^^ PLC2401 +22 | +23 | try: + | + + diff --git a/ruff.schema.json b/ruff.schema.json index 72e0e76efc3f8..7a2c5330de692 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2952,6 +2952,7 @@ "PLC2", "PLC24", "PLC240", + "PLC2401", "PLC2403", "PLC3", "PLC30",