Compare commits

..

6 Commits

Author SHA1 Message Date
colin99d
6c2d430430 Updated output format 2023-03-21 13:31:49 -04:00
colin99d
b0242ccfa1 Updated tests 2023-03-21 12:22:38 -04:00
colin99d
8ffc77d0ee Got tests working 2023-03-21 12:22:38 -04:00
colin99d
adc677bc9c Got tests working 2023-03-21 12:22:27 -04:00
colin99d
213e83aff2 Made small changes 2023-03-21 12:22:00 -04:00
colin99d
4b91826a0b Everything ready 2023-03-21 12:21:41 -04:00
93 changed files with 1188 additions and 2185 deletions

View File

@@ -85,6 +85,7 @@ jobs:
name: ruff
path: target/debug/ruff
cargo-test-wasm:
runs-on: ubuntu-latest
name: "cargo test (wasm)"
@@ -175,26 +176,3 @@ jobs:
with:
name: ecosystem-result
path: ecosystem-result
cargo-udeps:
name: "cargo udeps"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: "Install Rust toolchain"
run: rustup toolchain install nightly
- name: "Install cargo-udeps"
uses: taiki-e/install-action@cargo-udeps
- name: "Run cargo-udeps"
run: |
unused_dependencies=$(cargo +nightly udeps > unused.txt && cat unused.txt | cut -d $'\n' -f 2-)
if [ -z "$unused_dependencies" ]; then
echo "No unused dependencies found" > $GITHUB_STEP_SUMMARY
exit 0
else
echo "Unused dependencies found" > $GITHUB_STEP_SUMMARY
echo '```console' >> $GITHUB_STEP_SUMMARY
echo "$unused_dependencies" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi

6
Cargo.lock generated
View File

@@ -780,7 +780,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.258"
version = "0.0.257"
dependencies = [
"anyhow",
"clap 4.1.8",
@@ -1982,7 +1982,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.258"
version = "0.0.257"
dependencies = [
"anyhow",
"bisection",
@@ -2063,7 +2063,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.258"
version = "0.0.257"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",

View File

@@ -137,7 +137,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.258'
rev: 'v0.0.257'
hooks:
- id: ruff
```
@@ -243,7 +243,7 @@ quality tools, including:
- [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/)
- [flake8-datetimez](https://pypi.org/project/flake8-datetimez/)
- [flake8-debugger](https://pypi.org/project/flake8-debugger/)
- [flake8-django](https://pypi.org/project/flake8-django/)
- [flake8-django](https://pypi.org/project/flake8-django/) ([#2817](https://github.com/charliermarsh/ruff/issues/2817))
- [flake8-docstrings](https://pypi.org/project/flake8-docstrings/)
- [flake8-eradicate](https://pypi.org/project/flake8-eradicate/)
- [flake8-errmsg](https://pypi.org/project/flake8-errmsg/)

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.258"
version = "0.0.257"
edition = { workspace = true }
rust-version = { workspace = true }

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.258"
version = "0.0.257"
authors.workspace = true
edition.workspace = true
rust-version.workspace = true

View File

@@ -5,4 +5,3 @@ uses-*
rewrite-*
prefer-*
consider-*
use-*

View File

@@ -1,3 +1,6 @@
import pickle
from telnetlib import Telnet
pickle.loads()
Telnet("localhost", 23)

View File

@@ -1,3 +0,0 @@
import pickle
pickle.loads()

View File

@@ -1,113 +0,0 @@
from django.db import models
from django.db.models import Model
class StrBeforeRandomField(models.Model):
"""Model with `__str__` before a random property."""
class Meta:
verbose_name = "test"
verbose_name_plural = "tests"
def __str__(self):
return ""
random_property = "foo"
class StrBeforeFieldModel(models.Model):
"""Model with `__str__` before fields."""
class Meta:
verbose_name = "test"
verbose_name_plural = "tests"
def __str__(self):
return "foobar"
first_name = models.CharField(max_length=32)
class ManagerBeforeField(models.Model):
"""Model with manager before fields."""
objects = "manager"
class Meta:
verbose_name = "test"
verbose_name_plural = "tests"
def __str__(self):
return "foobar"
first_name = models.CharField(max_length=32)
class CustomMethodBeforeStr(models.Model):
"""Model with a custom method before `__str__`."""
class Meta:
verbose_name = "test"
verbose_name_plural = "tests"
def my_method(self):
pass
def __str__(self):
return "foobar"
class GetAbsoluteUrlBeforeSave(Model):
"""Model with `get_absolute_url` method before `save` method.
Subclass this directly using the `Model` class.
"""
def get_absolute_url(self):
pass
def save(self):
pass
class ConstantsAreNotFields(models.Model):
"""Model with an assignment to a constant after `__str__`."""
first_name = models.CharField(max_length=32)
class Meta:
verbose_name = "test"
verbose_name_plural = "tests"
def __str__(self):
pass
MY_CONSTANT = id(1)
class PerfectlyFine(models.Model):
"""Model which has everything in perfect order."""
first_name = models.CharField(max_length=32)
last_name = models.CharField(max_length=32)
objects = "manager"
class Meta:
verbose_name = "test"
verbose_name_plural = "tests"
def __str__(self):
return "Perfectly fine!"
def save(self, **kwargs):
super(PerfectlyFine, self).save(**kwargs)
def get_absolute_url(self):
return "http://%s" % self
def my_method(self):
pass
@property
def random_property(self):
return "%s" % self

View File

@@ -2,9 +2,7 @@ from a import a1 # import_from
from c import * # import_from_star
import a # import
import c.d
from z import z1
import b as b1 # import_as
import z
from ..parent import *
from .my import fn

View File

@@ -13,8 +13,3 @@ result = {
'key1': 'value',
'key2': 'value',
}
def foo() -> None:
#: E231
if (1,2):
pass

View File

@@ -1,19 +1,10 @@
"""Test: imports within `ModuleNotFoundError` and `ImportError` handlers."""
"""Test: imports within `ModuleNotFoundError` handlers."""
def module_not_found_error():
def check_orjson():
try:
import orjson
return True
except ModuleNotFoundError:
return False
def import_error():
try:
import orjson
return True
except ImportError:
return False

View File

@@ -30,17 +30,8 @@ def f(x: "List[str]") -> None:
...
def f(x: r"List[str]") -> None:
...
def f(x: "List[str]") -> None:
...
def f(x: """List[str]""") -> None:
...
def f(x: "Li" "st[str]") -> None:
list = "abc"
def f(x: List[str]) -> None:
...

View File

@@ -1,6 +1,5 @@
input = [1, 2, 3]
otherInput = [2, 3, 4]
foo = [1, 2, 3, 4]
# OK
zip(input, otherInput) # different inputs
@@ -9,8 +8,6 @@ zip(input, input[2:]) # not successive
zip(input[:-1], input[2:]) # not successive
list(zip(input, otherInput)) # nested call
zip(input, input[1::2]) # not successive
zip(foo[:-1], foo[1:], foo, strict=False) # more than 2 inputs
zip(foo[:-1], foo[1:], foo, strict=True) # more than 2 inputs
# Errors
zip(input, input[1:])
@@ -20,6 +17,3 @@ zip(input[1:], input[2:])
zip(input[1:-1], input[2:])
list(zip(input, input[1:]))
list(zip(input[:-1], input[1:]))
zip(foo[:-1], foo[1:], strict=True)
zip(foo[:-1], foo[1:], strict=False)
zip(foo[:-1], foo[1:], strict=bool(foo))

View File

@@ -6,6 +6,7 @@ use log::error;
use nohash_hasher::IntMap;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_common::cformat::{CFormatError, CFormatErrorType};
use rustpython_parser as parser;
use rustpython_parser::ast::{
Arg, Arguments, Comprehension, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext,
ExprKind, KeywordData, Located, Location, Operator, Pattern, PatternKind, Stmt, StmtKind,
@@ -14,17 +15,18 @@ use rustpython_parser::ast::{
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::context::Context;
use ruff_python_ast::helpers::{binding_range, extract_handled_exceptions, to_module_path};
use ruff_python_ast::helpers::{
binding_range, extract_handled_exceptions, to_module_path, Exceptions,
};
use ruff_python_ast::operations::{extract_all_names, AllNamesFlags};
use ruff_python_ast::relocate::relocate_expr;
use ruff_python_ast::scope::{
Binding, BindingId, BindingKind, ClassDef, Exceptions, ExecutionContext, FunctionDef, Lambda,
Scope, ScopeId, ScopeKind, ScopeStack,
Binding, BindingId, BindingKind, ClassDef, ExecutionContext, FunctionDef, Lambda, Scope,
ScopeId, ScopeKind, ScopeStack,
};
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
use ruff_python_ast::types::{Node, Range, RefEquality};
use ruff_python_ast::typing::{
match_annotated_subscript, parse_type_annotation, Callable, SubscriptKind,
};
use ruff_python_ast::typing::{match_annotated_subscript, Callable, SubscriptKind};
use ruff_python_ast::visitor::{walk_excepthandler, walk_pattern, Visitor};
use ruff_python_ast::{
branch_detection, cast, helpers, operations, str, typing, visibility, visitor,
@@ -196,7 +198,6 @@ where
if !scope_index.is_global() {
// Add the binding to the current scope.
let context = self.ctx.execution_context();
let exceptions = self.ctx.exceptions();
let scope = &mut self.ctx.scopes[scope_index];
let usage = Some((scope.id, Range::from(stmt)));
for (name, range) in names.iter().zip(ranges.iter()) {
@@ -208,7 +209,6 @@ where
range: *range,
source: Some(RefEquality(stmt)),
context,
exceptions,
});
scope.add(name, id);
}
@@ -226,7 +226,6 @@ where
let ranges: Vec<Range> = helpers::find_names(stmt, self.locator).collect();
if !scope_index.is_global() {
let context = self.ctx.execution_context();
let exceptions = self.ctx.exceptions();
let scope = &mut self.ctx.scopes[scope_index];
let usage = Some((scope.id, Range::from(stmt)));
for (name, range) in names.iter().zip(ranges.iter()) {
@@ -239,7 +238,6 @@ where
range: *range,
source: Some(RefEquality(stmt)),
context,
exceptions,
});
scope.add(name, id);
}
@@ -641,6 +639,7 @@ where
self.visit_expr(expr);
}
let context = self.ctx.execution_context();
self.add_binding(
name,
Binding {
@@ -650,8 +649,7 @@ where
typing_usage: None,
range: Range::from(stmt),
source: Some(self.ctx.current_stmt().clone()),
context: self.ctx.execution_context(),
exceptions: self.ctx.exceptions(),
context,
},
);
}
@@ -710,13 +708,6 @@ where
self.diagnostics.push(diagnostic);
}
}
if self
.settings
.rules
.enabled(Rule::DjangoUnorderedBodyContentInModel)
{
flake8_django::rules::unordered_body_content_in_model(self, bases, body);
}
if self.settings.rules.enabled(Rule::GlobalStatement) {
pylint::rules::global_statement(self, name);
}
@@ -836,6 +827,14 @@ where
pyupgrade::rules::deprecated_mock_import(self, stmt);
}
// If a module is imported within a `ModuleNotFoundError` body, treat that as a
// synthetic usage.
let is_handled = self
.ctx
.handled_exceptions
.iter()
.any(|exceptions| exceptions.contains(Exceptions::MODULE_NOT_FOUND_ERROR));
for alias in names {
if alias.node.name == "__future__" {
let name = alias.node.asname.as_ref().unwrap_or(&alias.node.name);
@@ -850,7 +849,6 @@ where
range: Range::from(alias),
source: Some(self.ctx.current_stmt().clone()),
context: self.ctx.execution_context(),
exceptions: self.ctx.exceptions(),
},
);
@@ -872,12 +870,15 @@ where
Binding {
kind: BindingKind::SubmoduleImportation(name, full_name),
runtime_usage: None,
synthetic_usage: None,
synthetic_usage: if is_handled {
Some((self.ctx.scope_id(), Range::from(alias)))
} else {
None
},
typing_usage: None,
range: Range::from(alias),
source: Some(self.ctx.current_stmt().clone()),
context: self.ctx.execution_context(),
exceptions: self.ctx.exceptions(),
},
);
} else {
@@ -899,7 +900,7 @@ where
Binding {
kind: BindingKind::Importation(name, full_name),
runtime_usage: None,
synthetic_usage: if is_explicit_reexport {
synthetic_usage: if is_handled || is_explicit_reexport {
Some((self.ctx.scope_id(), Range::from(alias)))
} else {
None
@@ -908,7 +909,6 @@ where
range: Range::from(alias),
source: Some(self.ctx.current_stmt().clone()),
context: self.ctx.execution_context(),
exceptions: self.ctx.exceptions(),
},
);
@@ -1153,6 +1153,14 @@ where
}
}
// If a module is imported within a `ModuleNotFoundError` body, treat that as a
// synthetic usage.
let is_handled = self
.ctx
.handled_exceptions
.iter()
.any(|exceptions| exceptions.contains(Exceptions::MODULE_NOT_FOUND_ERROR));
for alias in names {
if let Some("__future__") = module.as_deref() {
let name = alias.node.asname.as_ref().unwrap_or(&alias.node.name);
@@ -1167,7 +1175,6 @@ where
range: Range::from(alias),
source: Some(self.ctx.current_stmt().clone()),
context: self.ctx.execution_context(),
exceptions: self.ctx.exceptions(),
},
);
@@ -1193,12 +1200,15 @@ where
Binding {
kind: BindingKind::StarImportation(*level, module.clone()),
runtime_usage: None,
synthetic_usage: None,
synthetic_usage: if is_handled {
Some((self.ctx.scope_id(), Range::from(alias)))
} else {
None
},
typing_usage: None,
range: Range::from(stmt),
source: Some(self.ctx.current_stmt().clone()),
context: self.ctx.execution_context(),
exceptions: self.ctx.exceptions(),
},
);
@@ -1266,16 +1276,16 @@ where
Binding {
kind: BindingKind::FromImportation(name, full_name),
runtime_usage: None,
synthetic_usage: if is_explicit_reexport {
synthetic_usage: if is_handled || is_explicit_reexport {
Some((self.ctx.scope_id(), Range::from(alias)))
} else {
None
},
typing_usage: None,
range: Range::from(alias),
source: Some(self.ctx.current_stmt().clone()),
context: self.ctx.execution_context(),
exceptions: self.ctx.exceptions(),
},
);
}
@@ -1701,8 +1711,8 @@ where
flake8_pytest_style::rules::assert_in_exception_handler(handlers),
);
}
if self.settings.rules.enabled(Rule::SuppressibleException) {
flake8_simplify::rules::suppressible_exception(
if self.settings.rules.enabled(Rule::UseContextlibSuppress) {
flake8_simplify::rules::use_contextlib_suppress(
self, stmt, body, handlers, orelse, finalbody,
);
}
@@ -1897,7 +1907,6 @@ where
range: Range::from(stmt),
source: Some(RefEquality(stmt)),
context: self.ctx.execution_context(),
exceptions: self.ctx.exceptions(),
});
self.ctx.global_scope_mut().add(name, id);
}
@@ -1960,7 +1969,6 @@ where
range: Range::from(*stmt),
source: Some(RefEquality(stmt)),
context: self.ctx.execution_context(),
exceptions: self.ctx.exceptions(),
});
self.ctx.global_scope_mut().add(name, id);
}
@@ -2110,7 +2118,6 @@ where
range: Range::from(stmt),
source: Some(self.ctx.current_stmt().clone()),
context: self.ctx.execution_context(),
exceptions: self.ctx.exceptions(),
},
);
}
@@ -2128,8 +2135,7 @@ where
}
fn visit_expr(&mut self, expr: &'b Expr) {
if !self.ctx.in_deferred_type_definition
&& self.ctx.in_deferred_string_type_definition.is_none()
if !(self.ctx.in_deferred_type_definition || self.ctx.in_deferred_string_type_definition)
&& self.ctx.in_type_definition
&& self.ctx.annotations_future_enabled
{
@@ -2254,9 +2260,9 @@ where
if self
.settings
.rules
.enabled(Rule::LoadBeforeGlobalDeclaration)
.enabled(Rule::UsePriorToGlobalDeclaration)
{
pylint::rules::load_before_global_declaration(self, id, expr);
pylint::rules::use_prior_to_global_declaration(self, id, expr);
}
}
ExprKind::Attribute { attr, value, .. } => {
@@ -2439,30 +2445,8 @@ where
}
// flake8-bandit
if self.settings.rules.any_enabled(&[
Rule::SuspiciousPickleUsage,
Rule::SuspiciousMarshalUsage,
Rule::SuspiciousInsecureHashUsage,
Rule::SuspiciousInsecureCipherUsage,
Rule::SuspiciousInsecureCipherModeUsage,
Rule::SuspiciousMktempUsage,
Rule::SuspiciousEvalUsage,
Rule::SuspiciousMarkSafeUsage,
Rule::SuspiciousURLOpenUsage,
Rule::SuspiciousNonCryptographicRandomUsage,
Rule::SuspiciousXMLCElementTreeUsage,
Rule::SuspiciousXMLElementTreeUsage,
Rule::SuspiciousXMLExpatReaderUsage,
Rule::SuspiciousXMLExpatBuilderUsage,
Rule::SuspiciousXMLSaxUsage,
Rule::SuspiciousXMLMiniDOMUsage,
Rule::SuspiciousXMLPullDOMUsage,
Rule::SuspiciousXMLETreeUsage,
Rule::SuspiciousUnverifiedContextUsage,
Rule::SuspiciousTelnetUsage,
Rule::SuspiciousFTPLibUsage,
]) {
flake8_bandit::rules::suspicious_function_call(self, expr);
if self.settings.rules.enabled(Rule::DeniedFunctionCall) {
flake8_bandit::rules::denied_function_call(self, expr);
}
// flake8-bugbear
@@ -2873,30 +2857,30 @@ where
// flake8-use-pathlib
if self.settings.rules.any_enabled(&[
Rule::OsPathAbspath,
Rule::OsChmod,
Rule::OsMkdir,
Rule::OsMakedirs,
Rule::OsRename,
Rule::PathlibAbspath,
Rule::PathlibChmod,
Rule::PathlibMkdir,
Rule::PathlibMakedirs,
Rule::PathlibRename,
Rule::PathlibReplace,
Rule::OsRmdir,
Rule::OsRemove,
Rule::OsUnlink,
Rule::OsGetcwd,
Rule::OsPathExists,
Rule::OsPathExpanduser,
Rule::OsPathIsdir,
Rule::OsPathIsfile,
Rule::OsPathIslink,
Rule::OsReadlink,
Rule::OsStat,
Rule::OsPathIsabs,
Rule::OsPathJoin,
Rule::OsPathBasename,
Rule::OsPathSamefile,
Rule::OsPathSplitext,
Rule::BuiltinOpen,
Rule::PyPath,
Rule::PathlibRmdir,
Rule::PathlibRemove,
Rule::PathlibUnlink,
Rule::PathlibGetcwd,
Rule::PathlibExists,
Rule::PathlibExpanduser,
Rule::PathlibIsDir,
Rule::PathlibIsFile,
Rule::PathlibIsLink,
Rule::PathlibReadlink,
Rule::PathlibStat,
Rule::PathlibIsAbs,
Rule::PathlibJoin,
Rule::PathlibBasename,
Rule::PathlibSamefile,
Rule::PathlibSplitext,
Rule::PathlibOpen,
Rule::PathlibPyPath,
]) {
flake8_use_pathlib::helpers::replaceable_by_pathlib(self, func);
}
@@ -3892,7 +3876,6 @@ where
range: Range::from(arg),
source: Some(self.ctx.current_stmt().clone()),
context: self.ctx.execution_context(),
exceptions: self.ctx.exceptions(),
},
);
@@ -3936,7 +3919,6 @@ where
range: Range::from(pattern),
source: Some(self.ctx.current_stmt().clone()),
context: self.ctx.execution_context(),
exceptions: self.ctx.exceptions(),
},
);
}
@@ -4115,7 +4097,6 @@ impl<'a> Checker<'a> {
typing_usage: None,
source: None,
context: ExecutionContext::Runtime,
exceptions: Exceptions::empty(),
});
scope.add(builtin, id);
}
@@ -4148,7 +4129,7 @@ impl<'a> Checker<'a> {
self.ctx.bindings[*index].mark_used(scope_id, Range::from(expr), context);
if self.ctx.bindings[*index].kind.is_annotation()
&& self.ctx.in_deferred_string_type_definition.is_none()
&& !self.ctx.in_deferred_string_type_definition
&& !self.ctx.in_deferred_type_definition
{
continue;
@@ -4340,7 +4321,6 @@ impl<'a> Checker<'a> {
range: Range::from(expr),
source: Some(self.ctx.current_stmt().clone()),
context: self.ctx.execution_context(),
exceptions: self.ctx.exceptions(),
},
);
return;
@@ -4361,7 +4341,6 @@ impl<'a> Checker<'a> {
range: Range::from(expr),
source: Some(self.ctx.current_stmt().clone()),
context: self.ctx.execution_context(),
exceptions: self.ctx.exceptions(),
},
);
return;
@@ -4378,7 +4357,6 @@ impl<'a> Checker<'a> {
range: Range::from(expr),
source: Some(self.ctx.current_stmt().clone()),
context: self.ctx.execution_context(),
exceptions: self.ctx.exceptions(),
},
);
return;
@@ -4444,7 +4422,6 @@ impl<'a> Checker<'a> {
range: Range::from(expr),
source: Some(self.ctx.current_stmt().clone()),
context: self.ctx.execution_context(),
exceptions: self.ctx.exceptions(),
},
);
return;
@@ -4461,7 +4438,6 @@ impl<'a> Checker<'a> {
range: Range::from(expr),
source: Some(self.ctx.current_stmt().clone()),
context: self.ctx.execution_context(),
exceptions: self.ctx.exceptions(),
},
);
}
@@ -4537,19 +4513,20 @@ impl<'a> Checker<'a> {
where
'b: 'a,
{
let mut stacks = Vec::with_capacity(self.deferred.string_type_definitions.len());
let mut stacks = vec![];
self.deferred.string_type_definitions.reverse();
while let Some((range, value, (in_annotation, in_type_checking_block), deferral)) =
while let Some((range, expression, (in_annotation, in_type_checking_block), deferral)) =
self.deferred.string_type_definitions.pop()
{
if let Ok((expr, kind)) = parse_type_annotation(value, range, self.locator) {
if let Ok(mut expr) = parser::parse_expression(expression, "<filename>") {
if in_annotation && self.ctx.annotations_future_enabled {
if self.settings.rules.enabled(Rule::QuotedAnnotation) {
pyupgrade::rules::quoted_annotation(self, value, range);
pyupgrade::rules::quoted_annotation(self, expression, range);
}
}
relocate_expr(&mut expr, range);
allocator.push(expr);
stacks.push((kind, (in_annotation, in_type_checking_block), deferral));
stacks.push(((in_annotation, in_type_checking_block), deferral));
} else {
if self
.settings
@@ -4558,14 +4535,14 @@ impl<'a> Checker<'a> {
{
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::ForwardAnnotationSyntaxError {
body: value.to_string(),
body: expression.to_string(),
},
range,
));
}
}
}
for (expr, (kind, (in_annotation, in_type_checking_block), (scopes, parents))) in
for (expr, ((in_annotation, in_type_checking_block), (scopes, parents))) in
allocator.iter().zip(stacks)
{
self.ctx.scope_stack = scopes;
@@ -4573,9 +4550,9 @@ impl<'a> Checker<'a> {
self.ctx.in_annotation = in_annotation;
self.ctx.in_type_checking_block = in_type_checking_block;
self.ctx.in_type_definition = true;
self.ctx.in_deferred_string_type_definition = Some(kind);
self.ctx.in_deferred_string_type_definition = true;
self.visit_expr(expr);
self.ctx.in_deferred_string_type_definition = None;
self.ctx.in_deferred_string_type_definition = false;
self.ctx.in_type_definition = false;
}
}
@@ -4919,11 +4896,8 @@ impl<'a> Checker<'a> {
// Collect all unused imports by location. (Multiple unused imports at the same
// location indicates an `import from`.)
type UnusedImport<'a> = (&'a str, &'a Range);
type BindingContext<'a, 'b> = (
&'a RefEquality<'b, Stmt>,
Option<&'a RefEquality<'b, Stmt>>,
Exceptions,
);
type BindingContext<'a, 'b> =
(&'a RefEquality<'b, Stmt>, Option<&'a RefEquality<'b, Stmt>>);
let mut unused: FxHashMap<BindingContext, Vec<UnusedImport>> = FxHashMap::default();
let mut ignored: FxHashMap<BindingContext, Vec<UnusedImport>> =
@@ -4945,7 +4919,6 @@ impl<'a> Checker<'a> {
let defined_by = binding.source.as_ref().unwrap();
let defined_in = self.ctx.child_to_parent.get(defined_by);
let exceptions = binding.exceptions;
let child: &Stmt = defined_by.into();
let diagnostic_lineno = binding.range.location.row();
@@ -4963,30 +4936,27 @@ impl<'a> Checker<'a> {
})
{
ignored
.entry((defined_by, defined_in, exceptions))
.entry((defined_by, defined_in))
.or_default()
.push((full_name, &binding.range));
} else {
unused
.entry((defined_by, defined_in, exceptions))
.entry((defined_by, defined_in))
.or_default()
.push((full_name, &binding.range));
}
}
let in_init =
let ignore_init =
self.settings.ignore_init_module_imports && self.path.ends_with("__init__.py");
for ((defined_by, defined_in, exceptions), unused_imports) in unused
for ((defined_by, defined_in), unused_imports) in unused
.into_iter()
.sorted_by_key(|((defined_by, ..), ..)| defined_by.location)
.sorted_by_key(|((defined_by, _), _)| defined_by.location)
{
let child: &Stmt = defined_by.into();
let parent: Option<&Stmt> = defined_in.map(Into::into);
let multiple = unused_imports.len() > 1;
let in_except_handler = exceptions
.intersects(Exceptions::MODULE_NOT_FOUND_ERROR | Exceptions::IMPORT_ERROR);
let fix = if !in_init && !in_except_handler && self.patch(Rule::UnusedImport) {
let fix = if !ignore_init && self.patch(Rule::UnusedImport) {
let deleted: Vec<&Stmt> = self.deletions.iter().map(Into::into).collect();
match autofix::helpers::remove_unused_imports(
unused_imports.iter().map(|(full_name, _)| *full_name),
@@ -5012,17 +4982,12 @@ impl<'a> Checker<'a> {
None
};
let multiple = unused_imports.len() > 1;
for (full_name, range) in unused_imports {
let mut diagnostic = Diagnostic::new(
pyflakes::rules::UnusedImport {
name: full_name.to_string(),
context: if in_except_handler {
Some(pyflakes::rules::UnusedImportContext::ExceptHandler)
} else if in_init {
Some(pyflakes::rules::UnusedImportContext::Init)
} else {
None
},
ignore_init,
multiple,
},
*range,
@@ -5038,25 +5003,17 @@ impl<'a> Checker<'a> {
diagnostics.push(diagnostic);
}
}
for ((defined_by, .., exceptions), unused_imports) in ignored
for ((defined_by, ..), unused_imports) in ignored
.into_iter()
.sorted_by_key(|((defined_by, ..), ..)| defined_by.location)
.sorted_by_key(|((defined_by, _), _)| defined_by.location)
{
let child: &Stmt = defined_by.into();
let multiple = unused_imports.len() > 1;
let in_except_handler = exceptions
.intersects(Exceptions::MODULE_NOT_FOUND_ERROR | Exceptions::IMPORT_ERROR);
for (full_name, range) in unused_imports {
let mut diagnostic = Diagnostic::new(
pyflakes::rules::UnusedImport {
name: full_name.to_string(),
context: if in_except_handler {
Some(pyflakes::rules::UnusedImportContext::ExceptHandler)
} else if in_init {
Some(pyflakes::rules::UnusedImportContext::Init)
} else {
None
},
ignore_init,
multiple,
},
*range,

View File

@@ -171,9 +171,7 @@ pub fn check_logical_lines(
#[cfg(not(debug_assertions))]
let should_fix = false;
for diagnostic in
missing_whitespace(&line.text, start_loc.row(), should_fix, indent_level)
{
for diagnostic in missing_whitespace(&line.text, start_loc.row(), should_fix) {
if settings.rules.enabled(diagnostic.kind.rule()) {
diagnostics.push(diagnostic);
}

View File

@@ -1,5 +1,6 @@
//! `NoQA` enforcement and validation.
use log::warn;
use nohash_hasher::IntMap;
use rustpython_parser::ast::Location;
@@ -9,7 +10,7 @@ use ruff_python_ast::types::Range;
use crate::codes::NoqaCode;
use crate::noqa;
use crate::noqa::{Directive, FileExemption};
use crate::noqa::{extract_file_exemption, Directive, Exemption};
use crate::registry::{AsRule, Rule};
use crate::rule_redirects::get_redirect_target;
use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA};
@@ -25,47 +26,63 @@ pub fn check_noqa(
) -> Vec<usize> {
let enforce_noqa = settings.rules.enabled(Rule::UnusedNOQA);
let lines: Vec<&str> = contents.universal_newlines().collect();
// Whether the file is exempted from all checks.
let mut file_exempted = false;
// Identify any codes that are globally exempted (within the current file).
let exemption = noqa::file_exemption(&lines, commented_lines);
// Codes that are globally exempted (within the current file).
let mut file_exemptions: Vec<NoqaCode> = vec![];
// Map from line number to `noqa` directive on that line, along with any codes
// that were matched by the directive.
let mut noqa_directives: IntMap<usize, (Directive, Vec<NoqaCode>)> = IntMap::default();
// Extract all `noqa` directives.
if enforce_noqa {
for lineno in commented_lines {
// Indices of diagnostics that were ignored by a `noqa` directive.
let mut ignored_diagnostics = vec![];
let lines: Vec<&str> = contents.universal_newlines().collect();
for lineno in commented_lines {
match extract_file_exemption(lines[lineno - 1]) {
Exemption::All => {
file_exempted = true;
}
Exemption::Codes(codes) => {
file_exemptions.extend(codes.into_iter().filter_map(|code| {
if let Ok(rule) = Rule::from_code(get_redirect_target(code).unwrap_or(code)) {
Some(rule.noqa_code())
} else {
warn!("Invalid code provided to `# ruff: noqa`: {}", code);
None
}
}));
}
Exemption::None => {}
}
if enforce_noqa {
noqa_directives
.entry(lineno - 1)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[lineno - 1]), vec![]));
}
}
// Indices of diagnostics that were ignored by a `noqa` directive.
let mut ignored_diagnostics = vec![];
// Remove any ignored diagnostics.
for (index, diagnostic) in diagnostics.iter().enumerate() {
if matches!(diagnostic.kind.rule(), Rule::BlanketNOQA) {
continue;
}
match &exemption {
FileExemption::All => {
// If the file is exempted, ignore all diagnostics.
// If the file is exempted, ignore all diagnostics.
if file_exempted {
ignored_diagnostics.push(index);
continue;
}
// If the diagnostic is ignored by a global exemption, ignore it.
if !file_exemptions.is_empty() {
if file_exemptions.contains(&diagnostic.kind.rule().noqa_code()) {
ignored_diagnostics.push(index);
continue;
}
FileExemption::Codes(codes) => {
// If the diagnostic is ignored by a global exemption, ignore it.
if codes.contains(&diagnostic.kind.rule().noqa_code()) {
ignored_diagnostics.push(index);
continue;
}
}
FileExemption::None => {}
}
// Is the violation ignored by a `noqa` directive on the parent line?

View File

@@ -168,7 +168,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
(Pylint, "E0101") => Rule::ReturnInInit,
(Pylint, "E0116") => Rule::ContinueInFinally,
(Pylint, "E0117") => Rule::NonlocalWithoutBinding,
(Pylint, "E0118") => Rule::LoadBeforeGlobalDeclaration,
(Pylint, "E0118") => Rule::UsePriorToGlobalDeclaration,
(Pylint, "E0604") => Rule::InvalidAllObject,
(Pylint, "E0605") => Rule::InvalidAllFormat,
(Pylint, "E1142") => Rule::AwaitOutsideAsync,
@@ -327,7 +327,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
(Flake8Simplify, "101") => Rule::DuplicateIsinstanceCall,
(Flake8Simplify, "102") => Rule::CollapsibleIf,
(Flake8Simplify, "103") => Rule::NeedlessBool,
(Flake8Simplify, "105") => Rule::SuppressibleException,
(Flake8Simplify, "105") => Rule::UseContextlibSuppress,
(Flake8Simplify, "107") => Rule::ReturnInTryExceptFinally,
(Flake8Simplify, "108") => Rule::IfElseBlockInsteadOfIfExp,
(Flake8Simplify, "109") => Rule::CompareWithTuple,
@@ -463,6 +463,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
(Eradicate, "001") => Rule::CommentedOutCode,
// flake8-bandit
(Flake8Bandit, "001") => Rule::DeniedFunctionCall,
(Flake8Bandit, "101") => Rule::Assert,
(Flake8Bandit, "102") => Rule::ExecBuiltin,
(Flake8Bandit, "103") => Rule::BadFilePermissions,
@@ -470,37 +471,16 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
(Flake8Bandit, "105") => Rule::HardcodedPasswordString,
(Flake8Bandit, "106") => Rule::HardcodedPasswordFuncArg,
(Flake8Bandit, "107") => Rule::HardcodedPasswordDefault,
(Flake8Bandit, "608") => Rule::HardcodedSQLExpression,
(Flake8Bandit, "108") => Rule::HardcodedTempFile,
(Flake8Bandit, "110") => Rule::TryExceptPass,
(Flake8Bandit, "112") => Rule::TryExceptContinue,
(Flake8Bandit, "113") => Rule::RequestWithoutTimeout,
(Flake8Bandit, "301") => Rule::SuspiciousPickleUsage,
(Flake8Bandit, "302") => Rule::SuspiciousMarshalUsage,
(Flake8Bandit, "303") => Rule::SuspiciousInsecureHashUsage,
(Flake8Bandit, "304") => Rule::SuspiciousInsecureCipherUsage,
(Flake8Bandit, "305") => Rule::SuspiciousInsecureCipherModeUsage,
(Flake8Bandit, "306") => Rule::SuspiciousMktempUsage,
(Flake8Bandit, "307") => Rule::SuspiciousEvalUsage,
(Flake8Bandit, "308") => Rule::SuspiciousMarkSafeUsage,
(Flake8Bandit, "310") => Rule::SuspiciousURLOpenUsage,
(Flake8Bandit, "311") => Rule::SuspiciousNonCryptographicRandomUsage,
(Flake8Bandit, "312") => Rule::SuspiciousTelnetUsage,
(Flake8Bandit, "313") => Rule::SuspiciousXMLCElementTreeUsage,
(Flake8Bandit, "314") => Rule::SuspiciousXMLElementTreeUsage,
(Flake8Bandit, "315") => Rule::SuspiciousXMLExpatReaderUsage,
(Flake8Bandit, "316") => Rule::SuspiciousXMLExpatBuilderUsage,
(Flake8Bandit, "317") => Rule::SuspiciousXMLSaxUsage,
(Flake8Bandit, "318") => Rule::SuspiciousXMLMiniDOMUsage,
(Flake8Bandit, "319") => Rule::SuspiciousXMLPullDOMUsage,
(Flake8Bandit, "320") => Rule::SuspiciousXMLETreeUsage,
(Flake8Bandit, "321") => Rule::SuspiciousFTPLibUsage,
(Flake8Bandit, "323") => Rule::SuspiciousUnverifiedContextUsage,
(Flake8Bandit, "324") => Rule::HashlibInsecureHashFunction,
(Flake8Bandit, "501") => Rule::RequestWithNoCertValidation,
(Flake8Bandit, "506") => Rule::UnsafeYAMLLoad,
(Flake8Bandit, "508") => Rule::SnmpInsecureVersion,
(Flake8Bandit, "509") => Rule::SnmpWeakCryptography,
(Flake8Bandit, "608") => Rule::HardcodedSQLExpression,
(Flake8Bandit, "612") => Rule::LoggingConfigInsecureListen,
(Flake8Bandit, "701") => Rule::Jinja2AutoescapeFalse,
@@ -638,31 +618,31 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
(Tryceratops, "401") => Rule::VerboseLogMessage,
// flake8-use-pathlib
(Flake8UsePathlib, "100") => Rule::OsPathAbspath,
(Flake8UsePathlib, "101") => Rule::OsChmod,
(Flake8UsePathlib, "102") => Rule::OsMkdir,
(Flake8UsePathlib, "103") => Rule::OsMakedirs,
(Flake8UsePathlib, "104") => Rule::OsRename,
(Flake8UsePathlib, "100") => Rule::PathlibAbspath,
(Flake8UsePathlib, "101") => Rule::PathlibChmod,
(Flake8UsePathlib, "102") => Rule::PathlibMkdir,
(Flake8UsePathlib, "103") => Rule::PathlibMakedirs,
(Flake8UsePathlib, "104") => Rule::PathlibRename,
(Flake8UsePathlib, "105") => Rule::PathlibReplace,
(Flake8UsePathlib, "106") => Rule::OsRmdir,
(Flake8UsePathlib, "107") => Rule::OsRemove,
(Flake8UsePathlib, "108") => Rule::OsUnlink,
(Flake8UsePathlib, "109") => Rule::OsGetcwd,
(Flake8UsePathlib, "110") => Rule::OsPathExists,
(Flake8UsePathlib, "111") => Rule::OsPathExpanduser,
(Flake8UsePathlib, "112") => Rule::OsPathIsdir,
(Flake8UsePathlib, "113") => Rule::OsPathIsfile,
(Flake8UsePathlib, "114") => Rule::OsPathIslink,
(Flake8UsePathlib, "115") => Rule::OsReadlink,
(Flake8UsePathlib, "116") => Rule::OsStat,
(Flake8UsePathlib, "117") => Rule::OsPathIsabs,
(Flake8UsePathlib, "118") => Rule::OsPathJoin,
(Flake8UsePathlib, "119") => Rule::OsPathBasename,
(Flake8UsePathlib, "120") => Rule::OsPathDirname,
(Flake8UsePathlib, "121") => Rule::OsPathSamefile,
(Flake8UsePathlib, "122") => Rule::OsPathSplitext,
(Flake8UsePathlib, "123") => Rule::BuiltinOpen,
(Flake8UsePathlib, "124") => Rule::PyPath,
(Flake8UsePathlib, "106") => Rule::PathlibRmdir,
(Flake8UsePathlib, "107") => Rule::PathlibRemove,
(Flake8UsePathlib, "108") => Rule::PathlibUnlink,
(Flake8UsePathlib, "109") => Rule::PathlibGetcwd,
(Flake8UsePathlib, "110") => Rule::PathlibExists,
(Flake8UsePathlib, "111") => Rule::PathlibExpanduser,
(Flake8UsePathlib, "112") => Rule::PathlibIsDir,
(Flake8UsePathlib, "113") => Rule::PathlibIsFile,
(Flake8UsePathlib, "114") => Rule::PathlibIsLink,
(Flake8UsePathlib, "115") => Rule::PathlibReadlink,
(Flake8UsePathlib, "116") => Rule::PathlibStat,
(Flake8UsePathlib, "117") => Rule::PathlibIsAbs,
(Flake8UsePathlib, "118") => Rule::PathlibJoin,
(Flake8UsePathlib, "119") => Rule::PathlibBasename,
(Flake8UsePathlib, "120") => Rule::PathlibDirname,
(Flake8UsePathlib, "121") => Rule::PathlibSamefile,
(Flake8UsePathlib, "122") => Rule::PathlibSplitext,
(Flake8UsePathlib, "123") => Rule::PathlibOpen,
(Flake8UsePathlib, "124") => Rule::PathlibPyPath,
// flake8-logging-format
(Flake8LoggingFormat, "001") => Rule::LoggingStringFormat,
@@ -699,7 +679,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
(Flake8Django, "006") => Rule::DjangoExcludeWithModelForm,
(Flake8Django, "007") => Rule::DjangoAllWithModelForm,
(Flake8Django, "008") => Rule::DjangoModelWithoutDunderStr,
(Flake8Django, "012") => Rule::DjangoUnorderedBodyContentInModel,
(Flake8Django, "013") => Rule::DjangoNonLeadingReceiverDecorator,
_ => return None,

View File

@@ -28,6 +28,49 @@ static NOQA_LINE_REGEX: Lazy<Regex> = Lazy::new(|| {
});
static SPLIT_COMMA_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").unwrap());
#[derive(Debug)]
pub enum Exemption<'a> {
None,
All,
Codes(Vec<&'a str>),
}
/// Return `true` if a file is exempt from checking based on the contents of the
/// given line.
pub fn extract_file_exemption(line: &str) -> Exemption {
let line = line.trim_start();
if line.starts_with("# flake8: noqa")
|| line.starts_with("# flake8: NOQA")
|| line.starts_with("# flake8: NoQA")
{
return Exemption::All;
}
if let Some(remainder) = line
.strip_prefix("# ruff: noqa")
.or_else(|| line.strip_prefix("# ruff: NOQA"))
.or_else(|| line.strip_prefix("# ruff: NoQA"))
{
if remainder.is_empty() {
return Exemption::All;
} else if let Some(codes) = remainder.strip_prefix(':') {
let codes: Vec<&str> = SPLIT_COMMA_REGEX
.split(codes.trim())
.map(str::trim)
.filter(|code| !code.is_empty())
.collect();
if codes.is_empty() {
warn!("Expected rule codes on `noqa` directive: \"{line}\"");
}
return Exemption::Codes(codes);
}
warn!("Unexpected suffix on `noqa` directive: \"{line}\"");
}
Exemption::None
}
#[derive(Debug)]
pub enum Directive<'a> {
None,
@@ -76,47 +119,6 @@ pub fn extract_noqa_directive(line: &str) -> Directive {
}
}
enum ParsedExemption<'a> {
None,
All,
Codes(Vec<&'a str>),
}
/// Return a [`ParsedExemption`] for a given comment line.
fn parse_file_exemption(line: &str) -> ParsedExemption {
let line = line.trim_start();
if line.starts_with("# flake8: noqa")
|| line.starts_with("# flake8: NOQA")
|| line.starts_with("# flake8: NoQA")
{
return ParsedExemption::All;
}
if let Some(remainder) = line
.strip_prefix("# ruff: noqa")
.or_else(|| line.strip_prefix("# ruff: NOQA"))
.or_else(|| line.strip_prefix("# ruff: NoQA"))
{
if remainder.is_empty() {
return ParsedExemption::All;
} else if let Some(codes) = remainder.strip_prefix(':') {
let codes: Vec<&str> = SPLIT_COMMA_REGEX
.split(codes.trim())
.map(str::trim)
.filter(|code| !code.is_empty())
.collect();
if codes.is_empty() {
warn!("Expected rule codes on `noqa` directive: \"{line}\"");
}
return ParsedExemption::Codes(codes);
}
warn!("Unexpected suffix on `noqa` directive: \"{line}\"");
}
ParsedExemption::None
}
/// Returns `true` if the string list of `codes` includes `code` (or an alias
/// thereof).
pub fn includes(needle: Rule, haystack: &[&str]) -> bool {
@@ -145,43 +147,6 @@ pub fn rule_is_ignored(
}
}
pub enum FileExemption {
None,
All,
Codes(Vec<NoqaCode>),
}
/// Extract the [`FileExemption`] for a given Python source file, enumerating any rules that are
/// globally ignored within the file.
pub fn file_exemption(lines: &[&str], commented_lines: &[usize]) -> FileExemption {
let mut exempt_codes: Vec<NoqaCode> = vec![];
for lineno in commented_lines {
match parse_file_exemption(lines[lineno - 1]) {
ParsedExemption::All => {
return FileExemption::All;
}
ParsedExemption::Codes(codes) => {
exempt_codes.extend(codes.into_iter().filter_map(|code| {
if let Ok(rule) = Rule::from_code(get_redirect_target(code).unwrap_or(code)) {
Some(rule.noqa_code())
} else {
warn!("Invalid code provided to `# ruff: noqa`: {}", code);
None
}
}));
}
ParsedExemption::None => {}
}
}
if exempt_codes.is_empty() {
FileExemption::None
} else {
FileExemption::Codes(exempt_codes)
}
}
pub fn add_noqa(
path: &Path,
diagnostics: &[Diagnostic],
@@ -211,26 +176,44 @@ fn add_noqa_inner(
// Map of line number to set of (non-ignored) diagnostic codes that are triggered on that line.
let mut matches_by_line: FxHashMap<usize, RuleSet> = FxHashMap::default();
let lines: Vec<&str> = contents.universal_newlines().collect();
// Whether the file is exempted from all checks.
let mut file_exempted = false;
// Codes that are globally exempted (within the current file).
let exemption = file_exemption(&lines, commented_lines);
let mut file_exemptions: Vec<NoqaCode> = vec![];
let lines: Vec<&str> = contents.universal_newlines().collect();
for lineno in commented_lines {
match extract_file_exemption(lines[lineno - 1]) {
Exemption::All => {
file_exempted = true;
}
Exemption::Codes(codes) => {
file_exemptions.extend(codes.into_iter().filter_map(|code| {
if let Ok(rule) = Rule::from_code(get_redirect_target(code).unwrap_or(code)) {
Some(rule.noqa_code())
} else {
warn!("Invalid code provided to `# ruff: noqa`: {}", code);
None
}
}));
}
Exemption::None => {}
}
}
// Mark any non-ignored diagnostics.
for diagnostic in diagnostics {
match &exemption {
FileExemption::All => {
// If the file is exempted, don't add any noqa directives.
// If the file is exempted, don't add any noqa directives.
if file_exempted {
continue;
}
// If the diagnostic is ignored by a global exemption, don't add a noqa directive.
if !file_exemptions.is_empty() {
if file_exemptions.contains(&diagnostic.kind.rule().noqa_code()) {
continue;
}
FileExemption::Codes(codes) => {
// If the diagnostic is ignored by a global exemption, don't add a noqa directive.
if codes.contains(&diagnostic.kind.rule().noqa_code()) {
continue;
}
}
FileExemption::None => {}
}
// Is the violation ignored by a `noqa` directive on the parent line?

View File

@@ -167,7 +167,7 @@ ruff_macros::register_rules!(
rules::pylint::rules::UselessImportAlias,
rules::pylint::rules::UnnecessaryDirectLambdaCall,
rules::pylint::rules::NonlocalWithoutBinding,
rules::pylint::rules::LoadBeforeGlobalDeclaration,
rules::pylint::rules::UsePriorToGlobalDeclaration,
rules::pylint::rules::AwaitOutsideAsync,
rules::pylint::rules::PropertyWithParameters,
rules::pylint::rules::ReturnInInit,
@@ -299,7 +299,7 @@ ruff_macros::register_rules!(
rules::flake8_simplify::rules::DuplicateIsinstanceCall,
rules::flake8_simplify::rules::CollapsibleIf,
rules::flake8_simplify::rules::NeedlessBool,
rules::flake8_simplify::rules::SuppressibleException,
rules::flake8_simplify::rules::UseContextlibSuppress,
rules::flake8_simplify::rules::ReturnInTryExceptFinally,
rules::flake8_simplify::rules::IfElseBlockInsteadOfIfExp,
rules::flake8_simplify::rules::CompareWithTuple,
@@ -429,45 +429,25 @@ ruff_macros::register_rules!(
rules::eradicate::rules::CommentedOutCode,
// flake8-bandit
rules::flake8_bandit::rules::Assert,
rules::flake8_bandit::rules::BadFilePermissions,
rules::flake8_bandit::rules::DeniedFunctionCall,
rules::flake8_bandit::rules::ExecBuiltin,
rules::flake8_bandit::rules::BadFilePermissions,
rules::flake8_bandit::rules::HardcodedBindAllInterfaces,
rules::flake8_bandit::rules::HardcodedPasswordDefault,
rules::flake8_bandit::rules::HardcodedPasswordFuncArg,
rules::flake8_bandit::rules::HardcodedPasswordString,
rules::flake8_bandit::rules::HardcodedPasswordFuncArg,
rules::flake8_bandit::rules::HardcodedPasswordDefault,
rules::flake8_bandit::rules::HardcodedSQLExpression,
rules::flake8_bandit::rules::HardcodedTempFile,
rules::flake8_bandit::rules::HashlibInsecureHashFunction,
rules::flake8_bandit::rules::Jinja2AutoescapeFalse,
rules::flake8_bandit::rules::LoggingConfigInsecureListen,
rules::flake8_bandit::rules::RequestWithNoCertValidation,
rules::flake8_bandit::rules::TryExceptPass,
rules::flake8_bandit::rules::TryExceptContinue,
rules::flake8_bandit::rules::RequestWithoutTimeout,
rules::flake8_bandit::rules::HashlibInsecureHashFunction,
rules::flake8_bandit::rules::RequestWithNoCertValidation,
rules::flake8_bandit::rules::UnsafeYAMLLoad,
rules::flake8_bandit::rules::SnmpInsecureVersion,
rules::flake8_bandit::rules::SnmpWeakCryptography,
rules::flake8_bandit::rules::SuspiciousEvalUsage,
rules::flake8_bandit::rules::SuspiciousFTPLibUsage,
rules::flake8_bandit::rules::SuspiciousInsecureCipherUsage,
rules::flake8_bandit::rules::SuspiciousInsecureCipherModeUsage,
rules::flake8_bandit::rules::SuspiciousInsecureHashUsage,
rules::flake8_bandit::rules::SuspiciousMarkSafeUsage,
rules::flake8_bandit::rules::SuspiciousMarshalUsage,
rules::flake8_bandit::rules::SuspiciousMktempUsage,
rules::flake8_bandit::rules::SuspiciousNonCryptographicRandomUsage,
rules::flake8_bandit::rules::SuspiciousPickleUsage,
rules::flake8_bandit::rules::SuspiciousTelnetUsage,
rules::flake8_bandit::rules::SuspiciousURLOpenUsage,
rules::flake8_bandit::rules::SuspiciousUnverifiedContextUsage,
rules::flake8_bandit::rules::SuspiciousXMLCElementTreeUsage,
rules::flake8_bandit::rules::SuspiciousXMLETreeUsage,
rules::flake8_bandit::rules::SuspiciousXMLElementTreeUsage,
rules::flake8_bandit::rules::SuspiciousXMLExpatBuilderUsage,
rules::flake8_bandit::rules::SuspiciousXMLExpatReaderUsage,
rules::flake8_bandit::rules::SuspiciousXMLMiniDOMUsage,
rules::flake8_bandit::rules::SuspiciousXMLPullDOMUsage,
rules::flake8_bandit::rules::SuspiciousXMLSaxUsage,
rules::flake8_bandit::rules::TryExceptContinue,
rules::flake8_bandit::rules::TryExceptPass,
rules::flake8_bandit::rules::UnsafeYAMLLoad,
rules::flake8_bandit::rules::LoggingConfigInsecureListen,
rules::flake8_bandit::rules::Jinja2AutoescapeFalse,
// flake8-boolean-trap
rules::flake8_boolean_trap::rules::BooleanPositionalArgInFunctionDefinition,
rules::flake8_boolean_trap::rules::BooleanDefaultValueInFunctionDefinition,
@@ -587,31 +567,31 @@ ruff_macros::register_rules!(
rules::tryceratops::rules::ErrorInsteadOfException,
rules::tryceratops::rules::VerboseLogMessage,
// flake8-use-pathlib
rules::flake8_use_pathlib::violations::OsPathAbspath,
rules::flake8_use_pathlib::violations::OsChmod,
rules::flake8_use_pathlib::violations::OsMkdir,
rules::flake8_use_pathlib::violations::OsMakedirs,
rules::flake8_use_pathlib::violations::OsRename,
rules::flake8_use_pathlib::violations::PathlibAbspath,
rules::flake8_use_pathlib::violations::PathlibChmod,
rules::flake8_use_pathlib::violations::PathlibMkdir,
rules::flake8_use_pathlib::violations::PathlibMakedirs,
rules::flake8_use_pathlib::violations::PathlibRename,
rules::flake8_use_pathlib::violations::PathlibReplace,
rules::flake8_use_pathlib::violations::OsRmdir,
rules::flake8_use_pathlib::violations::OsRemove,
rules::flake8_use_pathlib::violations::OsUnlink,
rules::flake8_use_pathlib::violations::OsGetcwd,
rules::flake8_use_pathlib::violations::OsPathExists,
rules::flake8_use_pathlib::violations::OsPathExpanduser,
rules::flake8_use_pathlib::violations::OsPathIsdir,
rules::flake8_use_pathlib::violations::OsPathIsfile,
rules::flake8_use_pathlib::violations::OsPathIslink,
rules::flake8_use_pathlib::violations::OsReadlink,
rules::flake8_use_pathlib::violations::OsStat,
rules::flake8_use_pathlib::violations::OsPathIsabs,
rules::flake8_use_pathlib::violations::OsPathJoin,
rules::flake8_use_pathlib::violations::OsPathBasename,
rules::flake8_use_pathlib::violations::OsPathDirname,
rules::flake8_use_pathlib::violations::OsPathSamefile,
rules::flake8_use_pathlib::violations::OsPathSplitext,
rules::flake8_use_pathlib::violations::BuiltinOpen,
rules::flake8_use_pathlib::violations::PyPath,
rules::flake8_use_pathlib::violations::PathlibRmdir,
rules::flake8_use_pathlib::violations::PathlibRemove,
rules::flake8_use_pathlib::violations::PathlibUnlink,
rules::flake8_use_pathlib::violations::PathlibGetcwd,
rules::flake8_use_pathlib::violations::PathlibExists,
rules::flake8_use_pathlib::violations::PathlibExpanduser,
rules::flake8_use_pathlib::violations::PathlibIsDir,
rules::flake8_use_pathlib::violations::PathlibIsFile,
rules::flake8_use_pathlib::violations::PathlibIsLink,
rules::flake8_use_pathlib::violations::PathlibReadlink,
rules::flake8_use_pathlib::violations::PathlibStat,
rules::flake8_use_pathlib::violations::PathlibIsAbs,
rules::flake8_use_pathlib::violations::PathlibJoin,
rules::flake8_use_pathlib::violations::PathlibBasename,
rules::flake8_use_pathlib::violations::PathlibDirname,
rules::flake8_use_pathlib::violations::PathlibSamefile,
rules::flake8_use_pathlib::violations::PathlibSplitext,
rules::flake8_use_pathlib::violations::PathlibOpen,
rules::flake8_use_pathlib::violations::PathlibPyPath,
// flake8-logging-format
rules::flake8_logging_format::violations::LoggingStringFormat,
rules::flake8_logging_format::violations::LoggingPercentFormat,
@@ -642,7 +622,6 @@ ruff_macros::register_rules!(
rules::flake8_django::rules::DjangoExcludeWithModelForm,
rules::flake8_django::rules::DjangoAllWithModelForm,
rules::flake8_django::rules::DjangoModelWithoutDunderStr,
rules::flake8_django::rules::DjangoUnorderedBodyContentInModel,
rules::flake8_django::rules::DjangoNonLeadingReceiverDecorator,
);

View File

@@ -12,30 +12,30 @@ mod tests {
use test_case::test_case;
use crate::registry::Rule;
use crate::rules::flake8_bandit::settings::Severity;
use crate::settings::Settings;
use crate::test::test_path;
#[test_case(Rule::DeniedFunctionCall, Path::new("S001.py"); "S001")]
#[test_case(Rule::Assert, Path::new("S101.py"); "S101")]
#[test_case(Rule::BadFilePermissions, Path::new("S103.py"); "S103")]
#[test_case(Rule::ExecBuiltin, Path::new("S102.py"); "S102")]
#[test_case(Rule::BadFilePermissions, Path::new("S103.py"); "S103")]
#[test_case(Rule::HardcodedBindAllInterfaces, Path::new("S104.py"); "S104")]
#[test_case(Rule::HardcodedPasswordDefault, Path::new("S107.py"); "S107")]
#[test_case(Rule::HardcodedPasswordFuncArg, Path::new("S106.py"); "S106")]
#[test_case(Rule::HardcodedPasswordString, Path::new("S105.py"); "S105")]
#[test_case(Rule::HardcodedPasswordFuncArg, Path::new("S106.py"); "S106")]
#[test_case(Rule::HardcodedPasswordDefault, Path::new("S107.py"); "S107")]
#[test_case(Rule::HardcodedSQLExpression, Path::new("S608.py"); "S608")]
#[test_case(Rule::HardcodedTempFile, Path::new("S108.py"); "S108")]
#[test_case(Rule::HashlibInsecureHashFunction, Path::new("S324.py"); "S324")]
#[test_case(Rule::Jinja2AutoescapeFalse, Path::new("S701.py"); "S701")]
#[test_case(Rule::LoggingConfigInsecureListen, Path::new("S612.py"); "S612")]
#[test_case(Rule::RequestWithNoCertValidation, Path::new("S501.py"); "S501")]
#[test_case(Rule::RequestWithoutTimeout, Path::new("S113.py"); "S113")]
#[test_case(Rule::HashlibInsecureHashFunction, Path::new("S324.py"); "S324")]
#[test_case(Rule::RequestWithNoCertValidation, Path::new("S501.py"); "S501")]
#[test_case(Rule::UnsafeYAMLLoad, Path::new("S506.py"); "S506")]
#[test_case(Rule::SnmpInsecureVersion, Path::new("S508.py"); "S508")]
#[test_case(Rule::SnmpWeakCryptography, Path::new("S509.py"); "S509")]
#[test_case(Rule::SuspiciousPickleUsage, Path::new("S301.py"); "S301")]
#[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"); "S312")]
#[test_case(Rule::TryExceptContinue, Path::new("S112.py"); "S112")]
#[test_case(Rule::LoggingConfigInsecureListen, Path::new("S612.py"); "S612")]
#[test_case(Rule::Jinja2AutoescapeFalse, Path::new("S701.py"); "S701")]
#[test_case(Rule::TryExceptPass, Path::new("S110.py"); "S110")]
#[test_case(Rule::UnsafeYAMLLoad, Path::new("S506.py"); "S506")]
#[test_case(Rule::TryExceptContinue, Path::new("S112.py"); "S112")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
@@ -59,6 +59,7 @@ mod tests {
"/foo".to_string(),
],
check_typed_exception: false,
severity: Severity::Low,
},
..Settings::for_rule(Rule::HardcodedTempFile)
},

View File

@@ -0,0 +1,294 @@
//! Check for calls to suspicious functions, or calls into suspicious modules.
//!
//! See: <https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html>
use rustpython_parser::ast::{Expr, ExprKind};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::Range;
use crate::checkers::ast::Checker;
use crate::rules::flake8_bandit::settings::Severity;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Reason {
Pickle,
Marshal,
InsecureHash,
InsecureCipher,
Mktemp,
Eval,
MarkSafe,
URLOpen,
NonCryptographicRandom,
UntrustedXML,
UnverifiedSSL,
Telnet,
FTPLib,
}
#[violation]
pub struct DeniedFunctionCall {
pub reason: Reason,
}
impl Violation for DeniedFunctionCall {
#[derive_message_formats]
fn message(&self) -> String {
let DeniedFunctionCall { reason } = self;
match reason {
Reason::Pickle => format!("`pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue"),
Reason::Marshal => format!("Deserialization with the `marshal` module is possibly dangerous"),
Reason::InsecureHash => format!("Use of insecure MD2, MD4, MD5, or SHA1 hash function"),
Reason::InsecureCipher => format!("Use of insecure cipher or cipher mode, replace with a known secure cipher such as AES"),
Reason::Mktemp => format!("Use of insecure and deprecated function (`mktemp`)"),
Reason::Eval => format!("Use of possibly insecure function; consider using `ast.literal_eval`"),
Reason::MarkSafe => format!("Use of `mark_safe` may expose cross-site scripting vulnerabilities"),
Reason::URLOpen => format!("Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected."),
Reason::NonCryptographicRandom => format!("Standard pseudo-random generators are not suitable for cryptographic purposes"),
Reason::UntrustedXML => format!("Using various XLM methods to parse untrusted XML data is known to be vulnerable to XML attacks; use `defusedxml` equivalents"),
Reason::UnverifiedSSL => format!("Python allows using an insecure context via the `_create_unverified_context` that reverts to the previous behavior that does not validate certificates or perform hostname checks"),
Reason::Telnet => format!("Telnet-related functions are being called. Telnet is considered insecure. Use SSH or some other encrypted protocol"),
Reason::FTPLib => format!("FTP-related functions are being called. FTP is considered insecure. Use SSH/SFTP/SCP or some other encrypted protocol"),
}
}
}
struct SuspiciousMembers<'a> {
members: &'a [&'a [&'a str]],
reason: Reason,
severity: Severity,
}
impl<'a> SuspiciousMembers<'a> {
pub const fn new(members: &'a [&'a [&'a str]], reason: Reason, severity: Severity) -> Self {
Self {
members,
reason,
severity,
}
}
}
struct SuspiciousModule<'a> {
name: &'a str,
reason: Reason,
severity: Severity,
}
impl<'a> SuspiciousModule<'a> {
pub const fn new(name: &'a str, reason: Reason, severity: Severity) -> Self {
Self {
name,
reason,
severity,
}
}
}
const SUSPICIOUS_MEMBERS: &[SuspiciousMembers] = &[
SuspiciousMembers::new(
&[
&["pickle", "loads"],
&["pickle", "load"],
&["pickle", "Unpickler"],
&["dill", "loads"],
&["dill", "load"],
&["dill", "Unpickler"],
&["shelve", "open"],
&["shelve", "DbfilenameShelf"],
&["jsonpickle", "decode"],
&["jsonpickle", "unpickler", "decode"],
&["pandas", "read_pickle"],
],
Reason::Pickle,
Severity::Medium,
),
SuspiciousMembers::new(
&[&["marshal", "loads"], &["marshal", "load"]],
Reason::Marshal,
Severity::Medium,
),
SuspiciousMembers::new(
&[
&["hashlib", "md5"],
&["hashlib", "sha1"],
&["Crypto", "Hash", "MD5", "new"],
&["Crypto", "Hash", "MD4", "new"],
&["Crypto", "Hash", "MD3", "new"],
&["Crypto", "Hash", "MD2", "new"],
&["Crypto", "Hash", "SHA", "new"],
&["Cryptodome", "Hash", "MD5", "new"],
&["Cryptodome", "Hash", "MD4", "new"],
&["Cryptodome", "Hash", "MD3", "new"],
&["Cryptodome", "Hash", "MD2", "new"],
&["Cryptodome", "Hash", "SHA", "new"],
&["cryptography", "hazmat", "primitives", "hashes", "MD5"],
&["cryptography", "hazmat", "primitives", "hashes", "SHA1"],
],
Reason::InsecureHash,
Severity::Medium,
),
SuspiciousMembers::new(
&[
&["Crypto", "Cipher", "ARC2", "new"],
&["Crypto", "Cipher", "ARC2", "new"],
&["Crypto", "Cipher", "Blowfish", "new"],
&["Crypto", "Cipher", "DES", "new"],
&["Crypto", "Cipher", "XOR", "new"],
&["Cryptodome", "Cipher", "ARC2", "new"],
&["Cryptodome", "Cipher", "ARC2", "new"],
&["Cryptodome", "Cipher", "Blowfish", "new"],
&["Cryptodome", "Cipher", "DES", "new"],
&["Cryptodome", "Cipher", "XOR", "new"],
&[
"cryptography",
"hazmat",
"primitives",
"ciphers",
"algorithms",
"ARC4",
],
&[
"cryptography",
"hazmat",
"primitives",
"ciphers",
"algorithms",
"Blowfish",
],
&[
"cryptography",
"hazmat",
"primitives",
"ciphers",
"algorithms",
"IDEA",
],
&[
"cryptography",
"hazmat",
"primitives",
"ciphers",
"modes",
"ECB",
],
],
Reason::InsecureCipher,
Severity::High,
),
SuspiciousMembers::new(&[&["tempfile", "mktemp"]], Reason::Mktemp, Severity::Medium),
SuspiciousMembers::new(&[&["eval"]], Reason::Eval, Severity::Medium),
SuspiciousMembers::new(
&[&["django", "utils", "safestring", "mark_safe"]],
Reason::MarkSafe,
Severity::Medium,
),
SuspiciousMembers::new(
&[
&["urllib", "urlopen"],
&["urllib", "request", "urlopen"],
&["urllib", "urlretrieve"],
&["urllib", "request", "urlretrieve"],
&["urllib", "URLopener"],
&["urllib", "request", "URLopener"],
&["urllib", "FancyURLopener"],
&["urllib", "request", "FancyURLopener"],
&["urllib2", "urlopen"],
&["urllib2", "Request"],
&["six", "moves", "urllib", "request", "urlopen"],
&["six", "moves", "urllib", "request", "urlretrieve"],
&["six", "moves", "urllib", "request", "URLopener"],
&["six", "moves", "urllib", "request", "FancyURLopener"],
],
Reason::URLOpen,
Severity::Medium,
),
SuspiciousMembers::new(
&[
&["random", "random"],
&["random", "randrange"],
&["random", "randint"],
&["random", "choice"],
&["random", "choices"],
&["random", "uniform"],
&["random", "triangular"],
],
Reason::NonCryptographicRandom,
Severity::Low,
),
SuspiciousMembers::new(
&[
&["xml", "etree", "cElementTree", "parse"],
&["xml", "etree", "cElementTree", "iterparse"],
&["xml", "etree", "cElementTree", "fromstring"],
&["xml", "etree", "cElementTree", "XMLParser"],
&["xml", "etree", "ElementTree", "parse"],
&["xml", "etree", "ElementTree", "iterparse"],
&["xml", "etree", "ElementTree", "fromstring"],
&["xml", "etree", "ElementTree", "XMLParser"],
&["xml", "sax", "expatreader", "create_parser"],
&["xml", "dom", "expatbuilder", "parse"],
&["xml", "dom", "expatbuilder", "parseString"],
&["xml", "sax", "parse"],
&["xml", "sax", "parseString"],
&["xml", "sax", "make_parser"],
&["xml", "dom", "minidom", "parse"],
&["xml", "dom", "minidom", "parseString"],
&["xml", "dom", "pulldom", "parse"],
&["xml", "dom", "pulldom", "parseString"],
&["lxml", "etree", "parse"],
&["lxml", "etree", "fromstring"],
&["lxml", "etree", "RestrictedElement"],
&["lxml", "etree", "GlobalParserTLS"],
&["lxml", "etree", "getDefaultParser"],
&["lxml", "etree", "check_docinfo"],
],
Reason::UntrustedXML,
Severity::High,
),
SuspiciousMembers::new(
&[&["ssl", "_create_unverified_context"]],
Reason::UnverifiedSSL,
Severity::Medium,
),
];
const SUSPICIOUS_MODULES: &[SuspiciousModule] = &[
SuspiciousModule::new("telnetlib", Reason::Telnet, Severity::High),
SuspiciousModule::new("ftplib", Reason::FTPLib, Severity::High),
];
/// S001
pub fn denied_function_call(checker: &mut Checker, expr: &Expr) {
let ExprKind::Call { func, .. } = &expr.node else {
return;
};
let Some(reason) = checker.ctx.resolve_call_path(func).and_then(|call_path| {
for module in SUSPICIOUS_MEMBERS {
if module.severity >= checker.settings.flake8_bandit.severity {
for member in module.members {
if call_path.as_slice() == *member {
return Some(module.reason);
}
}
}
}
for module in SUSPICIOUS_MODULES {
if module.severity >= checker.settings.flake8_bandit.severity {
if call_path.first() == Some(&module.name) {
return Some(module.reason);
}
}
}
None
}) else {
return;
};
let issue = DeniedFunctionCall { reason };
checker
.diagnostics
.push(Diagnostic::new(issue, Range::from(expr)));
}

View File

@@ -1,5 +1,6 @@
pub use assert_used::{assert_used, Assert};
pub use bad_file_permissions::{bad_file_permissions, BadFilePermissions};
pub use denied_function_call::{denied_function_call, DeniedFunctionCall};
pub use exec_used::{exec_used, ExecBuiltin};
pub use hardcoded_bind_all_interfaces::{
hardcoded_bind_all_interfaces, HardcodedBindAllInterfaces,
@@ -24,22 +25,13 @@ pub use request_with_no_cert_validation::{
pub use request_without_timeout::{request_without_timeout, RequestWithoutTimeout};
pub use snmp_insecure_version::{snmp_insecure_version, SnmpInsecureVersion};
pub use snmp_weak_cryptography::{snmp_weak_cryptography, SnmpWeakCryptography};
pub use suspicious_function_call::{
suspicious_function_call, SuspiciousEvalUsage, SuspiciousFTPLibUsage,
SuspiciousInsecureCipherModeUsage, SuspiciousInsecureCipherUsage, SuspiciousInsecureHashUsage,
SuspiciousMarkSafeUsage, SuspiciousMarshalUsage, SuspiciousMktempUsage,
SuspiciousNonCryptographicRandomUsage, SuspiciousPickleUsage, SuspiciousTelnetUsage,
SuspiciousURLOpenUsage, SuspiciousUnverifiedContextUsage, SuspiciousXMLCElementTreeUsage,
SuspiciousXMLETreeUsage, SuspiciousXMLElementTreeUsage, SuspiciousXMLExpatBuilderUsage,
SuspiciousXMLExpatReaderUsage, SuspiciousXMLMiniDOMUsage, SuspiciousXMLPullDOMUsage,
SuspiciousXMLSaxUsage,
};
pub use try_except_continue::{try_except_continue, TryExceptContinue};
pub use try_except_pass::{try_except_pass, TryExceptPass};
pub use unsafe_yaml_load::{unsafe_yaml_load, UnsafeYAMLLoad};
mod assert_used;
mod bad_file_permissions;
mod denied_function_call;
mod exec_used;
mod hardcoded_bind_all_interfaces;
mod hardcoded_password_default;
@@ -54,7 +46,6 @@ mod request_with_no_cert_validation;
mod request_without_timeout;
mod snmp_insecure_version;
mod snmp_weak_cryptography;
mod suspicious_function_call;
mod try_except_continue;
mod try_except_pass;
mod unsafe_yaml_load;

View File

@@ -1,519 +0,0 @@
//! Check for calls to suspicious functions, or calls into suspicious modules.
//!
//! See: <https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html>
use rustpython_parser::ast::{Expr, ExprKind};
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
#[violation]
pub struct SuspiciousPickleUsage;
impl Violation for SuspiciousPickleUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("`pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue")
}
}
#[violation]
pub struct SuspiciousMarshalUsage;
impl Violation for SuspiciousMarshalUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("Deserialization with the `marshal` module is possibly dangerous")
}
}
#[violation]
pub struct SuspiciousInsecureHashUsage;
impl Violation for SuspiciousInsecureHashUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use of insecure MD2, MD4, MD5, or SHA1 hash function")
}
}
#[violation]
pub struct SuspiciousInsecureCipherUsage;
impl Violation for SuspiciousInsecureCipherUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use of insecure cipher, replace with a known secure cipher such as AES")
}
}
#[violation]
pub struct SuspiciousInsecureCipherModeUsage;
impl Violation for SuspiciousInsecureCipherModeUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use of insecure cipher mode, replace with a known secure cipher such as AES")
}
}
#[violation]
pub struct SuspiciousMktempUsage;
impl Violation for SuspiciousMktempUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use of insecure and deprecated function (`mktemp`)")
}
}
#[violation]
pub struct SuspiciousEvalUsage;
impl Violation for SuspiciousEvalUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use of possibly insecure function; consider using `ast.literal_eval`")
}
}
#[violation]
pub struct SuspiciousMarkSafeUsage;
impl Violation for SuspiciousMarkSafeUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use of `mark_safe` may expose cross-site scripting vulnerabilities")
}
}
#[violation]
pub struct SuspiciousURLOpenUsage;
impl Violation for SuspiciousURLOpenUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.")
}
}
#[violation]
pub struct SuspiciousNonCryptographicRandomUsage;
impl Violation for SuspiciousNonCryptographicRandomUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("Standard pseudo-random generators are not suitable for cryptographic purposes")
}
}
#[violation]
pub struct SuspiciousXMLCElementTreeUsage;
impl Violation for SuspiciousXMLCElementTreeUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents")
}
}
#[violation]
pub struct SuspiciousXMLElementTreeUsage;
impl Violation for SuspiciousXMLElementTreeUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents")
}
}
#[violation]
pub struct SuspiciousXMLExpatReaderUsage;
impl Violation for SuspiciousXMLExpatReaderUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents")
}
}
#[violation]
pub struct SuspiciousXMLExpatBuilderUsage;
impl Violation for SuspiciousXMLExpatBuilderUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents")
}
}
#[violation]
pub struct SuspiciousXMLSaxUsage;
impl Violation for SuspiciousXMLSaxUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents")
}
}
#[violation]
pub struct SuspiciousXMLMiniDOMUsage;
impl Violation for SuspiciousXMLMiniDOMUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents")
}
}
#[violation]
pub struct SuspiciousXMLPullDOMUsage;
impl Violation for SuspiciousXMLPullDOMUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents")
}
}
#[violation]
pub struct SuspiciousXMLETreeUsage;
impl Violation for SuspiciousXMLETreeUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents")
}
}
#[violation]
pub struct SuspiciousUnverifiedContextUsage;
impl Violation for SuspiciousUnverifiedContextUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("Python allows using an insecure context via the `_create_unverified_context` that reverts to the previous behavior that does not validate certificates or perform hostname checks.")
}
}
#[violation]
pub struct SuspiciousTelnetUsage;
impl Violation for SuspiciousTelnetUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("Telnet-related functions are being called. Telnet is considered insecure. Use SSH or some other encrypted protocol.")
}
}
#[violation]
pub struct SuspiciousFTPLibUsage;
impl Violation for SuspiciousFTPLibUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("FTP-related functions are being called. FTP is considered insecure. Use SSH/SFTP/SCP or some other encrypted protocol.")
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Reason {
Pickle,
Marshal,
InsecureHash,
InsecureCipher,
InsecureCipherMode,
Mktemp,
Eval,
MarkSafe,
URLOpen,
NonCryptographicRandom,
XMLCElementTree,
XMLElementTree,
XMLExpatReader,
XMLExpatBuilder,
XMLSax,
XMLMiniDOM,
XMLPullDOM,
XMLETree,
UnverifiedContext,
Telnet,
FTPLib,
}
struct SuspiciousMembers<'a> {
members: &'a [&'a [&'a str]],
reason: Reason,
}
impl<'a> SuspiciousMembers<'a> {
pub const fn new(members: &'a [&'a [&'a str]], reason: Reason) -> Self {
Self { members, reason }
}
}
struct SuspiciousModule<'a> {
name: &'a str,
reason: Reason,
}
impl<'a> SuspiciousModule<'a> {
pub const fn new(name: &'a str, reason: Reason) -> Self {
Self { name, reason }
}
}
const SUSPICIOUS_MEMBERS: &[SuspiciousMembers] = &[
SuspiciousMembers::new(
&[
&["pickle", "loads"],
&["pickle", "load"],
&["pickle", "Unpickler"],
&["dill", "loads"],
&["dill", "load"],
&["dill", "Unpickler"],
&["shelve", "open"],
&["shelve", "DbfilenameShelf"],
&["jsonpickle", "decode"],
&["jsonpickle", "unpickler", "decode"],
&["pandas", "read_pickle"],
],
Reason::Pickle,
),
SuspiciousMembers::new(
&[&["marshal", "loads"], &["marshal", "load"]],
Reason::Marshal,
),
SuspiciousMembers::new(
&[
&["Crypto", "Hash", "MD5", "new"],
&["Crypto", "Hash", "MD4", "new"],
&["Crypto", "Hash", "MD3", "new"],
&["Crypto", "Hash", "MD2", "new"],
&["Crypto", "Hash", "SHA", "new"],
&["Cryptodome", "Hash", "MD5", "new"],
&["Cryptodome", "Hash", "MD4", "new"],
&["Cryptodome", "Hash", "MD3", "new"],
&["Cryptodome", "Hash", "MD2", "new"],
&["Cryptodome", "Hash", "SHA", "new"],
&["cryptography", "hazmat", "primitives", "hashes", "MD5"],
&["cryptography", "hazmat", "primitives", "hashes", "SHA1"],
],
Reason::InsecureHash,
),
SuspiciousMembers::new(
&[
&["Crypto", "Cipher", "ARC2", "new"],
&["Crypto", "Cipher", "ARC2", "new"],
&["Crypto", "Cipher", "Blowfish", "new"],
&["Crypto", "Cipher", "DES", "new"],
&["Crypto", "Cipher", "XOR", "new"],
&["Cryptodome", "Cipher", "ARC2", "new"],
&["Cryptodome", "Cipher", "ARC2", "new"],
&["Cryptodome", "Cipher", "Blowfish", "new"],
&["Cryptodome", "Cipher", "DES", "new"],
&["Cryptodome", "Cipher", "XOR", "new"],
&[
"cryptography",
"hazmat",
"primitives",
"ciphers",
"algorithms",
"ARC4",
],
&[
"cryptography",
"hazmat",
"primitives",
"ciphers",
"algorithms",
"Blowfish",
],
&[
"cryptography",
"hazmat",
"primitives",
"ciphers",
"algorithms",
"IDEA",
],
],
Reason::InsecureCipher,
),
SuspiciousMembers::new(
&[&[
"cryptography",
"hazmat",
"primitives",
"ciphers",
"modes",
"ECB",
]],
Reason::InsecureCipherMode,
),
SuspiciousMembers::new(&[&["tempfile", "mktemp"]], Reason::Mktemp),
SuspiciousMembers::new(&[&["eval"]], Reason::Eval),
SuspiciousMembers::new(
&[&["django", "utils", "safestring", "mark_safe"]],
Reason::MarkSafe,
),
SuspiciousMembers::new(
&[
&["urllib", "urlopen"],
&["urllib", "request", "urlopen"],
&["urllib", "urlretrieve"],
&["urllib", "request", "urlretrieve"],
&["urllib", "URLopener"],
&["urllib", "request", "URLopener"],
&["urllib", "FancyURLopener"],
&["urllib", "request", "FancyURLopener"],
&["urllib2", "urlopen"],
&["urllib2", "Request"],
&["six", "moves", "urllib", "request", "urlopen"],
&["six", "moves", "urllib", "request", "urlretrieve"],
&["six", "moves", "urllib", "request", "URLopener"],
&["six", "moves", "urllib", "request", "FancyURLopener"],
],
Reason::URLOpen,
),
SuspiciousMembers::new(
&[
&["random", "random"],
&["random", "randrange"],
&["random", "randint"],
&["random", "choice"],
&["random", "choices"],
&["random", "uniform"],
&["random", "triangular"],
],
Reason::NonCryptographicRandom,
),
SuspiciousMembers::new(
&[&["ssl", "_create_unverified_context"]],
Reason::UnverifiedContext,
),
SuspiciousMembers::new(
&[
&["xml", "etree", "cElementTree", "parse"],
&["xml", "etree", "cElementTree", "iterparse"],
&["xml", "etree", "cElementTree", "fromstring"],
&["xml", "etree", "cElementTree", "XMLParser"],
],
Reason::XMLCElementTree,
),
SuspiciousMembers::new(
&[
&["xml", "etree", "ElementTree", "parse"],
&["xml", "etree", "ElementTree", "iterparse"],
&["xml", "etree", "ElementTree", "fromstring"],
&["xml", "etree", "ElementTree", "XMLParser"],
],
Reason::XMLElementTree,
),
SuspiciousMembers::new(
&[&["xml", "sax", "expatreader", "create_parser"]],
Reason::XMLExpatReader,
),
SuspiciousMembers::new(
&[
&["xml", "dom", "expatbuilder", "parse"],
&["xml", "dom", "expatbuilder", "parseString"],
],
Reason::XMLExpatBuilder,
),
SuspiciousMembers::new(
&[
&["xml", "sax", "parse"],
&["xml", "sax", "parseString"],
&["xml", "sax", "make_parser"],
],
Reason::XMLSax,
),
SuspiciousMembers::new(
&[
&["xml", "dom", "minidom", "parse"],
&["xml", "dom", "minidom", "parseString"],
],
Reason::XMLMiniDOM,
),
SuspiciousMembers::new(
&[
&["xml", "dom", "pulldom", "parse"],
&["xml", "dom", "pulldom", "parseString"],
],
Reason::XMLPullDOM,
),
SuspiciousMembers::new(
&[
&["lxml", "etree", "parse"],
&["lxml", "etree", "fromstring"],
&["lxml", "etree", "RestrictedElement"],
&["lxml", "etree", "GlobalParserTLS"],
&["lxml", "etree", "getDefaultParser"],
&["lxml", "etree", "check_docinfo"],
],
Reason::XMLETree,
),
];
const SUSPICIOUS_MODULES: &[SuspiciousModule] = &[
SuspiciousModule::new("telnetlib", Reason::Telnet),
SuspiciousModule::new("ftplib", Reason::FTPLib),
];
/// S001
pub fn suspicious_function_call(checker: &mut Checker, expr: &Expr) {
let ExprKind::Call { func, .. } = &expr.node else {
return;
};
let Some(reason) = checker.ctx.resolve_call_path(func).and_then(|call_path| {
for module in SUSPICIOUS_MEMBERS {
for member in module.members {
if call_path.as_slice() == *member {
return Some(module.reason);
}
}
}
for module in SUSPICIOUS_MODULES {
if call_path.first() == Some(&module.name) {
return Some(module.reason);
}
}
None
}) else {
return;
};
let diagnostic_kind = match reason {
Reason::Pickle => SuspiciousPickleUsage.into(),
Reason::Marshal => SuspiciousMarshalUsage.into(),
Reason::InsecureHash => SuspiciousInsecureHashUsage.into(),
Reason::InsecureCipher => SuspiciousInsecureCipherUsage.into(),
Reason::InsecureCipherMode => SuspiciousInsecureCipherModeUsage.into(),
Reason::Mktemp => SuspiciousMktempUsage.into(),
Reason::Eval => SuspiciousEvalUsage.into(),
Reason::MarkSafe => SuspiciousMarkSafeUsage.into(),
Reason::URLOpen => SuspiciousURLOpenUsage.into(),
Reason::NonCryptographicRandom => SuspiciousNonCryptographicRandomUsage.into(),
Reason::XMLCElementTree => SuspiciousXMLCElementTreeUsage.into(),
Reason::XMLElementTree => SuspiciousXMLElementTreeUsage.into(),
Reason::XMLExpatReader => SuspiciousXMLExpatReaderUsage.into(),
Reason::XMLExpatBuilder => SuspiciousXMLExpatBuilderUsage.into(),
Reason::XMLSax => SuspiciousXMLSaxUsage.into(),
Reason::XMLMiniDOM => SuspiciousXMLMiniDOMUsage.into(),
Reason::XMLPullDOM => SuspiciousXMLPullDOMUsage.into(),
Reason::XMLETree => SuspiciousXMLETreeUsage.into(),
Reason::UnverifiedContext => SuspiciousUnverifiedContextUsage.into(),
Reason::Telnet => SuspiciousTelnetUsage.into(),
Reason::FTPLib => SuspiciousFTPLibUsage.into(),
};
let diagnostic = Diagnostic::new::<DiagnosticKind>(diagnostic_kind, Range::from(expr));
if checker.settings.rules.enabled(diagnostic.kind.rule()) {
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -5,6 +5,16 @@ use serde::{Deserialize, Serialize};
use ruff_macros::{CacheKey, ConfigurationOptions};
#[derive(
Debug, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema, Hash, CacheKey, PartialOrd,
)]
pub enum Severity {
#[default]
Low,
Medium,
High,
}
fn default_tmp_dirs() -> Vec<String> {
["/tmp", "/var/tmp", "/dev/shm"]
.map(std::string::ToString::to_string)
@@ -44,12 +54,18 @@ pub struct Options {
/// exception types. By default, `try`-`except`-`pass` is only
/// disallowed for `Exception` and `BaseException`.
pub check_typed_exception: Option<bool>,
#[option(default = "low", value_type = "str", example = "severity = \"high\"")]
/// The minimum severity to enforce for denied function calls.
///
/// Valid values are `low`, `medium`, and `high`.
pub severity: Option<Severity>,
}
#[derive(Debug, CacheKey)]
pub struct Settings {
pub hardcoded_tmp_directory: Vec<String>,
pub check_typed_exception: bool,
pub severity: Severity,
}
impl From<Options> for Settings {
@@ -67,6 +83,7 @@ impl From<Options> for Settings {
)
.collect(),
check_typed_exception: options.check_typed_exception.unwrap_or(false),
severity: options.severity.unwrap_or_default(),
}
}
}
@@ -77,6 +94,7 @@ impl From<Settings> for Options {
hardcoded_tmp_directory: Some(settings.hardcoded_tmp_directory),
hardcoded_tmp_directory_extend: None,
check_typed_exception: Some(settings.check_typed_exception),
severity: Some(settings.severity),
}
}
}
@@ -86,6 +104,7 @@ impl Default for Settings {
Self {
hardcoded_tmp_directory: default_tmp_dirs(),
check_typed_exception: false,
severity: Severity::default(),
}
}
}

View File

@@ -0,0 +1,31 @@
---
source: crates/ruff/src/rules/flake8_bandit/mod.rs
expression: diagnostics
---
- kind:
name: DeniedFunctionCall
body: "`pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue"
suggestion: ~
fixable: false
location:
row: 4
column: 0
end_location:
row: 4
column: 14
fix: ~
parent: ~
- kind:
name: DeniedFunctionCall
body: Telnet-related functions are being called. Telnet is considered insecure. Use SSH or some other encrypted protocol
suggestion: ~
fixable: false
location:
row: 6
column: 0
end_location:
row: 6
column: 23
fix: ~
parent: ~

View File

@@ -1,18 +0,0 @@
---
source: crates/ruff/src/rules/flake8_bandit/mod.rs
expression: diagnostics
---
- kind:
name: SuspiciousPickleUsage
body: "`pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue"
suggestion: ~
fixable: false
location:
row: 3
column: 0
end_location:
row: 3
column: 14
fix: ~
parent: ~

View File

@@ -1,18 +0,0 @@
---
source: crates/ruff/src/rules/flake8_bandit/mod.rs
expression: diagnostics
---
- kind:
name: SuspiciousTelnetUsage
body: Telnet-related functions are being called. Telnet is considered insecure. Use SSH or some other encrypted protocol.
suggestion: ~
fixable: false
location:
row: 3
column: 0
end_location:
row: 3
column: 23
fix: ~
parent: ~

View File

@@ -18,7 +18,6 @@ mod tests {
#[test_case(Rule::DjangoExcludeWithModelForm, Path::new("DJ006.py"); "DJ006")]
#[test_case(Rule::DjangoAllWithModelForm, Path::new("DJ007.py"); "DJ007")]
#[test_case(Rule::DjangoModelWithoutDunderStr, Path::new("DJ008.py"); "DJ008")]
#[test_case(Rule::DjangoUnorderedBodyContentInModel, Path::new("DJ012.py"); "DJ012")]
#[test_case(Rule::DjangoNonLeadingReceiverDecorator, Path::new("DJ013.py"); "DJ013")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());

View File

@@ -49,7 +49,7 @@ impl Violation for DjangoAllWithModelForm {
/// DJ007
pub fn all_with_model_form(checker: &Checker, bases: &[Expr], body: &[Stmt]) -> Option<Diagnostic> {
if !bases.iter().any(|base| is_model_form(&checker.ctx, base)) {
if !bases.iter().any(|base| is_model_form(checker, base)) {
return None;
}
for element in body.iter() {

View File

@@ -49,7 +49,7 @@ pub fn exclude_with_model_form(
bases: &[Expr],
body: &[Stmt],
) -> Option<Diagnostic> {
if !bases.iter().any(|base| is_model_form(&checker.ctx, base)) {
if !bases.iter().any(|base| is_model_form(checker, base)) {
return None;
}
for element in body.iter() {

View File

@@ -1,33 +1,31 @@
use ruff_python_ast::context::Context;
use rustpython_parser::ast::Expr;
use crate::checkers::ast::Checker;
/// Return `true` if a Python class appears to be a Django model, based on its base classes.
pub fn is_model(context: &Context, base: &Expr) -> bool {
context.resolve_call_path(base).map_or(false, |call_path| {
call_path.as_slice() == ["django", "db", "models", "Model"]
})
pub fn is_model(checker: &Checker, base: &Expr) -> bool {
checker
.ctx
.resolve_call_path(base)
.map_or(false, |call_path| {
call_path.as_slice() == ["django", "db", "models", "Model"]
})
}
/// Return `true` if a Python class appears to be a Django model form, based on its base classes.
pub fn is_model_form(context: &Context, base: &Expr) -> bool {
context.resolve_call_path(base).map_or(false, |call_path| {
call_path.as_slice() == ["django", "forms", "ModelForm"]
|| call_path.as_slice() == ["django", "forms", "models", "ModelForm"]
})
}
/// Return `true` if the expression is constructor for a Django model field.
pub fn is_model_field(context: &Context, expr: &Expr) -> bool {
context.resolve_call_path(expr).map_or(false, |call_path| {
call_path
.as_slice()
.starts_with(&["django", "db", "models"])
})
pub fn is_model_form(checker: &Checker, base: &Expr) -> bool {
checker
.ctx
.resolve_call_path(base)
.map_or(false, |call_path| {
call_path.as_slice() == ["django", "forms", "ModelForm"]
|| call_path.as_slice() == ["django", "forms", "models", "ModelForm"]
})
}
/// Return the name of the field type, if the expression is constructor for a Django model field.
pub fn get_model_field_name<'a>(context: &'a Context, expr: &'a Expr) -> Option<&'a str> {
context.resolve_call_path(expr).and_then(|call_path| {
pub fn get_model_field_name<'a>(checker: &'a Checker, expr: &'a Expr) -> Option<&'a str> {
checker.ctx.resolve_call_path(expr).and_then(|call_path| {
let call_path = call_path.as_slice();
if !call_path.starts_with(&["django", "db", "models"]) {
return None;

View File

@@ -8,9 +8,6 @@ pub use non_leading_receiver_decorator::{
pub use nullable_model_string_field::{
nullable_model_string_field, DjangoNullableModelStringField,
};
pub use unordered_body_content_in_model::{
unordered_body_content_in_model, DjangoUnorderedBodyContentInModel,
};
mod all_with_model_form;
mod exclude_with_model_form;
@@ -19,4 +16,3 @@ mod locals_in_render_function;
mod model_without_dunder_str;
mod non_leading_receiver_decorator;
mod nullable_model_string_field;
mod unordered_body_content_in_model;

View File

@@ -86,7 +86,7 @@ fn checker_applies(checker: &Checker, bases: &[Expr], body: &[Stmt]) -> bool {
if is_model_abstract(body) {
continue;
}
if helpers::is_model(&checker.ctx, base) {
if helpers::is_model(checker, base) {
return true;
}
}

View File

@@ -85,7 +85,7 @@ fn is_nullable_field<'a>(checker: &'a Checker, value: &'a Expr) -> Option<&'a st
return None;
};
let Some(valid_field_name) = helpers::get_model_field_name(&checker.ctx, func) else {
let Some(valid_field_name) = helpers::get_model_field_name(checker, func) else {
return None;
};

View File

@@ -1,167 +0,0 @@
use std::fmt;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::Range;
use rustpython_parser::ast::{Expr, ExprKind, Stmt, StmtKind};
use crate::checkers::ast::Checker;
use super::helpers;
/// ## What it does
/// Checks for the order of Model's inner classes, methods, and fields as per
/// the [Django Style Guide].
///
/// ## Why is it bad?
/// The [Django Style Guide] specifies that the order of Model inner classes,
/// attributes and methods should be as follows:
///
/// 1. All database fields
/// 2. Custom manager attributes
/// 3. `class Meta`
/// 4. `def __str__()`
/// 5. `def save()`
/// 6. `def get_absolute_url()`
/// 7. Any custom methods
///
/// ## Examples
/// ```python
/// from django.db import models
///
///
/// class StrBeforeFieldModel(models.Model):
/// class Meta:
/// verbose_name = "test"
/// verbose_name_plural = "tests"
///
/// def __str__(self):
/// return "foobar"
///
/// first_name = models.CharField(max_length=32)
/// last_name = models.CharField(max_length=40)
/// ```
///
/// Use instead:
/// ```python
/// from django.db import models
///
///
/// class StrBeforeFieldModel(models.Model):
/// first_name = models.CharField(max_length=32)
/// last_name = models.CharField(max_length=40)
///
/// class Meta:
/// verbose_name = "test"
/// verbose_name_plural = "tests"
///
/// def __str__(self):
/// return "foobar"
/// ```
///
/// [Django Style Guide]: https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/#model-style
#[violation]
pub struct DjangoUnorderedBodyContentInModel {
pub elem_type: ContentType,
pub before: ContentType,
}
impl Violation for DjangoUnorderedBodyContentInModel {
#[derive_message_formats]
fn message(&self) -> String {
let DjangoUnorderedBodyContentInModel { elem_type, before } = self;
format!("Order of model's inner classes, methods, and fields does not follow the Django Style Guide: {elem_type} should come before {before}")
}
}
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
pub enum ContentType {
FieldDeclaration,
ManagerDeclaration,
MetaClass,
StrMethod,
SaveMethod,
GetAbsoluteUrlMethod,
CustomMethod,
}
impl fmt::Display for ContentType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ContentType::FieldDeclaration => f.write_str("field declaration"),
ContentType::ManagerDeclaration => f.write_str("manager declaration"),
ContentType::MetaClass => f.write_str("`Meta` class"),
ContentType::StrMethod => f.write_str("`__str__` method"),
ContentType::SaveMethod => f.write_str("`save` method"),
ContentType::GetAbsoluteUrlMethod => f.write_str("`get_absolute_url` method"),
ContentType::CustomMethod => f.write_str("custom method"),
}
}
}
fn get_element_type(checker: &Checker, element: &StmtKind) -> Option<ContentType> {
match element {
StmtKind::Assign { targets, value, .. } => {
if let ExprKind::Call { func, .. } = &value.node {
if helpers::is_model_field(&checker.ctx, func) {
return Some(ContentType::FieldDeclaration);
}
}
let Some(expr) = targets.first() else {
return None;
};
let ExprKind::Name { id, .. } = &expr.node else {
return None;
};
if id == "objects" {
Some(ContentType::ManagerDeclaration)
} else {
None
}
}
StmtKind::ClassDef { name, .. } => {
if name == "Meta" {
Some(ContentType::MetaClass)
} else {
None
}
}
StmtKind::FunctionDef { name, .. } => match name.as_str() {
"__str__" => Some(ContentType::StrMethod),
"save" => Some(ContentType::SaveMethod),
"get_absolute_url" => Some(ContentType::GetAbsoluteUrlMethod),
_ => Some(ContentType::CustomMethod),
},
_ => None,
}
}
/// DJ012
pub fn unordered_body_content_in_model(checker: &mut Checker, bases: &[Expr], body: &[Stmt]) {
if !bases
.iter()
.any(|base| helpers::is_model(&checker.ctx, base))
{
return;
}
let mut elements_type_found = Vec::new();
for element in body.iter() {
let Some(current_element_type) = get_element_type(checker, &element.node) else {
continue;
};
let Some(&element_type) = elements_type_found
.iter()
.find(|&&element_type| element_type > current_element_type) else {
elements_type_found.push(current_element_type);
continue;
};
let diagnostic = Diagnostic::new(
DjangoUnorderedBodyContentInModel {
elem_type: current_element_type,
before: element_type,
},
Range::from(element),
);
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -1,57 +0,0 @@
---
source: crates/ruff/src/rules/flake8_django/mod.rs
expression: diagnostics
---
- kind:
name: DjangoUnorderedBodyContentInModel
body: "Order of model's inner classes, methods, and fields does not follow the Django Style Guide: field declaration should come before `Meta` class"
suggestion: ~
fixable: false
location:
row: 28
column: 4
end_location:
row: 28
column: 48
fix: ~
parent: ~
- kind:
name: DjangoUnorderedBodyContentInModel
body: "Order of model's inner classes, methods, and fields does not follow the Django Style Guide: field declaration should come before manager declaration"
suggestion: ~
fixable: false
location:
row: 43
column: 4
end_location:
row: 43
column: 48
fix: ~
parent: ~
- kind:
name: DjangoUnorderedBodyContentInModel
body: "Order of model's inner classes, methods, and fields does not follow the Django Style Guide: `__str__` method should come before custom method"
suggestion: ~
fixable: false
location:
row: 56
column: 4
end_location:
row: 57
column: 23
fix: ~
parent: ~
- kind:
name: DjangoUnorderedBodyContentInModel
body: "Order of model's inner classes, methods, and fields does not follow the Django Style Guide: `save` method should come before `get_absolute_url` method"
suggestion: ~
fixable: false
location:
row: 69
column: 4
end_location:
row: 70
column: 12
fix: ~
parent: ~

View File

@@ -16,7 +16,7 @@ mod tests {
#[test_case(Rule::DuplicateIsinstanceCall, Path::new("SIM101.py"); "SIM101")]
#[test_case(Rule::CollapsibleIf, Path::new("SIM102.py"); "SIM102")]
#[test_case(Rule::NeedlessBool, Path::new("SIM103.py"); "SIM103")]
#[test_case(Rule::SuppressibleException, Path::new("SIM105.py"); "SIM105")]
#[test_case(Rule::UseContextlibSuppress, Path::new("SIM105.py"); "SIM105")]
#[test_case(Rule::ReturnInTryExceptFinally, Path::new("SIM107.py"); "SIM107")]
#[test_case(Rule::IfElseBlockInsteadOfIfExp, Path::new("SIM108.py"); "SIM108")]
#[test_case(Rule::CompareWithTuple, Path::new("SIM109.py"); "SIM109")]

View File

@@ -24,7 +24,7 @@ pub use open_file_with_context_handler::{
};
pub use reimplemented_builtin::{convert_for_loop_to_any_all, ReimplementedBuiltin};
pub use return_in_try_except_finally::{return_in_try_except_finally, ReturnInTryExceptFinally};
pub use suppressible_exception::{suppressible_exception, SuppressibleException};
pub use use_contextlib_suppress::{use_contextlib_suppress, UseContextlibSuppress};
pub use yoda_conditions::{yoda_conditions, YodaConditions};
mod ast_bool_op;
@@ -39,5 +39,5 @@ mod key_in_dict;
mod open_file_with_context_handler;
mod reimplemented_builtin;
mod return_in_try_except_finally;
mod suppressible_exception;
mod use_contextlib_suppress;
mod yoda_conditions;

View File

@@ -9,19 +9,19 @@ use ruff_python_ast::types::Range;
use crate::checkers::ast::Checker;
#[violation]
pub struct SuppressibleException {
pub struct UseContextlibSuppress {
pub exception: String,
}
impl Violation for SuppressibleException {
impl Violation for UseContextlibSuppress {
#[derive_message_formats]
fn message(&self) -> String {
let SuppressibleException { exception } = self;
let UseContextlibSuppress { exception } = self;
format!("Use `contextlib.suppress({exception})` instead of try-except-pass")
}
}
/// SIM105
pub fn suppressible_exception(
pub fn use_contextlib_suppress(
checker: &mut Checker,
stmt: &Stmt,
body: &[Stmt],
@@ -63,7 +63,7 @@ pub fn suppressible_exception(
handler_names.join(", ")
};
checker.diagnostics.push(Diagnostic::new(
SuppressibleException { exception },
UseContextlibSuppress { exception },
Range::from(stmt),
));
}

View File

@@ -3,7 +3,7 @@ source: crates/ruff/src/rules/flake8_simplify/mod.rs
expression: diagnostics
---
- kind:
name: SuppressibleException
name: UseContextlibSuppress
body: "Use `contextlib.suppress(ValueError)` instead of try-except-pass"
suggestion: ~
fixable: false
@@ -16,7 +16,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: SuppressibleException
name: UseContextlibSuppress
body: "Use `contextlib.suppress(ValueError, OSError)` instead of try-except-pass"
suggestion: ~
fixable: false
@@ -29,7 +29,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: SuppressibleException
name: UseContextlibSuppress
body: "Use `contextlib.suppress(Exception)` instead of try-except-pass"
suggestion: ~
fixable: false
@@ -42,7 +42,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: SuppressibleException
name: UseContextlibSuppress
body: "Use `contextlib.suppress(a.Error, b.Error)` instead of try-except-pass"
suggestion: ~
fixable: false

View File

@@ -6,10 +6,11 @@ use ruff_python_ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::rules::flake8_use_pathlib::violations::{
BuiltinOpen, OsChmod, OsGetcwd, OsMakedirs, OsMkdir, OsPathAbspath, OsPathBasename,
OsPathDirname, OsPathExists, OsPathExpanduser, OsPathIsabs, OsPathIsdir, OsPathIsfile,
OsPathIslink, OsPathJoin, OsPathSamefile, OsPathSplitext, OsReadlink, OsRemove, OsRename,
OsRmdir, OsStat, OsUnlink, PathlibReplace, PyPath,
PathlibAbspath, PathlibBasename, PathlibChmod, PathlibDirname, PathlibExists,
PathlibExpanduser, PathlibGetcwd, PathlibIsAbs, PathlibIsDir, PathlibIsFile, PathlibIsLink,
PathlibJoin, PathlibMakedirs, PathlibMkdir, PathlibOpen, PathlibPyPath, PathlibReadlink,
PathlibRemove, PathlibRename, PathlibReplace, PathlibRmdir, PathlibSamefile, PathlibSplitext,
PathlibStat, PathlibUnlink,
};
use crate::settings::types::PythonVersion;
@@ -19,34 +20,34 @@ pub fn replaceable_by_pathlib(checker: &mut Checker, expr: &Expr) {
.ctx
.resolve_call_path(expr)
.and_then(|call_path| match call_path.as_slice() {
["os", "path", "abspath"] => Some(OsPathAbspath.into()),
["os", "chmod"] => Some(OsChmod.into()),
["os", "mkdir"] => Some(OsMkdir.into()),
["os", "makedirs"] => Some(OsMakedirs.into()),
["os", "rename"] => Some(OsRename.into()),
["os", "path", "abspath"] => Some(PathlibAbspath.into()),
["os", "chmod"] => Some(PathlibChmod.into()),
["os", "mkdir"] => Some(PathlibMkdir.into()),
["os", "makedirs"] => Some(PathlibMakedirs.into()),
["os", "rename"] => Some(PathlibRename.into()),
["os", "replace"] => Some(PathlibReplace.into()),
["os", "rmdir"] => Some(OsRmdir.into()),
["os", "remove"] => Some(OsRemove.into()),
["os", "unlink"] => Some(OsUnlink.into()),
["os", "getcwd"] => Some(OsGetcwd.into()),
["os", "getcwdb"] => Some(OsGetcwd.into()),
["os", "path", "exists"] => Some(OsPathExists.into()),
["os", "path", "expanduser"] => Some(OsPathExpanduser.into()),
["os", "path", "isdir"] => Some(OsPathIsdir.into()),
["os", "path", "isfile"] => Some(OsPathIsfile.into()),
["os", "path", "islink"] => Some(OsPathIslink.into()),
["os", "stat"] => Some(OsStat.into()),
["os", "path", "isabs"] => Some(OsPathIsabs.into()),
["os", "path", "join"] => Some(OsPathJoin.into()),
["os", "path", "basename"] => Some(OsPathBasename.into()),
["os", "path", "dirname"] => Some(OsPathDirname.into()),
["os", "path", "samefile"] => Some(OsPathSamefile.into()),
["os", "path", "splitext"] => Some(OsPathSplitext.into()),
["", "open"] => Some(BuiltinOpen.into()),
["py", "path", "local"] => Some(PyPath.into()),
["os", "rmdir"] => Some(PathlibRmdir.into()),
["os", "remove"] => Some(PathlibRemove.into()),
["os", "unlink"] => Some(PathlibUnlink.into()),
["os", "getcwd"] => Some(PathlibGetcwd.into()),
["os", "getcwdb"] => Some(PathlibGetcwd.into()),
["os", "path", "exists"] => Some(PathlibExists.into()),
["os", "path", "expanduser"] => Some(PathlibExpanduser.into()),
["os", "path", "isdir"] => Some(PathlibIsDir.into()),
["os", "path", "isfile"] => Some(PathlibIsFile.into()),
["os", "path", "islink"] => Some(PathlibIsLink.into()),
["os", "stat"] => Some(PathlibStat.into()),
["os", "path", "isabs"] => Some(PathlibIsAbs.into()),
["os", "path", "join"] => Some(PathlibJoin.into()),
["os", "path", "basename"] => Some(PathlibBasename.into()),
["os", "path", "dirname"] => Some(PathlibDirname.into()),
["os", "path", "samefile"] => Some(PathlibSamefile.into()),
["os", "path", "splitext"] => Some(PathlibSplitext.into()),
["", "open"] => Some(PathlibOpen.into()),
["py", "path", "local"] => Some(PathlibPyPath.into()),
// Python 3.9+
["os", "readlink"] if checker.settings.target_version >= PythonVersion::Py39 => {
Some(OsReadlink.into())
Some(PathlibReadlink.into())
}
_ => None,
})

View File

@@ -23,38 +23,38 @@ mod tests {
let diagnostics = test_path(
Path::new("flake8_use_pathlib").join(path).as_path(),
&settings::Settings::for_rules(vec![
Rule::OsPathAbspath,
Rule::OsChmod,
Rule::OsMkdir,
Rule::OsMakedirs,
Rule::OsRename,
Rule::PathlibAbspath,
Rule::PathlibChmod,
Rule::PathlibMkdir,
Rule::PathlibMakedirs,
Rule::PathlibRename,
Rule::PathlibReplace,
Rule::OsRmdir,
Rule::OsRemove,
Rule::OsUnlink,
Rule::OsGetcwd,
Rule::OsPathExists,
Rule::OsPathExpanduser,
Rule::OsPathIsdir,
Rule::OsPathIsfile,
Rule::OsPathIslink,
Rule::OsReadlink,
Rule::OsStat,
Rule::OsPathIsabs,
Rule::OsPathJoin,
Rule::OsPathBasename,
Rule::OsPathDirname,
Rule::OsPathSamefile,
Rule::OsPathSplitext,
Rule::BuiltinOpen,
Rule::PathlibRmdir,
Rule::PathlibRemove,
Rule::PathlibUnlink,
Rule::PathlibGetcwd,
Rule::PathlibExists,
Rule::PathlibExpanduser,
Rule::PathlibIsDir,
Rule::PathlibIsFile,
Rule::PathlibIsLink,
Rule::PathlibReadlink,
Rule::PathlibStat,
Rule::PathlibIsAbs,
Rule::PathlibJoin,
Rule::PathlibBasename,
Rule::PathlibDirname,
Rule::PathlibSamefile,
Rule::PathlibSplitext,
Rule::PathlibOpen,
]),
)?;
insta::assert_yaml_snapshot!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::PyPath, Path::new("py_path_1.py"); "PTH024_1")]
#[test_case(Rule::PyPath, Path::new("py_path_2.py"); "PTH024_2")]
#[test_case(Rule::PathlibPyPath, Path::new("py_path_1.py"); "PTH024_1")]
#[test_case(Rule::PathlibPyPath, Path::new("py_path_2.py"); "PTH024_2")]
fn rules_pypath(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -3,7 +3,7 @@ source: crates/ruff/src/rules/flake8_use_pathlib/mod.rs
expression: diagnostics
---
- kind:
name: PyPath
name: PathlibPyPath
body: "`py.path` is in maintenance mode, use `pathlib` instead"
suggestion: ~
fixable: false

View File

@@ -3,7 +3,7 @@ source: crates/ruff/src/rules/flake8_use_pathlib/mod.rs
expression: diagnostics
---
- kind:
name: PyPath
name: PathlibPyPath
body: "`py.path` is in maintenance mode, use `pathlib` instead"
suggestion: ~
fixable: false

View File

@@ -3,7 +3,7 @@ source: crates/ruff/src/rules/flake8_use_pathlib/mod.rs
expression: diagnostics
---
- kind:
name: OsPathAbspath
name: PathlibAbspath
body: "`os.path.abspath()` should be replaced by `Path.resolve()`"
suggestion: ~
fixable: false
@@ -16,7 +16,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsChmod
name: PathlibChmod
body: "`os.chmod()` should be replaced by `Path.chmod()`"
suggestion: ~
fixable: false
@@ -29,7 +29,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsMkdir
name: PathlibMkdir
body: "`os.mkdir()` should be replaced by `Path.mkdir()`"
suggestion: ~
fixable: false
@@ -42,7 +42,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsMakedirs
name: PathlibMakedirs
body: "`os.makedirs()` should be replaced by `Path.mkdir(parents=True)`"
suggestion: ~
fixable: false
@@ -55,7 +55,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsRename
name: PathlibRename
body: "`os.rename()` should be replaced by `Path.rename()`"
suggestion: ~
fixable: false
@@ -81,7 +81,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsRmdir
name: PathlibRmdir
body: "`os.rmdir()` should be replaced by `Path.rmdir()`"
suggestion: ~
fixable: false
@@ -94,7 +94,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsRemove
name: PathlibRemove
body: "`os.remove()` should be replaced by `Path.unlink()`"
suggestion: ~
fixable: false
@@ -107,7 +107,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsUnlink
name: PathlibUnlink
body: "`os.unlink()` should be replaced by `Path.unlink()`"
suggestion: ~
fixable: false
@@ -120,7 +120,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsGetcwd
name: PathlibGetcwd
body: "`os.getcwd()` should be replaced by `Path.cwd()`"
suggestion: ~
fixable: false
@@ -133,7 +133,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathExists
name: PathlibExists
body: "`os.path.exists()` should be replaced by `Path.exists()`"
suggestion: ~
fixable: false
@@ -146,7 +146,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathExpanduser
name: PathlibExpanduser
body: "`os.path.expanduser()` should be replaced by `Path.expanduser()`"
suggestion: ~
fixable: false
@@ -159,7 +159,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathIsdir
name: PathlibIsDir
body: "`os.path.isdir()` should be replaced by `Path.is_dir()`"
suggestion: ~
fixable: false
@@ -172,7 +172,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathIsfile
name: PathlibIsFile
body: "`os.path.isfile()` should be replaced by `Path.is_file()`"
suggestion: ~
fixable: false
@@ -185,7 +185,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathIslink
name: PathlibIsLink
body: "`os.path.islink()` should be replaced by `Path.is_symlink()`"
suggestion: ~
fixable: false
@@ -198,7 +198,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsReadlink
name: PathlibReadlink
body: "`os.readlink()` should be replaced by `Path.readlink()`"
suggestion: ~
fixable: false
@@ -211,7 +211,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsStat
name: PathlibStat
body: "`os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`"
suggestion: ~
fixable: false
@@ -224,7 +224,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathIsabs
name: PathlibIsAbs
body: "`os.path.isabs()` should be replaced by `Path.is_absolute()`"
suggestion: ~
fixable: false
@@ -237,7 +237,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathJoin
name: PathlibJoin
body: "`os.path.join()` should be replaced by `Path` with `/` operator"
suggestion: ~
fixable: false
@@ -250,7 +250,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathBasename
name: PathlibBasename
body: "`os.path.basename()` should be replaced by `Path.name`"
suggestion: ~
fixable: false
@@ -263,7 +263,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathDirname
name: PathlibDirname
body: "`os.path.dirname()` should be replaced by `Path.parent`"
suggestion: ~
fixable: false
@@ -276,7 +276,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathSamefile
name: PathlibSamefile
body: "`os.path.samefile()` should be replaced by `Path.samefile()`"
suggestion: ~
fixable: false
@@ -289,7 +289,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathSplitext
name: PathlibSplitext
body: "`os.path.splitext()` should be replaced by `Path.suffix`"
suggestion: ~
fixable: false
@@ -302,7 +302,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: BuiltinOpen
name: PathlibOpen
body: "`open()` should be replaced by `Path.open()`"
suggestion: ~
fixable: false
@@ -315,7 +315,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: BuiltinOpen
name: PathlibOpen
body: "`open()` should be replaced by `Path.open()`"
suggestion: ~
fixable: false
@@ -328,7 +328,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsGetcwd
name: PathlibGetcwd
body: "`os.getcwd()` should be replaced by `Path.cwd()`"
suggestion: ~
fixable: false

View File

@@ -3,7 +3,7 @@ source: crates/ruff/src/rules/flake8_use_pathlib/mod.rs
expression: diagnostics
---
- kind:
name: OsPathAbspath
name: PathlibAbspath
body: "`os.path.abspath()` should be replaced by `Path.resolve()`"
suggestion: ~
fixable: false
@@ -16,7 +16,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsChmod
name: PathlibChmod
body: "`os.chmod()` should be replaced by `Path.chmod()`"
suggestion: ~
fixable: false
@@ -29,7 +29,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsMkdir
name: PathlibMkdir
body: "`os.mkdir()` should be replaced by `Path.mkdir()`"
suggestion: ~
fixable: false
@@ -42,7 +42,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsMakedirs
name: PathlibMakedirs
body: "`os.makedirs()` should be replaced by `Path.mkdir(parents=True)`"
suggestion: ~
fixable: false
@@ -55,7 +55,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsRename
name: PathlibRename
body: "`os.rename()` should be replaced by `Path.rename()`"
suggestion: ~
fixable: false
@@ -81,7 +81,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsRmdir
name: PathlibRmdir
body: "`os.rmdir()` should be replaced by `Path.rmdir()`"
suggestion: ~
fixable: false
@@ -94,7 +94,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsRemove
name: PathlibRemove
body: "`os.remove()` should be replaced by `Path.unlink()`"
suggestion: ~
fixable: false
@@ -107,7 +107,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsUnlink
name: PathlibUnlink
body: "`os.unlink()` should be replaced by `Path.unlink()`"
suggestion: ~
fixable: false
@@ -120,7 +120,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsGetcwd
name: PathlibGetcwd
body: "`os.getcwd()` should be replaced by `Path.cwd()`"
suggestion: ~
fixable: false
@@ -133,7 +133,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathExists
name: PathlibExists
body: "`os.path.exists()` should be replaced by `Path.exists()`"
suggestion: ~
fixable: false
@@ -146,7 +146,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathExpanduser
name: PathlibExpanduser
body: "`os.path.expanduser()` should be replaced by `Path.expanduser()`"
suggestion: ~
fixable: false
@@ -159,7 +159,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathIsdir
name: PathlibIsDir
body: "`os.path.isdir()` should be replaced by `Path.is_dir()`"
suggestion: ~
fixable: false
@@ -172,7 +172,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathIsfile
name: PathlibIsFile
body: "`os.path.isfile()` should be replaced by `Path.is_file()`"
suggestion: ~
fixable: false
@@ -185,7 +185,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathIslink
name: PathlibIsLink
body: "`os.path.islink()` should be replaced by `Path.is_symlink()`"
suggestion: ~
fixable: false
@@ -198,7 +198,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsReadlink
name: PathlibReadlink
body: "`os.readlink()` should be replaced by `Path.readlink()`"
suggestion: ~
fixable: false
@@ -211,7 +211,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsStat
name: PathlibStat
body: "`os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`"
suggestion: ~
fixable: false
@@ -224,7 +224,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathIsabs
name: PathlibIsAbs
body: "`os.path.isabs()` should be replaced by `Path.is_absolute()`"
suggestion: ~
fixable: false
@@ -237,7 +237,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathJoin
name: PathlibJoin
body: "`os.path.join()` should be replaced by `Path` with `/` operator"
suggestion: ~
fixable: false
@@ -250,7 +250,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathBasename
name: PathlibBasename
body: "`os.path.basename()` should be replaced by `Path.name`"
suggestion: ~
fixable: false
@@ -263,7 +263,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathDirname
name: PathlibDirname
body: "`os.path.dirname()` should be replaced by `Path.parent`"
suggestion: ~
fixable: false
@@ -276,7 +276,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathSamefile
name: PathlibSamefile
body: "`os.path.samefile()` should be replaced by `Path.samefile()`"
suggestion: ~
fixable: false
@@ -289,7 +289,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathSplitext
name: PathlibSplitext
body: "`os.path.splitext()` should be replaced by `Path.suffix`"
suggestion: ~
fixable: false

View File

@@ -3,7 +3,7 @@ source: crates/ruff/src/rules/flake8_use_pathlib/mod.rs
expression: diagnostics
---
- kind:
name: OsPathAbspath
name: PathlibAbspath
body: "`os.path.abspath()` should be replaced by `Path.resolve()`"
suggestion: ~
fixable: false
@@ -16,7 +16,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsChmod
name: PathlibChmod
body: "`os.chmod()` should be replaced by `Path.chmod()`"
suggestion: ~
fixable: false
@@ -29,7 +29,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsMkdir
name: PathlibMkdir
body: "`os.mkdir()` should be replaced by `Path.mkdir()`"
suggestion: ~
fixable: false
@@ -42,7 +42,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsMakedirs
name: PathlibMakedirs
body: "`os.makedirs()` should be replaced by `Path.mkdir(parents=True)`"
suggestion: ~
fixable: false
@@ -55,7 +55,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsRename
name: PathlibRename
body: "`os.rename()` should be replaced by `Path.rename()`"
suggestion: ~
fixable: false
@@ -81,7 +81,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsRmdir
name: PathlibRmdir
body: "`os.rmdir()` should be replaced by `Path.rmdir()`"
suggestion: ~
fixable: false
@@ -94,7 +94,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsRemove
name: PathlibRemove
body: "`os.remove()` should be replaced by `Path.unlink()`"
suggestion: ~
fixable: false
@@ -107,7 +107,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsUnlink
name: PathlibUnlink
body: "`os.unlink()` should be replaced by `Path.unlink()`"
suggestion: ~
fixable: false
@@ -120,7 +120,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsGetcwd
name: PathlibGetcwd
body: "`os.getcwd()` should be replaced by `Path.cwd()`"
suggestion: ~
fixable: false
@@ -133,7 +133,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathExists
name: PathlibExists
body: "`os.path.exists()` should be replaced by `Path.exists()`"
suggestion: ~
fixable: false
@@ -146,7 +146,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathExpanduser
name: PathlibExpanduser
body: "`os.path.expanduser()` should be replaced by `Path.expanduser()`"
suggestion: ~
fixable: false
@@ -159,7 +159,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathIsdir
name: PathlibIsDir
body: "`os.path.isdir()` should be replaced by `Path.is_dir()`"
suggestion: ~
fixable: false
@@ -172,7 +172,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathIsfile
name: PathlibIsFile
body: "`os.path.isfile()` should be replaced by `Path.is_file()`"
suggestion: ~
fixable: false
@@ -185,7 +185,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathIslink
name: PathlibIsLink
body: "`os.path.islink()` should be replaced by `Path.is_symlink()`"
suggestion: ~
fixable: false
@@ -198,7 +198,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsReadlink
name: PathlibReadlink
body: "`os.readlink()` should be replaced by `Path.readlink()`"
suggestion: ~
fixable: false
@@ -211,7 +211,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsStat
name: PathlibStat
body: "`os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`"
suggestion: ~
fixable: false
@@ -224,7 +224,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathIsabs
name: PathlibIsAbs
body: "`os.path.isabs()` should be replaced by `Path.is_absolute()`"
suggestion: ~
fixable: false
@@ -237,7 +237,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathJoin
name: PathlibJoin
body: "`os.path.join()` should be replaced by `Path` with `/` operator"
suggestion: ~
fixable: false
@@ -250,7 +250,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathBasename
name: PathlibBasename
body: "`os.path.basename()` should be replaced by `Path.name`"
suggestion: ~
fixable: false
@@ -263,7 +263,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathDirname
name: PathlibDirname
body: "`os.path.dirname()` should be replaced by `Path.parent`"
suggestion: ~
fixable: false
@@ -276,7 +276,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathSamefile
name: PathlibSamefile
body: "`os.path.samefile()` should be replaced by `Path.samefile()`"
suggestion: ~
fixable: false
@@ -289,7 +289,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathSplitext
name: PathlibSplitext
body: "`os.path.splitext()` should be replaced by `Path.suffix`"
suggestion: ~
fixable: false
@@ -302,7 +302,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: BuiltinOpen
name: PathlibOpen
body: "`open()` should be replaced by `Path.open()`"
suggestion: ~
fixable: false
@@ -315,7 +315,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: BuiltinOpen
name: PathlibOpen
body: "`open()` should be replaced by `Path.open()`"
suggestion: ~
fixable: false

View File

@@ -3,7 +3,7 @@ source: crates/ruff/src/rules/flake8_use_pathlib/mod.rs
expression: diagnostics
---
- kind:
name: OsPathAbspath
name: PathlibAbspath
body: "`os.path.abspath()` should be replaced by `Path.resolve()`"
suggestion: ~
fixable: false
@@ -16,7 +16,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsChmod
name: PathlibChmod
body: "`os.chmod()` should be replaced by `Path.chmod()`"
suggestion: ~
fixable: false
@@ -29,7 +29,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsMkdir
name: PathlibMkdir
body: "`os.mkdir()` should be replaced by `Path.mkdir()`"
suggestion: ~
fixable: false
@@ -42,7 +42,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsMakedirs
name: PathlibMakedirs
body: "`os.makedirs()` should be replaced by `Path.mkdir(parents=True)`"
suggestion: ~
fixable: false
@@ -55,7 +55,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsRename
name: PathlibRename
body: "`os.rename()` should be replaced by `Path.rename()`"
suggestion: ~
fixable: false
@@ -81,7 +81,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsRmdir
name: PathlibRmdir
body: "`os.rmdir()` should be replaced by `Path.rmdir()`"
suggestion: ~
fixable: false
@@ -94,7 +94,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsRemove
name: PathlibRemove
body: "`os.remove()` should be replaced by `Path.unlink()`"
suggestion: ~
fixable: false
@@ -107,7 +107,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsUnlink
name: PathlibUnlink
body: "`os.unlink()` should be replaced by `Path.unlink()`"
suggestion: ~
fixable: false
@@ -120,7 +120,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsGetcwd
name: PathlibGetcwd
body: "`os.getcwd()` should be replaced by `Path.cwd()`"
suggestion: ~
fixable: false
@@ -133,7 +133,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathExists
name: PathlibExists
body: "`os.path.exists()` should be replaced by `Path.exists()`"
suggestion: ~
fixable: false
@@ -146,7 +146,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathExpanduser
name: PathlibExpanduser
body: "`os.path.expanduser()` should be replaced by `Path.expanduser()`"
suggestion: ~
fixable: false
@@ -159,7 +159,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathIsdir
name: PathlibIsDir
body: "`os.path.isdir()` should be replaced by `Path.is_dir()`"
suggestion: ~
fixable: false
@@ -172,7 +172,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathIsfile
name: PathlibIsFile
body: "`os.path.isfile()` should be replaced by `Path.is_file()`"
suggestion: ~
fixable: false
@@ -185,7 +185,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathIslink
name: PathlibIsLink
body: "`os.path.islink()` should be replaced by `Path.is_symlink()`"
suggestion: ~
fixable: false
@@ -198,7 +198,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsReadlink
name: PathlibReadlink
body: "`os.readlink()` should be replaced by `Path.readlink()`"
suggestion: ~
fixable: false
@@ -211,7 +211,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsStat
name: PathlibStat
body: "`os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`"
suggestion: ~
fixable: false
@@ -224,7 +224,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathIsabs
name: PathlibIsAbs
body: "`os.path.isabs()` should be replaced by `Path.is_absolute()`"
suggestion: ~
fixable: false
@@ -237,7 +237,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathJoin
name: PathlibJoin
body: "`os.path.join()` should be replaced by `Path` with `/` operator"
suggestion: ~
fixable: false
@@ -250,7 +250,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathBasename
name: PathlibBasename
body: "`os.path.basename()` should be replaced by `Path.name`"
suggestion: ~
fixable: false
@@ -263,7 +263,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathDirname
name: PathlibDirname
body: "`os.path.dirname()` should be replaced by `Path.parent`"
suggestion: ~
fixable: false
@@ -276,7 +276,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathSamefile
name: PathlibSamefile
body: "`os.path.samefile()` should be replaced by `Path.samefile()`"
suggestion: ~
fixable: false
@@ -289,7 +289,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: OsPathSplitext
name: PathlibSplitext
body: "`os.path.splitext()` should be replaced by `Path.suffix`"
suggestion: ~
fixable: false

View File

@@ -3,9 +3,9 @@ use ruff_macros::{derive_message_formats, violation};
// PTH100
#[violation]
pub struct OsPathAbspath;
pub struct PathlibAbspath;
impl Violation for OsPathAbspath {
impl Violation for PathlibAbspath {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.abspath()` should be replaced by `Path.resolve()`")
@@ -14,9 +14,9 @@ impl Violation for OsPathAbspath {
// PTH101
#[violation]
pub struct OsChmod;
pub struct PathlibChmod;
impl Violation for OsChmod {
impl Violation for PathlibChmod {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.chmod()` should be replaced by `Path.chmod()`")
@@ -25,9 +25,9 @@ impl Violation for OsChmod {
// PTH102
#[violation]
pub struct OsMakedirs;
pub struct PathlibMakedirs;
impl Violation for OsMakedirs {
impl Violation for PathlibMakedirs {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.makedirs()` should be replaced by `Path.mkdir(parents=True)`")
@@ -36,9 +36,9 @@ impl Violation for OsMakedirs {
// PTH103
#[violation]
pub struct OsMkdir;
pub struct PathlibMkdir;
impl Violation for OsMkdir {
impl Violation for PathlibMkdir {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.mkdir()` should be replaced by `Path.mkdir()`")
@@ -47,9 +47,9 @@ impl Violation for OsMkdir {
// PTH104
#[violation]
pub struct OsRename;
pub struct PathlibRename;
impl Violation for OsRename {
impl Violation for PathlibRename {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.rename()` should be replaced by `Path.rename()`")
@@ -69,9 +69,9 @@ impl Violation for PathlibReplace {
// PTH106
#[violation]
pub struct OsRmdir;
pub struct PathlibRmdir;
impl Violation for OsRmdir {
impl Violation for PathlibRmdir {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.rmdir()` should be replaced by `Path.rmdir()`")
@@ -80,9 +80,9 @@ impl Violation for OsRmdir {
// PTH107
#[violation]
pub struct OsRemove;
pub struct PathlibRemove;
impl Violation for OsRemove {
impl Violation for PathlibRemove {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.remove()` should be replaced by `Path.unlink()`")
@@ -91,9 +91,9 @@ impl Violation for OsRemove {
// PTH108
#[violation]
pub struct OsUnlink;
pub struct PathlibUnlink;
impl Violation for OsUnlink {
impl Violation for PathlibUnlink {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.unlink()` should be replaced by `Path.unlink()`")
@@ -102,9 +102,9 @@ impl Violation for OsUnlink {
// PTH109
#[violation]
pub struct OsGetcwd;
pub struct PathlibGetcwd;
impl Violation for OsGetcwd {
impl Violation for PathlibGetcwd {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.getcwd()` should be replaced by `Path.cwd()`")
@@ -113,9 +113,9 @@ impl Violation for OsGetcwd {
// PTH110
#[violation]
pub struct OsPathExists;
pub struct PathlibExists;
impl Violation for OsPathExists {
impl Violation for PathlibExists {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.exists()` should be replaced by `Path.exists()`")
@@ -124,9 +124,9 @@ impl Violation for OsPathExists {
// PTH111
#[violation]
pub struct OsPathExpanduser;
pub struct PathlibExpanduser;
impl Violation for OsPathExpanduser {
impl Violation for PathlibExpanduser {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.expanduser()` should be replaced by `Path.expanduser()`")
@@ -135,9 +135,9 @@ impl Violation for OsPathExpanduser {
// PTH112
#[violation]
pub struct OsPathIsdir;
pub struct PathlibIsDir;
impl Violation for OsPathIsdir {
impl Violation for PathlibIsDir {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.isdir()` should be replaced by `Path.is_dir()`")
@@ -146,9 +146,9 @@ impl Violation for OsPathIsdir {
// PTH113
#[violation]
pub struct OsPathIsfile;
pub struct PathlibIsFile;
impl Violation for OsPathIsfile {
impl Violation for PathlibIsFile {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.isfile()` should be replaced by `Path.is_file()`")
@@ -157,9 +157,9 @@ impl Violation for OsPathIsfile {
// PTH114
#[violation]
pub struct OsPathIslink;
pub struct PathlibIsLink;
impl Violation for OsPathIslink {
impl Violation for PathlibIsLink {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.islink()` should be replaced by `Path.is_symlink()`")
@@ -168,9 +168,9 @@ impl Violation for OsPathIslink {
// PTH115
#[violation]
pub struct OsReadlink;
pub struct PathlibReadlink;
impl Violation for OsReadlink {
impl Violation for PathlibReadlink {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.readlink()` should be replaced by `Path.readlink()`")
@@ -179,9 +179,9 @@ impl Violation for OsReadlink {
// PTH116
#[violation]
pub struct OsStat;
pub struct PathlibStat;
impl Violation for OsStat {
impl Violation for PathlibStat {
#[derive_message_formats]
fn message(&self) -> String {
format!(
@@ -192,9 +192,9 @@ impl Violation for OsStat {
// PTH117
#[violation]
pub struct OsPathIsabs;
pub struct PathlibIsAbs;
impl Violation for OsPathIsabs {
impl Violation for PathlibIsAbs {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.isabs()` should be replaced by `Path.is_absolute()`")
@@ -203,9 +203,9 @@ impl Violation for OsPathIsabs {
// PTH118
#[violation]
pub struct OsPathJoin;
pub struct PathlibJoin;
impl Violation for OsPathJoin {
impl Violation for PathlibJoin {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.join()` should be replaced by `Path` with `/` operator")
@@ -214,9 +214,9 @@ impl Violation for OsPathJoin {
// PTH119
#[violation]
pub struct OsPathBasename;
pub struct PathlibBasename;
impl Violation for OsPathBasename {
impl Violation for PathlibBasename {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.basename()` should be replaced by `Path.name`")
@@ -225,9 +225,9 @@ impl Violation for OsPathBasename {
// PTH120
#[violation]
pub struct OsPathDirname;
pub struct PathlibDirname;
impl Violation for OsPathDirname {
impl Violation for PathlibDirname {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.dirname()` should be replaced by `Path.parent`")
@@ -236,9 +236,9 @@ impl Violation for OsPathDirname {
// PTH121
#[violation]
pub struct OsPathSamefile;
pub struct PathlibSamefile;
impl Violation for OsPathSamefile {
impl Violation for PathlibSamefile {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.samefile()` should be replaced by `Path.samefile()`")
@@ -247,9 +247,9 @@ impl Violation for OsPathSamefile {
// PTH122
#[violation]
pub struct OsPathSplitext;
pub struct PathlibSplitext;
impl Violation for OsPathSplitext {
impl Violation for PathlibSplitext {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.splitext()` should be replaced by `Path.suffix`")
@@ -258,9 +258,9 @@ impl Violation for OsPathSplitext {
// PTH123
#[violation]
pub struct BuiltinOpen;
pub struct PathlibOpen;
impl Violation for BuiltinOpen {
impl Violation for PathlibOpen {
#[derive_message_formats]
fn message(&self) -> String {
format!("`open()` should be replaced by `Path.open()`")
@@ -269,9 +269,9 @@ impl Violation for BuiltinOpen {
// PTH124
#[violation]
pub struct PyPath;
pub struct PathlibPyPath;
impl Violation for PyPath {
impl Violation for PathlibPyPath {
#[derive_message_formats]
fn message(&self) -> String {
format!("`py.path` is in maintenance mode, use `pathlib` instead")

View File

@@ -665,7 +665,6 @@ mod tests {
&Settings {
isort: super::settings::Settings {
force_sort_within_sections: true,
force_to_top: BTreeSet::from(["z".to_string()]),
..super::settings::Settings::default()
},
src: vec![test_resource_path("fixtures/isort")],

View File

@@ -11,15 +11,15 @@ expression: diagnostics
row: 1
column: 0
end_location:
row: 14
row: 12
column: 0
fix:
content: "import a # import\nimport b as b1 # import_as\nimport c.d\nimport z\nfrom a import a1 # import_from\nfrom c import * # import_from_star\nfrom z import z1\n\nfrom ...grandparent import fn3\nfrom ..parent import *\nfrom . import my\nfrom .my import fn\nfrom .my.nested import fn2\n"
content: "import a # import\nimport b as b1 # import_as\nimport c.d\nfrom a import a1 # import_from\nfrom c import * # import_from_star\n\nfrom ...grandparent import fn3\nfrom ..parent import *\nfrom . import my\nfrom .my import fn\nfrom .my.nested import fn2\n"
location:
row: 1
column: 0
end_location:
row: 14
row: 12
column: 0
parent: ~

View File

@@ -11,15 +11,15 @@ expression: diagnostics
row: 1
column: 0
end_location:
row: 14
row: 12
column: 0
fix:
content: "import z\nfrom z import z1\nimport a # import\nfrom a import a1 # import_from\nimport b as b1 # import_as\nfrom c import * # import_from_star\nimport c.d\n\nfrom ...grandparent import fn3\nfrom ..parent import *\nfrom . import my\nfrom .my import fn\nfrom .my.nested import fn2\n"
content: "import a # import\nfrom a import a1 # import_from\nimport b as b1 # import_as\nfrom c import * # import_from_star\nimport c.d\n\nfrom ...grandparent import fn3\nfrom ..parent import *\nfrom . import my\nfrom .my import fn\nfrom .my.nested import fn2\n"
location:
row: 1
column: 0
end_location:
row: 14
row: 12
column: 0
parent: ~

View File

@@ -44,28 +44,29 @@ fn prefix(
}
}
/// Compare two module names' by their `force-to-top`ness.
fn cmp_force_to_top(name1: &str, name2: &str, force_to_top: &BTreeSet<String>) -> Ordering {
let force_to_top1 = force_to_top.contains(name1);
let force_to_top2 = force_to_top.contains(name2);
force_to_top1.cmp(&force_to_top2).reverse()
}
/// Compare two top-level modules.
pub fn cmp_modules(
alias1: &AliasData,
alias2: &AliasData,
force_to_top: &BTreeSet<String>,
) -> Ordering {
cmp_force_to_top(alias1.name, alias2.name, force_to_top)
.then_with(|| natord::compare_ignore_case(alias1.name, alias2.name))
.then_with(|| natord::compare(alias1.name, alias2.name))
.then_with(|| match (alias1.asname, alias2.asname) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Less,
(Some(_), None) => Ordering::Greater,
(Some(asname1), Some(asname2)) => natord::compare(asname1, asname2),
})
(match (
force_to_top.contains(alias1.name),
force_to_top.contains(alias2.name),
) {
(true, true) => Ordering::Equal,
(false, false) => Ordering::Equal,
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
})
.then_with(|| natord::compare_ignore_case(alias1.name, alias2.name))
.then_with(|| natord::compare(alias1.name, alias2.name))
.then_with(|| match (alias1.asname, alias2.asname) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Less,
(Some(_), None) => Ordering::Greater,
(Some(asname1), Some(asname2)) => natord::compare(asname1, asname2),
})
}
/// Compare two member imports within `StmtKind::ImportFrom` blocks.
@@ -123,11 +124,15 @@ pub fn cmp_import_from(
relative_imports_order,
)
.then_with(|| {
cmp_force_to_top(
&import_from1.module_name(),
&import_from2.module_name(),
force_to_top,
)
match (
force_to_top.contains(&import_from1.module_name()),
force_to_top.contains(&import_from2.module_name()),
) {
(true, true) => Ordering::Equal,
(false, false) => Ordering::Equal,
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
}
})
.then_with(|| match (&import_from1.module, import_from2.module) {
(None, None) => Ordering::Equal,
@@ -138,17 +143,6 @@ pub fn cmp_import_from(
})
}
/// Compare an import to an import-from.
fn cmp_import_import_from(
import: &AliasData,
import_from: &ImportFromData,
force_to_top: &BTreeSet<String>,
) -> Ordering {
cmp_force_to_top(import.name, &import_from.module_name(), force_to_top).then_with(|| {
natord::compare_ignore_case(import.name, import_from.module.unwrap_or_default())
})
}
/// Compare two [`EitherImport`] enums which may be [`Import`] or [`ImportFrom`]
/// structs.
pub fn cmp_either_import(
@@ -160,10 +154,10 @@ pub fn cmp_either_import(
match (a, b) {
(Import((alias1, _)), Import((alias2, _))) => cmp_modules(alias1, alias2, force_to_top),
(ImportFrom((import_from, ..)), Import((alias, _))) => {
cmp_import_import_from(alias, import_from, force_to_top).reverse()
natord::compare_ignore_case(import_from.module.unwrap_or_default(), alias.name)
}
(Import((alias, _)), ImportFrom((import_from, ..))) => {
cmp_import_import_from(alias, import_from, force_to_top)
natord::compare_ignore_case(alias.name, import_from.module.unwrap_or_default())
}
(ImportFrom((import_from1, ..)), ImportFrom((import_from2, ..))) => cmp_import_from(
import_from1,

View File

@@ -33,12 +33,7 @@ impl AlwaysAutofixableViolation for MissingWhitespace {
/// E231
#[cfg(debug_assertions)]
pub fn missing_whitespace(
line: &str,
row: usize,
autofix: bool,
indent_level: usize,
) -> Vec<Diagnostic> {
pub fn missing_whitespace(line: &str, row: usize, autofix: bool) -> Vec<Diagnostic> {
let mut diagnostics = vec![];
for (idx, char) in line.chars().enumerate() {
if idx + 1 == line.len() {
@@ -67,17 +62,11 @@ pub fn missing_whitespace(
let mut diagnostic = Diagnostic::new(
kind,
Range::new(
Location::new(row, indent_level + idx),
Location::new(row, indent_level + idx),
),
Range::new(Location::new(row, idx), Location::new(row, idx)),
);
if autofix {
diagnostic.amend(Fix::insertion(
" ".to_string(),
Location::new(row, indent_level + idx + 1),
));
diagnostic.amend(Fix::insertion(" ".to_string(), Location::new(row, idx + 1)));
}
diagnostics.push(diagnostic);
}
@@ -86,11 +75,6 @@ pub fn missing_whitespace(
}
#[cfg(not(debug_assertions))]
pub fn missing_whitespace(
_line: &str,
_row: usize,
_autofix: bool,
indent_level: usize,
) -> Vec<Diagnostic> {
pub fn missing_whitespace(_line: &str, _row: usize, _autofix: bool) -> Vec<Diagnostic> {
vec![]
}

View File

@@ -62,24 +62,4 @@ expression: diagnostics
row: 6
column: 10
parent: ~
- kind:
name: MissingWhitespace
body: "Missing whitespace after ','"
suggestion: "Added missing whitespace after ','"
fixable: true
location:
row: 19
column: 9
end_location:
row: 19
column: 9
fix:
content: " "
location:
row: 19
column: 10
end_location:
row: 19
column: 10
parent: ~

View File

@@ -9,52 +9,46 @@ use ruff_python_stdlib::future::ALL_FEATURE_NAMES;
use crate::checkers::ast::Checker;
#[derive(Debug, PartialEq, Eq)]
pub enum UnusedImportContext {
ExceptHandler,
Init,
}
#[violation]
pub struct UnusedImport {
pub name: String,
pub context: Option<UnusedImportContext>,
pub ignore_init: bool,
pub multiple: bool,
}
fn fmt_unused_import_autofix_msg(unused_import: &UnusedImport) -> String {
let UnusedImport { name, multiple, .. } = unused_import;
if *multiple {
"Remove unused import".to_string()
} else {
format!("Remove unused import: `{name}`")
}
}
impl Violation for UnusedImport {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let UnusedImport { name, context, .. } = self;
match context {
Some(UnusedImportContext::ExceptHandler) => {
format!(
"`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
)
}
Some(UnusedImportContext::Init) => {
format!(
"`{name}` imported but unused; consider adding to `__all__` or using a redundant \
alias"
)
}
None => format!("`{name}` imported but unused"),
let UnusedImport {
name, ignore_init, ..
} = self;
if *ignore_init {
format!(
"`{name}` imported but unused; consider adding to `__all__` or using a redundant \
alias"
)
} else {
format!("`{name}` imported but unused")
}
}
fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
let UnusedImport { context, .. } = self;
context
.is_none()
.then_some(|UnusedImport { name, multiple, .. }| {
if *multiple {
"Remove unused import".to_string()
} else {
format!("Remove unused import: `{name}`")
}
})
let UnusedImport { ignore_init, .. } = self;
if *ignore_init {
None
} else {
Some(fmt_unused_import_autofix_msg)
}
}
}
#[violation]

View File

@@ -10,7 +10,7 @@ pub use if_tuple::{if_tuple, IfTuple};
pub use imports::{
future_feature_not_defined, FutureFeatureNotDefined, ImportShadowedByLoopVar, LateFutureImport,
UndefinedLocalWithImportStar, UndefinedLocalWithImportStarUsage,
UndefinedLocalWithNestedImportStarUsage, UnusedImport, UnusedImportContext,
UndefinedLocalWithNestedImportStarUsage, UnusedImport,
};
pub use invalid_literal_comparisons::{invalid_literal_comparison, IsLiteral};
pub use invalid_print_syntax::{invalid_print_syntax, InvalidPrintSyntax};

View File

@@ -2,37 +2,5 @@
source: crates/ruff/src/rules/pyflakes/mod.rs
expression: diagnostics
---
- kind:
name: UnusedImport
body: "`orjson` imported but unused; consider using `importlib.util.find_spec` to test for availability"
suggestion: ~
fixable: false
location:
row: 6
column: 15
end_location:
row: 6
column: 21
fix: ~
parent: ~
- kind:
name: UnusedImport
body: "`orjson` imported but unused"
suggestion: "Remove unused import: `orjson`"
fixable: true
location:
row: 15
column: 15
end_location:
row: 15
column: 21
fix:
content: ""
location:
row: 15
column: 0
end_location:
row: 16
column: 0
parent: ~
[]

View File

@@ -61,10 +61,10 @@ expression: diagnostics
fixable: false
location:
row: 58
column: 4
column: 3
end_location:
row: 58
column: 7
column: 8
fix: ~
parent: ~
- kind:
@@ -139,10 +139,10 @@ expression: diagnostics
fixable: false
location:
row: 115
column: 9
column: 8
end_location:
row: 115
column: 22
column: 23
fix: ~
parent: ~
- kind:
@@ -152,10 +152,10 @@ expression: diagnostics
fixable: false
location:
row: 123
column: 14
column: 13
end_location:
row: 123
column: 17
column: 18
fix: ~
parent: ~
- kind:
@@ -165,10 +165,10 @@ expression: diagnostics
fixable: false
location:
row: 123
column: 21
column: 20
end_location:
row: 123
column: 24
column: 25
fix: ~
parent: ~

View File

@@ -9,49 +9,49 @@ expression: diagnostics
fixable: false
location:
row: 11
column: 10
column: 9
end_location:
row: 11
column: 15
fix: ~
parent: ~
- kind:
name: UndefinedName
body: "Undefined name `Model`"
suggestion: ~
fixable: false
location:
row: 18
column: 17
end_location:
row: 18
column: 22
fix: ~
parent: ~
- kind:
name: UndefinedName
body: "Undefined name `Model`"
suggestion: ~
fixable: false
location:
row: 24
column: 13
end_location:
row: 24
column: 18
fix: ~
parent: ~
- kind:
name: UndefinedName
body: "Undefined name `Model`"
suggestion: ~
fixable: false
location:
row: 30
column: 11
end_location:
row: 30
column: 16
fix: ~
parent: ~
- kind:
name: UndefinedName
body: "Undefined name `Model`"
suggestion: ~
fixable: false
location:
row: 18
column: 16
end_location:
row: 18
column: 23
fix: ~
parent: ~
- kind:
name: UndefinedName
body: "Undefined name `Model`"
suggestion: ~
fixable: false
location:
row: 24
column: 12
end_location:
row: 24
column: 19
fix: ~
parent: ~
- kind:
name: UndefinedName
body: "Undefined name `Model`"
suggestion: ~
fixable: false
location:
row: 30
column: 10
end_location:
row: 30
column: 17
fix: ~
parent: ~

View File

@@ -9,10 +9,10 @@ expression: diagnostics
fixable: false
location:
row: 18
column: 27
column: 26
end_location:
row: 18
column: 29
column: 30
fix: ~
parent: ~
- kind:
@@ -22,10 +22,10 @@ expression: diagnostics
fixable: false
location:
row: 23
column: 13
column: 12
end_location:
row: 23
column: 16
column: 17
fix: ~
parent: ~

View File

@@ -9,10 +9,10 @@ expression: diagnostics
fixable: false
location:
row: 20
column: 27
column: 26
end_location:
row: 20
column: 29
column: 30
fix: ~
parent: ~
- kind:
@@ -22,10 +22,10 @@ expression: diagnostics
fixable: false
location:
row: 25
column: 13
column: 12
end_location:
row: 25
column: 16
column: 17
fix: ~
parent: ~

View File

@@ -9,10 +9,10 @@ expression: diagnostics
fixable: false
location:
row: 5
column: 12
column: 11
end_location:
row: 5
column: 17
column: 18
fix: ~
parent: ~

View File

@@ -9,10 +9,10 @@ expression: diagnostics
fixable: false
location:
row: 11
column: 9
column: 8
end_location:
row: 11
column: 12
column: 13
fix: ~
parent: ~
- kind:
@@ -22,10 +22,10 @@ expression: diagnostics
fixable: false
location:
row: 11
column: 16
column: 15
end_location:
row: 11
column: 21
column: 22
fix: ~
parent: ~

View File

@@ -9,25 +9,25 @@ expression: diagnostics
fixable: false
location:
row: 4
column: 10
column: 9
end_location:
row: 4
column: 15
fix: ~
parent: ~
- kind:
name: UndefinedName
body: "Undefined name `Model`"
suggestion: ~
fixable: false
location:
row: 9
column: 11
end_location:
row: 9
column: 16
fix: ~
parent: ~
- kind:
name: UndefinedName
body: "Undefined name `Model`"
suggestion: ~
fixable: false
location:
row: 9
column: 10
end_location:
row: 9
column: 17
fix: ~
parent: ~
- kind:
name: UndefinedName
body: "Undefined name `Model`"
@@ -35,10 +35,10 @@ expression: diagnostics
fixable: false
location:
row: 14
column: 15
column: 14
end_location:
row: 14
column: 20
column: 21
fix: ~
parent: ~
- kind:
@@ -48,10 +48,10 @@ expression: diagnostics
fixable: false
location:
row: 19
column: 31
column: 30
end_location:
row: 19
column: 36
column: 37
fix: ~
parent: ~
- kind:
@@ -61,10 +61,10 @@ expression: diagnostics
fixable: false
location:
row: 24
column: 19
column: 18
end_location:
row: 24
column: 24
column: 25
fix: ~
parent: ~

View File

@@ -9,10 +9,10 @@ expression: diagnostics
fixable: false
location:
row: 5
column: 30
column: 29
end_location:
row: 5
column: 40
column: 41
fix: ~
parent: ~

View File

@@ -9,10 +9,10 @@ expression: diagnostics
fixable: false
location:
row: 11
column: 21
column: 20
end_location:
row: 11
column: 30
column: 31
fix: ~
parent: ~
- kind:
@@ -22,10 +22,10 @@ expression: diagnostics
fixable: false
location:
row: 12
column: 26
column: 25
end_location:
row: 12
column: 35
column: 36
fix: ~
parent: ~
- kind:
@@ -35,10 +35,10 @@ expression: diagnostics
fixable: false
location:
row: 13
column: 21
column: 20
end_location:
row: 13
column: 30
column: 31
fix: ~
parent: ~

View File

@@ -9,10 +9,10 @@ expression: diagnostics
fixable: false
location:
row: 6
column: 35
column: 34
end_location:
row: 6
column: 37
column: 38
fix: ~
parent: ~

View File

@@ -9,10 +9,10 @@ expression: diagnostics
fixable: false
location:
row: 7
column: 14
column: 13
end_location:
row: 7
column: 19
column: 20
fix: ~
parent: ~

View File

@@ -9,10 +9,10 @@ expression: diagnostics
fixable: false
location:
row: 26
column: 16
column: 15
end_location:
row: 26
column: 19
column: 20
fix: ~
parent: ~
- kind:
@@ -22,10 +22,10 @@ expression: diagnostics
fixable: false
location:
row: 33
column: 16
column: 15
end_location:
row: 33
column: 19
column: 20
fix: ~
parent: ~

View File

@@ -9,10 +9,10 @@ expression: diagnostics
fixable: false
location:
row: 26
column: 16
column: 15
end_location:
row: 26
column: 19
column: 20
fix: ~
parent: ~

View File

@@ -60,7 +60,7 @@ mod tests {
#[test_case(Rule::TooManyReturnStatements, Path::new("too_many_return_statements.py"); "PLR0911")]
#[test_case(Rule::TooManyStatements, Path::new("too_many_statements.py"); "PLR0915")]
#[test_case(Rule::UnnecessaryDirectLambdaCall, Path::new("unnecessary_direct_lambda_call.py"); "PLC3002")]
#[test_case(Rule::LoadBeforeGlobalDeclaration, Path::new("load_before_global_declaration.py"); "PLE0118")]
#[test_case(Rule::UsePriorToGlobalDeclaration, Path::new("use_prior_to_global_declaration.py"); "PLE0118")]
#[test_case(Rule::UselessElseOnLoop, Path::new("useless_else_on_loop.py"); "PLW0120")]
#[test_case(Rule::UselessImportAlias, Path::new("import_aliasing.py"); "PLC0414")]
#[test_case(Rule::UselessReturn, Path::new("useless_return.py"); "PLR1711")]

View File

@@ -18,9 +18,6 @@ pub use invalid_string_characters::{
invalid_string_characters, InvalidCharacterBackspace, InvalidCharacterEsc, InvalidCharacterNul,
InvalidCharacterSub, InvalidCharacterZeroWidthSpace,
};
pub use load_before_global_declaration::{
load_before_global_declaration, LoadBeforeGlobalDeclaration,
};
pub use logging::{logging_call, LoggingTooFewArgs, LoggingTooManyArgs};
pub use magic_value_comparison::{magic_value_comparison, MagicValueComparison};
pub use manual_import_from::{manual_from_import, ManualFromImport};
@@ -37,6 +34,9 @@ pub use too_many_statements::{too_many_statements, TooManyStatements};
pub use unnecessary_direct_lambda_call::{
unnecessary_direct_lambda_call, UnnecessaryDirectLambdaCall,
};
pub use use_prior_to_global_declaration::{
use_prior_to_global_declaration, UsePriorToGlobalDeclaration,
};
pub use useless_else_on_loop::{useless_else_on_loop, UselessElseOnLoop};
pub use useless_import_alias::{useless_import_alias, UselessImportAlias};
pub use useless_return::{useless_return, UselessReturn};
@@ -59,7 +59,6 @@ mod invalid_all_object;
mod invalid_envvar_default;
mod invalid_envvar_value;
mod invalid_string_characters;
mod load_before_global_declaration;
mod logging;
mod magic_value_comparison;
mod manual_import_from;
@@ -74,6 +73,7 @@ mod too_many_branches;
mod too_many_return_statements;
mod too_many_statements;
mod unnecessary_direct_lambda_call;
mod use_prior_to_global_declaration;
mod useless_else_on_loop;
mod useless_import_alias;
mod useless_return;

View File

@@ -8,20 +8,20 @@ use ruff_python_ast::types::Range;
use crate::checkers::ast::Checker;
#[violation]
pub struct LoadBeforeGlobalDeclaration {
pub struct UsePriorToGlobalDeclaration {
pub name: String,
pub line: usize,
}
impl Violation for LoadBeforeGlobalDeclaration {
impl Violation for UsePriorToGlobalDeclaration {
#[derive_message_formats]
fn message(&self) -> String {
let LoadBeforeGlobalDeclaration { name, line } = self;
let UsePriorToGlobalDeclaration { name, line } = self;
format!("Name `{name}` is used prior to global declaration on line {line}")
}
}
/// PLE0118
pub fn load_before_global_declaration(checker: &mut Checker, name: &str, expr: &Expr) {
pub fn use_prior_to_global_declaration(checker: &mut Checker, name: &str, expr: &Expr) {
let globals = match &checker.ctx.scope().kind {
ScopeKind::Class(class_def) => &class_def.globals,
ScopeKind::Function(function_def) => &function_def.globals,
@@ -30,7 +30,7 @@ pub fn load_before_global_declaration(checker: &mut Checker, name: &str, expr: &
if let Some(stmt) = globals.get(name) {
if expr.location < stmt.location {
checker.diagnostics.push(Diagnostic::new(
LoadBeforeGlobalDeclaration {
UsePriorToGlobalDeclaration {
name: name.to_string(),
line: stmt.location.row(),
},

View File

@@ -3,7 +3,7 @@ source: crates/ruff/src/rules/pylint/mod.rs
expression: diagnostics
---
- kind:
name: LoadBeforeGlobalDeclaration
name: UsePriorToGlobalDeclaration
body: "Name `x` is used prior to global declaration on line 7"
suggestion: ~
fixable: false
@@ -16,7 +16,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: LoadBeforeGlobalDeclaration
name: UsePriorToGlobalDeclaration
body: "Name `x` is used prior to global declaration on line 17"
suggestion: ~
fixable: false
@@ -29,7 +29,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: LoadBeforeGlobalDeclaration
name: UsePriorToGlobalDeclaration
body: "Name `x` is used prior to global declaration on line 25"
suggestion: ~
fixable: false
@@ -42,7 +42,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: LoadBeforeGlobalDeclaration
name: UsePriorToGlobalDeclaration
body: "Name `x` is used prior to global declaration on line 35"
suggestion: ~
fixable: false
@@ -55,7 +55,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: LoadBeforeGlobalDeclaration
name: UsePriorToGlobalDeclaration
body: "Name `x` is used prior to global declaration on line 43"
suggestion: ~
fixable: false
@@ -68,7 +68,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: LoadBeforeGlobalDeclaration
name: UsePriorToGlobalDeclaration
body: "Name `x` is used prior to global declaration on line 53"
suggestion: ~
fixable: false
@@ -81,7 +81,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: LoadBeforeGlobalDeclaration
name: UsePriorToGlobalDeclaration
body: "Name `x` is used prior to global declaration on line 61"
suggestion: ~
fixable: false
@@ -94,7 +94,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: LoadBeforeGlobalDeclaration
name: UsePriorToGlobalDeclaration
body: "Name `x` is used prior to global declaration on line 71"
suggestion: ~
fixable: false
@@ -107,7 +107,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: LoadBeforeGlobalDeclaration
name: UsePriorToGlobalDeclaration
body: "Name `x` is used prior to global declaration on line 79"
suggestion: ~
fixable: false
@@ -120,7 +120,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: LoadBeforeGlobalDeclaration
name: UsePriorToGlobalDeclaration
body: "Name `x` is used prior to global declaration on line 89"
suggestion: ~
fixable: false
@@ -133,7 +133,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: LoadBeforeGlobalDeclaration
name: UsePriorToGlobalDeclaration
body: "Name `x` is used prior to global declaration on line 97"
suggestion: ~
fixable: false
@@ -146,7 +146,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: LoadBeforeGlobalDeclaration
name: UsePriorToGlobalDeclaration
body: "Name `x` is used prior to global declaration on line 107"
suggestion: ~
fixable: false
@@ -159,7 +159,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
name: LoadBeforeGlobalDeclaration
name: UsePriorToGlobalDeclaration
body: "Name `x` is used prior to global declaration on line 114"
suggestion: ~
fixable: false

View File

@@ -3,7 +3,6 @@ use rustpython_parser::ast::Expr;
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::Range;
use ruff_python_ast::typing::AnnotationKind;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -41,11 +40,7 @@ pub fn use_pep585_annotation(checker: &mut Checker, expr: &Expr) {
.resolve_call_path(expr)
.and_then(|call_path| call_path.last().copied())
{
let fixable = checker
.ctx
.in_deferred_string_type_definition
.as_ref()
.map_or(true, AnnotationKind::is_simple);
let fixable = !checker.ctx.in_deferred_string_type_definition;
let mut diagnostic = Diagnostic::new(
NonPEP585Annotation {
name: binding.to_string(),

View File

@@ -4,7 +4,6 @@ use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::unparse_expr;
use ruff_python_ast::types::Range;
use ruff_python_ast::typing::AnnotationKind;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -100,11 +99,7 @@ pub fn use_pep604_annotation(checker: &mut Checker, expr: &Expr, value: &Expr, s
};
// Avoid fixing forward references.
let fixable = checker
.ctx
.in_deferred_string_type_definition
.as_ref()
.map_or(true, AnnotationKind::is_simple);
let fixable = !checker.ctx.in_deferred_string_type_definition;
match typing_member {
TypingMember::Optional => {

View File

@@ -82,97 +82,30 @@ expression: diagnostics
row: 25
column: 14
parent: ~
- kind:
name: NonPEP585Annotation
body: "Use `list` instead of `List` for type annotations"
suggestion: "Replace `List` with `list`"
fixable: true
location:
row: 29
column: 10
end_location:
row: 29
column: 14
fix:
content: list
location:
row: 29
column: 10
end_location:
row: 29
column: 14
parent: ~
- kind:
name: NonPEP585Annotation
body: "Use `list` instead of `List` for type annotations"
suggestion: "Replace `List` with `list`"
fixable: true
location:
row: 33
column: 11
end_location:
row: 33
column: 15
fix:
content: list
location:
row: 33
column: 11
end_location:
row: 33
column: 15
parent: ~
- kind:
name: NonPEP585Annotation
body: "Use `list` instead of `List` for type annotations"
suggestion: "Replace `List` with `list`"
fixable: true
location:
row: 37
column: 10
end_location:
row: 37
column: 14
fix:
content: list
location:
row: 37
column: 10
end_location:
row: 37
column: 14
parent: ~
- kind:
name: NonPEP585Annotation
body: "Use `list` instead of `List` for type annotations"
suggestion: "Replace `List` with `list`"
fixable: true
location:
row: 41
column: 12
end_location:
row: 41
column: 16
fix:
content: list
location:
row: 41
column: 12
end_location:
row: 41
column: 16
parent: ~
- kind:
name: NonPEP585Annotation
body: "Use `list` instead of `List` for type annotations"
suggestion: ~
fixable: false
location:
row: 45
row: 29
column: 9
end_location:
row: 45
column: 23
row: 29
column: 20
fix: ~
parent: ~
- kind:
name: NonPEP585Annotation
body: "Use `list` instead of `List` for type annotations"
suggestion: "Replace `List` with `list`"
fixable: true
location:
row: 36
column: 9
end_location:
row: 36
column: 13
fix: ~
parent: ~

View File

@@ -145,62 +145,41 @@ expression: diagnostics
- kind:
name: NonPEP604Annotation
body: "Use `X | Y` for type annotations"
suggestion: "Convert to `X | Y`"
fixable: true
suggestion: ~
fixable: false
location:
row: 30
column: 10
column: 9
end_location:
row: 30
column: 46
fix:
content: "str | int | Union[float, bytes]"
location:
row: 30
column: 10
end_location:
row: 30
column: 46
column: 47
fix: ~
parent: ~
- kind:
name: NonPEP604Annotation
body: "Use `X | Y` for type annotations"
suggestion: "Convert to `X | Y`"
fixable: true
suggestion: ~
fixable: false
location:
row: 30
column: 26
column: 9
end_location:
row: 30
column: 45
fix:
content: float | bytes
location:
row: 30
column: 26
end_location:
row: 30
column: 45
column: 47
fix: ~
parent: ~
- kind:
name: NonPEP604Annotation
body: "Use `X | Y` for type annotations"
suggestion: "Convert to `X | Y`"
fixable: true
suggestion: ~
fixable: false
location:
row: 34
column: 10
column: 9
end_location:
row: 34
column: 32
fix:
content: str | int
location:
row: 34
column: 10
end_location:
row: 34
column: 32
column: 33
fix: ~
parent: ~
- kind:
name: NonPEP604Annotation

View File

@@ -93,18 +93,9 @@ pub fn pairwise_over_zipped(checker: &mut Checker, func: &Expr, args: &[Expr]) {
return;
};
// Require exactly two positional arguments.
if args.len() != 2 {
if !(args.len() > 1 && id == "zip" && checker.ctx.is_builtin(id)) {
return;
}
// Require the function to be the builtin `zip`.
if id != "zip" {
return;
}
if !checker.ctx.is_builtin(id) {
return;
}
};
// Allow the first argument to be a `Name` or `Subscript`.
let Some(first_arg_info) = ({

View File

@@ -2,6 +2,45 @@
source: crates/ruff/src/rules/ruff/mod.rs
expression: diagnostics
---
- kind:
name: PairwiseOverZipped
body: "Prefer `itertools.pairwise()` over `zip()` when iterating over successive pairs"
suggestion: ~
fixable: false
location:
row: 13
column: 0
end_location:
row: 13
column: 3
fix: ~
parent: ~
- kind:
name: PairwiseOverZipped
body: "Prefer `itertools.pairwise()` over `zip()` when iterating over successive pairs"
suggestion: ~
fixable: false
location:
row: 14
column: 0
end_location:
row: 14
column: 3
fix: ~
parent: ~
- kind:
name: PairwiseOverZipped
body: "Prefer `itertools.pairwise()` over `zip()` when iterating over successive pairs"
suggestion: ~
fixable: false
location:
row: 15
column: 0
end_location:
row: 15
column: 3
fix: ~
parent: ~
- kind:
name: PairwiseOverZipped
body: "Prefer `itertools.pairwise()` over `zip()` when iterating over successive pairs"
@@ -35,48 +74,9 @@ expression: diagnostics
fixable: false
location:
row: 18
column: 0
column: 5
end_location:
row: 18
column: 3
fix: ~
parent: ~
- kind:
name: PairwiseOverZipped
body: "Prefer `itertools.pairwise()` over `zip()` when iterating over successive pairs"
suggestion: ~
fixable: false
location:
row: 19
column: 0
end_location:
row: 19
column: 3
fix: ~
parent: ~
- kind:
name: PairwiseOverZipped
body: "Prefer `itertools.pairwise()` over `zip()` when iterating over successive pairs"
suggestion: ~
fixable: false
location:
row: 20
column: 0
end_location:
row: 20
column: 3
fix: ~
parent: ~
- kind:
name: PairwiseOverZipped
body: "Prefer `itertools.pairwise()` over `zip()` when iterating over successive pairs"
suggestion: ~
fixable: false
location:
row: 21
column: 5
end_location:
row: 21
column: 8
fix: ~
parent: ~
@@ -86,50 +86,11 @@ expression: diagnostics
suggestion: ~
fixable: false
location:
row: 22
row: 19
column: 5
end_location:
row: 22
row: 19
column: 8
fix: ~
parent: ~
- kind:
name: PairwiseOverZipped
body: "Prefer `itertools.pairwise()` over `zip()` when iterating over successive pairs"
suggestion: ~
fixable: false
location:
row: 23
column: 0
end_location:
row: 23
column: 3
fix: ~
parent: ~
- kind:
name: PairwiseOverZipped
body: "Prefer `itertools.pairwise()` over `zip()` when iterating over successive pairs"
suggestion: ~
fixable: false
location:
row: 24
column: 0
end_location:
row: 24
column: 3
fix: ~
parent: ~
- kind:
name: PairwiseOverZipped
body: "Prefer `itertools.pairwise()` over `zip()` when iterating over successive pairs"
suggestion: ~
fixable: false
location:
row: 25
column: 0
end_location:
row: 25
column: 3
fix: ~
parent: ~

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.0.258"
version = "0.0.257"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = { workspace = true }
rust-version = { workspace = true }

View File

@@ -8,13 +8,12 @@ use smallvec::smallvec;
use ruff_python_stdlib::path::is_python_stub_file;
use ruff_python_stdlib::typing::TYPING_EXTENSIONS;
use crate::helpers::{collect_call_path, from_relative_import};
use crate::helpers::{collect_call_path, from_relative_import, Exceptions};
use crate::scope::{
Binding, BindingId, BindingKind, Bindings, Exceptions, ExecutionContext, Scope, ScopeId,
ScopeKind, ScopeStack, Scopes,
Binding, BindingId, BindingKind, Bindings, ExecutionContext, Scope, ScopeId, ScopeKind,
ScopeStack, Scopes,
};
use crate::types::{CallPath, RefEquality};
use crate::typing::AnnotationKind;
use crate::visibility::{module_visibility, Modifier, VisibleScope};
#[allow(clippy::struct_excessive_bools)]
@@ -42,7 +41,7 @@ pub struct Context<'a> {
pub visible_scope: VisibleScope,
pub in_annotation: bool,
pub in_type_definition: bool,
pub in_deferred_string_type_definition: Option<AnnotationKind>,
pub in_deferred_string_type_definition: bool,
pub in_deferred_type_definition: bool,
pub in_exception_handler: bool,
pub in_literal: bool,
@@ -80,7 +79,7 @@ impl<'a> Context<'a> {
},
in_annotation: false,
in_type_definition: false,
in_deferred_string_type_definition: None,
in_deferred_string_type_definition: false,
in_deferred_type_definition: false,
in_exception_handler: false,
in_literal: false,
@@ -309,24 +308,14 @@ impl<'a> Context<'a> {
self.in_exception_handler
}
/// Return the [`ExecutionContext`] of the current scope.
pub const fn execution_context(&self) -> ExecutionContext {
if self.in_type_checking_block
|| self.in_annotation
|| self.in_deferred_string_type_definition.is_some()
|| self.in_deferred_string_type_definition
{
ExecutionContext::Typing
} else {
ExecutionContext::Runtime
}
}
/// Return the union of all handled exceptions as an [`Exceptions`] bitflag.
pub fn exceptions(&self) -> Exceptions {
let mut exceptions = Exceptions::empty();
for exception in &self.handled_exceptions {
exceptions.insert(*exception);
}
exceptions
}
}

View File

@@ -1,5 +1,6 @@
use std::path::Path;
use bitflags::bitflags;
use itertools::Itertools;
use log::error;
use once_cell::sync::Lazy;
@@ -576,6 +577,13 @@ pub fn has_non_none_keyword(keywords: &[Keyword], keyword: &str) -> bool {
})
}
bitflags! {
pub struct Exceptions: u32 {
const NAME_ERROR = 0b0000_0001;
const MODULE_NOT_FOUND_ERROR = 0b0000_0010;
}
}
/// Extract the names of all handled exceptions.
pub fn extract_handled_exceptions(handlers: &[Excepthandler]) -> Vec<&Expr> {
let mut handled_exceptions = Vec::new();

View File

@@ -1,5 +1,4 @@
use crate::types::{Range, RefEquality};
use bitflags::bitflags;
use rustc_hash::FxHashMap;
use rustpython_parser::ast::{Arguments, Expr, Keyword, Stmt};
use std::num::TryFromIntError;
@@ -223,14 +222,6 @@ impl Default for ScopeStack {
}
}
bitflags! {
pub struct Exceptions: u32 {
const NAME_ERROR = 0b0000_0001;
const MODULE_NOT_FOUND_ERROR = 0b0000_0010;
const IMPORT_ERROR = 0b0000_0100;
}
}
#[derive(Debug, Clone)]
pub struct Binding<'a> {
pub kind: BindingKind<'a>,
@@ -250,8 +241,6 @@ pub struct Binding<'a> {
/// (e.g.) `__future__` imports, explicit re-exports, and other bindings
/// that should be considered used even if they're never referenced.
pub synthetic_usage: Option<(ScopeId, Range)>,
/// The exceptions that were handled when the binding was defined.
pub exceptions: Exceptions,
}
impl<'a> Binding<'a> {

View File

@@ -1,14 +1,8 @@
use anyhow::Result;
use rustpython_parser as parser;
use rustpython_parser::ast::{Expr, ExprKind, Location};
use rustpython_parser::ast::{Expr, ExprKind};
use ruff_python_stdlib::typing::{PEP_585_BUILTINS_ELIGIBLE, PEP_593_SUBSCRIPTS, SUBSCRIPTS};
use crate::context::Context;
use crate::relocate::relocate_expr;
use crate::source_code::Locator;
use crate::str;
use crate::types::Range;
pub enum Callable {
ForwardRef,
@@ -72,48 +66,3 @@ pub fn is_pep585_builtin(expr: &Expr, context: &Context) -> bool {
PEP_585_BUILTINS_ELIGIBLE.contains(&call_path.as_slice())
})
}
#[derive(is_macro::Is)]
pub enum AnnotationKind {
/// The annotation is defined as part a simple string literal,
/// e.g. `x: "List[int]" = []`. Annotations within simple literals
/// can be accurately located. For example, we can underline specific
/// expressions within the annotation and apply automatic fixes, which is
/// not possible for complex string literals.
Simple,
/// The annotation is defined as part of a complex string literal, such as
/// a literal containing an implicit concatenation or escaped characters,
/// e.g. `x: "List" "[int]" = []`. These are comparatively rare, but valid.
Complex,
}
/// Parse a type annotation from a string.
pub fn parse_type_annotation(
value: &str,
range: Range,
locator: &Locator,
) -> Result<(Expr, AnnotationKind)> {
let expression = locator.slice(range);
let body = str::raw_contents(expression);
if body == value {
// The annotation is considered "simple" if and only if the raw representation (e.g.,
// `List[int]` within "List[int]") exactly matches the parsed representation. This
// isn't the case, e.g., for implicit concatenations, or for annotations that contain
// escaped quotes.
let leading_quote = str::leading_quote(expression).unwrap();
let expr = parser::parse_expression_located(
body,
"<filename>",
Location::new(
range.location.row(),
range.location.column() + leading_quote.len(),
),
)?;
Ok((expr, AnnotationKind::Simple))
} else {
// Otherwise, consider this a "complex" annotation.
let mut expr = parser::parse_expression(value, "<filename>")?;
relocate_expr(&mut expr, range);
Ok((expr, AnnotationKind::Complex))
}
}

View File

@@ -36,7 +36,7 @@ natively, including:
- [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/)
- [flake8-datetimez](https://pypi.org/project/flake8-datetimez/)
- [flake8-debugger](https://pypi.org/project/flake8-debugger/)
- [flake8-django](https://pypi.org/project/flake8-django/)
- [flake8-django](https://pypi.org/project/flake8-django/) ([#2817](https://github.com/charliermarsh/ruff/issues/2817))
- [flake8-docstrings](https://pypi.org/project/flake8-docstrings/)
- [flake8-eradicate](https://pypi.org/project/flake8-eradicate/)
- [flake8-errmsg](https://pypi.org/project/flake8-errmsg/)
@@ -133,7 +133,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/)
- [flake8-datetimez](https://pypi.org/project/flake8-datetimez/)
- [flake8-debugger](https://pypi.org/project/flake8-debugger/)
- [flake8-django](https://pypi.org/project/flake8-django/)
- [flake8-django](https://pypi.org/project/flake8-django/) ([#2817](https://github.com/charliermarsh/ruff/issues/2817))
- [flake8-docstrings](https://pypi.org/project/flake8-docstrings/)
- [flake8-eradicate](https://pypi.org/project/flake8-eradicate/)
- [flake8-errmsg](https://pypi.org/project/flake8-errmsg/)

View File

@@ -242,7 +242,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.258'
rev: 'v0.0.257'
hooks:
- id: ruff
```

View File

@@ -20,7 +20,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.258'
rev: 'v0.0.257'
hooks:
- id: ruff
```
@@ -30,7 +30,7 @@ Or, to enable autofix:
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.258'
rev: 'v0.0.257'
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]

View File

@@ -7,7 +7,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.0.258"
version = "0.0.257"
description = "An extremely fast Python linter, written in Rust."
authors = [{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" }]
maintainers = [{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" }]

46
ruff.schema.json generated
View File

@@ -630,6 +630,17 @@
"items": {
"type": "string"
}
},
"severity": {
"description": "The minimum severity to catch. Choose from `low`, `medium`, `high`,",
"anyOf": [
{
"$ref": "#/definitions/Severity"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
@@ -1575,7 +1586,6 @@
"DJ007",
"DJ008",
"DJ01",
"DJ012",
"DJ013",
"DTZ",
"DTZ0",
@@ -2032,6 +2042,9 @@
"RUF10",
"RUF100",
"S",
"S0",
"S00",
"S001",
"S1",
"S10",
"S101",
@@ -2047,30 +2060,7 @@
"S112",
"S113",
"S3",
"S30",
"S301",
"S302",
"S303",
"S304",
"S305",
"S306",
"S307",
"S308",
"S31",
"S310",
"S311",
"S312",
"S313",
"S314",
"S315",
"S316",
"S317",
"S318",
"S319",
"S32",
"S320",
"S321",
"S323",
"S324",
"S5",
"S50",
@@ -2253,6 +2243,14 @@
"azure"
]
},
"Severity": {
"type": "string",
"enum": [
"Low",
"Medium",
"High"
]
},
"Strictness": {
"oneOf": [
{