Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[wps-light] Implement consecutive-underscores-in-name (WPS116) #10

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
41 changes: 41 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/wps_light/WPS116.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@

# correct

a: int

for _ in range(1):
pass

_ = 1

_private_variable = 1

CONSTANT_VARIABLE = 1

def function_name():
in_function_variable = 1

long_variable_name = 1

class ClassName:
def __dunder_method__(self, parameter_x):
pass


# incorect

_private__variable = 1

CONSTANT__VARIABLE = 1

def function__name():
in_function__variable = 1

long__variable___name = 1

class ClassName:
def __dunder___method__(self, parameter_x):
pass



10 changes: 9 additions & 1 deletion crates/ruff_linter/src/checkers/ast/analyze/bindings.rs
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ use ruff_text_size::Ranged;

use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_import_conventions, flake8_pyi, pyflakes, pylint, ruff};
use crate::rules::{flake8_import_conventions, flake8_pyi, pyflakes, pylint, ruff, wps_light};

/// Run lint rules over the [`Binding`]s.
pub(crate) fn bindings(checker: &mut Checker) {
@@ -15,6 +15,7 @@ pub(crate) fn bindings(checker: &mut Checker) {
Rule::UnconventionalImportAlias,
Rule::UnsortedDunderSlots,
Rule::UnusedVariable,
Rule::ConsecutiveUnderscoresInName,
]) {
return;
}
@@ -77,5 +78,12 @@ pub(crate) fn bindings(checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::ConsecutiveUnderscoresInName) {
if let Some(diagnostic) =
wps_light::rules::consecutive_underscores_in_name(checker.locator(), binding)
{
checker.diagnostics.push(diagnostic);
}
}
}
}
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
@@ -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, "116") => (RuleGroup::Preview, rules::wps_light::rules::ConsecutiveUnderscoresInName),

// ruff
(Ruff, "001") => (RuleGroup::Stable, rules::ruff::rules::AmbiguousUnicodeCharacterString),
(Ruff, "002") => (RuleGroup::Stable, rules::ruff::rules::AmbiguousUnicodeCharacterDocstring),
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/registry.rs
Original file line number Diff line number Diff line change
@@ -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,
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/mod.rs
Original file line number Diff line number Diff line change
@@ -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::ConsecutiveUnderscoresInName, Path::new("WPS116.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(())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
use std::fmt;

use itertools::Itertools;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::{Binding, BindingKind};
use ruff_text_size::Ranged;

use crate::Locator;

/// ## What it does
/// Checks for consecutive underscores in name
/// (variables, attributes, functions, and methods)
///
/// ## Why is this bad?
/// More consecutive underscoress lowers readability.
///
/// ## Example
/// ```python
/// long___variable__name: int = 3
/// ```
///
/// Use instead:
/// ```python
/// long_variable_name: int = 3
/// ```
#[violation]
pub struct ConsecutiveUnderscoresInName {
name: String,
replacement: String,
kind: Kind,
}

impl Violation for ConsecutiveUnderscoresInName {
#[derive_message_formats]
fn message(&self) -> String {
let Self {
name,
replacement: _,
kind,
} = self;
format!("{kind} name {name} contains consecutive underscors inside.")
}

fn fix_title(&self) -> Option<String> {
Some(format!("Rename variable to {}", self.replacement))
}
}

/// WPS116
pub(crate) fn consecutive_underscores_in_name(
locator: &Locator,
binding: &Binding,
) -> Option<Diagnostic> {
let name = binding.name(locator.contents());
if !name.len() < 3 || !name.contains("__") {
return None;
}

let kind = match binding.kind {
BindingKind::Annotation => Kind::Annotation,
BindingKind::Argument => Kind::Argument,
BindingKind::NamedExprAssignment => Kind::NamedExprAssignment,
BindingKind::Assignment => Kind::Assignment,
BindingKind::TypeParam => Kind::TypeParam,
BindingKind::LoopVar => Kind::LoopVar,
BindingKind::WithItemVar => Kind::WithItemVar,
BindingKind::Global(_) => Kind::Global,
BindingKind::Nonlocal(_, _) => Kind::Nonlocal,
BindingKind::ClassDefinition(_) => Kind::ClassDefinition,
BindingKind::FunctionDefinition(_) => Kind::FunctionDefinition,
BindingKind::BoundException => Kind::BoundException,

BindingKind::Builtin
| BindingKind::Export(_)
| BindingKind::FutureImport
| BindingKind::Import(_)
| BindingKind::FromImport(_)
| BindingKind::SubmoduleImport(_)
| BindingKind::Deletion
| BindingKind::ConditionalDeletion(_)
| BindingKind::UnboundException(_) => {
return None;
}
};

let prefix_under = name.chars().take_while(|&c| c == '_').count();
let suffix_under = name.chars().rev().take_while(|&c| c == '_').count();
let trimmed = &name[prefix_under..name.len() - suffix_under];

if !trimmed.contains("__") {
return None;
}

let mut replacement = String::with_capacity(name.len());
replacement.push_str(&"_".repeat(prefix_under));
replacement.push_str(&trimmed.split('_').filter(|part| !part.is_empty()).join("_"));
replacement.push_str(&"_".repeat(suffix_under));

Some(Diagnostic::new(
ConsecutiveUnderscoresInName {
name: name.to_string(),
replacement: replacement.to_string(),
kind,
},
binding.range(),
))
}

#[derive(Debug, PartialEq, Eq, Copy, Clone)]
enum Kind {
Annotation,
Argument,
NamedExprAssignment,
Assignment,
TypeParam,
LoopVar,
WithItemVar,
Global,
Nonlocal,
ClassDefinition,
FunctionDefinition,
BoundException,
}

impl fmt::Display for Kind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Kind::Annotation => f.write_str("Annotation"),
Kind::Argument => f.write_str("Argument"),
Kind::NamedExprAssignment => f.write_str("Variable"),
Kind::Assignment => f.write_str("Variable"),
Kind::TypeParam => f.write_str("Type parameter"),
Kind::LoopVar => f.write_str("Variable"),
Kind::WithItemVar => f.write_str("Variable"),
Kind::Global => f.write_str("Global"),
Kind::Nonlocal => f.write_str("Nonlocal"),
Kind::ClassDefinition => f.write_str("Class"),
Kind::FunctionDefinition => f.write_str("Function"),
Kind::BoundException => f.write_str("Exception"),
}
}
}
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 consecutive_underscores_in_name::*;

mod consecutive_underscores_in_name;
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
source: crates/ruff_linter/src/rules/wps_light/mod.rs
---
WPS116.py:27:1: WPS116 Variable name _private__variable contains consecutive underscors inside.
|
25 | # incorect
26 |
27 | _private__variable = 1
| ^^^^^^^^^^^^^^^^^^ WPS116
28 |
29 | CONSTANT__VARIABLE = 1
|
= help: Rename variable to _private_variable

WPS116.py:29:1: WPS116 Variable name CONSTANT__VARIABLE contains consecutive underscors inside.
|
27 | _private__variable = 1
28 |
29 | CONSTANT__VARIABLE = 1
| ^^^^^^^^^^^^^^^^^^ WPS116
30 |
31 | def function__name():
|
= help: Rename variable to CONSTANT_VARIABLE

WPS116.py:31:5: WPS116 Function name function__name contains consecutive underscors inside.
|
29 | CONSTANT__VARIABLE = 1
30 |
31 | def function__name():
| ^^^^^^^^^^^^^^ WPS116
32 | in_function__variable = 1
|
= help: Rename variable to function_name

WPS116.py:32:5: WPS116 Variable name in_function__variable contains consecutive underscors inside.
|
31 | def function__name():
32 | in_function__variable = 1
| ^^^^^^^^^^^^^^^^^^^^^ WPS116
33 |
34 | long__variable___name = 1
|
= help: Rename variable to in_function_variable

WPS116.py:34:1: WPS116 Variable name long__variable___name contains consecutive underscors inside.
|
32 | in_function__variable = 1
33 |
34 | long__variable___name = 1
| ^^^^^^^^^^^^^^^^^^^^^ WPS116
35 |
36 | class ClassName:
|
= help: Rename variable to long_variable_name

WPS116.py:37:9: WPS116 Function name __dunder___method__ contains consecutive underscors inside.
|
36 | class ClassName:
37 | def __dunder___method__(self, parameter_x):
| ^^^^^^^^^^^^^^^^^^^ WPS116
38 | pass
|
= help: Rename variable to __dunder_method__
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.

Loading