Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cfa350112 | ||
|
|
41f163fc8d | ||
|
|
d21dd994e6 | ||
|
|
6f5a6b8c8b | ||
|
|
35606d7b05 | ||
|
|
3ad257cfea | ||
|
|
b39f960cd1 | ||
|
|
c297d46899 | ||
|
|
d6a100028c | ||
|
|
35d4e03f2a |
@@ -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
|
||||
|
||||
@@ -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
6
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.248"
|
||||
version = "0.0.249"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/0001_initial.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/0001_initial.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/__main__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/__main__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/__setup__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/__setup__.py
vendored
Normal 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"
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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: ~
|
||||
|
||||
|
||||
@@ -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(".")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pep8_naming/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
[]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pep8_naming/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
[]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pep8_naming/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
[]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: ~
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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; };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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" }]
|
||||
|
||||
Reference in New Issue
Block a user