diff --git a/resources/test/fixtures/pyproject.toml b/resources/test/fixtures/pyproject.toml index 69d119f8bb..f5995de45e 100644 --- a/resources/test/fixtures/pyproject.toml +++ b/resources/test/fixtures/pyproject.toml @@ -14,3 +14,25 @@ inline-quotes = "single" multiline-quotes = "double" docstring-quotes = "double" avoid-escape = true + +[tool.ruff.pep8-naming] +ignore-names = [ + "setUp", + "tearDown", + "setUpClass", + "tearDownClass", + "setUpModule", + "tearDownModule", + "asyncSetUp", + "asyncTearDown", + "setUpTestData", + "failureException", + "longMessage", + "maxDiff", +] +classmethod-decorators = [ + "classmethod", +] +staticmethod-decorators = [ + "staticmethod", +] diff --git a/src/check_ast.rs b/src/check_ast.rs index 66d06707a2..756df1b1a6 100644 --- a/src/check_ast.rs +++ b/src/check_ast.rs @@ -236,7 +236,11 @@ where } if self.settings.enabled.contains(&CheckCode::N802) { - if let Some(check) = pep8_naming::checks::invalid_function_name(stmt, name) { + if let Some(check) = pep8_naming::checks::invalid_function_name( + stmt, + name, + &self.settings.pep8_naming, + ) { self.checks.push(check); } } @@ -247,6 +251,7 @@ where self.current_scope(), decorator_list, args, + &self.settings.pep8_naming, ) { self.checks.push(check); @@ -258,6 +263,7 @@ where self.current_scope(), decorator_list, args, + &self.settings.pep8_naming, ) { self.checks.push(check); } diff --git a/src/flake8_quotes/settings.rs b/src/flake8_quotes/settings.rs index 559e78d59c..29ad1db65f 100644 --- a/src/flake8_quotes/settings.rs +++ b/src/flake8_quotes/settings.rs @@ -1,4 +1,4 @@ -//! Settings for the `flake_quotes` plugin. +//! Settings for the `flake-quotes` plugin. use serde::{Deserialize, Serialize}; @@ -27,12 +27,12 @@ pub struct Settings { } impl Settings { - pub fn from_config(config: Options) -> Self { + pub fn from_options(options: Options) -> Self { Self { - inline_quotes: config.inline_quotes.unwrap_or(Quote::Single), - multiline_quotes: config.multiline_quotes.unwrap_or(Quote::Double), - docstring_quotes: config.docstring_quotes.unwrap_or(Quote::Double), - avoid_escape: config.avoid_escape.unwrap_or(true), + inline_quotes: options.inline_quotes.unwrap_or(Quote::Single), + multiline_quotes: options.multiline_quotes.unwrap_or(Quote::Double), + docstring_quotes: options.docstring_quotes.unwrap_or(Quote::Double), + avoid_escape: options.avoid_escape.unwrap_or(true), } } } diff --git a/src/pep8_naming/checks.rs b/src/pep8_naming/checks.rs index 834dfa4ef8..e2cbf481ff 100644 --- a/src/pep8_naming/checks.rs +++ b/src/pep8_naming/checks.rs @@ -3,7 +3,9 @@ use rustpython_ast::{Arguments, Expr, ExprKind, Stmt}; use crate::ast::types::{FunctionScope, Range, Scope, ScopeKind}; use crate::checks::{Check, CheckKind}; +use crate::pep8_naming::settings::Settings; +/// N801 pub fn invalid_class_name(class_def: &Stmt, name: &str) -> Option { let stripped = name.strip_prefix('_').unwrap_or(name); if !stripped @@ -21,23 +23,14 @@ pub fn invalid_class_name(class_def: &Stmt, name: &str) -> Option { None } -const IGNORE_NAMES: &[&str] = &[ - "setUp", - "tearDown", - "setUpClass", - "tearDownClass", - "setUpModule", - "tearDownModule", - "asyncSetUp", - "asyncTearDown", - "setUpTestData", - "failureException", - "longMessage", - "maxDiff", -]; - -pub fn invalid_function_name(func_def: &Stmt, name: &str) -> Option { - if !is_lower(name) && !IGNORE_NAMES.contains(&name) { +/// N802 +pub fn invalid_function_name(func_def: &Stmt, name: &str, settings: &Settings) -> Option { + if !is_lower(name) + && !settings + .ignore_names + .iter() + .any(|ignore_name| ignore_name == name) + { return Some(Check::new( CheckKind::InvalidFunctionName(name.to_string()), Range::from_located(func_def), @@ -46,6 +39,7 @@ pub fn invalid_function_name(func_def: &Stmt, name: &str) -> Option { None } +/// N803 pub fn invalid_argument_name(location: Range, name: &str) -> Option { if !is_lower(name) { return Some(Check::new( @@ -56,10 +50,12 @@ pub fn invalid_argument_name(location: Range, name: &str) -> Option { None } +/// N804 pub fn invalid_first_argument_name_for_class_method( scope: &Scope, decorator_list: &[Expr], args: &Arguments, + settings: &Settings, ) -> Option { if !matches!(scope.kind, ScopeKind::Class) { return None; @@ -67,7 +63,7 @@ pub fn invalid_first_argument_name_for_class_method( if decorator_list.iter().any(|decorator| { if let ExprKind::Name { id, .. } = &decorator.node { - id == "classmethod" + settings.classmethod_decorators.contains(id) } else { false } @@ -84,10 +80,12 @@ pub fn invalid_first_argument_name_for_class_method( None } +/// N805 pub fn invalid_first_argument_name_for_method( scope: &Scope, decorator_list: &[Expr], args: &Arguments, + settings: &Settings, ) -> Option { if !matches!(scope.kind, ScopeKind::Class) { return None; @@ -95,7 +93,8 @@ pub fn invalid_first_argument_name_for_method( if decorator_list.iter().any(|decorator| { if let ExprKind::Name { id, .. } = &decorator.node { - id == "classmethod" || id == "staticmethod" + settings.classmethod_decorators.contains(id) + || settings.staticmethod_decorators.contains(id) } else { false } @@ -114,6 +113,7 @@ pub fn invalid_first_argument_name_for_method( None } +/// N806 pub fn non_lowercase_variable_in_function(scope: &Scope, expr: &Expr, name: &str) -> Option { if !matches!(scope.kind, ScopeKind::Function(FunctionScope { .. })) { return None; @@ -127,6 +127,83 @@ pub fn non_lowercase_variable_in_function(scope: &Scope, expr: &Expr, name: &str None } +/// N807 +pub fn dunder_function_name(func_def: &Stmt, scope: &Scope, name: &str) -> Option { + if matches!(scope.kind, ScopeKind::Class) { + return None; + } + + if name.starts_with("__") && name.ends_with("__") { + return Some(Check::new( + CheckKind::DunderFunctionName, + Range::from_located(func_def), + )); + } + + None +} + +/// N811 +pub fn constant_imported_as_non_constant( + import_from: &Stmt, + name: &str, + asname: &str, +) -> Option { + if is_upper(name) && !is_upper(asname) { + return Some(Check::new( + CheckKind::ConstantImportedAsNonConstant(name.to_string(), asname.to_string()), + Range::from_located(import_from), + )); + } + None +} + +/// N812 +pub fn lowercase_imported_as_non_lowercase( + import_from: &Stmt, + name: &str, + asname: &str, +) -> Option { + if is_lower(name) && asname.to_lowercase() != asname { + return Some(Check::new( + CheckKind::LowercaseImportedAsNonLowercase(name.to_string(), asname.to_string()), + Range::from_located(import_from), + )); + } + None +} + +/// N813 +pub fn camelcase_imported_as_lowercase( + import_from: &Stmt, + name: &str, + asname: &str, +) -> Option { + if is_camelcase(name) && is_lower(asname) { + return Some(Check::new( + CheckKind::CamelcaseImportedAsLowercase(name.to_string(), asname.to_string()), + Range::from_located(import_from), + )); + } + None +} + +/// N814 +pub fn camelcase_imported_as_constant( + import_from: &Stmt, + name: &str, + asname: &str, +) -> Option { + if is_camelcase(name) && is_upper(asname) && !is_acronym(name, asname) { + return Some(Check::new( + CheckKind::CamelcaseImportedAsConstant(name.to_string(), asname.to_string()), + Range::from_located(import_from), + )); + } + None +} + +/// N815 pub fn mixed_case_variable_in_class_scope(scope: &Scope, expr: &Expr, name: &str) -> Option { if !matches!(scope.kind, ScopeKind::Class) { return None; @@ -140,6 +217,7 @@ pub fn mixed_case_variable_in_class_scope(scope: &Scope, expr: &Expr, name: &str None } +/// N816 pub fn mixed_case_variable_in_global_scope( scope: &Scope, expr: &Expr, @@ -157,18 +235,41 @@ pub fn mixed_case_variable_in_global_scope( None } -pub fn dunder_function_name(func_def: &Stmt, scope: &Scope, name: &str) -> Option { - if matches!(scope.kind, ScopeKind::Class) { - return None; - } - - if name.starts_with("__") && name.ends_with("__") { +/// N817 +pub fn camelcase_imported_as_acronym( + import_from: &Stmt, + name: &str, + asname: &str, +) -> Option { + if is_camelcase(name) && is_upper(asname) && is_acronym(name, asname) { return Some(Check::new( - CheckKind::DunderFunctionName, - Range::from_located(func_def), + CheckKind::CamelcaseImportedAsAcronym(name.to_string(), asname.to_string()), + Range::from_located(import_from), )); } + None +} +/// N818 +pub fn error_suffix_on_exception_name( + class_def: &Stmt, + bases: &[Expr], + name: &str, +) -> Option { + if bases.iter().any(|base| { + if let ExprKind::Name { id, .. } = &base.node { + id == "Exception" + } else { + false + } + }) { + if !name.ends_with("Error") { + return Some(Check::new( + CheckKind::ErrorSuffixOnExceptionName(name.to_string()), + Range::from_located(class_def), + )); + } + } None } @@ -196,34 +297,6 @@ fn is_upper(s: &str) -> bool { cased } -pub fn constant_imported_as_non_constant( - import_from: &Stmt, - name: &str, - asname: &str, -) -> Option { - if is_upper(name) && !is_upper(asname) { - return Some(Check::new( - CheckKind::ConstantImportedAsNonConstant(name.to_string(), asname.to_string()), - Range::from_located(import_from), - )); - } - None -} - -pub fn lowercase_imported_as_non_lowercase( - import_from: &Stmt, - name: &str, - asname: &str, -) -> Option { - if is_lower(name) && asname.to_lowercase() != asname { - return Some(Check::new( - CheckKind::LowercaseImportedAsNonLowercase(name.to_string(), asname.to_string()), - Range::from_located(import_from), - )); - } - None -} - fn is_camelcase(name: &str) -> bool { !is_lower(name) && !is_upper(name) && !name.contains('_') } @@ -242,70 +315,6 @@ fn is_acronym(name: &str, asname: &str) -> bool { name.chars().filter(|c| c.is_uppercase()).join("") == asname } -pub fn camelcase_imported_as_lowercase( - import_from: &Stmt, - name: &str, - asname: &str, -) -> Option { - if is_camelcase(name) && is_lower(asname) { - return Some(Check::new( - CheckKind::CamelcaseImportedAsLowercase(name.to_string(), asname.to_string()), - Range::from_located(import_from), - )); - } - None -} - -pub fn camelcase_imported_as_constant( - import_from: &Stmt, - name: &str, - asname: &str, -) -> Option { - if is_camelcase(name) && is_upper(asname) && !is_acronym(name, asname) { - return Some(Check::new( - CheckKind::CamelcaseImportedAsConstant(name.to_string(), asname.to_string()), - Range::from_located(import_from), - )); - } - None -} - -pub fn camelcase_imported_as_acronym( - import_from: &Stmt, - name: &str, - asname: &str, -) -> Option { - if is_camelcase(name) && is_upper(asname) && is_acronym(name, asname) { - return Some(Check::new( - CheckKind::CamelcaseImportedAsAcronym(name.to_string(), asname.to_string()), - Range::from_located(import_from), - )); - } - None -} - -pub fn error_suffix_on_exception_name( - class_def: &Stmt, - bases: &[Expr], - name: &str, -) -> Option { - if bases.iter().any(|base| { - if let ExprKind::Name { id, .. } = &base.node { - id == "Exception" - } else { - false - } - }) { - if !name.ends_with("Error") { - return Some(Check::new( - CheckKind::ErrorSuffixOnExceptionName(name.to_string()), - Range::from_located(class_def), - )); - } - } - None -} - #[cfg(test)] mod tests { use super::{is_acronym, is_camelcase, is_lower, is_mixed_case, is_upper}; diff --git a/src/pep8_naming/mod.rs b/src/pep8_naming/mod.rs index f6b5329a02..bf79386dbb 100644 --- a/src/pep8_naming/mod.rs +++ b/src/pep8_naming/mod.rs @@ -1 +1,2 @@ pub mod checks; +pub mod settings; diff --git a/src/pep8_naming/settings.rs b/src/pep8_naming/settings.rs new file mode 100644 index 0000000000..2d917e21af --- /dev/null +++ b/src/pep8_naming/settings.rs @@ -0,0 +1,63 @@ +//! Settings for the `pep8-naming` plugin. + +use serde::Deserialize; + +const IGNORE_NAMES: [&str; 12] = [ + "setUp", + "tearDown", + "setUpClass", + "tearDownClass", + "setUpModule", + "tearDownModule", + "asyncSetUp", + "asyncTearDown", + "setUpTestData", + "failureException", + "longMessage", + "maxDiff", +]; + +const CLASSMETHOD_DECORATORS: [&str; 1] = ["classmethod"]; + +const STATICMETHOD_DECORATORS: [&str; 1] = ["staticmethod"]; + +#[derive(Debug, PartialEq, Eq, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +pub struct Options { + pub ignore_names: Option>, + pub classmethod_decorators: Option>, + pub staticmethod_decorators: Option>, +} + +#[derive(Debug)] +pub struct Settings { + pub ignore_names: Vec, + pub classmethod_decorators: Vec, + pub staticmethod_decorators: Vec, +} + +impl Settings { + pub fn from_options(options: Options) -> Self { + Self { + ignore_names: options + .ignore_names + .unwrap_or_else(|| IGNORE_NAMES.map(String::from).to_vec()), + classmethod_decorators: options + .classmethod_decorators + .unwrap_or_else(|| CLASSMETHOD_DECORATORS.map(String::from).to_vec()), + staticmethod_decorators: options + .staticmethod_decorators + .unwrap_or_else(|| STATICMETHOD_DECORATORS.map(String::from).to_vec()), + } + } +} + +impl Default for Settings { + fn default() -> Self { + Self { + ignore_names: IGNORE_NAMES.map(String::from).to_vec(), + classmethod_decorators: CLASSMETHOD_DECORATORS.map(String::from).to_vec(), + staticmethod_decorators: STATICMETHOD_DECORATORS.map(String::from).to_vec(), + } + } +} diff --git a/src/settings/configuration.rs b/src/settings/configuration.rs index 61a8267ecc..15806842ab 100644 --- a/src/settings/configuration.rs +++ b/src/settings/configuration.rs @@ -8,9 +8,9 @@ use once_cell::sync::Lazy; use regex::Regex; use crate::checks_gen::CheckCodePrefix; -use crate::flake8_quotes; use crate::settings::pyproject::load_options; use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion}; +use crate::{flake8_quotes, pep8_naming}; #[derive(Debug)] pub struct Configuration { @@ -26,6 +26,7 @@ pub struct Configuration { pub target_version: PythonVersion, // Plugins pub flake8_quotes: flake8_quotes::settings::Settings, + pub pep8_naming: pep8_naming::settings::Settings, } static DEFAULT_EXCLUDE: Lazy> = Lazy::new(|| { @@ -98,7 +99,11 @@ impl Configuration { // Plugins flake8_quotes: options .flake8_quotes - .map(flake8_quotes::settings::Settings::from_config) + .map(flake8_quotes::settings::Settings::from_options) + .unwrap_or_default(), + pep8_naming: options + .pep8_naming + .map(pep8_naming::settings::Settings::from_options) .unwrap_or_default(), }) } diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 19e6d978ea..b124fb3bd3 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -8,9 +8,9 @@ use regex::Regex; use crate::checks::CheckCode; use crate::checks_gen::{CheckCodePrefix, PrefixSpecificity}; -use crate::flake8_quotes; use crate::settings::configuration::Configuration; use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion}; +use crate::{flake8_quotes, pep8_naming}; pub mod configuration; pub mod options; @@ -29,6 +29,7 @@ pub struct Settings { pub target_version: PythonVersion, // Plugins pub flake8_quotes: flake8_quotes::settings::Settings, + pub pep8_naming: pep8_naming::settings::Settings, } impl Settings { @@ -45,6 +46,7 @@ impl Settings { extend_exclude: config.extend_exclude, flake8_quotes: config.flake8_quotes, line_length: config.line_length, + pep8_naming: config.pep8_naming, per_file_ignores: config.per_file_ignores, target_version: config.target_version, } @@ -60,6 +62,7 @@ impl Settings { per_file_ignores: vec![], target_version: PythonVersion::Py310, flake8_quotes: Default::default(), + pep8_naming: Default::default(), } } @@ -73,6 +76,7 @@ impl Settings { per_file_ignores: vec![], target_version: PythonVersion::Py310, flake8_quotes: Default::default(), + pep8_naming: Default::default(), } } } diff --git a/src/settings/options.rs b/src/settings/options.rs index 0a5834c6c0..0c14bdddcc 100644 --- a/src/settings/options.rs +++ b/src/settings/options.rs @@ -3,8 +3,8 @@ use serde::Deserialize; use crate::checks_gen::CheckCodePrefix; -use crate::flake8_quotes; use crate::settings::types::{PythonVersion, StrCheckCodePair}; +use crate::{flake8_quotes, pep8_naming}; #[derive(Debug, PartialEq, Eq, Deserialize, Default)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] @@ -24,5 +24,7 @@ pub struct Options { pub per_file_ignores: Vec, pub dummy_variable_rgx: Option, pub target_version: Option, + // Plugins pub flake8_quotes: Option, + pub pep8_naming: Option, } diff --git a/src/settings/pyproject.rs b/src/settings/pyproject.rs index 68e33616f8..108bf22d0f 100644 --- a/src/settings/pyproject.rs +++ b/src/settings/pyproject.rs @@ -94,12 +94,12 @@ mod tests { use anyhow::Result; use crate::checks_gen::CheckCodePrefix; - use crate::flake8_quotes; use crate::flake8_quotes::settings::Quote; use crate::settings::pyproject::{ find_project_root, find_pyproject_toml, parse_pyproject_toml, Options, Pyproject, Tools, }; use crate::settings::types::StrCheckCodePair; + use crate::{flake8_quotes, pep8_naming}; #[test] fn deserialize() -> Result<()> { @@ -133,7 +133,8 @@ mod tests { per_file_ignores: vec![], dummy_variable_rgx: None, target_version: None, - flake8_quotes: None + flake8_quotes: None, + pep8_naming: None, }) }) ); @@ -159,7 +160,8 @@ line-length = 79 per_file_ignores: vec![], dummy_variable_rgx: None, target_version: None, - flake8_quotes: None + flake8_quotes: None, + pep8_naming: None, }) }) ); @@ -185,7 +187,8 @@ exclude = ["foo.py"] per_file_ignores: vec![], dummy_variable_rgx: None, target_version: None, - flake8_quotes: None + flake8_quotes: None, + pep8_naming: None, }) }) ); @@ -211,7 +214,8 @@ select = ["E501"] per_file_ignores: vec![], dummy_variable_rgx: None, target_version: None, - flake8_quotes: None + flake8_quotes: None, + pep8_naming: None, }) }) ); @@ -238,7 +242,8 @@ ignore = ["E501"] per_file_ignores: vec![], dummy_variable_rgx: None, target_version: None, - flake8_quotes: None + flake8_quotes: None, + pep8_naming: None, }) }) ); @@ -316,7 +321,25 @@ other-attribute = 1 multiline_quotes: Some(Quote::Double), docstring_quotes: Some(Quote::Double), avoid_escape: Some(true), - }) + }), + pep8_naming: Some(pep8_naming::settings::Options { + ignore_names: Some(vec![ + "setUp".to_string(), + "tearDown".to_string(), + "setUpClass".to_string(), + "tearDownClass".to_string(), + "setUpModule".to_string(), + "tearDownModule".to_string(), + "asyncSetUp".to_string(), + "asyncTearDown".to_string(), + "setUpTestData".to_string(), + "failureException".to_string(), + "longMessage".to_string(), + "maxDiff".to_string(), + ]), + classmethod_decorators: Some(vec!["classmethod".to_string()]), + staticmethod_decorators: Some(vec!["staticmethod".to_string()]), + }), } ); diff --git a/src/settings/user.rs b/src/settings/user.rs index 5c11f45fa7..d6f7f6fdc8 100644 --- a/src/settings/user.rs +++ b/src/settings/user.rs @@ -6,7 +6,7 @@ use regex::Regex; use crate::checks_gen::CheckCodePrefix; use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion}; -use crate::{flake8_quotes, Configuration}; +use crate::{flake8_quotes, pep8_naming, Configuration}; /// Struct to render user-facing exclusion patterns. #[derive(Debug)] @@ -46,6 +46,7 @@ pub struct UserConfiguration { pub target_version: PythonVersion, // Plugins pub flake8_quotes: flake8_quotes::settings::Settings, + pub pep8_naming: pep8_naming::settings::Settings, // Non-settings exposed to the user pub project_root: Option, pub pyproject: Option, @@ -53,30 +54,31 @@ pub struct UserConfiguration { impl UserConfiguration { pub fn from_configuration( - settings: Configuration, + configuration: Configuration, project_root: Option, pyproject: Option, ) -> Self { Self { - dummy_variable_rgx: settings.dummy_variable_rgx, - exclude: settings + dummy_variable_rgx: configuration.dummy_variable_rgx, + exclude: configuration .exclude .into_iter() .map(Exclusion::from_file_pattern) .collect(), - extend_exclude: settings + extend_exclude: configuration .extend_exclude .into_iter() .map(Exclusion::from_file_pattern) .collect(), - extend_ignore: settings.extend_ignore, - extend_select: settings.extend_select, - ignore: settings.ignore, - line_length: settings.line_length, - per_file_ignores: settings.per_file_ignores, - select: settings.select, - target_version: settings.target_version, - flake8_quotes: settings.flake8_quotes, + extend_ignore: configuration.extend_ignore, + extend_select: configuration.extend_select, + ignore: configuration.ignore, + line_length: configuration.line_length, + per_file_ignores: configuration.per_file_ignores, + select: configuration.select, + target_version: configuration.target_version, + flake8_quotes: configuration.flake8_quotes, + pep8_naming: configuration.pep8_naming, project_root, pyproject, }