Compare commits

...

7 Commits

Author SHA1 Message Date
Zanie Blue
608df9a1bc Bump version to 0.3.3 (#10425)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-03-15 12:51:49 -05:00
Steve C
740c08b033 [pylint] - implement redeclared-assigned-name (W0128) (#9268)
## Summary

Implements
[`W0128`/`redeclared-assigned-name`](https://pylint.readthedocs.io/en/latest/user_guide/messages/warning/redeclared-assigned-name.html)

See: #970 

## Test Plan

`cargo test`
2024-03-15 09:43:55 -05:00
hikaru-kajita
7e652e8fcb [flake8_comprehensions] Handled special case for C400 which also matches C416 (#10419)
## Summary

Short-circuit implementation mentioned in #10403.

I implemented this by extending C400:
- Made `UnnecessaryGeneratorList` have information of whether the the
short-circuiting occurred (to put diagnostic)
- Add additional check for whether in `unnecessary_generator_list`
function.

Please give me suggestions if you think this isn't the best way to
handle this :)

## Test Plan

Extended `C400.py` a little, and written the cases where:
- Code could be converted to one single conversion to `list` e.g.
`list(x for x in range(3))` -> `list(range(3))`
- Code couldn't be converted to one single conversion to `list` e.g.
`list(2 * x for x in range(3))` -> `[2 * x for x in range(3)]`
- `list` function is not built-in, and should not modify the code in any
way.
2024-03-15 14:34:18 +00:00
Tom Kuson
9675e1867a Allow trailing ellipsis in typing.TYPE_CHECKING (#10413)
## Summary

Trailing ellipses in objects defined in `typing.TYPE_CHECKING` might be
meaningful (it might be declaring a stub). Thus, we should skip the
`unnecessary-placeholder` (`PIE970`) rule in such contexts.

Closes #10358.

## Test Plan

`cargo nextest run`
2024-03-15 03:55:57 +00:00
Charlie Marsh
10ace88e9a Track conditional deletions in the semantic model (#10415)
## Summary

Given `del X`, we'll typically add a `BindingKind::Deletion` to `X` to
shadow the current binding. However, if the deletion is inside of a
conditional operation, we _won't_, as in:

```python
def f():
    global X

    if X > 0:
        del X
```

We will, however, track it as a reference to the binding. This PR adds
the expression context to those resolved references, so that we can
detect that the `X` in `global X` was "assigned to".

Closes https://github.com/astral-sh/ruff/issues/10397.
2024-03-14 20:45:46 -04:00
Guilherme Vasconcelos
a8e50a7f40 [RUF008] Make it clearer that a mutable default in a dataclass is only valid if it is typed as a ClassVar (#10395)
## Summary

The previous documentation sounded as if typing a mutable default as a
`ClassVar` were optional. However, it is not, as not doing so causes a
`ValueError`. The snippet below was tested in Python's interactive
shell:

```
>>> from dataclasses import dataclass
>>> @dataclass
... class A:
...     mutable_default: list[int] = []
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.11/dataclasses.py", line 1230, in dataclass
    return wrap(cls)
           ^^^^^^^^^
  File "/usr/lib/python3.11/dataclasses.py", line 1220, in wrap
    return _process_class(cls, init, repr, eq, order, unsafe_hash,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/dataclasses.py", line 958, in _process_class
    cls_fields.append(_get_field(cls, name, type, kw_only))
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/dataclasses.py", line 815, in _get_field
    raise ValueError(f'mutable default {type(f.default)} for field '
ValueError: mutable default <class 'list'> for field mutable_default is not allowed: use default_factory
>>>
```

This behavior is also documented in Python's docs, see
[here](https://docs.python.org/3/library/dataclasses.html#mutable-default-values):

> [...] the
[dataclass()](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass)
decorator will raise a
[ValueError](https://docs.python.org/3/library/exceptions.html#ValueError)
if it detects an unhashable default parameter. The assumption is that if
a value is unhashable, it is mutable. This is a partial solution, but it
does protect against many common errors.

And
[here](https://docs.python.org/3/library/dataclasses.html#class-variables)
it is documented why it works if it is typed as a `ClassVar`:

> One of the few places where
[dataclass()](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass)
actually inspects the type of a field is to determine if a field is a
class variable as defined in [PEP
526](https://peps.python.org/pep-0526/). It does this by checking if the
type of the field is typing.ClassVar. If a field is a ClassVar, it is
excluded from consideration as a field and is ignored by the dataclass
mechanisms. Such ClassVar pseudo-fields are not returned by the
module-level
[fields()](https://docs.python.org/3/library/dataclasses.html#dataclasses.fields)
function.

In this PR I have changed the documentation to make it a little bit
clearer that not using `ClassVar` makes the code invalid.
2024-03-14 23:18:03 +00:00
Hoël Bagard
e944c16c46 [pycodestyle] Do not ignore lines before the first logical line in blank lines rules (#10382)
## Summary

Ignoring all lines until the first logical line does not match the
behavior from pycodestyle. This PR therefore removes the `if
state.is_not_first_logical_line` skipping the line check before the
first logical line, and applies it only to `E302`.

For example, in the snippet below a rule violation should be detected on
the second comment and on the import.

```python
# first comment




# second comment




import foo
```

Fixes #10374

## Test Plan

Add test cases, update the snapshots and verify the ecosystem check output
2024-03-14 14:05:24 +05:30
51 changed files with 725 additions and 114 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.3.2"
version = "0.3.3"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.3.2"
version = "0.3.3"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -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))

View File

@@ -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."""
...

View File

@@ -0,0 +1,4 @@
"""Test where the error is after the module's docstring."""
def fn():
pass

View File

@@ -0,0 +1,4 @@
"Test where the first line is a comment, " + "and the rule violation follows it."
def fn():
pass

View File

@@ -0,0 +1,5 @@
def fn1():
pass
def fn2():
pass

View File

@@ -0,0 +1,4 @@
print("Test where the first line is a statement, and the rule violation follows it.")
def fn():
pass

View File

@@ -0,0 +1,6 @@
# Test where the first line is a comment, and the rule violation follows it.
def fn():
pass

View File

@@ -0,0 +1,6 @@
"""Test where the error is after the module's docstring."""
def fn():
pass

View File

@@ -0,0 +1,6 @@
"Test where the first line is a comment, " + "and the rule violation follows it."
def fn():
pass

View File

@@ -0,0 +1,6 @@
print("Test where the first line is a statement, and the rule violation follows it.")
def fn():
pass

View File

@@ -11,6 +11,13 @@ def f():
print(X)
def f():
global X
if X > 0:
del X
###
# Non-errors.
###

View 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

View File

@@ -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(),
));
}
}
}
}

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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),

View File

@@ -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()))
}

View File

@@ -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);
}

View File

@@ -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):

View File

@@ -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.

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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"))]

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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());
}
_ => {}
}
}

View File

@@ -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
|

View File

@@ -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
///
///

View File

@@ -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:

View File

@@ -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,

View File

@@ -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,
})
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -3330,6 +3330,7 @@
"PLW012",
"PLW0120",
"PLW0127",
"PLW0128",
"PLW0129",
"PLW013",
"PLW0131",

View File

@@ -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>"]