Compare commits
7 Commits
dhruv/curr
...
v0.3.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
608df9a1bc | ||
|
|
740c08b033 | ||
|
|
7e652e8fcb | ||
|
|
9675e1867a | ||
|
|
10ace88e9a | ||
|
|
a8e50a7f40 | ||
|
|
e944c16c46 |
46
CHANGELOG.md
46
CHANGELOG.md
@@ -1,5 +1,49 @@
|
||||
# Changelog
|
||||
|
||||
## 0.3.3
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-bandit`\]: Implement `S610` rule ([#10316](https://github.com/astral-sh/ruff/pull/10316))
|
||||
- \[`pycodestyle`\] Implement `blank-line-at-end-of-file` (`W391`) ([#10243](https://github.com/astral-sh/ruff/pull/10243))
|
||||
- \[`pycodestyle`\] Implement `redundant-backslash` (`E502`) ([#10292](https://github.com/astral-sh/ruff/pull/10292))
|
||||
- \[`pylint`\] - implement `redeclared-assigned-name` (`W0128`) ([#9268](https://github.com/astral-sh/ruff/pull/9268))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8_comprehensions`\] Handled special case for `C400` which also matches `C416` ([#10419](https://github.com/astral-sh/ruff/pull/10419))
|
||||
- \[`flake8-bandit`\] Implement upstream updates for `S311`, `S324` and `S605` ([#10313](https://github.com/astral-sh/ruff/pull/10313))
|
||||
- \[`pyflakes`\] Remove `F401` fix for `__init__` imports by default and allow opt-in to unsafe fix ([#10365](https://github.com/astral-sh/ruff/pull/10365))
|
||||
- \[`pylint`\] Implement `invalid-bool-return-type` (`E304`) ([#10377](https://github.com/astral-sh/ruff/pull/10377))
|
||||
- \[`pylint`\] Include builtin warnings in useless-exception-statement (`PLW0133`) ([#10394](https://github.com/astral-sh/ruff/pull/10394))
|
||||
|
||||
### CLI
|
||||
|
||||
- Add message on success to `ruff check` ([#8631](https://github.com/astral-sh/ruff/pull/8631))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`PIE970`\] Allow trailing ellipsis in `typing.TYPE_CHECKING` ([#10413](https://github.com/astral-sh/ruff/pull/10413))
|
||||
- Avoid `TRIO115` if the argument is a variable ([#10376](https://github.com/astral-sh/ruff/pull/10376))
|
||||
- \[`F811`\] Avoid removing shadowed imports that point to different symbols ([#10387](https://github.com/astral-sh/ruff/pull/10387))
|
||||
- Fix `F821` and `F822` false positives in `.pyi` files ([#10341](https://github.com/astral-sh/ruff/pull/10341))
|
||||
- Fix `F821` false negatives in `.py` files when `from __future__ import annotations` is active ([#10362](https://github.com/astral-sh/ruff/pull/10362))
|
||||
- Fix case where `Indexer` fails to identify continuation preceded by newline #10351 ([#10354](https://github.com/astral-sh/ruff/pull/10354))
|
||||
- Sort hash maps in `Settings` display ([#10370](https://github.com/astral-sh/ruff/pull/10370))
|
||||
- Track conditional deletions in the semantic model ([#10415](https://github.com/astral-sh/ruff/pull/10415))
|
||||
- \[`C413`\] Wrap expressions in parentheses when negating ([#10346](https://github.com/astral-sh/ruff/pull/10346))
|
||||
- \[`pycodestyle`\] Do not ignore lines before the first logical line in blank lines rules. ([#10382](https://github.com/astral-sh/ruff/pull/10382))
|
||||
- \[`pycodestyle`\] Do not trigger `E225` and `E275` when the next token is a ')' ([#10315](https://github.com/astral-sh/ruff/pull/10315))
|
||||
- \[`pylint`\] Avoid false-positive slot non-assignment for `__dict__` (`PLE0237`) ([#10348](https://github.com/astral-sh/ruff/pull/10348))
|
||||
- Gate f-string struct size test for Rustc \< 1.76 ([#10371](https://github.com/astral-sh/ruff/pull/10371))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Use `ruff.toml` format in README ([#10393](https://github.com/astral-sh/ruff/pull/10393))
|
||||
- \[`RUF008`\] Make it clearer that a mutable default in a dataclass is only valid if it is typed as a ClassVar ([#10395](https://github.com/astral-sh/ruff/pull/10395))
|
||||
- \[`pylint`\] Extend docs and test in `invalid-str-return-type` (`E307`) ([#10400](https://github.com/astral-sh/ruff/pull/10400))
|
||||
- Remove `.` from `check` and `format` commands ([#10217](https://github.com/astral-sh/ruff/pull/10217))
|
||||
|
||||
## 0.3.2
|
||||
|
||||
### Preview features
|
||||
@@ -1199,7 +1243,7 @@ Read Ruff's new [versioning policy](https://docs.astral.sh/ruff/versioning/).
|
||||
- \[`refurb`\] Add `single-item-membership-test` (`FURB171`) ([#7815](https://github.com/astral-sh/ruff/pull/7815))
|
||||
- \[`pylint`\] Add `and-or-ternary` (`R1706`) ([#7811](https://github.com/astral-sh/ruff/pull/7811))
|
||||
|
||||
_New rules are added in [preview](https://docs.astral.sh/ruff/preview/)._
|
||||
*New rules are added in [preview](https://docs.astral.sh/ruff/preview/).*
|
||||
|
||||
### Configuration
|
||||
|
||||
|
||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -2003,7 +2003,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2167,7 +2167,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2448,7 +2448,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_shrinking"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
|
||||
@@ -151,7 +151,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.3.2
|
||||
rev: v0.3.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
# Cannot combine with C416. Should use list comprehension here.
|
||||
even_nums = list(2 * x for x in range(3))
|
||||
odd_nums = list(
|
||||
2 * x + 1 for x in range(3)
|
||||
)
|
||||
|
||||
|
||||
# Short-circuit case, combine with C416 and should produce x = list(range(3))
|
||||
x = list(x for x in range(3))
|
||||
x = list(
|
||||
x for x in range(3)
|
||||
)
|
||||
|
||||
|
||||
# Not built-in list.
|
||||
def list(*args, **kwargs):
|
||||
return None
|
||||
|
||||
|
||||
list(2 * x for x in range(3))
|
||||
list(x for x in range(3))
|
||||
|
||||
@@ -227,3 +227,11 @@ class Repro[int](Protocol):
|
||||
def impl(self) -> str:
|
||||
"""Docstring"""
|
||||
return self.func()
|
||||
|
||||
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
def contains_meaningful_ellipsis() -> list[int]:
|
||||
"""Allow this in a TYPE_CHECKING block."""
|
||||
...
|
||||
|
||||
4
crates/ruff_linter/resources/test/fixtures/pycodestyle/E302_first_line_docstring.py
vendored
Normal file
4
crates/ruff_linter/resources/test/fixtures/pycodestyle/E302_first_line_docstring.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Test where the error is after the module's docstring."""
|
||||
|
||||
def fn():
|
||||
pass
|
||||
4
crates/ruff_linter/resources/test/fixtures/pycodestyle/E302_first_line_expression.py
vendored
Normal file
4
crates/ruff_linter/resources/test/fixtures/pycodestyle/E302_first_line_expression.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
"Test where the first line is a comment, " + "and the rule violation follows it."
|
||||
|
||||
def fn():
|
||||
pass
|
||||
5
crates/ruff_linter/resources/test/fixtures/pycodestyle/E302_first_line_function.py
vendored
Normal file
5
crates/ruff_linter/resources/test/fixtures/pycodestyle/E302_first_line_function.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
def fn1():
|
||||
pass
|
||||
|
||||
def fn2():
|
||||
pass
|
||||
4
crates/ruff_linter/resources/test/fixtures/pycodestyle/E302_first_line_statement.py
vendored
Normal file
4
crates/ruff_linter/resources/test/fixtures/pycodestyle/E302_first_line_statement.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
print("Test where the first line is a statement, and the rule violation follows it.")
|
||||
|
||||
def fn():
|
||||
pass
|
||||
6
crates/ruff_linter/resources/test/fixtures/pycodestyle/E303_first_line_comment.py
vendored
Normal file
6
crates/ruff_linter/resources/test/fixtures/pycodestyle/E303_first_line_comment.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Test where the first line is a comment, and the rule violation follows it.
|
||||
|
||||
|
||||
|
||||
def fn():
|
||||
pass
|
||||
6
crates/ruff_linter/resources/test/fixtures/pycodestyle/E303_first_line_docstring.py
vendored
Normal file
6
crates/ruff_linter/resources/test/fixtures/pycodestyle/E303_first_line_docstring.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Test where the error is after the module's docstring."""
|
||||
|
||||
|
||||
|
||||
def fn():
|
||||
pass
|
||||
6
crates/ruff_linter/resources/test/fixtures/pycodestyle/E303_first_line_expression.py
vendored
Normal file
6
crates/ruff_linter/resources/test/fixtures/pycodestyle/E303_first_line_expression.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
"Test where the first line is a comment, " + "and the rule violation follows it."
|
||||
|
||||
|
||||
|
||||
def fn():
|
||||
pass
|
||||
6
crates/ruff_linter/resources/test/fixtures/pycodestyle/E303_first_line_statement.py
vendored
Normal file
6
crates/ruff_linter/resources/test/fixtures/pycodestyle/E303_first_line_statement.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
print("Test where the first line is a statement, and the rule violation follows it.")
|
||||
|
||||
|
||||
|
||||
def fn():
|
||||
pass
|
||||
@@ -11,6 +11,13 @@ def f():
|
||||
print(X)
|
||||
|
||||
|
||||
def f():
|
||||
global X
|
||||
|
||||
if X > 0:
|
||||
del X
|
||||
|
||||
|
||||
###
|
||||
# Non-errors.
|
||||
###
|
||||
|
||||
6
crates/ruff_linter/resources/test/fixtures/pylint/redeclared_assigned_name.py
vendored
Normal file
6
crates/ruff_linter/resources/test/fixtures/pylint/redeclared_assigned_name.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
FIRST, FIRST = (1, 2) # PLW0128
|
||||
FIRST, (FIRST, SECOND) = (1, (1, 2)) # PLW0128
|
||||
FIRST, (FIRST, SECOND, (THIRD, FIRST)) = (1, (1, 2)) # PLW0128
|
||||
FIRST, SECOND, THIRD, FIRST, SECOND = (1, 2, 3, 4) # PLW0128
|
||||
|
||||
FIRST, SECOND, _, _, _ignored = (1, 2, 3, 4, 5) # OK
|
||||
@@ -1,6 +1,6 @@
|
||||
use ruff_diagnostics::{Diagnostic, Fix};
|
||||
use ruff_python_semantic::analyze::visibility;
|
||||
use ruff_python_semantic::{Binding, BindingKind, Imported, ScopeKind};
|
||||
use ruff_python_semantic::{Binding, BindingKind, Imported, ResolvedReference, ScopeKind};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -91,13 +91,29 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
if checker.enabled(Rule::GlobalVariableNotAssigned) {
|
||||
for (name, binding_id) in scope.bindings() {
|
||||
let binding = checker.semantic.binding(binding_id);
|
||||
// If the binding is a `global`, then it's a top-level `global` that was never
|
||||
// assigned in the current scope. If it were assigned, the `global` would be
|
||||
// shadowed by the assignment.
|
||||
if binding.kind.is_global() {
|
||||
diagnostics.push(Diagnostic::new(
|
||||
pylint::rules::GlobalVariableNotAssigned {
|
||||
name: (*name).to_string(),
|
||||
},
|
||||
binding.range(),
|
||||
));
|
||||
// If the binding was conditionally deleted, it will include a reference within
|
||||
// a `Del` context, but won't be shadowed by a `BindingKind::Deletion`, as in:
|
||||
// ```python
|
||||
// if condition:
|
||||
// del var
|
||||
// ```
|
||||
if binding
|
||||
.references
|
||||
.iter()
|
||||
.map(|id| checker.semantic.reference(*id))
|
||||
.all(ResolvedReference::is_load)
|
||||
{
|
||||
diagnostics.push(Diagnostic::new(
|
||||
pylint::rules::GlobalVariableNotAssigned {
|
||||
name: (*name).to_string(),
|
||||
},
|
||||
binding.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1389,6 +1389,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
}
|
||||
}
|
||||
Stmt::Assign(assign @ ast::StmtAssign { targets, value, .. }) => {
|
||||
if checker.enabled(Rule::RedeclaredAssignedName) {
|
||||
pylint::rules::redeclared_assigned_name(checker, targets);
|
||||
}
|
||||
if checker.enabled(Rule::LambdaAssignment) {
|
||||
if let [target] = &targets[..] {
|
||||
pycodestyle::rules::lambda_assignment(checker, target, value, None, stmt);
|
||||
|
||||
@@ -540,7 +540,11 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
for name in names {
|
||||
if let Some((scope_id, binding_id)) = self.semantic.nonlocal(name) {
|
||||
// Mark the binding as "used".
|
||||
self.semantic.add_local_reference(binding_id, name.range());
|
||||
self.semantic.add_local_reference(
|
||||
binding_id,
|
||||
ExprContext::Load,
|
||||
name.range(),
|
||||
);
|
||||
|
||||
// Mark the binding in the enclosing scope as "rebound" in the current
|
||||
// scope.
|
||||
@@ -2113,7 +2117,8 @@ impl<'a> Checker<'a> {
|
||||
// Mark anything referenced in `__all__` as used.
|
||||
// TODO(charlie): `range` here should be the range of the name in `__all__`, not
|
||||
// the range of `__all__` itself.
|
||||
self.semantic.add_global_reference(binding_id, range);
|
||||
self.semantic
|
||||
.add_global_reference(binding_id, ExprContext::Load, range);
|
||||
} else {
|
||||
if self.semantic.global_scope().uses_star_imports() {
|
||||
if self.enabled(Rule::UndefinedLocalWithImportStarUsage) {
|
||||
|
||||
@@ -294,6 +294,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "W0108") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryLambda),
|
||||
(Pylint, "W0120") => (RuleGroup::Stable, rules::pylint::rules::UselessElseOnLoop),
|
||||
(Pylint, "W0127") => (RuleGroup::Stable, rules::pylint::rules::SelfAssigningVariable),
|
||||
(Pylint, "W0128") => (RuleGroup::Preview, rules::pylint::rules::RedeclaredAssignedName),
|
||||
(Pylint, "W0129") => (RuleGroup::Stable, rules::pylint::rules::AssertOnStringLiteral),
|
||||
(Pylint, "W0131") => (RuleGroup::Stable, rules::pylint::rules::NamedExprWithoutContext),
|
||||
(Pylint, "W0133") => (RuleGroup::Preview, rules::pylint::rules::UselessExceptionStatement),
|
||||
|
||||
@@ -255,6 +255,7 @@ impl Renamer {
|
||||
| BindingKind::ClassDefinition(_)
|
||||
| BindingKind::FunctionDefinition(_)
|
||||
| BindingKind::Deletion
|
||||
| BindingKind::ConditionalDeletion(_)
|
||||
| BindingKind::UnboundException(_) => {
|
||||
Some(Edit::range_replacement(target.to_string(), binding.range()))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
use ruff_python_ast::ExprGenerator;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -9,37 +11,53 @@ use super::helpers;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for unnecessary generators that can be rewritten as `list`
|
||||
/// comprehensions.
|
||||
/// comprehensions (or with `list` directly).
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// It is unnecessary to use `list` around a generator expression, since
|
||||
/// there are equivalent comprehensions for these types. Using a
|
||||
/// comprehension is clearer and more idiomatic.
|
||||
///
|
||||
/// Further, if the comprehension can be removed entirely, as in the case of
|
||||
/// `list(x for x in foo)`, it's better to use `list(foo)` directly, since it's
|
||||
/// even more direct.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// list(f(x) for x in foo)
|
||||
/// list(x for x in foo)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// [f(x) for x in foo]
|
||||
/// list(foo)
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
|
||||
/// when rewriting the call. In most cases, though, comments will be preserved.
|
||||
#[violation]
|
||||
pub struct UnnecessaryGeneratorList;
|
||||
pub struct UnnecessaryGeneratorList {
|
||||
short_circuit: bool,
|
||||
}
|
||||
|
||||
impl AlwaysFixableViolation for UnnecessaryGeneratorList {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Unnecessary generator (rewrite as a `list` comprehension)")
|
||||
if self.short_circuit {
|
||||
format!("Unnecessary generator (rewrite using `list()`")
|
||||
} else {
|
||||
format!("Unnecessary generator (rewrite as a `list` comprehension)")
|
||||
}
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Rewrite as a `list` comprehension".to_string()
|
||||
if self.short_circuit {
|
||||
"Rewrite using `list()`".to_string()
|
||||
} else {
|
||||
"Rewrite as a `list` comprehension".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,28 +74,59 @@ pub(crate) fn unnecessary_generator_list(checker: &mut Checker, call: &ast::Expr
|
||||
if !checker.semantic().is_builtin("list") {
|
||||
return;
|
||||
}
|
||||
if argument.is_generator_expr() {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorList, call.range());
|
||||
|
||||
// Convert `list(x for x in y)` to `[x for x in y]`.
|
||||
diagnostic.set_fix({
|
||||
// Replace `list(` with `[`.
|
||||
let call_start = Edit::replacement(
|
||||
"[".to_string(),
|
||||
call.start(),
|
||||
call.arguments.start() + TextSize::from(1),
|
||||
);
|
||||
let Some(ExprGenerator {
|
||||
elt, generators, ..
|
||||
}) = argument.as_generator_expr()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Replace `)` with `]`.
|
||||
let call_end = Edit::replacement(
|
||||
"]".to_string(),
|
||||
call.arguments.end() - TextSize::from(1),
|
||||
call.end(),
|
||||
);
|
||||
|
||||
Fix::unsafe_edits(call_start, [call_end])
|
||||
});
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
// Short-circuit: given `list(x for x in y)`, generate `list(y)` (in lieu of `[x for x in y]`).
|
||||
if let [generator] = generators.as_slice() {
|
||||
if generator.ifs.is_empty() && !generator.is_async {
|
||||
if ComparableExpr::from(elt) == ComparableExpr::from(&generator.target) {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnnecessaryGeneratorList {
|
||||
short_circuit: true,
|
||||
},
|
||||
call.range(),
|
||||
);
|
||||
let iterator = format!("list({})", checker.locator().slice(generator.iter.range()));
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
iterator,
|
||||
call.range(),
|
||||
)));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert `list(f(x) for x in y)` to `[f(x) for x in y]`.
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnnecessaryGeneratorList {
|
||||
short_circuit: false,
|
||||
},
|
||||
call.range(),
|
||||
);
|
||||
diagnostic.set_fix({
|
||||
// Replace `list(` with `[`.
|
||||
let call_start = Edit::replacement(
|
||||
"[".to_string(),
|
||||
call.start(),
|
||||
call.arguments.start() + TextSize::from(1),
|
||||
);
|
||||
|
||||
// Replace `)` with `]`.
|
||||
let call_end = Edit::replacement(
|
||||
"]".to_string(),
|
||||
call.arguments.end() - TextSize::from(1),
|
||||
call.end(),
|
||||
);
|
||||
|
||||
Fix::unsafe_edits(call_start, [call_end])
|
||||
});
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,42 +1,90 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs
|
||||
---
|
||||
C400.py:1:5: C400 [*] Unnecessary generator (rewrite as a `list` comprehension)
|
||||
C400.py:2:13: C400 [*] Unnecessary generator (rewrite as a `list` comprehension)
|
||||
|
|
||||
1 | x = list(x for x in range(3))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ C400
|
||||
2 | x = list(
|
||||
3 | x for x in range(3)
|
||||
1 | # Cannot combine with C416. Should use list comprehension here.
|
||||
2 | even_nums = list(2 * x for x in range(3))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C400
|
||||
3 | odd_nums = list(
|
||||
4 | 2 * x + 1 for x in range(3)
|
||||
|
|
||||
= help: Rewrite as a `list` comprehension
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |-x = list(x for x in range(3))
|
||||
1 |+x = [x for x in range(3)]
|
||||
2 2 | x = list(
|
||||
3 3 | x for x in range(3)
|
||||
4 4 | )
|
||||
1 1 | # Cannot combine with C416. Should use list comprehension here.
|
||||
2 |-even_nums = list(2 * x for x in range(3))
|
||||
2 |+even_nums = [2 * x for x in range(3)]
|
||||
3 3 | odd_nums = list(
|
||||
4 4 | 2 * x + 1 for x in range(3)
|
||||
5 5 | )
|
||||
|
||||
C400.py:2:5: C400 [*] Unnecessary generator (rewrite as a `list` comprehension)
|
||||
C400.py:3:12: C400 [*] Unnecessary generator (rewrite as a `list` comprehension)
|
||||
|
|
||||
1 | x = list(x for x in range(3))
|
||||
2 | x = list(
|
||||
| _____^
|
||||
3 | | x for x in range(3)
|
||||
4 | | )
|
||||
1 | # Cannot combine with C416. Should use list comprehension here.
|
||||
2 | even_nums = list(2 * x for x in range(3))
|
||||
3 | odd_nums = list(
|
||||
| ____________^
|
||||
4 | | 2 * x + 1 for x in range(3)
|
||||
5 | | )
|
||||
| |_^ C400
|
||||
|
|
||||
= help: Rewrite as a `list` comprehension
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 1 | x = list(x for x in range(3))
|
||||
2 |-x = list(
|
||||
2 |+x = [
|
||||
3 3 | x for x in range(3)
|
||||
4 |-)
|
||||
4 |+]
|
||||
5 5 |
|
||||
1 1 | # Cannot combine with C416. Should use list comprehension here.
|
||||
2 2 | even_nums = list(2 * x for x in range(3))
|
||||
3 |-odd_nums = list(
|
||||
3 |+odd_nums = [
|
||||
4 4 | 2 * x + 1 for x in range(3)
|
||||
5 |-)
|
||||
5 |+]
|
||||
6 6 |
|
||||
7 7 | def list(*args, **kwargs):
|
||||
7 7 |
|
||||
8 8 | # Short-circuit case, combine with C416 and should produce x = list(range(3))
|
||||
|
||||
C400.py:9:5: C400 [*] Unnecessary generator (rewrite using `list()`
|
||||
|
|
||||
8 | # Short-circuit case, combine with C416 and should produce x = list(range(3))
|
||||
9 | x = list(x for x in range(3))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ C400
|
||||
10 | x = list(
|
||||
11 | x for x in range(3)
|
||||
|
|
||||
= help: Rewrite using `list()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
6 6 |
|
||||
7 7 |
|
||||
8 8 | # Short-circuit case, combine with C416 and should produce x = list(range(3))
|
||||
9 |-x = list(x for x in range(3))
|
||||
9 |+x = list(range(3))
|
||||
10 10 | x = list(
|
||||
11 11 | x for x in range(3)
|
||||
12 12 | )
|
||||
|
||||
C400.py:10:5: C400 [*] Unnecessary generator (rewrite using `list()`
|
||||
|
|
||||
8 | # Short-circuit case, combine with C416 and should produce x = list(range(3))
|
||||
9 | x = list(x for x in range(3))
|
||||
10 | x = list(
|
||||
| _____^
|
||||
11 | | x for x in range(3)
|
||||
12 | | )
|
||||
| |_^ C400
|
||||
13 |
|
||||
14 | # Not built-in list.
|
||||
|
|
||||
= help: Rewrite using `list()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
7 7 |
|
||||
8 8 | # Short-circuit case, combine with C416 and should produce x = list(range(3))
|
||||
9 9 | x = list(x for x in range(3))
|
||||
10 |-x = list(
|
||||
11 |- x for x in range(3)
|
||||
12 |-)
|
||||
10 |+x = list(range(3))
|
||||
13 11 |
|
||||
14 12 | # Not built-in list.
|
||||
15 13 | def list(*args, **kwargs):
|
||||
|
||||
@@ -87,6 +87,12 @@ pub(crate) fn unnecessary_placeholder(checker: &mut Checker, body: &[Stmt]) {
|
||||
let kind = match stmt {
|
||||
Stmt::Pass(_) => Placeholder::Pass,
|
||||
Stmt::Expr(expr) if expr.value.is_ellipsis_literal_expr() => {
|
||||
// In a type-checking block, a trailing ellipsis might be meaningful. A
|
||||
// user might be using the type-checking context to declare a stub.
|
||||
if checker.semantic().in_type_checking_block() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ellipses are significant in protocol methods and abstract methods. Specifically,
|
||||
// Pyright uses the presence of an ellipsis to indicate that a method is a stub,
|
||||
// rather than a default implementation.
|
||||
|
||||
@@ -121,8 +121,7 @@ pub(crate) fn runtime_import_in_type_checking_block(
|
||||
checker
|
||||
.semantic()
|
||||
.reference(reference_id)
|
||||
.context()
|
||||
.is_runtime()
|
||||
.in_runtime_context()
|
||||
})
|
||||
{
|
||||
let Some(node_id) = binding.source else {
|
||||
@@ -155,8 +154,7 @@ pub(crate) fn runtime_import_in_type_checking_block(
|
||||
if checker.settings.flake8_type_checking.quote_annotations
|
||||
&& binding.references().all(|reference_id| {
|
||||
let reference = checker.semantic().reference(reference_id);
|
||||
reference.context().is_typing()
|
||||
|| reference.in_runtime_evaluated_annotation()
|
||||
reference.in_typing_context() || reference.in_runtime_evaluated_annotation()
|
||||
})
|
||||
{
|
||||
actions
|
||||
@@ -268,7 +266,7 @@ fn quote_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding])
|
||||
.flat_map(|ImportBinding { binding, .. }| {
|
||||
binding.references.iter().filter_map(|reference_id| {
|
||||
let reference = checker.semantic().reference(*reference_id);
|
||||
if reference.context().is_runtime() {
|
||||
if reference.in_runtime_context() {
|
||||
Some(quote_annotation(
|
||||
reference.expression_id()?,
|
||||
checker.semantic(),
|
||||
|
||||
@@ -499,7 +499,7 @@ fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) ->
|
||||
.flat_map(|ImportBinding { binding, .. }| {
|
||||
binding.references.iter().filter_map(|reference_id| {
|
||||
let reference = checker.semantic().reference(*reference_id);
|
||||
if reference.context().is_runtime() {
|
||||
if reference.in_runtime_context() {
|
||||
Some(quote_annotation(
|
||||
reference.expression_id()?,
|
||||
checker.semantic(),
|
||||
|
||||
@@ -171,6 +171,24 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::BlankLinesTopLevel, Path::new("E302_first_line_docstring.py"))]
|
||||
#[test_case(Rule::BlankLinesTopLevel, Path::new("E302_first_line_expression.py"))]
|
||||
#[test_case(Rule::BlankLinesTopLevel, Path::new("E302_first_line_function.py"))]
|
||||
#[test_case(Rule::BlankLinesTopLevel, Path::new("E302_first_line_statement.py"))]
|
||||
#[test_case(Rule::TooManyBlankLines, Path::new("E303_first_line_comment.py"))]
|
||||
#[test_case(Rule::TooManyBlankLines, Path::new("E303_first_line_docstring.py"))]
|
||||
#[test_case(Rule::TooManyBlankLines, Path::new("E303_first_line_expression.py"))]
|
||||
#[test_case(Rule::TooManyBlankLines, Path::new("E303_first_line_statement.py"))]
|
||||
fn blank_lines_first_line(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("pycodestyle").join(path).as_path(),
|
||||
&settings::LinterSettings::for_rule(rule_code),
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::BlankLineBetweenMethods, Path::new("E30.py"))]
|
||||
#[test_case(Rule::BlankLinesTopLevel, Path::new("E30.py"))]
|
||||
#[test_case(Rule::TooManyBlankLines, Path::new("E30.py"))]
|
||||
|
||||
@@ -696,9 +696,7 @@ impl<'a> BlankLinesChecker<'a> {
|
||||
state.class_status.update(&logical_line);
|
||||
state.fn_status.update(&logical_line);
|
||||
|
||||
if state.is_not_first_logical_line {
|
||||
self.check_line(&logical_line, &state, prev_indent_length, diagnostics);
|
||||
}
|
||||
self.check_line(&logical_line, &state, prev_indent_length, diagnostics);
|
||||
|
||||
match logical_line.kind {
|
||||
LogicalLineKind::Class => {
|
||||
@@ -818,6 +816,8 @@ impl<'a> BlankLinesChecker<'a> {
|
||||
&& line.kind.is_class_function_or_decorator()
|
||||
// Blank lines in stub files are used to group definitions. Don't enforce blank lines.
|
||||
&& !self.source_type.is_stub()
|
||||
// Do not expect blank lines before the first logical line.
|
||||
&& state.is_not_first_logical_line
|
||||
{
|
||||
// E302
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
E302_first_line_docstring.py:3:1: E302 [*] Expected 2 blank lines, found 1
|
||||
|
|
||||
1 | """Test where the error is after the module's docstring."""
|
||||
2 |
|
||||
3 | def fn():
|
||||
| ^^^ E302
|
||||
4 | pass
|
||||
|
|
||||
= help: Add missing blank line(s)
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | """Test where the error is after the module's docstring."""
|
||||
2 2 |
|
||||
3 |+
|
||||
3 4 | def fn():
|
||||
4 5 | pass
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
E302_first_line_expression.py:3:1: E302 [*] Expected 2 blank lines, found 1
|
||||
|
|
||||
1 | "Test where the first line is a comment, " + "and the rule violation follows it."
|
||||
2 |
|
||||
3 | def fn():
|
||||
| ^^^ E302
|
||||
4 | pass
|
||||
|
|
||||
= help: Add missing blank line(s)
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | "Test where the first line is a comment, " + "and the rule violation follows it."
|
||||
2 2 |
|
||||
3 |+
|
||||
3 4 | def fn():
|
||||
4 5 | pass
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
E302_first_line_function.py:4:1: E302 [*] Expected 2 blank lines, found 1
|
||||
|
|
||||
2 | pass
|
||||
3 |
|
||||
4 | def fn2():
|
||||
| ^^^ E302
|
||||
5 | pass
|
||||
|
|
||||
= help: Add missing blank line(s)
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | def fn1():
|
||||
2 2 | pass
|
||||
3 3 |
|
||||
4 |+
|
||||
4 5 | def fn2():
|
||||
5 6 | pass
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
E302_first_line_statement.py:3:1: E302 [*] Expected 2 blank lines, found 1
|
||||
|
|
||||
1 | print("Test where the first line is a statement, and the rule violation follows it.")
|
||||
2 |
|
||||
3 | def fn():
|
||||
| ^^^ E302
|
||||
4 | pass
|
||||
|
|
||||
= help: Add missing blank line(s)
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | print("Test where the first line is a statement, and the rule violation follows it.")
|
||||
2 2 |
|
||||
3 |+
|
||||
3 4 | def fn():
|
||||
4 5 | pass
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
E303_first_line_comment.py:5:1: E303 [*] Too many blank lines (3)
|
||||
|
|
||||
5 | def fn():
|
||||
| ^^^ E303
|
||||
6 | pass
|
||||
|
|
||||
= help: Remove extraneous blank line(s)
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Test where the first line is a comment, and the rule violation follows it.
|
||||
2 2 |
|
||||
3 3 |
|
||||
4 |-
|
||||
5 4 | def fn():
|
||||
6 5 | pass
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
E303_first_line_docstring.py:5:1: E303 [*] Too many blank lines (3)
|
||||
|
|
||||
5 | def fn():
|
||||
| ^^^ E303
|
||||
6 | pass
|
||||
|
|
||||
= help: Remove extraneous blank line(s)
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | """Test where the error is after the module's docstring."""
|
||||
2 2 |
|
||||
3 3 |
|
||||
4 |-
|
||||
5 4 | def fn():
|
||||
6 5 | pass
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
E303_first_line_expression.py:5:1: E303 [*] Too many blank lines (3)
|
||||
|
|
||||
5 | def fn():
|
||||
| ^^^ E303
|
||||
6 | pass
|
||||
|
|
||||
= help: Remove extraneous blank line(s)
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | "Test where the first line is a comment, " + "and the rule violation follows it."
|
||||
2 2 |
|
||||
3 3 |
|
||||
4 |-
|
||||
5 4 | def fn():
|
||||
6 5 | pass
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
E303_first_line_statement.py:5:1: E303 [*] Too many blank lines (3)
|
||||
|
|
||||
5 | def fn():
|
||||
| ^^^ E303
|
||||
6 | pass
|
||||
|
|
||||
= help: Remove extraneous blank line(s)
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | print("Test where the first line is a statement, and the rule violation follows it.")
|
||||
2 2 |
|
||||
3 3 |
|
||||
4 |-
|
||||
5 4 | def fn():
|
||||
6 5 | pass
|
||||
@@ -95,6 +95,7 @@ mod tests {
|
||||
#[test_case(Rule::NonlocalWithoutBinding, Path::new("nonlocal_without_binding.py"))]
|
||||
#[test_case(Rule::NonSlotAssignment, Path::new("non_slot_assignment.py"))]
|
||||
#[test_case(Rule::PropertyWithParameters, Path::new("property_with_parameters.py"))]
|
||||
#[test_case(Rule::RedeclaredAssignedName, Path::new("redeclared_assigned_name.py"))]
|
||||
#[test_case(
|
||||
Rule::RedefinedArgumentFromLocal,
|
||||
Path::new("redefined_argument_from_local.py")
|
||||
|
||||
@@ -47,6 +47,7 @@ pub(crate) use non_slot_assignment::*;
|
||||
pub(crate) use nonlocal_without_binding::*;
|
||||
pub(crate) use potential_index_error::*;
|
||||
pub(crate) use property_with_parameters::*;
|
||||
pub(crate) use redeclared_assigned_name::*;
|
||||
pub(crate) use redefined_argument_from_local::*;
|
||||
pub(crate) use redefined_loop_name::*;
|
||||
pub(crate) use repeated_equality_comparison::*;
|
||||
@@ -136,6 +137,7 @@ mod non_slot_assignment;
|
||||
mod nonlocal_without_binding;
|
||||
mod potential_index_error;
|
||||
mod property_with_parameters;
|
||||
mod redeclared_assigned_name;
|
||||
mod redefined_argument_from_local;
|
||||
mod redefined_loop_name;
|
||||
mod repeated_equality_comparison;
|
||||
|
||||
@@ -67,6 +67,7 @@ pub(crate) fn non_ascii_name(binding: &Binding, locator: &Locator) -> Option<Dia
|
||||
| BindingKind::FromImport(_)
|
||||
| BindingKind::SubmoduleImport(_)
|
||||
| BindingKind::Deletion
|
||||
| BindingKind::ConditionalDeletion(_)
|
||||
| BindingKind::UnboundException(_) => {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for declared assignments to the same variable multiple times
|
||||
/// in the same assignment.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Assigning a variable multiple times in the same assignment is redundant,
|
||||
/// as the final assignment to the variable is what the value will be.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// a, b, a = (1, 2, 3)
|
||||
/// print(a) # 3
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// # this is assuming you want to assign 3 to `a`
|
||||
/// _, b, a = (1, 2, 3)
|
||||
/// print(a) # 3
|
||||
/// ```
|
||||
///
|
||||
#[violation]
|
||||
pub struct RedeclaredAssignedName {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Violation for RedeclaredAssignedName {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let RedeclaredAssignedName { name } = self;
|
||||
format!("Redeclared variable `{name}` in assignment")
|
||||
}
|
||||
}
|
||||
|
||||
/// PLW0128
|
||||
pub(crate) fn redeclared_assigned_name(checker: &mut Checker, targets: &Vec<Expr>) {
|
||||
let mut names: Vec<String> = Vec::new();
|
||||
|
||||
for target in targets {
|
||||
check_expr(checker, target, &mut names);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_expr(checker: &mut Checker, expr: &Expr, names: &mut Vec<String>) {
|
||||
match expr {
|
||||
Expr::Tuple(ast::ExprTuple { elts, .. }) => {
|
||||
for target in elts {
|
||||
check_expr(checker, target, names);
|
||||
}
|
||||
}
|
||||
Expr::Name(ast::ExprName { id, .. }) => {
|
||||
if checker.settings.dummy_variable_rgx.is_match(id) {
|
||||
// Ignore dummy variable assignments
|
||||
return;
|
||||
}
|
||||
if names.contains(id) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
RedeclaredAssignedName {
|
||||
name: id.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
));
|
||||
}
|
||||
names.push(id.to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||
---
|
||||
redeclared_assigned_name.py:1:8: PLW0128 Redeclared variable `FIRST` in assignment
|
||||
|
|
||||
1 | FIRST, FIRST = (1, 2) # PLW0128
|
||||
| ^^^^^ PLW0128
|
||||
2 | FIRST, (FIRST, SECOND) = (1, (1, 2)) # PLW0128
|
||||
3 | FIRST, (FIRST, SECOND, (THIRD, FIRST)) = (1, (1, 2)) # PLW0128
|
||||
|
|
||||
|
||||
redeclared_assigned_name.py:2:9: PLW0128 Redeclared variable `FIRST` in assignment
|
||||
|
|
||||
1 | FIRST, FIRST = (1, 2) # PLW0128
|
||||
2 | FIRST, (FIRST, SECOND) = (1, (1, 2)) # PLW0128
|
||||
| ^^^^^ PLW0128
|
||||
3 | FIRST, (FIRST, SECOND, (THIRD, FIRST)) = (1, (1, 2)) # PLW0128
|
||||
4 | FIRST, SECOND, THIRD, FIRST, SECOND = (1, 2, 3, 4) # PLW0128
|
||||
|
|
||||
|
||||
redeclared_assigned_name.py:3:9: PLW0128 Redeclared variable `FIRST` in assignment
|
||||
|
|
||||
1 | FIRST, FIRST = (1, 2) # PLW0128
|
||||
2 | FIRST, (FIRST, SECOND) = (1, (1, 2)) # PLW0128
|
||||
3 | FIRST, (FIRST, SECOND, (THIRD, FIRST)) = (1, (1, 2)) # PLW0128
|
||||
| ^^^^^ PLW0128
|
||||
4 | FIRST, SECOND, THIRD, FIRST, SECOND = (1, 2, 3, 4) # PLW0128
|
||||
|
|
||||
|
||||
redeclared_assigned_name.py:3:32: PLW0128 Redeclared variable `FIRST` in assignment
|
||||
|
|
||||
1 | FIRST, FIRST = (1, 2) # PLW0128
|
||||
2 | FIRST, (FIRST, SECOND) = (1, (1, 2)) # PLW0128
|
||||
3 | FIRST, (FIRST, SECOND, (THIRD, FIRST)) = (1, (1, 2)) # PLW0128
|
||||
| ^^^^^ PLW0128
|
||||
4 | FIRST, SECOND, THIRD, FIRST, SECOND = (1, 2, 3, 4) # PLW0128
|
||||
|
|
||||
|
||||
redeclared_assigned_name.py:4:23: PLW0128 Redeclared variable `FIRST` in assignment
|
||||
|
|
||||
2 | FIRST, (FIRST, SECOND) = (1, (1, 2)) # PLW0128
|
||||
3 | FIRST, (FIRST, SECOND, (THIRD, FIRST)) = (1, (1, 2)) # PLW0128
|
||||
4 | FIRST, SECOND, THIRD, FIRST, SECOND = (1, 2, 3, 4) # PLW0128
|
||||
| ^^^^^ PLW0128
|
||||
5 |
|
||||
6 | FIRST, SECOND, _, _, _ignored = (1, 2, 3, 4, 5) # OK
|
||||
|
|
||||
|
||||
redeclared_assigned_name.py:4:30: PLW0128 Redeclared variable `SECOND` in assignment
|
||||
|
|
||||
2 | FIRST, (FIRST, SECOND) = (1, (1, 2)) # PLW0128
|
||||
3 | FIRST, (FIRST, SECOND, (THIRD, FIRST)) = (1, (1, 2)) # PLW0128
|
||||
4 | FIRST, SECOND, THIRD, FIRST, SECOND = (1, 2, 3, 4) # PLW0128
|
||||
| ^^^^^^ PLW0128
|
||||
5 |
|
||||
6 | FIRST, SECOND, _, _, _ignored = (1, 2, 3, 4, 5) # OK
|
||||
|
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ use crate::rules::ruff::rules::helpers::{is_class_var_annotation, is_dataclass};
|
||||
/// Instead of sharing mutable defaults, use the `field(default_factory=...)`
|
||||
/// pattern.
|
||||
///
|
||||
/// If the default value is intended to be mutable, it should be annotated with
|
||||
/// `typing.ClassVar`.
|
||||
/// If the default value is intended to be mutable, it must be annotated with
|
||||
/// `typing.ClassVar`; otherwise, a `ValueError` will be raised.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
@@ -29,6 +29,8 @@ use crate::rules::ruff::rules::helpers::{is_class_var_annotation, is_dataclass};
|
||||
///
|
||||
/// @dataclass
|
||||
/// class A:
|
||||
/// # A list without a `default_factory` or `ClassVar` annotation
|
||||
/// # will raise a `ValueError`.
|
||||
/// mutable_default: list[int] = []
|
||||
/// ```
|
||||
///
|
||||
@@ -44,7 +46,7 @@ use crate::rules::ruff::rules::helpers::{is_class_var_annotation, is_dataclass};
|
||||
///
|
||||
/// Or:
|
||||
/// ```python
|
||||
/// from dataclasses import dataclass, field
|
||||
/// from dataclasses import dataclass
|
||||
/// from typing import ClassVar
|
||||
///
|
||||
///
|
||||
|
||||
@@ -75,6 +75,11 @@ impl<'a> Binding<'a> {
|
||||
self.flags.intersects(BindingFlags::GLOBAL)
|
||||
}
|
||||
|
||||
/// Return `true` if this [`Binding`] was deleted.
|
||||
pub const fn is_deleted(&self) -> bool {
|
||||
self.flags.intersects(BindingFlags::DELETED)
|
||||
}
|
||||
|
||||
/// Return `true` if this [`Binding`] represents an assignment to `__all__` with an invalid
|
||||
/// value (e.g., `__all__ = "Foo"`).
|
||||
pub const fn is_invalid_all_format(&self) -> bool {
|
||||
@@ -165,6 +170,7 @@ impl<'a> Binding<'a> {
|
||||
// Deletions, annotations, `__future__` imports, and builtins are never considered
|
||||
// redefinitions.
|
||||
BindingKind::Deletion
|
||||
| BindingKind::ConditionalDeletion(_)
|
||||
| BindingKind::Annotation
|
||||
| BindingKind::FutureImport
|
||||
| BindingKind::Builtin => {
|
||||
@@ -265,6 +271,19 @@ bitflags! {
|
||||
/// ```
|
||||
const GLOBAL = 1 << 4;
|
||||
|
||||
/// The binding was deleted (i.e., the target of a `del` statement).
|
||||
///
|
||||
/// For example, the binding could be `x` in:
|
||||
/// ```python
|
||||
/// del x
|
||||
/// ```
|
||||
///
|
||||
/// The semantic model will typically shadow a deleted binding via an additional binding
|
||||
/// with [`BindingKind::Deletion`]; however, conditional deletions (e.g.,
|
||||
/// `if condition: del x`) do _not_ generate a shadow binding. This flag is thus used to
|
||||
/// detect whether a binding was _ever_ deleted, even conditionally.
|
||||
const DELETED = 1 << 5;
|
||||
|
||||
/// The binding represents an export via `__all__`, but the assigned value uses an invalid
|
||||
/// expression (i.e., a non-container type).
|
||||
///
|
||||
@@ -272,7 +291,7 @@ bitflags! {
|
||||
/// ```python
|
||||
/// __all__ = 1
|
||||
/// ```
|
||||
const INVALID_ALL_FORMAT = 1 << 5;
|
||||
const INVALID_ALL_FORMAT = 1 << 6;
|
||||
|
||||
/// The binding represents an export via `__all__`, but the assigned value contains an
|
||||
/// invalid member (i.e., a non-string).
|
||||
@@ -281,7 +300,7 @@ bitflags! {
|
||||
/// ```python
|
||||
/// __all__ = [1]
|
||||
/// ```
|
||||
const INVALID_ALL_OBJECT = 1 << 6;
|
||||
const INVALID_ALL_OBJECT = 1 << 7;
|
||||
|
||||
/// The binding represents a private declaration.
|
||||
///
|
||||
@@ -289,7 +308,7 @@ bitflags! {
|
||||
/// ```python
|
||||
/// _T = "This is a private variable"
|
||||
/// ```
|
||||
const PRIVATE_DECLARATION = 1 << 7;
|
||||
const PRIVATE_DECLARATION = 1 << 8;
|
||||
|
||||
/// The binding represents an unpacked assignment.
|
||||
///
|
||||
@@ -297,7 +316,7 @@ bitflags! {
|
||||
/// ```python
|
||||
/// (x, y) = 1, 2
|
||||
/// ```
|
||||
const UNPACKED_ASSIGNMENT = 1 << 8;
|
||||
const UNPACKED_ASSIGNMENT = 1 << 9;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,6 +531,13 @@ pub enum BindingKind<'a> {
|
||||
/// ```
|
||||
Deletion,
|
||||
|
||||
/// A binding for a deletion, like `x` in:
|
||||
/// ```python
|
||||
/// if x > 0:
|
||||
/// del x
|
||||
/// ```
|
||||
ConditionalDeletion(BindingId),
|
||||
|
||||
/// A binding to bind an exception to a local variable, like `x` in:
|
||||
/// ```python
|
||||
/// try:
|
||||
|
||||
@@ -5,7 +5,7 @@ use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_python_ast::helpers::from_relative_import;
|
||||
use ruff_python_ast::name::{QualifiedName, UnqualifiedName};
|
||||
use ruff_python_ast::{self as ast, Expr, Operator, Stmt};
|
||||
use ruff_python_ast::{self as ast, Expr, ExprContext, Operator, Stmt};
|
||||
use ruff_python_stdlib::path::is_python_stub_file;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
@@ -271,7 +271,7 @@ impl<'a> SemanticModel<'a> {
|
||||
.get(symbol)
|
||||
.map_or(true, |binding_id| {
|
||||
// Treat the deletion of a name as a reference to that name.
|
||||
self.add_local_reference(binding_id, range);
|
||||
self.add_local_reference(binding_id, ExprContext::Del, range);
|
||||
self.bindings[binding_id].is_unbound()
|
||||
});
|
||||
|
||||
@@ -296,8 +296,9 @@ impl<'a> SemanticModel<'a> {
|
||||
let reference_id = self.resolved_references.push(
|
||||
ScopeId::global(),
|
||||
self.node_id,
|
||||
name.range,
|
||||
ExprContext::Load,
|
||||
self.flags,
|
||||
name.range,
|
||||
);
|
||||
self.bindings[binding_id].references.push(reference_id);
|
||||
|
||||
@@ -308,8 +309,9 @@ impl<'a> SemanticModel<'a> {
|
||||
let reference_id = self.resolved_references.push(
|
||||
ScopeId::global(),
|
||||
self.node_id,
|
||||
name.range,
|
||||
ExprContext::Load,
|
||||
self.flags,
|
||||
name.range,
|
||||
);
|
||||
self.bindings[binding_id].references.push(reference_id);
|
||||
}
|
||||
@@ -365,8 +367,9 @@ impl<'a> SemanticModel<'a> {
|
||||
let reference_id = self.resolved_references.push(
|
||||
self.scope_id,
|
||||
self.node_id,
|
||||
name.range,
|
||||
ExprContext::Load,
|
||||
self.flags,
|
||||
name.range,
|
||||
);
|
||||
self.bindings[binding_id].references.push(reference_id);
|
||||
|
||||
@@ -377,8 +380,9 @@ impl<'a> SemanticModel<'a> {
|
||||
let reference_id = self.resolved_references.push(
|
||||
self.scope_id,
|
||||
self.node_id,
|
||||
name.range,
|
||||
ExprContext::Load,
|
||||
self.flags,
|
||||
name.range,
|
||||
);
|
||||
self.bindings[binding_id].references.push(reference_id);
|
||||
}
|
||||
@@ -426,6 +430,15 @@ impl<'a> SemanticModel<'a> {
|
||||
return ReadResult::UnboundLocal(binding_id);
|
||||
}
|
||||
|
||||
BindingKind::ConditionalDeletion(binding_id) => {
|
||||
self.unresolved_references.push(
|
||||
name.range,
|
||||
self.exceptions(),
|
||||
UnresolvedReferenceFlags::empty(),
|
||||
);
|
||||
return ReadResult::UnboundLocal(binding_id);
|
||||
}
|
||||
|
||||
// If we hit an unbound exception that shadowed a bound name, resole to the
|
||||
// bound name. For example, given:
|
||||
//
|
||||
@@ -446,8 +459,9 @@ impl<'a> SemanticModel<'a> {
|
||||
let reference_id = self.resolved_references.push(
|
||||
self.scope_id,
|
||||
self.node_id,
|
||||
name.range,
|
||||
ExprContext::Load,
|
||||
self.flags,
|
||||
name.range,
|
||||
);
|
||||
self.bindings[binding_id].references.push(reference_id);
|
||||
|
||||
@@ -458,8 +472,9 @@ impl<'a> SemanticModel<'a> {
|
||||
let reference_id = self.resolved_references.push(
|
||||
self.scope_id,
|
||||
self.node_id,
|
||||
name.range,
|
||||
ExprContext::Load,
|
||||
self.flags,
|
||||
name.range,
|
||||
);
|
||||
self.bindings[binding_id].references.push(reference_id);
|
||||
}
|
||||
@@ -548,6 +563,7 @@ impl<'a> SemanticModel<'a> {
|
||||
match self.bindings[binding_id].kind {
|
||||
BindingKind::Annotation => continue,
|
||||
BindingKind::Deletion | BindingKind::UnboundException(None) => return None,
|
||||
BindingKind::ConditionalDeletion(binding_id) => return Some(binding_id),
|
||||
BindingKind::UnboundException(Some(binding_id)) => return Some(binding_id),
|
||||
_ => return Some(binding_id),
|
||||
}
|
||||
@@ -1315,18 +1331,28 @@ impl<'a> SemanticModel<'a> {
|
||||
}
|
||||
|
||||
/// Add a reference to the given [`BindingId`] in the local scope.
|
||||
pub fn add_local_reference(&mut self, binding_id: BindingId, range: TextRange) {
|
||||
pub fn add_local_reference(
|
||||
&mut self,
|
||||
binding_id: BindingId,
|
||||
ctx: ExprContext,
|
||||
range: TextRange,
|
||||
) {
|
||||
let reference_id =
|
||||
self.resolved_references
|
||||
.push(self.scope_id, self.node_id, range, self.flags);
|
||||
.push(self.scope_id, self.node_id, ctx, self.flags, range);
|
||||
self.bindings[binding_id].references.push(reference_id);
|
||||
}
|
||||
|
||||
/// Add a reference to the given [`BindingId`] in the global scope.
|
||||
pub fn add_global_reference(&mut self, binding_id: BindingId, range: TextRange) {
|
||||
pub fn add_global_reference(
|
||||
&mut self,
|
||||
binding_id: BindingId,
|
||||
ctx: ExprContext,
|
||||
range: TextRange,
|
||||
) {
|
||||
let reference_id =
|
||||
self.resolved_references
|
||||
.push(ScopeId::global(), self.node_id, range, self.flags);
|
||||
.push(ScopeId::global(), self.node_id, ctx, self.flags, range);
|
||||
self.bindings[binding_id].references.push(reference_id);
|
||||
}
|
||||
|
||||
@@ -1700,7 +1726,6 @@ bitflags! {
|
||||
/// only required by the Python interpreter, but by runtime type checkers too.
|
||||
const RUNTIME_REQUIRED_ANNOTATION = 1 << 2;
|
||||
|
||||
|
||||
/// The model is in a type definition.
|
||||
///
|
||||
/// For example, the model could be visiting `int` in:
|
||||
@@ -1886,7 +1911,6 @@ bitflags! {
|
||||
/// ```
|
||||
const COMPREHENSION_ASSIGNMENT = 1 << 19;
|
||||
|
||||
|
||||
/// The model is in a module / class / function docstring.
|
||||
///
|
||||
/// For example, the model could be visiting either the module, class,
|
||||
|
||||
@@ -3,10 +3,10 @@ use std::ops::Deref;
|
||||
use bitflags::bitflags;
|
||||
|
||||
use ruff_index::{newtype_index, IndexSlice, IndexVec};
|
||||
use ruff_python_ast::ExprContext;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::context::ExecutionContext;
|
||||
use crate::scope::ScopeId;
|
||||
use crate::{Exceptions, NodeId, SemanticModelFlags};
|
||||
|
||||
@@ -18,10 +18,12 @@ pub struct ResolvedReference {
|
||||
node_id: Option<NodeId>,
|
||||
/// The scope in which the reference is defined.
|
||||
scope_id: ScopeId,
|
||||
/// The range of the reference in the source code.
|
||||
range: TextRange,
|
||||
/// The expression context in which the reference occurs (e.g., `Load`, `Store`, `Del`).
|
||||
ctx: ExprContext,
|
||||
/// The model state in which the reference occurs.
|
||||
flags: SemanticModelFlags,
|
||||
/// The range of the reference in the source code.
|
||||
range: TextRange,
|
||||
}
|
||||
|
||||
impl ResolvedReference {
|
||||
@@ -35,13 +37,19 @@ impl ResolvedReference {
|
||||
self.scope_id
|
||||
}
|
||||
|
||||
/// The [`ExecutionContext`] of the reference.
|
||||
pub const fn context(&self) -> ExecutionContext {
|
||||
if self.flags.intersects(SemanticModelFlags::TYPING_CONTEXT) {
|
||||
ExecutionContext::Typing
|
||||
} else {
|
||||
ExecutionContext::Runtime
|
||||
}
|
||||
/// Return `true` if the reference occurred in a `Load` operation.
|
||||
pub const fn is_load(&self) -> bool {
|
||||
self.ctx.is_load()
|
||||
}
|
||||
|
||||
/// Return `true` if the context is in a typing context.
|
||||
pub const fn in_typing_context(&self) -> bool {
|
||||
self.flags.intersects(SemanticModelFlags::TYPING_CONTEXT)
|
||||
}
|
||||
|
||||
/// Return `true` if the context is in a runtime context.
|
||||
pub const fn in_runtime_context(&self) -> bool {
|
||||
!self.flags.intersects(SemanticModelFlags::TYPING_CONTEXT)
|
||||
}
|
||||
|
||||
/// Return `true` if the context is in a typing-only type annotation.
|
||||
@@ -108,14 +116,16 @@ impl ResolvedReferences {
|
||||
&mut self,
|
||||
scope_id: ScopeId,
|
||||
node_id: Option<NodeId>,
|
||||
range: TextRange,
|
||||
ctx: ExprContext,
|
||||
flags: SemanticModelFlags,
|
||||
range: TextRange,
|
||||
) -> ResolvedReferenceId {
|
||||
self.0.push(ResolvedReference {
|
||||
node_id,
|
||||
scope_id,
|
||||
range,
|
||||
ctx,
|
||||
flags,
|
||||
range,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_shrinking"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -14,7 +14,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.3.2
|
||||
rev: v0.3.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -27,7 +27,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook:
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.3.2
|
||||
rev: v0.3.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -41,7 +41,7 @@ To run the hooks over Jupyter Notebooks too, add `jupyter` to the list of allowe
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.3.2
|
||||
rev: v0.3.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "ruff"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
|
||||
readme = "README.md"
|
||||
|
||||
1
ruff.schema.json
generated
1
ruff.schema.json
generated
@@ -3330,6 +3330,7 @@
|
||||
"PLW012",
|
||||
"PLW0120",
|
||||
"PLW0127",
|
||||
"PLW0128",
|
||||
"PLW0129",
|
||||
"PLW013",
|
||||
"PLW0131",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "scripts"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
description = ""
|
||||
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user