Compare commits
48 Commits
charlie/fa
...
charlie/pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89d9f1e124 | ||
|
|
205d234856 | ||
|
|
4889b84338 | ||
|
|
847432cacf | ||
|
|
9b6e008cf1 | ||
|
|
39c6665ff9 | ||
|
|
19a87c220a | ||
|
|
0688883404 | ||
|
|
3bb638875f | ||
|
|
26e63ab137 | ||
|
|
417a1d0717 | ||
|
|
34b2ae73b4 | ||
|
|
71c25e4f9d | ||
|
|
4bdd99f882 | ||
|
|
1e6d1182bf | ||
|
|
d08f697a04 | ||
|
|
4bc5eddf91 | ||
|
|
5f5de52aba | ||
|
|
1cb1bd731c | ||
|
|
db2e548f4f | ||
|
|
94f5f18ddb | ||
|
|
c34a342ab4 | ||
|
|
42ff833d00 | ||
|
|
e1f4438498 | ||
|
|
1acdec3e29 | ||
|
|
ca2bb20063 | ||
|
|
2e00983762 | ||
|
|
fb7caf43c8 | ||
|
|
e53bf25616 | ||
|
|
214eb707a6 | ||
|
|
5c1f7fd5dd | ||
|
|
cc278c24e2 | ||
|
|
558b56f8a8 | ||
|
|
749da6589a | ||
|
|
d2eace3377 | ||
|
|
ccac9681e1 | ||
|
|
b52cc84df6 | ||
|
|
fec6fc2fab | ||
|
|
ba4c27598a | ||
|
|
0f9ccfcad9 | ||
|
|
fa32cd9b6f | ||
|
|
0aad0c41f6 | ||
|
|
424b8d4ad2 | ||
|
|
abc5065fc7 | ||
|
|
37f4920e1e | ||
|
|
c0df99b965 | ||
|
|
7650c6ee45 | ||
|
|
7b14d17e39 |
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2399,6 +2399,7 @@ dependencies = [
|
||||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
"static_assertions",
|
||||
"test-case",
|
||||
"tiny-keccak",
|
||||
"unic-emoji-char",
|
||||
"unic-ucd-ident",
|
||||
|
||||
@@ -152,3 +152,9 @@ def f(a: Union[str, bytes, Any]) -> None: ...
|
||||
def f(a: Optional[Any]) -> None: ...
|
||||
def f(a: Annotated[Any, ...]) -> None: ...
|
||||
def f(a: "Union[str, bytes, Any]") -> None: ...
|
||||
|
||||
|
||||
class Foo:
|
||||
@decorator()
|
||||
def __init__(self: "Foo", foo: int):
|
||||
...
|
||||
|
||||
18
crates/ruff/resources/test/fixtures/flake8_bugbear/B006_extended.py
vendored
Normal file
18
crates/ruff/resources/test/fixtures/flake8_bugbear/B006_extended.py
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
import custom
|
||||
from custom import ImmutableTypeB
|
||||
|
||||
|
||||
def okay(foo: ImmutableTypeB = []):
|
||||
...
|
||||
|
||||
|
||||
def okay(foo: custom.ImmutableTypeA = []):
|
||||
...
|
||||
|
||||
|
||||
def okay(foo: custom.ImmutableTypeB = []):
|
||||
...
|
||||
|
||||
|
||||
def error_due_to_missing_import(foo: ImmutableTypeA = []):
|
||||
...
|
||||
@@ -15,11 +15,6 @@ filter(func, map(lambda v: v, nums))
|
||||
_ = f"{set(map(lambda x: x % 2 == 0, nums))}"
|
||||
_ = f"{dict(map(lambda v: (v, v**2), nums))}"
|
||||
|
||||
# Error, but unfixable.
|
||||
# For simple expressions, this could be: `(x if x else 1 for x in nums)`.
|
||||
# For more complex expressions, this would differ: `(x + 2 if x else 3 for x in nums)`.
|
||||
map(lambda x=1: x, nums)
|
||||
|
||||
# False negatives.
|
||||
map(lambda x=2, y=1: x + y, nums, nums)
|
||||
set(map(lambda x, y: x, nums, nums))
|
||||
@@ -37,3 +32,8 @@ map(lambda x: lambda: x, range(4))
|
||||
|
||||
# Error: the `x` is overridden by the inner lambda.
|
||||
map(lambda x: lambda x: x, range(4))
|
||||
|
||||
# Ok because of the default parameters, and variadic arguments.
|
||||
map(lambda x=1: x, nums)
|
||||
map(lambda *args: len(args), range(4))
|
||||
map(lambda **kwargs: len(kwargs), range(4))
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
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 networkx
|
||||
|
||||
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
|
||||
import networkx as nxy
|
||||
|
||||
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
|
||||
import networkx as nx
|
||||
|
||||
@@ -43,3 +43,12 @@ message
|
||||
assert something # OK
|
||||
assert something and something_else # Error
|
||||
assert something and something_else and something_third # Error
|
||||
|
||||
|
||||
def test_multiline():
|
||||
assert something and something_else; x = 1
|
||||
|
||||
x = 1; assert something and something_else
|
||||
|
||||
x = 1; \
|
||||
assert something and something_else
|
||||
|
||||
@@ -320,3 +320,9 @@ def end_of_statement():
|
||||
if True:
|
||||
return "" \
|
||||
; # type: ignore
|
||||
|
||||
|
||||
def end_of_file():
|
||||
if False:
|
||||
return 1
|
||||
x = 2 \
|
||||
|
||||
@@ -28,3 +28,6 @@ mdtypes_template = {
|
||||
'tag_full': [('mdtype', 'u4'), ('byte_count', 'u4')],
|
||||
'tag_smalldata':[('byte_count_mdtype', 'u4'), ('data', 'S4')],
|
||||
}
|
||||
|
||||
#: Okay
|
||||
a = (1,
|
||||
|
||||
13
crates/ruff/resources/test/fixtures/pydocstyle/D200.py
vendored
Normal file
13
crates/ruff/resources/test/fixtures/pydocstyle/D200.py
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
def func():
|
||||
"""\
|
||||
"""
|
||||
|
||||
|
||||
def func():
|
||||
"""\\
|
||||
"""
|
||||
|
||||
|
||||
def func():
|
||||
"""\ \
|
||||
"""
|
||||
@@ -99,3 +99,16 @@ import foo.bar as bop
|
||||
import foo.bar.baz
|
||||
|
||||
print(bop.baz.read_csv("test.csv"))
|
||||
|
||||
# Test: isolated deletions.
|
||||
if TYPE_CHECKING:
|
||||
import a1
|
||||
|
||||
import a2
|
||||
|
||||
|
||||
match *0, 1, *2:
|
||||
case 0,:
|
||||
import b1
|
||||
|
||||
import b2
|
||||
|
||||
@@ -2,6 +2,5 @@
|
||||
"{bar}{}".format(1, bar=2, spam=3) # F522
|
||||
"{bar:{spam}}".format(bar=2, spam=3) # No issues
|
||||
"{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
|
||||
# Not fixable
|
||||
(''
|
||||
.format(x=2))
|
||||
.format(x=2)) # F522
|
||||
|
||||
@@ -28,6 +28,6 @@
|
||||
"{1}{3}".format(1, 2, 3, 4) # F523, # F524
|
||||
"{1} {8}".format(0, 1) # F523, # F524
|
||||
|
||||
# Not fixable
|
||||
# Multiline
|
||||
(''
|
||||
.format(2))
|
||||
|
||||
@@ -154,3 +154,14 @@ def f() -> None:
|
||||
print("hello")
|
||||
except A as e :
|
||||
print("oh no!")
|
||||
|
||||
|
||||
def f():
|
||||
x = 1
|
||||
y = 2
|
||||
|
||||
|
||||
def f():
|
||||
x = 1
|
||||
|
||||
y = 2
|
||||
|
||||
@@ -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
|
||||
@@ -9,13 +9,22 @@ foo != "a" and foo != "b" and foo != "c"
|
||||
|
||||
foo == a or foo == "b" or foo == 3 # Mixed types.
|
||||
|
||||
# False negatives (the current implementation doesn't support Yoda conditions).
|
||||
"a" == foo or "b" == foo or "c" == foo
|
||||
|
||||
"a" != foo and "b" != foo and "c" != foo
|
||||
|
||||
"a" == foo or foo == "b" or "c" == foo
|
||||
|
||||
foo == bar or baz == foo or qux == foo
|
||||
|
||||
foo == "a" or "b" == foo or foo == "c"
|
||||
|
||||
foo != "a" and "b" != foo and foo != "c"
|
||||
|
||||
foo == "a" or foo == "b" or "c" == bar or "d" == bar # Multiple targets
|
||||
|
||||
foo.bar == "a" or foo.bar == "b" # Attributes.
|
||||
|
||||
# OK
|
||||
foo == "a" and foo == "b" and foo == "c" # `and` mixed with `==`.
|
||||
|
||||
@@ -36,3 +45,9 @@ foo != "a" # Single comparison.
|
||||
foo == "a" == "b" or foo == "c" # Multiple comparisons.
|
||||
|
||||
foo == bar == "b" or foo == "c" # Multiple comparisons.
|
||||
|
||||
foo == foo or foo == bar # Self-comparison.
|
||||
|
||||
foo[0] == "a" or foo[0] == "b" # Subscripts.
|
||||
|
||||
foo() == "a" or foo() == "b" # Calls.
|
||||
|
||||
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)
|
||||
7
crates/ruff/resources/test/fixtures/pyupgrade/UP009_10.py
vendored
Normal file
7
crates/ruff/resources/test/fixtures/pyupgrade/UP009_10.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
# coding=utf8""" # empty comment
|
||||
|
||||
"""
|
||||
Invalid coding declaration since it is nested inside a docstring
|
||||
The following empty comment tests for false positives as our implementation visits comments
|
||||
"""
|
||||
7
crates/ruff/resources/test/fixtures/pyupgrade/UP009_6.py
vendored
Normal file
7
crates/ruff/resources/test/fixtures/pyupgrade/UP009_6.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# coding=utf8
|
||||
print("Hello world")
|
||||
|
||||
"""
|
||||
Regression test for https://github.com/astral-sh/ruff/issues/6756
|
||||
The leading space must be removed to prevent invalid syntax.
|
||||
"""
|
||||
7
crates/ruff/resources/test/fixtures/pyupgrade/UP009_7.py
vendored
Normal file
7
crates/ruff/resources/test/fixtures/pyupgrade/UP009_7.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# coding=utf8
|
||||
print("Hello world")
|
||||
|
||||
"""
|
||||
Regression test for https://github.com/astral-sh/ruff/issues/6756
|
||||
The leading tab must be removed to prevent invalid syntax.
|
||||
"""
|
||||
6
crates/ruff/resources/test/fixtures/pyupgrade/UP009_8.py
vendored
Normal file
6
crates/ruff/resources/test/fixtures/pyupgrade/UP009_8.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
print("foo") # coding=utf8
|
||||
print("Hello world")
|
||||
|
||||
"""
|
||||
Invalid coding declaration due to a statement before the comment
|
||||
"""
|
||||
7
crates/ruff/resources/test/fixtures/pyupgrade/UP009_9.py
vendored
Normal file
7
crates/ruff/resources/test/fixtures/pyupgrade/UP009_9.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
x = 1 \
|
||||
# coding=utf8
|
||||
x = 2
|
||||
|
||||
"""
|
||||
Invalid coding declaration due to continuation on preceding line
|
||||
"""
|
||||
@@ -31,6 +31,7 @@ bool("foo")
|
||||
bool("")
|
||||
bool(b"")
|
||||
bool(1.0)
|
||||
int().denominator
|
||||
|
||||
# These become string or byte literals
|
||||
str()
|
||||
@@ -49,3 +50,6 @@ float(1.0)
|
||||
bool()
|
||||
bool(True)
|
||||
bool(False)
|
||||
|
||||
# These become a literal but retain parentheses
|
||||
int(1).denominator
|
||||
|
||||
@@ -11,7 +11,7 @@ use ruff_python_trivia::{
|
||||
has_leading_content, is_python_whitespace, PythonWhitespace, SimpleTokenKind, SimpleTokenizer,
|
||||
};
|
||||
use ruff_source_file::{Locator, NewlineWithTrailingNewline};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use ruff_text_size::{TextLen, TextSize};
|
||||
|
||||
use crate::autofix::codemods;
|
||||
|
||||
@@ -48,7 +48,7 @@ pub(crate) fn delete_stmt(
|
||||
} else if has_leading_content(stmt.start(), locator) {
|
||||
Edit::range_deletion(stmt.range())
|
||||
} else if let Some(start) = indexer.preceded_by_continuations(stmt.start(), locator) {
|
||||
Edit::range_deletion(TextRange::new(start, stmt.end()))
|
||||
Edit::deletion(start, stmt.end())
|
||||
} else {
|
||||
let range = locator.full_lines_range(stmt.range());
|
||||
Edit::range_deletion(range)
|
||||
@@ -133,10 +133,8 @@ pub(crate) fn remove_argument<T: Ranged>(
|
||||
// Case 3: argument or keyword is the only node, so delete the arguments (but preserve
|
||||
// parentheses, if needed).
|
||||
Ok(match parentheses {
|
||||
Parentheses::Remove => Edit::deletion(arguments.start(), arguments.end()),
|
||||
Parentheses::Preserve => {
|
||||
Edit::replacement("()".to_string(), arguments.start(), arguments.end())
|
||||
}
|
||||
Parentheses::Remove => Edit::range_deletion(arguments.range()),
|
||||
Parentheses::Preserve => Edit::range_replacement("()".to_string(), arguments.range()),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -228,25 +226,25 @@ fn trailing_semicolon(offset: TextSize, locator: &Locator) -> Option<TextSize> {
|
||||
fn next_stmt_break(semicolon: TextSize, locator: &Locator) -> TextSize {
|
||||
let start_location = semicolon + TextSize::from(1);
|
||||
|
||||
let contents = &locator.contents()[usize::from(start_location)..];
|
||||
for line in NewlineWithTrailingNewline::from(contents) {
|
||||
for line in
|
||||
NewlineWithTrailingNewline::with_offset(locator.after(start_location), start_location)
|
||||
{
|
||||
let trimmed = line.trim_whitespace();
|
||||
// Skip past any continuations.
|
||||
if trimmed.starts_with('\\') {
|
||||
continue;
|
||||
}
|
||||
|
||||
return start_location
|
||||
+ if trimmed.is_empty() {
|
||||
// If the line is empty, then despite the previous statement ending in a
|
||||
// semicolon, we know that it's not a multi-statement line.
|
||||
line.start()
|
||||
} else {
|
||||
// Otherwise, find the start of the next statement. (Or, anything that isn't
|
||||
// whitespace.)
|
||||
let relative_offset = line.find(|c: char| !is_python_whitespace(c)).unwrap();
|
||||
line.start() + TextSize::try_from(relative_offset).unwrap()
|
||||
};
|
||||
return if trimmed.is_empty() {
|
||||
// If the line is empty, then despite the previous statement ending in a
|
||||
// semicolon, we know that it's not a multi-statement line.
|
||||
line.start()
|
||||
} else {
|
||||
// Otherwise, find the start of the next statement. (Or, anything that isn't
|
||||
// whitespace.)
|
||||
let relative_offset = line.find(|c: char| !is_python_whitespace(c)).unwrap();
|
||||
line.start() + TextSize::try_from(relative_offset).unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
locator.line_end(start_location)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -381,34 +381,34 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
Ok(summary) => {
|
||||
if checker.enabled(Rule::StringDotFormatExtraNamedArguments) {
|
||||
pyflakes::rules::string_dot_format_extra_named_arguments(
|
||||
checker, &summary, keywords, location,
|
||||
checker, call, &summary, keywords,
|
||||
);
|
||||
}
|
||||
if checker
|
||||
.enabled(Rule::StringDotFormatExtraPositionalArguments)
|
||||
{
|
||||
pyflakes::rules::string_dot_format_extra_positional_arguments(
|
||||
checker, &summary, args, location,
|
||||
checker, call, &summary, args,
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::StringDotFormatMissingArguments) {
|
||||
pyflakes::rules::string_dot_format_missing_argument(
|
||||
checker, &summary, args, keywords, location,
|
||||
checker, call, &summary, args, keywords,
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::StringDotFormatMixingAutomatic) {
|
||||
pyflakes::rules::string_dot_format_mixing_automatic(
|
||||
checker, &summary, location,
|
||||
checker, call, &summary,
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::FormatLiterals) {
|
||||
pyupgrade::rules::format_literals(checker, &summary, call);
|
||||
pyupgrade::rules::format_literals(checker, call, &summary);
|
||||
}
|
||||
if checker.enabled(Rule::FString) {
|
||||
pyupgrade::rules::f_strings(
|
||||
checker,
|
||||
call,
|
||||
&summary,
|
||||
expr,
|
||||
value,
|
||||
checker.settings.line_length,
|
||||
);
|
||||
@@ -441,7 +441,11 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
pyupgrade::rules::redundant_open_modes(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::NativeLiterals) {
|
||||
pyupgrade::rules::native_literals(checker, expr, func, args, keywords);
|
||||
pyupgrade::rules::native_literals(
|
||||
checker,
|
||||
call,
|
||||
checker.semantic().current_expression_parent(),
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::OpenAlias) {
|
||||
pyupgrade::rules::open_alias(checker, expr, func);
|
||||
|
||||
@@ -52,7 +52,8 @@ use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind};
|
||||
use ruff_python_semantic::analyze::{typing, visibility};
|
||||
use ruff_python_semantic::{
|
||||
BindingFlags, BindingId, BindingKind, Exceptions, Export, FromImport, Globals, Import, Module,
|
||||
ModuleKind, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport, SubmoduleImport,
|
||||
ModuleKind, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport,
|
||||
SubmoduleImport,
|
||||
};
|
||||
use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS};
|
||||
use ruff_source_file::Locator;
|
||||
@@ -193,24 +194,6 @@ impl<'a> Checker<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`IsolationLevel`] to isolate fixes for the current statement.
|
||||
///
|
||||
/// The primary use-case for fix isolation is to ensure that we don't delete all statements
|
||||
/// in a given indented block, which would cause a syntax error. We therefore need to ensure
|
||||
/// that we delete at most one statement per indented block per fixer pass. Fix isolation should
|
||||
/// thus be applied whenever we delete a statement, but can otherwise be omitted.
|
||||
pub(crate) fn statement_isolation(&self) -> IsolationLevel {
|
||||
IsolationLevel::Group(self.semantic.current_statement_id().into())
|
||||
}
|
||||
|
||||
/// Returns the [`IsolationLevel`] to isolate fixes in the current statement's parent.
|
||||
pub(crate) fn parent_isolation(&self) -> IsolationLevel {
|
||||
self.semantic
|
||||
.current_statement_parent_id()
|
||||
.map(|node_id| IsolationLevel::Group(node_id.into()))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The [`Locator`] for the current file, which enables extraction of source code from byte
|
||||
/// offsets.
|
||||
pub(crate) const fn locator(&self) -> &'a Locator<'a> {
|
||||
@@ -259,6 +242,18 @@ impl<'a> Checker<'a> {
|
||||
pub(crate) const fn any_enabled(&self, rules: &[Rule]) -> bool {
|
||||
self.settings.rules.any_enabled(rules)
|
||||
}
|
||||
|
||||
/// Returns the [`IsolationLevel`] to isolate fixes for a given node.
|
||||
///
|
||||
/// The primary use-case for fix isolation is to ensure that we don't delete all statements
|
||||
/// in a given indented block, which would cause a syntax error. We therefore need to ensure
|
||||
/// that we delete at most one statement per indented block per fixer pass. Fix isolation should
|
||||
/// thus be applied whenever we delete a statement, but can otherwise be omitted.
|
||||
pub(crate) fn isolation(node_id: Option<NodeId>) -> IsolationLevel {
|
||||
node_id
|
||||
.map(|node_id| IsolationLevel::Group(node_id.into()))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Visitor<'b> for Checker<'a>
|
||||
@@ -267,7 +262,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 +774,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 +810,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 +1230,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) {
|
||||
@@ -1340,23 +1335,46 @@ where
|
||||
|
||||
fn visit_pattern(&mut self, pattern: &'b Pattern) {
|
||||
// Step 1: Binding
|
||||
if let Pattern::MatchAs(ast::PatternMatchAs {
|
||||
name: Some(name), ..
|
||||
})
|
||||
| Pattern::MatchStar(ast::PatternMatchStar {
|
||||
name: Some(name),
|
||||
range: _,
|
||||
})
|
||||
| Pattern::MatchMapping(ast::PatternMatchMapping {
|
||||
rest: Some(name), ..
|
||||
}) = pattern
|
||||
{
|
||||
self.add_binding(
|
||||
name,
|
||||
name.range(),
|
||||
BindingKind::Assignment,
|
||||
BindingFlags::empty(),
|
||||
);
|
||||
match &pattern {
|
||||
Pattern::MatchAs(ast::PatternMatchAs {
|
||||
name: Some(name),
|
||||
pattern: _,
|
||||
range: _,
|
||||
}) => {
|
||||
self.add_binding(
|
||||
name,
|
||||
name.range(),
|
||||
BindingKind::Assignment,
|
||||
BindingFlags::empty(),
|
||||
);
|
||||
}
|
||||
Pattern::MatchStar(ast::PatternMatchStar {
|
||||
name: Some(name),
|
||||
range: _,
|
||||
}) => {
|
||||
self.add_binding(
|
||||
name,
|
||||
name.range(),
|
||||
BindingKind::Assignment,
|
||||
BindingFlags::empty(),
|
||||
);
|
||||
}
|
||||
Pattern::MatchMapping(ast::PatternMatchMapping {
|
||||
rest: Some(rest), ..
|
||||
}) => {
|
||||
if let Pattern::MatchAs(ast::PatternMatchAs {
|
||||
name: Some(name), ..
|
||||
}) = rest.as_ref()
|
||||
{
|
||||
self.add_binding(
|
||||
name,
|
||||
name.range(),
|
||||
BindingKind::Assignment,
|
||||
BindingFlags::empty(),
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Step 2: Traversal
|
||||
|
||||
@@ -110,8 +110,8 @@ pub(crate) fn check_noqa(
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(UnusedNOQA { codes: None }, directive.range());
|
||||
if settings.rules.should_fix(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix_from_edit(delete_noqa(directive.range(), locator));
|
||||
diagnostic
|
||||
.set_fix(Fix::automatic(delete_noqa(directive.range(), locator)));
|
||||
}
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
@@ -175,12 +175,12 @@ pub(crate) fn check_noqa(
|
||||
);
|
||||
if settings.rules.should_fix(diagnostic.kind.rule()) {
|
||||
if valid_codes.is_empty() {
|
||||
#[allow(deprecated)]
|
||||
diagnostic
|
||||
.set_fix_from_edit(delete_noqa(directive.range(), locator));
|
||||
diagnostic.set_fix(Fix::automatic(delete_noqa(
|
||||
directive.range(),
|
||||
locator,
|
||||
)));
|
||||
} else {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
format!("# noqa: {}", valid_codes.join(", ")),
|
||||
directive.range(),
|
||||
)));
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use crate::autofix::codemods::CodegenStylist;
|
||||
use anyhow::{bail, Result};
|
||||
use libcst_native::{
|
||||
Arg, Attribute, Call, Comparison, CompoundStatement, Dict, Expression, FunctionDef,
|
||||
GeneratorExp, If, Import, ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda,
|
||||
ListComp, Module, Name, SmallStatement, Statement, Suite, Tuple, With,
|
||||
};
|
||||
use ruff_python_codegen::Stylist;
|
||||
|
||||
pub(crate) fn match_module(module_text: &str) -> Result<Module> {
|
||||
match libcst_native::parse_module(module_text, None) {
|
||||
@@ -12,13 +14,6 @@ pub(crate) fn match_module(module_text: &str) -> Result<Module> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn match_expression(expression_text: &str) -> Result<Expression> {
|
||||
match libcst_native::parse_expression(expression_text) {
|
||||
Ok(expression) => Ok(expression),
|
||||
Err(_) => bail!("Failed to extract expression from source"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn match_statement(statement_text: &str) -> Result<Statement> {
|
||||
match libcst_native::parse_statement(statement_text) {
|
||||
Ok(statement) => Ok(statement),
|
||||
@@ -205,3 +200,59 @@ pub(crate) fn match_if<'a, 'b>(statement: &'a mut Statement<'b>) -> Result<&'a m
|
||||
bail!("Expected Statement::Compound")
|
||||
}
|
||||
}
|
||||
|
||||
/// Given the source code for an expression, return the parsed [`Expression`].
|
||||
///
|
||||
/// If the expression is not guaranteed to be valid as a standalone expression (e.g., if it may
|
||||
/// span multiple lines and/or require parentheses), use [`transform_expression`] instead.
|
||||
pub(crate) fn match_expression(expression_text: &str) -> Result<Expression> {
|
||||
match libcst_native::parse_expression(expression_text) {
|
||||
Ok(expression) => Ok(expression),
|
||||
Err(_) => bail!("Failed to extract expression from source"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a transformation function over an expression.
|
||||
///
|
||||
/// Passing an expression to [`match_expression`] directly can lead to parse errors if the
|
||||
/// expression is not a valid standalone expression (e.g., it was parenthesized in the original
|
||||
/// source). This method instead wraps the expression in "fake" parentheses, runs the
|
||||
/// transformation, then removes the "fake" parentheses.
|
||||
pub(crate) fn transform_expression(
|
||||
source_code: &str,
|
||||
stylist: &Stylist,
|
||||
func: impl FnOnce(Expression) -> Result<Expression>,
|
||||
) -> Result<String> {
|
||||
// Wrap the expression in parentheses.
|
||||
let source_code = format!("({source_code})");
|
||||
let expression = match_expression(&source_code)?;
|
||||
|
||||
// Run the function on the expression.
|
||||
let expression = func(expression)?;
|
||||
|
||||
// Codegen the expression.
|
||||
let mut source_code = expression.codegen_stylist(stylist);
|
||||
|
||||
// Drop the outer parentheses.
|
||||
source_code.drain(0..1);
|
||||
source_code.drain(source_code.len() - 1..source_code.len());
|
||||
Ok(source_code)
|
||||
}
|
||||
|
||||
/// Like [`transform_expression`], but operates on the source code of the expression, rather than
|
||||
/// the parsed [`Expression`]. This _shouldn't_ exist, but does to accommodate lifetime issues.
|
||||
pub(crate) fn transform_expression_text(
|
||||
source_code: &str,
|
||||
func: impl FnOnce(String) -> Result<String>,
|
||||
) -> Result<String> {
|
||||
// Wrap the expression in parentheses.
|
||||
let source_code = format!("({source_code})");
|
||||
|
||||
// Run the function on the expression.
|
||||
let mut transformed = func(source_code)?;
|
||||
|
||||
// Drop the outer parentheses.
|
||||
transformed.drain(0..1);
|
||||
transformed.drain(transformed.len() - 1..transformed.len());
|
||||
Ok(transformed)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -562,7 +562,7 @@ fn add_noqa_inner(
|
||||
let mut prev_end = TextSize::default();
|
||||
|
||||
for (offset, (rules, directive)) in matches_by_line {
|
||||
output.push_str(&locator.contents()[TextRange::new(prev_end, offset)]);
|
||||
output.push_str(locator.slice(TextRange::new(prev_end, offset)));
|
||||
|
||||
let line = locator.full_line(offset);
|
||||
|
||||
@@ -619,7 +619,7 @@ fn add_noqa_inner(
|
||||
prev_end = offset + line.text_len();
|
||||
}
|
||||
|
||||
output.push_str(&locator.contents()[usize::from(prev_end)..]);
|
||||
output.push_str(locator.after(prev_end));
|
||||
|
||||
(count, output)
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
use anyhow::{bail, Result};
|
||||
use ruff_python_ast::{PySourceType, Ranged};
|
||||
use ruff_python_parser::{lexer, AsMode, Tok};
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
/// ANN204
|
||||
pub(crate) fn add_return_annotation<T: Ranged>(
|
||||
statement: &T,
|
||||
annotation: &str,
|
||||
source_type: PySourceType,
|
||||
locator: &Locator,
|
||||
) -> Result<Edit> {
|
||||
let contents = &locator.contents()[statement.range()];
|
||||
|
||||
// Find the colon (following the `def` keyword).
|
||||
let mut seen_lpar = false;
|
||||
let mut seen_rpar = false;
|
||||
let mut count = 0u32;
|
||||
for (tok, range) in
|
||||
lexer::lex_starts_at(contents, source_type.as_mode(), statement.start()).flatten()
|
||||
{
|
||||
if seen_lpar && seen_rpar {
|
||||
if matches!(tok, Tok::Colon) {
|
||||
return Ok(Edit::insertion(format!(" -> {annotation}"), range.start()));
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(tok, Tok::Lpar) {
|
||||
if count == 0 {
|
||||
seen_lpar = true;
|
||||
}
|
||||
count = count.saturating_add(1);
|
||||
}
|
||||
if matches!(tok, Tok::Rpar) {
|
||||
count = count.saturating_sub(1);
|
||||
if count == 0 {
|
||||
seen_rpar = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bail!("Unable to locate colon in function definition");
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Rules from [flake8-annotations](https://pypi.org/project/flake8-annotations/).
|
||||
mod fixes;
|
||||
pub(crate) mod helpers;
|
||||
pub(crate) mod rules;
|
||||
pub mod settings;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, Violation};
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::ReturnStatementVisitor;
|
||||
use ruff_python_ast::identifier::Identifier;
|
||||
@@ -11,7 +11,6 @@ use ruff_python_stdlib::typing::simple_magic_return_type;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
use crate::rules::flake8_annotations::fixes;
|
||||
use crate::rules::ruff::typing::type_hint_resolves_to_any;
|
||||
|
||||
/// ## What it does
|
||||
@@ -704,15 +703,10 @@ pub(crate) fn definition(
|
||||
function.identifier(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::add_return_annotation(
|
||||
function,
|
||||
"None",
|
||||
checker.source_type,
|
||||
checker.locator(),
|
||||
)
|
||||
.map(Fix::suggested)
|
||||
});
|
||||
diagnostic.set_fix(Fix::suggested(Edit::insertion(
|
||||
" -> None".to_string(),
|
||||
function.parameters.range().end(),
|
||||
)));
|
||||
}
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
@@ -727,15 +721,10 @@ pub(crate) fn definition(
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(return_type) = simple_magic_return_type(name) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::add_return_annotation(
|
||||
function,
|
||||
return_type,
|
||||
checker.source_type,
|
||||
checker.locator(),
|
||||
)
|
||||
.map(Fix::suggested)
|
||||
});
|
||||
diagnostic.set_fix(Fix::suggested(Edit::insertion(
|
||||
format!(" -> {return_type}"),
|
||||
function.parameters.range().end(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
diagnostics.push(diagnostic);
|
||||
|
||||
@@ -242,4 +242,22 @@ annotation_presence.py:154:10: ANN401 Dynamically typed expressions (typing.Any)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ ANN401
|
||||
|
|
||||
|
||||
annotation_presence.py:159:9: ANN204 [*] Missing return type annotation for special method `__init__`
|
||||
|
|
||||
157 | class Foo:
|
||||
158 | @decorator()
|
||||
159 | def __init__(self: "Foo", foo: int):
|
||||
| ^^^^^^^^ ANN204
|
||||
160 | ...
|
||||
|
|
||||
= help: Add `None` return type
|
||||
|
||||
ℹ Suggested fix
|
||||
156 156 |
|
||||
157 157 | class Foo:
|
||||
158 158 | @decorator()
|
||||
159 |- def __init__(self: "Foo", foo: int):
|
||||
159 |+ def __init__(self: "Foo", foo: int) -> None:
|
||||
160 160 | ...
|
||||
|
||||
|
||||
|
||||
@@ -71,8 +71,27 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extend_immutable_calls() -> Result<()> {
|
||||
let snapshot = "extend_immutable_calls".to_string();
|
||||
fn extend_immutable_calls_arg_annotation() -> Result<()> {
|
||||
let snapshot = "extend_immutable_calls_arg_annotation".to_string();
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_bugbear/B006_extended.py"),
|
||||
&Settings {
|
||||
flake8_bugbear: super::settings::Settings {
|
||||
extend_immutable_calls: vec![
|
||||
"custom.ImmutableTypeA".to_string(),
|
||||
"custom.ImmutableTypeB".to_string(),
|
||||
],
|
||||
},
|
||||
..Settings::for_rule(Rule::MutableArgumentDefault)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extend_immutable_calls_arg_default() -> Result<()> {
|
||||
let snapshot = "extend_immutable_calls_arg_default".to_string();
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_bugbear/B008_extended.py"),
|
||||
&Settings {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ast::call_path::{from_qualified_name, CallPath};
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::is_docstring_stmt;
|
||||
@@ -25,6 +26,10 @@ use crate::registry::AsRule;
|
||||
/// default, and initialize a new mutable object inside the function body
|
||||
/// for each call.
|
||||
///
|
||||
/// Arguments with immutable type annotations will be ignored by this rule.
|
||||
/// Types outside of the standard library can be marked as immutable with the
|
||||
/// [`flake8-bugbear.extend-immutable-calls`] configuration option.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def add_to_list(item, some_list=[]):
|
||||
@@ -49,6 +54,9 @@ use crate::registry::AsRule;
|
||||
/// l2 = add_to_list(1) # [1]
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `flake8-bugbear.extend-immutable-calls`
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Default Argument Values](https://docs.python.org/3/tutorial/controlflow.html#default-argument-values)
|
||||
#[violation]
|
||||
@@ -84,11 +92,18 @@ pub(crate) fn mutable_argument_default(checker: &mut Checker, function_def: &ast
|
||||
continue;
|
||||
};
|
||||
|
||||
let extend_immutable_calls: Vec<CallPath> = checker
|
||||
.settings
|
||||
.flake8_bugbear
|
||||
.extend_immutable_calls
|
||||
.iter()
|
||||
.map(|target| from_qualified_name(target))
|
||||
.collect();
|
||||
|
||||
if is_mutable_expr(default, checker.semantic())
|
||||
&& !parameter
|
||||
.annotation
|
||||
.as_ref()
|
||||
.is_some_and(|expr| is_immutable_annotation(expr, checker.semantic()))
|
||||
&& !parameter.annotation.as_ref().is_some_and(|expr| {
|
||||
is_immutable_annotation(expr, checker.semantic(), extend_immutable_calls.as_slice())
|
||||
})
|
||||
{
|
||||
let mut diagnostic = Diagnostic::new(MutableArgumentDefault, default.range());
|
||||
|
||||
@@ -125,7 +140,7 @@ fn move_initialization(
|
||||
let mut body = function_def.body.iter();
|
||||
|
||||
let statement = body.next()?;
|
||||
if indexer.preceded_by_multi_statement_line(statement, locator) {
|
||||
if indexer.in_multi_statement_line(statement, locator) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -155,7 +170,7 @@ fn move_initialization(
|
||||
if let Some(statement) = body.next() {
|
||||
// If there's a second statement, insert _before_ it, but ensure this isn't a
|
||||
// multi-statement line.
|
||||
if indexer.preceded_by_multi_statement_line(statement, locator) {
|
||||
if indexer.in_multi_statement_line(statement, locator) {
|
||||
return None;
|
||||
}
|
||||
Edit::insertion(content, locator.line_start(statement.start()))
|
||||
|
||||
@@ -79,7 +79,6 @@ pub(crate) fn redundant_tuple_in_exception_handler(
|
||||
type_.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
checker.generator().expr(elt),
|
||||
type_.range(),
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006_extended.py:17:55: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
17 | def error_due_to_missing_import(foo: ImmutableTypeA = []):
|
||||
| ^^ B006
|
||||
18 | ...
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
14 14 | ...
|
||||
15 15 |
|
||||
16 16 |
|
||||
17 |-def error_due_to_missing_import(foo: ImmutableTypeA = []):
|
||||
17 |+def error_due_to_missing_import(foo: ImmutableTypeA = None):
|
||||
18 |+ if foo is None:
|
||||
19 |+ foo = []
|
||||
18 20 | ...
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{Expr, Keyword, Ranged};
|
||||
|
||||
@@ -86,8 +86,9 @@ pub(crate) fn unnecessary_collection_call(
|
||||
expr.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| fixes::fix_unnecessary_collection_call(checker, expr));
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_collection_call(checker, expr).map(Fix::suggested)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Comprehension, Expr, Ranged};
|
||||
|
||||
@@ -63,9 +63,9 @@ fn add_diagnostic(checker: &mut Checker, expr: &Expr) {
|
||||
expr.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| {
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_comprehension(checker.locator(), checker.stylist(), expr)
|
||||
.map(Fix::suggested)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::comparable::ComparableKeyword;
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Ranged};
|
||||
@@ -130,13 +130,13 @@ pub(crate) fn unnecessary_double_cast_or_process(
|
||||
expr.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| {
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_double_cast_or_process(
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
expr,
|
||||
)
|
||||
.map(Fix::suggested)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
@@ -59,9 +58,8 @@ pub(crate) fn unnecessary_generator_dict(
|
||||
Expr::Tuple(ast::ExprTuple { elts, .. }) if elts.len() == 2 => {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorDict, expr.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| {
|
||||
fixes::fix_unnecessary_generator_dict(checker, expr)
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_generator_dict(checker, expr).map(Fix::suggested)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use ruff_python_ast::{Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -60,9 +60,9 @@ pub(crate) fn unnecessary_generator_list(
|
||||
if let Expr::GeneratorExp(_) = argument {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorList, expr.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| {
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_generator_list(checker.locator(), checker.stylist(), expr)
|
||||
.map(Fix::suggested)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use ruff_python_ast::{Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -60,9 +60,9 @@ pub(crate) fn unnecessary_generator_set(
|
||||
if let Expr::GeneratorExp(_) = argument {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorSet, expr.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic
|
||||
.try_set_fix_from_edit(|| fixes::fix_unnecessary_generator_set(checker, expr));
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_generator_set(checker, expr).map(Fix::suggested)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use ruff_python_ast::{Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -56,9 +56,9 @@ pub(crate) fn unnecessary_list_call(
|
||||
}
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryListCall, expr.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| {
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_list_call(checker.locator(), checker.stylist(), expr)
|
||||
.map(Fix::suggested)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
@@ -66,9 +65,8 @@ pub(crate) fn unnecessary_list_comprehension_dict(
|
||||
}
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionDict, expr.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| {
|
||||
fixes::fix_unnecessary_list_comprehension_dict(checker, expr)
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_list_comprehension_dict(checker, expr).map(Fix::suggested)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use ruff_python_ast::{Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -58,9 +58,8 @@ pub(crate) fn unnecessary_list_comprehension_set(
|
||||
if argument.is_list_comp_expr() {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionSet, expr.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| {
|
||||
fixes::fix_unnecessary_list_comprehension_set(checker, expr)
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_list_comprehension_set(checker, expr).map(Fix::suggested)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
@@ -81,8 +80,8 @@ pub(crate) fn unnecessary_literal_dict(
|
||||
expr.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| fixes::fix_unnecessary_literal_dict(checker, expr));
|
||||
diagnostic
|
||||
.try_set_fix(|| fixes::fix_unnecessary_literal_dict(checker, expr).map(Fix::suggested));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use ruff_python_ast::{Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -75,8 +75,8 @@ pub(crate) fn unnecessary_literal_set(
|
||||
expr.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| fixes::fix_unnecessary_literal_set(checker, expr));
|
||||
diagnostic
|
||||
.try_set_fix(|| fixes::fix_unnecessary_literal_set(checker, expr).map(Fix::suggested));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::fmt;
|
||||
|
||||
use ruff_python_ast::{Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -91,13 +91,13 @@ pub(crate) fn unnecessary_literal_within_dict_call(
|
||||
expr.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| {
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_literal_within_dict_call(
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
expr,
|
||||
)
|
||||
.map(Fix::suggested)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use ruff_python_ast::{Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{Expr, Keyword, Ranged};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
@@ -94,13 +93,13 @@ pub(crate) fn unnecessary_literal_within_list_call(
|
||||
expr.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| {
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_literal_within_list_call(
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
expr,
|
||||
)
|
||||
.map(Fix::suggested)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use ruff_python_ast::{Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -95,13 +95,13 @@ pub(crate) fn unnecessary_literal_within_tuple_call(
|
||||
expr.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| {
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_literal_within_tuple_call(
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
expr,
|
||||
)
|
||||
.map(Fix::suggested)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -103,10 +103,17 @@ pub(crate) fn unnecessary_map(
|
||||
return;
|
||||
};
|
||||
|
||||
if parameters
|
||||
.as_ref()
|
||||
.is_some_and(|parameters| late_binding(parameters, body))
|
||||
{
|
||||
if parameters.as_ref().is_some_and(|parameters| {
|
||||
late_binding(parameters, body)
|
||||
|| parameters
|
||||
.posonlyargs
|
||||
.iter()
|
||||
.chain(¶meters.args)
|
||||
.chain(¶meters.kwonlyargs)
|
||||
.any(|param| param.default.is_some())
|
||||
|| parameters.vararg.is_some()
|
||||
|| parameters.kwarg.is_some()
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -137,10 +144,17 @@ pub(crate) fn unnecessary_map(
|
||||
return;
|
||||
};
|
||||
|
||||
if parameters
|
||||
.as_ref()
|
||||
.is_some_and(|parameters| late_binding(parameters, body))
|
||||
{
|
||||
if parameters.as_ref().is_some_and(|parameters| {
|
||||
late_binding(parameters, body)
|
||||
|| parameters
|
||||
.posonlyargs
|
||||
.iter()
|
||||
.chain(¶meters.args)
|
||||
.chain(¶meters.kwonlyargs)
|
||||
.any(|param| param.default.is_some())
|
||||
|| parameters.vararg.is_some()
|
||||
|| parameters.kwarg.is_some()
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -177,14 +191,21 @@ pub(crate) fn unnecessary_map(
|
||||
return;
|
||||
}
|
||||
|
||||
if parameters
|
||||
.as_ref()
|
||||
.is_some_and(|parameters| late_binding(parameters, body))
|
||||
{
|
||||
if parameters.as_ref().is_some_and(|parameters| {
|
||||
late_binding(parameters, body)
|
||||
|| parameters
|
||||
.posonlyargs
|
||||
.iter()
|
||||
.chain(¶meters.args)
|
||||
.chain(¶meters.kwonlyargs)
|
||||
.any(|param| param.default.is_some())
|
||||
|| parameters.vararg.is_some()
|
||||
|| parameters.kwarg.is_some()
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryMap { object_type }, expr.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
|
||||
@@ -226,7 +226,7 @@ C417.py:15:8: C417 [*] Unnecessary `map` usage (rewrite using a `set` comprehens
|
||||
15 |+_ = f"{ {x % 2 == 0 for x in nums} }"
|
||||
16 16 | _ = f"{dict(map(lambda v: (v, v**2), nums))}"
|
||||
17 17 |
|
||||
18 18 | # Error, but unfixable.
|
||||
18 18 | # False negatives.
|
||||
|
||||
C417.py:16:8: C417 [*] Unnecessary `map` usage (rewrite using a `dict` comprehension)
|
||||
|
|
||||
@@ -235,7 +235,7 @@ C417.py:16:8: C417 [*] Unnecessary `map` usage (rewrite using a `dict` comprehen
|
||||
16 | _ = f"{dict(map(lambda v: (v, v**2), nums))}"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417
|
||||
17 |
|
||||
18 | # Error, but unfixable.
|
||||
18 | # False negatives.
|
||||
|
|
||||
= help: Replace `map` with a `dict` comprehension
|
||||
|
||||
@@ -246,33 +246,27 @@ C417.py:16:8: C417 [*] Unnecessary `map` usage (rewrite using a `dict` comprehen
|
||||
16 |-_ = f"{dict(map(lambda v: (v, v**2), nums))}"
|
||||
16 |+_ = f"{ {v: v**2 for v in nums} }"
|
||||
17 17 |
|
||||
18 18 | # Error, but unfixable.
|
||||
19 19 | # For simple expressions, this could be: `(x if x else 1 for x in nums)`.
|
||||
18 18 | # False negatives.
|
||||
19 19 | map(lambda x=2, y=1: x + y, nums, nums)
|
||||
|
||||
C417.py:21:1: C417 Unnecessary `map` usage (rewrite using a generator expression)
|
||||
C417.py:34:1: C417 [*] Unnecessary `map` usage (rewrite using a generator expression)
|
||||
|
|
||||
19 | # For simple expressions, this could be: `(x if x else 1 for x in nums)`.
|
||||
20 | # For more complex expressions, this would differ: `(x + 2 if x else 3 for x in nums)`.
|
||||
21 | map(lambda x=1: x, nums)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ C417
|
||||
22 |
|
||||
23 | # False negatives.
|
||||
|
|
||||
= help: Replace `map` with a generator expression
|
||||
|
||||
C417.py:39:1: C417 [*] Unnecessary `map` usage (rewrite using a generator expression)
|
||||
|
|
||||
38 | # Error: the `x` is overridden by the inner lambda.
|
||||
39 | map(lambda x: lambda x: x, range(4))
|
||||
33 | # Error: the `x` is overridden by the inner lambda.
|
||||
34 | map(lambda x: lambda x: x, range(4))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417
|
||||
35 |
|
||||
36 | # Ok because of the default parameters, and variadic arguments.
|
||||
|
|
||||
= help: Replace `map` with a generator expression
|
||||
|
||||
ℹ Suggested fix
|
||||
36 36 | map(lambda x: lambda: x, range(4))
|
||||
37 37 |
|
||||
38 38 | # Error: the `x` is overridden by the inner lambda.
|
||||
39 |-map(lambda x: lambda x: x, range(4))
|
||||
39 |+(lambda x: x for x in range(4))
|
||||
31 31 | map(lambda x: lambda: x, range(4))
|
||||
32 32 |
|
||||
33 33 | # Error: the `x` is overridden by the inner lambda.
|
||||
34 |-map(lambda x: lambda x: x, range(4))
|
||||
34 |+(lambda x: x for x in range(4))
|
||||
35 35 |
|
||||
36 36 | # Ok because of the default parameters, and variadic arguments.
|
||||
37 37 | map(lambda x=1: x, nums)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,6 +5,49 @@ use ruff_python_ast::{self as ast, Constant, Expr, Ranged};
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_datetimez::rules::helpers::has_non_none_keyword;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `datetime.datetime.strptime()` that lead to naive
|
||||
/// datetime objects.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Python datetime objects can be naive or timezone-aware. While an aware
|
||||
/// object represents a specific moment in time, a naive object does not
|
||||
/// contain enough information to unambiguously locate itself relative to other
|
||||
/// datetime objects. Since this can lead to errors, it is recommended to
|
||||
/// always use timezone-aware objects.
|
||||
///
|
||||
/// `datetime.datetime.strptime()` without `%z` returns a naive datetime
|
||||
/// object. Follow it with `.replace(tzinfo=)` or `.astimezone()`.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import datetime
|
||||
///
|
||||
/// datetime.datetime.strptime("2022/01/31", "%Y/%m/%d")
|
||||
/// ```
|
||||
///
|
||||
/// Instead, use `.replace(tzinfo=)`:
|
||||
/// ```python
|
||||
/// import datetime
|
||||
///
|
||||
/// datetime.datetime.strptime("2022/01/31", "%Y/%m/%d").replace(
|
||||
/// tzinfo=datetime.timezone.utc
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Or, use `.astimezone()`:
|
||||
/// ```python
|
||||
/// import datetime
|
||||
///
|
||||
/// datetime.datetime.strptime("2022/01/31", "%Y/%m/%d").astimezone(datetime.timezone.utc)
|
||||
/// ```
|
||||
///
|
||||
/// On Python 3.11 and later, `datetime.timezone.utc` can be replaced with
|
||||
/// `datetime.UTC`.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects)
|
||||
/// - [Python documentation: `strftime()` and `strptime()` Behavior](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior)
|
||||
#[violation]
|
||||
pub struct CallDatetimeStrptimeWithoutZone;
|
||||
|
||||
|
||||
@@ -126,8 +126,8 @@ pub(crate) fn implicit(
|
||||
}
|
||||
|
||||
fn concatenate_strings(a_range: TextRange, b_range: TextRange, locator: &Locator) -> Option<Fix> {
|
||||
let a_text = &locator.contents()[a_range];
|
||||
let b_text = &locator.contents()[b_range];
|
||||
let a_text = locator.slice(a_range);
|
||||
let b_text = locator.slice(b_range);
|
||||
|
||||
let a_leading_quote = leading_quote(a_text)?;
|
||||
let b_leading_quote = leading_quote(b_text)?;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -9,6 +9,7 @@ const CONVENTIONAL_ALIASES: &[(&str, &str)] = &[
|
||||
("altair", "alt"),
|
||||
("matplotlib", "mpl"),
|
||||
("matplotlib.pyplot", "plt"),
|
||||
("networkx", "nx"),
|
||||
("numpy", "np"),
|
||||
("pandas", "pd"),
|
||||
("seaborn", "sns"),
|
||||
|
||||
@@ -1,132 +1,282 @@
|
||||
---
|
||||
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 | import networkx
|
||||
|
||||
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
|
||||
12 | import networkx
|
||||
|
|
||||
= 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 | import networkx
|
||||
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
|
||||
12 | import networkx
|
||||
|
|
||||
= 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 | import networkx
|
||||
13 13 |
|
||||
14 14 |
|
||||
|
||||
defaults.py:12:12: ICN001 [*] `networkx` should be imported as `nx`
|
||||
|
|
||||
10 | import seaborn
|
||||
11 | import tkinter
|
||||
12 | import networkx
|
||||
| ^^^^^^^^ ICN001
|
||||
|
|
||||
= help: Alias `networkx` to `nx`
|
||||
|
||||
ℹ Suggested fix
|
||||
9 9 | import pandas
|
||||
10 10 | import seaborn
|
||||
11 11 | import tkinter
|
||||
12 |- import networkx
|
||||
12 |+ import networkx as nx
|
||||
13 13 |
|
||||
14 14 |
|
||||
15 15 | def unconventional_aliases():
|
||||
|
||||
defaults.py:16:22: ICN001 [*] `altair` should be imported as `alt`
|
||||
|
|
||||
15 | def unconventional_aliases():
|
||||
16 | import altair as altr
|
||||
| ^^^^ ICN001
|
||||
17 | import matplotlib.pyplot as plot
|
||||
18 | import numpy as nmp
|
||||
|
|
||||
= help: Alias `altair` to `alt`
|
||||
|
||||
ℹ Suggested fix
|
||||
13 13 |
|
||||
14 14 |
|
||||
15 15 | def unconventional_aliases():
|
||||
16 |- import altair as altr
|
||||
16 |+ import altair as alt
|
||||
17 17 | import matplotlib.pyplot as plot
|
||||
18 18 | import numpy as nmp
|
||||
19 19 | import pandas as pdas
|
||||
|
||||
defaults.py:17:33: ICN001 [*] `matplotlib.pyplot` should be imported as `plt`
|
||||
|
|
||||
15 | def unconventional_aliases():
|
||||
16 | import altair as altr
|
||||
17 | import matplotlib.pyplot as plot
|
||||
| ^^^^ ICN001
|
||||
18 | import numpy as nmp
|
||||
19 | import pandas as pdas
|
||||
|
|
||||
= help: Alias `matplotlib.pyplot` to `plt`
|
||||
|
||||
ℹ Suggested fix
|
||||
14 14 |
|
||||
15 15 | def unconventional_aliases():
|
||||
16 16 | import altair as altr
|
||||
17 |- import matplotlib.pyplot as plot
|
||||
17 |+ import matplotlib.pyplot as plt
|
||||
18 18 | import numpy as nmp
|
||||
19 19 | import pandas as pdas
|
||||
20 20 | import seaborn as sbrn
|
||||
|
||||
defaults.py:18:21: ICN001 [*] `numpy` should be imported as `np`
|
||||
|
|
||||
16 | import altair as altr
|
||||
17 | import matplotlib.pyplot as plot
|
||||
18 | import numpy as nmp
|
||||
| ^^^ ICN001
|
||||
19 | import pandas as pdas
|
||||
20 | import seaborn as sbrn
|
||||
|
|
||||
= help: Alias `numpy` to `np`
|
||||
|
||||
ℹ Suggested fix
|
||||
15 15 | def unconventional_aliases():
|
||||
16 16 | import altair as altr
|
||||
17 17 | import matplotlib.pyplot as plot
|
||||
18 |- import numpy as nmp
|
||||
18 |+ import numpy as np
|
||||
19 19 | import pandas as pdas
|
||||
20 20 | import seaborn as sbrn
|
||||
21 21 | import tkinter as tkr
|
||||
|
||||
defaults.py:19:22: ICN001 [*] `pandas` should be imported as `pd`
|
||||
|
|
||||
17 | import matplotlib.pyplot as plot
|
||||
18 | import numpy as nmp
|
||||
19 | import pandas as pdas
|
||||
| ^^^^ ICN001
|
||||
20 | import seaborn as sbrn
|
||||
21 | import tkinter as tkr
|
||||
|
|
||||
= help: Alias `pandas` to `pd`
|
||||
|
||||
ℹ Suggested fix
|
||||
16 16 | import altair as altr
|
||||
17 17 | import matplotlib.pyplot as plot
|
||||
18 18 | import numpy as nmp
|
||||
19 |- import pandas as pdas
|
||||
19 |+ import pandas as pd
|
||||
20 20 | import seaborn as sbrn
|
||||
21 21 | import tkinter as tkr
|
||||
22 22 | import networkx as nxy
|
||||
|
||||
defaults.py:20:23: ICN001 [*] `seaborn` should be imported as `sns`
|
||||
|
|
||||
18 | import numpy as nmp
|
||||
19 | import pandas as pdas
|
||||
20 | import seaborn as sbrn
|
||||
| ^^^^ ICN001
|
||||
21 | import tkinter as tkr
|
||||
22 | import networkx as nxy
|
||||
|
|
||||
= help: Alias `seaborn` to `sns`
|
||||
|
||||
ℹ Suggested fix
|
||||
17 17 | import matplotlib.pyplot as plot
|
||||
18 18 | import numpy as nmp
|
||||
19 19 | import pandas as pdas
|
||||
20 |- import seaborn as sbrn
|
||||
20 |+ import seaborn as sns
|
||||
21 21 | import tkinter as tkr
|
||||
22 22 | import networkx as nxy
|
||||
23 23 |
|
||||
|
||||
defaults.py:21:23: ICN001 [*] `tkinter` should be imported as `tk`
|
||||
|
|
||||
19 | import pandas as pdas
|
||||
20 | import seaborn as sbrn
|
||||
21 | import tkinter as tkr
|
||||
| ^^^ ICN001
|
||||
22 | import networkx as nxy
|
||||
|
|
||||
= help: Alias `tkinter` to `tk`
|
||||
|
||||
ℹ Suggested fix
|
||||
18 18 | import numpy as nmp
|
||||
19 19 | import pandas as pdas
|
||||
20 20 | import seaborn as sbrn
|
||||
21 |- import tkinter as tkr
|
||||
21 |+ import tkinter as tk
|
||||
22 22 | import networkx as nxy
|
||||
23 23 |
|
||||
24 24 | def conventional_aliases():
|
||||
|
||||
defaults.py:22:24: ICN001 [*] `networkx` should be imported as `nx`
|
||||
|
|
||||
20 | import seaborn as sbrn
|
||||
21 | import tkinter as tkr
|
||||
22 | import networkx as nxy
|
||||
| ^^^ ICN001
|
||||
23 |
|
||||
24 | def conventional_aliases():
|
||||
|
|
||||
= help: Alias `networkx` to `nx`
|
||||
|
||||
ℹ Suggested fix
|
||||
19 19 | import pandas as pdas
|
||||
20 20 | import seaborn as sbrn
|
||||
21 21 | import tkinter as tkr
|
||||
22 |- import networkx as nxy
|
||||
22 |+ import networkx as nx
|
||||
23 23 |
|
||||
24 24 | def conventional_aliases():
|
||||
25 25 | import altair as alt
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,19 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
/// As an alternative to `extra`, passing values as arguments to the logging
|
||||
/// method can also be used to defer string formatting until required.
|
||||
///
|
||||
/// ## Known problems
|
||||
///
|
||||
/// This rule detects uses of the `logging` module via a heuristic.
|
||||
/// Specifically, it matches against:
|
||||
///
|
||||
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
|
||||
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
|
||||
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
|
||||
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
|
||||
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
|
||||
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
|
||||
/// when [`logger-objects`] is set to `["module.logger"]`).
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import logging
|
||||
@@ -54,6 +67,9 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
/// logging.info("%s - Something happened", user)
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `logger-objects`
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html)
|
||||
/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization)
|
||||
@@ -89,6 +105,19 @@ impl Violation for LoggingStringFormat {
|
||||
/// As an alternative to `extra`, passing values as arguments to the logging
|
||||
/// method can also be used to defer string formatting until required.
|
||||
///
|
||||
/// ## Known problems
|
||||
///
|
||||
/// This rule detects uses of the `logging` module via a heuristic.
|
||||
/// Specifically, it matches against:
|
||||
///
|
||||
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
|
||||
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
|
||||
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
|
||||
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
|
||||
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
|
||||
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
|
||||
/// when [`logger-objects`] is set to `["module.logger"]`).
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import logging
|
||||
@@ -122,6 +151,9 @@ impl Violation for LoggingStringFormat {
|
||||
/// logging.info("%s - Something happened", user)
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `logger-objects`
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html)
|
||||
/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization)
|
||||
@@ -156,6 +188,19 @@ impl Violation for LoggingPercentFormat {
|
||||
/// As an alternative to `extra`, passing values as arguments to the logging
|
||||
/// method can also be used to defer string formatting until required.
|
||||
///
|
||||
/// ## Known problems
|
||||
///
|
||||
/// This rule detects uses of the `logging` module via a heuristic.
|
||||
/// Specifically, it matches against:
|
||||
///
|
||||
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
|
||||
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
|
||||
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
|
||||
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
|
||||
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
|
||||
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
|
||||
/// when [`logger-objects`] is set to `["module.logger"]`).
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import logging
|
||||
@@ -189,6 +234,9 @@ impl Violation for LoggingPercentFormat {
|
||||
/// logging.info("%s - Something happened", user)
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `logger-objects`
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html)
|
||||
/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization)
|
||||
@@ -222,6 +270,19 @@ impl Violation for LoggingStringConcat {
|
||||
/// As an alternative to `extra`, passing values as arguments to the logging
|
||||
/// method can also be used to defer string formatting until required.
|
||||
///
|
||||
/// ## Known problems
|
||||
///
|
||||
/// This rule detects uses of the `logging` module via a heuristic.
|
||||
/// Specifically, it matches against:
|
||||
///
|
||||
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
|
||||
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
|
||||
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
|
||||
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
|
||||
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
|
||||
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
|
||||
/// when [`logger-objects`] is set to `["module.logger"]`).
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import logging
|
||||
@@ -255,6 +316,9 @@ impl Violation for LoggingStringConcat {
|
||||
/// logging.info("%s - Something happened", user)
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `logger-objects`
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html)
|
||||
/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization)
|
||||
@@ -276,6 +340,19 @@ impl Violation for LoggingFString {
|
||||
/// `logging.warning` and `logging.Logger.warning`, which are functionally
|
||||
/// equivalent.
|
||||
///
|
||||
/// ## Known problems
|
||||
///
|
||||
/// This rule detects uses of the `logging` module via a heuristic.
|
||||
/// Specifically, it matches against:
|
||||
///
|
||||
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
|
||||
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
|
||||
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
|
||||
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
|
||||
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
|
||||
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
|
||||
/// when [`logger-objects`] is set to `["module.logger"]`).
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import logging
|
||||
@@ -290,6 +367,9 @@ impl Violation for LoggingFString {
|
||||
/// logging.warning("Something happened")
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `logger-objects`
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `logging.warning`](https://docs.python.org/3/library/logging.html#logging.warning)
|
||||
/// - [Python documentation: `logging.Logger.warning`](https://docs.python.org/3/library/logging.html#logging.Logger.warning)
|
||||
@@ -320,6 +400,19 @@ impl AlwaysAutofixableViolation for LoggingWarn {
|
||||
/// the `LogRecord` constructor will raise a `KeyError` when the `LogRecord` is
|
||||
/// constructed.
|
||||
///
|
||||
/// ## Known problems
|
||||
///
|
||||
/// This rule detects uses of the `logging` module via a heuristic.
|
||||
/// Specifically, it matches against:
|
||||
///
|
||||
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
|
||||
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
|
||||
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
|
||||
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
|
||||
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
|
||||
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
|
||||
/// when [`logger-objects`] is set to `["module.logger"]`).
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import logging
|
||||
@@ -342,6 +435,9 @@ impl AlwaysAutofixableViolation for LoggingWarn {
|
||||
/// logging.info("Something happened", extra=dict(user=username))
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `logger-objects`
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: LogRecord attributes](https://docs.python.org/3/library/logging.html#logrecord-attributes)
|
||||
#[violation]
|
||||
@@ -365,6 +461,19 @@ impl Violation for LoggingExtraAttrClash {
|
||||
/// `logging.exception`. Using `logging.exception` is more concise, more
|
||||
/// readable, and conveys the intent of the logging statement more clearly.
|
||||
///
|
||||
/// ## Known problems
|
||||
///
|
||||
/// This rule detects uses of the `logging` module via a heuristic.
|
||||
/// Specifically, it matches against:
|
||||
///
|
||||
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
|
||||
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
|
||||
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
|
||||
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
|
||||
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
|
||||
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
|
||||
/// when [`logger-objects`] is set to `["module.logger"]`).
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import logging
|
||||
@@ -385,6 +494,9 @@ impl Violation for LoggingExtraAttrClash {
|
||||
/// logging.exception("Exception occurred")
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `logger-objects`
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `logging.exception`](https://docs.python.org/3/library/logging.html#logging.exception)
|
||||
/// - [Python documentation: `exception`](https://docs.python.org/3/library/logging.html#logging.Logger.exception)
|
||||
@@ -410,6 +522,19 @@ impl Violation for LoggingExcInfo {
|
||||
/// Passing `exc_info=True` to `logging.exception` calls is redundant, as is
|
||||
/// passing `exc_info=False` to `logging.error` calls.
|
||||
///
|
||||
/// ## Known problems
|
||||
///
|
||||
/// This rule detects uses of the `logging` module via a heuristic.
|
||||
/// Specifically, it matches against:
|
||||
///
|
||||
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
|
||||
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
|
||||
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
|
||||
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
|
||||
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
|
||||
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
|
||||
/// when [`logger-objects`] is set to `["module.logger"]`).
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import logging
|
||||
@@ -430,6 +555,9 @@ impl Violation for LoggingExcInfo {
|
||||
/// logging.exception("Exception occurred")
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `logger-objects`
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `logging.exception`](https://docs.python.org/3/library/logging.html#logging.exception)
|
||||
/// - [Python documentation: `exception`](https://docs.python.org/3/library/logging.html#logging.Logger.exception)
|
||||
|
||||
@@ -85,7 +85,9 @@ pub(crate) fn duplicate_class_field_definition(checker: &mut Checker, body: &[St
|
||||
checker.locator(),
|
||||
checker.indexer(),
|
||||
);
|
||||
diagnostic.set_fix(Fix::suggested(edit).isolate(checker.statement_isolation()));
|
||||
diagnostic.set_fix(Fix::suggested(edit).isolate(Checker::isolation(Some(
|
||||
checker.semantic().current_statement_id(),
|
||||
))));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -50,12 +50,12 @@ pub(crate) fn duplicate_union_member<'a>(checker: &mut Checker, expr: &'a Expr)
|
||||
// parent without the duplicate.
|
||||
|
||||
// If the parent node is not a `BinOp` we will not perform a fix
|
||||
if let Some(Expr::BinOp(ast::ExprBinOp { left, right, .. })) = parent {
|
||||
if let Some(parent @ Expr::BinOp(ast::ExprBinOp { left, right, .. })) = parent {
|
||||
// Replace the parent with its non-duplicate child.
|
||||
let child = if expr == left.as_ref() { right } else { left };
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
checker.locator().slice(child.range()).to_string(),
|
||||
parent.unwrap().range(),
|
||||
parent.range(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,9 @@ pub(crate) fn ellipsis_in_non_empty_class_body(checker: &mut Checker, body: &[St
|
||||
checker.locator(),
|
||||
checker.indexer(),
|
||||
);
|
||||
diagnostic.set_fix(Fix::automatic(edit).isolate(checker.statement_isolation()));
|
||||
diagnostic.set_fix(Fix::automatic(edit).isolate(Checker::isolation(Some(
|
||||
checker.semantic().current_statement_id(),
|
||||
))));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,9 @@ pub(crate) fn pass_in_class_body(checker: &mut Checker, class_def: &ast::StmtCla
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
let edit =
|
||||
autofix::edits::delete_stmt(stmt, Some(stmt), checker.locator(), checker.indexer());
|
||||
diagnostic.set_fix(Fix::automatic(edit).isolate(checker.statement_isolation()));
|
||||
diagnostic.set_fix(Fix::automatic(edit).isolate(Checker::isolation(Some(
|
||||
checker.semantic().current_statement_id(),
|
||||
))));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -99,7 +99,9 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) {
|
||||
let stmt = checker.semantic().current_statement();
|
||||
let parent = checker.semantic().current_statement_parent();
|
||||
let edit = delete_stmt(stmt, parent, checker.locator(), checker.indexer());
|
||||
diagnostic.set_fix(Fix::automatic(edit).isolate(checker.parent_isolation()));
|
||||
diagnostic.set_fix(Fix::automatic(edit).isolate(Checker::isolation(
|
||||
checker.semantic().current_statement_parent_id(),
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use anyhow::bail;
|
||||
use anyhow::Result;
|
||||
use anyhow::{bail, Context};
|
||||
use libcst_native::{
|
||||
self, Assert, BooleanOp, CompoundStatement, Expression, ParenthesizableWhitespace,
|
||||
ParenthesizedNode, SimpleStatementLine, SimpleWhitespace, SmallStatement, Statement,
|
||||
@@ -635,9 +635,8 @@ fn parenthesize<'a>(expression: Expression<'a>, parent: &Expression<'a>) -> Expr
|
||||
/// `assert a == "hello"` and `assert b == "world"`.
|
||||
fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> Result<Edit> {
|
||||
// Infer the indentation of the outer block.
|
||||
let Some(outer_indent) = whitespace::indentation(locator, stmt) else {
|
||||
bail!("Unable to fix multiline statement");
|
||||
};
|
||||
let outer_indent =
|
||||
whitespace::indentation(locator, stmt).context("Unable to fix multiline statement")?;
|
||||
|
||||
// Extract the module text.
|
||||
let contents = locator.lines(stmt.range());
|
||||
@@ -672,11 +671,11 @@ fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) ->
|
||||
&mut indented_block.body
|
||||
};
|
||||
|
||||
let [Statement::Simple(simple_statement_line)] = &statements[..] else {
|
||||
let [Statement::Simple(simple_statement_line)] = statements.as_slice() else {
|
||||
bail!("Expected one simple statement")
|
||||
};
|
||||
|
||||
let [SmallStatement::Assert(assert_statement)] = &simple_statement_line.body[..] else {
|
||||
let [SmallStatement::Assert(assert_statement)] = simple_statement_line.body.as_slice() else {
|
||||
bail!("Expected simple statement to be an assert")
|
||||
};
|
||||
|
||||
@@ -754,10 +753,13 @@ pub(crate) fn composite_condition(
|
||||
if matches!(composite, CompositionKind::Simple)
|
||||
&& msg.is_none()
|
||||
&& !checker.indexer().comment_ranges().intersects(stmt.range())
|
||||
&& !checker
|
||||
.indexer()
|
||||
.in_multi_statement_line(stmt, checker.locator())
|
||||
{
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| {
|
||||
diagnostic.try_set_fix(|| {
|
||||
fix_composite_condition(stmt, checker.locator(), checker.stylist())
|
||||
.map(Fix::suggested)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::hash::BuildHasherDefault;
|
||||
|
||||
use ruff_python_ast::{
|
||||
self as ast, Arguments, Constant, Decorator, Expr, ExprContext, PySourceType, Ranged,
|
||||
};
|
||||
use ruff_python_parser::{lexer, AsMode, Tok};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
use ruff_python_ast::node::AstNode;
|
||||
use ruff_python_ast::parenthesize::parenthesized_range;
|
||||
use ruff_python_ast::{self as ast, Arguments, Constant, Decorator, Expr, ExprContext, Ranged};
|
||||
use ruff_python_codegen::Generator;
|
||||
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
@@ -74,7 +72,7 @@ use super::helpers::{is_pytest_parametrize, split_names};
|
||||
/// - [`pytest` documentation: How to parametrize fixtures and test functions](https://docs.pytest.org/en/latest/how-to/parametrize.html#pytest-mark-parametrize)
|
||||
#[violation]
|
||||
pub struct PytestParametrizeNamesWrongType {
|
||||
pub expected: types::ParametrizeNameType,
|
||||
expected: types::ParametrizeNameType,
|
||||
}
|
||||
|
||||
impl Violation for PytestParametrizeNamesWrongType {
|
||||
@@ -159,8 +157,8 @@ impl Violation for PytestParametrizeNamesWrongType {
|
||||
/// - [`pytest` documentation: How to parametrize fixtures and test functions](https://docs.pytest.org/en/latest/how-to/parametrize.html#pytest-mark-parametrize)
|
||||
#[violation]
|
||||
pub struct PytestParametrizeValuesWrongType {
|
||||
pub values: types::ParametrizeValuesType,
|
||||
pub row: types::ParametrizeValuesRowType,
|
||||
values: types::ParametrizeValuesType,
|
||||
row: types::ParametrizeValuesRowType,
|
||||
}
|
||||
|
||||
impl Violation for PytestParametrizeValuesWrongType {
|
||||
@@ -283,34 +281,12 @@ fn elts_to_csv(elts: &[Expr], generator: Generator) -> Option<String> {
|
||||
fn get_parametrize_name_range(
|
||||
decorator: &Decorator,
|
||||
expr: &Expr,
|
||||
locator: &Locator,
|
||||
source_type: PySourceType,
|
||||
) -> TextRange {
|
||||
let mut locations = Vec::new();
|
||||
let mut name_range = None;
|
||||
|
||||
// The parenthesis are not part of the AST, so we need to tokenize the
|
||||
// decorator to find them.
|
||||
for (tok, range) in lexer::lex_starts_at(
|
||||
locator.slice(decorator.range()),
|
||||
source_type.as_mode(),
|
||||
decorator.start(),
|
||||
)
|
||||
.flatten()
|
||||
{
|
||||
match tok {
|
||||
Tok::Lpar => locations.push(range.start()),
|
||||
Tok::Rpar => {
|
||||
if let Some(start) = locations.pop() {
|
||||
name_range = Some(TextRange::new(start, range.end()));
|
||||
}
|
||||
}
|
||||
// Stop after the first argument.
|
||||
Tok::Comma => break,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
name_range.unwrap_or_else(|| expr.range())
|
||||
source: &str,
|
||||
) -> Option<TextRange> {
|
||||
decorator
|
||||
.expression
|
||||
.as_call_expr()
|
||||
.and_then(|call| parenthesized_range(expr.into(), call.arguments.as_any_node_ref(), source))
|
||||
}
|
||||
|
||||
/// PT006
|
||||
@@ -329,9 +305,9 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
|
||||
let name_range = get_parametrize_name_range(
|
||||
decorator,
|
||||
expr,
|
||||
checker.locator(),
|
||||
checker.source_type,
|
||||
);
|
||||
checker.locator().contents(),
|
||||
)
|
||||
.unwrap_or(expr.range());
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
PytestParametrizeNamesWrongType {
|
||||
expected: names_type,
|
||||
@@ -364,9 +340,9 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
|
||||
let name_range = get_parametrize_name_range(
|
||||
decorator,
|
||||
expr,
|
||||
checker.locator(),
|
||||
checker.source_type,
|
||||
);
|
||||
checker.locator().contents(),
|
||||
)
|
||||
.unwrap_or(expr.range());
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
PytestParametrizeNamesWrongType {
|
||||
expected: names_type,
|
||||
|
||||
@@ -299,6 +299,8 @@ PT018.py:44:1: PT018 [*] Assertion should be broken down into multiple parts
|
||||
44 |+assert something
|
||||
45 |+assert something_else
|
||||
45 46 | assert something and something_else and something_third # Error
|
||||
46 47 |
|
||||
47 48 |
|
||||
|
||||
PT018.py:45:1: PT018 [*] Assertion should be broken down into multiple parts
|
||||
|
|
||||
@@ -316,5 +318,37 @@ PT018.py:45:1: PT018 [*] Assertion should be broken down into multiple parts
|
||||
45 |-assert something and something_else and something_third # Error
|
||||
45 |+assert something and something_else
|
||||
46 |+assert something_third
|
||||
46 47 |
|
||||
47 48 |
|
||||
48 49 | def test_multiline():
|
||||
|
||||
PT018.py:49:5: PT018 Assertion should be broken down into multiple parts
|
||||
|
|
||||
48 | def test_multiline():
|
||||
49 | assert something and something_else; x = 1
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
|
||||
50 |
|
||||
51 | x = 1; assert something and something_else
|
||||
|
|
||||
= help: Break down assertion into multiple parts
|
||||
|
||||
PT018.py:51:12: PT018 Assertion should be broken down into multiple parts
|
||||
|
|
||||
49 | assert something and something_else; x = 1
|
||||
50 |
|
||||
51 | x = 1; assert something and something_else
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
|
||||
52 |
|
||||
53 | x = 1; \
|
||||
|
|
||||
= help: Break down assertion into multiple parts
|
||||
|
||||
PT018.py:54:9: PT018 Assertion should be broken down into multiple parts
|
||||
|
|
||||
53 | x = 1; \
|
||||
54 | assert something and something_else
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
|
||||
|
|
||||
= help: Break down assertion into multiple parts
|
||||
|
||||
|
||||
|
||||
@@ -25,19 +25,11 @@ pub(super) fn result_exists(returns: &[&ast::StmtReturn]) -> bool {
|
||||
/// This method assumes that the statement is the last statement in its body; specifically, that
|
||||
/// the statement isn't followed by a semicolon, followed by a multi-line statement.
|
||||
pub(super) fn end_of_last_statement(stmt: &Stmt, locator: &Locator) -> TextSize {
|
||||
if stmt.end() == locator.text_len() {
|
||||
// End-of-file, so just return the end of the statement.
|
||||
stmt.end()
|
||||
} else {
|
||||
// Otherwise, find the end of the last line that's "part of" the statement.
|
||||
let contents = locator.after(stmt.end());
|
||||
|
||||
for line in contents.universal_newlines() {
|
||||
if !line.ends_with('\\') {
|
||||
return stmt.end() + line.end();
|
||||
}
|
||||
// Find the end of the last line that's "part of" the statement.
|
||||
for line in locator.after(stmt.end()).universal_newlines() {
|
||||
if !line.ends_with('\\') {
|
||||
return stmt.end() + line.end();
|
||||
}
|
||||
|
||||
unreachable!("Expected to find end-of-statement")
|
||||
}
|
||||
locator.text_len()
|
||||
}
|
||||
|
||||
@@ -380,5 +380,24 @@ RET503.py:320:9: RET503 [*] Missing explicit `return` at the end of function abl
|
||||
321 321 | return "" \
|
||||
322 322 | ; # type: ignore
|
||||
323 |+ return None
|
||||
323 324 |
|
||||
324 325 |
|
||||
325 326 | def end_of_file():
|
||||
|
||||
RET503.py:328:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
326 | if False:
|
||||
327 | return 1
|
||||
328 | x = 2 \
|
||||
| ^^^^^ RET503
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Suggested fix
|
||||
326 326 | if False:
|
||||
327 327 | return 1
|
||||
328 328 | x = 2 \
|
||||
329 |+
|
||||
330 |+ return None
|
||||
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::comparable::{ComparableConstant, ComparableExpr, ComparableStmt};
|
||||
use ruff_python_ast::helpers::{any_over_expr, contains_effect};
|
||||
use ruff_python_ast::stmt_if::{if_elif_branches, IfElifBranch};
|
||||
use ruff_python_parser::first_colon_range;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_source_file::{Locator, UniversalNewlines};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -369,16 +369,10 @@ pub(crate) fn nested_if_statements(
|
||||
};
|
||||
|
||||
// Find the deepest nested if-statement, to inform the range.
|
||||
let Some((test, first_stmt)) = find_last_nested_if(body) else {
|
||||
let Some((test, _first_stmt)) = find_last_nested_if(body) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let colon = first_colon_range(
|
||||
TextRange::new(test.end(), first_stmt.start()),
|
||||
checker.locator().contents(),
|
||||
checker.source_type.is_jupyter(),
|
||||
);
|
||||
|
||||
// Check if the parent is already emitting a larger diagnostic including this if statement
|
||||
if let Some(Stmt::If(stmt_if)) = parent {
|
||||
if let Some((body, _range, _is_elif)) = nested_if_body(stmt_if) {
|
||||
@@ -392,10 +386,14 @@ pub(crate) fn nested_if_statements(
|
||||
}
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
CollapsibleIf,
|
||||
colon.map_or(range, |colon| TextRange::new(range.start(), colon.end())),
|
||||
);
|
||||
let Some(colon) = SimpleTokenizer::starts_at(test.end(), checker.locator().contents())
|
||||
.skip_trivia()
|
||||
.find(|token| token.kind == SimpleTokenKind::Colon)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(CollapsibleIf, TextRange::new(range.start(), colon.end()));
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
// The fixer preserves comments in the nested body, but removes comments between
|
||||
// the outer and inner if statements.
|
||||
@@ -721,10 +719,16 @@ pub(crate) fn if_with_same_arms(checker: &mut Checker, locator: &Locator, stmt_i
|
||||
// ...and the same comments
|
||||
let first_comments = checker
|
||||
.indexer()
|
||||
.comments_in_range(body_range(¤t_branch, locator), locator);
|
||||
.comment_ranges()
|
||||
.comments_in_range(body_range(¤t_branch, locator))
|
||||
.iter()
|
||||
.map(|range| locator.slice(*range));
|
||||
let second_comments = checker
|
||||
.indexer()
|
||||
.comments_in_range(body_range(following_branch, locator), locator);
|
||||
.comment_ranges()
|
||||
.comments_in_range(body_range(following_branch, locator))
|
||||
.iter()
|
||||
.map(|range| locator.slice(*range));
|
||||
if !first_comments.eq(second_comments) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use log::error;
|
||||
use ruff_python_ast::{self as ast, Ranged, Stmt, WithItem};
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use ruff_diagnostics::{AutofixKind, Violation};
|
||||
use ruff_diagnostics::{Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_parser::first_colon_range;
|
||||
use ruff_python_ast::{self as ast, Ranged, Stmt, WithItem};
|
||||
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_source_file::UniversalNewlines;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::line_width::LineWidth;
|
||||
@@ -106,32 +106,24 @@ pub(crate) fn multiple_with_statements(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((is_async, items, body)) = next_with(&with_stmt.body) {
|
||||
if let Some((is_async, items, _body)) = next_with(&with_stmt.body) {
|
||||
if is_async != with_stmt.is_async {
|
||||
// One of the statements is an async with, while the other is not,
|
||||
// we can't merge those statements.
|
||||
return;
|
||||
}
|
||||
|
||||
let last_item = items.last().expect("Expected items to be non-empty");
|
||||
let colon = first_colon_range(
|
||||
TextRange::new(
|
||||
last_item
|
||||
.optional_vars
|
||||
.as_ref()
|
||||
.map_or(last_item.context_expr.end(), |v| v.end()),
|
||||
body.first().expect("Expected body to be non-empty").start(),
|
||||
),
|
||||
checker.locator().contents(),
|
||||
checker.source_type.is_jupyter(),
|
||||
);
|
||||
let Some(colon) = items.last().and_then(|item| {
|
||||
SimpleTokenizer::starts_at(item.end(), checker.locator().contents())
|
||||
.skip_trivia()
|
||||
.find(|token| token.kind == SimpleTokenKind::Colon)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
MultipleWithStatements,
|
||||
colon.map_or_else(
|
||||
|| with_stmt.range(),
|
||||
|colon| TextRange::new(with_stmt.start(), colon.end()),
|
||||
),
|
||||
TextRange::new(with_stmt.start(), colon.end()),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if !checker
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
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::snippet::SourceCodeSnippet;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::cst::matchers::{match_comparison, match_expression};
|
||||
use crate::cst::matchers::{match_comparison, transform_expression};
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// ## What it does
|
||||
@@ -45,7 +45,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 +54,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 +66,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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,68 +95,69 @@ fn is_constant_like(expr: &Expr) -> bool {
|
||||
/// Generate a fix to reverse a comparison.
|
||||
fn reverse_comparison(expr: &Expr, locator: &Locator, stylist: &Stylist) -> Result<String> {
|
||||
let range = expr.range();
|
||||
let contents = locator.slice(range);
|
||||
let source_code = locator.slice(range);
|
||||
|
||||
let mut expression = match_expression(contents)?;
|
||||
let comparison = match_comparison(&mut expression)?;
|
||||
transform_expression(source_code, stylist, |mut expression| {
|
||||
let comparison = match_comparison(&mut expression)?;
|
||||
|
||||
let left = (*comparison.left).clone();
|
||||
let left = (*comparison.left).clone();
|
||||
|
||||
// Copy the right side to the left side.
|
||||
comparison.left = Box::new(comparison.comparisons[0].comparator.clone());
|
||||
// Copy the right side to the left side.
|
||||
comparison.left = Box::new(comparison.comparisons[0].comparator.clone());
|
||||
|
||||
// Copy the left side to the right side.
|
||||
comparison.comparisons[0].comparator = left;
|
||||
// Copy the left side to the right side.
|
||||
comparison.comparisons[0].comparator = left;
|
||||
|
||||
// Reverse the operator.
|
||||
let op = comparison.comparisons[0].operator.clone();
|
||||
comparison.comparisons[0].operator = match op {
|
||||
CompOp::LessThan {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
} => CompOp::GreaterThan {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
},
|
||||
CompOp::GreaterThan {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
} => CompOp::LessThan {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
},
|
||||
CompOp::LessThanEqual {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
} => CompOp::GreaterThanEqual {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
},
|
||||
CompOp::GreaterThanEqual {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
} => CompOp::LessThanEqual {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
},
|
||||
CompOp::Equal {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
} => CompOp::Equal {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
},
|
||||
CompOp::NotEqual {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
} => CompOp::NotEqual {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
},
|
||||
_ => panic!("Expected comparison operator"),
|
||||
};
|
||||
// Reverse the operator.
|
||||
let op = comparison.comparisons[0].operator.clone();
|
||||
comparison.comparisons[0].operator = match op {
|
||||
CompOp::LessThan {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
} => CompOp::GreaterThan {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
},
|
||||
CompOp::GreaterThan {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
} => CompOp::LessThan {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
},
|
||||
CompOp::LessThanEqual {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
} => CompOp::GreaterThanEqual {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
},
|
||||
CompOp::GreaterThanEqual {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
} => CompOp::LessThanEqual {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
},
|
||||
CompOp::Equal {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
} => CompOp::Equal {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
},
|
||||
CompOp::NotEqual {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
} => CompOp::NotEqual {
|
||||
whitespace_before,
|
||||
whitespace_after,
|
||||
},
|
||||
_ => panic!("Expected comparison operator"),
|
||||
};
|
||||
|
||||
Ok(expression.codegen_stylist(stylist))
|
||||
Ok(expression)
|
||||
})
|
||||
}
|
||||
|
||||
/// SIM300
|
||||
@@ -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
|
||||
|
||||
@@ -61,7 +61,9 @@ pub(crate) fn empty_type_checking_block(checker: &mut Checker, stmt: &ast::StmtI
|
||||
let stmt = checker.semantic().current_statement();
|
||||
let parent = checker.semantic().current_statement_parent();
|
||||
let edit = autofix::edits::delete_stmt(stmt, parent, checker.locator(), checker.indexer());
|
||||
diagnostic.set_fix(Fix::automatic(edit).isolate(checker.parent_isolation()));
|
||||
diagnostic.set_fix(Fix::automatic(edit).isolate(Checker::isolation(
|
||||
checker.semantic().current_statement_parent_id(),
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -243,7 +236,8 @@ fn fix_imports(
|
||||
)?;
|
||||
|
||||
Ok(
|
||||
Fix::suggested_edits(remove_import_edit, add_import_edit.into_edits())
|
||||
.isolate(checker.parent_isolation()),
|
||||
Fix::suggested_edits(remove_import_edit, add_import_edit.into_edits()).isolate(
|
||||
Checker::isolation(checker.semantic().parent_statement_id(node_id)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -490,7 +486,8 @@ fn fix_imports(
|
||||
)?;
|
||||
|
||||
Ok(
|
||||
Fix::suggested_edits(remove_import_edit, add_import_edit.into_edits())
|
||||
.isolate(checker.parent_isolation()),
|
||||
Fix::suggested_edits(remove_import_edit, add_import_edit.into_edits()).isolate(
|
||||
Checker::isolation(checker.semantic().parent_statement_id(node_id)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use ruff_python_ast::{PySourceType, Ranged};
|
||||
use ruff_python_parser::{lexer, AsMode, Tok};
|
||||
use ruff_python_ast::Ranged;
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
@@ -21,20 +21,15 @@ impl Ranged for Comment<'_> {
|
||||
pub(crate) fn collect_comments<'a>(
|
||||
range: TextRange,
|
||||
locator: &'a Locator,
|
||||
source_type: PySourceType,
|
||||
indexer: &'a Indexer,
|
||||
) -> Vec<Comment<'a>> {
|
||||
let contents = locator.slice(range);
|
||||
lexer::lex_starts_at(contents, source_type.as_mode(), range.start())
|
||||
.flatten()
|
||||
.filter_map(|(tok, range)| {
|
||||
if let Tok::Comment(value) = tok {
|
||||
Some(Comment {
|
||||
value: value.into(),
|
||||
range,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
indexer
|
||||
.comment_ranges()
|
||||
.comments_in_range(range)
|
||||
.iter()
|
||||
.map(|range| Comment {
|
||||
value: locator.slice(*range).into(),
|
||||
range: *range,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use std::path::Path;
|
||||
|
||||
use itertools::{EitherOrBoth, Itertools};
|
||||
use ruff_python_ast::{PySourceType, Ranged, Stmt};
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::whitespace::{followed_by_multi_statement_line, trailing_lines_end};
|
||||
use ruff_python_ast::whitespace::trailing_lines_end;
|
||||
use ruff_python_ast::{PySourceType, Ranged, Stmt};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_trivia::{leading_indentation, textwrap::indent, PythonWhitespace};
|
||||
use ruff_source_file::{Locator, UniversalNewlines};
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use crate::line_width::LineWidth;
|
||||
use crate::registry::AsRule;
|
||||
@@ -97,7 +97,7 @@ pub(crate) fn organize_imports(
|
||||
// Special-cases: there's leading or trailing content in the import block. These
|
||||
// are too hard to get right, and relatively rare, so flag but don't fix.
|
||||
if indexer.preceded_by_multi_statement_line(block.imports.first().unwrap(), locator)
|
||||
|| followed_by_multi_statement_line(block.imports.last().unwrap(), locator)
|
||||
|| indexer.followed_by_multi_statement_line(block.imports.last().unwrap(), locator)
|
||||
{
|
||||
return Some(Diagnostic::new(UnsortedImports, range));
|
||||
}
|
||||
@@ -106,7 +106,7 @@ pub(crate) fn organize_imports(
|
||||
let comments = comments::collect_comments(
|
||||
TextRange::new(range.start(), locator.full_line_end(range.end())),
|
||||
locator,
|
||||
source_type,
|
||||
indexer,
|
||||
);
|
||||
|
||||
let trailing_line_end = if block.trailer.is_none() {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ mod tests {
|
||||
#[test_case(Rule::NewLineAfterLastParagraph, Path::new("D.py"))]
|
||||
#[test_case(Rule::NewLineAfterSectionName, Path::new("sections.py"))]
|
||||
#[test_case(Rule::NoBlankLineAfterFunction, Path::new("D.py"))]
|
||||
#[test_case(Rule::FitsOnOneLine, Path::new("D200.py"))]
|
||||
#[test_case(Rule::NoBlankLineAfterFunction, Path::new("D202.py"))]
|
||||
#[test_case(Rule::BlankLineBeforeClass, Path::new("D.py"))]
|
||||
#[test_case(Rule::NoBlankLineBeforeFunction, Path::new("D.py"))]
|
||||
|
||||
@@ -74,7 +74,8 @@ pub(crate) fn one_liner(checker: &mut Checker, docstring: &Docstring) {
|
||||
// characters, avoid applying the fix.
|
||||
let body = docstring.body();
|
||||
let trimmed = body.trim();
|
||||
if !trimmed.ends_with(trailing.chars().last().unwrap())
|
||||
if trimmed.chars().rev().take_while(|c| *c == '\\').count() % 2 == 0
|
||||
&& !trimmed.ends_with(trailing.chars().last().unwrap())
|
||||
&& !trimmed.starts_with(leading.chars().last().unwrap())
|
||||
{
|
||||
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pydocstyle/mod.rs
|
||||
---
|
||||
D200.py:2:5: D200 One-line docstring should fit on one line
|
||||
|
|
||||
1 | def func():
|
||||
2 | """\
|
||||
| _____^
|
||||
3 | | """
|
||||
| |_______^ D200
|
||||
|
|
||||
= help: Reformat to one line
|
||||
|
||||
D200.py:7:5: D200 [*] One-line docstring should fit on one line
|
||||
|
|
||||
6 | def func():
|
||||
7 | """\\
|
||||
| _____^
|
||||
8 | | """
|
||||
| |_______^ D200
|
||||
|
|
||||
= help: Reformat to one line
|
||||
|
||||
ℹ Suggested fix
|
||||
4 4 |
|
||||
5 5 |
|
||||
6 6 | def func():
|
||||
7 |- """\\
|
||||
8 |- """
|
||||
7 |+ """\\"""
|
||||
9 8 |
|
||||
10 9 |
|
||||
11 10 | def func():
|
||||
|
||||
D200.py:12:5: D200 One-line docstring should fit on one line
|
||||
|
|
||||
11 | def func():
|
||||
12 | """\ \
|
||||
| _____^
|
||||
13 | | """
|
||||
| |_______^ D200
|
||||
|
|
||||
= help: Reformat to one line
|
||||
|
||||
|
||||
@@ -1,93 +1,88 @@
|
||||
use anyhow::{Context, Ok, Result};
|
||||
use ruff_python_ast::{Expr, Ranged};
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::{self as ast, Ranged};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_semantic::Binding;
|
||||
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
use crate::autofix::codemods::CodegenStylist;
|
||||
use crate::cst::matchers::{match_call_mut, match_dict, match_expression};
|
||||
use crate::cst::matchers::{match_call_mut, match_dict, transform_expression};
|
||||
|
||||
/// Generate a [`Edit`] to remove unused keys from format dict.
|
||||
pub(super) fn remove_unused_format_arguments_from_dict(
|
||||
unused_arguments: &[usize],
|
||||
stmt: &Expr,
|
||||
dict: &ast::ExprDict,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
) -> Result<Edit> {
|
||||
let module_text = locator.slice(stmt.range());
|
||||
let mut tree = match_expression(module_text)?;
|
||||
let dict = match_dict(&mut tree)?;
|
||||
let source_code = locator.slice(dict.range());
|
||||
transform_expression(source_code, stylist, |mut expression| {
|
||||
let dict = match_dict(&mut expression)?;
|
||||
|
||||
// Remove the elements at the given indexes.
|
||||
let mut index = 0;
|
||||
dict.elements.retain(|_| {
|
||||
let is_unused = unused_arguments.contains(&index);
|
||||
index += 1;
|
||||
!is_unused
|
||||
});
|
||||
// Remove the elements at the given indexes.
|
||||
let mut index = 0;
|
||||
dict.elements.retain(|_| {
|
||||
let is_unused = unused_arguments.contains(&index);
|
||||
index += 1;
|
||||
!is_unused
|
||||
});
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
tree.codegen_stylist(stylist),
|
||||
stmt.range(),
|
||||
))
|
||||
Ok(expression)
|
||||
})
|
||||
.map(|output| Edit::range_replacement(output, dict.range()))
|
||||
}
|
||||
|
||||
/// Generate a [`Edit`] to remove unused keyword arguments from a `format` call.
|
||||
pub(super) fn remove_unused_keyword_arguments_from_format_call(
|
||||
unused_arguments: &[usize],
|
||||
location: TextRange,
|
||||
call: &ast::ExprCall,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
) -> Result<Edit> {
|
||||
let module_text = locator.slice(location);
|
||||
let mut tree = match_expression(module_text)?;
|
||||
let call = match_call_mut(&mut tree)?;
|
||||
let source_code = locator.slice(call.range());
|
||||
transform_expression(source_code, stylist, |mut expression| {
|
||||
let call = match_call_mut(&mut expression)?;
|
||||
|
||||
// Remove the keyword arguments at the given indexes.
|
||||
let mut index = 0;
|
||||
call.args.retain(|arg| {
|
||||
if arg.keyword.is_none() {
|
||||
return true;
|
||||
}
|
||||
// Remove the keyword arguments at the given indexes.
|
||||
let mut index = 0;
|
||||
call.args.retain(|arg| {
|
||||
if arg.keyword.is_none() {
|
||||
return true;
|
||||
}
|
||||
|
||||
let is_unused = unused_arguments.contains(&index);
|
||||
index += 1;
|
||||
!is_unused
|
||||
});
|
||||
let is_unused = unused_arguments.contains(&index);
|
||||
index += 1;
|
||||
!is_unused
|
||||
});
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
tree.codegen_stylist(stylist),
|
||||
location,
|
||||
))
|
||||
Ok(expression)
|
||||
})
|
||||
.map(|output| Edit::range_replacement(output, call.range()))
|
||||
}
|
||||
|
||||
/// Generate a [`Edit`] to remove unused positional arguments from a `format` call.
|
||||
pub(crate) fn remove_unused_positional_arguments_from_format_call(
|
||||
unused_arguments: &[usize],
|
||||
location: TextRange,
|
||||
call: &ast::ExprCall,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
) -> Result<Edit> {
|
||||
let module_text = locator.slice(location);
|
||||
let mut tree = match_expression(module_text)?;
|
||||
let call = match_call_mut(&mut tree)?;
|
||||
let source_code = locator.slice(call.range());
|
||||
transform_expression(source_code, stylist, |mut expression| {
|
||||
let call = match_call_mut(&mut expression)?;
|
||||
|
||||
// Remove any unused arguments.
|
||||
let mut index = 0;
|
||||
call.args.retain(|_| {
|
||||
let is_unused = unused_arguments.contains(&index);
|
||||
index += 1;
|
||||
!is_unused
|
||||
});
|
||||
// Remove any unused arguments.
|
||||
let mut index = 0;
|
||||
call.args.retain(|_| {
|
||||
let is_unused = unused_arguments.contains(&index);
|
||||
index += 1;
|
||||
!is_unused
|
||||
});
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
tree.codegen_stylist(stylist),
|
||||
location,
|
||||
))
|
||||
Ok(expression)
|
||||
})
|
||||
.map(|output| Edit::range_replacement(output, call.range()))
|
||||
}
|
||||
|
||||
/// Generate a [`Edit`] to remove the binding from an exception handler.
|
||||
|
||||
@@ -62,8 +62,7 @@ fn find_useless_f_strings<'a>(
|
||||
kind: StringKind::FString | StringKind::RawFString,
|
||||
..
|
||||
} => {
|
||||
let first_char =
|
||||
&locator.contents()[TextRange::at(range.start(), TextSize::from(1))];
|
||||
let first_char = locator.slice(TextRange::at(range.start(), TextSize::from(1)));
|
||||
// f"..." => f_position = 0
|
||||
// fr"..." => f_position = 0
|
||||
// rf"..." => f_position = 1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::string::ToString;
|
||||
|
||||
use ruff_python_ast::{self as ast, Constant, Expr, Identifier, Keyword};
|
||||
use ruff_python_ast::{self as ast, Constant, Expr, Identifier, Keyword, Ranged};
|
||||
use ruff_text_size::TextRange;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
@@ -570,15 +570,16 @@ pub(crate) fn percent_format_extra_named_arguments(
|
||||
if summary.num_positional > 0 {
|
||||
return;
|
||||
}
|
||||
let Expr::Dict(ast::ExprDict { keys, .. }) = &right else {
|
||||
let Expr::Dict(dict) = &right else {
|
||||
return;
|
||||
};
|
||||
// If any of the keys are spread, abort.
|
||||
if keys.iter().any(Option::is_none) {
|
||||
if dict.keys.iter().any(Option::is_none) {
|
||||
return;
|
||||
}
|
||||
|
||||
let missing: Vec<(usize, &str)> = keys
|
||||
let missing: Vec<(usize, &str)> = dict
|
||||
.keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, key)| match key {
|
||||
@@ -613,7 +614,7 @@ pub(crate) fn percent_format_extra_named_arguments(
|
||||
diagnostic.try_set_fix(|| {
|
||||
let edit = remove_unused_format_arguments_from_dict(
|
||||
&indexes,
|
||||
right,
|
||||
dict,
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
)?;
|
||||
@@ -739,9 +740,9 @@ pub(crate) fn percent_format_star_requires_sequence(
|
||||
/// F522
|
||||
pub(crate) fn string_dot_format_extra_named_arguments(
|
||||
checker: &mut Checker,
|
||||
call: &ast::ExprCall,
|
||||
summary: &FormatSummary,
|
||||
keywords: &[Keyword],
|
||||
location: TextRange,
|
||||
) {
|
||||
// If there are any **kwargs, abort.
|
||||
if has_star_star_kwargs(keywords) {
|
||||
@@ -773,14 +774,14 @@ pub(crate) fn string_dot_format_extra_named_arguments(
|
||||
.collect();
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
StringDotFormatExtraNamedArguments { missing: names },
|
||||
location,
|
||||
call.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
let indexes: Vec<usize> = missing.iter().map(|(index, _)| *index).collect();
|
||||
diagnostic.try_set_fix(|| {
|
||||
let edit = remove_unused_keyword_arguments_from_format_call(
|
||||
&indexes,
|
||||
location,
|
||||
call,
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
)?;
|
||||
@@ -793,9 +794,9 @@ pub(crate) fn string_dot_format_extra_named_arguments(
|
||||
/// F523
|
||||
pub(crate) fn string_dot_format_extra_positional_arguments(
|
||||
checker: &mut Checker,
|
||||
call: &ast::ExprCall,
|
||||
summary: &FormatSummary,
|
||||
args: &[Expr],
|
||||
location: TextRange,
|
||||
) {
|
||||
let missing: Vec<usize> = args
|
||||
.iter()
|
||||
@@ -817,7 +818,7 @@ pub(crate) fn string_dot_format_extra_positional_arguments(
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<String>>(),
|
||||
},
|
||||
location,
|
||||
call.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
// We can only fix if the positional arguments we're removing don't require re-indexing
|
||||
@@ -849,7 +850,7 @@ pub(crate) fn string_dot_format_extra_positional_arguments(
|
||||
diagnostic.try_set_fix(|| {
|
||||
let edit = remove_unused_positional_arguments_from_format_call(
|
||||
&missing,
|
||||
location,
|
||||
call,
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
)?;
|
||||
@@ -863,10 +864,10 @@ pub(crate) fn string_dot_format_extra_positional_arguments(
|
||||
/// F524
|
||||
pub(crate) fn string_dot_format_missing_argument(
|
||||
checker: &mut Checker,
|
||||
call: &ast::ExprCall,
|
||||
summary: &FormatSummary,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
location: TextRange,
|
||||
) {
|
||||
if has_star_args(args) || has_star_star_kwargs(keywords) {
|
||||
return;
|
||||
@@ -898,7 +899,7 @@ pub(crate) fn string_dot_format_missing_argument(
|
||||
if !missing.is_empty() {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
StringDotFormatMissingArguments { missing },
|
||||
location,
|
||||
call.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -906,12 +907,13 @@ pub(crate) fn string_dot_format_missing_argument(
|
||||
/// F525
|
||||
pub(crate) fn string_dot_format_mixing_automatic(
|
||||
checker: &mut Checker,
|
||||
call: &ast::ExprCall,
|
||||
summary: &FormatSummary,
|
||||
location: TextRange,
|
||||
) {
|
||||
if !(summary.autos.is_empty() || summary.indices.is_empty()) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(StringDotFormatMixingAutomatic, location));
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
StringDotFormatMixingAutomatic,
|
||||
call.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -218,6 +217,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
|
||||
}
|
||||
|
||||
/// An unused import with its surrounding context.
|
||||
#[derive(Debug)]
|
||||
struct ImportBinding<'a> {
|
||||
/// The qualified name of the import (e.g., `typing.List` for `from typing import List`).
|
||||
import: AnyImport<'a>,
|
||||
@@ -234,13 +234,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()
|
||||
@@ -256,5 +252,7 @@ fn fix_imports(
|
||||
checker.stylist(),
|
||||
checker.indexer(),
|
||||
)?;
|
||||
Ok(Fix::automatic(edit).isolate(checker.parent_isolation()))
|
||||
Ok(Fix::automatic(edit).isolate(Checker::isolation(
|
||||
checker.semantic().parent_statement_id(node_id),
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use itertools::Itertools;
|
||||
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, IsolationLevel, Violation};
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::contains_effect;
|
||||
use ruff_python_ast::{self as ast, PySourceType, Ranged, Stmt};
|
||||
@@ -203,14 +203,10 @@ 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 isolation = checker
|
||||
.semantic()
|
||||
.parent_statement_id(statement_id)
|
||||
.map(|node_id| IsolationLevel::Group(node_id.into()))
|
||||
.unwrap_or_default();
|
||||
let node_id = binding.source?;
|
||||
let statement = checker.semantic().statement(node_id);
|
||||
let parent = checker.semantic().parent_statement(node_id);
|
||||
let isolation = Checker::isolation(checker.semantic().parent_statement_id(node_id));
|
||||
|
||||
// First case: simple assignment (`x = 1`)
|
||||
if let Stmt::Assign(ast::StmtAssign { targets, value, .. }) = statement {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user