Compare commits

..

1 Commits

Author SHA1 Message Date
Charlie Marsh
a0afde1a3f Avoid token clone in implicit string rule 2024-02-05 22:14:29 -05:00
167 changed files with 1772 additions and 6134 deletions

View File

@@ -117,7 +117,10 @@ jobs:
tool: cargo-insta
- uses: Swatinem/rust-cache@v2
- name: "Run tests"
run: cargo insta test --all --all-features --unreferenced reject
run: cargo insta test --all --exclude ruff_dev --all-features --unreferenced reject
- name: "Run dev tests"
# e.g. generating the schema — these should not run with all features enabled
run: cargo insta test -p ruff_dev --unreferenced reject
# Check for broken links in the documentation.
- run: cargo doc --all --no-deps
env:

47
Cargo.lock generated
View File

@@ -217,12 +217,12 @@ checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
[[package]]
name = "bstr"
version = "1.9.0"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc"
checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a"
dependencies = [
"memchr",
"regex-automata 0.4.3",
"regex-automata 0.3.9",
"serde",
]
@@ -1569,6 +1569,18 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa00462b37ead6d11a82c9d568b26682d78e0477dc02d1966c013af80969739"
[[package]]
name = "pep440_rs"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "887f66cc62717ea72caac4f1eb4e6f392224da3ffff3f40ec13ab427802746d6"
dependencies = [
"lazy_static",
"regex",
"serde",
"unicode-width",
]
[[package]]
name = "pep440_rs"
version = "0.4.0"
@@ -1583,12 +1595,12 @@ dependencies = [
[[package]]
name = "pep508_rs"
version = "0.3.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "910c513bea0f4f833122321c0f20e8c704e01de98692f6989c2ec21f43d88b1e"
checksum = "c0713d7bb861ca2b7d4c50a38e1f31a4b63a2e2df35ef1e5855cc29e108453e2"
dependencies = [
"once_cell",
"pep440_rs",
"pep440_rs 0.3.12",
"regex",
"serde",
"thiserror",
@@ -1768,12 +1780,12 @@ dependencies = [
[[package]]
name = "pyproject-toml"
version = "0.9.0"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95c3dd745f99aa3c554b7bb00859f7d18c2f1d6afd749ccc86d60b61e702abd9"
checksum = "ef61ae096a2f8c8b49eca360679dbc25f57c99145f6634b6bc18fedb1f9c6c30"
dependencies = [
"indexmap",
"pep440_rs",
"pep440_rs 0.4.0",
"pep508_rs",
"serde",
"toml",
@@ -1921,6 +1933,12 @@ dependencies = [
"regex-syntax 0.6.29",
]
[[package]]
name = "regex-automata"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9"
[[package]]
name = "regex-automata"
version = "0.4.3"
@@ -2171,7 +2189,7 @@ dependencies = [
"once_cell",
"path-absolutize",
"pathdiff",
"pep440_rs",
"pep440_rs 0.4.0",
"pretty_assertions",
"pyproject-toml",
"quick-junit",
@@ -2259,6 +2277,7 @@ dependencies = [
"rustc-hash",
"serde",
"smallvec",
"static_assertions",
]
[[package]]
@@ -2336,22 +2355,16 @@ version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.4.1",
"bstr",
"codspeed-criterion-compat",
"criterion",
"insta",
"is-macro",
"itertools 0.12.1",
"lalrpop",
"lalrpop-util",
"memchr",
"mimalloc",
"once_cell",
"ruff_python_ast",
"ruff_text_size",
"rustc-hash",
"static_assertions",
"tikv-jemallocator",
"tiny-keccak",
"unicode-ident",
"unicode_names2",
@@ -2481,7 +2494,7 @@ dependencies = [
"log",
"once_cell",
"path-absolutize",
"pep440_rs",
"pep440_rs 0.4.0",
"regex",
"ruff_cache",
"ruff_formatter",

View File

@@ -19,7 +19,6 @@ argfile = { version = "0.1.6" }
assert_cmd = { version = "2.0.13" }
bincode = { version = "1.3.3" }
bitflags = { version = "2.4.1" }
bstr = { version = "1.9.0" }
cachedir = { version = "0.3.1" }
chrono = { version = "0.4.33", default-features = false, features = ["clock"] }
clap = { version = "4.4.18", features = ["derive"] }
@@ -66,7 +65,7 @@ pathdiff = { version = "0.2.1" }
pep440_rs = { version = "0.4.0", features = ["serde"] }
pretty_assertions = "1.3.0"
proc-macro2 = { version = "1.0.78" }
pyproject-toml = { version = "0.9.0" }
pyproject-toml = { version = "0.8.2" }
quick-junit = { version = "0.3.5" }
quote = { version = "1.0.23" }
rand = { version = "0.8.5" }

View File

@@ -31,9 +31,9 @@ pub(crate) fn show_settings(
let settings = resolver.resolve(&path);
writeln!(writer, "Resolved settings for: \"{}\"", path.display())?;
writeln!(writer, "Resolved settings for: {path:?}")?;
if let Some(settings_path) = pyproject_config.path.as_ref() {
writeln!(writer, "Settings path: \"{}\"", settings_path.display())?;
writeln!(writer, "Settings path: {settings_path:?}")?;
}
write!(writer, "{settings}")?;

View File

@@ -4,29 +4,25 @@ use std::process::Command;
const BIN_NAME: &str = "ruff";
#[cfg(not(target_os = "windows"))]
const TEST_FILTERS: &[(&str, &str)] = &[
("\"[^\\*\"]*/pyproject.toml", "\"[BASEPATH]/pyproject.toml"),
("\".*/crates", "\"[BASEPATH]/crates"),
("\".*/\\.ruff_cache", "\"[BASEPATH]/.ruff_cache"),
("\".*/ruff\"", "\"[BASEPATH]\""),
];
#[cfg(target_os = "windows")]
const TEST_FILTERS: &[(&str, &str)] = &[
(r#""[^\*"]*\\pyproject.toml"#, "\"[BASEPATH]/pyproject.toml"),
(r#"".*\\crates"#, "\"[BASEPATH]/crates"),
(r#"".*\\\.ruff_cache"#, "\"[BASEPATH]/.ruff_cache"),
(r#"".*\\ruff""#, "\"[BASEPATH]\""),
(r#"\\+(\w\w|\s|")"#, "/$1"),
];
#[test]
fn display_default_settings() {
// Navigate from the crate directory to the workspace root.
let base_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap();
let base_path = base_path.to_string_lossy();
// Escape the backslashes for the regex.
let base_path = regex::escape(&base_path);
#[cfg(not(target_os = "windows"))]
let test_filters = &[(base_path.as_ref(), "[BASEPATH]")];
#[cfg(target_os = "windows")]
let test_filters = &[
(base_path.as_ref(), "[BASEPATH]"),
(r#"\\+(\w\w|\s|\.|")"#, "/$1"),
];
insta::with_settings!({ filters => test_filters.to_vec() }, {
insta::with_settings!({ filters => TEST_FILTERS.to_vec() }, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--show-settings", "unformatted.py"]).current_dir(Path::new("./resources/test/fixtures")));
});

View File

@@ -205,9 +205,7 @@ linter.external = []
linter.ignore_init_module_imports = false
linter.logger_objects = []
linter.namespace_packages = []
linter.src = [
"[BASEPATH]",
]
linter.src = ["[BASEPATH]"]
linter.tab_size = 4
linter.line_length = 88
linter.task_tags = [

View File

@@ -21,7 +21,8 @@ impl<'a> LineSuffixes<'a> {
/// Takes all the pending line suffixes.
pub(super) fn take_pending<'l>(
&'l mut self,
) -> impl DoubleEndedIterator<Item = LineSuffixEntry<'a>> + 'l + ExactSizeIterator {
) -> impl Iterator<Item = LineSuffixEntry<'a>> + DoubleEndedIterator + 'l + ExactSizeIterator
{
self.suffixes.drain(..)
}

View File

@@ -1,22 +0,0 @@
from django.utils.safestring import mark_safe
def some_func():
return mark_safe('<script>alert("evil!")</script>')
@mark_safe
def some_func():
return '<script>alert("evil!")</script>'
from django.utils.html import mark_safe
def some_func():
return mark_safe('<script>alert("evil!")</script>')
@mark_safe
def some_func():
return '<script>alert("evil!")</script>'

View File

@@ -1,27 +1,18 @@
import trio
async def func():
async def foo():
with trio.fail_after():
...
async def func():
async def foo():
with trio.fail_at():
await ...
async def func():
async def foo():
with trio.move_on_after():
...
async def func():
async def foo():
with trio.move_at():
await ...
async def func():
with trio.move_at():
async with trio.open_nursery() as nursery:
...

View File

@@ -19,11 +19,8 @@ numpy.random.seed()
numpy.random.get_state()
numpy.random.set_state()
numpy.random.rand()
numpy.random.ranf()
numpy.random.sample()
numpy.random.randn()
numpy.random.randint()
numpy.random.random()
numpy.random.random_integers()
numpy.random.random_sample()
numpy.random.choice()
@@ -38,6 +35,7 @@ numpy.random.exponential()
numpy.random.f()
numpy.random.gamma()
numpy.random.geometric()
numpy.random.get_state()
numpy.random.gumbel()
numpy.random.hypergeometric()
numpy.random.laplace()

View File

@@ -1,848 +0,0 @@
"""Fixtures for the errors E301, E302, E303, E304, E305 and E306.
Since these errors are about new lines, each test starts with either "No error" or "# E30X".
Each test's end is signaled by a "# end" line.
There should be no E30X error outside of a test's bound.
"""
# No error
class Class:
pass
# end
# No error
class Class:
"""Docstring"""
def __init__(self) -> None:
pass
# end
# No error
def func():
pass
# end
# No error
# comment
class Class:
pass
# end
# No error
# comment
def func():
pass
# end
# no error
def foo():
pass
def bar():
pass
class Foo(object):
pass
class Bar(object):
pass
# end
# No error
class Class(object):
def func1():
pass
def func2():
pass
# end
# No error
class Class(object):
def func1():
pass
# comment
def func2():
pass
# end
# No error
class Class:
def func1():
pass
# comment
def func2():
pass
# This is a
# ... multi-line comment
def func3():
pass
# This is a
# ... multi-line comment
@decorator
class Class:
def func1():
pass
# comment
def func2():
pass
@property
def func3():
pass
# end
# No error
try:
from nonexistent import Bar
except ImportError:
class Bar(object):
"""This is a Bar replacement"""
# end
# No error
def with_feature(f):
"""Some decorator"""
wrapper = f
if has_this_feature(f):
def wrapper(*args):
call_feature(args[0])
return f(*args)
return wrapper
# end
# No error
try:
next
except NameError:
def next(iterator, default):
for item in iterator:
return item
return default
# end
# No error
def fn():
pass
class Foo():
"""Class Foo"""
def fn():
pass
# end
# No error
# comment
def c():
pass
# comment
def d():
pass
# This is a
# ... multi-line comment
# And this one is
# ... a second paragraph
# ... which spans on 3 lines
# Function `e` is below
# NOTE: Hey this is a testcase
def e():
pass
def fn():
print()
# comment
print()
print()
# Comment 1
# Comment 2
# Comment 3
def fn2():
pass
# end
# no error
if __name__ == '__main__':
foo()
# end
# no error
defaults = {}
defaults.update({})
# end
# no error
def foo(x):
classification = x
definitely = not classification
# end
# no error
def bar(): pass
def baz(): pass
# end
# no error
def foo():
def bar(): pass
def baz(): pass
# end
# no error
from typing import overload
from typing import Union
# end
# no error
@overload
def f(x: int) -> int: ...
@overload
def f(x: str) -> str: ...
# end
# no error
def f(x: Union[int, str]) -> Union[int, str]:
return x
# end
# no error
from typing import Protocol
class C(Protocol):
@property
def f(self) -> int: ...
@property
def g(self) -> str: ...
# end
# no error
def f(
a,
):
pass
# end
# no error
if True:
class Class:
"""Docstring"""
def function(self):
...
# end
# no error
if True:
def function(self):
...
# end
# no error
@decorator
# comment
@decorator
def function():
pass
# end
# no error
class Class:
def method(self):
if True:
def function():
pass
# end
# no error
@decorator
async def function(data: None) -> None:
...
# end
# no error
class Class:
def method():
"""docstring"""
# comment
def function():
pass
# end
# no error
try:
if True:
# comment
class Class:
pass
except:
pass
# end
# no error
def f():
def f():
pass
# end
# no error
class MyClass:
# comment
def method(self) -> None:
pass
# end
# no error
def function1():
# Comment
def function2():
pass
# end
# no error
async def function1():
await function2()
async with function3():
pass
# end
# no error
if (
cond1
and cond2
):
pass
#end
# no error
async def function1():
await function2()
async with function3():
pass
# end
# no error
async def function1():
await function2()
async with function3():
pass
# end
# no error
async def function1():
await function2()
async with function3():
pass
# end
# no error
class Test:
async
def a(self): pass
# end
# no error
class Test:
def a():
pass
# wrongly indented comment
def b():
pass
# end
# no error
def test():
pass
# Wrongly indented comment
pass
# end
# E301
class Class(object):
def func1():
pass
def func2():
pass
# end
# E301
class Class:
def fn1():
pass
# comment
def fn2():
pass
# end
# E302
"""Main module."""
def fn():
pass
# end
# E302
import sys
def get_sys_path():
return sys.path
# end
# E302
def a():
pass
def b():
pass
# end
# E302
def a():
pass
# comment
def b():
pass
# end
# E302
def a():
pass
async def b():
pass
# end
# E302
async def x():
pass
async def x(y: int = 1):
pass
# end
# E302
def bar():
pass
def baz(): pass
# end
# E302
def bar(): pass
def baz():
pass
# end
# E302
def f():
pass
# comment
@decorator
def g():
pass
# end
# E302
class Test:
pass
def method1():
return 1
def method2():
return 22
# end
# E303
def fn():
_ = None
# arbitrary comment
def inner(): # E306 not expected (pycodestyle detects E306)
pass
# end
# E303
def fn():
_ = None
# arbitrary comment
def inner(): # E306 not expected (pycodestyle detects E306)
pass
# end
# E303
print()
print()
# end
# E303:5:1
print()
# comment
print()
# end
# E303:5:5 E303:8:5
def a():
print()
# comment
# another comment
print()
# end
# E303
#!python
"""This class docstring comes on line 5.
It gives error E303: too many blank lines (3)
"""
# end
# E303
class Class:
def a(self):
pass
def b(self):
pass
# end
# E303
if True:
a = 1
a = 2
# end
# E303
class Test:
# comment
# another comment
def test(self): pass
# end
# E303
class Test:
def a(self):
pass
# wrongly indented comment
def b(self):
pass
# end
# E303
def fn():
pass
pass
# end
# E304
@decorator
def function():
pass
# end
# E304
@decorator
# comment E304 not expected
def function():
pass
# end
# E304
@decorator
# comment E304 not expected
# second comment E304 not expected
def function():
pass
# end
# E305:7:1
def fn():
print()
# comment
# another comment
fn()
# end
# E305
class Class():
pass
# comment
# another comment
a = 1
# end
# E305:8:1
def fn():
print()
# comment
# another comment
try:
fn()
except Exception:
pass
# end
# E305:5:1
def a():
print()
# Two spaces before comments, too.
if a():
a()
# end
#: E305:8:1
# Example from https://github.com/PyCQA/pycodestyle/issues/400
import stuff
def main():
blah, blah
if __name__ == '__main__':
main()
# end
# E306:3:5
def a():
x = 1
def b():
pass
# end
#: E306:3:5
async def a():
x = 1
def b():
pass
# end
#: E306:3:5 E306:5:9
def a():
x = 2
def b():
x = 1
def c():
pass
# end
# E306:3:5 E306:6:5
def a():
x = 1
class C:
pass
x = 2
def b():
pass
# end
# E306
def foo():
def bar():
pass
def baz(): pass
# end
# E306:3:5
def foo():
def bar(): pass
def baz():
pass
# end
# E306
def a():
x = 2
@decorator
def b():
pass
# end
# E306
def a():
x = 2
@decorator
async def b():
pass
# end
# E306
def a():
x = 2
async def b():
pass
# end

View File

@@ -562,46 +562,3 @@ def titlecase_sub_section_header():
Returns:
"""
def test_method_should_be_correctly_capitalized(parameters: list[str], other_parameters: dict[str, str]): # noqa: D213
"""Test parameters and attributes sections are capitalized correctly.
Parameters
----------
parameters:
A list of string parameters
other_parameters:
A dictionary of string attributes
Other Parameters
----------
other_parameters:
A dictionary of string attributes
parameters:
A list of string parameters
"""
def test_lowercase_sub_section_header_should_be_valid(parameters: list[str], value: int): # noqa: D213
"""Test that lower case subsection header is valid even if it has the same name as section kind.
Parameters:
----------
parameters:
A list of string parameters
value:
Some value
"""
def test_lowercase_sub_section_header_different_kind(returns: int):
"""Test that lower case subsection header is valid even if it is of a different kind.
Parameters
------------------
returns:
some value
"""

View File

@@ -46,8 +46,3 @@ x: typing.TypeAlias = list[T]
# OK
x: TypeAlias
x: int = 1
# Ensure that "T" appears only once in the type parameters for the modernized
# type alias.
T = typing.TypeVar["T"]
Decorator: TypeAlias = typing.Callable[[T], T]

View File

@@ -0,0 +1,85 @@
val = 2
def simple_cases():
a = 4
b = "{a}" # RUF027
c = "{a} {b} f'{val}' " # RUF027
def escaped_string():
a = 4
b = "escaped string: {{ brackets surround me }}" # RUF027
def raw_string():
a = 4
b = r"raw string with formatting: {a}" # RUF027
c = r"raw string with \backslashes\ and \"escaped quotes\": {a}" # RUF027
def print_name(name: str):
a = 4
print("Hello, {name}!") # RUF027
print("The test value we're using today is {a}") # RUF027
def do_nothing(a):
return a
def nested_funcs():
a = 4
print(do_nothing(do_nothing("{a}"))) # RUF027
def tripled_quoted():
a = 4
c = a
single_line = """ {a} """ # RUF027
# RUF027
multi_line = a = """b { # comment
c} d
"""
def single_quoted_multi_line():
a = 4
# RUF027
b = " {\
a} \
"
def implicit_concat():
a = 4
b = "{a}" "+" "{b}" r" \\ " # RUF027 for the first part only
print(f"{a}" "{a}" f"{b}") # RUF027
def escaped_chars():
a = 4
b = "\"not escaped:\" \'{a}\' \"escaped:\": \'{{c}}\'" # RUF027
def alternative_formatter(src, **kwargs):
src.format(**kwargs)
def format2(src, *args):
pass
# These should not cause an RUF027 message
def negative_cases():
a = 4
positive = False
"""{a}"""
"don't format: {a}"
c = """ {b} """
d = "bad variable: {invalid}"
e = "incorrect syntax: {}"
json = "{ positive: false }"
json2 = "{ 'positive': false }"
json3 = "{ 'positive': 'false' }"
alternative_formatter("{a}", a = 5)
formatted = "{a}".fmt(a = 7)
print(do_nothing("{a}".format(a=3)))
print(do_nothing(alternative_formatter("{a}", a = 5)))
print(format(do_nothing("{a}"), a = 5))
print("{a}".to_upper())
print(do_nothing("{a}").format(a = "Test"))
print(do_nothing("{a}").format2(a))
a = 4
"always ignore this: {a}"
print("but don't ignore this: {val}") # RUF027

View File

@@ -1,70 +0,0 @@
val = 2
"always ignore this: {val}"
print("but don't ignore this: {val}") # RUF027
def simple_cases():
a = 4
b = "{a}" # RUF027
c = "{a} {b} f'{val}' " # RUF027
def escaped_string():
a = 4
b = "escaped string: {{ brackets surround me }}" # RUF027
def raw_string():
a = 4
b = r"raw string with formatting: {a}" # RUF027
c = r"raw string with \backslashes\ and \"escaped quotes\": {a}" # RUF027
def print_name(name: str):
a = 4
print("Hello, {name}!") # RUF027
print("The test value we're using today is {a}") # RUF027
def nested_funcs():
a = 4
print(do_nothing(do_nothing("{a}"))) # RUF027
def tripled_quoted():
a = 4
c = a
single_line = """ {a} """ # RUF027
# RUF027
multi_line = a = """b { # comment
c} d
"""
def single_quoted_multi_line():
a = 4
# RUF027
b = " {\
a} \
"
def implicit_concat():
a = 4
b = "{a}" "+" "{b}" r" \\ " # RUF027 for the first part only
print(f"{a}" "{a}" f"{b}") # RUF027
def escaped_chars():
a = 4
b = "\"not escaped:\" '{a}' \"escaped:\": '{{c}}'" # RUF027
def method_calls():
value = {}
value.method = print_name
first = "Wendy"
last = "Appleseed"
value.method("{first} {last}") # RUF027

View File

@@ -1,36 +0,0 @@
def do_nothing(a):
return a
def alternative_formatter(src, **kwargs):
src.format(**kwargs)
def format2(src, *args):
pass
# These should not cause an RUF027 message
def negative_cases():
a = 4
positive = False
"""{a}"""
"don't format: {a}"
c = """ {b} """
d = "bad variable: {invalid}"
e = "incorrect syntax: {}"
f = "uses a builtin: {max}"
json = "{ positive: false }"
json2 = "{ 'positive': false }"
json3 = "{ 'positive': 'false' }"
alternative_formatter("{a}", a=5)
formatted = "{a}".fmt(a=7)
print(do_nothing("{a}".format(a=3)))
print(do_nothing(alternative_formatter("{a}", a=5)))
print(format(do_nothing("{a}"), a=5))
print("{a}".to_upper())
print(do_nothing("{a}").format(a="Test"))
print(do_nothing("{a}").format2(a))
print(("{a}" "{c}").format(a=1, c=2))
print("{a}".attribute.chaining.call(a=2))
print("{a} {c}".format(a))

View File

@@ -247,11 +247,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::HardcodedPasswordDefault) {
flake8_bandit::rules::hardcoded_password_default(checker, parameters);
}
if checker.enabled(Rule::SuspiciousMarkSafeUsage) {
for decorator in decorator_list {
flake8_bandit::rules::suspicious_function_decorator(checker, decorator);
}
}
if checker.enabled(Rule::PropertyWithParameters) {
pylint::rules::property_with_parameters(checker, stmt, decorator_list, parameters);
}

View File

@@ -31,8 +31,8 @@ use std::path::Path;
use itertools::Itertools;
use log::debug;
use ruff_python_ast::{
self as ast, Comprehension, ElifElseClause, ExceptHandler, Expr, ExprContext, Keyword,
MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Stmt, Suite, UnaryOp,
self as ast, Arguments, Comprehension, ElifElseClause, ExceptHandler, Expr, ExprContext,
Keyword, MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Stmt, Suite, UnaryOp,
};
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -989,7 +989,12 @@ where
}
Expr::Call(ast::ExprCall {
func,
arguments,
arguments:
Arguments {
args,
keywords,
range: _,
},
range: _,
}) => {
self.visit_expr(func);
@@ -1032,7 +1037,7 @@ where
});
match callable {
Some(typing::Callable::Bool) => {
let mut args = arguments.args.iter();
let mut args = args.iter();
if let Some(arg) = args.next() {
self.visit_boolean_test(arg);
}
@@ -1041,7 +1046,7 @@ where
}
}
Some(typing::Callable::Cast) => {
let mut args = arguments.args.iter();
let mut args = args.iter();
if let Some(arg) = args.next() {
self.visit_type_definition(arg);
}
@@ -1050,7 +1055,7 @@ where
}
}
Some(typing::Callable::NewType) => {
let mut args = arguments.args.iter();
let mut args = args.iter();
if let Some(arg) = args.next() {
self.visit_non_type_definition(arg);
}
@@ -1059,21 +1064,21 @@ where
}
}
Some(typing::Callable::TypeVar) => {
let mut args = arguments.args.iter();
let mut args = args.iter();
if let Some(arg) = args.next() {
self.visit_non_type_definition(arg);
}
for arg in args {
self.visit_type_definition(arg);
}
for keyword in arguments.keywords.iter() {
for keyword in keywords {
let Keyword {
arg,
value,
range: _,
} = keyword;
if let Some(id) = arg {
if id.as_str() == "bound" {
if id == "bound" {
self.visit_type_definition(value);
} else {
self.visit_non_type_definition(value);
@@ -1083,7 +1088,7 @@ where
}
Some(typing::Callable::NamedTuple) => {
// Ex) NamedTuple("a", [("a", int)])
let mut args = arguments.args.iter();
let mut args = args.iter();
if let Some(arg) = args.next() {
self.visit_non_type_definition(arg);
}
@@ -1112,7 +1117,7 @@ where
}
}
for keyword in arguments.keywords.iter() {
for keyword in keywords {
let Keyword { arg, value, .. } = keyword;
match (arg.as_ref(), value) {
// Ex) NamedTuple("a", **{"a": int})
@@ -1139,7 +1144,7 @@ where
}
Some(typing::Callable::TypedDict) => {
// Ex) TypedDict("a", {"a": int})
let mut args = arguments.args.iter();
let mut args = args.iter();
if let Some(arg) = args.next() {
self.visit_non_type_definition(arg);
}
@@ -1162,13 +1167,13 @@ where
}
// Ex) TypedDict("a", a=int)
for keyword in arguments.keywords.iter() {
for keyword in keywords {
let Keyword { value, .. } = keyword;
self.visit_type_definition(value);
}
}
Some(typing::Callable::MypyExtension) => {
let mut args = arguments.args.iter();
let mut args = args.iter();
if let Some(arg) = args.next() {
// Ex) DefaultNamedArg(bool | None, name="some_prop_name")
self.visit_type_definition(arg);
@@ -1176,13 +1181,13 @@ where
for arg in args {
self.visit_non_type_definition(arg);
}
for keyword in arguments.keywords.iter() {
for keyword in keywords {
let Keyword { value, .. } = keyword;
self.visit_non_type_definition(value);
}
} else {
// Ex) DefaultNamedArg(type="bool", name="some_prop_name")
for keyword in arguments.keywords.iter() {
for keyword in keywords {
let Keyword {
value,
arg,
@@ -1200,10 +1205,10 @@ where
// If we're in a type definition, we need to treat the arguments to any
// other callables as non-type definitions (i.e., we don't want to treat
// any strings as deferred type definitions).
for arg in arguments.args.iter() {
for arg in args {
self.visit_non_type_definition(arg);
}
for keyword in arguments.keywords.iter() {
for keyword in keywords {
let Keyword { value, .. } = keyword;
self.visit_non_type_definition(value);
}

View File

@@ -1,4 +1,3 @@
use crate::line_width::IndentWidth;
use ruff_diagnostics::Diagnostic;
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
@@ -16,11 +15,11 @@ use crate::rules::pycodestyle::rules::logical_lines::{
use crate::settings::LinterSettings;
/// Return the amount of indentation, expanding tabs to the next multiple of the settings' tab size.
pub(crate) fn expand_indent(line: &str, indent_width: IndentWidth) -> usize {
fn expand_indent(line: &str, settings: &LinterSettings) -> usize {
let line = line.trim_end_matches(['\n', '\r']);
let mut indent = 0;
let tab_size = indent_width.as_usize();
let tab_size = settings.tab_size.as_usize();
for c in line.bytes() {
match c {
b'\t' => indent = (indent / tab_size) * tab_size + tab_size,
@@ -86,7 +85,7 @@ pub(crate) fn check_logical_lines(
TextRange::new(locator.line_start(first_token.start()), first_token.start())
};
let indent_level = expand_indent(locator.slice(range), settings.tab_size);
let indent_level = expand_indent(locator.slice(range), settings);
let indent_size = 4;

View File

@@ -4,7 +4,6 @@ use std::path::Path;
use ruff_notebook::CellOffsets;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
@@ -15,7 +14,6 @@ use ruff_source_file::Locator;
use crate::directives::TodoComment;
use crate::lex::docstring_detection::StateMachine;
use crate::registry::{AsRule, Rule};
use crate::rules::pycodestyle::rules::BlankLinesChecker;
use crate::rules::ruff::rules::Context;
use crate::rules::{
eradicate, flake8_commas, flake8_executable, flake8_fixme, flake8_implicit_str_concat,
@@ -23,37 +21,17 @@ use crate::rules::{
};
use crate::settings::LinterSettings;
#[allow(clippy::too_many_arguments)]
pub(crate) fn check_tokens(
tokens: &[LexResult],
path: &Path,
locator: &Locator,
indexer: &Indexer,
stylist: &Stylist,
settings: &LinterSettings,
source_type: PySourceType,
cell_offsets: Option<&CellOffsets>,
) -> Vec<Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = vec![];
if settings.rules.any_enabled(&[
Rule::BlankLineBetweenMethods,
Rule::BlankLinesTopLevel,
Rule::TooManyBlankLines,
Rule::BlankLineAfterDecorator,
Rule::BlankLinesAfterFunctionOrClass,
Rule::BlankLinesBeforeNestedDefinition,
]) {
let mut blank_lines_checker = BlankLinesChecker::default();
blank_lines_checker.check_lines(
tokens,
locator,
stylist,
settings.tab_size,
&mut diagnostics,
);
}
if settings.rules.enabled(Rule::BlanketNOQA) {
pygrep_hooks::rules::blanket_noqa(&mut diagnostics, indexer, locator);
}
@@ -117,7 +95,7 @@ pub(crate) fn check_tokens(
}
if settings.rules.enabled(Rule::TabIndentation) {
pycodestyle::rules::tab_indentation(&mut diagnostics, locator, indexer);
pycodestyle::rules::tab_indentation(&mut diagnostics, tokens, locator, indexer);
}
if settings.rules.any_enabled(&[

View File

@@ -137,12 +137,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pycodestyle, "E274") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabBeforeKeyword),
#[allow(deprecated)]
(Pycodestyle, "E275") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAfterKeyword),
(Pycodestyle, "E301") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLineBetweenMethods),
(Pycodestyle, "E302") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLinesTopLevel),
(Pycodestyle, "E303") => (RuleGroup::Preview, rules::pycodestyle::rules::TooManyBlankLines),
(Pycodestyle, "E304") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLineAfterDecorator),
(Pycodestyle, "E305") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLinesAfterFunctionOrClass),
(Pycodestyle, "E306") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLinesBeforeNestedDefinition),
(Pycodestyle, "E401") => (RuleGroup::Stable, rules::pycodestyle::rules::MultipleImportsOnOneLine),
(Pycodestyle, "E402") => (RuleGroup::Stable, rules::pycodestyle::rules::ModuleImportNotAtTopOfFile),
(Pycodestyle, "E501") => (RuleGroup::Stable, rules::pycodestyle::rules::LineTooLong),

View File

@@ -5,7 +5,7 @@ use ruff_python_ast::docstrings::{leading_space, leading_words};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use strum_macros::EnumIter;
use ruff_source_file::{Line, NewlineWithTrailingNewline, UniversalNewlines};
use ruff_source_file::{Line, UniversalNewlineIterator, UniversalNewlines};
use crate::docstrings::styles::SectionStyle;
use crate::docstrings::{Docstring, DocstringBody};
@@ -130,34 +130,6 @@ impl SectionKind {
Self::Yields => "Yields",
}
}
/// Returns `true` if a section can contain subsections, as in:
/// ```python
/// Yields
/// ------
/// int
/// Description of the anonymous integer return value.
/// ```
///
/// For NumPy, see: <https://numpydoc.readthedocs.io/en/latest/format.html>
///
/// For Google, see: <https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings>
pub(crate) fn has_subsections(self) -> bool {
matches!(
self,
Self::Args
| Self::Arguments
| Self::OtherArgs
| Self::OtherParameters
| Self::OtherParams
| Self::Parameters
| Self::Raises
| Self::Returns
| Self::SeeAlso
| Self::Warns
| Self::Yields
)
}
}
pub(crate) struct SectionContexts<'a> {
@@ -384,16 +356,13 @@ impl<'a> SectionContext<'a> {
pub(crate) fn previous_line(&self) -> Option<&'a str> {
let previous =
&self.docstring_body.as_str()[TextRange::up_to(self.range_relative().start())];
previous
.universal_newlines()
.last()
.map(|line| line.as_str())
previous.universal_newlines().last().map(|l| l.as_str())
}
/// Returns the lines belonging to this section after the summary line.
pub(crate) fn following_lines(&self) -> NewlineWithTrailingNewline<'a> {
pub(crate) fn following_lines(&self) -> UniversalNewlineIterator<'a> {
let lines = self.following_lines_str();
NewlineWithTrailingNewline::with_offset(lines, self.offset() + self.data.summary_full_end)
UniversalNewlineIterator::with_offset(lines, self.offset() + self.data.summary_full_end)
}
fn following_lines_str(&self) -> &'a str {
@@ -490,54 +459,13 @@ fn is_docstring_section(
// args: The arguments to the function.
// """
// ```
// Or `parameters` in:
// ```python
// def func(parameters: tuple[int]):
// """Toggle the gizmo.
//
// Parameters:
// -----
// parameters:
// The arguments to the function.
// """
// ```
// However, if the header is an _exact_ match (like `Returns:`, as opposed to `returns:`), then
// continue to treat it as a section header.
if section_kind.has_subsections() {
if let Some(previous_section) = previous_section {
if let Some(previous_section) = previous_section {
if previous_section.indent_size < indent_size {
let verbatim = &line[TextRange::at(indent_size, section_name_size)];
// If the section is more deeply indented, assume it's a subsection, as in:
// ```python
// def func(args: tuple[int]):
// """Toggle the gizmo.
//
// Args:
// args: The arguments to the function.
// """
// ```
if previous_section.indent_size < indent_size {
if section_kind.as_str() != verbatim {
return false;
}
}
// If the section isn't underlined, and isn't title-cased, assume it's a subsection,
// as in:
// ```python
// def func(parameters: tuple[int]):
// """Toggle the gizmo.
//
// Parameters:
// -----
// parameters:
// The arguments to the function.
// """
// ```
if !next_line_is_underline && verbatim.chars().next().is_some_and(char::is_lowercase) {
if section_kind.as_str() != verbatim {
return false;
}
if section_kind.as_str() != verbatim {
return false;
}
}
}

View File

@@ -109,7 +109,6 @@ pub fn check_path(
path,
locator,
indexer,
stylist,
settings,
source_type,
source_kind.as_ipy_notebook().map(Notebook::cell_offsets),

View File

@@ -264,11 +264,6 @@ impl Rule {
| Rule::BadQuotesMultilineString
| Rule::BlanketNOQA
| Rule::BlanketTypeIgnore
| Rule::BlankLineAfterDecorator
| Rule::BlankLineBetweenMethods
| Rule::BlankLinesAfterFunctionOrClass
| Rule::BlankLinesBeforeNestedDefinition
| Rule::BlankLinesTopLevel
| Rule::CommentedOutCode
| Rule::EmptyComment
| Rule::ExtraneousParentheses
@@ -301,7 +296,6 @@ impl Rule {
| Rule::ShebangNotFirstLine
| Rule::SingleLineImplicitStringConcatenation
| Rule::TabIndentation
| Rule::TooManyBlankLines
| Rule::TrailingCommaOnBareTuple
| Rule::TypeCommentInStub
| Rule::UselessSemicolon

View File

@@ -321,16 +321,6 @@ mod schema {
true
}
})
.filter(|_rule| {
// Filter out all test-only rules
#[cfg(feature = "test-rules")]
#[allow(clippy::used_underscore_binding)]
if _rule.starts_with("RUF9") {
return false;
}
true
})
.sorted()
.map(Value::String)
.collect(),

View File

@@ -46,7 +46,6 @@ mod tests {
#[test_case(Rule::SubprocessWithoutShellEqualsTrue, Path::new("S603.py"))]
#[test_case(Rule::SuspiciousPickleUsage, Path::new("S301.py"))]
#[test_case(Rule::SuspiciousEvalUsage, Path::new("S307.py"))]
#[test_case(Rule::SuspiciousMarkSafeUsage, Path::new("S308.py"))]
#[test_case(Rule::SuspiciousURLOpenUsage, Path::new("S310.py"))]
#[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))]
#[test_case(Rule::SuspiciousTelnetlibImport, Path::new("S401.py"))]

View File

@@ -40,9 +40,7 @@ impl Violation for HardcodedBindAllInterfaces {
pub(crate) fn hardcoded_bind_all_interfaces(checker: &mut Checker, string: StringLike) {
let is_bind_all_interface = match string {
StringLike::StringLiteral(ast::ExprStringLiteral { value, .. }) => value == "0.0.0.0",
StringLike::FStringLiteral(ast::FStringLiteralElement { value, .. }) => {
&**value == "0.0.0.0"
}
StringLike::FStringLiteral(ast::FStringLiteralElement { value, .. }) => value == "0.0.0.0",
StringLike::BytesLiteral(_) => return,
};

View File

@@ -3,7 +3,7 @@
//! See: <https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html>
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Decorator, Expr, ExprCall};
use ruff_python_ast::{self as ast, Expr, ExprCall};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -848,7 +848,7 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
// Eval
["" | "builtins", "eval"] => Some(SuspiciousEvalUsage.into()),
// MarkSafe
["django", "utils", "safestring" | "html", "mark_safe"] => Some(SuspiciousMarkSafeUsage.into()),
["django", "utils", "safestring", "mark_safe"] => Some(SuspiciousMarkSafeUsage.into()),
// URLOpen (`urlopen`, `urlretrieve`, `Request`)
["urllib", "request", "urlopen" | "urlretrieve" | "Request"] |
["six", "moves", "urllib", "request", "urlopen" | "urlretrieve" | "Request"] => {
@@ -901,27 +901,3 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
checker.diagnostics.push(diagnostic);
}
}
/// S308
pub(crate) fn suspicious_function_decorator(checker: &mut Checker, decorator: &Decorator) {
let Some(diagnostic_kind) = checker
.semantic()
.resolve_call_path(&decorator.expression)
.and_then(|call_path| {
match call_path.as_slice() {
// MarkSafe
["django", "utils", "safestring" | "html", "mark_safe"] => {
Some(SuspiciousMarkSafeUsage.into())
}
_ => None,
}
})
else {
return;
};
let diagnostic = Diagnostic::new::<DiagnosticKind>(diagnostic_kind, decorator.range());
if checker.enabled(diagnostic.kind.rule()) {
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -1,34 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S308.py:5:12: S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
4 | def some_func():
5 | return mark_safe('<script>alert("evil!")</script>')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S308
|
S308.py:8:1: S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
8 | @mark_safe
| ^^^^^^^^^^ S308
9 | def some_func():
10 | return '<script>alert("evil!")</script>'
|
S308.py:17:12: S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
16 | def some_func():
17 | return mark_safe('<script>alert("evil!")</script>')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S308
|
S308.py:20:1: S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
20 | @mark_safe
| ^^^^^^^^^^ S308
21 | def some_func():
22 | return '<script>alert("evil!")</script>'
|

View File

@@ -59,11 +59,11 @@ fn assertion_error(msg: Option<&Expr>) -> Stmt {
})),
arguments: Arguments {
args: if let Some(msg) = msg {
Box::from([msg.clone()])
vec![msg.clone()]
} else {
Box::from([])
vec![]
},
keywords: Box::from([]),
keywords: vec![],
range: TextRange::default(),
},
range: TextRange::default(),

View File

@@ -91,7 +91,7 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem])
return;
}
let [arg] = &*arguments.args else {
let [arg] = arguments.args.as_slice() else {
return;
};

View File

@@ -3,7 +3,7 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::Node;
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Comprehension, Expr, ExprContext, Stmt};
use ruff_python_ast::{self as ast, Arguments, Comprehension, Expr, ExprContext, Stmt};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -126,13 +126,18 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
match expr {
Expr::Call(ast::ExprCall {
func,
arguments,
arguments:
Arguments {
args,
keywords,
range: _,
},
range: _,
}) => {
match func.as_ref() {
Expr::Name(ast::ExprName { id, .. }) => {
if matches!(id.as_str(), "filter" | "reduce" | "map") {
for arg in arguments.args.iter() {
for arg in args {
if arg.is_lambda_expr() {
self.safe_functions.push(arg);
}
@@ -143,7 +148,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
if attr == "reduce" {
if let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() {
if id == "functools" {
for arg in arguments.args.iter() {
for arg in args {
if arg.is_lambda_expr() {
self.safe_functions.push(arg);
}
@@ -155,7 +160,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
_ => {}
}
for keyword in arguments.keywords.iter() {
for keyword in keywords {
if keyword.arg.as_ref().is_some_and(|arg| arg == "key")
&& keyword.value.is_lambda_expr()
{

View File

@@ -114,7 +114,7 @@ fn is_infinite_iterator(arg: &Expr, semantic: &SemanticModel) -> bool {
}
// Ex) `iterools.repeat(1, times=None)`
for keyword in keywords.iter() {
for keyword in keywords {
if keyword.arg.as_ref().is_some_and(|name| name == "times") {
if keyword.value.is_none_literal_expr() {
return true;

View File

@@ -1,8 +1,10 @@
use itertools::Itertools;
use ruff_diagnostics::{AlwaysFixableViolation, Violation};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_index::Indexer;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::lexer::{LexResult, Spanned};
use ruff_python_parser::Tok;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange};
@@ -10,20 +12,20 @@ use ruff_text_size::{Ranged, TextRange};
/// Simplified token type.
#[derive(Copy, Clone, PartialEq, Eq)]
enum TokenType {
Named,
String,
Newline,
NonLogicalNewline,
OpeningBracket,
ClosingBracket,
OpeningSquareBracket,
Colon,
Comma,
OpeningCurlyBracket,
Def,
For,
Lambda,
Irrelevant,
NonLogicalNewline,
Newline,
Comma,
OpeningBracket,
OpeningSquareBracket,
OpeningCurlyBracket,
ClosingBracket,
For,
Named,
Def,
Lambda,
Colon,
String,
}
/// Simplified token specialized for the task.
@@ -52,30 +54,30 @@ impl Token {
}
}
impl From<(&Tok, TextRange)> for Token {
fn from((tok, range): (&Tok, TextRange)) -> Self {
let r#type = match tok {
Tok::Name { .. } => TokenType::Named,
Tok::String { .. } => TokenType::String,
Tok::Newline => TokenType::Newline,
impl From<&Spanned> for Token {
fn from(spanned: &Spanned) -> Self {
let r#type = match &spanned.0 {
Tok::NonLogicalNewline => TokenType::NonLogicalNewline,
Tok::Lpar => TokenType::OpeningBracket,
Tok::Rpar => TokenType::ClosingBracket,
Tok::Lsqb => TokenType::OpeningSquareBracket,
Tok::Rsqb => TokenType::ClosingBracket,
Tok::Colon => TokenType::Colon,
Tok::Comma => TokenType::Comma,
Tok::Lbrace => TokenType::OpeningCurlyBracket,
Tok::Rbrace => TokenType::ClosingBracket,
Tok::Def => TokenType::Def,
Tok::Newline => TokenType::Newline,
Tok::For => TokenType::For,
Tok::Def => TokenType::Def,
Tok::Lambda => TokenType::Lambda,
// Import treated like a function.
Tok::Import => TokenType::Named,
Tok::Name { .. } => TokenType::Named,
Tok::String { .. } => TokenType::String,
Tok::Comma => TokenType::Comma,
Tok::Lpar => TokenType::OpeningBracket,
Tok::Lsqb => TokenType::OpeningSquareBracket,
Tok::Lbrace => TokenType::OpeningCurlyBracket,
Tok::Rpar | Tok::Rsqb | Tok::Rbrace => TokenType::ClosingBracket,
Tok::Colon => TokenType::Colon,
_ => TokenType::Irrelevant,
};
#[allow(clippy::inconsistent_struct_constructor)]
Self { range, r#type }
Self {
range: spanned.1,
r#type,
}
}
}
@@ -235,12 +237,10 @@ pub(crate) fn trailing_commas(
indexer: &Indexer,
) {
let mut fstrings = 0u32;
let tokens = tokens.iter().filter_map(|result| {
let Ok((tok, tok_range)) = result else {
return None;
};
match tok {
let tokens = tokens
.iter()
.flatten()
.filter_map(|spanned @ (tok, tok_range)| match tok {
// Completely ignore comments -- they just interfere with the logic.
Tok::Comment(_) => None,
// F-strings are handled as `String` token type with the complete range
@@ -263,30 +263,69 @@ pub(crate) fn trailing_commas(
}
_ => {
if fstrings == 0 {
Some(Token::from((tok, *tok_range)))
Some(Token::from(spanned))
} else {
None
}
}
});
let tokens = [Token::irrelevant(), Token::irrelevant()]
.into_iter()
.chain(tokens);
// Collapse consecutive newlines to the first one -- trailing commas are
// added before the first newline.
let tokens = tokens.coalesce(|previous, current| {
if previous.r#type == TokenType::NonLogicalNewline
&& current.r#type == TokenType::NonLogicalNewline
{
Ok(previous)
} else {
Err((previous, current))
}
});
let mut prev = Token::irrelevant();
let mut prev_prev = Token::irrelevant();
// The current nesting of the comma contexts.
let mut stack = vec![Context::new(ContextType::No)];
for token in tokens {
if prev.r#type == TokenType::NonLogicalNewline
&& token.r#type == TokenType::NonLogicalNewline
{
// Collapse consecutive newlines to the first one -- trailing commas are
// added before the first newline.
continue;
}
for (prev_prev, prev, token) in tokens.tuple_windows() {
// Update the comma context stack.
let context = update_context(token, prev, prev_prev, &mut stack);
match token.r#type {
TokenType::OpeningBracket => match (prev.r#type, prev_prev.r#type) {
(TokenType::Named, TokenType::Def) => {
stack.push(Context::new(ContextType::FunctionParameters));
}
(TokenType::Named | TokenType::ClosingBracket, _) => {
stack.push(Context::new(ContextType::CallArguments));
}
_ => {
stack.push(Context::new(ContextType::Tuple));
}
},
TokenType::OpeningSquareBracket => match prev.r#type {
TokenType::ClosingBracket | TokenType::Named | TokenType::String => {
stack.push(Context::new(ContextType::Subscript));
}
_ => {
stack.push(Context::new(ContextType::List));
}
},
TokenType::OpeningCurlyBracket => {
stack.push(Context::new(ContextType::Dict));
}
TokenType::Lambda => {
stack.push(Context::new(ContextType::LambdaParameters));
}
TokenType::For => {
let len = stack.len();
stack[len - 1] = Context::new(ContextType::No);
}
TokenType::Comma => {
let len = stack.len();
stack[len - 1].inc();
}
_ => {}
}
let context = &stack[stack.len() - 1];
// Is it allowed to have a trailing comma before this token?
let comma_allowed = token.r#type == TokenType::ClosingBracket
@@ -373,47 +412,5 @@ pub(crate) fn trailing_commas(
if pop_context && stack.len() > 1 {
stack.pop();
}
prev_prev = prev;
prev = token;
}
}
fn update_context(
token: Token,
prev: Token,
prev_prev: Token,
stack: &mut Vec<Context>,
) -> Context {
let new_context = match token.r#type {
TokenType::OpeningBracket => match (prev.r#type, prev_prev.r#type) {
(TokenType::Named, TokenType::Def) => Context::new(ContextType::FunctionParameters),
(TokenType::Named | TokenType::ClosingBracket, _) => {
Context::new(ContextType::CallArguments)
}
_ => Context::new(ContextType::Tuple),
},
TokenType::OpeningSquareBracket => match prev.r#type {
TokenType::ClosingBracket | TokenType::Named | TokenType::String => {
Context::new(ContextType::Subscript)
}
_ => Context::new(ContextType::List),
},
TokenType::OpeningCurlyBracket => Context::new(ContextType::Dict),
TokenType::Lambda => Context::new(ContextType::LambdaParameters),
TokenType::For => {
let last = stack.last_mut().expect("Stack to never be empty");
*last = Context::new(ContextType::No);
return *last;
}
TokenType::Comma => {
let last = stack.last_mut().expect("Stack to never be empty");
last.inc();
return *last;
}
_ => return stack.last().copied().expect("Stack to never be empty"),
};
stack.push(new_context);
new_context
}

View File

@@ -88,7 +88,7 @@ fn is_nullable_field<'a>(value: &'a Expr, semantic: &'a SemanticModel) -> Option
let mut null_key = false;
let mut blank_key = false;
let mut unique_key = false;
for keyword in call.arguments.keywords.iter() {
for keyword in &call.arguments.keywords {
let Some(argument) = &keyword.arg else {
continue;
};

View File

@@ -5,7 +5,7 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::str::{leading_quote, trailing_quote};
use ruff_python_index::Indexer;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_python_parser::TokenKind;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange};
@@ -101,28 +101,37 @@ pub(crate) fn implicit(
for ((a_tok, a_range), (b_tok, b_range)) in tokens
.iter()
.flatten()
.filter(|(tok, _)| {
!tok.is_comment()
&& (settings.flake8_implicit_str_concat.allow_multiline
|| !tok.is_non_logical_newline())
.filter_map(|(tok, range)| {
// Ignore comments.
if tok.is_comment() {
return None;
}
// Ignore non-localized newlines.
if !settings.flake8_implicit_str_concat.allow_multiline && tok.is_non_logical_newline()
{
return None;
}
Some((TokenKind::from(tok), *range))
})
.tuple_windows()
{
let (a_range, b_range) = match (a_tok, b_tok) {
(Tok::String { .. }, Tok::String { .. }) => (*a_range, *b_range),
(Tok::String { .. }, Tok::FStringStart) => {
(TokenKind::String, TokenKind::String) => (a_range, b_range),
(TokenKind::String, TokenKind::FStringStart) => {
match indexer.fstring_ranges().innermost(b_range.start()) {
Some(b_range) => (*a_range, b_range),
Some(b_range) => (a_range, b_range),
None => continue,
}
}
(Tok::FStringEnd, Tok::String { .. }) => {
(TokenKind::FStringEnd, TokenKind::String { .. }) => {
match indexer.fstring_ranges().innermost(a_range.start()) {
Some(a_range) => (a_range, *b_range),
Some(a_range) => (a_range, b_range),
None => continue,
}
}
(Tok::FStringEnd, Tok::FStringStart) => {
(TokenKind::FStringEnd, TokenKind::FStringStart) => {
match (
indexer.fstring_ranges().innermost(a_range.start()),
indexer.fstring_ranges().innermost(b_range.start()),

View File

@@ -113,7 +113,7 @@ fn check_log_record_attr_clash(checker: &mut Checker, extra: &Keyword) {
.resolve_call_path(func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["", "dict"]))
{
for keyword in keywords.iter() {
for keyword in keywords {
if let Some(attr) = &keyword.arg {
if is_reserved_attr(attr) {
checker.diagnostics.push(Diagnostic::new(

View File

@@ -97,7 +97,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
continue;
}
let [arg] = &**args else {
let [arg] = args.as_slice() else {
continue;
};
@@ -188,8 +188,8 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
let node3 = Expr::Call(ast::ExprCall {
func: Box::new(node2),
arguments: Arguments {
args: Box::from([node]),
keywords: Box::from([]),
args: vec![node],
keywords: vec![],
range: TextRange::default(),
},
range: TextRange::default(),

View File

@@ -59,7 +59,7 @@ impl Violation for UnnecessaryDictKwargs {
/// PIE804
pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, call: &ast::ExprCall) {
let mut duplicate_keywords = None;
for keyword in call.arguments.keywords.iter() {
for keyword in &call.arguments.keywords {
// keyword is a spread operator (indicated by None).
if keyword.arg.is_some() {
continue;
@@ -145,7 +145,7 @@ fn duplicates(call: &ast::ExprCall) -> FxHashSet<&str> {
call.arguments.keywords.len(),
BuildHasherDefault::default(),
);
for keyword in call.arguments.keywords.iter() {
for keyword in &call.arguments.keywords {
if let Some(name) = &keyword.arg {
if !seen.insert(name.as_str()) {
duplicates.insert(name.as_str());

View File

@@ -60,7 +60,7 @@ pub(crate) fn unnecessary_range_start(checker: &mut Checker, call: &ast::ExprCal
}
// Verify that the call has exactly two arguments (no `step`).
let [start, _] = &*call.arguments.args else {
let [start, _] = call.arguments.args.as_slice() else {
return;
};

View File

@@ -69,7 +69,7 @@ pub(crate) fn bad_version_info_comparison(checker: &mut Checker, test: &Expr) {
return;
};
let ([op], [_right]) = (&**ops, &**comparators) else {
let ([op], [_right]) = (ops.as_slice(), comparators.as_slice()) else {
return;
};

View File

@@ -101,7 +101,7 @@ pub(crate) fn unrecognized_platform(checker: &mut Checker, test: &Expr) {
return;
};
let ([op], [right]) = (&**ops, &**comparators) else {
let ([op], [right]) = (ops.as_slice(), comparators.as_slice()) else {
return;
};

View File

@@ -129,7 +129,7 @@ pub(crate) fn unrecognized_version_info(checker: &mut Checker, test: &Expr) {
return;
};
let ([op], [comparator]) = (&**ops, &**comparators) else {
let ([op], [comparator]) = (ops.as_slice(), comparators.as_slice()) else {
return;
};

View File

@@ -411,7 +411,7 @@ fn to_pytest_raises_args<'a>(
) -> Option<Cow<'a, str>> {
let args = match attr {
"assertRaises" | "failUnlessRaises" => {
match (&*arguments.args, &*arguments.keywords) {
match (arguments.args.as_slice(), arguments.keywords.as_slice()) {
// Ex) `assertRaises(Exception)`
([arg], []) => Cow::Borrowed(checker.locator().slice(arg)),
// Ex) `assertRaises(expected_exception=Exception)`
@@ -427,7 +427,7 @@ fn to_pytest_raises_args<'a>(
}
}
"assertRaisesRegex" | "assertRaisesRegexp" => {
match (&*arguments.args, &*arguments.keywords) {
match (arguments.args.as_slice(), arguments.keywords.as_slice()) {
// Ex) `assertRaisesRegex(Exception, regex)`
([arg1, arg2], []) => Cow::Owned(format!(
"{}, match={}",

View File

@@ -257,18 +257,15 @@ fn elts_to_csv(elts: &[Expr], generator: Generator) -> Option<String> {
}
let node = Expr::from(ast::StringLiteral {
value: elts
.iter()
.fold(String::new(), |mut acc, elt| {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = elt {
if !acc.is_empty() {
acc.push(',');
}
acc.push_str(value.to_str());
value: elts.iter().fold(String::new(), |mut acc, elt| {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = elt {
if !acc.is_empty() {
acc.push(',');
}
acc
})
.into_boxed_str(),
acc.push_str(value.to_str());
}
acc
}),
..ast::StringLiteral::default()
});
Some(generator.expr(&node))
@@ -330,7 +327,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
.iter()
.map(|name| {
Expr::from(ast::StringLiteral {
value: (*name).to_string().into_boxed_str(),
value: (*name).to_string(),
..ast::StringLiteral::default()
})
})
@@ -363,7 +360,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
.iter()
.map(|name| {
Expr::from(ast::StringLiteral {
value: (*name).to_string().into_boxed_str(),
value: (*name).to_string(),
..ast::StringLiteral::default()
})
})
@@ -638,17 +635,17 @@ pub(crate) fn parametrize(checker: &mut Checker, decorators: &[Decorator]) {
}) = &decorator.expression
{
if checker.enabled(Rule::PytestParametrizeNamesWrongType) {
if let [names, ..] = &**args {
if let [names, ..] = args.as_slice() {
check_names(checker, decorator, names);
}
}
if checker.enabled(Rule::PytestParametrizeValuesWrongType) {
if let [names, values, ..] = &**args {
if let [names, values, ..] = args.as_slice() {
check_values(checker, names, values);
}
}
if checker.enabled(Rule::PytestDuplicateParametrizeTestCases) {
if let [_, values, ..] = &**args {
if let [_, values, ..] = args.as_slice() {
check_duplicates(checker, values);
}
}

View File

@@ -173,8 +173,8 @@ fn assert(expr: &Expr, msg: Option<&Expr>) -> Stmt {
fn compare(left: &Expr, cmp_op: CmpOp, right: &Expr) -> Expr {
Expr::Compare(ast::ExprCompare {
left: Box::new(left.clone()),
ops: Box::from([cmp_op]),
comparators: Box::from([right.clone()]),
ops: vec![cmp_op],
comparators: vec![right.clone()],
range: TextRange::default(),
})
}
@@ -390,8 +390,8 @@ impl UnittestAssert {
let node1 = ast::ExprCall {
func: Box::new(node.into()),
arguments: Arguments {
args: Box::from([(**obj).clone(), (**cls).clone()]),
keywords: Box::from([]),
args: vec![(**obj).clone(), (**cls).clone()],
keywords: vec![],
range: TextRange::default(),
},
range: TextRange::default(),
@@ -434,8 +434,8 @@ impl UnittestAssert {
let node2 = ast::ExprCall {
func: Box::new(node1.into()),
arguments: Arguments {
args: Box::from([(**regex).clone(), (**text).clone()]),
keywords: Box::from([]),
args: vec![(**regex).clone(), (**text).clone()],
keywords: vec![],
range: TextRange::default(),
},
range: TextRange::default(),

View File

@@ -437,8 +437,8 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) {
let node2 = ast::ExprCall {
func: Box::new(node1.into()),
arguments: Arguments {
args: Box::from([target.clone(), node.into()]),
keywords: Box::from([]),
args: vec![target.clone(), node.into()],
keywords: vec![],
range: TextRange::default(),
},
range: TextRange::default(),
@@ -480,13 +480,13 @@ fn match_eq_target(expr: &Expr) -> Option<(&str, &Expr)> {
else {
return None;
};
if **ops != [CmpOp::Eq] {
if ops != &[CmpOp::Eq] {
return None;
}
let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() else {
return None;
};
let [comparator] = &**comparators else {
let [comparator] = comparators.as_slice() else {
return None;
};
if !comparator.is_name_expr() {
@@ -551,8 +551,8 @@ pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) {
};
let node2 = ast::ExprCompare {
left: Box::new(node1.into()),
ops: Box::from([CmpOp::In]),
comparators: Box::from([node.into()]),
ops: vec![CmpOp::In],
comparators: vec![node.into()],
range: TextRange::default(),
};
let in_expr = node2.into();

View File

@@ -217,7 +217,7 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) {
slice.range(),
);
let node = ast::StringLiteral {
value: capital_env_var.into_boxed_str(),
value: capital_env_var,
unicode: env_var.is_unicode(),
..ast::StringLiteral::default()
};

View File

@@ -185,8 +185,8 @@ pub(crate) fn if_expr_with_true_false(
.into(),
),
arguments: Arguments {
args: Box::from([test.clone()]),
keywords: Box::from([]),
args: vec![test.clone()],
keywords: vec![],
range: TextRange::default(),
},
range: TextRange::default(),

View File

@@ -176,7 +176,7 @@ pub(crate) fn negation_with_equal_op(
);
let node = ast::ExprCompare {
left: left.clone(),
ops: Box::from([CmpOp::NotEq]),
ops: vec![CmpOp::NotEq],
comparators: comparators.clone(),
range: TextRange::default(),
};
@@ -206,7 +206,7 @@ pub(crate) fn negation_with_not_equal_op(
else {
return;
};
if !matches!(&**ops, [CmpOp::NotEq]) {
if !matches!(&ops[..], [CmpOp::NotEq]) {
return;
}
if is_exception_check(checker.semantic().current_statement()) {
@@ -231,7 +231,7 @@ pub(crate) fn negation_with_not_equal_op(
);
let node = ast::ExprCompare {
left: left.clone(),
ops: Box::from([CmpOp::Eq]),
ops: vec![CmpOp::Eq],
comparators: comparators.clone(),
range: TextRange::default(),
};
@@ -279,8 +279,8 @@ pub(crate) fn double_negation(checker: &mut Checker, expr: &Expr, op: UnaryOp, o
let node1 = ast::ExprCall {
func: Box::new(node.into()),
arguments: Arguments {
args: Box::from([*operand.clone()]),
keywords: Box::from([]),
args: vec![*operand.clone()],
keywords: vec![],
range: TextRange::default(),
},
range: TextRange::default(),

View File

@@ -253,7 +253,8 @@ fn is_main_check(expr: &Expr) -> bool {
{
if let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() {
if id == "__name__" {
if let [Expr::StringLiteral(ast::ExprStringLiteral { value, .. })] = &**comparators
if let [Expr::StringLiteral(ast::ExprStringLiteral { value, .. })] =
comparators.as_slice()
{
if value == "__main__" {
return true;

View File

@@ -122,7 +122,7 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &mut Checker, stmt_if:
else {
return;
};
let [test_dict] = &**test_dict else {
let [test_dict] = test_dict.as_slice() else {
return;
};
let (expected_var, expected_value, default_var, default_value) = match ops[..] {
@@ -176,8 +176,8 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &mut Checker, stmt_if:
let node3 = ast::ExprCall {
func: Box::new(node2.into()),
arguments: Arguments {
args: Box::from([node1, node]),
keywords: Box::from([]),
args: vec![node1, node],
keywords: vec![],
range: TextRange::default(),
},
range: TextRange::default(),
@@ -233,11 +233,11 @@ pub(crate) fn if_exp_instead_of_dict_get(
else {
return;
};
let [test_dict] = &**test_dict else {
let [test_dict] = test_dict.as_slice() else {
return;
};
let (body, default_value) = match &**ops {
let (body, default_value) = match ops.as_slice() {
[CmpOp::In] => (body, orelse),
[CmpOp::NotIn] => (orelse, body),
_ => {
@@ -276,8 +276,8 @@ pub(crate) fn if_exp_instead_of_dict_get(
let fixed_node = ast::ExprCall {
func: Box::new(dict_get_node.into()),
arguments: Arguments {
args: Box::from([dict_key_node, default_value_node]),
keywords: Box::from([]),
args: vec![dict_key_node, default_value_node],
keywords: vec![],
range: TextRange::default(),
},
range: TextRange::default(),

View File

@@ -64,10 +64,10 @@ pub(crate) fn if_else_block_instead_of_dict_lookup(checker: &mut Checker, stmt_i
let Expr::Name(ast::ExprName { id: target, .. }) = left.as_ref() else {
return;
};
if **ops != [CmpOp::Eq] {
if ops != &[CmpOp::Eq] {
return;
}
let [expr] = &**comparators else {
let [expr] = comparators.as_slice() else {
return;
};
let Some(literal_expr) = expr.as_literal_expr() else {
@@ -127,10 +127,10 @@ pub(crate) fn if_else_block_instead_of_dict_lookup(checker: &mut Checker, stmt_i
let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() else {
return;
};
if id != target || **ops != [CmpOp::Eq] {
if id != target || ops != &[CmpOp::Eq] {
return;
}
let [expr] = &**comparators else {
let [expr] = comparators.as_slice() else {
return;
};
let Some(literal_expr) = expr.as_literal_expr() else {

View File

@@ -194,7 +194,7 @@ pub(crate) fn key_in_dict_comprehension(checker: &mut Checker, comprehension: &C
/// SIM118 in a comparison.
pub(crate) fn key_in_dict_compare(checker: &mut Checker, compare: &ast::ExprCompare) {
let [op] = &*compare.ops else {
let [op] = compare.ops.as_slice() else {
return;
};
@@ -202,7 +202,7 @@ pub(crate) fn key_in_dict_compare(checker: &mut Checker, compare: &ast::ExprComp
return;
}
let [right] = &*compare.comparators else {
let [right] = compare.comparators.as_slice() else {
return;
};

View File

@@ -161,8 +161,8 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt_if: &ast::StmtIf) {
let value_node = ast::ExprCall {
func: Box::new(func_node.into()),
arguments: Arguments {
args: Box::from([if_test.clone()]),
keywords: Box::from([]),
args: vec![if_test.clone()],
keywords: vec![],
range: TextRange::default(),
},
range: TextRange::default(),

View File

@@ -140,7 +140,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt) {
range: _,
}) = &loop_.test
{
if let ([op], [comparator]) = (&**ops, &**comparators) {
if let ([op], [comparator]) = (ops.as_slice(), comparators.as_slice()) {
let op = match op {
CmpOp::Eq => CmpOp::NotEq,
CmpOp::NotEq => CmpOp::Eq,
@@ -155,8 +155,8 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt) {
};
let node = ast::ExprCompare {
left: left.clone(),
ops: Box::from([op]),
comparators: Box::from([comparator.clone()]),
ops: vec![op],
comparators: vec![comparator.clone()],
range: TextRange::default(),
};
node.into()
@@ -391,8 +391,8 @@ fn return_stmt(id: &str, test: &Expr, target: &Expr, iter: &Expr, generator: Gen
let node2 = ast::ExprCall {
func: Box::new(node1.into()),
arguments: Arguments {
args: Box::from([node.into()]),
keywords: Box::from([]),
args: vec![node.into()],
keywords: vec![],
range: TextRange::default(),
},
range: TextRange::default(),

View File

@@ -3,20 +3,24 @@ source: crates/ruff_linter/src/rules/flake8_trio/mod.rs
---
TRIO100.py:5:5: TRIO100 A `with trio.fail_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
4 | async def func():
4 | async def foo():
5 | with trio.fail_after():
| _____^
6 | | ...
| |___________^ TRIO100
7 |
8 | async def foo():
|
TRIO100.py:15:5: TRIO100 A `with trio.move_on_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
TRIO100.py:13:5: TRIO100 A `with trio.move_on_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
14 | async def func():
15 | with trio.move_on_after():
12 | async def foo():
13 | with trio.move_on_after():
| _____^
16 | | ...
14 | | ...
| |___________^ TRIO100
15 |
16 | async def foo():
|

View File

@@ -1,6 +1,7 @@
use ruff_python_ast::{self as ast, Arguments, Expr, ExprCall};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, ExprCall};
use crate::checkers::ast::Checker;
@@ -52,15 +53,19 @@ pub(crate) fn path_constructor_current_directory(checker: &mut Checker, expr: &E
return;
}
let Expr::Call(ExprCall { arguments, .. }) = expr else {
let Expr::Call(ExprCall {
arguments: Arguments { args, keywords, .. },
..
}) = expr
else {
return;
};
if !arguments.keywords.is_empty() {
if !keywords.is_empty() {
return;
}
let [Expr::StringLiteral(ast::ExprStringLiteral { value, range })] = &*arguments.args else {
let [Expr::StringLiteral(ast::ExprStringLiteral { value, range })] = args.as_slice() else {
return;
};

View File

@@ -610,7 +610,7 @@ impl Violation for OsPathIsfile {
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.is_symlink()` can improve readability over the `os`
/// methods such as `Path.is_link()` can improve readability over the `os`
/// module's counterparts (e.g., `os.path.islink()`).
///
/// Note that `os` functions may be preferable if performance is a concern,
@@ -627,11 +627,11 @@ impl Violation for OsPathIsfile {
/// ```python
/// from pathlib import Path
///
/// Path("docs").is_symlink()
/// Path("docs").is_link()
/// ```
///
/// ## References
/// - [Python documentation: `Path.is_symlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_symlink)
/// - [Python documentation: `Path.is_link`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_link)
/// - [Python documentation: `os.path.islink`](https://docs.python.org/3/library/os.path.html#os.path.islink)
/// - [PEP 428](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)

View File

@@ -15,7 +15,7 @@ fn to_f_string_expression_element(inner: &Expr) -> ast::FStringElement {
/// Convert a string to a [`ast::FStringElement::Literal`].
pub(super) fn to_f_string_literal_element(s: &str) -> ast::FStringElement {
ast::FStringElement::Literal(ast::FStringLiteralElement {
value: s.to_string().into_boxed_str(),
value: s.to_owned(),
range: TextRange::default(),
})
}
@@ -53,7 +53,7 @@ pub(super) fn to_f_string_element(expr: &Expr) -> Option<ast::FStringElement> {
match expr {
Expr::StringLiteral(ast::ExprStringLiteral { value, range }) => {
Some(ast::FStringElement::Literal(ast::FStringLiteralElement {
value: value.to_string().into_boxed_str(),
value: value.to_string(),
range: *range,
}))
}

View File

@@ -72,8 +72,7 @@ fn build_fstring(joiner: &str, joinees: &[Expr]) -> Option<Expr> {
None
}
})
.join(joiner)
.into_boxed_str(),
.join(joiner),
..ast::StringLiteral::default()
};
return Some(node.into());
@@ -116,7 +115,7 @@ pub(crate) fn static_join_to_fstring(checker: &mut Checker, expr: &Expr, joiner:
if !keywords.is_empty() {
return;
}
let [arg] = &**args else {
let [arg] = args.as_slice() else {
return;
};

View File

@@ -80,11 +80,8 @@ pub(crate) fn legacy_random(checker: &mut Checker, expr: &Expr) {
"set_state" |
// Simple random data
"rand" |
"ranf" |
"sample" |
"randn" |
"randint" |
"random" |
"random_integers" |
"random_sample" |
"choice" |

View File

@@ -45,7 +45,7 @@ NPY002.py:20:1: NPY002 Replace legacy `np.random.set_state` call with `np.random
20 | numpy.random.set_state()
| ^^^^^^^^^^^^^^^^^^^^^^ NPY002
21 | numpy.random.rand()
22 | numpy.random.ranf()
22 | numpy.random.randn()
|
NPY002.py:21:1: NPY002 Replace legacy `np.random.rand` call with `np.random.Generator`
@@ -54,464 +54,444 @@ NPY002.py:21:1: NPY002 Replace legacy `np.random.rand` call with `np.random.Gene
20 | numpy.random.set_state()
21 | numpy.random.rand()
| ^^^^^^^^^^^^^^^^^ NPY002
22 | numpy.random.ranf()
23 | numpy.random.sample()
22 | numpy.random.randn()
23 | numpy.random.randint()
|
NPY002.py:22:1: NPY002 Replace legacy `np.random.ranf` call with `np.random.Generator`
NPY002.py:22:1: NPY002 Replace legacy `np.random.randn` call with `np.random.Generator`
|
20 | numpy.random.set_state()
21 | numpy.random.rand()
22 | numpy.random.ranf()
| ^^^^^^^^^^^^^^^^^ NPY002
23 | numpy.random.sample()
24 | numpy.random.randn()
22 | numpy.random.randn()
| ^^^^^^^^^^^^^^^^^^ NPY002
23 | numpy.random.randint()
24 | numpy.random.random_integers()
|
NPY002.py:23:1: NPY002 Replace legacy `np.random.sample` call with `np.random.Generator`
NPY002.py:23:1: NPY002 Replace legacy `np.random.randint` call with `np.random.Generator`
|
21 | numpy.random.rand()
22 | numpy.random.ranf()
23 | numpy.random.sample()
| ^^^^^^^^^^^^^^^^^^^ NPY002
24 | numpy.random.randn()
25 | numpy.random.randint()
|
NPY002.py:24:1: NPY002 Replace legacy `np.random.randn` call with `np.random.Generator`
|
22 | numpy.random.ranf()
23 | numpy.random.sample()
24 | numpy.random.randn()
| ^^^^^^^^^^^^^^^^^^ NPY002
25 | numpy.random.randint()
26 | numpy.random.random()
|
NPY002.py:25:1: NPY002 Replace legacy `np.random.randint` call with `np.random.Generator`
|
23 | numpy.random.sample()
24 | numpy.random.randn()
25 | numpy.random.randint()
22 | numpy.random.randn()
23 | numpy.random.randint()
| ^^^^^^^^^^^^^^^^^^^^ NPY002
26 | numpy.random.random()
27 | numpy.random.random_integers()
24 | numpy.random.random_integers()
25 | numpy.random.random_sample()
|
NPY002.py:26:1: NPY002 Replace legacy `np.random.random` call with `np.random.Generator`
NPY002.py:24:1: NPY002 Replace legacy `np.random.random_integers` call with `np.random.Generator`
|
24 | numpy.random.randn()
25 | numpy.random.randint()
26 | numpy.random.random()
| ^^^^^^^^^^^^^^^^^^^ NPY002
27 | numpy.random.random_integers()
28 | numpy.random.random_sample()
|
NPY002.py:27:1: NPY002 Replace legacy `np.random.random_integers` call with `np.random.Generator`
|
25 | numpy.random.randint()
26 | numpy.random.random()
27 | numpy.random.random_integers()
22 | numpy.random.randn()
23 | numpy.random.randint()
24 | numpy.random.random_integers()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
28 | numpy.random.random_sample()
29 | numpy.random.choice()
25 | numpy.random.random_sample()
26 | numpy.random.choice()
|
NPY002.py:28:1: NPY002 Replace legacy `np.random.random_sample` call with `np.random.Generator`
NPY002.py:25:1: NPY002 Replace legacy `np.random.random_sample` call with `np.random.Generator`
|
26 | numpy.random.random()
27 | numpy.random.random_integers()
28 | numpy.random.random_sample()
23 | numpy.random.randint()
24 | numpy.random.random_integers()
25 | numpy.random.random_sample()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
29 | numpy.random.choice()
30 | numpy.random.bytes()
26 | numpy.random.choice()
27 | numpy.random.bytes()
|
NPY002.py:29:1: NPY002 Replace legacy `np.random.choice` call with `np.random.Generator`
NPY002.py:26:1: NPY002 Replace legacy `np.random.choice` call with `np.random.Generator`
|
27 | numpy.random.random_integers()
28 | numpy.random.random_sample()
29 | numpy.random.choice()
24 | numpy.random.random_integers()
25 | numpy.random.random_sample()
26 | numpy.random.choice()
| ^^^^^^^^^^^^^^^^^^^ NPY002
30 | numpy.random.bytes()
31 | numpy.random.shuffle()
27 | numpy.random.bytes()
28 | numpy.random.shuffle()
|
NPY002.py:30:1: NPY002 Replace legacy `np.random.bytes` call with `np.random.Generator`
NPY002.py:27:1: NPY002 Replace legacy `np.random.bytes` call with `np.random.Generator`
|
28 | numpy.random.random_sample()
29 | numpy.random.choice()
30 | numpy.random.bytes()
25 | numpy.random.random_sample()
26 | numpy.random.choice()
27 | numpy.random.bytes()
| ^^^^^^^^^^^^^^^^^^ NPY002
31 | numpy.random.shuffle()
32 | numpy.random.permutation()
28 | numpy.random.shuffle()
29 | numpy.random.permutation()
|
NPY002.py:31:1: NPY002 Replace legacy `np.random.shuffle` call with `np.random.Generator`
NPY002.py:28:1: NPY002 Replace legacy `np.random.shuffle` call with `np.random.Generator`
|
29 | numpy.random.choice()
30 | numpy.random.bytes()
31 | numpy.random.shuffle()
26 | numpy.random.choice()
27 | numpy.random.bytes()
28 | numpy.random.shuffle()
| ^^^^^^^^^^^^^^^^^^^^ NPY002
32 | numpy.random.permutation()
33 | numpy.random.beta()
29 | numpy.random.permutation()
30 | numpy.random.beta()
|
NPY002.py:32:1: NPY002 Replace legacy `np.random.permutation` call with `np.random.Generator`
NPY002.py:29:1: NPY002 Replace legacy `np.random.permutation` call with `np.random.Generator`
|
30 | numpy.random.bytes()
31 | numpy.random.shuffle()
32 | numpy.random.permutation()
27 | numpy.random.bytes()
28 | numpy.random.shuffle()
29 | numpy.random.permutation()
| ^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
33 | numpy.random.beta()
34 | numpy.random.binomial()
30 | numpy.random.beta()
31 | numpy.random.binomial()
|
NPY002.py:33:1: NPY002 Replace legacy `np.random.beta` call with `np.random.Generator`
NPY002.py:30:1: NPY002 Replace legacy `np.random.beta` call with `np.random.Generator`
|
31 | numpy.random.shuffle()
32 | numpy.random.permutation()
33 | numpy.random.beta()
28 | numpy.random.shuffle()
29 | numpy.random.permutation()
30 | numpy.random.beta()
| ^^^^^^^^^^^^^^^^^ NPY002
34 | numpy.random.binomial()
35 | numpy.random.chisquare()
31 | numpy.random.binomial()
32 | numpy.random.chisquare()
|
NPY002.py:34:1: NPY002 Replace legacy `np.random.binomial` call with `np.random.Generator`
NPY002.py:31:1: NPY002 Replace legacy `np.random.binomial` call with `np.random.Generator`
|
32 | numpy.random.permutation()
33 | numpy.random.beta()
34 | numpy.random.binomial()
29 | numpy.random.permutation()
30 | numpy.random.beta()
31 | numpy.random.binomial()
| ^^^^^^^^^^^^^^^^^^^^^ NPY002
35 | numpy.random.chisquare()
36 | numpy.random.dirichlet()
32 | numpy.random.chisquare()
33 | numpy.random.dirichlet()
|
NPY002.py:35:1: NPY002 Replace legacy `np.random.chisquare` call with `np.random.Generator`
NPY002.py:32:1: NPY002 Replace legacy `np.random.chisquare` call with `np.random.Generator`
|
33 | numpy.random.beta()
34 | numpy.random.binomial()
35 | numpy.random.chisquare()
30 | numpy.random.beta()
31 | numpy.random.binomial()
32 | numpy.random.chisquare()
| ^^^^^^^^^^^^^^^^^^^^^^ NPY002
36 | numpy.random.dirichlet()
37 | numpy.random.exponential()
33 | numpy.random.dirichlet()
34 | numpy.random.exponential()
|
NPY002.py:36:1: NPY002 Replace legacy `np.random.dirichlet` call with `np.random.Generator`
NPY002.py:33:1: NPY002 Replace legacy `np.random.dirichlet` call with `np.random.Generator`
|
34 | numpy.random.binomial()
35 | numpy.random.chisquare()
36 | numpy.random.dirichlet()
31 | numpy.random.binomial()
32 | numpy.random.chisquare()
33 | numpy.random.dirichlet()
| ^^^^^^^^^^^^^^^^^^^^^^ NPY002
37 | numpy.random.exponential()
38 | numpy.random.f()
34 | numpy.random.exponential()
35 | numpy.random.f()
|
NPY002.py:37:1: NPY002 Replace legacy `np.random.exponential` call with `np.random.Generator`
NPY002.py:34:1: NPY002 Replace legacy `np.random.exponential` call with `np.random.Generator`
|
35 | numpy.random.chisquare()
36 | numpy.random.dirichlet()
37 | numpy.random.exponential()
32 | numpy.random.chisquare()
33 | numpy.random.dirichlet()
34 | numpy.random.exponential()
| ^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
38 | numpy.random.f()
39 | numpy.random.gamma()
35 | numpy.random.f()
36 | numpy.random.gamma()
|
NPY002.py:38:1: NPY002 Replace legacy `np.random.f` call with `np.random.Generator`
NPY002.py:35:1: NPY002 Replace legacy `np.random.f` call with `np.random.Generator`
|
36 | numpy.random.dirichlet()
37 | numpy.random.exponential()
38 | numpy.random.f()
33 | numpy.random.dirichlet()
34 | numpy.random.exponential()
35 | numpy.random.f()
| ^^^^^^^^^^^^^^ NPY002
39 | numpy.random.gamma()
40 | numpy.random.geometric()
36 | numpy.random.gamma()
37 | numpy.random.geometric()
|
NPY002.py:39:1: NPY002 Replace legacy `np.random.gamma` call with `np.random.Generator`
NPY002.py:36:1: NPY002 Replace legacy `np.random.gamma` call with `np.random.Generator`
|
37 | numpy.random.exponential()
38 | numpy.random.f()
39 | numpy.random.gamma()
34 | numpy.random.exponential()
35 | numpy.random.f()
36 | numpy.random.gamma()
| ^^^^^^^^^^^^^^^^^^ NPY002
40 | numpy.random.geometric()
41 | numpy.random.gumbel()
37 | numpy.random.geometric()
38 | numpy.random.get_state()
|
NPY002.py:40:1: NPY002 Replace legacy `np.random.geometric` call with `np.random.Generator`
NPY002.py:37:1: NPY002 Replace legacy `np.random.geometric` call with `np.random.Generator`
|
38 | numpy.random.f()
39 | numpy.random.gamma()
40 | numpy.random.geometric()
35 | numpy.random.f()
36 | numpy.random.gamma()
37 | numpy.random.geometric()
| ^^^^^^^^^^^^^^^^^^^^^^ NPY002
41 | numpy.random.gumbel()
42 | numpy.random.hypergeometric()
38 | numpy.random.get_state()
39 | numpy.random.gumbel()
|
NPY002.py:41:1: NPY002 Replace legacy `np.random.gumbel` call with `np.random.Generator`
NPY002.py:38:1: NPY002 Replace legacy `np.random.get_state` call with `np.random.Generator`
|
39 | numpy.random.gamma()
40 | numpy.random.geometric()
41 | numpy.random.gumbel()
36 | numpy.random.gamma()
37 | numpy.random.geometric()
38 | numpy.random.get_state()
| ^^^^^^^^^^^^^^^^^^^^^^ NPY002
39 | numpy.random.gumbel()
40 | numpy.random.hypergeometric()
|
NPY002.py:39:1: NPY002 Replace legacy `np.random.gumbel` call with `np.random.Generator`
|
37 | numpy.random.geometric()
38 | numpy.random.get_state()
39 | numpy.random.gumbel()
| ^^^^^^^^^^^^^^^^^^^ NPY002
42 | numpy.random.hypergeometric()
43 | numpy.random.laplace()
40 | numpy.random.hypergeometric()
41 | numpy.random.laplace()
|
NPY002.py:42:1: NPY002 Replace legacy `np.random.hypergeometric` call with `np.random.Generator`
NPY002.py:40:1: NPY002 Replace legacy `np.random.hypergeometric` call with `np.random.Generator`
|
40 | numpy.random.geometric()
41 | numpy.random.gumbel()
42 | numpy.random.hypergeometric()
38 | numpy.random.get_state()
39 | numpy.random.gumbel()
40 | numpy.random.hypergeometric()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
43 | numpy.random.laplace()
44 | numpy.random.logistic()
41 | numpy.random.laplace()
42 | numpy.random.logistic()
|
NPY002.py:43:1: NPY002 Replace legacy `np.random.laplace` call with `np.random.Generator`
NPY002.py:41:1: NPY002 Replace legacy `np.random.laplace` call with `np.random.Generator`
|
41 | numpy.random.gumbel()
42 | numpy.random.hypergeometric()
43 | numpy.random.laplace()
39 | numpy.random.gumbel()
40 | numpy.random.hypergeometric()
41 | numpy.random.laplace()
| ^^^^^^^^^^^^^^^^^^^^ NPY002
44 | numpy.random.logistic()
45 | numpy.random.lognormal()
42 | numpy.random.logistic()
43 | numpy.random.lognormal()
|
NPY002.py:44:1: NPY002 Replace legacy `np.random.logistic` call with `np.random.Generator`
NPY002.py:42:1: NPY002 Replace legacy `np.random.logistic` call with `np.random.Generator`
|
42 | numpy.random.hypergeometric()
43 | numpy.random.laplace()
44 | numpy.random.logistic()
40 | numpy.random.hypergeometric()
41 | numpy.random.laplace()
42 | numpy.random.logistic()
| ^^^^^^^^^^^^^^^^^^^^^ NPY002
45 | numpy.random.lognormal()
46 | numpy.random.logseries()
43 | numpy.random.lognormal()
44 | numpy.random.logseries()
|
NPY002.py:45:1: NPY002 Replace legacy `np.random.lognormal` call with `np.random.Generator`
NPY002.py:43:1: NPY002 Replace legacy `np.random.lognormal` call with `np.random.Generator`
|
43 | numpy.random.laplace()
44 | numpy.random.logistic()
45 | numpy.random.lognormal()
41 | numpy.random.laplace()
42 | numpy.random.logistic()
43 | numpy.random.lognormal()
| ^^^^^^^^^^^^^^^^^^^^^^ NPY002
46 | numpy.random.logseries()
47 | numpy.random.multinomial()
44 | numpy.random.logseries()
45 | numpy.random.multinomial()
|
NPY002.py:46:1: NPY002 Replace legacy `np.random.logseries` call with `np.random.Generator`
NPY002.py:44:1: NPY002 Replace legacy `np.random.logseries` call with `np.random.Generator`
|
44 | numpy.random.logistic()
45 | numpy.random.lognormal()
46 | numpy.random.logseries()
42 | numpy.random.logistic()
43 | numpy.random.lognormal()
44 | numpy.random.logseries()
| ^^^^^^^^^^^^^^^^^^^^^^ NPY002
47 | numpy.random.multinomial()
48 | numpy.random.multivariate_normal()
45 | numpy.random.multinomial()
46 | numpy.random.multivariate_normal()
|
NPY002.py:47:1: NPY002 Replace legacy `np.random.multinomial` call with `np.random.Generator`
NPY002.py:45:1: NPY002 Replace legacy `np.random.multinomial` call with `np.random.Generator`
|
45 | numpy.random.lognormal()
46 | numpy.random.logseries()
47 | numpy.random.multinomial()
43 | numpy.random.lognormal()
44 | numpy.random.logseries()
45 | numpy.random.multinomial()
| ^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
48 | numpy.random.multivariate_normal()
49 | numpy.random.negative_binomial()
46 | numpy.random.multivariate_normal()
47 | numpy.random.negative_binomial()
|
NPY002.py:48:1: NPY002 Replace legacy `np.random.multivariate_normal` call with `np.random.Generator`
NPY002.py:46:1: NPY002 Replace legacy `np.random.multivariate_normal` call with `np.random.Generator`
|
46 | numpy.random.logseries()
47 | numpy.random.multinomial()
48 | numpy.random.multivariate_normal()
44 | numpy.random.logseries()
45 | numpy.random.multinomial()
46 | numpy.random.multivariate_normal()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
49 | numpy.random.negative_binomial()
50 | numpy.random.noncentral_chisquare()
47 | numpy.random.negative_binomial()
48 | numpy.random.noncentral_chisquare()
|
NPY002.py:49:1: NPY002 Replace legacy `np.random.negative_binomial` call with `np.random.Generator`
NPY002.py:47:1: NPY002 Replace legacy `np.random.negative_binomial` call with `np.random.Generator`
|
47 | numpy.random.multinomial()
48 | numpy.random.multivariate_normal()
49 | numpy.random.negative_binomial()
45 | numpy.random.multinomial()
46 | numpy.random.multivariate_normal()
47 | numpy.random.negative_binomial()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
50 | numpy.random.noncentral_chisquare()
51 | numpy.random.noncentral_f()
48 | numpy.random.noncentral_chisquare()
49 | numpy.random.noncentral_f()
|
NPY002.py:50:1: NPY002 Replace legacy `np.random.noncentral_chisquare` call with `np.random.Generator`
NPY002.py:48:1: NPY002 Replace legacy `np.random.noncentral_chisquare` call with `np.random.Generator`
|
48 | numpy.random.multivariate_normal()
49 | numpy.random.negative_binomial()
50 | numpy.random.noncentral_chisquare()
46 | numpy.random.multivariate_normal()
47 | numpy.random.negative_binomial()
48 | numpy.random.noncentral_chisquare()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
51 | numpy.random.noncentral_f()
52 | numpy.random.normal()
49 | numpy.random.noncentral_f()
50 | numpy.random.normal()
|
NPY002.py:51:1: NPY002 Replace legacy `np.random.noncentral_f` call with `np.random.Generator`
NPY002.py:49:1: NPY002 Replace legacy `np.random.noncentral_f` call with `np.random.Generator`
|
49 | numpy.random.negative_binomial()
50 | numpy.random.noncentral_chisquare()
51 | numpy.random.noncentral_f()
47 | numpy.random.negative_binomial()
48 | numpy.random.noncentral_chisquare()
49 | numpy.random.noncentral_f()
| ^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
52 | numpy.random.normal()
53 | numpy.random.pareto()
50 | numpy.random.normal()
51 | numpy.random.pareto()
|
NPY002.py:52:1: NPY002 Replace legacy `np.random.normal` call with `np.random.Generator`
NPY002.py:50:1: NPY002 Replace legacy `np.random.normal` call with `np.random.Generator`
|
50 | numpy.random.noncentral_chisquare()
51 | numpy.random.noncentral_f()
52 | numpy.random.normal()
48 | numpy.random.noncentral_chisquare()
49 | numpy.random.noncentral_f()
50 | numpy.random.normal()
| ^^^^^^^^^^^^^^^^^^^ NPY002
53 | numpy.random.pareto()
54 | numpy.random.poisson()
51 | numpy.random.pareto()
52 | numpy.random.poisson()
|
NPY002.py:53:1: NPY002 Replace legacy `np.random.pareto` call with `np.random.Generator`
NPY002.py:51:1: NPY002 Replace legacy `np.random.pareto` call with `np.random.Generator`
|
51 | numpy.random.noncentral_f()
52 | numpy.random.normal()
53 | numpy.random.pareto()
49 | numpy.random.noncentral_f()
50 | numpy.random.normal()
51 | numpy.random.pareto()
| ^^^^^^^^^^^^^^^^^^^ NPY002
54 | numpy.random.poisson()
55 | numpy.random.power()
52 | numpy.random.poisson()
53 | numpy.random.power()
|
NPY002.py:54:1: NPY002 Replace legacy `np.random.poisson` call with `np.random.Generator`
NPY002.py:52:1: NPY002 Replace legacy `np.random.poisson` call with `np.random.Generator`
|
52 | numpy.random.normal()
53 | numpy.random.pareto()
54 | numpy.random.poisson()
50 | numpy.random.normal()
51 | numpy.random.pareto()
52 | numpy.random.poisson()
| ^^^^^^^^^^^^^^^^^^^^ NPY002
55 | numpy.random.power()
56 | numpy.random.rayleigh()
53 | numpy.random.power()
54 | numpy.random.rayleigh()
|
NPY002.py:55:1: NPY002 Replace legacy `np.random.power` call with `np.random.Generator`
NPY002.py:53:1: NPY002 Replace legacy `np.random.power` call with `np.random.Generator`
|
53 | numpy.random.pareto()
54 | numpy.random.poisson()
55 | numpy.random.power()
51 | numpy.random.pareto()
52 | numpy.random.poisson()
53 | numpy.random.power()
| ^^^^^^^^^^^^^^^^^^ NPY002
56 | numpy.random.rayleigh()
57 | numpy.random.standard_cauchy()
54 | numpy.random.rayleigh()
55 | numpy.random.standard_cauchy()
|
NPY002.py:56:1: NPY002 Replace legacy `np.random.rayleigh` call with `np.random.Generator`
NPY002.py:54:1: NPY002 Replace legacy `np.random.rayleigh` call with `np.random.Generator`
|
54 | numpy.random.poisson()
55 | numpy.random.power()
56 | numpy.random.rayleigh()
52 | numpy.random.poisson()
53 | numpy.random.power()
54 | numpy.random.rayleigh()
| ^^^^^^^^^^^^^^^^^^^^^ NPY002
57 | numpy.random.standard_cauchy()
58 | numpy.random.standard_exponential()
55 | numpy.random.standard_cauchy()
56 | numpy.random.standard_exponential()
|
NPY002.py:57:1: NPY002 Replace legacy `np.random.standard_cauchy` call with `np.random.Generator`
NPY002.py:55:1: NPY002 Replace legacy `np.random.standard_cauchy` call with `np.random.Generator`
|
55 | numpy.random.power()
56 | numpy.random.rayleigh()
57 | numpy.random.standard_cauchy()
53 | numpy.random.power()
54 | numpy.random.rayleigh()
55 | numpy.random.standard_cauchy()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
58 | numpy.random.standard_exponential()
59 | numpy.random.standard_gamma()
56 | numpy.random.standard_exponential()
57 | numpy.random.standard_gamma()
|
NPY002.py:58:1: NPY002 Replace legacy `np.random.standard_exponential` call with `np.random.Generator`
NPY002.py:56:1: NPY002 Replace legacy `np.random.standard_exponential` call with `np.random.Generator`
|
56 | numpy.random.rayleigh()
57 | numpy.random.standard_cauchy()
58 | numpy.random.standard_exponential()
54 | numpy.random.rayleigh()
55 | numpy.random.standard_cauchy()
56 | numpy.random.standard_exponential()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
59 | numpy.random.standard_gamma()
60 | numpy.random.standard_normal()
57 | numpy.random.standard_gamma()
58 | numpy.random.standard_normal()
|
NPY002.py:59:1: NPY002 Replace legacy `np.random.standard_gamma` call with `np.random.Generator`
NPY002.py:57:1: NPY002 Replace legacy `np.random.standard_gamma` call with `np.random.Generator`
|
57 | numpy.random.standard_cauchy()
58 | numpy.random.standard_exponential()
59 | numpy.random.standard_gamma()
55 | numpy.random.standard_cauchy()
56 | numpy.random.standard_exponential()
57 | numpy.random.standard_gamma()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
60 | numpy.random.standard_normal()
61 | numpy.random.standard_t()
58 | numpy.random.standard_normal()
59 | numpy.random.standard_t()
|
NPY002.py:60:1: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator`
NPY002.py:58:1: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator`
|
58 | numpy.random.standard_exponential()
59 | numpy.random.standard_gamma()
60 | numpy.random.standard_normal()
56 | numpy.random.standard_exponential()
57 | numpy.random.standard_gamma()
58 | numpy.random.standard_normal()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
61 | numpy.random.standard_t()
62 | numpy.random.triangular()
59 | numpy.random.standard_t()
60 | numpy.random.triangular()
|
NPY002.py:61:1: NPY002 Replace legacy `np.random.standard_t` call with `np.random.Generator`
NPY002.py:59:1: NPY002 Replace legacy `np.random.standard_t` call with `np.random.Generator`
|
59 | numpy.random.standard_gamma()
60 | numpy.random.standard_normal()
61 | numpy.random.standard_t()
57 | numpy.random.standard_gamma()
58 | numpy.random.standard_normal()
59 | numpy.random.standard_t()
| ^^^^^^^^^^^^^^^^^^^^^^^ NPY002
62 | numpy.random.triangular()
63 | numpy.random.uniform()
60 | numpy.random.triangular()
61 | numpy.random.uniform()
|
NPY002.py:62:1: NPY002 Replace legacy `np.random.triangular` call with `np.random.Generator`
NPY002.py:60:1: NPY002 Replace legacy `np.random.triangular` call with `np.random.Generator`
|
60 | numpy.random.standard_normal()
61 | numpy.random.standard_t()
62 | numpy.random.triangular()
58 | numpy.random.standard_normal()
59 | numpy.random.standard_t()
60 | numpy.random.triangular()
| ^^^^^^^^^^^^^^^^^^^^^^^ NPY002
63 | numpy.random.uniform()
64 | numpy.random.vonmises()
61 | numpy.random.uniform()
62 | numpy.random.vonmises()
|
NPY002.py:63:1: NPY002 Replace legacy `np.random.uniform` call with `np.random.Generator`
NPY002.py:61:1: NPY002 Replace legacy `np.random.uniform` call with `np.random.Generator`
|
61 | numpy.random.standard_t()
62 | numpy.random.triangular()
63 | numpy.random.uniform()
59 | numpy.random.standard_t()
60 | numpy.random.triangular()
61 | numpy.random.uniform()
| ^^^^^^^^^^^^^^^^^^^^ NPY002
64 | numpy.random.vonmises()
65 | numpy.random.wald()
62 | numpy.random.vonmises()
63 | numpy.random.wald()
|
NPY002.py:64:1: NPY002 Replace legacy `np.random.vonmises` call with `np.random.Generator`
NPY002.py:62:1: NPY002 Replace legacy `np.random.vonmises` call with `np.random.Generator`
|
62 | numpy.random.triangular()
63 | numpy.random.uniform()
64 | numpy.random.vonmises()
60 | numpy.random.triangular()
61 | numpy.random.uniform()
62 | numpy.random.vonmises()
| ^^^^^^^^^^^^^^^^^^^^^ NPY002
65 | numpy.random.wald()
66 | numpy.random.weibull()
63 | numpy.random.wald()
64 | numpy.random.weibull()
|
NPY002.py:65:1: NPY002 Replace legacy `np.random.wald` call with `np.random.Generator`
NPY002.py:63:1: NPY002 Replace legacy `np.random.wald` call with `np.random.Generator`
|
63 | numpy.random.uniform()
64 | numpy.random.vonmises()
65 | numpy.random.wald()
61 | numpy.random.uniform()
62 | numpy.random.vonmises()
63 | numpy.random.wald()
| ^^^^^^^^^^^^^^^^^ NPY002
66 | numpy.random.weibull()
67 | numpy.random.zipf()
64 | numpy.random.weibull()
65 | numpy.random.zipf()
|
NPY002.py:66:1: NPY002 Replace legacy `np.random.weibull` call with `np.random.Generator`
NPY002.py:64:1: NPY002 Replace legacy `np.random.weibull` call with `np.random.Generator`
|
64 | numpy.random.vonmises()
65 | numpy.random.wald()
66 | numpy.random.weibull()
62 | numpy.random.vonmises()
63 | numpy.random.wald()
64 | numpy.random.weibull()
| ^^^^^^^^^^^^^^^^^^^^ NPY002
67 | numpy.random.zipf()
65 | numpy.random.zipf()
|
NPY002.py:67:1: NPY002 Replace legacy `np.random.zipf` call with `np.random.Generator`
NPY002.py:65:1: NPY002 Replace legacy `np.random.zipf` call with `np.random.Generator`
|
65 | numpy.random.wald()
66 | numpy.random.weibull()
67 | numpy.random.zipf()
63 | numpy.random.wald()
64 | numpy.random.weibull()
65 | numpy.random.zipf()
| ^^^^^^^^^^^^^^^^^ NPY002
|

View File

@@ -109,7 +109,7 @@ pub(crate) fn manual_list_comprehension(checker: &mut Checker, target: &Expr, bo
return;
}
let [arg] = &**args else {
let [arg] = args.as_slice() else {
return;
};

View File

@@ -76,7 +76,7 @@ pub(crate) fn manual_list_copy(checker: &mut Checker, target: &Expr, body: &[Stm
return;
}
let [arg] = &**args else {
let [arg] = args.as_slice() else {
return;
};

View File

@@ -64,7 +64,7 @@ pub(crate) fn unnecessary_list_cast(checker: &mut Checker, iter: &Expr, body: &[
return;
};
let [arg] = &**args else {
let [arg] = args.as_slice() else {
return;
};

View File

@@ -136,13 +136,6 @@ mod tests {
Path::new("E25.py")
)]
#[test_case(Rule::MissingWhitespaceAroundParameterEquals, Path::new("E25.py"))]
#[test_case(Rule::BlankLineBetweenMethods, Path::new("E30.py"))]
#[test_case(Rule::BlankLinesTopLevel, Path::new("E30.py"))]
#[test_case(Rule::TooManyBlankLines, Path::new("E30.py"))]
#[test_case(Rule::BlankLineAfterDecorator, Path::new("E30.py"))]
#[test_case(Rule::BlankLinesAfterFunctionOrClass, Path::new("E30.py"))]
#[test_case(Rule::BlankLinesBeforeNestedDefinition, Path::new("E30.py"))]
fn logical(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -1,895 +0,0 @@
use itertools::Itertools;
use std::cmp::Ordering;
use std::num::NonZeroU32;
use std::slice::Iter;
use ruff_diagnostics::AlwaysFixableViolation;
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Edit;
use ruff_diagnostics::Fix;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::lexer::LexicalError;
use ruff_python_parser::Tok;
use ruff_python_parser::TokenKind;
use ruff_source_file::{Locator, UniversalNewlines};
use ruff_text_size::TextRange;
use ruff_text_size::TextSize;
use crate::checkers::logical_lines::expand_indent;
use crate::line_width::IndentWidth;
use ruff_python_trivia::PythonWhitespace;
/// Number of blank lines around top level classes and functions.
const BLANK_LINES_TOP_LEVEL: u32 = 2;
/// Number of blank lines around methods and nested classes and functions.
const BLANK_LINES_METHOD_LEVEL: u32 = 1;
/// ## What it does
/// Checks for missing blank lines between methods of a class.
///
/// ## Why is this bad?
/// PEP 8 recommends exactly one blank line between methods of a class.
///
/// ## Example
/// ```python
/// class MyClass(object):
/// def func1():
/// pass
/// def func2():
/// pass
/// ```
///
/// Use instead:
/// ```python
/// class MyClass(object):
/// def func1():
/// pass
///
/// def func2():
/// pass
/// ```
///
/// ## References
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E301.html)
#[violation]
pub struct BlankLineBetweenMethods;
impl AlwaysFixableViolation for BlankLineBetweenMethods {
#[derive_message_formats]
fn message(&self) -> String {
format!("Expected {BLANK_LINES_METHOD_LEVEL:?} blank line, found 0")
}
fn fix_title(&self) -> String {
"Add missing blank line".to_string()
}
}
/// ## What it does
/// Checks for missing blank lines between top level functions and classes.
///
/// ## Why is this bad?
/// PEP 8 recommends exactly two blank lines between top level functions and classes.
///
/// ## Example
/// ```python
/// def func1():
/// pass
/// def func2():
/// pass
/// ```
///
/// Use instead:
/// ```python
/// def func1():
/// pass
///
///
/// def func2():
/// pass
/// ```
///
/// ## References
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E302.html)
#[violation]
pub struct BlankLinesTopLevel {
actual_blank_lines: u32,
}
impl AlwaysFixableViolation for BlankLinesTopLevel {
#[derive_message_formats]
fn message(&self) -> String {
let BlankLinesTopLevel {
actual_blank_lines: nb_blank_lines,
} = self;
format!("Expected {BLANK_LINES_TOP_LEVEL:?} blank lines, found {nb_blank_lines}")
}
fn fix_title(&self) -> String {
"Add missing blank line(s)".to_string()
}
}
/// ## What it does
/// Checks for extraneous blank lines.
///
/// ## Why is this bad?
/// PEP 8 recommends using blank lines as follows:
/// - No more than two blank lines between top-level statements.
/// - No more than one blank line between non-top-level statements.
///
/// ## Example
/// ```python
/// def func1():
/// pass
///
///
///
/// def func2():
/// pass
/// ```
///
/// Use instead:
/// ```python
/// def func1():
/// pass
///
///
/// def func2():
/// pass
/// ```
///
/// ## References
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E303.html)
#[violation]
pub struct TooManyBlankLines {
actual_blank_lines: u32,
}
impl AlwaysFixableViolation for TooManyBlankLines {
#[derive_message_formats]
fn message(&self) -> String {
let TooManyBlankLines {
actual_blank_lines: nb_blank_lines,
} = self;
format!("Too many blank lines ({nb_blank_lines})")
}
fn fix_title(&self) -> String {
"Remove extraneous blank line(s)".to_string()
}
}
/// ## What it does
/// Checks for extraneous blank line(s) after function decorators.
///
/// ## Why is this bad?
/// There should be no blank lines between a decorator and the object it is decorating.
///
/// ## Example
/// ```python
/// class User(object):
///
/// @property
///
/// def name(self):
/// pass
/// ```
///
/// Use instead:
/// ```python
/// class User(object):
///
/// @property
/// def name(self):
/// pass
/// ```
///
/// ## References
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E304.html)
#[violation]
pub struct BlankLineAfterDecorator {
actual_blank_lines: u32,
}
impl AlwaysFixableViolation for BlankLineAfterDecorator {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Blank lines found after function decorator ({lines})",
lines = self.actual_blank_lines
)
}
fn fix_title(&self) -> String {
"Remove extraneous blank line(s)".to_string()
}
}
/// ## What it does
/// Checks for missing blank lines after the end of function or class.
///
/// ## Why is this bad?
/// PEP 8 recommends using blank lines as following:
/// - Two blank lines are expected between functions and classes
/// - One blank line is expected between methods of a class.
///
/// ## Example
/// ```python
/// class User(object):
/// pass
/// user = User()
/// ```
///
/// Use instead:
/// ```python
/// class User(object):
/// pass
///
///
/// user = User()
/// ```
///
/// ## References
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E305.html)
#[violation]
pub struct BlankLinesAfterFunctionOrClass {
actual_blank_lines: u32,
}
impl AlwaysFixableViolation for BlankLinesAfterFunctionOrClass {
#[derive_message_formats]
fn message(&self) -> String {
let BlankLinesAfterFunctionOrClass {
actual_blank_lines: blank_lines,
} = self;
format!("Expected 2 blank lines after class or function definition, found ({blank_lines})")
}
fn fix_title(&self) -> String {
"Add missing blank line(s)".to_string()
}
}
/// ## What it does
/// Checks for 1 blank line between nested function or class definitions.
///
/// ## Why is this bad?
/// PEP 8 recommends using blank lines as following:
/// - Two blank lines are expected between functions and classes
/// - One blank line is expected between methods of a class.
///
/// ## Example
/// ```python
/// def outer():
/// def inner():
/// pass
/// def inner2():
/// pass
/// ```
///
/// Use instead:
/// ```python
/// def outer():
/// def inner():
/// pass
///
/// def inner2():
/// pass
/// ```
///
/// ## References
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E306.html)
#[violation]
pub struct BlankLinesBeforeNestedDefinition;
impl AlwaysFixableViolation for BlankLinesBeforeNestedDefinition {
#[derive_message_formats]
fn message(&self) -> String {
format!("Expected 1 blank line before a nested definition, found 0")
}
fn fix_title(&self) -> String {
"Add missing blank line".to_string()
}
}
#[derive(Debug)]
struct LogicalLineInfo {
kind: LogicalLineKind,
first_token_range: TextRange,
// The token's kind right before the newline ending the logical line.
last_token: TokenKind,
// The end of the logical line including the newline.
logical_line_end: TextSize,
// `true` if this is not a blank but only consists of a comment.
is_comment_only: bool,
/// `true` if the line is a string only (including trivia tokens) line, which is a docstring if coming right after a class/function definition.
is_docstring: bool,
/// The indentation length in columns. See [`expand_indent`] for the computation of the indent.
indent_length: usize,
/// The number of blank lines preceding the current line.
blank_lines: BlankLines,
/// The maximum number of consecutive blank lines between the current line
/// and the previous non-comment logical line.
/// One of its main uses is to allow a comments to directly precede or follow a class/function definition.
/// As such, `preceding_blank_lines` is used for rules that cannot trigger on comments (all rules except E303),
/// and `blank_lines` is used for the rule that can trigger on comments (E303).
preceding_blank_lines: BlankLines,
}
/// Iterator that processes tokens until a full logical line (or comment line) is "built".
/// It then returns characteristics of that logical line (see `LogicalLineInfo`).
struct LinePreprocessor<'a> {
tokens: Iter<'a, Result<(Tok, TextRange), LexicalError>>,
locator: &'a Locator<'a>,
indent_width: IndentWidth,
/// The start position of the next logical line.
line_start: TextSize,
/// Maximum number of consecutive blank lines between the current line and the previous non-comment logical line.
/// One of its main uses is to allow a comment to directly precede a class/function definition.
max_preceding_blank_lines: BlankLines,
}
impl<'a> LinePreprocessor<'a> {
fn new(
tokens: &'a [LexResult],
locator: &'a Locator,
indent_width: IndentWidth,
) -> LinePreprocessor<'a> {
LinePreprocessor {
tokens: tokens.iter(),
locator,
line_start: TextSize::new(0),
max_preceding_blank_lines: BlankLines::Zero,
indent_width,
}
}
}
impl<'a> Iterator for LinePreprocessor<'a> {
type Item = LogicalLineInfo;
fn next(&mut self) -> Option<LogicalLineInfo> {
let mut line_is_comment_only = true;
let mut is_docstring = false;
// Number of consecutive blank lines directly preceding this logical line.
let mut blank_lines = BlankLines::Zero;
let mut first_logical_line_token: Option<(LogicalLineKind, TextRange)> = None;
let mut last_token: TokenKind = TokenKind::EndOfFile;
let mut parens = 0u32;
while let Some(result) = self.tokens.next() {
let Ok((token, range)) = result else {
continue;
};
if matches!(token, Tok::Indent | Tok::Dedent) {
continue;
}
let token_kind = TokenKind::from_token(token);
let (logical_line_kind, first_token_range) = if let Some(first_token_range) =
first_logical_line_token
{
first_token_range
}
// At the start of the line...
else {
// An empty line
if token_kind == TokenKind::NonLogicalNewline {
blank_lines.add(*range);
self.line_start = range.end();
continue;
}
is_docstring = token_kind == TokenKind::String;
let logical_line_kind = match token_kind {
TokenKind::Class => LogicalLineKind::Class,
TokenKind::Comment => LogicalLineKind::Comment,
TokenKind::At => LogicalLineKind::Decorator,
TokenKind::Def => LogicalLineKind::Function,
// Lookahead to distinguish `async def` from `async with`.
TokenKind::Async
if matches!(self.tokens.as_slice().first(), Some(Ok((Tok::Def, _)))) =>
{
LogicalLineKind::Function
}
_ => LogicalLineKind::Other,
};
first_logical_line_token = Some((logical_line_kind, *range));
(logical_line_kind, *range)
};
if !token_kind.is_trivia() {
line_is_comment_only = false;
}
// A docstring line is composed only of the docstring (TokenKind::String) and trivia tokens.
// (If a comment follows a docstring, we still count the line as a docstring)
if token_kind != TokenKind::String && !token_kind.is_trivia() {
is_docstring = false;
}
match token_kind {
TokenKind::Lbrace | TokenKind::Lpar | TokenKind::Lsqb => {
parens = parens.saturating_add(1);
}
TokenKind::Rbrace | TokenKind::Rpar | TokenKind::Rsqb => {
parens = parens.saturating_sub(1);
}
TokenKind::Newline | TokenKind::NonLogicalNewline if parens == 0 => {
let indent_range = TextRange::new(self.line_start, first_token_range.start());
let indent_length =
expand_indent(self.locator.slice(indent_range), self.indent_width);
self.max_preceding_blank_lines =
self.max_preceding_blank_lines.max(blank_lines);
let logical_line = LogicalLineInfo {
kind: logical_line_kind,
first_token_range,
last_token,
logical_line_end: range.end(),
is_comment_only: line_is_comment_only,
is_docstring,
indent_length,
blank_lines,
preceding_blank_lines: self.max_preceding_blank_lines,
};
// Reset the blank lines after a non-comment only line.
if !line_is_comment_only {
self.max_preceding_blank_lines = BlankLines::Zero;
}
// Set the start for the next logical line.
self.line_start = range.end();
return Some(logical_line);
}
_ => {}
}
last_token = token_kind;
}
None
}
}
#[derive(Clone, Copy, Debug, Default)]
enum BlankLines {
/// No blank lines
#[default]
Zero,
/// One or more blank lines
Many { count: NonZeroU32, range: TextRange },
}
impl BlankLines {
fn add(&mut self, line_range: TextRange) {
match self {
BlankLines::Zero => {
*self = BlankLines::Many {
count: NonZeroU32::MIN,
range: line_range,
}
}
BlankLines::Many { count, range } => {
*count = count.saturating_add(1);
*range = TextRange::new(range.start(), line_range.end());
}
}
}
fn count(&self) -> u32 {
match self {
BlankLines::Zero => 0,
BlankLines::Many { count, .. } => count.get(),
}
}
fn range(&self) -> Option<TextRange> {
match self {
BlankLines::Zero => None,
BlankLines::Many { range, .. } => Some(*range),
}
}
}
impl PartialEq<u32> for BlankLines {
fn eq(&self, other: &u32) -> bool {
self.partial_cmp(other) == Some(Ordering::Equal)
}
}
impl PartialOrd<u32> for BlankLines {
fn partial_cmp(&self, other: &u32) -> Option<Ordering> {
self.count().partial_cmp(other)
}
}
impl PartialOrd for BlankLines {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for BlankLines {
fn cmp(&self, other: &Self) -> Ordering {
self.count().cmp(&other.count())
}
}
impl PartialEq for BlankLines {
fn eq(&self, other: &Self) -> bool {
self.count() == other.count()
}
}
impl Eq for BlankLines {}
#[derive(Copy, Clone, Debug, Default)]
enum Follows {
#[default]
Other,
Decorator,
Def,
Docstring,
}
#[derive(Copy, Clone, Debug, Default)]
enum Status {
/// Stores the indent level where the nesting started.
Inside(usize),
/// This is used to rectify a Inside switched to a Outside because of a dedented comment.
CommentAfter(usize),
#[default]
Outside,
}
impl Status {
fn update(&mut self, line: &LogicalLineInfo) {
match *self {
Status::Inside(nesting_indent) => {
if line.indent_length <= nesting_indent {
if line.is_comment_only {
*self = Status::CommentAfter(nesting_indent);
} else {
*self = Status::Outside;
}
}
}
Status::CommentAfter(indent) => {
if !line.is_comment_only {
if line.indent_length > indent {
*self = Status::Inside(indent);
} else {
*self = Status::Outside;
}
}
}
Status::Outside => {
// Nothing to do
}
}
}
}
/// Contains variables used for the linting of blank lines.
#[derive(Debug, Default)]
pub(crate) struct BlankLinesChecker {
follows: Follows,
fn_status: Status,
class_status: Status,
/// First line that is not a comment.
is_not_first_logical_line: bool,
/// Used for the fix in case a comment separates two non-comment logical lines to make the comment "stick"
/// to the second line instead of the first.
last_non_comment_line_end: TextSize,
previous_unindented_line_kind: Option<LogicalLineKind>,
}
impl BlankLinesChecker {
/// E301, E302, E303, E304, E305, E306
pub(crate) fn check_lines(
&mut self,
tokens: &[LexResult],
locator: &Locator,
stylist: &Stylist,
indent_width: IndentWidth,
diagnostics: &mut Vec<Diagnostic>,
) {
let mut prev_indent_length: Option<usize> = None;
let line_preprocessor = LinePreprocessor::new(tokens, locator, indent_width);
for logical_line in line_preprocessor {
self.check_line(
&logical_line,
prev_indent_length,
locator,
stylist,
diagnostics,
);
if !logical_line.is_comment_only {
prev_indent_length = Some(logical_line.indent_length);
}
}
}
#[allow(clippy::nonminimal_bool)]
fn check_line(
&mut self,
line: &LogicalLineInfo,
prev_indent_length: Option<usize>,
locator: &Locator,
stylist: &Stylist,
diagnostics: &mut Vec<Diagnostic>,
) {
self.class_status.update(line);
self.fn_status.update(line);
// Don't expect blank lines before the first non comment line.
if self.is_not_first_logical_line {
if line.preceding_blank_lines == 0
// Only applies to methods.
&& matches!(line.kind, LogicalLineKind::Function)
&& matches!(self.class_status, Status::Inside(_))
// The class/parent method's docstring can directly precede the def.
// Allow following a decorator (if there is an error it will be triggered on the first decorator).
&& !matches!(self.follows, Follows::Docstring | Follows::Decorator)
// Do not trigger when the def follows an if/while/etc...
&& prev_indent_length.is_some_and(|prev_indent_length| prev_indent_length >= line.indent_length)
{
// E301
let mut diagnostic =
Diagnostic::new(BlankLineBetweenMethods, line.first_token_range);
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
stylist.line_ending().to_string(),
locator.line_start(self.last_non_comment_line_end),
)));
diagnostics.push(diagnostic);
}
if line.preceding_blank_lines < BLANK_LINES_TOP_LEVEL
// Allow following a decorator (if there is an error it will be triggered on the first decorator).
&& !matches!(self.follows, Follows::Decorator)
// Allow groups of one-liners.
&& !(matches!(self.follows, Follows::Def) && !matches!(line.last_token, TokenKind::Colon))
// Only trigger on non-indented classes and functions (for example functions within an if are ignored)
&& line.indent_length == 0
// Only apply to functions or classes.
&& line.kind.is_top_level()
{
// E302
let mut diagnostic = Diagnostic::new(
BlankLinesTopLevel {
actual_blank_lines: line.preceding_blank_lines.count(),
},
line.first_token_range,
);
if let Some(blank_lines_range) = line.blank_lines.range() {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
stylist.line_ending().repeat(BLANK_LINES_TOP_LEVEL as usize),
blank_lines_range,
)));
} else {
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
stylist.line_ending().repeat(BLANK_LINES_TOP_LEVEL as usize),
locator.line_start(self.last_non_comment_line_end),
)));
}
diagnostics.push(diagnostic);
}
let expected_blank_lines = if line.indent_length > 0 {
BLANK_LINES_METHOD_LEVEL
} else {
BLANK_LINES_TOP_LEVEL
};
if line.blank_lines > expected_blank_lines {
// E303
let mut diagnostic = Diagnostic::new(
TooManyBlankLines {
actual_blank_lines: line.blank_lines.count(),
},
line.first_token_range,
);
if let Some(blank_lines_range) = line.blank_lines.range() {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
stylist.line_ending().repeat(expected_blank_lines as usize),
blank_lines_range,
)));
}
diagnostics.push(diagnostic);
}
if matches!(self.follows, Follows::Decorator)
&& !line.is_comment_only
&& line.preceding_blank_lines > 0
{
// E304
let mut diagnostic = Diagnostic::new(
BlankLineAfterDecorator {
actual_blank_lines: line.preceding_blank_lines.count(),
},
line.first_token_range,
);
// Get all the lines between the last decorator line (included) and the current line (included).
// Then remove all blank lines.
let trivia_range = TextRange::new(
self.last_non_comment_line_end,
locator.line_start(line.first_token_range.start()),
);
let trivia_text = locator.slice(trivia_range);
let mut trivia_without_blank_lines = trivia_text
.universal_newlines()
.filter_map(|line| {
(!line.trim_whitespace().is_empty()).then_some(line.as_str())
})
.join(&stylist.line_ending());
let fix = if trivia_without_blank_lines.is_empty() {
Fix::safe_edit(Edit::range_deletion(trivia_range))
} else {
trivia_without_blank_lines.push_str(&stylist.line_ending());
Fix::safe_edit(Edit::range_replacement(
trivia_without_blank_lines,
trivia_range,
))
};
diagnostic.set_fix(fix);
diagnostics.push(diagnostic);
}
if line.preceding_blank_lines < BLANK_LINES_TOP_LEVEL
&& self
.previous_unindented_line_kind
.is_some_and(LogicalLineKind::is_top_level)
&& line.indent_length == 0
&& !line.is_comment_only
&& !line.kind.is_top_level()
{
// E305
let mut diagnostic = Diagnostic::new(
BlankLinesAfterFunctionOrClass {
actual_blank_lines: line.preceding_blank_lines.count(),
},
line.first_token_range,
);
if let Some(blank_lines_range) = line.blank_lines.range() {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
stylist.line_ending().repeat(BLANK_LINES_TOP_LEVEL as usize),
blank_lines_range,
)));
} else {
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
stylist.line_ending().repeat(BLANK_LINES_TOP_LEVEL as usize),
locator.line_start(line.first_token_range.start()),
)));
}
diagnostics.push(diagnostic);
}
if line.preceding_blank_lines == 0
// Only apply to nested functions.
&& matches!(self.fn_status, Status::Inside(_))
&& line.kind.is_top_level()
// Allow following a decorator (if there is an error it will be triggered on the first decorator).
&& !matches!(self.follows, Follows::Decorator)
// The class's docstring can directly precede the first function.
&& !matches!(self.follows, Follows::Docstring)
// Do not trigger when the def/class follows an "indenting token" (if/while/etc...).
&& prev_indent_length.is_some_and(|prev_indent_length| prev_indent_length >= line.indent_length)
// Allow groups of one-liners.
&& !(matches!(self.follows, Follows::Def) && line.last_token != TokenKind::Colon)
{
// E306
let mut diagnostic =
Diagnostic::new(BlankLinesBeforeNestedDefinition, line.first_token_range);
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
stylist.line_ending().to_string(),
locator.line_start(line.first_token_range.start()),
)));
diagnostics.push(diagnostic);
}
}
match line.kind {
LogicalLineKind::Class => {
if matches!(self.class_status, Status::Outside) {
self.class_status = Status::Inside(line.indent_length);
}
self.follows = Follows::Other;
}
LogicalLineKind::Decorator => {
self.follows = Follows::Decorator;
}
LogicalLineKind::Function => {
if matches!(self.fn_status, Status::Outside) {
self.fn_status = Status::Inside(line.indent_length);
}
self.follows = Follows::Def;
}
LogicalLineKind::Comment => {}
LogicalLineKind::Other => {
self.follows = Follows::Other;
}
}
if line.is_docstring {
self.follows = Follows::Docstring;
}
if !line.is_comment_only {
self.is_not_first_logical_line = true;
self.last_non_comment_line_end = line.logical_line_end;
if line.indent_length == 0 {
self.previous_unindented_line_kind = Some(line.kind);
}
}
}
}
#[derive(Copy, Clone, Debug)]
enum LogicalLineKind {
/// The clause header of a class definition
Class,
/// A decorator
Decorator,
/// The clause header of a function
Function,
/// A comment only line
Comment,
/// Any other statement or clause header
Other,
}
impl LogicalLineKind {
fn is_top_level(self) -> bool {
matches!(
self,
LogicalLineKind::Class | LogicalLineKind::Function | LogicalLineKind::Decorator
)
}
}

View File

@@ -74,7 +74,7 @@ pub(crate) fn invalid_escape_sequence(
let Some(range) = indexer.fstring_ranges().innermost(token_range.start()) else {
return;
};
(&**value, range.start())
(value.as_str(), range.start())
}
Tok::String { kind, .. } => {
if kind.is_raw() {

View File

@@ -139,10 +139,10 @@ pub(crate) fn literal_comparisons(checker: &mut Checker, compare: &ast::ExprComp
// Check `left`.
let mut comparator = compare.left.as_ref();
let [op, ..] = &*compare.ops else {
let [op, ..] = compare.ops.as_slice() else {
return;
};
let [next, ..] = &*compare.comparators else {
let [next, ..] = compare.comparators.as_slice() else {
return;
};

View File

@@ -2,7 +2,6 @@ pub(crate) use ambiguous_class_name::*;
pub(crate) use ambiguous_function_name::*;
pub(crate) use ambiguous_variable_name::*;
pub(crate) use bare_except::*;
pub(crate) use blank_lines::*;
pub(crate) use compound_statements::*;
pub(crate) use doc_line_too_long::*;
pub(crate) use errors::*;
@@ -24,7 +23,6 @@ mod ambiguous_class_name;
mod ambiguous_function_name;
mod ambiguous_variable_name;
mod bare_except;
mod blank_lines;
mod compound_statements;
mod doc_line_too_long;
mod errors;

View File

@@ -90,7 +90,7 @@ pub(crate) fn not_tests(checker: &mut Checker, unary_op: &ast::ExprUnaryOp) {
return;
};
match &**ops {
match ops.as_slice() {
[CmpOp::In] => {
if checker.enabled(Rule::NotInTest) {
let mut diagnostic = Diagnostic::new(NotInTest, unary_op.operand.range());

View File

@@ -1,8 +1,11 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_index::Indexer;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_python_trivia::leading_indentation;
use ruff_source_file::Locator;
use ruff_text_size::{TextRange, TextSize};
use ruff_text_size::{TextLen, TextRange, TextSize};
/// ## What it does
/// Checks for indentation that uses tabs.
@@ -45,52 +48,44 @@ impl Violation for TabIndentation {
/// W191
pub(crate) fn tab_indentation(
diagnostics: &mut Vec<Diagnostic>,
tokens: &[LexResult],
locator: &Locator,
indexer: &Indexer,
) {
let contents = locator.contents().as_bytes();
let mut offset = 0;
while let Some(index) = memchr::memchr(b'\t', &contents[offset..]) {
// If we find a tab in the file, grab the entire line.
let range = locator.full_line_range(TextSize::try_from(offset + index).unwrap());
// Always check the first line for tab indentation as there's no newline
// token before it.
tab_indentation_at_line_start(diagnostics, locator, TextSize::default());
// Determine whether the tab is part of the line's indentation.
if let Some(indent) = tab_indentation_at_line_start(range.start(), locator, indexer) {
diagnostics.push(Diagnostic::new(TabIndentation, indent));
for (tok, range) in tokens.iter().flatten() {
if matches!(tok, Tok::Newline | Tok::NonLogicalNewline) {
tab_indentation_at_line_start(diagnostics, locator, range.end());
}
}
// Advance to the next line.
offset = range.end().to_usize();
// The lexer doesn't emit `Newline` / `NonLogicalNewline` for a line
// continuation character (`\`), so we need to manually check for tab
// indentation for lines that follow a line continuation character.
for continuation_line in indexer.continuation_line_starts() {
tab_indentation_at_line_start(
diagnostics,
locator,
locator.full_line_end(*continuation_line),
);
}
}
/// If a line includes tabs in its indentation, returns the range of the
/// indent.
/// Checks for indentation that uses tabs for a line starting at
/// the given [`TextSize`].
fn tab_indentation_at_line_start(
line_start: TextSize,
diagnostics: &mut Vec<Diagnostic>,
locator: &Locator,
indexer: &Indexer,
) -> Option<TextRange> {
let mut contains_tab = false;
for (i, char) in locator.after(line_start).as_bytes().iter().enumerate() {
match char {
// If we find a tab character, report it as a violation.
b'\t' => {
contains_tab = true;
}
// If we find a space, continue.
b' ' | b'\x0C' => {}
// If we find a non-whitespace character, stop.
_ => {
if contains_tab {
let range = TextRange::at(line_start, TextSize::try_from(i).unwrap());
if !indexer.multiline_ranges().contains_range(range) {
return Some(range);
}
}
break;
}
}
line_start: TextSize,
) {
let indent = leading_indentation(locator.after(line_start));
if indent.find('\t').is_some() {
diagnostics.push(Diagnostic::new(
TabIndentation,
TextRange::at(line_start, indent.text_len()),
));
}
None
}

View File

@@ -1,44 +0,0 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E30.py:453:5: E301 [*] Expected 1 blank line, found 0
|
451 | def func1():
452 | pass
453 | def func2():
| ^^^ E301
454 | pass
455 | # end
|
= help: Add missing blank line
Safe fix
450 450 |
451 451 | def func1():
452 452 | pass
453 |+
453 454 | def func2():
454 455 | pass
455 456 | # end
E30.py:464:5: E301 [*] Expected 1 blank line, found 0
|
462 | pass
463 | # comment
464 | def fn2():
| ^^^ E301
465 | pass
466 | # end
|
= help: Add missing blank line
Safe fix
460 460 |
461 461 | def fn1():
462 462 | pass
463 |+
463 464 | # comment
464 465 | def fn2():
465 466 | pass

View File

@@ -1,187 +0,0 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E30.py:471:1: E302 [*] Expected 2 blank lines, found 0
|
469 | # E302
470 | """Main module."""
471 | def fn():
| ^^^ E302
472 | pass
473 | # end
|
= help: Add missing blank line(s)
Safe fix
468 468 |
469 469 | # E302
470 470 | """Main module."""
471 |+
472 |+
471 473 | def fn():
472 474 | pass
473 475 | # end
E30.py:478:1: E302 [*] Expected 2 blank lines, found 0
|
476 | # E302
477 | import sys
478 | def get_sys_path():
| ^^^ E302
479 | return sys.path
480 | # end
|
= help: Add missing blank line(s)
Safe fix
475 475 |
476 476 | # E302
477 477 | import sys
478 |+
479 |+
478 480 | def get_sys_path():
479 481 | return sys.path
480 482 | # end
E30.py:487:1: E302 [*] Expected 2 blank lines, found 1
|
485 | pass
486 |
487 | def b():
| ^^^ E302
488 | pass
489 | # end
|
= help: Add missing blank line(s)
Safe fix
484 484 | def a():
485 485 | pass
486 486 |
487 |+
487 488 | def b():
488 489 | pass
489 490 | # end
E30.py:498:1: E302 [*] Expected 2 blank lines, found 1
|
496 | # comment
497 |
498 | def b():
| ^^^ E302
499 | pass
500 | # end
|
= help: Add missing blank line(s)
Safe fix
495 495 |
496 496 | # comment
497 497 |
498 |+
498 499 | def b():
499 500 | pass
500 501 | # end
E30.py:507:1: E302 [*] Expected 2 blank lines, found 1
|
505 | pass
506 |
507 | async def b():
| ^^^^^ E302
508 | pass
509 | # end
|
= help: Add missing blank line(s)
Safe fix
504 504 | def a():
505 505 | pass
506 506 |
507 |+
507 508 | async def b():
508 509 | pass
509 510 | # end
E30.py:516:1: E302 [*] Expected 2 blank lines, found 1
|
514 | pass
515 |
516 | async def x(y: int = 1):
| ^^^^^ E302
517 | pass
518 | # end
|
= help: Add missing blank line(s)
Safe fix
513 513 | async def x():
514 514 | pass
515 515 |
516 |+
516 517 | async def x(y: int = 1):
517 518 | pass
518 519 | # end
E30.py:524:1: E302 [*] Expected 2 blank lines, found 0
|
522 | def bar():
523 | pass
524 | def baz(): pass
| ^^^ E302
525 | # end
|
= help: Add missing blank line(s)
Safe fix
521 521 | # E302
522 522 | def bar():
523 523 | pass
524 |+
525 |+
524 526 | def baz(): pass
525 527 | # end
526 528 |
E30.py:530:1: E302 [*] Expected 2 blank lines, found 0
|
528 | # E302
529 | def bar(): pass
530 | def baz():
| ^^^ E302
531 | pass
532 | # end
|
= help: Add missing blank line(s)
Safe fix
527 527 |
528 528 | # E302
529 529 | def bar(): pass
530 |+
531 |+
530 532 | def baz():
531 533 | pass
532 534 | # end
E30.py:540:1: E302 [*] Expected 2 blank lines, found 1
|
539 | # comment
540 | @decorator
| ^ E302
541 | def g():
542 | pass
|
= help: Add missing blank line(s)
Safe fix
536 536 | def f():
537 537 | pass
538 538 |
539 |+
540 |+
539 541 | # comment
540 542 | @decorator
541 543 | def g():

View File

@@ -1,250 +0,0 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E30.py:555:2: E303 [*] Too many blank lines (2)
|
555 | def method2():
| ^^^ E303
556 | return 22
557 | # end
|
= help: Remove extraneous blank line(s)
Safe fix
551 551 | def method1():
552 552 | return 1
553 553 |
554 |-
555 554 | def method2():
556 555 | return 22
557 556 | # end
E30.py:565:5: E303 [*] Too many blank lines (2)
|
565 | # arbitrary comment
| ^^^^^^^^^^^^^^^^^^^ E303
566 |
567 | def inner(): # E306 not expected (pycodestyle detects E306)
|
= help: Remove extraneous blank line(s)
Safe fix
561 561 | def fn():
562 562 | _ = None
563 563 |
564 |-
565 564 | # arbitrary comment
566 565 |
567 566 | def inner(): # E306 not expected (pycodestyle detects E306)
E30.py:577:5: E303 [*] Too many blank lines (2)
|
577 | # arbitrary comment
| ^^^^^^^^^^^^^^^^^^^ E303
578 | def inner(): # E306 not expected (pycodestyle detects E306)
579 | pass
|
= help: Remove extraneous blank line(s)
Safe fix
573 573 | def fn():
574 574 | _ = None
575 575 |
576 |-
577 576 | # arbitrary comment
578 577 | def inner(): # E306 not expected (pycodestyle detects E306)
579 578 | pass
E30.py:588:1: E303 [*] Too many blank lines (3)
|
588 | print()
| ^^^^^ E303
589 | # end
|
= help: Remove extraneous blank line(s)
Safe fix
584 584 | print()
585 585 |
586 586 |
587 |-
588 587 | print()
589 588 | # end
590 589 |
E30.py:597:1: E303 [*] Too many blank lines (3)
|
597 | # comment
| ^^^^^^^^^ E303
598 |
599 | print()
|
= help: Remove extraneous blank line(s)
Safe fix
593 593 | print()
594 594 |
595 595 |
596 |-
597 596 | # comment
598 597 |
599 598 | print()
E30.py:608:5: E303 [*] Too many blank lines (2)
|
608 | # comment
| ^^^^^^^^^ E303
|
= help: Remove extraneous blank line(s)
Safe fix
604 604 | def a():
605 605 | print()
606 606 |
607 |-
608 607 | # comment
609 608 |
610 609 |
E30.py:611:5: E303 [*] Too many blank lines (2)
|
611 | # another comment
| ^^^^^^^^^^^^^^^^^ E303
612 |
613 | print()
|
= help: Remove extraneous blank line(s)
Safe fix
607 607 |
608 608 | # comment
609 609 |
610 |-
611 610 | # another comment
612 611 |
613 612 | print()
E30.py:622:1: E303 [*] Too many blank lines (3)
|
622 | / """This class docstring comes on line 5.
623 | | It gives error E303: too many blank lines (3)
624 | | """
| |___^ E303
625 | # end
|
= help: Remove extraneous blank line(s)
Safe fix
618 618 | #!python
619 619 |
620 620 |
621 |-
622 621 | """This class docstring comes on line 5.
623 622 | It gives error E303: too many blank lines (3)
624 623 | """
E30.py:634:5: E303 [*] Too many blank lines (2)
|
634 | def b(self):
| ^^^ E303
635 | pass
636 | # end
|
= help: Remove extraneous blank line(s)
Safe fix
630 630 | def a(self):
631 631 | pass
632 632 |
633 |-
634 633 | def b(self):
635 634 | pass
636 635 | # end
E30.py:644:5: E303 [*] Too many blank lines (2)
|
644 | a = 2
| ^ E303
645 | # end
|
= help: Remove extraneous blank line(s)
Safe fix
640 640 | if True:
641 641 | a = 1
642 642 |
643 |-
644 643 | a = 2
645 644 | # end
646 645 |
E30.py:652:5: E303 [*] Too many blank lines (2)
|
652 | # comment
| ^^^^^^^^^ E303
|
= help: Remove extraneous blank line(s)
Safe fix
648 648 | # E303
649 649 | class Test:
650 650 |
651 |-
652 651 | # comment
653 652 |
654 653 |
E30.py:655:5: E303 [*] Too many blank lines (2)
|
655 | # another comment
| ^^^^^^^^^^^^^^^^^ E303
656 |
657 | def test(self): pass
|
= help: Remove extraneous blank line(s)
Safe fix
651 651 |
652 652 | # comment
653 653 |
654 |-
655 654 | # another comment
656 655 |
657 656 | def test(self): pass
E30.py:669:5: E303 [*] Too many blank lines (2)
|
669 | def b(self):
| ^^^ E303
670 | pass
671 | # end
|
= help: Remove extraneous blank line(s)
Safe fix
665 665 |
666 666 | # wrongly indented comment
667 667 |
668 |-
669 668 | def b(self):
670 669 | pass
671 670 | # end
E30.py:679:5: E303 [*] Too many blank lines (2)
|
679 | pass
| ^^^^ E303
680 | # end
|
= help: Remove extraneous blank line(s)
Safe fix
675 675 | def fn():
676 676 | pass
677 677 |
678 |-
679 678 | pass
680 679 | # end
681 680 |

View File

@@ -1,65 +0,0 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E30.py:686:1: E304 [*] Blank lines found after function decorator (1)
|
684 | @decorator
685 |
686 | def function():
| ^^^ E304
687 | pass
688 | # end
|
= help: Remove extraneous blank line(s)
Safe fix
682 682 |
683 683 | # E304
684 684 | @decorator
685 |-
686 685 | def function():
687 686 | pass
688 687 | # end
E30.py:695:1: E304 [*] Blank lines found after function decorator (1)
|
694 | # comment E304 not expected
695 | def function():
| ^^^ E304
696 | pass
697 | # end
|
= help: Remove extraneous blank line(s)
Safe fix
690 690 |
691 691 | # E304
692 692 | @decorator
693 |-
694 693 | # comment E304 not expected
695 694 | def function():
696 695 | pass
E30.py:707:1: E304 [*] Blank lines found after function decorator (2)
|
706 | # second comment E304 not expected
707 | def function():
| ^^^ E304
708 | pass
709 | # end
|
= help: Remove extraneous blank line(s)
Safe fix
699 699 |
700 700 | # E304
701 701 | @decorator
702 |-
703 702 | # comment E304 not expected
704 |-
705 |-
706 703 | # second comment E304 not expected
707 704 | def function():
708 705 | pass

View File

@@ -1,102 +0,0 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E30.py:719:1: E305 [*] Expected 2 blank lines after class or function definition, found (1)
|
718 | # another comment
719 | fn()
| ^^ E305
720 | # end
|
= help: Add missing blank line(s)
Safe fix
716 716 | # comment
717 717 |
718 718 | # another comment
719 |+
720 |+
719 721 | fn()
720 722 | # end
721 723 |
E30.py:730:1: E305 [*] Expected 2 blank lines after class or function definition, found (1)
|
729 | # another comment
730 | a = 1
| ^ E305
731 | # end
|
= help: Add missing blank line(s)
Safe fix
727 727 | # comment
728 728 |
729 729 | # another comment
730 |+
731 |+
730 732 | a = 1
731 733 | # end
732 734 |
E30.py:742:1: E305 [*] Expected 2 blank lines after class or function definition, found (1)
|
740 | # another comment
741 |
742 | try:
| ^^^ E305
743 | fn()
744 | except Exception:
|
= help: Add missing blank line(s)
Safe fix
739 739 |
740 740 | # another comment
741 741 |
742 |+
742 743 | try:
743 744 | fn()
744 745 | except Exception:
E30.py:754:1: E305 [*] Expected 2 blank lines after class or function definition, found (1)
|
753 | # Two spaces before comments, too.
754 | if a():
| ^^ E305
755 | a()
756 | # end
|
= help: Add missing blank line(s)
Safe fix
751 751 | print()
752 752 |
753 753 | # Two spaces before comments, too.
754 |+
755 |+
754 756 | if a():
755 757 | a()
756 758 | # end
E30.py:767:1: E305 [*] Expected 2 blank lines after class or function definition, found (1)
|
765 | blah, blah
766 |
767 | if __name__ == '__main__':
| ^^ E305
768 | main()
769 | # end
|
= help: Add missing blank line(s)
Safe fix
764 764 | def main():
765 765 | blah, blah
766 766 |
767 |+
767 768 | if __name__ == '__main__':
768 769 | main()
769 770 | # end

View File

@@ -1,223 +0,0 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E30.py:775:5: E306 [*] Expected 1 blank line before a nested definition, found 0
|
773 | def a():
774 | x = 1
775 | def b():
| ^^^ E306
776 | pass
777 | # end
|
= help: Add missing blank line
Safe fix
772 772 | # E306:3:5
773 773 | def a():
774 774 | x = 1
775 |+
775 776 | def b():
776 777 | pass
777 778 | # end
E30.py:783:5: E306 [*] Expected 1 blank line before a nested definition, found 0
|
781 | async def a():
782 | x = 1
783 | def b():
| ^^^ E306
784 | pass
785 | # end
|
= help: Add missing blank line
Safe fix
780 780 | #: E306:3:5
781 781 | async def a():
782 782 | x = 1
783 |+
783 784 | def b():
784 785 | pass
785 786 | # end
E30.py:791:5: E306 [*] Expected 1 blank line before a nested definition, found 0
|
789 | def a():
790 | x = 2
791 | def b():
| ^^^ E306
792 | x = 1
793 | def c():
|
= help: Add missing blank line
Safe fix
788 788 | #: E306:3:5 E306:5:9
789 789 | def a():
790 790 | x = 2
791 |+
791 792 | def b():
792 793 | x = 1
793 794 | def c():
E30.py:793:9: E306 [*] Expected 1 blank line before a nested definition, found 0
|
791 | def b():
792 | x = 1
793 | def c():
| ^^^ E306
794 | pass
795 | # end
|
= help: Add missing blank line
Safe fix
790 790 | x = 2
791 791 | def b():
792 792 | x = 1
793 |+
793 794 | def c():
794 795 | pass
795 796 | # end
E30.py:801:5: E306 [*] Expected 1 blank line before a nested definition, found 0
|
799 | def a():
800 | x = 1
801 | class C:
| ^^^^^ E306
802 | pass
803 | x = 2
|
= help: Add missing blank line
Safe fix
798 798 | # E306:3:5 E306:6:5
799 799 | def a():
800 800 | x = 1
801 |+
801 802 | class C:
802 803 | pass
803 804 | x = 2
E30.py:804:5: E306 [*] Expected 1 blank line before a nested definition, found 0
|
802 | pass
803 | x = 2
804 | def b():
| ^^^ E306
805 | pass
806 | # end
|
= help: Add missing blank line
Safe fix
801 801 | class C:
802 802 | pass
803 803 | x = 2
804 |+
804 805 | def b():
805 806 | pass
806 807 | # end
E30.py:813:5: E306 [*] Expected 1 blank line before a nested definition, found 0
|
811 | def bar():
812 | pass
813 | def baz(): pass
| ^^^ E306
814 | # end
|
= help: Add missing blank line
Safe fix
810 810 | def foo():
811 811 | def bar():
812 812 | pass
813 |+
813 814 | def baz(): pass
814 815 | # end
815 816 |
E30.py:820:5: E306 [*] Expected 1 blank line before a nested definition, found 0
|
818 | def foo():
819 | def bar(): pass
820 | def baz():
| ^^^ E306
821 | pass
822 | # end
|
= help: Add missing blank line
Safe fix
817 817 | # E306:3:5
818 818 | def foo():
819 819 | def bar(): pass
820 |+
820 821 | def baz():
821 822 | pass
822 823 | # end
E30.py:828:5: E306 [*] Expected 1 blank line before a nested definition, found 0
|
826 | def a():
827 | x = 2
828 | @decorator
| ^ E306
829 | def b():
830 | pass
|
= help: Add missing blank line
Safe fix
825 825 | # E306
826 826 | def a():
827 827 | x = 2
828 |+
828 829 | @decorator
829 830 | def b():
830 831 | pass
E30.py:837:5: E306 [*] Expected 1 blank line before a nested definition, found 0
|
835 | def a():
836 | x = 2
837 | @decorator
| ^ E306
838 | async def b():
839 | pass
|
= help: Add missing blank line
Safe fix
834 834 | # E306
835 835 | def a():
836 836 | x = 2
837 |+
837 838 | @decorator
838 839 | async def b():
839 840 | pass
E30.py:846:5: E306 [*] Expected 1 blank line before a nested definition, found 0
|
844 | def a():
845 | x = 2
846 | async def b():
| ^^^^^ E306
847 | pass
848 | # end
|
= help: Add missing blank line
Safe fix
843 843 | # E306
844 844 | def a():
845 845 | x = 2
846 |+
846 847 | async def b():
847 848 | pass
848 849 | # end

View File

@@ -1634,13 +1634,12 @@ fn common_section(
let line_end = checker.stylist().line_ending().as_str();
if let Some(next) = next {
if checker.enabled(Rule::NoBlankLineAfterSection) {
let num_blank_lines = context
.following_lines()
.rev()
.take_while(|line| line.trim().is_empty())
.count();
if num_blank_lines < 2 {
if context
.following_lines()
.last()
.map_or(true, |line| !line.trim().is_empty())
{
if checker.enabled(Rule::NoBlankLineAfterSection) {
let mut diagnostic = Diagnostic::new(
NoBlankLineAfterSection {
name: context.section_name().to_string(),
@@ -1658,13 +1657,13 @@ fn common_section(
} else {
// The first blank line is the line containing the closing triple quotes, so we need at
// least two.
if checker.enabled(Rule::BlankLineAfterLastSection) {
let num_blank_lines = context
.following_lines()
.rev()
.take_while(|line| line.trim().is_empty())
.count();
if num_blank_lines < 2 {
let num_blank_lines = context
.following_lines()
.rev()
.take_while(|line| line.trim().is_empty())
.count();
if num_blank_lines < 2 {
if checker.enabled(Rule::BlankLineAfterLastSection) {
let mut diagnostic = Diagnostic::new(
BlankLineAfterLastSection {
name: context.section_name().to_string(),

View File

@@ -49,7 +49,5 @@ sections.py:558:5: D214 [*] Section is over-indented ("Returns")
563 |- Returns:
563 |+ Returns:
564 564 | """
565 565 |
566 566 |

View File

@@ -61,31 +61,4 @@ sections.py:216:5: D406 [*] Section name should end with a newline ("Raises")
229 229 |
230 230 | """
sections.py:588:5: D406 [*] Section name should end with a newline ("Parameters")
|
587 | def test_lowercase_sub_section_header_should_be_valid(parameters: list[str], value: int): # noqa: D213
588 | """Test that lower case subsection header is valid even if it has the same name as section kind.
| _____^
589 | |
590 | | Parameters:
591 | | ----------
592 | | parameters:
593 | | A list of string parameters
594 | | value:
595 | | Some value
596 | | """
| |_______^ D406
|
= help: Add newline after "Parameters"
Safe fix
587 587 | def test_lowercase_sub_section_header_should_be_valid(parameters: list[str], value: int): # noqa: D213
588 588 | """Test that lower case subsection header is valid even if it has the same name as section kind.
589 589 |
590 |- Parameters:
590 |+ Parameters
591 591 | ----------
592 592 | parameters:
593 593 | A list of string parameters

View File

@@ -567,32 +567,5 @@ sections.py:558:5: D407 [*] Missing dashed underline after section ("Returns")
563 563 | Returns:
564 |+ -------
564 565 | """
565 566 |
566 567 |
sections.py:600:4: D407 [*] Missing dashed underline after section ("Parameters")
|
599 | def test_lowercase_sub_section_header_different_kind(returns: int):
600 | """Test that lower case subsection header is valid even if it is of a different kind.
| ____^
601 | |
602 | | Parameters
603 | | ------------------
604 | | returns:
605 | | some value
606 | |
607 | | """
| |______^ D407
|
= help: Add dashed line under "Parameters"
Safe fix
600 600 | """Test that lower case subsection header is valid even if it is of a different kind.
601 601 |
602 602 | Parameters
603 |+ ----------
603 604 | ------------------
604 605 | returns:
605 606 | some value

View File

@@ -61,39 +61,4 @@ sections.py:216:5: D409 [*] Section underline should match the length of its nam
227 227 | Raises:
228 228 | My attention.
sections.py:568:5: D409 [*] Section underline should match the length of its name ("Other Parameters")
|
567 | def test_method_should_be_correctly_capitalized(parameters: list[str], other_parameters: dict[str, str]): # noqa: D213
568 | """Test parameters and attributes sections are capitalized correctly.
| _____^
569 | |
570 | | Parameters
571 | | ----------
572 | | parameters:
573 | | A list of string parameters
574 | | other_parameters:
575 | | A dictionary of string attributes
576 | |
577 | | Other Parameters
578 | | ----------
579 | | other_parameters:
580 | | A dictionary of string attributes
581 | | parameters:
582 | | A list of string parameters
583 | |
584 | | """
| |_______^ D409
|
= help: Adjust underline length to match "Other Parameters"
Safe fix
575 575 | A dictionary of string attributes
576 576 |
577 577 | Other Parameters
578 |- ----------
578 |+ ----------------
579 579 | other_parameters:
580 580 | A dictionary of string attributes
581 581 | parameters:

View File

@@ -21,9 +21,10 @@ D413.py:1:1: D413 [*] Missing blank line after last section ("Returns")
7 7 | Returns:
8 8 | the value
9 |+
9 10 | """
10 11 |
11 12 |
10 |+
9 11 | """
10 12 |
11 13 |
D413.py:13:5: D413 [*] Missing blank line after last section ("Returns")
|

View File

@@ -161,33 +161,5 @@ sections.py:558:5: D413 [*] Missing blank line after last section ("Returns")
563 563 | Returns:
564 |+
564 565 | """
565 566 |
566 567 |
sections.py:588:5: D413 [*] Missing blank line after last section ("Parameters")
|
587 | def test_lowercase_sub_section_header_should_be_valid(parameters: list[str], value: int): # noqa: D213
588 | """Test that lower case subsection header is valid even if it has the same name as section kind.
| _____^
589 | |
590 | | Parameters:
591 | | ----------
592 | | parameters:
593 | | A list of string parameters
594 | | value:
595 | | Some value
596 | | """
| |_______^ D413
|
= help: Add blank line after "Parameters"
Safe fix
593 593 | A list of string parameters
594 594 | value:
595 595 | Some value
596 |+
596 597 | """
597 598 |
598 599 |

View File

@@ -31,13 +31,7 @@ use ruff_text_size::TextSize;
/// ```
///
/// ## References
/// Mypy supports a [built-in setting](https://mypy.readthedocs.io/en/stable/error_code_list2.html#check-that-type-ignore-include-an-error-code-ignore-without-code)
/// to enforce that all `type: ignore` annotations include an error code, akin
/// to enabling this rule:
/// ```toml
/// [tool.mypy]
/// enable_error_code = ["ignore-without-code"]
/// ```
/// - [mypy](https://mypy.readthedocs.io/en/stable/common_issues.html#spurious-errors-and-locally-silencing-the-checker)
#[violation]
pub struct BlanketTypeIgnore;

View File

@@ -84,10 +84,10 @@ pub(crate) fn comparison_with_itself(
{
continue;
}
let [Expr::Name(left_arg)] = &*left_call.arguments.args else {
let [Expr::Name(left_arg)] = left_call.arguments.args.as_slice() else {
continue;
};
let [Expr::Name(right_right)] = &*right_call.arguments.args else {
let [Expr::Name(right_right)] = right_call.arguments.args.as_slice() else {
continue;
};
if left_arg.id != right_right.id {

View File

@@ -59,7 +59,7 @@ pub(crate) fn duplicate_bases(checker: &mut Checker, name: &str, arguments: Opti
let mut seen: FxHashSet<&str> =
FxHashSet::with_capacity_and_hasher(bases.len(), BuildHasherDefault::default());
for base in bases.iter() {
for base in bases {
if let Expr::Name(ast::ExprName { id, .. }) = base {
if !seen.insert(id) {
checker.diagnostics.push(Diagnostic::new(

View File

@@ -45,7 +45,7 @@ impl AlwaysFixableViolation for LiteralMembership {
/// PLR6201
pub(crate) fn literal_membership(checker: &mut Checker, compare: &ast::ExprCompare) {
let [op] = &*compare.ops else {
let [op] = compare.ops.as_slice() else {
return;
};
@@ -53,7 +53,7 @@ pub(crate) fn literal_membership(checker: &mut Checker, compare: &ast::ExprCompa
return;
}
let [right] = &*compare.comparators else {
let [right] = compare.comparators.as_slice() else {
return;
};

View File

@@ -106,7 +106,7 @@ fn collect_nested_args(min_max: MinMax, args: &[Expr], semantic: &SemanticModel)
range: _,
}) = arg
{
if let [arg] = &**args {
if let [arg] = args.as_slice() {
if arg.as_starred_expr().is_none() {
let new_arg = Expr::Starred(ast::ExprStarred {
value: Box::new(arg.clone()),
@@ -164,8 +164,8 @@ pub(crate) fn nested_min_max(
let flattened_expr = Expr::Call(ast::ExprCall {
func: Box::new(func.clone()),
arguments: Arguments {
args: collect_nested_args(min_max, args, checker.semantic()).into_boxed_slice(),
keywords: Box::from(keywords),
args: collect_nested_args(min_max, args, checker.semantic()),
keywords: keywords.to_owned(),
range: TextRange::default(),
},
range: TextRange::default(),

View File

@@ -96,7 +96,7 @@ pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast:
};
// Enforced via `is_allowed_value`.
let [right] = &**comparators else {
let [right] = comparators.as_slice() else {
return;
};
@@ -136,14 +136,14 @@ pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast:
checker.generator().expr(&Expr::Compare(ast::ExprCompare {
left: Box::new(value.as_expr().clone()),
ops: match bool_op.op {
BoolOp::Or => Box::from([CmpOp::In]),
BoolOp::And => Box::from([CmpOp::NotIn]),
BoolOp::Or => vec![CmpOp::In],
BoolOp::And => vec![CmpOp::NotIn],
},
comparators: Box::from([Expr::Tuple(ast::ExprTuple {
comparators: vec![Expr::Tuple(ast::ExprTuple {
elts: comparators.iter().copied().cloned().collect(),
range: TextRange::default(),
ctx: ExprContext::Load,
})]),
})],
range: bool_op.range(),
})),
bool_op.range(),
@@ -169,7 +169,7 @@ fn is_allowed_value(bool_op: BoolOp, value: &Expr) -> bool {
};
// Ignore, e.g., `foo == bar == baz`.
let [op] = &**ops else {
let [op] = ops.as_slice() else {
return false;
};
@@ -181,7 +181,7 @@ fn is_allowed_value(bool_op: BoolOp, value: &Expr) -> bool {
}
// Ignore self-comparisons, e.g., `foo == foo`.
let [right] = &**comparators else {
let [right] = comparators.as_slice() else {
return false;
};
if ComparableExpr::from(left) == ComparableExpr::from(right) {

View File

@@ -1,11 +1,10 @@
use std::hash::BuildHasherDefault;
use rustc_hash::FxHashSet;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Expr, ExprCall, ExprDict, ExprStringLiteral};
use ruff_python_ast::{Arguments, Expr, ExprCall, ExprDict, ExprStringLiteral};
use ruff_text_size::Ranged;
use rustc_hash::FxHashSet;
use crate::checkers::ast::Checker;
@@ -38,14 +37,15 @@ impl Violation for RepeatedKeywordArgument {
}
pub(crate) fn repeated_keyword_argument(checker: &mut Checker, call: &ExprCall) {
let ExprCall { arguments, .. } = call;
let ExprCall {
arguments: Arguments { keywords, .. },
..
} = call;
let mut seen = FxHashSet::with_capacity_and_hasher(
arguments.keywords.len(),
BuildHasherDefault::default(),
);
let mut seen =
FxHashSet::with_capacity_and_hasher(keywords.len(), BuildHasherDefault::default());
for keyword in arguments.keywords.iter() {
for keyword in keywords {
if let Some(id) = &keyword.arg {
// Ex) `func(a=1, a=2)`
if !seen.insert(id.as_str()) {

View File

@@ -111,7 +111,7 @@ pub(crate) fn unnecessary_dunder_call(checker: &mut Checker, call: &ast::ExprCal
let mut title: Option<String> = None;
if let Some(dunder) = DunderReplacement::from_method(attr) {
match (&*call.arguments.args, dunder) {
match (call.arguments.args.as_slice(), dunder) {
([], DunderReplacement::Builtin(replacement, message)) => {
if !checker.semantic().is_builtin(replacement) {
return;

View File

@@ -110,7 +110,7 @@ fn generate_keyword_fix(checker: &Checker, call: &ast::ExprCall) -> Fix {
.generator()
.expr(&Expr::StringLiteral(ast::ExprStringLiteral {
value: ast::StringLiteralValue::single(ast::StringLiteral {
value: "locale".to_string().into_boxed_str(),
value: "locale".to_string(),
unicode: false,
range: TextRange::default(),
}),

View File

@@ -21,7 +21,7 @@ pub(crate) fn remove_import_members(contents: &str, members: &[&str]) -> String
let last_range = names.last_mut().unwrap();
*last_range = TextRange::new(last_range.start(), range.end());
} else {
if members.contains(&&**name) {
if members.contains(&name.as_str()) {
removal_indices.push(names.len());
}
names.push(range);

View File

@@ -216,8 +216,8 @@ fn create_class_def_stmt(typename: &str, body: Vec<Stmt>, base_class: &Expr) ->
ast::StmtClassDef {
name: Identifier::new(typename.to_string(), TextRange::default()),
arguments: Some(Box::new(Arguments {
args: Box::from([base_class.clone()]),
keywords: Box::from([]),
args: vec![base_class.clone()],
keywords: vec![],
range: TextRange::default(),
})),
body,

View File

@@ -148,10 +148,10 @@ fn create_class_def_stmt(
ast::StmtClassDef {
name: Identifier::new(class_name.to_string(), TextRange::default()),
arguments: Some(Box::new(Arguments {
args: Box::from([base_class.clone()]),
args: vec![base_class.clone()],
keywords: match total_keyword {
Some(keyword) => Box::from([keyword.clone()]),
None => Box::from([]),
Some(keyword) => vec![keyword.clone()],
None => vec![],
},
range: TextRange::default(),
})),
@@ -226,7 +226,7 @@ fn fields_from_keywords(keywords: &[Keyword]) -> Option<Vec<Stmt>> {
/// Match the fields and `total` keyword from a `TypedDict` call.
fn match_fields_and_total(arguments: &Arguments) -> Option<(Vec<Stmt>, Option<&Keyword>)> {
match (&*arguments.args, &*arguments.keywords) {
match (arguments.args.as_slice(), arguments.keywords.as_slice()) {
// Ex) `TypedDict("MyType", {"a": int, "b": str})`
([_typename, fields], [..]) => {
let total = arguments.find_keyword("total");

View File

@@ -71,7 +71,7 @@ impl<'a> FormatSummaryValues<'a> {
let mut extracted_args: Vec<&Expr> = Vec::new();
let mut extracted_kwargs: FxHashMap<&str, &Expr> = FxHashMap::default();
for arg in call.arguments.args.iter() {
for arg in &call.arguments.args {
if matches!(arg, Expr::Starred(..))
|| contains_quotes(locator.slice(arg))
|| locator.contains_line_break(arg.range())
@@ -80,7 +80,7 @@ impl<'a> FormatSummaryValues<'a> {
}
extracted_args.push(arg);
}
for keyword in call.arguments.keywords.iter() {
for keyword in &call.arguments.keywords {
let Keyword {
arg,
value,

View File

@@ -90,7 +90,7 @@ pub(crate) fn outdated_version_block(checker: &mut Checker, stmt_if: &StmtIf) {
continue;
};
let ([op], [comparison]) = (&**ops, &**comparators) else {
let ([op], [comparison]) = (ops.as_slice(), comparators.as_slice()) else {
continue;
};

Some files were not shown because too many files have changed in this diff Show More