diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/0001_initial.py b/crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/0001_initial.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/__main__.py b/crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/__main__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/__setup__.py b/crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/__setup__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/ruff/src/rules/flake8_tidy_imports/relative_imports.rs b/crates/ruff/src/rules/flake8_tidy_imports/relative_imports.rs index 1d53eb93fe..7d7b0b12dd 100644 --- a/crates/ruff/src/rules/flake8_tidy_imports/relative_imports.rs +++ b/crates/ruff/src/rules/flake8_tidy_imports/relative_imports.rs @@ -3,7 +3,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use ruff_macros::{define_violation, derive_message_formats}; -use ruff_python::string::is_lower_with_underscore; +use ruff_python::identifiers::is_module_name; use crate::ast::helpers::{create_stmt, from_relative_import, unparse_stmt}; use crate::ast::types::Range; @@ -103,7 +103,7 @@ fn fix_banned_relative_import( let call_path = from_relative_import(&parts, module); // Require import to be a valid PEP 8 module: // https://python.org/dev/peps/pep-0008/#package-and-module-names - if !call_path.iter().all(|part| is_lower_with_underscore(part)) { + if !call_path.iter().all(|part| is_module_name(part)) { return None; } call_path.as_slice().join(".") @@ -112,14 +112,14 @@ fn fix_banned_relative_import( let call_path = from_relative_import(&parts, module); // Require import to be a valid PEP 8 module: // https://python.org/dev/peps/pep-0008/#package-and-module-names - if !call_path.iter().all(|part| is_lower_with_underscore(part)) { + if !call_path.iter().all(|part| is_module_name(part)) { return None; } call_path.as_slice().join(".") } else { // Require import to be a valid PEP 8 module: // https://python.org/dev/peps/pep-0008/#package-and-module-names - if !parts.iter().all(|part| is_lower_with_underscore(part)) { + if !parts.iter().all(|part| is_module_name(part)) { return None; } parts.join(".") diff --git a/crates/ruff/src/rules/pep8_naming/mod.rs b/crates/ruff/src/rules/pep8_naming/mod.rs index 3e10cad7a3..0e62ff6b59 100644 --- a/crates/ruff/src/rules/pep8_naming/mod.rs +++ b/crates/ruff/src/rules/pep8_naming/mod.rs @@ -39,6 +39,9 @@ mod tests { #[test_case(Rule::InvalidModuleName, Path::new("N999/module/valid_name/__init__.py"); "N999_7")] #[test_case(Rule::InvalidModuleName, Path::new("N999/module/no_module/test.txt"); "N999_8")] #[test_case(Rule::InvalidModuleName, Path::new("N999/module/valid_name/file-with-dashes.py"); "N999_9")] + #[test_case(Rule::InvalidModuleName, Path::new("N999/module/valid_name/__main__.py"); "N999_10")] + #[test_case(Rule::InvalidModuleName, Path::new("N999/module/valid_name/0001_initial.py"); "N999_11")] + #[test_case(Rule::InvalidModuleName, Path::new("N999/module/valid_name/__setup__.py"); "N999_12")] 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/src/rules/pep8_naming/rules/invalid_module_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_module_name.rs index b4a5777add..499ab17c11 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_module_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_module_name.rs @@ -1,7 +1,7 @@ use std::path::Path; use ruff_macros::{define_violation, derive_message_formats}; -use ruff_python::string::is_lower_with_underscore; +use ruff_python::identifiers::is_module_name; use crate::ast::types::Range; use crate::registry::Diagnostic; @@ -44,13 +44,18 @@ impl Violation for InvalidModuleName { /// N999 pub fn invalid_module_name(path: &Path, package: Option<&Path>) -> Option { if let Some(package) = package { - let module_name = if path.file_name().unwrap().to_string_lossy() == "__init__.py" { + let module_name = if path.file_name().map_or(false, |file_name| { + file_name == "__init__.py" + || file_name == "__init__.pyi" + || file_name == "__main__.py" + || file_name == "__main__.pyi" + }) { package.file_name().unwrap().to_string_lossy() } else { path.file_stem().unwrap().to_string_lossy() }; - if !is_lower_with_underscore(&module_name) { + if !is_module_name(&module_name) { return Some(Diagnostic::new( InvalidModuleName { name: module_name.to_string(), diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__N999_N999__module__valid_name__0001_initial.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__N999_N999__module__valid_name__0001_initial.py.snap new file mode 100644 index 0000000000..b0a1ebbaaf --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__N999_N999__module__valid_name__0001_initial.py.snap @@ -0,0 +1,6 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +expression: diagnostics +--- +[] + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__N999_N999__module__valid_name____main__.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__N999_N999__module__valid_name____main__.py.snap new file mode 100644 index 0000000000..b0a1ebbaaf --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__N999_N999__module__valid_name____main__.py.snap @@ -0,0 +1,6 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +expression: diagnostics +--- +[] + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__N999_N999__module__valid_name____setup__.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__N999_N999__module__valid_name____setup__.py.snap new file mode 100644 index 0000000000..b0a1ebbaaf --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__N999_N999__module__valid_name____setup__.py.snap @@ -0,0 +1,6 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +expression: diagnostics +--- +[] + diff --git a/crates/ruff_python/src/identifiers.rs b/crates/ruff_python/src/identifiers.rs index ff1ef5e70c..c58be92325 100644 --- a/crates/ruff_python/src/identifiers.rs +++ b/crates/ruff_python/src/identifiers.rs @@ -22,3 +22,28 @@ pub fn is_identifier(s: &str) -> bool { pub fn is_mangled_private(id: &str) -> bool { id.starts_with("__") && !id.ends_with("__") } + +/// Returns `true` if a string is a PEP 8-compliant module name (i.e., consists of lowercase +/// letters, numbers, and underscores). +pub fn is_module_name(s: &str) -> bool { + s.chars() + .all(|c| c.is_lowercase() || c.is_numeric() || c == '_') +} + +#[cfg(test)] +mod tests { + use crate::identifiers::is_module_name; + + #[test] + fn test_is_module_name() { + assert!(is_module_name("a")); + assert!(is_module_name("abc")); + assert!(is_module_name("abc0")); + assert!(is_module_name("abc_")); + assert!(is_module_name("a_b_c")); + assert!(is_module_name("0abc")); + assert!(is_module_name("_abc")); + assert!(!is_module_name("a-b-c")); + assert!(!is_module_name("a_B_c")); + } +} diff --git a/crates/ruff_python/src/string.rs b/crates/ruff_python/src/string.rs index b41a467e14..16499f2182 100644 --- a/crates/ruff_python/src/string.rs +++ b/crates/ruff_python/src/string.rs @@ -3,8 +3,6 @@ use regex::Regex; pub static STRING_QUOTE_PREFIX_REGEX: Lazy = Lazy::new(|| Regex::new(r#"^(?i)[urb]*['"](?P.*)['"]$"#).unwrap()); -pub static LOWER_OR_UNDERSCORE: Lazy = - Lazy::new(|| Regex::new(r"^[a-z][a-z0-9_]*$").unwrap()); pub fn is_lower(s: &str) -> bool { let mut cased = false; @@ -30,11 +28,6 @@ pub fn is_upper(s: &str) -> bool { cased } -// Module names should be lowercase, and may contain underscore -pub fn is_lower_with_underscore(s: &str) -> bool { - LOWER_OR_UNDERSCORE.is_match(s) -} - /// Remove prefixes (u, r, b) and quotes around a string. This expects the given /// string to be a valid Python string representation, it doesn't do any /// validation. @@ -50,7 +43,7 @@ pub fn strip_quotes_and_prefixes(s: &str) -> &str { #[cfg(test)] mod tests { - use crate::string::{is_lower, is_lower_with_underscore, is_upper, strip_quotes_and_prefixes}; + use crate::string::{is_lower, is_upper, strip_quotes_and_prefixes}; #[test] fn test_is_lower() { @@ -63,19 +56,6 @@ mod tests { assert!(!is_lower("_")); } - #[test] - fn test_is_lower_underscore() { - assert!(is_lower_with_underscore("a")); - assert!(is_lower_with_underscore("abc")); - assert!(is_lower_with_underscore("abc0")); - assert!(is_lower_with_underscore("abc_")); - assert!(is_lower_with_underscore("a_b_c")); - assert!(!is_lower_with_underscore("a-b-c")); - assert!(!is_lower_with_underscore("a_B_c")); - assert!(!is_lower_with_underscore("0abc")); - assert!(!is_lower_with_underscore("_abc")); - } - #[test] fn test_is_upper() { assert!(is_upper("ABC"));