Compare commits

...

10 Commits

Author SHA1 Message Date
Charlie Marsh
4cfa350112 Bump version to 0.0.249 (#3063) 2023-02-20 13:11:29 -05:00
Charlie Marsh
41f163fc8d Avoid assert() to assert statement conversion in expressions (#3062) 2023-02-20 17:49:22 +00:00
Charlie Marsh
d21dd994e6 Increase expected size of FormatElement (#3049) 2023-02-20 12:47:35 -05:00
Josh Karpel
6f5a6b8c8b Do not autofix E731 in class bodies (#3050) 2023-02-20 12:38:42 -05:00
Jeong YunWon
35606d7b05 clean up to fix nightly clippy warnings and dedents (#3057) 2023-02-20 09:33:47 -05:00
Matthew Lloyd
3ad257cfea Add PDM to "Who's Using Ruff?" (#3048) 2023-02-20 03:58:22 +00:00
Charlie Marsh
b39f960cd1 Relax constraints on pep8-naming module validation (#3043) 2023-02-19 17:34:23 -05:00
Charlie Marsh
c297d46899 Remove unused AsFormat trait for Option<T> (#3041)
We should re-add this, but it's currently unused and doesn't compile under 1.66.0.

See: #3039.
2023-02-19 20:19:35 +00:00
Jonathan Plasse
d6a100028c Update docs and pre-commit after #3006 (#3038) 2023-02-19 14:23:01 -05:00
Jonathan Plasse
35d4e03f2a Fix ruff_dev regex workspace dependency (#3037) 2023-02-19 18:02:08 +00:00
37 changed files with 553 additions and 422 deletions

View File

@@ -30,7 +30,7 @@ repos:
pass_filenames: false
- id: ruff
name: ruff
entry: cargo run -- --no-cache --fix
entry: cargo run -p ruff_cli -- check --no-cache --force-exclude --fix --exit-non-zero-on-fix
language: rust
types_or: [python, pyi]
require_serial: true

View File

@@ -52,7 +52,7 @@ cargo install cargo-insta
After cloning the repository, run Ruff locally with:
```shell
cargo run check /path/to/file.py --no-cache
cargo run -p ruff_cli -- check /path/to/file.py --no-cache
```
Prior to opening a pull request, ensure that your code has been auto-formatted,
@@ -135,7 +135,7 @@ contain a variety of violations and non-violations designed to evaluate and demo
of your lint rule.
Run `cargo dev generate-all` to generate the code for your new fixture. Then run Ruff
locally with (e.g.) `cargo run check crates/ruff/resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402`.
locally with (e.g.) `cargo run -p ruff_cli -- check crates/ruff/resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402`.
Once you're satisfied with the output, codify the behavior as a snapshot test by adding a new
`test_case` macro in the relevant `crates/ruff/src/[linter]/mod.rs` file. Then, run `cargo test --all`.

6
Cargo.lock generated
View File

@@ -753,7 +753,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.248"
version = "0.0.249"
dependencies = [
"anyhow",
"clap 4.1.6",
@@ -1927,7 +1927,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.248"
version = "0.0.249"
dependencies = [
"anyhow",
"bisection",
@@ -1983,7 +1983,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.248"
version = "0.0.249"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",

View File

@@ -167,7 +167,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.248'
rev: 'v0.0.249'
hooks:
- id: ruff
```
@@ -177,7 +177,7 @@ Or, to enable autofix:
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.248'
rev: 'v0.0.249'
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
@@ -720,6 +720,7 @@ Ruff is used in a number of major open-source projects, including:
* [Dagger](https://github.com/dagger/dagger)
* [Sphinx](https://github.com/sphinx-doc/sphinx)
* [Hatch](https://github.com/pypa/hatch)
* [PDM](https://github.com/pdm-project/pdm)
* [Jupyter](https://github.com/jupyter-server/jupyter_server)
* [Great Expectations](https://github.com/great-expectations/great_expectations)
* [ONNX](https://github.com/onnx/onnx)

View File

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

View File

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

View File

@@ -17,6 +17,12 @@ class Test(unittest.TestCase):
self.assertTrue(**{"expr": expr, "msg": msg}) # Error, unfixable
self.assertTrue(msg=msg, expr=expr, unexpected_arg=False) # Error, unfixable
self.assertTrue(msg=msg) # Error, unfixable
(
self.assertIsNotNone(value) # Error, unfixable
if expect_condition
else self.assertIsNone(value) # Error, unfixable
)
return self.assertEqual(True, False) # Error, unfixable
def test_assert_false(self):
self.assertFalse(True) # Error

View File

@@ -9,6 +9,9 @@ while False:
f = lambda: (yield 1)
#: E731
f = lambda: (yield from g())
#: E731
class F:
f = lambda x: 2 * x
f = object()
f.method = lambda: "Method"

View File

@@ -8,14 +8,14 @@ behaviors.
Running from the repo root should pick up and enforce the appropriate settings for each package:
```console
∴ cargo run resources/test/project/
resources/test/project/examples/.dotfiles/script.py:1:1: I001 Import block is un-sorted or un-formatted
resources/test/project/examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
resources/test/project/examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
resources/test/project/examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
resources/test/project/examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
resources/test/project/project/file.py:1:8: F401 `os` imported but unused
resources/test/project/project/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
∴ cargo run -p ruff_cli -- check crates/ruff/resources/test/project/
crates/ruff/resources/test/project/examples/.dotfiles/script.py:1:1: I001 Import block is un-sorted or un-formatted
crates/ruff/resources/test/project/examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
crates/ruff/resources/test/project/examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
crates/ruff/resources/test/project/examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
crates/ruff/resources/test/project/examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
crates/ruff/resources/test/project/project/file.py:1:8: F401 `os` imported but unused
crates/ruff/resources/test/project/project/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
Found 7 errors.
7 potentially fixable with the --fix option.
```
@@ -23,7 +23,7 @@ Found 7 errors.
Running from the project directory itself should exhibit the same behavior:
```console
∴ (cd resources/test/project/ && cargo run .)
∴ (cd crates/ruff/resources/test/project/ && cargo run -p ruff_cli -- check .)
examples/.dotfiles/script.py:1:1: I001 Import block is un-sorted or un-formatted
examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
@@ -39,7 +39,7 @@ Running from the sub-package directory should exhibit the same behavior, but omi
files:
```console
∴ (cd resources/test/project/examples/docs && cargo run .)
∴ (cd crates/ruff/resources/test/project/examples/docs && cargo run -p ruff_cli -- check .)
docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
Found 2 errors.
@@ -50,16 +50,16 @@ Found 2 errors.
file paths from the current working directory:
```console
∴ (cargo run -- --config=resources/test/project/pyproject.toml resources/test/project/)
resources/test/project/examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
resources/test/project/examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
resources/test/project/examples/docs/docs/concepts/file.py:1:8: F401 `os` imported but unused
resources/test/project/examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
resources/test/project/examples/docs/docs/file.py:1:8: F401 `os` imported but unused
resources/test/project/examples/docs/docs/file.py:3:8: F401 `numpy` imported but unused
resources/test/project/examples/docs/docs/file.py:4:27: F401 `docs.concepts.file` imported but unused
resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused
resources/test/project/project/file.py:1:8: F401 `os` imported but unused
∴ (cargo run -p ruff_cli -- check --config=crates/ruff/resources/test/project/pyproject.toml crates/ruff/resources/test/project/)
crates/ruff/resources/test/project/examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
crates/ruff/resources/test/project/examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
crates/ruff/resources/test/project/examples/docs/docs/concepts/file.py:1:8: F401 `os` imported but unused
crates/ruff/resources/test/project/examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
crates/ruff/resources/test/project/examples/docs/docs/file.py:1:8: F401 `os` imported but unused
crates/ruff/resources/test/project/examples/docs/docs/file.py:3:8: F401 `numpy` imported but unused
crates/ruff/resources/test/project/examples/docs/docs/file.py:4:27: F401 `docs.concepts.file` imported but unused
crates/ruff/resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused
crates/ruff/resources/test/project/project/file.py:1:8: F401 `os` imported but unused
Found 9 errors.
9 potentially fixable with the --fix option.
```
@@ -68,7 +68,7 @@ Running from a parent directory should "ignore" the `exclude` (hence, `concepts/
included in the output):
```console
∴ (cd resources/test/project/examples && cargo run -- --config=docs/ruff.toml .)
∴ (cd crates/ruff/resources/test/project/examples && cargo run -p ruff_cli -- check --config=docs/ruff.toml .)
docs/docs/concepts/file.py:5:5: F841 Local variable `x` is assigned to but never used
docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
@@ -80,8 +80,8 @@ Found 4 errors.
Passing an excluded directory directly should report errors in the contained files:
```console
∴ cargo run resources/test/project/examples/excluded/
resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused
∴ cargo run -p ruff_cli -- check crates/ruff/resources/test/project/examples/excluded/
crates/ruff/resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused
Found 1 error.
1 potentially fixable with the --fix option.
```
@@ -89,8 +89,8 @@ Found 1 error.
Unless we `--force-exclude`:
```console
∴ cargo run resources/test/project/examples/excluded/ --force-exclude
∴ cargo run -p ruff_cli -- check crates/ruff/resources/test/project/examples/excluded/ --force-exclude
warning: No Python files found under the given path(s)
∴ cargo run resources/test/project/examples/excluded/script.py --force-exclude
∴ cargo run -p ruff_cli -- check crates/ruff/resources/test/project/examples/excluded/script.py --force-exclude
warning: No Python files found under the given path(s)
```

View File

@@ -304,13 +304,14 @@ pub fn locate_cmpops(contents: &str) -> Vec<LocatedCmpop> {
ops.push(LocatedCmpop::new(start, end, Cmpop::In));
}
Tok::Is => {
if let Some((_, _, end)) =
let op = if let Some((_, _, end)) =
tok_iter.next_if(|(_, tok, _)| matches!(tok, Tok::Not))
{
ops.push(LocatedCmpop::new(start, end, Cmpop::IsNot));
LocatedCmpop::new(start, end, Cmpop::IsNot)
} else {
ops.push(LocatedCmpop::new(start, end, Cmpop::Is));
}
LocatedCmpop::new(start, end, Cmpop::Is)
};
ops.push(op);
}
Tok::NotEqual => {
ops.push(LocatedCmpop::new(start, end, Cmpop::NotEq));

View File

@@ -239,51 +239,51 @@ impl<'a> Checker<'a> {
'b: 'a,
{
let call_path = collect_call_path(value);
if let Some(head) = call_path.first() {
if let Some(binding) = self.find_binding(head) {
match &binding.kind {
BindingKind::Importation(.., name)
| BindingKind::SubmoduleImportation(name, ..) => {
return if name.starts_with('.') {
if let Some(module) = &self.module_path {
let mut source_path = from_relative_import(module, name);
source_path.extend(call_path.into_iter().skip(1));
Some(source_path)
} else {
None
}
} else {
let mut source_path: CallPath = name.split('.').collect();
source_path.extend(call_path.into_iter().skip(1));
Some(source_path)
};
let Some(head) = call_path.first() else {
return None;
};
let Some(binding) = self.find_binding(head) else {
return None;
};
match &binding.kind {
BindingKind::Importation(.., name) | BindingKind::SubmoduleImportation(name, ..) => {
if name.starts_with('.') {
if let Some(module) = &self.module_path {
let mut source_path = from_relative_import(module, name);
source_path.extend(call_path.into_iter().skip(1));
Some(source_path)
} else {
None
}
BindingKind::FromImportation(.., name) => {
return if name.starts_with('.') {
if let Some(module) = &self.module_path {
let mut source_path = from_relative_import(module, name);
source_path.extend(call_path.into_iter().skip(1));
Some(source_path)
} else {
None
}
} else {
let mut source_path: CallPath = name.split('.').collect();
source_path.extend(call_path.into_iter().skip(1));
Some(source_path)
};
}
BindingKind::Builtin => {
let mut source_path: CallPath = smallvec![];
source_path.push("");
source_path.extend(call_path);
return Some(source_path);
}
_ => {}
} else {
let mut source_path: CallPath = name.split('.').collect();
source_path.extend(call_path.into_iter().skip(1));
Some(source_path)
}
}
BindingKind::FromImportation(.., name) => {
if name.starts_with('.') {
if let Some(module) = &self.module_path {
let mut source_path = from_relative_import(module, name);
source_path.extend(call_path.into_iter().skip(1));
Some(source_path)
} else {
None
}
} else {
let mut source_path: CallPath = name.split('.').collect();
source_path.extend(call_path.into_iter().skip(1));
Some(source_path)
}
}
BindingKind::Builtin => {
let mut source_path: CallPath = smallvec![];
source_path.push("");
source_path.extend(call_path);
Some(source_path)
}
_ => None,
}
None
}
/// Return `true` if a `Rule` is disabled by a `noqa` directive.
@@ -4169,146 +4169,147 @@ impl<'a> Checker<'a> {
}
fn handle_node_load(&mut self, expr: &Expr) {
if let ExprKind::Name { id, .. } = &expr.node {
let scope_id = self.current_scope().id;
let ExprKind::Name { id, .. } = &expr.node else {
return;
};
let scope_id = self.current_scope().id;
let mut first_iter = true;
let mut in_generator = false;
let mut import_starred = false;
let mut first_iter = true;
let mut in_generator = false;
let mut import_starred = false;
for scope_index in self.scope_stack.iter().rev() {
let scope = &self.scopes[*scope_index];
if matches!(scope.kind, ScopeKind::Class(_)) {
if id == "__class__" {
return;
} else if !first_iter && !in_generator {
continue;
}
}
if let Some(index) = scope.bindings.get(&id.as_str()) {
// Mark the binding as used.
let context = self.execution_context();
self.bindings[*index].mark_used(scope_id, Range::from_located(expr), context);
if matches!(self.bindings[*index].kind, BindingKind::Annotation)
&& !self.in_deferred_string_type_definition
&& !self.in_deferred_type_definition
{
continue;
}
// If the name of the sub-importation is the same as an alias of another
// importation and the alias is used, that sub-importation should be
// marked as used too.
//
// This handles code like:
// import pyarrow as pa
// import pyarrow.csv
// print(pa.csv.read_csv("test.csv"))
match &self.bindings[*index].kind {
BindingKind::Importation(name, full_name)
| BindingKind::SubmoduleImportation(name, full_name) => {
let has_alias = full_name
.split('.')
.last()
.map(|segment| &segment != name)
.unwrap_or_default();
if has_alias {
// Mark the sub-importation as used.
if let Some(index) = scope.bindings.get(full_name) {
self.bindings[*index].mark_used(
scope_id,
Range::from_located(expr),
context,
);
}
}
}
BindingKind::FromImportation(name, full_name) => {
let has_alias = full_name
.split('.')
.last()
.map(|segment| &segment != name)
.unwrap_or_default();
if has_alias {
// Mark the sub-importation as used.
if let Some(index) = scope.bindings.get(full_name.as_str()) {
self.bindings[*index].mark_used(
scope_id,
Range::from_located(expr),
context,
);
}
}
}
_ => {}
}
for scope_index in self.scope_stack.iter().rev() {
let scope = &self.scopes[*scope_index];
if matches!(scope.kind, ScopeKind::Class(_)) {
if id == "__class__" {
return;
} else if !first_iter && !in_generator {
continue;
}
first_iter = false;
in_generator = matches!(scope.kind, ScopeKind::Generator);
import_starred = import_starred || scope.import_starred;
}
if import_starred {
if self.settings.rules.enabled(&Rule::ImportStarUsage) {
let mut from_list = vec![];
for scope_index in self.scope_stack.iter().rev() {
let scope = &self.scopes[*scope_index];
for binding in scope.bindings.values().map(|index| &self.bindings[*index]) {
if let BindingKind::StarImportation(level, module) = &binding.kind {
from_list.push(helpers::format_import_from(
level.as_ref(),
module.as_deref(),
));
if let Some(index) = scope.bindings.get(&id.as_str()) {
// Mark the binding as used.
let context = self.execution_context();
self.bindings[*index].mark_used(scope_id, Range::from_located(expr), context);
if matches!(self.bindings[*index].kind, BindingKind::Annotation)
&& !self.in_deferred_string_type_definition
&& !self.in_deferred_type_definition
{
continue;
}
// If the name of the sub-importation is the same as an alias of another
// importation and the alias is used, that sub-importation should be
// marked as used too.
//
// This handles code like:
// import pyarrow as pa
// import pyarrow.csv
// print(pa.csv.read_csv("test.csv"))
match &self.bindings[*index].kind {
BindingKind::Importation(name, full_name)
| BindingKind::SubmoduleImportation(name, full_name) => {
let has_alias = full_name
.split('.')
.last()
.map(|segment| &segment != name)
.unwrap_or_default();
if has_alias {
// Mark the sub-importation as used.
if let Some(index) = scope.bindings.get(full_name) {
self.bindings[*index].mark_used(
scope_id,
Range::from_located(expr),
context,
);
}
}
}
from_list.sort();
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportStarUsage {
name: id.to_string(),
sources: from_list,
},
Range::from_located(expr),
));
BindingKind::FromImportation(name, full_name) => {
let has_alias = full_name
.split('.')
.last()
.map(|segment| &segment != name)
.unwrap_or_default();
if has_alias {
// Mark the sub-importation as used.
if let Some(index) = scope.bindings.get(full_name.as_str()) {
self.bindings[*index].mark_used(
scope_id,
Range::from_located(expr),
context,
);
}
}
}
_ => {}
}
return;
}
if self.settings.rules.enabled(&Rule::UndefinedName) {
// Allow __path__.
if self.path.ends_with("__init__.py") && id == "__path__" {
return;
}
first_iter = false;
in_generator = matches!(scope.kind, ScopeKind::Generator);
import_starred = import_starred || scope.import_starred;
}
// Allow "__module__" and "__qualname__" in class scopes.
if (id == "__module__" || id == "__qualname__")
&& matches!(self.current_scope().kind, ScopeKind::Class(..))
{
return;
}
// Avoid flagging if NameError is handled.
if let Some(handler_names) = self.except_handlers.last() {
if handler_names
.iter()
.any(|call_path| call_path.as_slice() == ["NameError"])
{
return;
if import_starred {
if self.settings.rules.enabled(&Rule::ImportStarUsage) {
let mut from_list = vec![];
for scope_index in self.scope_stack.iter().rev() {
let scope = &self.scopes[*scope_index];
for binding in scope.bindings.values().map(|index| &self.bindings[*index]) {
if let BindingKind::StarImportation(level, module) = &binding.kind {
from_list.push(helpers::format_import_from(
level.as_ref(),
module.as_deref(),
));
}
}
}
from_list.sort();
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedName { name: id.clone() },
pyflakes::rules::ImportStarUsage {
name: id.to_string(),
sources: from_list,
},
Range::from_located(expr),
));
}
return;
}
if self.settings.rules.enabled(&Rule::UndefinedName) {
// Allow __path__.
if self.path.ends_with("__init__.py") && id == "__path__" {
return;
}
// Allow "__module__" and "__qualname__" in class scopes.
if (id == "__module__" || id == "__qualname__")
&& matches!(self.current_scope().kind, ScopeKind::Class(..))
{
return;
}
// Avoid flagging if NameError is handled.
if let Some(handler_names) = self.except_handlers.last() {
if handler_names
.iter()
.any(|call_path| call_path.as_slice() == ["NameError"])
{
return;
}
}
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedName { name: id.clone() },
Range::from_located(expr),
));
}
}
@@ -4506,26 +4507,29 @@ impl<'a> Checker<'a> {
where
'b: 'a,
{
if let ExprKind::Name { id, .. } = &expr.node {
if operations::on_conditional_branch(
&mut self.parents.iter().rev().map(std::convert::Into::into),
) {
return;
}
let scope =
&mut self.scopes[*(self.scope_stack.last().expect("No current scope found"))];
if scope.bindings.remove(&id.as_str()).is_none()
&& self.settings.rules.enabled(&Rule::UndefinedName)
{
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedName {
name: id.to_string(),
},
Range::from_located(expr),
));
}
let ExprKind::Name { id, .. } = &expr.node else {
return;
};
if operations::on_conditional_branch(
&mut self.parents.iter().rev().map(std::convert::Into::into),
) {
return;
}
let scope = &mut self.scopes[*(self.scope_stack.last().expect("No current scope found"))];
if scope.bindings.remove(&id.as_str()).is_some() {
return;
}
if !self.settings.rules.enabled(&Rule::UndefinedName) {
return;
}
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedName {
name: id.to_string(),
},
Range::from_located(expr),
));
}
fn visit_docstring<'b>(&mut self, python_ast: &'b Suite) -> bool

View File

@@ -322,7 +322,7 @@ fn push_codes<I: Display>(str: &mut String, codes: impl Iterator<Item = I>) {
if !first {
str.push_str(", ");
}
let _ = write!(str, "{}", code);
let _ = write!(str, "{code}");
first = false;
}
}

View File

@@ -18,8 +18,6 @@
//! method()
//! ```
use std::iter;
use ruff_macros::{define_violation, derive_message_formats};
use rustc_hash::FxHashMap;
use rustpython_parser::ast::{Expr, ExprKind, Stmt};
@@ -174,27 +172,19 @@ pub fn unused_loop_control_variable(
if matches!(certainty, Certainty::Certain) && checker.patch(diagnostic.kind.rule()) {
// Find the `BindingKind::LoopVar` corresponding to the name.
let scope = checker.current_scope();
if let Some(binding) = iter::once(scope.bindings.get(name))
.flatten()
.chain(
iter::once(scope.rebounds.get(name))
.flatten()
.into_iter()
.flatten(),
)
let binding = scope
.bindings
.get(name)
.into_iter()
.chain(scope.rebounds.get(name).into_iter().flatten())
.find_map(|index| {
let binding = &checker.bindings[*index];
if let Some(source) = &binding.source {
if source == &RefEquality(stmt) {
Some(binding)
} else {
None
}
} else {
None
}
})
{
binding
.source
.as_ref()
.and_then(|source| (source == &RefEquality(stmt)).then_some(binding))
});
if let Some(binding) = binding {
if matches!(binding.kind, BindingKind::LoopVar) {
if !binding.used() {
diagnostic.amend(Fix::replacement(

View File

@@ -12,7 +12,7 @@ use crate::checkers::ast::Checker;
use crate::fix::Fix;
use crate::registry::Diagnostic;
use crate::source_code::Stylist;
use crate::violation::{AlwaysAutofixableViolation, AutofixKind, Availability, Violation};
use crate::violation::{AutofixKind, Availability, Violation};
use super::helpers::is_falsy_constant;
use super::unittest_assert::UnittestAssert;
@@ -94,18 +94,23 @@ impl Violation for AssertAlwaysFalse {
define_violation!(
pub struct UnittestAssertion {
pub assertion: String,
pub fixable: bool,
}
);
impl AlwaysAutofixableViolation for UnittestAssertion {
impl Violation for UnittestAssertion {
const AUTOFIX: Option<AutofixKind> = Some(AutofixKind::new(Availability::Sometimes));
#[derive_message_formats]
fn message(&self) -> String {
let UnittestAssertion { assertion } = self;
let UnittestAssertion { assertion, .. } = self;
format!("Use a regular `assert` instead of unittest-style `{assertion}`")
}
fn autofix_title(&self) -> String {
let UnittestAssertion { assertion } = self;
format!("Replace `{assertion}(...)` with `assert ...`")
fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
self.fixable
.then_some(|UnittestAssertion { assertion, .. }| {
format!("Replace `{assertion}(...)` with `assert ...`")
})
}
}
@@ -181,13 +186,18 @@ pub fn unittest_assertion(
match &func.node {
ExprKind::Attribute { attr, .. } => {
if let Ok(unittest_assert) = UnittestAssert::try_from(attr.as_str()) {
// We're converting an expression to a statement, so avoid applying the fix if
// the assertion is part of a larger expression.
let fixable = checker.current_expr_parent().is_none()
&& matches!(checker.current_stmt().node, StmtKind::Expr { .. });
let mut diagnostic = Diagnostic::new(
UnittestAssertion {
assertion: unittest_assert.to_string(),
fixable,
},
Range::from_located(func),
);
if checker.patch(diagnostic.kind.rule()) {
if fixable && checker.patch(diagnostic.kind.rule()) {
if let Ok(stmt) = unittest_assert.generate_assert(args, keywords) {
diagnostic.amend(Fix::replacement(
unparse_stmt(&stmt, checker.stylist),

View File

@@ -5,6 +5,7 @@ expression: diagnostics
- kind:
UnittestAssertion:
assertion: assertTrue
fixable: true
location:
row: 11
column: 8
@@ -24,6 +25,7 @@ expression: diagnostics
- kind:
UnittestAssertion:
assertion: assertTrue
fixable: true
location:
row: 12
column: 8
@@ -43,6 +45,7 @@ expression: diagnostics
- kind:
UnittestAssertion:
assertion: assertTrue
fixable: true
location:
row: 13
column: 8
@@ -62,6 +65,7 @@ expression: diagnostics
- kind:
UnittestAssertion:
assertion: assertTrue
fixable: true
location:
row: 14
column: 8
@@ -81,6 +85,7 @@ expression: diagnostics
- kind:
UnittestAssertion:
assertion: assertTrue
fixable: true
location:
row: 15
column: 8
@@ -100,6 +105,7 @@ expression: diagnostics
- kind:
UnittestAssertion:
assertion: assertTrue
fixable: true
location:
row: 16
column: 8
@@ -111,6 +117,7 @@ expression: diagnostics
- kind:
UnittestAssertion:
assertion: assertTrue
fixable: true
location:
row: 17
column: 8
@@ -122,6 +129,7 @@ expression: diagnostics
- kind:
UnittestAssertion:
assertion: assertTrue
fixable: true
location:
row: 18
column: 8
@@ -133,6 +141,7 @@ expression: diagnostics
- kind:
UnittestAssertion:
assertion: assertTrue
fixable: true
location:
row: 19
column: 8
@@ -141,365 +150,420 @@ expression: diagnostics
column: 23
fix: ~
parent: ~
- kind:
UnittestAssertion:
assertion: assertIsNotNone
fixable: false
location:
row: 21
column: 12
end_location:
row: 21
column: 32
fix: ~
parent: ~
- kind:
UnittestAssertion:
assertion: assertIsNone
fixable: false
location:
row: 23
column: 17
end_location:
row: 23
column: 34
fix: ~
parent: ~
- kind:
UnittestAssertion:
assertion: assertEqual
fixable: false
location:
row: 25
column: 15
end_location:
row: 25
column: 31
fix: ~
parent: ~
- kind:
UnittestAssertion:
assertion: assertFalse
fixable: true
location:
row: 22
row: 28
column: 8
end_location:
row: 22
row: 28
column: 24
fix:
content:
- assert not True
location:
row: 22
row: 28
column: 8
end_location:
row: 22
row: 28
column: 30
parent: ~
- kind:
UnittestAssertion:
assertion: assertEqual
fixable: true
location:
row: 25
row: 31
column: 8
end_location:
row: 25
row: 31
column: 24
fix:
content:
- assert 1 == 2
location:
row: 25
row: 31
column: 8
end_location:
row: 25
row: 31
column: 30
parent: ~
- kind:
UnittestAssertion:
assertion: assertNotEqual
fixable: true
location:
row: 28
row: 34
column: 8
end_location:
row: 28
row: 34
column: 27
fix:
content:
- assert 1 != 1
location:
row: 28
row: 34
column: 8
end_location:
row: 28
row: 34
column: 33
parent: ~
- kind:
UnittestAssertion:
assertion: assertGreater
fixable: true
location:
row: 31
row: 37
column: 8
end_location:
row: 31
row: 37
column: 26
fix:
content:
- assert 1 > 2
location:
row: 31
row: 37
column: 8
end_location:
row: 31
row: 37
column: 32
parent: ~
- kind:
UnittestAssertion:
assertion: assertGreaterEqual
fixable: true
location:
row: 34
row: 40
column: 8
end_location:
row: 34
row: 40
column: 31
fix:
content:
- assert 1 >= 2
location:
row: 34
row: 40
column: 8
end_location:
row: 34
row: 40
column: 37
parent: ~
- kind:
UnittestAssertion:
assertion: assertLess
fixable: true
location:
row: 37
row: 43
column: 8
end_location:
row: 37
row: 43
column: 23
fix:
content:
- assert 2 < 1
location:
row: 37
row: 43
column: 8
end_location:
row: 37
row: 43
column: 29
parent: ~
- kind:
UnittestAssertion:
assertion: assertLessEqual
fixable: true
location:
row: 40
row: 46
column: 8
end_location:
row: 40
row: 46
column: 28
fix:
content:
- assert 1 <= 2
location:
row: 40
row: 46
column: 8
end_location:
row: 40
row: 46
column: 34
parent: ~
- kind:
UnittestAssertion:
assertion: assertIn
fixable: true
location:
row: 43
row: 49
column: 8
end_location:
row: 43
row: 49
column: 21
fix:
content:
- "assert 1 in [2, 3]"
location:
row: 43
row: 49
column: 8
end_location:
row: 43
row: 49
column: 32
parent: ~
- kind:
UnittestAssertion:
assertion: assertNotIn
fixable: true
location:
row: 46
row: 52
column: 8
end_location:
row: 46
row: 52
column: 24
fix:
content:
- "assert 2 not in [2, 3]"
location:
row: 46
row: 52
column: 8
end_location:
row: 46
row: 52
column: 35
parent: ~
- kind:
UnittestAssertion:
assertion: assertIsNone
fixable: true
location:
row: 49
row: 55
column: 8
end_location:
row: 49
row: 55
column: 25
fix:
content:
- assert 0 is None
location:
row: 49
row: 55
column: 8
end_location:
row: 49
row: 55
column: 28
parent: ~
- kind:
UnittestAssertion:
assertion: assertIsNotNone
fixable: true
location:
row: 52
row: 58
column: 8
end_location:
row: 52
row: 58
column: 28
fix:
content:
- assert 0 is not None
location:
row: 52
row: 58
column: 8
end_location:
row: 52
row: 58
column: 31
parent: ~
- kind:
UnittestAssertion:
assertion: assertIs
fixable: true
location:
row: 55
row: 61
column: 8
end_location:
row: 55
row: 61
column: 21
fix:
content:
- "assert [] is []"
location:
row: 55
row: 61
column: 8
end_location:
row: 55
row: 61
column: 29
parent: ~
- kind:
UnittestAssertion:
assertion: assertIsNot
fixable: true
location:
row: 58
row: 64
column: 8
end_location:
row: 58
row: 64
column: 24
fix:
content:
- assert 1 is not 1
location:
row: 58
row: 64
column: 8
end_location:
row: 58
row: 64
column: 30
parent: ~
- kind:
UnittestAssertion:
assertion: assertIsInstance
fixable: true
location:
row: 61
row: 67
column: 8
end_location:
row: 61
row: 67
column: 29
fix:
content:
- "assert isinstance(1, str)"
location:
row: 61
row: 67
column: 8
end_location:
row: 61
row: 67
column: 37
parent: ~
- kind:
UnittestAssertion:
assertion: assertNotIsInstance
fixable: true
location:
row: 64
row: 70
column: 8
end_location:
row: 64
row: 70
column: 32
fix:
content:
- "assert not isinstance(1, int)"
location:
row: 64
row: 70
column: 8
end_location:
row: 64
row: 70
column: 40
parent: ~
- kind:
UnittestAssertion:
assertion: assertRegex
fixable: true
location:
row: 67
row: 73
column: 8
end_location:
row: 67
row: 73
column: 24
fix:
content:
- "assert re.search(\"def\", \"abc\")"
location:
row: 67
row: 73
column: 8
end_location:
row: 67
row: 73
column: 39
parent: ~
- kind:
UnittestAssertion:
assertion: assertNotRegex
fixable: true
location:
row: 70
row: 76
column: 8
end_location:
row: 70
row: 76
column: 27
fix:
content:
- "assert not re.search(\"abc\", \"abc\")"
location:
row: 70
row: 76
column: 8
end_location:
row: 70
row: 76
column: 42
parent: ~
- kind:
UnittestAssertion:
assertion: assertRegexpMatches
fixable: true
location:
row: 73
row: 79
column: 8
end_location:
row: 73
row: 79
column: 32
fix:
content:
- "assert re.search(\"def\", \"abc\")"
location:
row: 73
row: 79
column: 8
end_location:
row: 73
row: 79
column: 47
parent: ~
- kind:
UnittestAssertion:
assertion: assertNotRegex
fixable: true
location:
row: 76
row: 82
column: 8
end_location:
row: 76
row: 82
column: 27
fix:
content:
- "assert not re.search(\"abc\", \"abc\")"
location:
row: 76
row: 82
column: 8
end_location:
row: 76
row: 82
column: 42
parent: ~

View File

@@ -3,7 +3,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ruff_macros::{define_violation, derive_message_formats};
use ruff_python::string::is_lower_with_underscore;
use ruff_python::identifiers::is_module_name;
use crate::ast::helpers::{create_stmt, from_relative_import, unparse_stmt};
use crate::ast::types::Range;
@@ -103,7 +103,7 @@ fn fix_banned_relative_import(
let call_path = from_relative_import(&parts, module);
// Require import to be a valid PEP 8 module:
// https://python.org/dev/peps/pep-0008/#package-and-module-names
if !call_path.iter().all(|part| is_lower_with_underscore(part)) {
if !call_path.iter().all(|part| is_module_name(part)) {
return None;
}
call_path.as_slice().join(".")
@@ -112,14 +112,14 @@ fn fix_banned_relative_import(
let call_path = from_relative_import(&parts, module);
// Require import to be a valid PEP 8 module:
// https://python.org/dev/peps/pep-0008/#package-and-module-names
if !call_path.iter().all(|part| is_lower_with_underscore(part)) {
if !call_path.iter().all(|part| is_module_name(part)) {
return None;
}
call_path.as_slice().join(".")
} else {
// Require import to be a valid PEP 8 module:
// https://python.org/dev/peps/pep-0008/#package-and-module-names
if !parts.iter().all(|part| is_lower_with_underscore(part)) {
if !parts.iter().all(|part| is_module_name(part)) {
return None;
}
parts.join(".")

View File

@@ -39,6 +39,9 @@ mod tests {
#[test_case(Rule::InvalidModuleName, Path::new("N999/module/valid_name/__init__.py"); "N999_7")]
#[test_case(Rule::InvalidModuleName, Path::new("N999/module/no_module/test.txt"); "N999_8")]
#[test_case(Rule::InvalidModuleName, Path::new("N999/module/valid_name/file-with-dashes.py"); "N999_9")]
#[test_case(Rule::InvalidModuleName, Path::new("N999/module/valid_name/__main__.py"); "N999_10")]
#[test_case(Rule::InvalidModuleName, Path::new("N999/module/valid_name/0001_initial.py"); "N999_11")]
#[test_case(Rule::InvalidModuleName, Path::new("N999/module/valid_name/__setup__.py"); "N999_12")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -1,7 +1,7 @@
use std::path::Path;
use ruff_macros::{define_violation, derive_message_formats};
use ruff_python::string::is_lower_with_underscore;
use ruff_python::identifiers::is_module_name;
use crate::ast::types::Range;
use crate::registry::Diagnostic;
@@ -44,13 +44,18 @@ impl Violation for InvalidModuleName {
/// N999
pub fn invalid_module_name(path: &Path, package: Option<&Path>) -> Option<Diagnostic> {
if let Some(package) = package {
let module_name = if path.file_name().unwrap().to_string_lossy() == "__init__.py" {
let module_name = if path.file_name().map_or(false, |file_name| {
file_name == "__init__.py"
|| file_name == "__init__.pyi"
|| file_name == "__main__.py"
|| file_name == "__main__.pyi"
}) {
package.file_name().unwrap().to_string_lossy()
} else {
path.file_stem().unwrap().to_string_lossy()
};
if !is_lower_with_underscore(&module_name) {
if !is_module_name(&module_name) {
return Some(Diagnostic::new(
InvalidModuleName {
name: module_name.to_string(),

View File

@@ -0,0 +1,6 @@
---
source: crates/ruff/src/rules/pep8_naming/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,6 @@
---
source: crates/ruff/src/rules/pep8_naming/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,6 @@
---
source: crates/ruff/src/rules/pep8_naming/mod.rs
expression: diagnostics
---
[]

View File

@@ -2,26 +2,31 @@ use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::{Arguments, Expr, ExprKind, Location, Stmt, StmtKind};
use crate::ast::helpers::{match_leading_content, match_trailing_content, unparse_stmt};
use crate::ast::types::Range;
use crate::ast::types::{Range, ScopeKind};
use crate::ast::whitespace::leading_space;
use crate::checkers::ast::Checker;
use crate::fix::Fix;
use crate::registry::Diagnostic;
use crate::source_code::Stylist;
use crate::violation::AlwaysAutofixableViolation;
use crate::violation::{AutofixKind, Availability, Violation};
define_violation!(
pub struct LambdaAssignment(pub String);
pub struct LambdaAssignment {
pub name: String,
pub fixable: bool,
}
);
impl AlwaysAutofixableViolation for LambdaAssignment {
impl Violation for LambdaAssignment {
const AUTOFIX: Option<AutofixKind> = Some(AutofixKind::new(Availability::Sometimes));
#[derive_message_formats]
fn message(&self) -> String {
format!("Do not assign a `lambda` expression, use a `def`")
}
fn autofix_title(&self) -> String {
let LambdaAssignment(name) = self;
format!("Rewrite `{name}` as a `def`")
fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
self.fixable
.then_some(|v| format!("Rewrite `{}` as a `def`", v.name))
}
}
@@ -29,37 +34,52 @@ impl AlwaysAutofixableViolation for LambdaAssignment {
pub fn lambda_assignment(checker: &mut Checker, target: &Expr, value: &Expr, stmt: &Stmt) {
if let ExprKind::Name { id, .. } = &target.node {
if let ExprKind::Lambda { args, body } = &value.node {
let mut diagnostic =
Diagnostic::new(LambdaAssignment(id.to_string()), Range::from_located(stmt));
if checker.patch(diagnostic.kind.rule()) {
if !match_leading_content(stmt, checker.locator)
&& !match_trailing_content(stmt, checker.locator)
// If the assignment is in a class body, it might not be safe
// to replace it because the assignment might be
// carrying a type annotation that will be used by some
// package like dataclasses, which wouldn't consider the
// rewritten function definition to be equivalent.
// See https://github.com/charliermarsh/ruff/issues/3046
let fixable = !matches!(checker.current_scope().kind, ScopeKind::Class(_));
let mut diagnostic = Diagnostic::new(
LambdaAssignment {
name: id.to_string(),
fixable,
},
Range::from_located(stmt),
);
if checker.patch(diagnostic.kind.rule())
&& fixable
&& !match_leading_content(stmt, checker.locator)
&& !match_trailing_content(stmt, checker.locator)
{
let first_line = checker.locator.slice(&Range::new(
Location::new(stmt.location.row(), 0),
Location::new(stmt.location.row() + 1, 0),
));
let indentation = &leading_space(first_line);
let mut indented = String::new();
for (idx, line) in function(id, args, body, checker.stylist)
.lines()
.enumerate()
{
let first_line = checker.locator.slice(&Range::new(
Location::new(stmt.location.row(), 0),
Location::new(stmt.location.row() + 1, 0),
));
let indentation = &leading_space(first_line);
let mut indented = String::new();
for (idx, line) in function(id, args, body, checker.stylist)
.lines()
.enumerate()
{
if idx == 0 {
indented.push_str(line);
} else {
indented.push_str(checker.stylist.line_ending().as_str());
indented.push_str(indentation);
indented.push_str(line);
}
if idx == 0 {
indented.push_str(line);
} else {
indented.push_str(checker.stylist.line_ending().as_str());
indented.push_str(indentation);
indented.push_str(line);
}
diagnostic.amend(Fix::replacement(
indented,
stmt.location,
stmt.end_location.unwrap(),
));
}
diagnostic.amend(Fix::replacement(
indented,
stmt.location,
stmt.end_location.unwrap(),
));
}
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -3,7 +3,9 @@ source: crates/ruff/src/rules/pycodestyle/mod.rs
expression: diagnostics
---
- kind:
LambdaAssignment: f
LambdaAssignment:
name: f
fixable: true
location:
row: 2
column: 0
@@ -22,7 +24,9 @@ expression: diagnostics
column: 19
parent: ~
- kind:
LambdaAssignment: f
LambdaAssignment:
name: f
fixable: true
location:
row: 4
column: 0
@@ -41,7 +45,9 @@ expression: diagnostics
column: 19
parent: ~
- kind:
LambdaAssignment: this
LambdaAssignment:
name: this
fixable: true
location:
row: 7
column: 4
@@ -60,7 +66,9 @@ expression: diagnostics
column: 29
parent: ~
- kind:
LambdaAssignment: f
LambdaAssignment:
name: f
fixable: true
location:
row: 9
column: 0
@@ -79,7 +87,9 @@ expression: diagnostics
column: 21
parent: ~
- kind:
LambdaAssignment: f
LambdaAssignment:
name: f
fixable: true
location:
row: 11
column: 0
@@ -97,4 +107,16 @@ expression: diagnostics
row: 11
column: 28
parent: ~
- kind:
LambdaAssignment:
name: f
fixable: false
location:
row: 14
column: 4
end_location:
row: 14
column: 23
fix: ~
parent: ~

View File

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

View File

@@ -478,7 +478,7 @@ impl ConfigProcessor for &Overrides {
.ignore
.iter()
.cloned()
.chain(self.extend_ignore.iter().cloned().into_iter())
.chain(self.extend_ignore.iter().cloned())
.flatten()
.collect(),
extend_select: self.extend_select.clone().unwrap_or_default(),

View File

@@ -639,7 +639,7 @@ fn print_fixed<T: Write>(stdout: &mut T, fixed: &FxHashMap<String, FixTable>) ->
);
let s = if total == 1 { "" } else { "s" };
let label = format!("Fixed {total} error{s}:", total = total, s = s);
let label = format!("Fixed {total} error{s}:");
writeln!(stdout, "{}", label.bold().green())?;
for (filename, table) in fixed

View File

@@ -11,7 +11,7 @@ clap = { workspace = true }
itertools = { workspace = true }
libcst = { workspace = true }
once_cell = { workspace = true }
regex.workspace = true
regex = { workspace = true }
ruff = { path = "../ruff" }
ruff_cli = { path = "../ruff_cli" }
rustpython-common = { workspace = true }

View File

@@ -4,7 +4,7 @@ use ruff::settings::options::Options;
use ruff::settings::options_base::{ConfigurationOptions, OptionEntry, OptionField};
fn emit_field(output: &mut String, name: &str, field: &OptionField, group_name: Option<&str>) {
output.push_str(&format!("#### [`{0}`](#{0})\n", name));
output.push_str(&format!("#### [`{name}`](#{name})\n"));
output.push('\n');
output.push_str(field.doc);
output.push_str("\n\n");
@@ -40,7 +40,7 @@ pub fn generate() -> String {
// Generate all the sub-groups.
for (group_name, entry) in &sorted_options {
let OptionEntry::Group(fields) = entry else { continue; };
output.push_str(&format!("### `{}`\n", group_name));
output.push_str(&format!("### `{group_name}`\n"));
output.push('\n');
for (name, entry) in fields.iter().sorted_by_key(|(name, _)| name) {
let OptionEntry::Field(field) = entry else { continue; };

View File

@@ -385,4 +385,4 @@ static_assert!(std::mem::size_of::<crate::format_element::Tag>() == 16usize);
#[cfg(not(debug_assertions))]
#[cfg(target_pointer_width = "64")]
static_assert!(std::mem::size_of::<crate::FormatElement>() == 24usize);
static_assert!(std::mem::size_of::<crate::FormatElement>() == 32usize);

View File

@@ -22,3 +22,28 @@ pub fn is_identifier(s: &str) -> bool {
pub fn is_mangled_private(id: &str) -> bool {
id.starts_with("__") && !id.ends_with("__")
}
/// Returns `true` if a string is a PEP 8-compliant module name (i.e., consists of lowercase
/// letters, numbers, and underscores).
pub fn is_module_name(s: &str) -> bool {
s.chars()
.all(|c| c.is_lowercase() || c.is_numeric() || c == '_')
}
#[cfg(test)]
mod tests {
use crate::identifiers::is_module_name;
#[test]
fn test_is_module_name() {
assert!(is_module_name("a"));
assert!(is_module_name("abc"));
assert!(is_module_name("abc0"));
assert!(is_module_name("abc_"));
assert!(is_module_name("a_b_c"));
assert!(is_module_name("0abc"));
assert!(is_module_name("_abc"));
assert!(!is_module_name("a-b-c"));
assert!(!is_module_name("a_B_c"));
}
}

View File

@@ -3,8 +3,6 @@ use regex::Regex;
pub static STRING_QUOTE_PREFIX_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"^(?i)[urb]*['"](?P<raw>.*)['"]$"#).unwrap());
pub static LOWER_OR_UNDERSCORE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[a-z][a-z0-9_]*$").unwrap());
pub fn is_lower(s: &str) -> bool {
let mut cased = false;
@@ -30,11 +28,6 @@ pub fn is_upper(s: &str) -> bool {
cased
}
// Module names should be lowercase, and may contain underscore
pub fn is_lower_with_underscore(s: &str) -> bool {
LOWER_OR_UNDERSCORE.is_match(s)
}
/// Remove prefixes (u, r, b) and quotes around a string. This expects the given
/// string to be a valid Python string representation, it doesn't do any
/// validation.
@@ -50,7 +43,7 @@ pub fn strip_quotes_and_prefixes(s: &str) -> &str {
#[cfg(test)]
mod tests {
use crate::string::{is_lower, is_lower_with_underscore, is_upper, strip_quotes_and_prefixes};
use crate::string::{is_lower, is_upper, strip_quotes_and_prefixes};
#[test]
fn test_is_lower() {
@@ -63,19 +56,6 @@ mod tests {
assert!(!is_lower("_"));
}
#[test]
fn test_is_lower_underscore() {
assert!(is_lower_with_underscore("a"));
assert!(is_lower_with_underscore("abc"));
assert!(is_lower_with_underscore("abc0"));
assert!(is_lower_with_underscore("abc_"));
assert!(is_lower_with_underscore("a_b_c"));
assert!(!is_lower_with_underscore("a-b-c"));
assert!(!is_lower_with_underscore("a_B_c"));
assert!(!is_lower_with_underscore("0abc"));
assert!(!is_lower_with_underscore("_abc"));
}
#[test]
fn test_is_upper() {
assert!(is_upper("ABC"));

View File

@@ -78,16 +78,10 @@ impl<'a> Visitor<'a> for NewlineNormalizer {
let required_newlines = match self.trailer {
Trailer::FunctionDef | Trailer::ClassDef => self.depth.max_newlines(),
Trailer::Docstring if matches!(self.scope, Scope::Class) => 1,
Trailer::Import => {
if matches!(
stmt.node,
StmtKind::Import { .. } | StmtKind::ImportFrom { .. }
) {
0
} else {
1
}
}
Trailer::Import => usize::from(!matches!(
stmt.node,
StmtKind::Import { .. } | StmtKind::ImportFrom { .. }
)),
_ => 0,
};
let present_newlines = stmt

View File

@@ -22,21 +22,6 @@ where
}
}
/// Implement [`AsFormat`] for [`Option`] when `T` implements [`AsFormat`]
///
/// Allows to call format on optional AST fields without having to unwrap the
/// field first.
impl<T, C> AsFormat<C> for Option<T>
where
T: AsFormat<C>,
{
type Format<'a> = Option<T::Format<'a>> where Self: 'a;
fn format(&self) -> Self::Format<'_> {
self.as_ref().map(AsFormat::format)
}
}
/// Used to convert this object into an object that can be formatted.
///
/// The difference to [`AsFormat`] is that this trait takes ownership of `self`.

View File

@@ -7,7 +7,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.0.248"
version = "0.0.249"
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" }]