Compare commits

..

19 Commits

Author SHA1 Message Date
Zanie
d2b0b1ca26 Simplify comment 2023-08-22 10:23:31 -05:00
Zanie
6d88ac4ca2 Restore unrelated snapshot 2023-08-22 10:22:34 -05:00
Zanie
268be0cf8e Add more test cases; check format types that occur after placeholders 2023-08-22 10:08:08 -05:00
Zanie
5df9ba716f Consume all remaining placeholders at end of format spec parsing 2023-08-22 09:57:13 -05:00
Zanie
0e3284244b Add failing test case for bad-string-format-character 2023-08-22 09:37:26 -05:00
Zanie
d272874dfd Clean up implementation 2023-08-22 09:36:48 -05:00
Micha Reiser
ccac9681e1 Preserve yield parentheses (#6766) 2023-08-22 10:27:20 +00:00
Micha Reiser
b52cc84df6 Omit tuple parentheses in for statements except when absolutely necessary (#6765) 2023-08-22 12:18:59 +02:00
Micha Reiser
fec6fc2fab Preserve empty lines between try clause headers (#6759) 2023-08-22 11:50:28 +02:00
konsti
ba4c27598a Document IO Error (#6712)
`IOError` is special, it is not actually a lint but an error before
linting. I'm not entirely sure how to document it since it does not
match the general lint rule pattern (`Checks that the file can be read
in its entirety.` is imho worse).

I added the in my experience two most common reasons for io errors on
unix systems and linked two tutorials on how to fix them.

See https://github.com/astral-sh/ruff/issues/2646

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2023-08-22 11:46:18 +02:00
Victor Hugo Gomes
0f9ccfcad9 Format PatternMatchSingleton (#6741) 2023-08-22 08:23:47 +02:00
Charlie Marsh
fa32cd9b6f Truncate some messages in diagnostics (#6748)
## Summary

I noticed this in the ecosystem CI check from
https://github.com/astral-sh/ruff/pull/6742. If we include source code
directly in a diagnostic, we need to be careful to avoid rendering
multi-line diagnostics or even excessively long diagnostics.

## Test Plan

`cargo test`
2023-08-21 23:46:24 -04:00
Victor Hugo Gomes
0aad0c41f6 [pylint] Implement no-self-use (R6301) (#6574) 2023-08-22 03:44:38 +00:00
Charlie Marsh
424b8d4ad2 Use a single node hierarchy to track statements and expressions (#6709)
## Summary

This PR is a follow-up to the suggestion in
https://github.com/astral-sh/ruff/pull/6345#discussion_r1285470953 to
use a single stack to store all statements and expressions, rather than
using separate vectors for each, which gives us something closer to a
full-fidelity chain. (We can then generalize this concept to include all
other AST nodes too.)

This is in part made possible by the removal of the hash map from
`&Stmt` to `StatementId` (#6694), which makes it much cheaper to store
these using a single interface (since doing so no longer introduces the
requirement that we hash all expressions).

I'll follow-up with some profiling, but a few notes on how the data
requirements have changed:

- We now store a `BranchId` for every expression, not just every
statement, so that's an extra `u32`.
- We now store a single `NodeId` on every snapshot, rather than separate
`StatementId` and `ExpressionId` IDs, so that's one fewer `u32` for each
snapshot.
- We're probably doing a few more lookups in general, since any calls to
`current_statement()` etc. now have to iterate up the node hierarchy
until they identify the first statement.

## Test Plan

`cargo test`
2023-08-21 21:32:57 -04:00
Charlie Marsh
abc5065fc7 Avoid E231 if comma is at end-of-line (#6747)
## Summary

I don't know how this could come up in valid Python, but anyway...

Closes https://github.com/astral-sh/ruff/issues/6738.
2023-08-21 20:47:20 -04:00
Victor Hugo Gomes
37f4920e1e Don't trigger eq-without-hash when __hash__ is explicitly set to None (#6739) 2023-08-21 23:51:21 +00:00
Charlie Marsh
c0df99b965 Avoid attempting to fix unconventional submodule imports (#6745)
## Summary

Avoid attempting to rewrite `import matplotlib.pyplot` as `import
matplotlib.pyplot as plt`. We can't support these right now, since we
don't track references at the attribute level (like
`matplotlib.pyplot`).

Closes https://github.com/astral-sh/ruff/issues/6719.
2023-08-21 23:45:32 +00:00
Charlie Marsh
7650c6ee45 Support C419 autofixes for set comprehensions (#6744)
Closes https://github.com/astral-sh/ruff/issues/6713.
2023-08-21 23:41:13 +00:00
Charlie Marsh
7b14d17e39 Ignore star imports when importing symbols in fixes (#6743)
## Summary

Given:

```python
from sys import *

exit(0)
```

We can't add `exit` to `from sys import *`, so we should just ignore it.
Ideally, we'd just resolve `exit` in the first place (since it's
imported from `from sys import *`), but as long as we don't support
wildcard imports, this is more consistent.

Closes https://github.com/astral-sh/ruff/issues/6718.

## Test Plan

`cargo test`
2023-08-21 23:31:30 +00:00
69 changed files with 1461 additions and 583 deletions

View File

@@ -1,22 +1,29 @@
import math # not checked
def not_checked():
import math
import altair # unconventional
import matplotlib.pyplot # unconventional
import numpy # unconventional
import pandas # unconventional
import seaborn # unconventional
import tkinter # unconventional
import altair as altr # unconventional
import matplotlib.pyplot as plot # unconventional
import numpy as nmp # unconventional
import pandas as pdas # unconventional
import seaborn as sbrn # unconventional
import tkinter as tkr # unconventional
def unconventional():
import altair
import matplotlib.pyplot
import numpy
import pandas
import seaborn
import tkinter
import altair as alt # conventional
import matplotlib.pyplot as plt # conventional
import numpy as np # conventional
import pandas as pd # conventional
import seaborn as sns # conventional
import tkinter as tk # conventional
def unconventional_aliases():
import altair as altr
import matplotlib.pyplot as plot
import numpy as nmp
import pandas as pdas
import seaborn as sbrn
import tkinter as tkr
def conventional_aliases():
import altair as alt
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
import tkinter as tk

View File

@@ -28,3 +28,6 @@ mdtypes_template = {
'tag_full': [('mdtype', 'u4'), ('byte_count', 'u4')],
'tag_smalldata':[('byte_count_mdtype', 'u4'), ('data', 'S4')],
}
#: Okay
a = (1,

View File

@@ -16,8 +16,10 @@
"{:*^30s}".format("centered") # OK
"{:{s}}".format("hello", s="s") # OK (nested replacement value not checked)
"{:{s:y}}".format("hello", s="s") # [bad-format-character] (nested replacement format spec checked)
"{0:.{prec}g}".format(1.23, prec=15) # OK
"{0:.{foo}x{bar}y{foobar}g}".format(...) # OK (all nested replacements are consumed without considering in between chars)
"{0:.{foo}{bar}{foobar}y}".format(...) # [bad-format-character] (check value after replacements)
## f-strings

View File

@@ -1,10 +1,11 @@
class Person:
class Person: # [eq-without-hash]
def __init__(self):
self.name = "monty"
def __eq__(self, other):
return isinstance(other, Person) and other.name == self.name
# OK
class Language:
def __init__(self):
self.name = "python"
@@ -14,3 +15,9 @@ class Language:
def __hash__(self):
return hash(self.name)
class MyClass:
def __eq__(self, other):
return True
__hash__ = None

View File

@@ -0,0 +1,62 @@
import abc
class Person:
def developer_greeting(self, name): # [no-self-use]
print(f"Greetings {name}!")
def greeting_1(self): # [no-self-use]
print("Hello!")
def greeting_2(self): # [no-self-use]
print("Hi!")
# OK
def developer_greeting():
print("Greetings developer!")
# OK
class Person:
name = "Paris"
def __init__(self):
pass
def __cmp__(self, other):
print(24)
def __repr__(self):
return "Person"
def func(self):
...
def greeting_1(self):
print(f"Hello from {self.name} !")
@staticmethod
def greeting_2():
print("Hi!")
class Base(abc.ABC):
"""abstract class"""
@abstractmethod
def abstract_method(self):
"""abstract method could not be a function"""
raise NotImplementedError
class Sub(Base):
@override
def abstract_method(self):
print("concret method")
class Prop:
@property
def count(self):
return 24

View File

@@ -0,0 +1,3 @@
from sys import *
exit(0)

View File

@@ -13,6 +13,7 @@ use crate::registry::{AsRule, Rule};
pub(crate) mod codemods;
pub(crate) mod edits;
pub(crate) mod snippet;
pub(crate) mod source_map;
pub(crate) struct FixResult {

View File

@@ -0,0 +1,36 @@
use unicode_width::UnicodeWidthStr;
/// A snippet of source code for user-facing display, as in a diagnostic.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SourceCodeSnippet(String);
impl SourceCodeSnippet {
pub(crate) fn new(source_code: String) -> Self {
Self(source_code)
}
/// Return the full snippet for user-facing display, or `None` if the snippet should be
/// truncated.
pub(crate) fn full_display(&self) -> Option<&str> {
if Self::should_truncate(&self.0) {
None
} else {
Some(&self.0)
}
}
/// Return a truncated snippet for user-facing display.
pub(crate) fn truncated_display(&self) -> &str {
if Self::should_truncate(&self.0) {
"..."
} else {
&self.0
}
}
/// Returns `true` if the source code should be truncated when included in a user-facing
/// diagnostic.
fn should_truncate(source_code: &str) -> bool {
source_code.width() > 50 || source_code.contains(['\r', '\n'])
}
}

View File

@@ -30,6 +30,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
Rule::UnusedPrivateTypedDict,
Rule::UnusedStaticMethodArgument,
Rule::UnusedVariable,
Rule::NoSelfUse,
]) {
return;
}
@@ -168,7 +169,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
continue;
}
let Some(statement_id) = shadowed.source else {
let Some(node_id) = shadowed.source else {
continue;
};
@@ -176,7 +177,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
if shadowed.kind.is_function_definition() {
if checker
.semantic
.statement(statement_id)
.statement(node_id)
.as_function_def_stmt()
.is_some_and(|function| {
visibility::is_overload(
@@ -302,6 +303,12 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
pyflakes::rules::unused_import(checker, scope, &mut diagnostics);
}
}
if scope.kind.is_function() {
if checker.enabled(Rule::NoSelfUse) {
pylint::rules::no_self_use(checker, scope, &mut diagnostics);
}
}
}
checker.diagnostics.extend(diagnostics);
}

View File

@@ -267,7 +267,7 @@ where
{
fn visit_stmt(&mut self, stmt: &'b Stmt) {
// Step 0: Pre-processing
self.semantic.push_statement(stmt);
self.semantic.push_node(stmt);
// Track whether we've seen docstrings, non-imports, etc.
match stmt {
@@ -779,7 +779,7 @@ where
analyze::statement(stmt, self);
self.semantic.flags = flags_snapshot;
self.semantic.pop_statement();
self.semantic.pop_node();
}
fn visit_annotation(&mut self, expr: &'b Expr) {
@@ -815,7 +815,7 @@ where
return;
}
self.semantic.push_expression(expr);
self.semantic.push_node(expr);
// Store the flags prior to any further descent, so that we can restore them after visiting
// the node.
@@ -1235,7 +1235,7 @@ where
analyze::expression(expr, self);
self.semantic.flags = flags_snapshot;
self.semantic.pop_expression();
self.semantic.pop_node();
}
fn visit_except_handler(&mut self, except_handler: &'b ExceptHandler) {

View File

@@ -216,6 +216,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R1722") => (RuleGroup::Unspecified, rules::pylint::rules::SysExitAlias),
(Pylint, "R2004") => (RuleGroup::Unspecified, rules::pylint::rules::MagicValueComparison),
(Pylint, "R5501") => (RuleGroup::Unspecified, rules::pylint::rules::CollapsibleElseIf),
(Pylint, "R6301") => (RuleGroup::Nursery, rules::pylint::rules::NoSelfUse),
(Pylint, "W0120") => (RuleGroup::Unspecified, rules::pylint::rules::UselessElseOnLoop),
(Pylint, "W0127") => (RuleGroup::Unspecified, rules::pylint::rules::SelfAssigningVariable),
(Pylint, "W0129") => (RuleGroup::Unspecified, rules::pylint::rules::AssertOnStringLiteral),

View File

@@ -301,12 +301,14 @@ impl<'a> Importer<'a> {
}
if let Stmt::ImportFrom(ast::StmtImportFrom {
module: name,
names,
level,
..
range: _,
}) = stmt
{
if level.map_or(true, |level| level.to_u32() == 0)
&& name.as_ref().is_some_and(|name| name == module)
&& names.iter().all(|alias| alias.name.as_str() != "*")
{
import_from = Some(*stmt);
}

View File

@@ -1045,8 +1045,26 @@ pub(crate) fn fix_unnecessary_comprehension_any_all(
let mut tree = match_expression(module_text)?;
let call = match_call_mut(&mut tree)?;
let Expression::ListComp(list_comp) = &call.args[0].value else {
bail!("Expected Expression::ListComp");
let (whitespace_after, whitespace_before, elt, for_in, lpar, rpar) = match &call.args[0].value {
Expression::ListComp(list_comp) => (
&list_comp.lbracket.whitespace_after,
&list_comp.rbracket.whitespace_before,
&list_comp.elt,
&list_comp.for_in,
&list_comp.lpar,
&list_comp.rpar,
),
Expression::SetComp(set_comp) => (
&set_comp.lbrace.whitespace_after,
&set_comp.rbrace.whitespace_before,
&set_comp.elt,
&set_comp.for_in,
&set_comp.lpar,
&set_comp.rpar,
),
_ => {
bail!("Expected Expression::ListComp | Expression::SetComp");
}
};
let mut new_empty_lines = vec![];
@@ -1055,7 +1073,7 @@ pub(crate) fn fix_unnecessary_comprehension_any_all(
first_line,
empty_lines,
..
}) = &list_comp.lbracket.whitespace_after
}) = &whitespace_after
{
// If there's a comment on the line after the opening bracket, we need
// to preserve it. The way we do this is by adding a new empty line
@@ -1144,7 +1162,7 @@ pub(crate) fn fix_unnecessary_comprehension_any_all(
..
},
..
}) = &list_comp.rbracket.whitespace_before
}) = &whitespace_before
{
Some(format!("{}{}", whitespace.0, comment.0))
} else {
@@ -1152,10 +1170,10 @@ pub(crate) fn fix_unnecessary_comprehension_any_all(
};
call.args[0].value = Expression::GeneratorExp(Box::new(GeneratorExp {
elt: list_comp.elt.clone(),
for_in: list_comp.for_in.clone(),
lpar: list_comp.lpar.clone(),
rpar: list_comp.rpar.clone(),
elt: elt.clone(),
for_in: for_in.clone(),
lpar: lpar.clone(),
rpar: rpar.clone(),
}));
let whitespace_after_arg = match &call.args[0].comma {

View File

@@ -69,30 +69,31 @@ pub(crate) fn unnecessary_comprehension_any_all(
let Expr::Name(ast::ExprName { id, .. }) = func else {
return;
};
if (matches!(id.as_str(), "all" | "any")) && args.len() == 1 {
let (Expr::ListComp(ast::ExprListComp { elt, .. })
| Expr::SetComp(ast::ExprSetComp { elt, .. })) = &args[0]
else {
return;
};
if contains_await(elt) {
return;
}
if !checker.semantic().is_builtin(id) {
return;
}
let mut diagnostic = Diagnostic::new(UnnecessaryComprehensionAnyAll, args[0].range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_comprehension_any_all(
checker.locator(),
checker.stylist(),
expr,
)
});
}
checker.diagnostics.push(diagnostic);
if !matches!(id.as_str(), "all" | "any") {
return;
}
let [arg] = args else {
return;
};
let (Expr::ListComp(ast::ExprListComp { elt, .. })
| Expr::SetComp(ast::ExprSetComp { elt, .. })) = arg
else {
return;
};
if contains_await(elt) {
return;
}
if !checker.semantic().is_builtin(id) {
return;
}
let mut diagnostic = Diagnostic::new(UnnecessaryComprehensionAnyAll, arg.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_comprehension_any_all(checker.locator(), checker.stylist(), expr)
});
}
checker.diagnostics.push(diagnostic);
}
/// Return `true` if the [`Expr`] contains an `await` expression.

View File

@@ -77,7 +77,7 @@ C419.py:7:5: C419 [*] Unnecessary list comprehension.
9 9 | any({x.id for x in bar})
10 10 |
C419.py:9:5: C419 Unnecessary list comprehension.
C419.py:9:5: C419 [*] Unnecessary list comprehension.
|
7 | [x.id for x in bar], # second comment
8 | ) # third comment
@@ -88,6 +88,16 @@ C419.py:9:5: C419 Unnecessary list comprehension.
|
= help: Remove unnecessary list comprehension
Suggested fix
6 6 | all( # first comment
7 7 | [x.id for x in bar], # second comment
8 8 | ) # third comment
9 |-any({x.id for x in bar})
9 |+any(x.id for x in bar)
10 10 |
11 11 | # OK
12 12 | all(x.id for x in bar)
C419.py:24:5: C419 [*] Unnecessary list comprehension.
|
22 | # Special comment handling

View File

@@ -80,13 +80,15 @@ pub(crate) fn unconventional_import_alias(
binding.range(),
);
if checker.patch(diagnostic.kind.rule()) {
if checker.semantic().is_available(expected_alias) {
diagnostic.try_set_fix(|| {
let scope = &checker.semantic().scopes[binding.scope];
let (edit, rest) =
Renamer::rename(name, expected_alias, scope, checker.semantic())?;
Ok(Fix::suggested_edits(edit, rest))
});
if !import.is_submodule_import() {
if checker.semantic().is_available(expected_alias) {
diagnostic.try_set_fix(|| {
let scope = &checker.semantic().scopes[binding.scope];
let (edit, rest) =
Renamer::rename(name, expected_alias, scope, checker.semantic())?;
Ok(Fix::suggested_edits(edit, rest))
});
}
}
}
Some(diagnostic)

View File

@@ -1,132 +1,238 @@
---
source: crates/ruff/src/rules/flake8_import_conventions/mod.rs
---
defaults.py:3:8: ICN001 `altair` should be imported as `alt`
defaults.py:6:12: ICN001 [*] `altair` should be imported as `alt`
|
1 | import math # not checked
2 |
3 | import altair # unconventional
| ^^^^^^ ICN001
4 | import matplotlib.pyplot # unconventional
5 | import numpy # unconventional
5 | def unconventional():
6 | import altair
| ^^^^^^ ICN001
7 | import matplotlib.pyplot
8 | import numpy
|
= help: Alias `altair` to `alt`
defaults.py:4:8: ICN001 `matplotlib.pyplot` should be imported as `plt`
Suggested fix
3 3 |
4 4 |
5 5 | def unconventional():
6 |- import altair
6 |+ import altair as alt
7 7 | import matplotlib.pyplot
8 8 | import numpy
9 9 | import pandas
defaults.py:7:12: ICN001 `matplotlib.pyplot` should be imported as `plt`
|
3 | import altair # unconventional
4 | import matplotlib.pyplot # unconventional
| ^^^^^^^^^^^^^^^^^ ICN001
5 | import numpy # unconventional
6 | import pandas # unconventional
5 | def unconventional():
6 | import altair
7 | import matplotlib.pyplot
| ^^^^^^^^^^^^^^^^^ ICN001
8 | import numpy
9 | import pandas
|
= help: Alias `matplotlib.pyplot` to `plt`
defaults.py:5:8: ICN001 `numpy` should be imported as `np`
|
3 | import altair # unconventional
4 | import matplotlib.pyplot # unconventional
5 | import numpy # unconventional
| ^^^^^ ICN001
6 | import pandas # unconventional
7 | import seaborn # unconventional
|
= help: Alias `numpy` to `np`
defaults.py:6:8: ICN001 `pandas` should be imported as `pd`
|
4 | import matplotlib.pyplot # unconventional
5 | import numpy # unconventional
6 | import pandas # unconventional
| ^^^^^^ ICN001
7 | import seaborn # unconventional
8 | import tkinter # unconventional
|
= help: Alias `pandas` to `pd`
defaults.py:7:8: ICN001 `seaborn` should be imported as `sns`
|
5 | import numpy # unconventional
6 | import pandas # unconventional
7 | import seaborn # unconventional
| ^^^^^^^ ICN001
8 | import tkinter # unconventional
|
= help: Alias `seaborn` to `sns`
defaults.py:8:8: ICN001 `tkinter` should be imported as `tk`
defaults.py:8:12: ICN001 [*] `numpy` should be imported as `np`
|
6 | import pandas # unconventional
7 | import seaborn # unconventional
8 | import tkinter # unconventional
| ^^^^^^^ ICN001
9 |
10 | import altair as altr # unconventional
|
= help: Alias `tkinter` to `tk`
defaults.py:10:18: ICN001 `altair` should be imported as `alt`
|
8 | import tkinter # unconventional
9 |
10 | import altair as altr # unconventional
| ^^^^ ICN001
11 | import matplotlib.pyplot as plot # unconventional
12 | import numpy as nmp # unconventional
|
= help: Alias `altair` to `alt`
defaults.py:11:29: ICN001 `matplotlib.pyplot` should be imported as `plt`
|
10 | import altair as altr # unconventional
11 | import matplotlib.pyplot as plot # unconventional
| ^^^^ ICN001
12 | import numpy as nmp # unconventional
13 | import pandas as pdas # unconventional
|
= help: Alias `matplotlib.pyplot` to `plt`
defaults.py:12:17: ICN001 `numpy` should be imported as `np`
|
10 | import altair as altr # unconventional
11 | import matplotlib.pyplot as plot # unconventional
12 | import numpy as nmp # unconventional
| ^^^ ICN001
13 | import pandas as pdas # unconventional
14 | import seaborn as sbrn # unconventional
6 | import altair
7 | import matplotlib.pyplot
8 | import numpy
| ^^^^^ ICN001
9 | import pandas
10 | import seaborn
|
= help: Alias `numpy` to `np`
defaults.py:13:18: ICN001 `pandas` should be imported as `pd`
Suggested fix
5 5 | def unconventional():
6 6 | import altair
7 7 | import matplotlib.pyplot
8 |- import numpy
8 |+ import numpy as np
9 9 | import pandas
10 10 | import seaborn
11 11 | import tkinter
defaults.py:9:12: ICN001 [*] `pandas` should be imported as `pd`
|
11 | import matplotlib.pyplot as plot # unconventional
12 | import numpy as nmp # unconventional
13 | import pandas as pdas # unconventional
| ^^^^ ICN001
14 | import seaborn as sbrn # unconventional
15 | import tkinter as tkr # unconventional
7 | import matplotlib.pyplot
8 | import numpy
9 | import pandas
| ^^^^^^ ICN001
10 | import seaborn
11 | import tkinter
|
= help: Alias `pandas` to `pd`
defaults.py:14:19: ICN001 `seaborn` should be imported as `sns`
Suggested fix
6 6 | import altair
7 7 | import matplotlib.pyplot
8 8 | import numpy
9 |- import pandas
9 |+ import pandas as pd
10 10 | import seaborn
11 11 | import tkinter
12 12 |
defaults.py:10:12: ICN001 [*] `seaborn` should be imported as `sns`
|
12 | import numpy as nmp # unconventional
13 | import pandas as pdas # unconventional
14 | import seaborn as sbrn # unconventional
| ^^^^ ICN001
15 | import tkinter as tkr # unconventional
8 | import numpy
9 | import pandas
10 | import seaborn
| ^^^^^^^ ICN001
11 | import tkinter
|
= help: Alias `seaborn` to `sns`
defaults.py:15:19: ICN001 `tkinter` should be imported as `tk`
Suggested fix
7 7 | import matplotlib.pyplot
8 8 | import numpy
9 9 | import pandas
10 |- import seaborn
10 |+ import seaborn as sns
11 11 | import tkinter
12 12 |
13 13 |
defaults.py:11:12: ICN001 [*] `tkinter` should be imported as `tk`
|
13 | import pandas as pdas # unconventional
14 | import seaborn as sbrn # unconventional
15 | import tkinter as tkr # unconventional
| ^^^ ICN001
16 |
17 | import altair as alt # conventional
9 | import pandas
10 | import seaborn
11 | import tkinter
| ^^^^^^^ ICN001
|
= help: Alias `tkinter` to `tk`
Suggested fix
8 8 | import numpy
9 9 | import pandas
10 10 | import seaborn
11 |- import tkinter
11 |+ import tkinter as tk
12 12 |
13 13 |
14 14 | def unconventional_aliases():
defaults.py:15:22: ICN001 [*] `altair` should be imported as `alt`
|
14 | def unconventional_aliases():
15 | import altair as altr
| ^^^^ ICN001
16 | import matplotlib.pyplot as plot
17 | import numpy as nmp
|
= help: Alias `altair` to `alt`
Suggested fix
12 12 |
13 13 |
14 14 | def unconventional_aliases():
15 |- import altair as altr
15 |+ import altair as alt
16 16 | import matplotlib.pyplot as plot
17 17 | import numpy as nmp
18 18 | import pandas as pdas
defaults.py:16:33: ICN001 [*] `matplotlib.pyplot` should be imported as `plt`
|
14 | def unconventional_aliases():
15 | import altair as altr
16 | import matplotlib.pyplot as plot
| ^^^^ ICN001
17 | import numpy as nmp
18 | import pandas as pdas
|
= help: Alias `matplotlib.pyplot` to `plt`
Suggested fix
13 13 |
14 14 | def unconventional_aliases():
15 15 | import altair as altr
16 |- import matplotlib.pyplot as plot
16 |+ import matplotlib.pyplot as plt
17 17 | import numpy as nmp
18 18 | import pandas as pdas
19 19 | import seaborn as sbrn
defaults.py:17:21: ICN001 [*] `numpy` should be imported as `np`
|
15 | import altair as altr
16 | import matplotlib.pyplot as plot
17 | import numpy as nmp
| ^^^ ICN001
18 | import pandas as pdas
19 | import seaborn as sbrn
|
= help: Alias `numpy` to `np`
Suggested fix
14 14 | def unconventional_aliases():
15 15 | import altair as altr
16 16 | import matplotlib.pyplot as plot
17 |- import numpy as nmp
17 |+ import numpy as np
18 18 | import pandas as pdas
19 19 | import seaborn as sbrn
20 20 | import tkinter as tkr
defaults.py:18:22: ICN001 [*] `pandas` should be imported as `pd`
|
16 | import matplotlib.pyplot as plot
17 | import numpy as nmp
18 | import pandas as pdas
| ^^^^ ICN001
19 | import seaborn as sbrn
20 | import tkinter as tkr
|
= help: Alias `pandas` to `pd`
Suggested fix
15 15 | import altair as altr
16 16 | import matplotlib.pyplot as plot
17 17 | import numpy as nmp
18 |- import pandas as pdas
18 |+ import pandas as pd
19 19 | import seaborn as sbrn
20 20 | import tkinter as tkr
21 21 |
defaults.py:19:23: ICN001 [*] `seaborn` should be imported as `sns`
|
17 | import numpy as nmp
18 | import pandas as pdas
19 | import seaborn as sbrn
| ^^^^ ICN001
20 | import tkinter as tkr
|
= help: Alias `seaborn` to `sns`
Suggested fix
16 16 | import matplotlib.pyplot as plot
17 17 | import numpy as nmp
18 18 | import pandas as pdas
19 |- import seaborn as sbrn
19 |+ import seaborn as sns
20 20 | import tkinter as tkr
21 21 |
22 22 |
defaults.py:20:23: ICN001 [*] `tkinter` should be imported as `tk`
|
18 | import pandas as pdas
19 | import seaborn as sbrn
20 | import tkinter as tkr
| ^^^ ICN001
|
= help: Alias `tkinter` to `tk`
Suggested fix
17 17 | import numpy as nmp
18 18 | import pandas as pdas
19 19 | import seaborn as sbrn
20 |- import tkinter as tkr
20 |+ import tkinter as tk
21 21 |
22 22 |
23 23 | def conventional_aliases():

View File

@@ -1,14 +1,15 @@
use anyhow::Result;
use libcst_native::CompOp;
use ruff_python_ast::{self as ast, CmpOp, Expr, Ranged, UnaryOp};
use crate::autofix::codemods::CodegenStylist;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, CmpOp, Expr, Ranged, UnaryOp};
use ruff_python_codegen::Stylist;
use ruff_python_stdlib::str::{self};
use ruff_source_file::Locator;
use crate::autofix::codemods::CodegenStylist;
use crate::autofix::snippet::SourceCodeSnippet;
use crate::checkers::ast::Checker;
use crate::cst::matchers::{match_comparison, match_expression};
use crate::registry::AsRule;
@@ -45,7 +46,7 @@ use crate::registry::AsRule;
/// - [Python documentation: Assignment statements](https://docs.python.org/3/reference/simple_stmts.html#assignment-statements)
#[violation]
pub struct YodaConditions {
pub suggestion: Option<String>,
suggestion: Option<SourceCodeSnippet>,
}
impl Violation for YodaConditions {
@@ -54,7 +55,10 @@ impl Violation for YodaConditions {
#[derive_message_formats]
fn message(&self) -> String {
let YodaConditions { suggestion } = self;
if let Some(suggestion) = suggestion {
if let Some(suggestion) = suggestion
.as_ref()
.and_then(SourceCodeSnippet::full_display)
{
format!("Yoda conditions are discouraged, use `{suggestion}` instead")
} else {
format!("Yoda conditions are discouraged")
@@ -63,9 +67,13 @@ impl Violation for YodaConditions {
fn autofix_title(&self) -> Option<String> {
let YodaConditions { suggestion } = self;
suggestion
.as_ref()
.map(|suggestion| format!("Replace Yoda condition with `{suggestion}`"))
suggestion.as_ref().map(|suggestion| {
if let Some(suggestion) = suggestion.full_display() {
format!("Replace Yoda condition with `{suggestion}`")
} else {
format!("Replace Yoda condition")
}
})
}
}
@@ -178,7 +186,7 @@ pub(crate) fn yoda_conditions(
if let Ok(suggestion) = reverse_comparison(expr, checker.locator(), checker.stylist()) {
let mut diagnostic = Diagnostic::new(
YodaConditions {
suggestion: Some(suggestion.to_string()),
suggestion: Some(SourceCodeSnippet::new(suggestion.clone())),
},
expr.range(),
);

View File

@@ -290,7 +290,7 @@ SIM300.py:15:1: SIM300 [*] Yoda conditions are discouraged, use `(number - 100)
17 17 |
18 18 | # OK
SIM300.py:16:1: SIM300 [*] Yoda conditions are discouraged, use `(60 * 60) < SomeClass().settings.SOME_CONSTANT_VALUE` instead
SIM300.py:16:1: SIM300 [*] Yoda conditions are discouraged
|
14 | JediOrder.YODA == age # SIM300
15 | 0 < (number - 100) # SIM300
@@ -299,7 +299,7 @@ SIM300.py:16:1: SIM300 [*] Yoda conditions are discouraged, use `(60 * 60) < Som
17 |
18 | # OK
|
= help: Replace Yoda condition with `(60 * 60) < SomeClass().settings.SOME_CONSTANT_VALUE`
= help: Replace Yoda condition
Fix
13 13 | YODA >= age # SIM300

View File

@@ -6,7 +6,7 @@ use rustc_hash::FxHashMap;
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::Ranged;
use ruff_python_semantic::{AnyImport, Imported, ResolvedReferenceId, Scope, StatementId};
use ruff_python_semantic::{AnyImport, Imported, NodeId, ResolvedReferenceId, Scope};
use ruff_text_size::TextRange;
use crate::autofix;
@@ -72,8 +72,8 @@ pub(crate) fn runtime_import_in_type_checking_block(
diagnostics: &mut Vec<Diagnostic>,
) {
// Collect all runtime imports by statement.
let mut errors_by_statement: FxHashMap<StatementId, Vec<ImportBinding>> = FxHashMap::default();
let mut ignores_by_statement: FxHashMap<StatementId, Vec<ImportBinding>> = FxHashMap::default();
let mut errors_by_statement: FxHashMap<NodeId, Vec<ImportBinding>> = FxHashMap::default();
let mut ignores_by_statement: FxHashMap<NodeId, Vec<ImportBinding>> = FxHashMap::default();
for binding_id in scope.binding_ids() {
let binding = checker.semantic().binding(binding_id);
@@ -95,7 +95,7 @@ pub(crate) fn runtime_import_in_type_checking_block(
.is_runtime()
})
{
let Some(statement_id) = binding.source else {
let Some(node_id) = binding.source else {
continue;
};
@@ -115,23 +115,20 @@ pub(crate) fn runtime_import_in_type_checking_block(
})
{
ignores_by_statement
.entry(statement_id)
.entry(node_id)
.or_default()
.push(import);
} else {
errors_by_statement
.entry(statement_id)
.or_default()
.push(import);
errors_by_statement.entry(node_id).or_default().push(import);
}
}
}
// Generate a diagnostic for every import, but share a fix across all imports within the same
// statement (excluding those that are ignored).
for (statement_id, imports) in errors_by_statement {
for (node_id, imports) in errors_by_statement {
let fix = if checker.patch(Rule::RuntimeImportInTypeCheckingBlock) {
fix_imports(checker, statement_id, &imports).ok()
fix_imports(checker, node_id, &imports).ok()
} else {
None
};
@@ -200,13 +197,9 @@ impl Ranged for ImportBinding<'_> {
}
/// Generate a [`Fix`] to remove runtime imports from a type-checking block.
fn fix_imports(
checker: &Checker,
statement_id: StatementId,
imports: &[ImportBinding],
) -> Result<Fix> {
let statement = checker.semantic().statement(statement_id);
let parent = checker.semantic().parent_statement(statement_id);
fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) -> Result<Fix> {
let statement = checker.semantic().statement(node_id);
let parent = checker.semantic().parent_statement(node_id);
let member_names: Vec<Cow<'_, str>> = imports
.iter()

View File

@@ -6,7 +6,7 @@ use rustc_hash::FxHashMap;
use ruff_diagnostics::{AutofixKind, Diagnostic, DiagnosticKind, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::Ranged;
use ruff_python_semantic::{AnyImport, Binding, Imported, ResolvedReferenceId, Scope, StatementId};
use ruff_python_semantic::{AnyImport, Binding, Imported, NodeId, ResolvedReferenceId, Scope};
use ruff_text_size::TextRange;
use crate::autofix;
@@ -227,9 +227,9 @@ pub(crate) fn typing_only_runtime_import(
diagnostics: &mut Vec<Diagnostic>,
) {
// Collect all typing-only imports by statement and import type.
let mut errors_by_statement: FxHashMap<(StatementId, ImportType), Vec<ImportBinding>> =
let mut errors_by_statement: FxHashMap<(NodeId, ImportType), Vec<ImportBinding>> =
FxHashMap::default();
let mut ignores_by_statement: FxHashMap<(StatementId, ImportType), Vec<ImportBinding>> =
let mut ignores_by_statement: FxHashMap<(NodeId, ImportType), Vec<ImportBinding>> =
FxHashMap::default();
for binding_id in scope.binding_ids() {
@@ -302,7 +302,7 @@ pub(crate) fn typing_only_runtime_import(
continue;
}
let Some(statement_id) = binding.source else {
let Some(node_id) = binding.source else {
continue;
};
@@ -319,12 +319,12 @@ pub(crate) fn typing_only_runtime_import(
})
{
ignores_by_statement
.entry((statement_id, import_type))
.entry((node_id, import_type))
.or_default()
.push(import);
} else {
errors_by_statement
.entry((statement_id, import_type))
.entry((node_id, import_type))
.or_default()
.push(import);
}
@@ -333,9 +333,9 @@ pub(crate) fn typing_only_runtime_import(
// Generate a diagnostic for every import, but share a fix across all imports within the same
// statement (excluding those that are ignored).
for ((statement_id, import_type), imports) in errors_by_statement {
for ((node_id, import_type), imports) in errors_by_statement {
let fix = if checker.patch(rule_for(import_type)) {
fix_imports(checker, statement_id, &imports).ok()
fix_imports(checker, node_id, &imports).ok()
} else {
None
};
@@ -445,13 +445,9 @@ fn is_exempt(name: &str, exempt_modules: &[&str]) -> bool {
}
/// Generate a [`Fix`] to remove typing-only imports from a runtime context.
fn fix_imports(
checker: &Checker,
statement_id: StatementId,
imports: &[ImportBinding],
) -> Result<Fix> {
let statement = checker.semantic().statement(statement_id);
let parent = checker.semantic().parent_statement(statement_id);
fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) -> Result<Fix> {
let statement = checker.semantic().statement(node_id);
let parent = checker.semantic().parent_statement(node_id);
let member_names: Vec<Cow<'_, str>> = imports
.iter()

View File

@@ -1,5 +1,5 @@
//! Rules from [flake8-unused-arguments](https://pypi.org/project/flake8-unused-arguments/).
mod helpers;
pub(crate) mod helpers;
pub(crate) mod rules;
pub mod settings;

View File

@@ -1,10 +1,11 @@
use itertools::Itertools;
use ruff_python_ast::{self as ast, Arguments, Constant, Expr, Ranged};
use ruff_text_size::TextRange;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Arguments, Constant, Expr, Ranged};
use ruff_text_size::TextRange;
use crate::autofix::snippet::SourceCodeSnippet;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::rules::flynt::helpers;
@@ -29,19 +30,27 @@ use crate::rules::flynt::helpers;
/// - [Python documentation: f-strings](https://docs.python.org/3/reference/lexical_analysis.html#f-strings)
#[violation]
pub struct StaticJoinToFString {
expr: String,
expression: SourceCodeSnippet,
}
impl AlwaysAutofixableViolation for StaticJoinToFString {
#[derive_message_formats]
fn message(&self) -> String {
let StaticJoinToFString { expr } = self;
format!("Consider `{expr}` instead of string join")
let StaticJoinToFString { expression } = self;
if let Some(expression) = expression.full_display() {
format!("Consider `{expression}` instead of string join")
} else {
format!("Consider f-string instead of string join")
}
}
fn autofix_title(&self) -> String {
let StaticJoinToFString { expr } = self;
format!("Replace with `{expr}`")
let StaticJoinToFString { expression } = self;
if let Some(expression) = expression.full_display() {
format!("Replace with `{expression}`")
} else {
format!("Replace with f-string")
}
}
}
@@ -140,7 +149,7 @@ pub(crate) fn static_join_to_fstring(checker: &mut Checker, expr: &Expr, joiner:
let mut diagnostic = Diagnostic::new(
StaticJoinToFString {
expr: contents.clone(),
expression: SourceCodeSnippet::new(contents.clone()),
},
expr.range(),
);

View File

@@ -7,6 +7,28 @@ use ruff_source_file::Locator;
use crate::logging::DisplayParseErrorType;
/// ## What it does
/// This is not a regular diagnostic; instead, it's raised when a file cannot be read
/// from disk.
///
/// ## Why is this bad?
/// An `IOError` indicates an error in the development setup. For example, the user may
/// not have permissions to read a given file, or the filesystem may contain a broken
/// symlink.
///
/// ## Example
/// On Linux or macOS:
/// ```shell
/// $ echo 'print("hello world!")' > a.py
/// $ chmod 000 a.py
/// $ ruff a.py
/// a.py:1:1: E902 Permission denied (os error 13)
/// Found 1 error.
/// ```
///
/// ## References
/// - [UNIX Permissions introduction](https://mason.gmu.edu/~montecin/UNIXpermiss.htm)
/// - [Command Line Basics: Symbolic Links](https://www.digitalocean.com/community/tutorials/workflow-symbolic-links)
#[violation]
pub struct IOError {
pub message: String,

View File

@@ -81,10 +81,10 @@ pub(crate) fn missing_whitespace(
TokenKind::Comma | TokenKind::Semi | TokenKind::Colon => {
let after = line.text_after(token);
if !after
if after
.chars()
.next()
.is_some_and(|c| char::is_whitespace(c) || c == '\\')
.is_some_and(|c| !(char::is_whitespace(c) || c == '\\'))
{
if let Some(next_token) = iter.peek() {
match (kind, next_token.kind()) {

View File

@@ -98,5 +98,7 @@ E23.py:29:20: E231 [*] Missing whitespace after ':'
29 |- 'tag_smalldata':[('byte_count_mdtype', 'u4'), ('data', 'S4')],
29 |+ 'tag_smalldata': [('byte_count_mdtype', 'u4'), ('data', 'S4')],
30 30 | }
31 31 |
32 32 | #: Okay

View File

@@ -6,7 +6,7 @@ use rustc_hash::FxHashMap;
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::Ranged;
use ruff_python_semantic::{AnyImport, Exceptions, Imported, Scope, StatementId};
use ruff_python_semantic::{AnyImport, Exceptions, Imported, NodeId, Scope};
use ruff_text_size::TextRange;
use crate::autofix;
@@ -100,9 +100,8 @@ impl Violation for UnusedImport {
pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut Vec<Diagnostic>) {
// Collect all unused imports by statement.
let mut unused: FxHashMap<(StatementId, Exceptions), Vec<ImportBinding>> = FxHashMap::default();
let mut ignored: FxHashMap<(StatementId, Exceptions), Vec<ImportBinding>> =
FxHashMap::default();
let mut unused: FxHashMap<(NodeId, Exceptions), Vec<ImportBinding>> = FxHashMap::default();
let mut ignored: FxHashMap<(NodeId, Exceptions), Vec<ImportBinding>> = FxHashMap::default();
for binding_id in scope.binding_ids() {
let binding = checker.semantic().binding(binding_id);
@@ -119,7 +118,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
continue;
};
let Some(statement_id) = binding.source else {
let Some(node_id) = binding.source else {
continue;
};
@@ -135,12 +134,12 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
})
{
ignored
.entry((statement_id, binding.exceptions))
.entry((node_id, binding.exceptions))
.or_default()
.push(import);
} else {
unused
.entry((statement_id, binding.exceptions))
.entry((node_id, binding.exceptions))
.or_default()
.push(import);
}
@@ -151,13 +150,13 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
// Generate a diagnostic for every import, but share a fix across all imports within the same
// statement (excluding those that are ignored).
for ((statement_id, exceptions), imports) in unused {
for ((node_id, exceptions), imports) in unused {
let in_except_handler =
exceptions.intersects(Exceptions::MODULE_NOT_FOUND_ERROR | Exceptions::IMPORT_ERROR);
let multiple = imports.len() > 1;
let fix = if !in_init && !in_except_handler && checker.patch(Rule::UnusedImport) {
fix_imports(checker, statement_id, &imports).ok()
fix_imports(checker, node_id, &imports).ok()
} else {
None
};
@@ -234,13 +233,9 @@ impl Ranged for ImportBinding<'_> {
}
/// Generate a [`Fix`] to remove unused imports from a statement.
fn fix_imports(
checker: &Checker,
statement_id: StatementId,
imports: &[ImportBinding],
) -> Result<Fix> {
let statement = checker.semantic().statement(statement_id);
let parent = checker.semantic().parent_statement(statement_id);
fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) -> Result<Fix> {
let statement = checker.semantic().statement(node_id);
let parent = checker.semantic().parent_statement(node_id);
let member_names: Vec<Cow<'_, str>> = imports
.iter()

View File

@@ -203,12 +203,12 @@ where
/// Generate a [`Edit`] to remove an unused variable assignment to a [`Binding`].
fn remove_unused_variable(binding: &Binding, checker: &Checker) -> Option<Fix> {
let statement_id = binding.source?;
let statement = checker.semantic().statement(statement_id);
let parent = checker.semantic().parent_statement(statement_id);
let node_id = binding.source?;
let statement = checker.semantic().statement(node_id);
let parent = checker.semantic().parent_statement(node_id);
let isolation = checker
.semantic()
.parent_statement_id(statement_id)
.parent_statement_id(node_id)
.map(|node_id| IsolationLevel::Group(node_id.into()))
.unwrap_or_default();

View File

@@ -50,6 +50,7 @@ mod tests {
#[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_8.py"))]
#[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_9.py"))]
#[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_10.py"))]
#[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_11.py"))]
#[test_case(Rule::ContinueInFinally, Path::new("continue_in_finally.py"))]
#[test_case(Rule::GlobalStatement, Path::new("global_statement.py"))]
#[test_case(
@@ -131,6 +132,7 @@ mod tests {
Path::new("subprocess_run_without_check.py")
)]
#[test_case(Rule::BadDunderMethodName, Path::new("bad_dunder_method_name.py"))]
#[test_case(Rule::NoSelfUse, Path::new("no_self_use.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -36,7 +36,8 @@ pub struct BadStringFormatCharacter {
impl Violation for BadStringFormatCharacter {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unsupported format character '{}'", self.format_char)
let BadStringFormatCharacter { format_char } = self;
format!("Unsupported format character '{format_char}'")
}
}

View File

@@ -1,7 +1,7 @@
use ruff_python_ast::{self as ast, Ranged, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_none;
use ruff_python_ast::{self as ast, Expr, Ranged, Stmt};
use crate::checkers::ast::Checker;
@@ -65,12 +65,29 @@ fn has_eq_without_hash(body: &[Stmt]) -> bool {
let mut has_hash = false;
let mut has_eq = false;
for statement in body {
let Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) = statement else {
continue;
};
match name.as_str() {
"__hash__" => has_hash = true,
"__eq__" => has_eq = true,
match statement {
Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
let [Expr::Name(ast::ExprName { id, .. })] = targets.as_slice() else {
continue;
};
// Check if `__hash__` was explicitly set to `None`, as in:
// ```python
// class Class:
// def __eq__(self, other):
// return True
//
// __hash__ = None
// ```
if id == "__hash__" && is_const_none(value) {
has_hash = true;
}
}
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) => match name.as_str() {
"__hash__" => has_hash = true,
"__eq__" => has_eq = true,
_ => {}
},
_ => {}
}
}

View File

@@ -29,6 +29,7 @@ pub(crate) use magic_value_comparison::*;
pub(crate) use manual_import_from::*;
pub(crate) use named_expr_without_context::*;
pub(crate) use nested_min_max::*;
pub(crate) use no_self_use::*;
pub(crate) use nonlocal_without_binding::*;
pub(crate) use property_with_parameters::*;
pub(crate) use redefined_loop_name::*;
@@ -86,6 +87,7 @@ mod magic_value_comparison;
mod manual_import_from;
mod named_expr_without_context;
mod nested_min_max;
mod no_self_use;
mod nonlocal_without_binding;
mod property_with_parameters;
mod redefined_loop_name;

View File

@@ -0,0 +1,120 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::{from_qualified_name, CallPath};
use ruff_python_ast::{self as ast, ParameterWithDefault, Ranged};
use ruff_python_semantic::{
analyze::{function_type, visibility},
Scope, ScopeKind,
};
use crate::{checkers::ast::Checker, rules::flake8_unused_arguments::helpers};
/// ## What it does
/// Checks for the presence of unused `self` parameter in methods definitions.
///
/// ## Why is this bad?
/// Unused `self` parameters are usually a sign of a method that could be
/// replaced by a function or a static method.
///
/// ## Example
/// ```python
/// class Person:
/// def greeting(self):
/// print("Greetings friend!")
/// ```
///
/// Use instead:
/// ```python
/// class Person:
/// @staticmethod
/// def greeting():
/// print(f"Greetings friend!")
/// ```
#[violation]
pub struct NoSelfUse {
method_name: String,
}
impl Violation for NoSelfUse {
#[derive_message_formats]
fn message(&self) -> String {
let NoSelfUse { method_name } = self;
format!("Method `{method_name}` could be a function or static method")
}
}
/// PLR6301
pub(crate) fn no_self_use(checker: &Checker, scope: &Scope, diagnostics: &mut Vec<Diagnostic>) {
let Some(parent) = &checker.semantic().first_non_type_parent_scope(scope) else {
return;
};
let ScopeKind::Function(ast::StmtFunctionDef {
name,
parameters,
body,
decorator_list,
..
}) = scope.kind
else {
return;
};
if !matches!(
function_type::classify(
name,
decorator_list,
parent,
checker.semantic(),
&checker.settings.pep8_naming.classmethod_decorators,
&checker.settings.pep8_naming.staticmethod_decorators,
),
function_type::FunctionType::Method
) {
return;
}
let property_decorators = checker
.settings
.pydocstyle
.property_decorators
.iter()
.map(|decorator| from_qualified_name(decorator))
.collect::<Vec<CallPath>>();
if helpers::is_empty(body)
|| visibility::is_magic(name)
|| visibility::is_abstract(decorator_list, checker.semantic())
|| visibility::is_override(decorator_list, checker.semantic())
|| visibility::is_overload(decorator_list, checker.semantic())
|| visibility::is_property(decorator_list, &property_decorators, checker.semantic())
{
return;
}
// Identify the `self` parameter.
let Some(parameter) = parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
.next()
.map(ParameterWithDefault::as_parameter)
else {
return;
};
if parameter.name.as_str() == "self"
&& scope
.get("self")
.map(|binding_id| checker.semantic().binding(binding_id))
.is_some_and(|binding| binding.kind.is_argument() && !binding.is_used())
{
diagnostics.push(Diagnostic::new(
NoSelfUse {
method_name: name.to_string(),
},
parameter.range(),
));
}
}

View File

@@ -5,6 +5,7 @@ use itertools::{any, Itertools};
use ruff_python_ast::{BoolOp, CmpOp, Expr, ExprBoolOp, ExprCompare, Ranged};
use rustc_hash::FxHashMap;
use crate::autofix::snippet::SourceCodeSnippet;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::hashable::HashableExpr;
@@ -42,16 +43,22 @@ use crate::checkers::ast::Checker;
/// - [Python documentation: `set`](https://docs.python.org/3/library/stdtypes.html#set)
#[violation]
pub struct RepeatedEqualityComparisonTarget {
expr: String,
expression: SourceCodeSnippet,
}
impl Violation for RepeatedEqualityComparisonTarget {
#[derive_message_formats]
fn message(&self) -> String {
let RepeatedEqualityComparisonTarget { expr } = self;
format!(
"Consider merging multiple comparisons: `{expr}`. Use a `set` if the elements are hashable."
)
let RepeatedEqualityComparisonTarget { expression } = self;
if let Some(expression) = expression.full_display() {
format!(
"Consider merging multiple comparisons: `{expression}`. Use a `set` if the elements are hashable."
)
} else {
format!(
"Consider merging multiple comparisons. Use a `set` if the elements are hashable."
)
}
}
}
@@ -84,12 +91,12 @@ pub(crate) fn repeated_equality_comparison_target(checker: &mut Checker, bool_op
if count > 1 {
checker.diagnostics.push(Diagnostic::new(
RepeatedEqualityComparisonTarget {
expr: merged_membership_test(
expression: SourceCodeSnippet::new(merged_membership_test(
left.as_expr(),
bool_op.op,
&comparators,
checker.locator(),
),
)),
},
bool_op.range(),
));

View File

@@ -51,14 +51,24 @@ bad_string_format_character.py:15:1: PLE1300 Unsupported format character 'y'
17 | "{:*^30s}".format("centered") # OK
|
bad_string_format_character.py:20:1: PLE1300 Unsupported format character 'y'
bad_string_format_character.py:19:1: PLE1300 Unsupported format character 'y'
|
17 | "{:*^30s}".format("centered") # OK
18 | "{:{s}}".format("hello", s="s") # OK (nested replacement value not checked)
19 |
20 | "{:{s:y}}".format("hello", s="s") # [bad-format-character] (nested replacement format spec checked)
19 | "{:{s:y}}".format("hello", s="s") # [bad-format-character] (nested replacement format spec checked)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLE1300
21 |
22 | ## f-strings
20 | "{0:.{prec}g}".format(1.23, prec=15) # OK
21 | "{0:.{foo}x{bar}y{foobar}g}".format(...) # OK (all nested replacements are consumed without considering in between chars)
|
bad_string_format_character.py:22:1: PLE1300 Unsupported format character 'y'
|
20 | "{0:.{prec}g}".format(1.23, prec=15) # OK
21 | "{0:.{foo}x{bar}y{foobar}g}".format(...) # OK (all nested replacements are consumed without considering in between chars)
22 | "{0:.{foo}{bar}{foobar}y}".format(...) # [bad-format-character] (check value after replacements)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLE1300
23 |
24 | ## f-strings
|

View File

@@ -0,0 +1,20 @@
---
source: crates/ruff/src/rules/pylint/mod.rs
---
sys_exit_alias_11.py:3:1: PLR1722 [*] Use `sys.exit()` instead of `exit`
|
1 | from sys import *
2 |
3 | exit(0)
| ^^^^ PLR1722
|
= help: Replace `exit` with `sys.exit()`
Suggested fix
1 1 | from sys import *
2 |+import sys
2 3 |
3 |-exit(0)
4 |+sys.exit(0)

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff/src/rules/pylint/mod.rs
---
sys_exit_alias_5.py:3:1: PLR1722 Use `sys.exit()` instead of `exit`
sys_exit_alias_5.py:3:1: PLR1722 [*] Use `sys.exit()` instead of `exit`
|
1 | from sys import *
2 |
@@ -11,7 +11,17 @@ sys_exit_alias_5.py:3:1: PLR1722 Use `sys.exit()` instead of `exit`
|
= help: Replace `exit` with `sys.exit()`
sys_exit_alias_5.py:4:1: PLR1722 Use `sys.exit()` instead of `quit`
Suggested fix
1 1 | from sys import *
2 |+import sys
2 3 |
3 |-exit(0)
4 |+sys.exit(0)
4 5 | quit(0)
5 6 |
6 7 |
sys_exit_alias_5.py:4:1: PLR1722 [*] Use `sys.exit()` instead of `quit`
|
3 | exit(0)
4 | quit(0)
@@ -19,7 +29,18 @@ sys_exit_alias_5.py:4:1: PLR1722 Use `sys.exit()` instead of `quit`
|
= help: Replace `quit` with `sys.exit()`
sys_exit_alias_5.py:8:5: PLR1722 Use `sys.exit()` instead of `exit`
Suggested fix
1 1 | from sys import *
2 |+import sys
2 3 |
3 4 | exit(0)
4 |-quit(0)
5 |+sys.exit(0)
5 6 |
6 7 |
7 8 | def main():
sys_exit_alias_5.py:8:5: PLR1722 [*] Use `sys.exit()` instead of `exit`
|
7 | def main():
8 | exit(1)
@@ -28,7 +49,20 @@ sys_exit_alias_5.py:8:5: PLR1722 Use `sys.exit()` instead of `exit`
|
= help: Replace `exit` with `sys.exit()`
sys_exit_alias_5.py:9:5: PLR1722 Use `sys.exit()` instead of `quit`
Suggested fix
1 1 | from sys import *
2 |+import sys
2 3 |
3 4 | exit(0)
4 5 | quit(0)
5 6 |
6 7 |
7 8 | def main():
8 |- exit(1)
9 |+ sys.exit(1)
9 10 | quit(1)
sys_exit_alias_5.py:9:5: PLR1722 [*] Use `sys.exit()` instead of `quit`
|
7 | def main():
8 | exit(1)
@@ -37,4 +71,17 @@ sys_exit_alias_5.py:9:5: PLR1722 Use `sys.exit()` instead of `quit`
|
= help: Replace `quit` with `sys.exit()`
Suggested fix
1 1 | from sys import *
2 |+import sys
2 3 |
3 4 | exit(0)
4 5 | quit(0)
--------------------------------------------------------------------------------
6 7 |
7 8 | def main():
8 9 | exit(1)
9 |- quit(1)
10 |+ sys.exit(1)

View File

@@ -0,0 +1,39 @@
---
source: crates/ruff/src/rules/pylint/mod.rs
---
no_self_use.py:5:28: PLR6301 Method `developer_greeting` could be a function or static method
|
4 | class Person:
5 | def developer_greeting(self, name): # [no-self-use]
| ^^^^ PLR6301
6 | print(f"Greetings {name}!")
|
no_self_use.py:8:20: PLR6301 Method `greeting_1` could be a function or static method
|
6 | print(f"Greetings {name}!")
7 |
8 | def greeting_1(self): # [no-self-use]
| ^^^^ PLR6301
9 | print("Hello!")
|
no_self_use.py:11:20: PLR6301 Method `greeting_2` could be a function or static method
|
9 | print("Hello!")
10 |
11 | def greeting_2(self): # [no-self-use]
| ^^^^ PLR6301
12 | print("Hi!")
|
no_self_use.py:55:25: PLR6301 Method `abstract_method` could be a function or static method
|
53 | class Sub(Base):
54 | @override
55 | def abstract_method(self):
| ^^^^ PLR6301
56 | print("concret method")
|

View File

@@ -3,7 +3,7 @@ source: crates/ruff/src/rules/pylint/mod.rs
---
eq_without_hash.py:1:7: PLW1641 Object does not implement `__hash__` method
|
1 | class Person:
1 | class Person: # [eq-without-hash]
| ^^^^^^ PLW1641
2 | def __init__(self):
3 | self.name = "monty"

View File

@@ -1,9 +1,9 @@
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, ExprContext, Operator, Ranged};
use ruff_text_size::TextRange;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::autofix::snippet::SourceCodeSnippet;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -39,7 +39,7 @@ use crate::registry::AsRule;
/// - [Python documentation: Sequence Types — `list`, `tuple`, `range`](https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range)
#[violation]
pub struct CollectionLiteralConcatenation {
expr: String,
expression: SourceCodeSnippet,
}
impl Violation for CollectionLiteralConcatenation {
@@ -47,13 +47,21 @@ impl Violation for CollectionLiteralConcatenation {
#[derive_message_formats]
fn message(&self) -> String {
let CollectionLiteralConcatenation { expr } = self;
format!("Consider `{expr}` instead of concatenation")
let CollectionLiteralConcatenation { expression } = self;
if let Some(expression) = expression.full_display() {
format!("Consider `{expression}` instead of concatenation")
} else {
format!("Consider iterable unpacking instead of concatenation")
}
}
fn autofix_title(&self) -> Option<String> {
let CollectionLiteralConcatenation { expr } = self;
Some(format!("Replace with `{expr}`"))
let CollectionLiteralConcatenation { expression } = self;
if let Some(expression) = expression.full_display() {
Some(format!("Replace with `{expression}`"))
} else {
Some(format!("Replace with iterable unpacking"))
}
}
}
@@ -186,7 +194,7 @@ pub(crate) fn collection_literal_concatenation(checker: &mut Checker, expr: &Exp
};
let mut diagnostic = Diagnostic::new(
CollectionLiteralConcatenation {
expr: contents.clone(),
expression: SourceCodeSnippet::new(contents.clone()),
},
expr.range(),
);

View File

@@ -90,7 +90,8 @@ impl Violation for ImplicitOptional {
}
fn autofix_title(&self) -> Option<String> {
Some(format!("Convert to `{}`", self.conversion_type))
let ImplicitOptional { conversion_type } = self;
Some(format!("Convert to `{conversion_type}`"))
}
}

View File

@@ -40,6 +40,7 @@ impl Violation for InvalidPyprojectToml {
#[derive_message_formats]
fn message(&self) -> String {
format!("Failed to parse pyproject.toml: {}", self.message)
let InvalidPyprojectToml { message } = self;
format!("Failed to parse pyproject.toml: {message}")
}
}

View File

@@ -1,7 +1,6 @@
use std::borrow::Cow;
use num_traits::Zero;
use unicode_width::UnicodeWidthStr;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
@@ -9,6 +8,7 @@ use ruff_python_ast::{self as ast, Arguments, Comprehension, Constant, Expr, Ran
use ruff_python_semantic::SemanticModel;
use ruff_text_size::{TextRange, TextSize};
use crate::autofix::snippet::SourceCodeSnippet;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -47,35 +47,24 @@ use crate::registry::AsRule;
/// - [Iterators and Iterables in Python: Run Efficient Iterations](https://realpython.com/python-iterators-iterables/#when-to-use-an-iterator-in-python)
#[violation]
pub(crate) struct UnnecessaryIterableAllocationForFirstElement {
iterable: String,
iterable: SourceCodeSnippet,
}
impl AlwaysAutofixableViolation for UnnecessaryIterableAllocationForFirstElement {
#[derive_message_formats]
fn message(&self) -> String {
let UnnecessaryIterableAllocationForFirstElement { iterable } = self;
let iterable = Self::truncate(iterable);
let iterable = iterable.truncated_display();
format!("Prefer `next({iterable})` over single element slice")
}
fn autofix_title(&self) -> String {
let UnnecessaryIterableAllocationForFirstElement { iterable } = self;
let iterable = Self::truncate(iterable);
let iterable = iterable.truncated_display();
format!("Replace with `next({iterable})`")
}
}
impl UnnecessaryIterableAllocationForFirstElement {
/// If the iterable is too long, or spans multiple lines, truncate it.
fn truncate(iterable: &str) -> &str {
if iterable.width() > 40 || iterable.contains(['\r', '\n']) {
"..."
} else {
iterable
}
}
}
/// RUF015
pub(crate) fn unnecessary_iterable_allocation_for_first_element(
checker: &mut Checker,
@@ -104,7 +93,7 @@ pub(crate) fn unnecessary_iterable_allocation_for_first_element(
let mut diagnostic = Diagnostic::new(
UnnecessaryIterableAllocationForFirstElement {
iterable: iterable.to_string(),
iterable: SourceCodeSnippet::new(iterable.to_string()),
},
*range,
);

View File

@@ -187,7 +187,7 @@ RUF005.py:47:16: RUF005 [*] Consider `("we all feel", *Fun.words)` instead of co
49 49 | chain = ["a", "b", "c"] + eggs + list(("yes", "no", "pants") + zoob)
50 50 |
RUF005.py:49:9: RUF005 [*] Consider `["a", "b", "c", *eggs, *list(("yes", "no", "pants") + zoob)]` instead of concatenation
RUF005.py:49:9: RUF005 [*] Consider iterable unpacking instead of concatenation
|
47 | astonishment = ("we all feel",) + Fun.words
48 |
@@ -196,7 +196,7 @@ RUF005.py:49:9: RUF005 [*] Consider `["a", "b", "c", *eggs, *list(("yes", "no",
50 |
51 | baz = () + zoob
|
= help: Replace with `["a", "b", "c", *eggs, *list(("yes", "no", "pants") + zoob)]`
= help: Replace with iterable unpacking
Suggested fix
46 46 | excitement = ("we all think",) + Fun().yay()
@@ -294,14 +294,14 @@ RUF005.py:56:15: RUF005 [*] Consider `[sys.executable, "-m", "pylint", *args, pa
58 58 | b = a + [2, 3] + [4]
59 59 |
RUF005.py:57:21: RUF005 [*] Consider `(sys.executable, "-m", "pylint", *args, path, path2)` instead of concatenation
RUF005.py:57:21: RUF005 [*] Consider iterable unpacking instead of concatenation
|
56 | pylint_call = [sys.executable, "-m", "pylint"] + args + [path]
57 | pylint_call_tuple = (sys.executable, "-m", "pylint") + args + (path, path2)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF005
58 | b = a + [2, 3] + [4]
|
= help: Replace with `(sys.executable, "-m", "pylint", *args, path, path2)`
= help: Replace with iterable unpacking
Suggested fix
54 54 | ]

View File

@@ -1,10 +1,9 @@
use anyhow::Result;
use log::debug;
use log::error;
use ruff_text_size::{TextRange, TextSize};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use ruff_text_size::{TextRange, TextSize};
use crate::{Edit, Fix};
#[derive(Debug, PartialEq, Eq, Clone)]
@@ -61,10 +60,11 @@ impl Diagnostic {
/// Set the [`Fix`] used to fix the diagnostic, if the provided function returns `Ok`.
/// Otherwise, log the error.
#[inline]
#[allow(deprecated)]
pub fn try_set_fix(&mut self, func: impl FnOnce() -> Result<Fix>) {
match func() {
Ok(fix) => self.fix = Some(fix),
Err(err) => debug!("Failed to create fix for {}: {}", self.kind.name, err),
Err(err) => error!("Failed to create fix for {}: {}", self.kind.name, err),
}
}
@@ -76,7 +76,7 @@ impl Diagnostic {
pub fn try_set_fix_from_edit(&mut self, func: impl FnOnce() -> Result<Edit>) {
match func() {
Ok(edit) => self.fix = Some(Fix::unspecified(edit)),
Err(err) => debug!("Failed to create fix for {}: {}", self.kind.name, err),
Err(err) => error!("Failed to create fix for {}: {}", self.kind.name, err),
}
}

View File

@@ -65,3 +65,35 @@ def foo():
test, # comment 4
1
)
yield ("Cache key will cause errors if used with memcached: %r " "(longer than %s)" % (
key,
MEMCACHE_MAX_KEY_LENGTH,
)
)
yield "Cache key will cause errors if used with memcached: %r " "(longer than %s)" % (
key,
MEMCACHE_MAX_KEY_LENGTH,
)
yield ("Unnecessary")
yield (
"# * Make sure each ForeignKey and OneToOneField has `on_delete` set "
"to the desired behavior"
)
yield (
"# * Remove `managed = False` lines if you wish to allow "
"Django to create, modify, and delete the table"
)
yield (
"# Feel free to rename the models, but don't rename db_table values or "
"field names."
)
yield "# * Make sure each ForeignKey and OneToOneField has `on_delete` set " "to the desired behavior"
yield "# * Remove `managed = False` lines if you wish to allow " "Django to create, modify, and delete the table"
yield "# Feel free to rename the models, but don't rename db_table values or " "field names."

View File

@@ -42,3 +42,31 @@ for x in (1, 2, 3):
for x in 1, 2, 3,:
pass
# Don't keep parentheses around right target if it can made fit by breaking sub expressions
for column_name, (
referenced_column_name,
referenced_table_name,
) in relations.items():
pass
for column_name, [
referenced_column_name,
referenced_table_name,
] in relations.items():
pass
for column_name, [
referenced_column_name,
referenced_table_name,
], in relations.items():
pass
for (
# leading comment
column_name, [
referenced_column_name,
referenced_table_name,
]) in relations.items():
pass

View File

@@ -141,3 +141,20 @@ match pattern_comments:
no_comments
):
pass
match pattern_singleton:
case (
# leading 1
# leading 2
None # trailing
# trailing own 1
# trailing own 2
):
pass
case (
True # trailing
):
...
case False:
...

View File

@@ -146,3 +146,23 @@ except (
# comment
):
pass
try:
pass
finally:
pass
try:
pass
except ZeroDivisonError:
pass
else:
pass
finally:
pass

View File

@@ -20,7 +20,7 @@ impl FormatNodeRule<ExprAwait> for FormatExprAwait {
[
text("await"),
space(),
maybe_parenthesize_expression(value, item, Parenthesize::IfRequired)
maybe_parenthesize_expression(value, item, Parenthesize::IfBreaks)
]
)
}

View File

@@ -8,7 +8,7 @@ use ruff_text_size::TextRange;
use crate::builders::parenthesize_if_expands;
use crate::comments::SourceComment;
use crate::expression::parentheses::{
empty_parenthesized, parenthesized, NeedsParentheses, OptionalParentheses,
empty_parenthesized, optional_parentheses, parenthesized, NeedsParentheses, OptionalParentheses,
};
use crate::prelude::*;
@@ -138,7 +138,7 @@ impl FormatNodeRule<ExprTuple> for FormatExprTuple {
}
},
// If the tuple has parentheses, we generally want to keep them. The exception are for
// loops, see `TupleParentheses::StripInsideForLoop` doc comment.
// loops, see `TupleParentheses::NeverPreserve` doc comment.
//
// Unlike other expression parentheses, tuple parentheses are part of the range of the
// tuple itself.
@@ -159,7 +159,12 @@ impl FormatNodeRule<ExprTuple> for FormatExprTuple {
.finish()
}
TupleParentheses::Preserve => group(&ExprSequence::new(item)).fmt(f),
_ => parenthesize_if_expands(&ExprSequence::new(item)).fmt(f),
TupleParentheses::NeverPreserve => {
optional_parentheses(&ExprSequence::new(item)).fmt(f)
}
TupleParentheses::Default => {
parenthesize_if_expands(&ExprSequence::new(item)).fmt(f)
}
},
}
}

View File

@@ -90,7 +90,7 @@ impl Format<PyFormatContext<'_>> for AnyExpressionYield<'_> {
[
text(keyword),
space(),
maybe_parenthesize_expression(val, self, Parenthesize::IfRequired)
maybe_parenthesize_expression(val, self, Parenthesize::Optional)
]
)?;
} else {

View File

@@ -190,16 +190,13 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
return expression.format().with_options(Parentheses::Always).fmt(f);
}
let needs_parentheses = expression.needs_parentheses(*parent, f.context());
let needs_parentheses = match parenthesize {
Parenthesize::IfRequired => match needs_parentheses {
OptionalParentheses::Always => OptionalParentheses::Always,
_ if f.context().node_level().is_parenthesized() => OptionalParentheses::Never,
needs_parentheses => needs_parentheses,
},
Parenthesize::Optional
| Parenthesize::IfBreaks
| Parenthesize::IfBreaksOrIfRequired => needs_parentheses,
let needs_parentheses = match expression.needs_parentheses(*parent, f.context()) {
OptionalParentheses::Always => OptionalParentheses::Always,
// The reason to add parentheses is to avoid a syntax error when breaking an expression over multiple lines.
// Therefore, it is unnecessary to add an additional pair of parentheses if an outer expression
// is parenthesized.
_ if f.context().node_level().is_parenthesized() => OptionalParentheses::Never,
needs_parentheses => needs_parentheses,
};
match needs_parentheses {

View File

@@ -41,9 +41,8 @@ pub(crate) enum Parenthesize {
/// Parenthesizes the expression only if it doesn't fit on a line.
IfBreaks,
/// Only adds parentheses if absolutely necessary:
/// * The expression is not enclosed by another parenthesized expression and it expands over multiple lines
/// * The expression has leading or trailing comments. Adding parentheses is desired to prevent the comments from wandering.
/// Only adds parentheses if the expression has leading or trailing comments.
/// Adding parentheses is desired to prevent the comments from wandering.
IfRequired,
/// Parenthesizes the expression if the group doesn't fit on a line (e.g., even name expressions are parenthesized), or if

View File

@@ -1,13 +1,18 @@
use ruff_formatter::{write, Buffer, FormatResult};
use ruff_python_ast::PatternMatchSingleton;
use crate::prelude::*;
use ruff_python_ast::{Constant, PatternMatchSingleton};
use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter};
use crate::{FormatNodeRule, PyFormatter};
#[derive(Default)]
pub struct FormatPatternMatchSingleton;
impl FormatNodeRule<PatternMatchSingleton> for FormatPatternMatchSingleton {
fn fmt_fields(&self, item: &PatternMatchSingleton, f: &mut PyFormatter) -> FormatResult<()> {
write!(f, [not_yet_implemented_custom_text("None", item)])
match item.value {
Constant::None => text("None").fmt(f),
Constant::Bool(true) => text("True").fmt(f),
Constant::Bool(false) => text("False").fmt(f),
_ => unreachable!(),
}
}
}

View File

@@ -105,9 +105,9 @@ impl FormatNodeRule<StmtTry> for FormatStmtTry {
}
fn format_case<'a>(
try_statement: &StmtTry,
try_statement: &'a StmtTry,
kind: CaseKind,
previous_node: Option<&Stmt>,
previous_node: Option<&'a Stmt>,
dangling_comments: &'a [SourceComment],
f: &mut PyFormatter,
) -> FormatResult<(Option<&'a Stmt>, &'a [SourceComment])> {
@@ -141,9 +141,9 @@ fn format_case<'a>(
clause_body(body, trailing_case_comments),
]
)?;
(None, rest)
(Some(last), rest)
} else {
(None, dangling_comments)
(previous_node, dangling_comments)
})
}

View File

@@ -292,7 +292,7 @@ match x:
y = 0
# case black_test_patma_232
match x:
@@ -108,37 +108,37 @@
@@ -108,7 +108,7 @@
y = 0
# case black_test_patma_058
match x:
@@ -301,8 +301,7 @@ match x:
y = 0
# case black_test_patma_233
match x:
- case False:
+ case None:
@@ -116,29 +116,29 @@
y = 0
# case black_test_patma_078
match x:
@@ -460,7 +459,7 @@ match x:
y = 0
# case black_test_patma_233
match x:
case None:
case False:
y = 0
# case black_test_patma_078
match x:

View File

@@ -71,6 +71,38 @@ def foo():
test, # comment 4
1
)
yield ("Cache key will cause errors if used with memcached: %r " "(longer than %s)" % (
key,
MEMCACHE_MAX_KEY_LENGTH,
)
)
yield "Cache key will cause errors if used with memcached: %r " "(longer than %s)" % (
key,
MEMCACHE_MAX_KEY_LENGTH,
)
yield ("Unnecessary")
yield (
"# * Make sure each ForeignKey and OneToOneField has `on_delete` set "
"to the desired behavior"
)
yield (
"# * Remove `managed = False` lines if you wish to allow "
"Django to create, modify, and delete the table"
)
yield (
"# Feel free to rename the models, but don't rename db_table values or "
"field names."
)
yield "# * Make sure each ForeignKey and OneToOneField has `on_delete` set " "to the desired behavior"
yield "# * Remove `managed = False` lines if you wish to allow " "Django to create, modify, and delete the table"
yield "# Feel free to rename the models, but don't rename db_table values or " "field names."
```
## Output
@@ -110,7 +142,7 @@ def foo():
for e in l:
# comment
yield e # Too many parentheses
yield (e) # Too many parentheses
# comment
for ridiculouslylongelementnameeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee in l:
@@ -135,6 +167,51 @@ def foo():
)
)
)
yield (
"Cache key will cause errors if used with memcached: %r "
"(longer than %s)"
% (
key,
MEMCACHE_MAX_KEY_LENGTH,
)
)
yield "Cache key will cause errors if used with memcached: %r " "(longer than %s)" % (
key,
MEMCACHE_MAX_KEY_LENGTH,
)
yield ("Unnecessary")
yield (
"# * Make sure each ForeignKey and OneToOneField has `on_delete` set "
"to the desired behavior"
)
yield (
"# * Remove `managed = False` lines if you wish to allow "
"Django to create, modify, and delete the table"
)
yield (
"# Feel free to rename the models, but don't rename db_table values or "
"field names."
)
yield (
"# * Make sure each ForeignKey and OneToOneField has `on_delete` set "
"to the desired behavior"
)
yield (
"# * Remove `managed = False` lines if you wish to allow "
"Django to create, modify, and delete the table"
)
yield (
"# Feel free to rename the models, but don't rename db_table values or "
"field names."
)
```

View File

@@ -48,6 +48,34 @@ for x in (1, 2, 3):
for x in 1, 2, 3,:
pass
# Don't keep parentheses around right target if it can made fit by breaking sub expressions
for column_name, (
referenced_column_name,
referenced_table_name,
) in relations.items():
pass
for column_name, [
referenced_column_name,
referenced_table_name,
] in relations.items():
pass
for column_name, [
referenced_column_name,
referenced_table_name,
], in relations.items():
pass
for (
# leading comment
column_name, [
referenced_column_name,
referenced_table_name,
]) in relations.items():
pass
```
## Output
@@ -100,6 +128,38 @@ for x in (
3,
):
pass
# Don't keep parentheses around right target if it can made fit by breaking sub expressions
for column_name, (
referenced_column_name,
referenced_table_name,
) in relations.items():
pass
for column_name, [
referenced_column_name,
referenced_table_name,
] in relations.items():
pass
for (
column_name,
[
referenced_column_name,
referenced_table_name,
],
) in relations.items():
pass
for (
# leading comment
column_name,
[
referenced_column_name,
referenced_table_name,
],
) in relations.items():
pass
```

View File

@@ -147,6 +147,23 @@ match pattern_comments:
no_comments
):
pass
match pattern_singleton:
case (
# leading 1
# leading 2
None # trailing
# trailing own 1
# trailing own 2
):
pass
case (
True # trailing
):
...
case False:
...
```
## Output
@@ -285,6 +302,23 @@ match pattern_comments:
match pattern_comments:
case (x as NOT_YET_IMPLEMENTED_PatternMatchAs):
pass
match pattern_singleton:
case (
# leading 1
# leading 2
None # trailing
# trailing own 1
# trailing own 2
):
pass
case (
True # trailing
):
...
case False:
...
```

View File

@@ -152,6 +152,26 @@ except (
# comment
):
pass
try:
pass
finally:
pass
try:
pass
except ZeroDivisonError:
pass
else:
pass
finally:
pass
```
## Output
@@ -320,6 +340,26 @@ except (
# comment
):
pass
try:
pass
finally:
pass
try:
pass
except ZeroDivisonError:
pass
else:
pass
finally:
pass
```

View File

@@ -318,9 +318,9 @@ fn parse_precision(text: &str) -> Result<(Option<usize>, &str), FormatSpecError>
})
}
/// Parses a format part within a format spec
/// Parses a placeholder within a format spec
fn parse_nested_placeholder<'a>(
parts: &mut Vec<FormatPart>,
placeholders: &mut Vec<FormatPart>,
text: &'a str,
) -> Result<&'a str, FormatSpecError> {
match FormatString::parse_spec(text, AllowPlaceholderNesting::No) {
@@ -328,16 +328,38 @@ fn parse_nested_placeholder<'a>(
Err(FormatParseError::MissingStartBracket) => Ok(text),
Err(err) => Err(FormatSpecError::InvalidPlaceholder(err)),
Ok((format_part, text)) => {
parts.push(format_part);
placeholders.push(format_part);
Ok(text)
}
}
}
/// Parse and consume all placeholders in a format spec
/// This will also consume any intermediate characters such as `x` and `y` in `"x{foo}y{bar}z"`
fn consume_all_placeholders<'a>(
placeholders: &mut Vec<FormatPart>,
text: &'a str,
) -> Result<&'a str, FormatSpecError> {
let mut chars = text.chars();
let mut text = text;
let mut placeholder_count = placeholders.len();
while chars.clone().contains(&'{') {
text = parse_nested_placeholder(placeholders, chars.as_str())?;
chars = text.chars();
// If we did not parse a placeholder, consume a character
if placeholder_count == placeholders.len() {
chars.next();
} else {
placeholder_count = placeholders.len();
}
}
Ok(text)
}
impl FormatSpec {
pub fn parse(text: &str) -> Result<Self, FormatSpecError> {
let mut replacements = vec![];
// get_integer in CPython
let text = parse_nested_placeholder(&mut replacements, text)?;
let (conversion, text) = FormatConversion::parse(text);
let text = parse_nested_placeholder(&mut replacements, text)?;
@@ -354,7 +376,7 @@ impl FormatSpec {
let (grouping_option, text) = FormatGrouping::parse(text);
let text = parse_nested_placeholder(&mut replacements, text)?;
let (precision, text) = parse_precision(text)?;
let text = parse_nested_placeholder(&mut replacements, text)?;
let text = consume_all_placeholders(&mut replacements, text)?;
let (format_type, _text) = if text.is_empty() {
(None, text)

View File

@@ -11,8 +11,8 @@ use ruff_text_size::TextRange;
use crate::context::ExecutionContext;
use crate::model::SemanticModel;
use crate::nodes::NodeId;
use crate::reference::ResolvedReferenceId;
use crate::statements::StatementId;
use crate::ScopeId;
#[derive(Debug, Clone)]
@@ -24,7 +24,7 @@ pub struct Binding<'a> {
/// The context in which the [`Binding`] was created.
pub context: ExecutionContext,
/// The statement in which the [`Binding`] was defined.
pub source: Option<StatementId>,
pub source: Option<NodeId>,
/// The references to the [`Binding`].
pub references: Vec<ResolvedReferenceId>,
/// The exceptions that were handled when the [`Binding`] was defined.
@@ -185,7 +185,7 @@ impl<'a> Binding<'a> {
/// Returns the range of the binding's parent.
pub fn parent_range(&self, semantic: &SemanticModel) -> Option<TextRange> {
self.source
.map(|statement_id| semantic.statement(statement_id))
.map(|id| semantic.statement(id))
.and_then(|parent| {
if parent.is_import_from_stmt() {
Some(parent.range())

View File

@@ -1,56 +0,0 @@
use std::ops::Index;
use ruff_index::{newtype_index, IndexVec};
use ruff_python_ast::Expr;
/// Id uniquely identifying an expression in a program.
///
/// Using a `u32` is sufficient because Ruff only supports parsing documents with a size of max
/// `u32::max` and it is impossible to have more nodes than characters in the file. We use a
/// `NonZeroU32` to take advantage of memory layout optimizations.
#[newtype_index]
#[derive(Ord, PartialOrd)]
pub struct ExpressionId;
/// An [`Expr`] AST node in a program, along with a pointer to its parent expression (if any).
#[derive(Debug)]
struct ExpressionWithParent<'a> {
/// A pointer to the AST node.
node: &'a Expr,
/// The ID of the parent of this node, if any.
parent: Option<ExpressionId>,
}
/// The nodes of a program indexed by [`ExpressionId`]
#[derive(Debug, Default)]
pub struct Expressions<'a>(IndexVec<ExpressionId, ExpressionWithParent<'a>>);
impl<'a> Expressions<'a> {
/// Inserts a new expression into the node tree and returns its unique id.
pub(crate) fn insert(&mut self, node: &'a Expr, parent: Option<ExpressionId>) -> ExpressionId {
self.0.push(ExpressionWithParent { node, parent })
}
/// Return the [`ExpressionId`] of the parent node.
#[inline]
pub fn parent_id(&self, node_id: ExpressionId) -> Option<ExpressionId> {
self.0[node_id].parent
}
/// Returns an iterator over all [`ExpressionId`] ancestors, starting from the given [`ExpressionId`].
pub(crate) fn ancestor_ids(
&self,
node_id: ExpressionId,
) -> impl Iterator<Item = ExpressionId> + '_ {
std::iter::successors(Some(node_id), |&node_id| self.0[node_id].parent)
}
}
impl<'a> Index<ExpressionId> for Expressions<'a> {
type Output = &'a Expr;
#[inline]
fn index(&self, index: ExpressionId) -> &Self::Output {
&self.0[index].node
}
}

View File

@@ -3,22 +3,20 @@ mod binding;
mod branches;
mod context;
mod definition;
mod expressions;
mod globals;
mod model;
mod nodes;
mod reference;
mod scope;
mod star_import;
mod statements;
pub use binding::*;
pub use branches::*;
pub use context::*;
pub use definition::*;
pub use expressions::*;
pub use globals::*;
pub use model::*;
pub use nodes::*;
pub use reference::*;
pub use scope::*;
pub use star_import::*;
pub use statements::*;

View File

@@ -18,14 +18,13 @@ use crate::binding::{
use crate::branches::{BranchId, Branches};
use crate::context::ExecutionContext;
use crate::definition::{Definition, DefinitionId, Definitions, Member, Module};
use crate::expressions::{ExpressionId, Expressions};
use crate::globals::{Globals, GlobalsArena};
use crate::nodes::{NodeId, NodeRef, Nodes};
use crate::reference::{
ResolvedReference, ResolvedReferenceId, ResolvedReferences, UnresolvedReference,
UnresolvedReferenceFlags, UnresolvedReferences,
};
use crate::scope::{Scope, ScopeId, ScopeKind, Scopes};
use crate::statements::{StatementId, Statements};
use crate::Imported;
/// A semantic model for a Python module, to enable querying the module's semantic information.
@@ -33,17 +32,11 @@ pub struct SemanticModel<'a> {
typing_modules: &'a [String],
module_path: Option<&'a [String]>,
/// Stack of statements in the program.
statements: Statements<'a>,
/// Stack of all AST nodes in the program.
nodes: Nodes<'a>,
/// The ID of the current statement.
statement_id: Option<StatementId>,
/// Stack of expressions in the program.
expressions: Expressions<'a>,
/// The ID of the current expression.
expression_id: Option<ExpressionId>,
/// The ID of the current AST node.
node_id: Option<NodeId>,
/// Stack of all branches in the program.
branches: Branches,
@@ -141,12 +134,10 @@ impl<'a> SemanticModel<'a> {
Self {
typing_modules,
module_path: module.path(),
statements: Statements::default(),
statement_id: None,
expressions: Expressions::default(),
expression_id: None,
branch_id: None,
nodes: Nodes::default(),
node_id: None,
branches: Branches::default(),
branch_id: None,
scopes: Scopes::default(),
scope_id: ScopeId::global(),
definitions: Definitions::for_module(module),
@@ -236,7 +227,7 @@ impl<'a> SemanticModel<'a> {
flags,
references: Vec::new(),
scope: self.scope_id,
source: self.statement_id,
source: self.node_id,
context: self.execution_context(),
exceptions: self.exceptions(),
})
@@ -728,7 +719,7 @@ impl<'a> SemanticModel<'a> {
{
return Some(ImportedName {
name: format!("{name}.{member}"),
range: self.statements[source].range(),
range: self.nodes[source].range(),
context: binding.context,
});
}
@@ -752,7 +743,7 @@ impl<'a> SemanticModel<'a> {
{
return Some(ImportedName {
name: (*name).to_string(),
range: self.statements[source].range(),
range: self.nodes[source].range(),
context: binding.context,
});
}
@@ -773,7 +764,7 @@ impl<'a> SemanticModel<'a> {
{
return Some(ImportedName {
name: format!("{name}.{member}"),
range: self.statements[source].range(),
range: self.nodes[source].range(),
context: binding.context,
});
}
@@ -788,33 +779,15 @@ impl<'a> SemanticModel<'a> {
})
}
/// Push a [`Stmt`] onto the stack.
pub fn push_statement(&mut self, stmt: &'a Stmt) {
self.statement_id = Some(
self.statements
.insert(stmt, self.statement_id, self.branch_id),
);
/// Push an AST node [`NodeRef`] onto the stack.
pub fn push_node<T: Into<NodeRef<'a>>>(&mut self, node: T) {
self.node_id = Some(self.nodes.insert(node.into(), self.node_id, self.branch_id));
}
/// Pop the current [`Stmt`] off the stack.
pub fn pop_statement(&mut self) {
let node_id = self
.statement_id
.expect("Attempted to pop without statement");
self.statement_id = self.statements.parent_id(node_id);
}
/// Push a [`Expr`] onto the stack.
pub fn push_expression(&mut self, expr: &'a Expr) {
self.expression_id = Some(self.expressions.insert(expr, self.expression_id));
}
/// Pop the current [`Expr`] off the stack.
pub fn pop_expression(&mut self) {
let node_id = self
.expression_id
.expect("Attempted to pop without expression");
self.expression_id = self.expressions.parent_id(node_id);
/// Pop the current AST node [`NodeRef`] off the stack.
pub fn pop_node(&mut self) {
let node_id = self.node_id.expect("Attempted to pop without node");
self.node_id = self.nodes.parent_id(node_id);
}
/// Push a [`Scope`] with the given [`ScopeKind`] onto the stack.
@@ -860,34 +833,20 @@ impl<'a> SemanticModel<'a> {
self.branch_id = branch_id;
}
/// Returns an [`Iterator`] over the current statement hierarchy represented as [`StatementId`],
/// from the current [`StatementId`] through to any parents.
pub fn current_statement_ids(&self) -> impl Iterator<Item = StatementId> + '_ {
self.statement_id
.iter()
.flat_map(|id| self.statements.ancestor_ids(*id))
}
/// Returns an [`Iterator`] over the current statement hierarchy, from the current [`Stmt`]
/// through to any parents.
pub fn current_statements(&self) -> impl Iterator<Item = &'a Stmt> + '_ {
self.current_statement_ids().map(|id| self.statements[id])
}
/// Return the [`StatementId`] of the current [`Stmt`].
pub fn current_statement_id(&self) -> StatementId {
self.statement_id.expect("No current statement")
}
/// Return the [`StatementId`] of the current [`Stmt`] parent, if any.
pub fn current_statement_parent_id(&self) -> Option<StatementId> {
self.current_statement_ids().nth(1)
let id = self.node_id.expect("No current node");
self.nodes
.ancestor_ids(id)
.filter_map(move |id| self.nodes[id].as_statement())
}
/// Return the current [`Stmt`].
pub fn current_statement(&self) -> &'a Stmt {
let node_id = self.statement_id.expect("No current statement");
self.statements[node_id]
self.current_statements()
.next()
.expect("No current statement")
}
/// Return the parent [`Stmt`] of the current [`Stmt`], if any.
@@ -895,24 +854,18 @@ impl<'a> SemanticModel<'a> {
self.current_statements().nth(1)
}
/// Returns an [`Iterator`] over the current expression hierarchy represented as
/// [`ExpressionId`], from the current [`Expr`] through to any parents.
pub fn current_expression_ids(&self) -> impl Iterator<Item = ExpressionId> + '_ {
self.expression_id
.iter()
.flat_map(|id| self.expressions.ancestor_ids(*id))
}
/// Returns an [`Iterator`] over the current expression hierarchy, from the current [`Expr`]
/// through to any parents.
pub fn current_expressions(&self) -> impl Iterator<Item = &'a Expr> + '_ {
self.current_expression_ids().map(|id| self.expressions[id])
let id = self.node_id.expect("No current node");
self.nodes
.ancestor_ids(id)
.filter_map(move |id| self.nodes[id].as_expression())
}
/// Return the current [`Expr`].
pub fn current_expression(&self) -> Option<&'a Expr> {
let node_id = self.expression_id?;
Some(self.expressions[node_id])
self.current_expressions().next()
}
/// Return the parent [`Expr`] of the current [`Expr`], if any.
@@ -925,6 +878,27 @@ impl<'a> SemanticModel<'a> {
self.current_expressions().nth(2)
}
/// Returns an [`Iterator`] over the current statement hierarchy represented as [`NodeId`],
/// from the current [`NodeId`] through to any parents.
pub fn current_statement_ids(&self) -> impl Iterator<Item = NodeId> + '_ {
self.node_id
.iter()
.flat_map(|id| self.nodes.ancestor_ids(*id))
.filter(|id| self.nodes[*id].is_statement())
}
/// Return the [`NodeId`] of the current [`Stmt`].
pub fn current_statement_id(&self) -> NodeId {
self.current_statement_ids()
.next()
.expect("No current statement")
}
/// Return the [`NodeId`] of the current [`Stmt`] parent, if any.
pub fn current_statement_parent_id(&self) -> Option<NodeId> {
self.current_statement_ids().nth(1)
}
/// Returns a reference to the global [`Scope`].
pub fn global_scope(&self) -> &Scope<'a> {
self.scopes.global()
@@ -973,24 +947,36 @@ impl<'a> SemanticModel<'a> {
None
}
/// Return the [`Stmt]` corresponding to the given [`StatementId`].
/// Return the [`Stmt`] corresponding to the given [`NodeId`].
#[inline]
pub fn statement(&self, statement_id: StatementId) -> &'a Stmt {
self.statements[statement_id]
pub fn node(&self, node_id: NodeId) -> &NodeRef<'a> {
&self.nodes[node_id]
}
/// Return the [`Stmt`] corresponding to the given [`NodeId`].
#[inline]
pub fn statement(&self, node_id: NodeId) -> &'a Stmt {
self.nodes
.ancestor_ids(node_id)
.find_map(|id| self.nodes[id].as_statement())
.expect("No statement found")
}
/// Given a [`Stmt`], return its parent, if any.
#[inline]
pub fn parent_statement(&self, statement_id: StatementId) -> Option<&'a Stmt> {
self.statements
.parent_id(statement_id)
.map(|id| self.statements[id])
pub fn parent_statement(&self, node_id: NodeId) -> Option<&'a Stmt> {
self.nodes
.ancestor_ids(node_id)
.filter_map(|id| self.nodes[id].as_statement())
.nth(1)
}
/// Given a [`StatementId`], return the ID of its parent statement, if any.
#[inline]
pub fn parent_statement_id(&self, statement_id: StatementId) -> Option<StatementId> {
self.statements.parent_id(statement_id)
/// Given a [`NodeId`], return the [`NodeId`] of the parent statement, if any.
pub fn parent_statement_id(&self, node_id: NodeId) -> Option<NodeId> {
self.nodes
.ancestor_ids(node_id)
.filter(|id| self.nodes[*id].is_statement())
.nth(1)
}
/// Set the [`Globals`] for the current [`Scope`].
@@ -1007,7 +993,7 @@ impl<'a> SemanticModel<'a> {
range: *range,
references: Vec::new(),
scope: self.scope_id,
source: self.statement_id,
source: self.node_id,
context: self.execution_context(),
exceptions: self.exceptions(),
flags: BindingFlags::empty(),
@@ -1053,10 +1039,7 @@ impl<'a> SemanticModel<'a> {
/// Return `true` if the model is at the top level of the module (i.e., in the module scope,
/// and not nested within any statements).
pub fn at_top_level(&self) -> bool {
self.scope_id.is_global()
&& self
.statement_id
.map_or(true, |stmt_id| self.statements.parent_id(stmt_id).is_none())
self.scope_id.is_global() && self.current_statement_parent_id().is_none()
}
/// Return `true` if the model is in an async context.
@@ -1101,10 +1084,10 @@ impl<'a> SemanticModel<'a> {
/// `try` statement.
///
/// This implementation assumes that the statements are in the same scope.
pub fn different_branches(&self, left: StatementId, right: StatementId) -> bool {
pub fn different_branches(&self, left: NodeId, right: NodeId) -> bool {
// Collect the branch path for the left statement.
let left = self
.statements
.nodes
.branch_id(left)
.iter()
.flat_map(|branch_id| self.branches.ancestor_ids(*branch_id))
@@ -1112,7 +1095,7 @@ impl<'a> SemanticModel<'a> {
// Collect the branch path for the right statement.
let right = self
.statements
.nodes
.branch_id(right)
.iter()
.flat_map(|branch_id| self.branches.ancestor_ids(*branch_id))
@@ -1191,8 +1174,7 @@ impl<'a> SemanticModel<'a> {
pub fn snapshot(&self) -> Snapshot {
Snapshot {
scope_id: self.scope_id,
stmt_id: self.statement_id,
expr_id: self.expression_id,
node_id: self.node_id,
branch_id: self.branch_id,
definition_id: self.definition_id,
flags: self.flags,
@@ -1203,15 +1185,13 @@ impl<'a> SemanticModel<'a> {
pub fn restore(&mut self, snapshot: Snapshot) {
let Snapshot {
scope_id,
stmt_id,
expr_id,
node_id,
branch_id,
definition_id,
flags,
} = snapshot;
self.scope_id = scope_id;
self.statement_id = stmt_id;
self.expression_id = expr_id;
self.node_id = node_id;
self.branch_id = branch_id;
self.definition_id = definition_id;
self.flags = flags;
@@ -1625,8 +1605,7 @@ impl SemanticModelFlags {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Snapshot {
scope_id: ScopeId,
stmt_id: Option<StatementId>,
expr_id: Option<ExpressionId>,
node_id: Option<NodeId>,
branch_id: Option<BranchId>,
definition_id: DefinitionId,
flags: SemanticModelFlags,

View File

@@ -0,0 +1,136 @@
use std::ops::Index;
use ruff_index::{newtype_index, IndexVec};
use ruff_python_ast::{Expr, Ranged, Stmt};
use ruff_text_size::TextRange;
use crate::BranchId;
/// Id uniquely identifying an AST node in a program.
///
/// Using a `u32` is sufficient because Ruff only supports parsing documents with a size of max
/// `u32::max` and it is impossible to have more nodes than characters in the file. We use a
/// `NonZeroU32` to take advantage of memory layout optimizations.
#[newtype_index]
#[derive(Ord, PartialOrd)]
pub struct NodeId;
/// An AST node in a program, along with a pointer to its parent node (if any).
#[derive(Debug)]
struct NodeWithParent<'a> {
/// A pointer to the AST node.
node: NodeRef<'a>,
/// The ID of the parent of this node, if any.
parent: Option<NodeId>,
/// The branch ID of this node, if any.
branch: Option<BranchId>,
}
/// The nodes of a program indexed by [`NodeId`]
#[derive(Debug, Default)]
pub struct Nodes<'a> {
nodes: IndexVec<NodeId, NodeWithParent<'a>>,
}
impl<'a> Nodes<'a> {
/// Inserts a new AST node into the tree and returns its unique ID.
pub(crate) fn insert(
&mut self,
node: NodeRef<'a>,
parent: Option<NodeId>,
branch: Option<BranchId>,
) -> NodeId {
self.nodes.push(NodeWithParent {
node,
parent,
branch,
})
}
/// Return the [`NodeId`] of the parent node.
#[inline]
pub fn parent_id(&self, node_id: NodeId) -> Option<NodeId> {
self.nodes[node_id].parent
}
/// Return the [`BranchId`] of the branch node.
#[inline]
pub(crate) fn branch_id(&self, node_id: NodeId) -> Option<BranchId> {
self.nodes[node_id].branch
}
/// Returns an iterator over all [`NodeId`] ancestors, starting from the given [`NodeId`].
pub(crate) fn ancestor_ids(&self, node_id: NodeId) -> impl Iterator<Item = NodeId> + '_ {
std::iter::successors(Some(node_id), |&node_id| self.nodes[node_id].parent)
}
}
impl<'a> Index<NodeId> for Nodes<'a> {
type Output = NodeRef<'a>;
#[inline]
fn index(&self, index: NodeId) -> &Self::Output {
&self.nodes[index].node
}
}
/// A reference to an AST node. Like [`ruff_python_ast::node::AnyNodeRef`], but wraps the node
/// itself (like [`Stmt`]) rather than the narrowed type (like [`ruff_python_ast::StmtAssign`]).
///
/// TODO(charlie): Replace with [`ruff_python_ast::node::AnyNodeRef`]. This requires migrating
/// the rest of the codebase to use [`ruff_python_ast::node::AnyNodeRef`] and related abstractions,
/// like [`ruff_python_ast::ExpressionRef`] instead of [`Expr`].
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum NodeRef<'a> {
Stmt(&'a Stmt),
Expr(&'a Expr),
}
impl<'a> NodeRef<'a> {
/// Returns the [`Stmt`] if this is a statement, or `None` if the reference is to another
/// kind of AST node.
pub fn as_statement(&self) -> Option<&'a Stmt> {
match self {
NodeRef::Stmt(stmt) => Some(stmt),
NodeRef::Expr(_) => None,
}
}
/// Returns the [`Expr`] if this is a expression, or `None` if the reference is to another
/// kind of AST node.
pub fn as_expression(&self) -> Option<&'a Expr> {
match self {
NodeRef::Stmt(_) => None,
NodeRef::Expr(expr) => Some(expr),
}
}
pub fn is_statement(&self) -> bool {
self.as_statement().is_some()
}
pub fn is_expression(&self) -> bool {
self.as_expression().is_some()
}
}
impl Ranged for NodeRef<'_> {
fn range(&self) -> TextRange {
match self {
NodeRef::Stmt(stmt) => stmt.range(),
NodeRef::Expr(expr) => expr.range(),
}
}
}
impl<'a> From<&'a Expr> for NodeRef<'a> {
fn from(expr: &'a Expr) -> Self {
NodeRef::Expr(expr)
}
}
impl<'a> From<&'a Stmt> for NodeRef<'a> {
fn from(stmt: &'a Stmt) -> Self {
NodeRef::Stmt(stmt)
}
}

View File

@@ -1,72 +0,0 @@
use std::ops::Index;
use ruff_index::{newtype_index, IndexVec};
use ruff_python_ast::Stmt;
use crate::branches::BranchId;
/// Id uniquely identifying a statement AST node.
///
/// Using a `u32` is sufficient because Ruff only supports parsing documents with a size of max
/// `u32::max` and it is impossible to have more nodes than characters in the file. We use a
/// `NonZeroU32` to take advantage of memory layout optimizations.
#[newtype_index]
#[derive(Ord, PartialOrd)]
pub struct StatementId;
/// A [`Stmt`] AST node, along with a pointer to its parent statement (if any).
#[derive(Debug)]
struct StatementWithParent<'a> {
/// A pointer to the AST node.
statement: &'a Stmt,
/// The ID of the parent of this node, if any.
parent: Option<StatementId>,
/// The branch ID of this node, if any.
branch: Option<BranchId>,
}
/// The statements of a program indexed by [`StatementId`]
#[derive(Debug, Default)]
pub struct Statements<'a>(IndexVec<StatementId, StatementWithParent<'a>>);
impl<'a> Statements<'a> {
/// Inserts a new statement into the statement vector and returns its unique ID.
pub(crate) fn insert(
&mut self,
statement: &'a Stmt,
parent: Option<StatementId>,
branch: Option<BranchId>,
) -> StatementId {
self.0.push(StatementWithParent {
statement,
parent,
branch,
})
}
/// Return the [`StatementId`] of the parent statement.
#[inline]
pub(crate) fn parent_id(&self, statement_id: StatementId) -> Option<StatementId> {
self.0[statement_id].parent
}
/// Return the [`StatementId`] of the parent statement.
#[inline]
pub(crate) fn branch_id(&self, statement_id: StatementId) -> Option<BranchId> {
self.0[statement_id].branch
}
/// Returns an iterator over all [`StatementId`] ancestors, starting from the given [`StatementId`].
pub(crate) fn ancestor_ids(&self, id: StatementId) -> impl Iterator<Item = StatementId> + '_ {
std::iter::successors(Some(id), |&id| self.0[id].parent)
}
}
impl<'a> Index<StatementId> for Statements<'a> {
type Output = &'a Stmt;
#[inline]
fn index(&self, index: StatementId) -> &Self::Output {
&self.0[index].statement
}
}

1
ruff.schema.json generated
View File

@@ -2270,6 +2270,7 @@
"PLR55",
"PLR550",
"PLR5501",
"PLR6301",
"PLW",
"PLW0",
"PLW01",