Compare commits
19 Commits
charlie/fa
...
fix/format
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2b0b1ca26 | ||
|
|
6d88ac4ca2 | ||
|
|
268be0cf8e | ||
|
|
5df9ba716f | ||
|
|
0e3284244b | ||
|
|
d272874dfd | ||
|
|
ccac9681e1 | ||
|
|
b52cc84df6 | ||
|
|
fec6fc2fab | ||
|
|
ba4c27598a | ||
|
|
0f9ccfcad9 | ||
|
|
fa32cd9b6f | ||
|
|
0aad0c41f6 | ||
|
|
424b8d4ad2 | ||
|
|
abc5065fc7 | ||
|
|
37f4920e1e | ||
|
|
c0df99b965 | ||
|
|
7650c6ee45 | ||
|
|
7b14d17e39 |
@@ -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
|
||||
|
||||
@@ -28,3 +28,6 @@ mdtypes_template = {
|
||||
'tag_full': [('mdtype', 'u4'), ('byte_count', 'u4')],
|
||||
'tag_smalldata':[('byte_count_mdtype', 'u4'), ('data', 'S4')],
|
||||
}
|
||||
|
||||
#: Okay
|
||||
a = (1,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
62
crates/ruff/resources/test/fixtures/pylint/no_self_use.py
vendored
Normal file
62
crates/ruff/resources/test/fixtures/pylint/no_self_use.py
vendored
Normal 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
|
||||
3
crates/ruff/resources/test/fixtures/pylint/sys_exit_alias_11.py
vendored
Normal file
3
crates/ruff/resources/test/fixtures/pylint/sys_exit_alias_11.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
from sys import *
|
||||
|
||||
exit(0)
|
||||
@@ -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 {
|
||||
|
||||
36
crates/ruff/src/autofix/snippet.rs
Normal file
36
crates/ruff/src/autofix/snippet.rs
Normal 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'])
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}'")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
120
crates/ruff/src/rules/pylint/rules/no_self_use.rs
Normal file
120
crates/ruff/src/rules/pylint/rules/no_self_use.rs
Normal 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(¶meters.args)
|
||||
.chain(¶meters.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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
));
|
||||
|
||||
@@ -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
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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}`"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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 | ]
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
...
|
||||
|
||||
@@ -146,3 +146,23 @@ except (
|
||||
# comment
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
|
||||
finally:
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
|
||||
except ZeroDivisonError:
|
||||
pass
|
||||
|
||||
else:
|
||||
pass
|
||||
|
||||
finally:
|
||||
pass
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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,
|
||||
|
||||
136
crates/ruff_python_semantic/src/nodes.rs
Normal file
136
crates/ruff_python_semantic/src/nodes.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
1
ruff.schema.json
generated
@@ -2270,6 +2270,7 @@
|
||||
"PLR55",
|
||||
"PLR550",
|
||||
"PLR5501",
|
||||
"PLR6301",
|
||||
"PLW",
|
||||
"PLW0",
|
||||
"PLW01",
|
||||
|
||||
Reference in New Issue
Block a user