diff --git a/LICENSE b/LICENSE index d7dbadbd38..f328ba5db2 100644 --- a/LICENSE +++ b/LICENSE @@ -1030,3 +1030,8 @@ are: OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + +- flake8-self, licensed as follows: + """ + Freely Distributable + """ diff --git a/README.md b/README.md index 7e94906b0c..f18fe64b8b 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,7 @@ This README is also available as [documentation](https://beta.ruff.rs/docs/). 1. [Pylint (PL)](#pylint-pl) 1. [tryceratops (TRY)](#tryceratops-try) 1. [flake8-raise (RSE)](#flake8-raise-rse) + 1. [flake8-self (SLF)](#flake8-self-slf) 1. [Ruff-specific rules (RUF)](#ruff-specific-rules-ruf) 1. [Editor Integrations](#editor-integrations) 1. [FAQ](#faq) @@ -1368,6 +1369,14 @@ For more, see [flake8-raise](https://pypi.org/project/flake8-raise/) on PyPI. | ---- | ---- | ------- | --- | | RSE102 | unnecessary-paren-on-raise-exception | Unnecessary parentheses on raised exception | | +### flake8-self (SLF) + +For more, see [flake8-self](https://pypi.org/project/flake8-self/) on PyPI. + +| Code | Name | Message | Fix | +| ---- | ---- | ------- | --- | +| SLF001 | private-member-access | Private member accessed: `{access}` | | + ### Ruff-specific rules (RUF) | Code | Name | Message | Fix | @@ -1661,6 +1670,7 @@ natively, including: - [flake8-quotes](https://pypi.org/project/flake8-quotes/) - [flake8-raise](https://pypi.org/project/flake8-raise/) - [flake8-return](https://pypi.org/project/flake8-return/) +- [flake8-self](https://pypi.org/project/flake8-self/) - [flake8-simplify](https://pypi.org/project/flake8-simplify/) ([#998](https://github.com/charliermarsh/ruff/issues/998)) - [flake8-super](https://pypi.org/project/flake8-super/) - [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/) @@ -1750,6 +1760,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl - [flake8-quotes](https://pypi.org/project/flake8-quotes/) - [flake8-raise](https://pypi.org/project/flake8-raise/) - [flake8-return](https://pypi.org/project/flake8-return/) +- [flake8-self](https://pypi.org/project/flake8-self/) - [flake8-simplify](https://pypi.org/project/flake8-simplify/) ([#998](https://github.com/charliermarsh/ruff/issues/998)) - [flake8-super](https://pypi.org/project/flake8-super/) - [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/) diff --git a/resources/test/fixtures/flake8_self/SLF001.py b/resources/test/fixtures/flake8_self/SLF001.py new file mode 100644 index 0000000000..2e1147acc6 --- /dev/null +++ b/resources/test/fixtures/flake8_self/SLF001.py @@ -0,0 +1,31 @@ +class Foo: + + def __init__(self): + self.public_thing = "foo" + self._private_thing = "bar" + self.__really_private_thing = "baz" + + def __str__(self): + return "foo" + + def public_func(self): + pass + + def _private_func(self): + pass + + def __really_private_func(self, arg): + pass + + +foo = Foo() + +print(foo.public_thing) +print(foo.public_func()) +print(foo.__dict__) +print(foo.__str__()) + +print(foo._private_thing) # SLF001 +print(foo.__really_private_thing) # SLF001 +print(foo._private_func()) # SLF001 +print(foo.__really_private_func(1)) # SLF001 diff --git a/ruff.schema.json b/ruff.schema.json index ed9569277c..6c2e10fb4f 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1828,6 +1828,10 @@ "SIM4", "SIM40", "SIM401", + "SLF", + "SLF0", + "SLF00", + "SLF001", "T", "T1", "T10", diff --git a/src/checkers/ast.rs b/src/checkers/ast.rs index a80f8790e2..9f35777d78 100644 --- a/src/checkers/ast.rs +++ b/src/checkers/ast.rs @@ -36,10 +36,10 @@ 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_errmsg, flake8_implicit_str_concat, flake8_import_conventions, flake8_logging_format, - flake8_pie, flake8_print, flake8_pytest_style, flake8_raise, flake8_return, flake8_simplify, - flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, flake8_use_pathlib, mccabe, - pandas_vet, pep8_naming, pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, - ruff, tryceratops, + flake8_pie, flake8_print, flake8_pytest_style, flake8_raise, flake8_return, flake8_self, + flake8_simplify, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, + flake8_use_pathlib, mccabe, pandas_vet, pep8_naming, pycodestyle, pydocstyle, pyflakes, + pygrep_hooks, pylint, pyupgrade, ruff, tryceratops, }; use crate::settings::types::PythonVersion; use crate::settings::{flags, Settings}; @@ -2148,6 +2148,9 @@ where if self.settings.rules.enabled(&Rule::BannedApi) { flake8_tidy_imports::banned_api::banned_attribute_access(self, expr); } + if self.settings.rules.enabled(&Rule::PrivateMemberAccess) { + flake8_self::rules::private_member_access(self, expr); + } pandas_vet::rules::check_attr(self, attr, value, expr); } ExprKind::Call { diff --git a/src/registry.rs b/src/registry.rs index 3fdf74c4dd..9b3c8ef0ae 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -484,6 +484,8 @@ ruff_macros::define_rule_mapping!( G202 => rules::flake8_logging_format::violations::LoggingRedundantExcInfo, // flake8-raise RSE102 => rules::flake8_raise::rules::UnnecessaryParenOnRaiseException, + // flake8-self + SLF001 => rules::flake8_self::rules::PrivateMemberAccess, // ruff RUF001 => violations::AmbiguousUnicodeCharacterString, RUF002 => violations::AmbiguousUnicodeCharacterDocstring, @@ -616,6 +618,9 @@ pub enum Linter { /// [flake8-raise](https://pypi.org/project/flake8-raise/) #[prefix = "RSE"] Flake8Raise, + /// [flake8-self](https://pypi.org/project/flake8-self/) + #[prefix = "SLF"] + Flake8Self, /// Ruff-specific rules #[prefix = "RUF"] Ruff, diff --git a/src/rules/flake8_self/mod.rs b/src/rules/flake8_self/mod.rs new file mode 100644 index 0000000000..d5099b6fd6 --- /dev/null +++ b/src/rules/flake8_self/mod.rs @@ -0,0 +1,26 @@ +//! Rules from [flake8-self](https://pypi.org/project/flake8-self/). +pub(crate) mod rules; + +#[cfg(test)] +mod tests { + use std::convert::AsRef; + use std::path::Path; + + use anyhow::Result; + use test_case::test_case; + + use crate::registry::Rule; + use crate::test::test_path; + use crate::{assert_yaml_snapshot, settings}; + + #[test_case(Rule::PrivateMemberAccess, Path::new("SLF001.py"); "SLF001")] + 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_self").join(path).as_path(), + &settings::Settings::for_rule(rule_code), + )?; + assert_yaml_snapshot!(snapshot, diagnostics); + Ok(()) + } +} diff --git a/src/rules/flake8_self/rules/mod.rs b/src/rules/flake8_self/rules/mod.rs new file mode 100644 index 0000000000..6eca540bf2 --- /dev/null +++ b/src/rules/flake8_self/rules/mod.rs @@ -0,0 +1,3 @@ +pub use private_member_access::{private_member_access, PrivateMemberAccess}; + +mod private_member_access; diff --git a/src/rules/flake8_self/rules/private_member_access.rs b/src/rules/flake8_self/rules/private_member_access.rs new file mode 100644 index 0000000000..3d943f0675 --- /dev/null +++ b/src/rules/flake8_self/rules/private_member_access.rs @@ -0,0 +1,43 @@ +use ruff_macros::derive_message_formats; + +use crate::ast::types::Range; +use crate::checkers::ast::Checker; +use crate::define_violation; +use crate::registry::Diagnostic; +use crate::violation::Violation; +use rustpython_ast::{Expr, ExprKind}; + +define_violation!( + pub struct PrivateMemberAccess { + pub access: String, + } +); +impl Violation for PrivateMemberAccess { + #[derive_message_formats] + fn message(&self) -> String { + let PrivateMemberAccess { access } = self; + format!("Private member accessed: `{access}`") + } +} + +const VALID_IDS: [&str; 3] = ["self", "cls", "mcs"]; + +/// SLF001 +pub fn private_member_access(checker: &mut Checker, expr: &Expr) { + if let ExprKind::Attribute { value, attr, .. } = &expr.node { + if !attr.ends_with("__") && (attr.starts_with('_') || attr.starts_with("__")) { + let ExprKind::Name { id, .. } = &value.node else { + return; + }; + + if !VALID_IDS.contains(&id.as_str()) { + checker.diagnostics.push(Diagnostic::new( + PrivateMemberAccess { + access: format!("{}.{}", id, attr), + }, + Range::from_located(expr), + )); + } + } + } +} diff --git a/src/rules/flake8_self/snapshots/ruff__rules__flake8_self__tests__private-member-access_SLF001.py.snap b/src/rules/flake8_self/snapshots/ruff__rules__flake8_self__tests__private-member-access_SLF001.py.snap new file mode 100644 index 0000000000..b48aa6075d --- /dev/null +++ b/src/rules/flake8_self/snapshots/ruff__rules__flake8_self__tests__private-member-access_SLF001.py.snap @@ -0,0 +1,45 @@ +--- +source: src/rules/flake8_self/mod.rs +expression: diagnostics +--- +- kind: + PrivateMemberAccess: ~ + location: + row: 28 + column: 6 + end_location: + row: 28 + column: 24 + fix: ~ + parent: ~ +- kind: + PrivateMemberAccess: ~ + location: + row: 29 + column: 6 + end_location: + row: 29 + column: 32 + fix: ~ + parent: ~ +- kind: + PrivateMemberAccess: ~ + location: + row: 30 + column: 6 + end_location: + row: 30 + column: 23 + fix: ~ + parent: ~ +- kind: + PrivateMemberAccess: ~ + location: + row: 31 + column: 6 + end_location: + row: 31 + column: 31 + fix: ~ + parent: ~ + diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 38ae33a825..2784888c14 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -23,6 +23,7 @@ pub mod flake8_pytest_style; pub mod flake8_quotes; pub mod flake8_raise; pub mod flake8_return; +pub mod flake8_self; pub mod flake8_simplify; pub mod flake8_tidy_imports; pub mod flake8_type_checking;