diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/non_ascii_import_name.py b/crates/ruff_linter/resources/test/fixtures/pylint/non_ascii_import_name.py new file mode 100644 index 00000000000000..a910449cce0665 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pylint/non_ascii_import_name.py @@ -0,0 +1,5 @@ +from os.path import join as los # Ok +from os.path import join as łos # Error + +import os.path.join as łos # Error +import os.path.join as los # Ok diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index bf5313b981e004..69d4829f0b4ec5 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -545,6 +545,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { for alias in names { if let Some(asname) = &alias.asname { + if checker.enabled(Rule::NonAsciiImportName) { + pylint::rules::non_ascii_import_name(checker, asname); + } if checker.enabled(Rule::BuiltinVariableShadowing) { flake8_builtins::rules::builtin_variable_shadowing( checker, @@ -716,6 +719,13 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { } } } + if checker.enabled(Rule::NonAsciiImportName) { + for name in names { + if let Some(asname) = name.asname.as_ref() { + pylint::rules::non_ascii_import_name(checker, asname); + } + } + } if checker.enabled(Rule::DeprecatedMockImport) { pyupgrade::rules::deprecated_mock_import(checker, stmt); } diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 6835e7e725a98a..008135f48f8c0e 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, "C2403") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiImportName), #[allow(deprecated)] (Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString), (Pylint, "C3002") => (RuleGroup::Stable, rules::pylint::rules::UnnecessaryDirectLambdaCall), diff --git a/crates/ruff_linter/src/rules/pylint/mod.rs b/crates/ruff_linter/src/rules/pylint/mod.rs index d17a662cbdb4ff..90a835c2a8f21a 100644 --- a/crates/ruff_linter/src/rules/pylint/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/mod.rs @@ -138,6 +138,7 @@ mod tests { #[test_case(Rule::NoSelfUse, Path::new("no_self_use.py"))] #[test_case(Rule::MisplacedBareRaise, Path::new("misplaced_bare_raise.py"))] #[test_case(Rule::LiteralMembership, Path::new("literal_membership.py"))] + #[test_case(Rule::NonAsciiImportName, Path::new("non_ascii_import_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 c4d7bd831b8ed0..01269692477bba 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/mod.rs @@ -33,6 +33,7 @@ pub(crate) use misplaced_bare_raise::*; pub(crate) use named_expr_without_context::*; pub(crate) use nested_min_max::*; pub(crate) use no_self_use::*; +pub(crate) use non_ascii_import_name::*; pub(crate) use nonlocal_without_binding::*; pub(crate) use property_with_parameters::*; pub(crate) use redefined_loop_name::*; @@ -97,6 +98,7 @@ mod misplaced_bare_raise; mod named_expr_without_context; mod nested_min_max; mod no_self_use; +mod non_ascii_import_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_import_name.rs b/crates/ruff_linter/src/rules/pylint/rules/non_ascii_import_name.rs new file mode 100644 index 00000000000000..8d7f0e014cb3e6 --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/rules/non_ascii_import_name.rs @@ -0,0 +1,38 @@ +use ast::Identifier; +use ruff_python_ast::{self as ast}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_text_size::Ranged; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for the use of non-ASCII characters in import symbol names. +/// +/// ## Why is this bad? +/// Pylint discourages the use of non-ASCII characters in symbol names as +/// they can cause confusion and compatibility issues. +/// +/// ## References +/// - [PEP 672](https://peps.python.org/pep-0672/) +#[violation] +pub struct NonAsciiImportName; + +impl Violation for NonAsciiImportName { + #[derive_message_formats] + fn message(&self) -> String { + format!("Symbol name contains a non-ASCII character, consider renaming it.") + } +} + +/// PLC2403 +pub(crate) fn non_ascii_import_name(checker: &mut Checker, target: &Identifier) { + if target.to_string().is_ascii() { + return; + }; + + checker + .diagnostics + .push(Diagnostic::new(NonAsciiImportName, target.range())); +} diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2403_non_ascii_import_name.py.snap.new b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2403_non_ascii_import_name.py.snap.new new file mode 100644 index 00000000000000..8de097d03e3a54 --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2403_non_ascii_import_name.py.snap.new @@ -0,0 +1,5 @@ +--- +source: crates/ruff_linter/src/rules/pylint/mod.rs +assertion_line: 148 +--- + diff --git a/ruff.schema.json b/ruff.schema.json index 7d676b8f99ea24..68d094f45ebbec 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2906,6 +2906,10 @@ "PLC19", "PLC190", "PLC1901", + "PLC2", + "PLC24", + "PLC240", + "PLC2403", "PLC3", "PLC30", "PLC300",