Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/wps_light/WPS604.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# all_class_statements.py

import math # Import statements can appear at the top of a file or within a class

class ExampleClass:
"""A class demonstrating all kinds of statements."""

# Class attributes (class variables)
class_var1 = "class-level attribute"
class_var2 = 42

# Instance attributes will be defined in the constructor
def __init__(self, instance_var):
"""Constructor to initialize instance attributes."""
self.instance_var = instance_var # Instance variable
self.computed_var = self._helper_method() # Using a helper method in initialization

# Instance method
def instance_method(self):
"""Regular instance method."""
print("This is an instance method.")
self._private_method()

# Private method (conventionally prefixed with an underscore)
def _private_method(self):
"""A private helper method."""
print("This is a private method.")

# Protected method (by convention, a single leading underscore indicates protected)
def _protected_method(self):
print("This is a protected method.")

# Static method
@staticmethod
def static_method():
"""A static method."""
print("Static method called. No access to instance or class data.")

# Class method
@classmethod
def class_method(cls):
"""A class method."""
print(f"Class method called. class_var1 = {cls.class_var1}")

# Special method (dunder methods)
def __str__(self):
"""Special method for string representation."""
return f"ExampleClass(instance_var={self.instance_var}, computed_var={self.computed_var})"

def __len__(self):
"""Special method to define the behavior of len()."""
return len(self.instance_var)

# Nested class
class NestedClass:
"""A class defined within another class."""
def nested_method(self):
print("Method of a nested class.")

# Pass statement (used as a placeholder)
def placeholder_method(self):
"""A method with no implementation yet."""
pass

# Try/Except block inside a method
def error_handling_method(self):
"""A method with error handling."""
try:
result = 10 / 0 # Intentional error
except ZeroDivisionError as e:
print(f"Caught an exception: {e}")
finally:
print("Cleanup actions can be placed here.")

# Using a decorator
@property
def readonly_property(self):
"""A read-only property."""
return self.computed_var

@readonly_property.setter
def readonly_property(self, value):
"""Attempt to set this property raises an error."""
raise AttributeError("This property is read-only.")

# Conditional logic inside the class (not recommended but valid)
if math.pi > 3:
def conditionally_defined_method(self):
print("This method exists because math.pi > 3.")

# For loop inside the class (unusual but valid)
for i in range(3):
exec(f"def loop_method_{i}(self): print('Loop-defined method {i}')")

# Docstrings for the class
"""
Additional class-level comments can be included in the docstring.
"""

# List comprehensions inside a class (valid but rarely used)
squares = [x ** 2 for x in range(5)]

# Lambda functions inside a class (unusual but valid)
double = lambda x: x * 2


g = 'module attribute (module-global variable)'
"""This is g's docstring."""

class AClass:

c = 'class attribute'
"""This is AClass.c's docstring."""

def __init__(self):
"""Method __init__'s docstring."""

self.i = 'instance attribute'
"""This is self.i's docstring."""

def f(x):
"""Function f's docstring."""
return x**2

f.a = 1
"""Function attribute f.a's docstring."""
4 changes: 4 additions & 0 deletions crates/ruff_linter/src/checkers/ast/analyze/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::rules::{
flake8_pie, flake8_pyi, flake8_pytest_style, flake8_raise, flake8_return, flake8_simplify,
flake8_slots, flake8_tidy_imports, flake8_type_checking, mccabe, pandas_vet, pep8_naming,
perflint, pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff, tryceratops,
wps_light,
};
use crate::settings::types::PythonVersion;

Expand Down Expand Up @@ -552,6 +553,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::SubclassBuiltin) {
refurb::rules::subclass_builtin(checker, class_def);
}
if checker.enabled(Rule::WrongClassBodyContent) {
wps_light::rules::wrong_class_body_content(checker, class_def);
}
}
Stmt::Import(ast::StmtImport { names, range: _ }) => {
if checker.enabled(Rule::MultipleImportsOnOneLine) {
Expand Down
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pydoclint, "501") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingException),
(Pydoclint, "502") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousException),

// wps-light
(WpsLight, "604") => (RuleGroup::Preview, rules::wps_light::rules::WrongClassBodyContent),

// ruff
(Ruff, "001") => (RuleGroup::Stable, rules::ruff::rules::AmbiguousUnicodeCharacterString),
(Ruff, "002") => (RuleGroup::Stable, rules::ruff::rules::AmbiguousUnicodeCharacterDocstring),
Expand Down
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,9 @@ pub enum Linter {
/// [pydoclint](https://pypi.org/project/pydoclint/)
#[prefix = "DOC"]
Pydoclint,
/// [wps-light](https://pypi.org/project/wps-light/)
#[prefix = "WPS"]
WpsLight,
/// Ruff-specific rules
#[prefix = "RUF"]
Ruff,
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ pub mod pyupgrade;
pub mod refurb;
pub mod ruff;
pub mod tryceratops;
pub mod wps_light;
26 changes: 26 additions & 0 deletions crates/ruff_linter/src/rules/wps_light/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//! Rules from [wps-light](https://pypi.org/project/wps-light/).
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_messages, settings};

#[test_case(Rule::WrongClassBodyContent, Path::new("WPS604.py"))]
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("wps_light").join(path).as_path(),
&settings::LinterSettings::for_rule(rule_code),
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
}
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/rules/wps_light/rules/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub(crate) use wrong_class_body_content::*;

mod wrong_class_body_content;
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{helpers::is_docstring_stmt, Stmt, StmtClassDef};
use ruff_text_size::Ranged;

use crate::checkers::ast::Checker;

/// ## What it does
/// Checks for disallowed statements in the body of a class.
///
/// ## Why is this bad?
/// Python allows us to have conditions, context managers,
/// and even infinite loops inside class definitions.
/// On the other hand, only methods, attributes, and docstrings make sense.
/// So, we discourage using anything except these nodes in class bodies.
///
/// ## Example
/// ```python
/// class Test:
/// for _ in range(10):
/// print("What?!")
/// ```
#[violation]
pub struct WrongClassBodyContent;

impl Violation for WrongClassBodyContent {
#[derive_message_formats]
fn message(&self) -> String {
"Wrong statement inside class definition".to_string()
}
}

/// WPS604
pub(crate) fn wrong_class_body_content(checker: &mut Checker, class: &StmtClassDef) {
let StmtClassDef { body, .. } = class;
for stmt in body {
if !is_docstring_stmt(stmt) && !is_allowed_statement(stmt) {
checker
.diagnostics
.push(Diagnostic::new(WrongClassBodyContent, stmt.range()));
}
}
}

fn is_allowed_statement(stmt: &Stmt) -> bool {
matches!(
stmt,
Stmt::FunctionDef(_) | Stmt::ClassDef(_) | Stmt::Assign(_) | Stmt::AnnAssign(_)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
source: crates/ruff_linter/src/rules/wps_light/mod.rs
snapshot_kind: text
---
WPS604.py:87:5: WPS604 Wrong statement inside class definition
|
86 | # Conditional logic inside the class (not recommended but valid)
87 | if math.pi > 3:
| _____^
88 | | def conditionally_defined_method(self):
89 | | print("This method exists because math.pi > 3.")
| |____________________________________________________________^ WPS604
90 |
91 | # For loop inside the class (unusual but valid)
|

WPS604.py:92:5: WPS604 Wrong statement inside class definition
|
91 | # For loop inside the class (unusual but valid)
92 | for i in range(3):
| _____^
93 | | exec(f"def loop_method_{i}(self): print('Loop-defined method {i}')")
| |____________________________________________________________________________^ WPS604
94 |
95 | # Docstrings for the class
|
4 changes: 4 additions & 0 deletions ruff.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.