Compare commits

..

1 Commits

Author SHA1 Message Date
Charlie Marsh
8c876f319b Only log fix failures in debug 2023-08-21 18:12:49 -04:00
301 changed files with 42922 additions and 42171 deletions

32
Cargo.lock generated
View File

@@ -812,7 +812,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.286"
version = "0.0.285"
dependencies = [
"anyhow",
"clap",
@@ -876,10 +876,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@@ -2066,7 +2064,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.286"
version = "0.0.285"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2130,7 +2128,6 @@ dependencies = [
"typed-arena",
"unicode-width",
"unicode_names2",
"uuid",
"wsl",
]
@@ -2167,7 +2164,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.286"
version = "0.0.285"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2343,7 +2340,6 @@ dependencies = [
"insta",
"is-macro",
"itertools",
"memchr",
"once_cell",
"ruff_formatter",
"ruff_python_ast",
@@ -2403,7 +2399,6 @@ dependencies = [
"ruff_text_size",
"rustc-hash",
"static_assertions",
"test-case",
"tiny-keccak",
"unic-emoji-char",
"unic-ucd-ident",
@@ -3349,26 +3344,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.4.1"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
dependencies = [
"getrandom",
"rand",
"uuid-macro-internal",
"wasm-bindgen",
]
[[package]]
name = "uuid-macro-internal"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7e1ba1f333bd65ce3c9f27de592fcbc256dafe3af2717f56d7c87761fbaccf4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
]
checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
[[package]]
name = "valuable"

View File

@@ -50,7 +50,6 @@ tracing = "0.1.37"
tracing-indicatif = "0.3.4"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
unicode-width = "0.1.10"
uuid = { version = "1.4.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
wsl = { version = "0.1.0" }
# v1.0.1

View File

@@ -140,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.286
rev: v0.0.285
hooks:
- id: ruff
```

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.286"
version = "0.0.285"
description = """
Convert Flake8 configuration files to Ruff configuration files.
"""

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.286"
version = "0.0.285"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -77,7 +77,6 @@ toml = { workspace = true }
typed-arena = { version = "2.0.2" }
unicode-width = { workspace = true }
unicode_names2 = { version = "0.6.0", git = "https://github.com/youknowone/unicode_names2.git", rev = "4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" }
uuid = { workspace = true, features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
wsl = { version = "0.1.0" }
[dev-dependencies]

View File

@@ -19,12 +19,3 @@ def foo(x, y, z):
class A():
pass
# b = c
dictionary = {
# "key1": 123, # noqa: ERA001
# "key2": 456,
# "key3": 789, # test
}
#import os # noqa

View File

@@ -152,9 +152,3 @@ def f(a: Union[str, bytes, Any]) -> None: ...
def f(a: Optional[Any]) -> None: ...
def f(a: Annotated[Any, ...]) -> None: ...
def f(a: "Union[str, bytes, Any]") -> None: ...
class Foo:
@decorator()
def __init__(self: "Foo", foo: int):
...

View File

@@ -230,10 +230,6 @@ def timedelta_okay(value=dt.timedelta(hours=1)):
def path_okay(value=Path(".")):
pass
# B008 allow arbitrary call with immutable annotation
def immutable_annotation_call(value: Sequence[int] = foo()):
pass
# B006 and B008
# We should handle arbitrary nesting of these B008.
def nested_combo(a=[float(3), dt.datetime.now()]):

View File

@@ -1,18 +0,0 @@
import custom
from custom import ImmutableTypeB
def okay(foo: ImmutableTypeB = []):
...
def okay(foo: custom.ImmutableTypeA = []):
...
def okay(foo: custom.ImmutableTypeB = []):
...
def error_due_to_missing_import(foo: ImmutableTypeA = []):
...

View File

@@ -1,7 +1,6 @@
from typing import List
import fastapi
import custom
from fastapi import Query
@@ -17,9 +16,5 @@ def okay(data: List[str] = Query(None)):
...
def okay(data: custom.ImmutableTypeA = foo()):
...
def error_due_to_missing_import(data: List[str] = Depends(None)):
...

View File

@@ -15,6 +15,11 @@ filter(func, map(lambda v: v, nums))
_ = f"{set(map(lambda x: x % 2 == 0, nums))}"
_ = f"{dict(map(lambda v: (v, v**2), nums))}"
# Error, but unfixable.
# For simple expressions, this could be: `(x if x else 1 for x in nums)`.
# For more complex expressions, this would differ: `(x + 2 if x else 3 for x in nums)`.
map(lambda x=1: x, nums)
# False negatives.
map(lambda x=2, y=1: x + y, nums, nums)
set(map(lambda x, y: x, nums, nums))
@@ -32,8 +37,3 @@ map(lambda x: lambda: x, range(4))
# Error: the `x` is overridden by the inner lambda.
map(lambda x: lambda x: x, range(4))
# Ok because of the default parameters, and variadic arguments.
map(lambda x=1: x, nums)
map(lambda *args: len(args), range(4))
map(lambda **kwargs: len(kwargs), range(4))

View File

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

View File

@@ -43,12 +43,3 @@ message
assert something # OK
assert something and something_else # Error
assert something and something_else and something_third # Error
def test_multiline():
assert something and something_else; x = 1
x = 1; assert something and something_else
x = 1; \
assert something and something_else

View File

@@ -320,9 +320,3 @@ def end_of_statement():
if True:
return "" \
; # type: ignore
def end_of_file():
if False:
return 1
x = 2 \

View File

@@ -1,3 +0,0 @@
import os
import pandas
import foo.baz

View File

@@ -1,2 +0,0 @@
[tool.ruff]
line-length = 88

View File

@@ -1,37 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"import os\n",
"\n",
"math.pi"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff)",
"language": "python",
"name": "ruff"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -1,37 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"import os\n",
"\n",
"math.pi"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff)",
"language": "python",
"name": "ruff"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

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

View File

@@ -1,13 +0,0 @@
def func():
"""\
"""
def func():
"""\\
"""
def func():
"""\ \
"""

View File

@@ -99,16 +99,3 @@ import foo.bar as bop
import foo.bar.baz
print(bop.baz.read_csv("test.csv"))
# Test: isolated deletions.
if TYPE_CHECKING:
import a1
import a2
match *0, 1, *2:
case 0,:
import b1
import b2

View File

@@ -2,5 +2,6 @@
"{bar}{}".format(1, bar=2, spam=3) # F522
"{bar:{spam}}".format(bar=2, spam=3) # No issues
"{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
# Not fixable
(''
.format(x=2)) # F522
.format(x=2))

View File

@@ -28,6 +28,6 @@
"{1}{3}".format(1, 2, 3, 4) # F523, # F524
"{1} {8}".format(0, 1) # F523, # F524
# Multiline
# Not fixable
(''
.format(2))

View File

@@ -154,14 +154,3 @@ def f() -> None:
print("hello")
except A as e :
print("oh no!")
def f():
x = 1
y = 2
def f():
x = 1
y = 2

View File

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

View File

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

View File

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

View File

@@ -9,22 +9,13 @@ foo != "a" and foo != "b" and foo != "c"
foo == a or foo == "b" or foo == 3 # Mixed types.
# False negatives (the current implementation doesn't support Yoda conditions).
"a" == foo or "b" == foo or "c" == foo
"a" != foo and "b" != foo and "c" != foo
"a" == foo or foo == "b" or "c" == foo
foo == bar or baz == foo or qux == foo
foo == "a" or "b" == foo or foo == "c"
foo != "a" and "b" != foo and foo != "c"
foo == "a" or foo == "b" or "c" == bar or "d" == bar # Multiple targets
foo.bar == "a" or foo.bar == "b" # Attributes.
# OK
foo == "a" and foo == "b" and foo == "c" # `and` mixed with `==`.
@@ -45,9 +36,3 @@ foo != "a" # Single comparison.
foo == "a" == "b" or foo == "c" # Multiple comparisons.
foo == bar == "b" or foo == "c" # Multiple comparisons.
foo == foo or foo == bar # Self-comparison.
foo[0] == "a" or foo[0] == "b" # Subscripts.
foo() == "a" or foo() == "b" # Calls.

View File

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

View File

@@ -1,7 +0,0 @@
"""
# coding=utf8""" # empty comment
"""
Invalid coding declaration since it is nested inside a docstring
The following empty comment tests for false positives as our implementation visits comments
"""

View File

@@ -1,7 +0,0 @@
# coding=utf8
print("Hello world")
"""
Regression test for https://github.com/astral-sh/ruff/issues/6756
The leading space must be removed to prevent invalid syntax.
"""

View File

@@ -1,7 +0,0 @@
# coding=utf8
print("Hello world")
"""
Regression test for https://github.com/astral-sh/ruff/issues/6756
The leading tab must be removed to prevent invalid syntax.
"""

View File

@@ -1,6 +0,0 @@
print("foo") # coding=utf8
print("Hello world")
"""
Invalid coding declaration due to a statement before the comment
"""

View File

@@ -1,7 +0,0 @@
x = 1 \
# coding=utf8
x = 2
"""
Invalid coding declaration due to continuation on preceding line
"""

View File

@@ -31,7 +31,6 @@ bool("foo")
bool("")
bool(b"")
bool(1.0)
int().denominator
# These become string or byte literals
str()
@@ -50,6 +49,3 @@ float(1.0)
bool()
bool(True)
bool(False)
# These become a literal but retain parentheses
int(1).denominator

View File

@@ -1,11 +0,0 @@
#import os # noqa
#import os # noqa: ERA001
dictionary = {
# "key1": 123, # noqa: ERA001
# "key2": 456, # noqa
# "key3": 789,
}
#import os # noqa: E501

View File

@@ -11,7 +11,7 @@ use ruff_python_trivia::{
has_leading_content, is_python_whitespace, PythonWhitespace, SimpleTokenKind, SimpleTokenizer,
};
use ruff_source_file::{Locator, NewlineWithTrailingNewline};
use ruff_text_size::{TextLen, TextSize};
use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::autofix::codemods;
@@ -48,7 +48,7 @@ pub(crate) fn delete_stmt(
} else if has_leading_content(stmt.start(), locator) {
Edit::range_deletion(stmt.range())
} else if let Some(start) = indexer.preceded_by_continuations(stmt.start(), locator) {
Edit::deletion(start, stmt.end())
Edit::range_deletion(TextRange::new(start, stmt.end()))
} else {
let range = locator.full_lines_range(stmt.range());
Edit::range_deletion(range)
@@ -133,8 +133,10 @@ pub(crate) fn remove_argument<T: Ranged>(
// Case 3: argument or keyword is the only node, so delete the arguments (but preserve
// parentheses, if needed).
Ok(match parentheses {
Parentheses::Remove => Edit::range_deletion(arguments.range()),
Parentheses::Preserve => Edit::range_replacement("()".to_string(), arguments.range()),
Parentheses::Remove => Edit::deletion(arguments.start(), arguments.end()),
Parentheses::Preserve => {
Edit::replacement("()".to_string(), arguments.start(), arguments.end())
}
})
}
}
@@ -226,25 +228,25 @@ fn trailing_semicolon(offset: TextSize, locator: &Locator) -> Option<TextSize> {
fn next_stmt_break(semicolon: TextSize, locator: &Locator) -> TextSize {
let start_location = semicolon + TextSize::from(1);
for line in
NewlineWithTrailingNewline::with_offset(locator.after(start_location), start_location)
{
let contents = &locator.contents()[usize::from(start_location)..];
for line in NewlineWithTrailingNewline::from(contents) {
let trimmed = line.trim_whitespace();
// Skip past any continuations.
if trimmed.starts_with('\\') {
continue;
}
return if trimmed.is_empty() {
// If the line is empty, then despite the previous statement ending in a
// semicolon, we know that it's not a multi-statement line.
line.start()
} else {
// Otherwise, find the start of the next statement. (Or, anything that isn't
// whitespace.)
let relative_offset = line.find(|c: char| !is_python_whitespace(c)).unwrap();
line.start() + TextSize::try_from(relative_offset).unwrap()
};
return start_location
+ if trimmed.is_empty() {
// If the line is empty, then despite the previous statement ending in a
// semicolon, we know that it's not a multi-statement line.
line.start()
} else {
// Otherwise, find the start of the next statement. (Or, anything that isn't
// whitespace.)
let relative_offset = line.find(|c: char| !is_python_whitespace(c)).unwrap();
line.start() + TextSize::try_from(relative_offset).unwrap()
};
}
locator.line_end(start_location)

View File

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

View File

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

View File

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

View File

@@ -381,34 +381,34 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Ok(summary) => {
if checker.enabled(Rule::StringDotFormatExtraNamedArguments) {
pyflakes::rules::string_dot_format_extra_named_arguments(
checker, call, &summary, keywords,
checker, &summary, keywords, location,
);
}
if checker
.enabled(Rule::StringDotFormatExtraPositionalArguments)
{
pyflakes::rules::string_dot_format_extra_positional_arguments(
checker, call, &summary, args,
checker, &summary, args, location,
);
}
if checker.enabled(Rule::StringDotFormatMissingArguments) {
pyflakes::rules::string_dot_format_missing_argument(
checker, call, &summary, args, keywords,
checker, &summary, args, keywords, location,
);
}
if checker.enabled(Rule::StringDotFormatMixingAutomatic) {
pyflakes::rules::string_dot_format_mixing_automatic(
checker, call, &summary,
checker, &summary, location,
);
}
if checker.enabled(Rule::FormatLiterals) {
pyupgrade::rules::format_literals(checker, call, &summary);
pyupgrade::rules::format_literals(checker, &summary, call);
}
if checker.enabled(Rule::FString) {
pyupgrade::rules::f_strings(
checker,
call,
&summary,
expr,
value,
checker.settings.line_length,
);
@@ -441,11 +441,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pyupgrade::rules::redundant_open_modes(checker, call);
}
if checker.enabled(Rule::NativeLiterals) {
pyupgrade::rules::native_literals(
checker,
call,
checker.semantic().current_expression_parent(),
);
pyupgrade::rules::native_literals(checker, expr, func, args, keywords);
}
if checker.enabled(Rule::OpenAlias) {
pyupgrade::rules::open_alias(checker, expr, func);

View File

@@ -52,8 +52,7 @@ use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind};
use ruff_python_semantic::analyze::{typing, visibility};
use ruff_python_semantic::{
BindingFlags, BindingId, BindingKind, Exceptions, Export, FromImport, Globals, Import, Module,
ModuleKind, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport,
SubmoduleImport,
ModuleKind, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport, SubmoduleImport,
};
use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS};
use ruff_source_file::Locator;
@@ -194,6 +193,24 @@ impl<'a> Checker<'a> {
}
}
/// Returns the [`IsolationLevel`] to isolate fixes for the current statement.
///
/// The primary use-case for fix isolation is to ensure that we don't delete all statements
/// in a given indented block, which would cause a syntax error. We therefore need to ensure
/// that we delete at most one statement per indented block per fixer pass. Fix isolation should
/// thus be applied whenever we delete a statement, but can otherwise be omitted.
pub(crate) fn statement_isolation(&self) -> IsolationLevel {
IsolationLevel::Group(self.semantic.current_statement_id().into())
}
/// Returns the [`IsolationLevel`] to isolate fixes in the current statement's parent.
pub(crate) fn parent_isolation(&self) -> IsolationLevel {
self.semantic
.current_statement_parent_id()
.map(|node_id| IsolationLevel::Group(node_id.into()))
.unwrap_or_default()
}
/// The [`Locator`] for the current file, which enables extraction of source code from byte
/// offsets.
pub(crate) const fn locator(&self) -> &'a Locator<'a> {
@@ -242,18 +259,6 @@ impl<'a> Checker<'a> {
pub(crate) const fn any_enabled(&self, rules: &[Rule]) -> bool {
self.settings.rules.any_enabled(rules)
}
/// Returns the [`IsolationLevel`] to isolate fixes for a given node.
///
/// The primary use-case for fix isolation is to ensure that we don't delete all statements
/// in a given indented block, which would cause a syntax error. We therefore need to ensure
/// that we delete at most one statement per indented block per fixer pass. Fix isolation should
/// thus be applied whenever we delete a statement, but can otherwise be omitted.
pub(crate) fn isolation(node_id: Option<NodeId>) -> IsolationLevel {
node_id
.map(|node_id| IsolationLevel::Group(node_id.into()))
.unwrap_or_default()
}
}
impl<'a, 'b> Visitor<'b> for Checker<'a>
@@ -262,7 +267,7 @@ where
{
fn visit_stmt(&mut self, stmt: &'b Stmt) {
// Step 0: Pre-processing
self.semantic.push_node(stmt);
self.semantic.push_statement(stmt);
// Track whether we've seen docstrings, non-imports, etc.
match stmt {
@@ -774,7 +779,7 @@ where
analyze::statement(stmt, self);
self.semantic.flags = flags_snapshot;
self.semantic.pop_node();
self.semantic.pop_statement();
}
fn visit_annotation(&mut self, expr: &'b Expr) {
@@ -810,7 +815,7 @@ where
return;
}
self.semantic.push_node(expr);
self.semantic.push_expression(expr);
// Store the flags prior to any further descent, so that we can restore them after visiting
// the node.
@@ -1230,7 +1235,7 @@ where
analyze::expression(expr, self);
self.semantic.flags = flags_snapshot;
self.semantic.pop_node();
self.semantic.pop_expression();
}
fn visit_except_handler(&mut self, except_handler: &'b ExceptHandler) {

View File

@@ -110,8 +110,8 @@ pub(crate) fn check_noqa(
let mut diagnostic =
Diagnostic::new(UnusedNOQA { codes: None }, directive.range());
if settings.rules.should_fix(diagnostic.kind.rule()) {
diagnostic
.set_fix(Fix::automatic(delete_noqa(directive.range(), locator)));
#[allow(deprecated)]
diagnostic.set_fix_from_edit(delete_noqa(directive.range(), locator));
}
diagnostics.push(diagnostic);
}
@@ -175,12 +175,12 @@ pub(crate) fn check_noqa(
);
if settings.rules.should_fix(diagnostic.kind.rule()) {
if valid_codes.is_empty() {
diagnostic.set_fix(Fix::automatic(delete_noqa(
directive.range(),
locator,
)));
#[allow(deprecated)]
diagnostic
.set_fix_from_edit(delete_noqa(directive.range(), locator));
} else {
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
format!("# noqa: {}", valid_codes.join(", ")),
directive.range(),
)));

View File

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

View File

@@ -1,11 +1,9 @@
use crate::autofix::codemods::CodegenStylist;
use anyhow::{bail, Result};
use libcst_native::{
Arg, Attribute, Call, Comparison, CompoundStatement, Dict, Expression, FunctionDef,
GeneratorExp, If, Import, ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda,
ListComp, Module, Name, SmallStatement, Statement, Suite, Tuple, With,
};
use ruff_python_codegen::Stylist;
pub(crate) fn match_module(module_text: &str) -> Result<Module> {
match libcst_native::parse_module(module_text, None) {
@@ -14,6 +12,13 @@ pub(crate) fn match_module(module_text: &str) -> Result<Module> {
}
}
pub(crate) fn match_expression(expression_text: &str) -> Result<Expression> {
match libcst_native::parse_expression(expression_text) {
Ok(expression) => Ok(expression),
Err(_) => bail!("Failed to extract expression from source"),
}
}
pub(crate) fn match_statement(statement_text: &str) -> Result<Statement> {
match libcst_native::parse_statement(statement_text) {
Ok(statement) => Ok(statement),
@@ -200,59 +205,3 @@ pub(crate) fn match_if<'a, 'b>(statement: &'a mut Statement<'b>) -> Result<&'a m
bail!("Expected Statement::Compound")
}
}
/// Given the source code for an expression, return the parsed [`Expression`].
///
/// If the expression is not guaranteed to be valid as a standalone expression (e.g., if it may
/// span multiple lines and/or require parentheses), use [`transform_expression`] instead.
pub(crate) fn match_expression(expression_text: &str) -> Result<Expression> {
match libcst_native::parse_expression(expression_text) {
Ok(expression) => Ok(expression),
Err(_) => bail!("Failed to extract expression from source"),
}
}
/// Run a transformation function over an expression.
///
/// Passing an expression to [`match_expression`] directly can lead to parse errors if the
/// expression is not a valid standalone expression (e.g., it was parenthesized in the original
/// source). This method instead wraps the expression in "fake" parentheses, runs the
/// transformation, then removes the "fake" parentheses.
pub(crate) fn transform_expression(
source_code: &str,
stylist: &Stylist,
func: impl FnOnce(Expression) -> Result<Expression>,
) -> Result<String> {
// Wrap the expression in parentheses.
let source_code = format!("({source_code})");
let expression = match_expression(&source_code)?;
// Run the function on the expression.
let expression = func(expression)?;
// Codegen the expression.
let mut source_code = expression.codegen_stylist(stylist);
// Drop the outer parentheses.
source_code.drain(0..1);
source_code.drain(source_code.len() - 1..source_code.len());
Ok(source_code)
}
/// Like [`transform_expression`], but operates on the source code of the expression, rather than
/// the parsed [`Expression`]. This _shouldn't_ exist, but does to accommodate lifetime issues.
pub(crate) fn transform_expression_text(
source_code: &str,
func: impl FnOnce(String) -> Result<String>,
) -> Result<String> {
// Wrap the expression in parentheses.
let source_code = format!("({source_code})");
// Run the function on the expression.
let mut transformed = func(source_code)?;
// Drop the outer parentheses.
transformed.drain(0..1);
transformed.drain(transformed.len() - 1..transformed.len());
Ok(transformed)
}

View File

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

View File

@@ -3,14 +3,14 @@
/// When we lint a jupyter notebook, we have to translate the row/column based on
/// [`ruff_text_size::TextSize`] to jupyter notebook cell/row/column.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NotebookIndex {
pub struct JupyterIndex {
/// Enter a row (1-based), get back the cell (1-based)
pub(super) row_to_cell: Vec<u32>,
/// Enter a row (1-based), get back the row in cell (1-based)
pub(super) row_to_row_in_cell: Vec<u32>,
}
impl NotebookIndex {
impl JupyterIndex {
/// Returns the cell number (1-based) for the given row (1-based).
pub fn cell(&self, row: usize) -> Option<u32> {
self.row_to_cell.get(row).copied()

View File

@@ -9,7 +9,6 @@ use itertools::Itertools;
use once_cell::sync::OnceCell;
use serde::Serialize;
use serde_json::error::Category;
use uuid::Uuid;
use ruff_diagnostics::Diagnostic;
use ruff_python_parser::lexer::lex;
@@ -18,7 +17,7 @@ use ruff_source_file::{NewlineWithTrailingNewline, UniversalNewlineIterator};
use ruff_text_size::{TextRange, TextSize};
use crate::autofix::source_map::{SourceMap, SourceMarker};
use crate::jupyter::index::NotebookIndex;
use crate::jupyter::index::JupyterIndex;
use crate::jupyter::schema::{Cell, RawNotebook, SortAlphabetically, SourceValue};
use crate::rules::pycodestyle::rules::SyntaxError;
use crate::IOError;
@@ -83,8 +82,8 @@ impl Cell {
Cell::Code(cell) => &cell.source,
_ => return false,
};
// Ignore cells containing cell magic as they act on the entire cell
// as compared to line magic which acts on a single line.
// Ignore cells containing cell magic. This is different from line magic
// which is allowed and ignored by the parser.
!match source {
SourceValue::String(string) => string
.lines()
@@ -107,7 +106,7 @@ pub struct Notebook {
source_code: String,
/// The index of the notebook. This is used to map between the concatenated
/// source code and the original notebook.
index: OnceCell<NotebookIndex>,
index: OnceCell<JupyterIndex>,
/// The raw notebook i.e., the deserialized version of JSON string.
raw: RawNotebook,
/// The offsets of each cell in the concatenated source code. This includes
@@ -157,7 +156,7 @@ impl Notebook {
TextRange::default(),
)
})?;
let mut raw_notebook: RawNotebook = match serde_json::from_reader(reader.by_ref()) {
let raw_notebook: RawNotebook = match serde_json::from_reader(reader.by_ref()) {
Ok(notebook) => notebook,
Err(err) => {
// Translate the error into a diagnostic
@@ -263,23 +262,6 @@ impl Notebook {
cell_offsets.push(current_offset);
}
// Add cell ids to 4.5+ notebooks if they are missing
// https://github.com/astral-sh/ruff/issues/6834
// https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md#required-field
if raw_notebook.nbformat == 4 && raw_notebook.nbformat_minor >= 5 {
for cell in &mut raw_notebook.cells {
let id = match cell {
Cell::Code(cell) => &mut cell.id,
Cell::Markdown(cell) => &mut cell.id,
Cell::Raw(cell) => &mut cell.id,
};
if id.is_none() {
// https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md#questions
*id = Some(Uuid::new_v4().to_string());
}
}
}
Ok(Self {
raw: raw_notebook,
index: OnceCell::new(),
@@ -386,7 +368,7 @@ impl Notebook {
///
/// The index building is expensive as it needs to go through the content of
/// every valid code cell.
fn build_index(&self) -> NotebookIndex {
fn build_index(&self) -> JupyterIndex {
let mut row_to_cell = vec![0];
let mut row_to_row_in_cell = vec![0];
@@ -413,7 +395,7 @@ impl Notebook {
row_to_row_in_cell.extend(1..=line_count);
}
NotebookIndex {
JupyterIndex {
row_to_cell,
row_to_row_in_cell,
}
@@ -431,7 +413,7 @@ impl Notebook {
/// The index is built only once when required. This is only used to
/// report diagnostics, so by that time all of the autofixes must have
/// been applied if `--fix` was passed.
pub(crate) fn index(&self) -> &NotebookIndex {
pub(crate) fn index(&self) -> &JupyterIndex {
self.index.get_or_init(|| self.build_index())
}
@@ -491,14 +473,12 @@ mod tests {
use anyhow::Result;
use test_case::test_case;
use crate::jupyter::index::NotebookIndex;
use crate::jupyter::index::JupyterIndex;
use crate::jupyter::schema::Cell;
use crate::jupyter::Notebook;
use crate::registry::Rule;
use crate::source_kind::SourceKind;
use crate::test::{
read_jupyter_notebook, test_contents, test_notebook_path, test_resource_path,
TestedNotebook,
read_jupyter_notebook, test_notebook_path, test_resource_path, TestedNotebook,
};
use crate::{assert_messages, settings};
@@ -579,7 +559,7 @@ print("after empty cells")
);
assert_eq!(
notebook.index(),
&NotebookIndex {
&JupyterIndex {
row_to_cell: vec![0, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 5, 7, 7, 8],
row_to_row_in_cell: vec![0, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 1, 1, 2, 1],
}
@@ -679,28 +659,4 @@ print("after empty cells")
Ok(())
}
// Version <4.5, don't emit cell ids
#[test_case(Path::new("no_cell_id.ipynb"), false; "no_cell_id")]
// Version 4.5, cell ids are missing and need to be added
#[test_case(Path::new("add_missing_cell_id.ipynb"), true; "add_missing_cell_id")]
fn test_cell_id(path: &Path, has_id: bool) -> Result<()> {
let source_notebook = read_jupyter_notebook(path)?;
let source_kind = SourceKind::IpyNotebook(source_notebook);
let (_, transformed) = test_contents(
&source_kind,
path,
&settings::Settings::for_rule(Rule::UnusedImport),
);
let linted_notebook = transformed.into_owned().expect_ipy_notebook();
let mut writer = Vec::new();
linted_notebook.write_inner(&mut writer)?;
let actual = String::from_utf8(writer)?;
if has_id {
assert!(actual.contains(r#""id": ""#));
} else {
assert!(!actual.contains(r#""id":"#));
}
Ok(())
}
}

View File

@@ -150,7 +150,6 @@ pub struct CodeCell {
/// Technically, id isn't required (it's not even present) in schema v4.0 through v4.4, but
/// it's required in v4.5. Main issue is that pycharm creates notebooks without an id
/// <https://youtrack.jetbrains.com/issue/PY-59438/Jupyter-notebooks-created-with-PyCharm-are-missing-the-id-field-in-cells-in-the-.ipynb-json>
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
/// Cell-level metadata.
pub metadata: Value,

View File

@@ -147,7 +147,7 @@ pub fn check_path(
match ruff_python_parser::parse_program_tokens(
tokens,
&path.to_string_lossy(),
source_type.is_ipynb(),
source_type.is_jupyter(),
) {
Ok(python_ast) => {
if use_ast {

View File

@@ -7,7 +7,7 @@ use colored::Colorize;
use ruff_source_file::OneIndexed;
use crate::fs::relativize_path;
use crate::jupyter::{Notebook, NotebookIndex};
use crate::jupyter::{JupyterIndex, Notebook};
use crate::message::diff::calculate_print_width;
use crate::message::text::{MessageCodeFrame, RuleCodeAndBody};
use crate::message::{
@@ -92,7 +92,7 @@ struct DisplayGroupedMessage<'a> {
show_source: bool,
row_length: NonZeroUsize,
column_length: NonZeroUsize,
jupyter_index: Option<&'a NotebookIndex>,
jupyter_index: Option<&'a JupyterIndex>,
}
impl Display for DisplayGroupedMessage<'_> {

View File

@@ -11,7 +11,7 @@ use ruff_source_file::{OneIndexed, SourceLocation};
use ruff_text_size::{TextRange, TextSize};
use crate::fs::relativize_path;
use crate::jupyter::{Notebook, NotebookIndex};
use crate::jupyter::{JupyterIndex, Notebook};
use crate::line_width::{LineWidth, TabSize};
use crate::message::diff::Diff;
use crate::message::{Emitter, EmitterContext, Message};
@@ -161,7 +161,7 @@ impl Display for RuleCodeAndBody<'_> {
pub(super) struct MessageCodeFrame<'a> {
pub(crate) message: &'a Message,
pub(crate) jupyter_index: Option<&'a NotebookIndex>,
pub(crate) jupyter_index: Option<&'a JupyterIndex>,
}
impl Display for MessageCodeFrame<'_> {

View File

@@ -562,7 +562,7 @@ fn add_noqa_inner(
let mut prev_end = TextSize::default();
for (offset, (rules, directive)) in matches_by_line {
output.push_str(locator.slice(TextRange::new(prev_end, offset)));
output.push_str(&locator.contents()[TextRange::new(prev_end, offset)]);
let line = locator.full_line(offset);
@@ -619,7 +619,7 @@ fn add_noqa_inner(
prev_end = offset + line.text_len();
}
output.push_str(locator.after(prev_end));
output.push_str(&locator.contents()[usize::from(prev_end)..]);
(count, output)
}

View File

@@ -28,7 +28,7 @@ static HASH_NUMBER: Lazy<Regex> = Lazy::new(|| Regex::new(r"#\d").unwrap());
static MULTILINE_ASSIGNMENT_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^\s*([(\[]\s*)?(\w+\s*,\s*)*\w+\s*([)\]]\s*)?=.*[(\[{]$").unwrap());
static PARTIAL_DICTIONARY_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"^\s*['"]\w+['"]\s*:.+[,{]\s*(#.*)?$"#).unwrap());
Lazy::new(|| Regex::new(r#"^\s*['"]\w+['"]\s*:.+[,{]\s*$"#).unwrap());
static PRINT_RETURN_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(print|return)\b\s*").unwrap());
/// Returns `true` if a comment contains Python code.

View File

@@ -105,47 +105,5 @@ ERA001.py:21:5: ERA001 [*] Found commented-out code
19 19 | class A():
20 20 | pass
21 |- # b = c
22 21 |
23 22 |
24 23 | dictionary = {
ERA001.py:26:5: ERA001 [*] Found commented-out code
|
24 | dictionary = {
25 | # "key1": 123, # noqa: ERA001
26 | # "key2": 456,
| ^^^^^^^^^^^^^^ ERA001
27 | # "key3": 789, # test
28 | }
|
= help: Remove commented-out code
Possible fix
23 23 |
24 24 | dictionary = {
25 25 | # "key1": 123, # noqa: ERA001
26 |- # "key2": 456,
27 26 | # "key3": 789, # test
28 27 | }
29 28 |
ERA001.py:27:5: ERA001 [*] Found commented-out code
|
25 | # "key1": 123, # noqa: ERA001
26 | # "key2": 456,
27 | # "key3": 789, # test
| ^^^^^^^^^^^^^^^^^^^^^^ ERA001
28 | }
|
= help: Remove commented-out code
Possible fix
24 24 | dictionary = {
25 25 | # "key1": 123, # noqa: ERA001
26 26 | # "key2": 456,
27 |- # "key3": 789, # test
28 27 | }
29 28 |
30 29 | #import os # noqa

View File

@@ -0,0 +1,45 @@
use anyhow::{bail, Result};
use ruff_python_ast::{PySourceType, Ranged};
use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_diagnostics::Edit;
use ruff_source_file::Locator;
/// ANN204
pub(crate) fn add_return_annotation<T: Ranged>(
statement: &T,
annotation: &str,
source_type: PySourceType,
locator: &Locator,
) -> Result<Edit> {
let contents = &locator.contents()[statement.range()];
// Find the colon (following the `def` keyword).
let mut seen_lpar = false;
let mut seen_rpar = false;
let mut count = 0u32;
for (tok, range) in
lexer::lex_starts_at(contents, source_type.as_mode(), statement.start()).flatten()
{
if seen_lpar && seen_rpar {
if matches!(tok, Tok::Colon) {
return Ok(Edit::insertion(format!(" -> {annotation}"), range.start()));
}
}
if matches!(tok, Tok::Lpar) {
if count == 0 {
seen_lpar = true;
}
count = count.saturating_add(1);
}
if matches!(tok, Tok::Rpar) {
count = count.saturating_sub(1);
if count == 0 {
seen_rpar = true;
}
}
}
bail!("Unable to locate colon in function definition");
}

View File

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

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix, Violation};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::ReturnStatementVisitor;
use ruff_python_ast::identifier::Identifier;
@@ -11,6 +11,7 @@ use ruff_python_stdlib::typing::simple_magic_return_type;
use crate::checkers::ast::Checker;
use crate::registry::{AsRule, Rule};
use crate::rules::flake8_annotations::fixes;
use crate::rules::ruff::typing::type_hint_resolves_to_any;
/// ## What it does
@@ -703,10 +704,15 @@ pub(crate) fn definition(
function.identifier(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::suggested(Edit::insertion(
" -> None".to_string(),
function.parameters.range().end(),
)));
diagnostic.try_set_fix(|| {
fixes::add_return_annotation(
function,
"None",
checker.source_type,
checker.locator(),
)
.map(Fix::suggested)
});
}
diagnostics.push(diagnostic);
}
@@ -721,10 +727,15 @@ pub(crate) fn definition(
);
if checker.patch(diagnostic.kind.rule()) {
if let Some(return_type) = simple_magic_return_type(name) {
diagnostic.set_fix(Fix::suggested(Edit::insertion(
format!(" -> {return_type}"),
function.parameters.range().end(),
)));
diagnostic.try_set_fix(|| {
fixes::add_return_annotation(
function,
return_type,
checker.source_type,
checker.locator(),
)
.map(Fix::suggested)
});
}
}
diagnostics.push(diagnostic);

View File

@@ -242,22 +242,4 @@ annotation_presence.py:154:10: ANN401 Dynamically typed expressions (typing.Any)
| ^^^^^^^^^^^^^^^^^^^^^^^^ ANN401
|
annotation_presence.py:159:9: ANN204 [*] Missing return type annotation for special method `__init__`
|
157 | class Foo:
158 | @decorator()
159 | def __init__(self: "Foo", foo: int):
| ^^^^^^^^ ANN204
160 | ...
|
= help: Add `None` return type
Suggested fix
156 156 |
157 157 | class Foo:
158 158 | @decorator()
159 |- def __init__(self: "Foo", foo: int):
159 |+ def __init__(self: "Foo", foo: int) -> None:
160 160 | ...

View File

@@ -71,27 +71,8 @@ mod tests {
}
#[test]
fn extend_immutable_calls_arg_annotation() -> Result<()> {
let snapshot = "extend_immutable_calls_arg_annotation".to_string();
let diagnostics = test_path(
Path::new("flake8_bugbear/B006_extended.py"),
&Settings {
flake8_bugbear: super::settings::Settings {
extend_immutable_calls: vec![
"custom.ImmutableTypeA".to_string(),
"custom.ImmutableTypeB".to_string(),
],
},
..Settings::for_rule(Rule::MutableArgumentDefault)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test]
fn extend_immutable_calls_arg_default() -> Result<()> {
let snapshot = "extend_immutable_calls_arg_default".to_string();
fn extend_immutable_calls() -> Result<()> {
let snapshot = "extend_immutable_calls".to_string();
let diagnostics = test_path(
Path::new("flake8_bugbear/B008_extended.py"),
&Settings {
@@ -99,7 +80,6 @@ mod tests {
extend_immutable_calls: vec![
"fastapi.Depends".to_string(),
"fastapi.Query".to_string(),
"custom.ImmutableTypeA".to_string(),
],
},
..Settings::for_rule(Rule::FunctionCallInDefaultArgument)

View File

@@ -7,9 +7,7 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::{compose_call_path, from_qualified_name, CallPath};
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
use ruff_python_semantic::analyze::typing::{
is_immutable_annotation, is_immutable_func, is_mutable_func,
};
use ruff_python_semantic::analyze::typing::{is_immutable_func, is_mutable_func};
use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
@@ -22,13 +20,6 @@ use crate::checkers::ast::Checker;
/// once, at definition time. The returned value will then be reused by all
/// calls to the function, which can lead to unexpected behaviour.
///
/// Calls can be marked as an exception to this rule with the
/// [`flake8-bugbear.extend-immutable-calls`] configuration option.
///
/// Arguments with immutable type annotations will be ignored by this rule.
/// Types outside of the standard library can be marked as immutable with the
/// [`flake8-bugbear.extend-immutable-calls`] configuration option as well.
///
/// ## Example
/// ```python
/// def create_list() -> list[int]:
@@ -69,14 +60,14 @@ impl Violation for FunctionCallInDefaultArgument {
}
}
struct ArgumentDefaultVisitor<'a, 'b> {
semantic: &'a SemanticModel<'b>,
extend_immutable_calls: &'a [CallPath<'b>],
struct ArgumentDefaultVisitor<'a> {
semantic: &'a SemanticModel<'a>,
extend_immutable_calls: Vec<CallPath<'a>>,
diagnostics: Vec<(DiagnosticKind, TextRange)>,
}
impl<'a, 'b> ArgumentDefaultVisitor<'a, 'b> {
fn new(semantic: &'a SemanticModel<'b>, extend_immutable_calls: &'a [CallPath<'b>]) -> Self {
impl<'a> ArgumentDefaultVisitor<'a> {
fn new(semantic: &'a SemanticModel<'a>, extend_immutable_calls: Vec<CallPath<'a>>) -> Self {
Self {
semantic,
extend_immutable_calls,
@@ -85,12 +76,15 @@ impl<'a, 'b> ArgumentDefaultVisitor<'a, 'b> {
}
}
impl Visitor<'_> for ArgumentDefaultVisitor<'_, '_> {
fn visit_expr(&mut self, expr: &Expr) {
impl<'a, 'b> Visitor<'b> for ArgumentDefaultVisitor<'b>
where
'b: 'a,
{
fn visit_expr(&mut self, expr: &'b Expr) {
match expr {
Expr::Call(ast::ExprCall { func, .. }) => {
if !is_mutable_func(func, self.semantic)
&& !is_immutable_func(func, self.semantic, self.extend_immutable_calls)
&& !is_immutable_func(func, self.semantic, &self.extend_immutable_calls)
{
self.diagnostics.push((
FunctionCallInDefaultArgument {
@@ -120,28 +114,25 @@ pub(crate) fn function_call_in_argument_default(checker: &mut Checker, parameter
.iter()
.map(|target| from_qualified_name(target))
.collect();
let mut visitor = ArgumentDefaultVisitor::new(checker.semantic(), &extend_immutable_calls);
for ParameterWithDefault {
default,
parameter,
range: _,
} in parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
{
if let Some(expr) = &default {
if !parameter.annotation.as_ref().is_some_and(|expr| {
is_immutable_annotation(expr, checker.semantic(), &extend_immutable_calls)
}) {
let diagnostics = {
let mut visitor = ArgumentDefaultVisitor::new(checker.semantic(), extend_immutable_calls);
for ParameterWithDefault {
default,
parameter: _,
range: _,
} in parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
{
if let Some(expr) = &default {
visitor.visit_expr(expr);
}
}
}
for (check, range) in visitor.diagnostics {
visitor.diagnostics
};
for (check, range) in diagnostics {
checker.diagnostics.push(Diagnostic::new(check, range));
}
}

View File

@@ -1,4 +1,3 @@
use ast::call_path::{from_qualified_name, CallPath};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_docstring_stmt;
@@ -26,10 +25,6 @@ use crate::registry::AsRule;
/// default, and initialize a new mutable object inside the function body
/// for each call.
///
/// Arguments with immutable type annotations will be ignored by this rule.
/// Types outside of the standard library can be marked as immutable with the
/// [`flake8-bugbear.extend-immutable-calls`] configuration option.
///
/// ## Example
/// ```python
/// def add_to_list(item, some_list=[]):
@@ -54,9 +49,6 @@ use crate::registry::AsRule;
/// l2 = add_to_list(1) # [1]
/// ```
///
/// ## Options
/// - `flake8-bugbear.extend-immutable-calls`
///
/// ## References
/// - [Python documentation: Default Argument Values](https://docs.python.org/3/tutorial/controlflow.html#default-argument-values)
#[violation]
@@ -92,18 +84,11 @@ pub(crate) fn mutable_argument_default(checker: &mut Checker, function_def: &ast
continue;
};
let extend_immutable_calls: Vec<CallPath> = checker
.settings
.flake8_bugbear
.extend_immutable_calls
.iter()
.map(|target| from_qualified_name(target))
.collect();
if is_mutable_expr(default, checker.semantic())
&& !parameter.annotation.as_ref().is_some_and(|expr| {
is_immutable_annotation(expr, checker.semantic(), extend_immutable_calls.as_slice())
})
&& !parameter
.annotation
.as_ref()
.is_some_and(|expr| is_immutable_annotation(expr, checker.semantic()))
{
let mut diagnostic = Diagnostic::new(MutableArgumentDefault, default.range());
@@ -140,7 +125,7 @@ fn move_initialization(
let mut body = function_def.body.iter();
let statement = body.next()?;
if indexer.in_multi_statement_line(statement, locator) {
if indexer.preceded_by_multi_statement_line(statement, locator) {
return None;
}
@@ -170,7 +155,7 @@ fn move_initialization(
if let Some(statement) = body.next() {
// If there's a second statement, insert _before_ it, but ensure this isn't a
// multi-statement line.
if indexer.in_multi_statement_line(statement, locator) {
if indexer.preceded_by_multi_statement_line(statement, locator) {
return None;
}
Edit::insertion(content, locator.line_start(statement.start()))

View File

@@ -79,6 +79,7 @@ pub(crate) fn redundant_tuple_in_exception_handler(
type_.range(),
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
checker.generator().expr(elt),
type_.range(),

View File

@@ -258,139 +258,161 @@ B006_B008.py:114:33: B006 [*] Do not use mutable data structures for argument de
116 118 |
117 119 |
B006_B008.py:239:20: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:235:20: B006 [*] Do not use mutable data structures for argument defaults
|
237 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008.
239 | def nested_combo(a=[float(3), dt.datetime.now()]):
233 | # B006 and B008
234 | # We should handle arbitrary nesting of these B008.
235 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006
240 | pass
236 | pass
|
= help: Replace with `None`; initialize within function
Possible fix
236 236 |
237 237 | # B006 and B008
238 238 | # We should handle arbitrary nesting of these B008.
239 |-def nested_combo(a=[float(3), dt.datetime.now()]):
239 |+def nested_combo(a=None):
240 |+ if a is None:
241 |+ a = [float(3), dt.datetime.now()]
240 242 | pass
241 243 |
242 244 |
232 232 |
233 233 | # B006 and B008
234 234 | # We should handle arbitrary nesting of these B008.
235 |-def nested_combo(a=[float(3), dt.datetime.now()]):
235 |+def nested_combo(a=None):
236 |+ if a is None:
237 |+ a = [float(3), dt.datetime.now()]
236 238 | pass
237 239 |
238 240 |
B006_B008.py:276:27: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:272:27: B006 [*] Do not use mutable data structures for argument defaults
|
275 | def mutable_annotations(
276 | a: list[int] | None = [],
271 | def mutable_annotations(
272 | a: list[int] | None = [],
| ^^ B006
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
273 | b: Optional[Dict[int, int]] = {},
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
= help: Replace with `None`; initialize within function
Possible fix
273 273 |
274 274 |
275 275 | def mutable_annotations(
276 |- a: list[int] | None = [],
276 |+ a: list[int] | None = None,
277 277 | b: Optional[Dict[int, int]] = {},
278 278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 280 | ):
281 |+ if a is None:
282 |+ a = []
281 283 | pass
282 284 |
283 285 |
269 269 |
270 270 |
271 271 | def mutable_annotations(
272 |- a: list[int] | None = [],
272 |+ a: list[int] | None = None,
273 273 | b: Optional[Dict[int, int]] = {},
274 274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 276 | ):
277 |+ if a is None:
278 |+ a = []
277 279 | pass
278 280 |
279 281 |
B006_B008.py:277:35: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:273:35: B006 [*] Do not use mutable data structures for argument defaults
|
275 | def mutable_annotations(
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
271 | def mutable_annotations(
272 | a: list[int] | None = [],
273 | b: Optional[Dict[int, int]] = {},
| ^^ B006
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
= help: Replace with `None`; initialize within function
Possible fix
274 274 |
275 275 | def mutable_annotations(
276 276 | a: list[int] | None = [],
277 |- b: Optional[Dict[int, int]] = {},
277 |+ b: Optional[Dict[int, int]] = None,
278 278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 280 | ):
281 |+ if b is None:
282 |+ b = {}
281 283 | pass
282 284 |
283 285 |
270 270 |
271 271 | def mutable_annotations(
272 272 | a: list[int] | None = [],
273 |- b: Optional[Dict[int, int]] = {},
273 |+ b: Optional[Dict[int, int]] = None,
274 274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 276 | ):
277 |+ if b is None:
278 |+ b = {}
277 279 | pass
278 280 |
279 281 |
B006_B008.py:278:62: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:274:62: B006 [*] Do not use mutable data structures for argument defaults
|
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
272 | a: list[int] | None = [],
273 | b: Optional[Dict[int, int]] = {},
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ B006
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ):
275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 | ):
|
= help: Replace with `None`; initialize within function
Possible fix
275 275 | def mutable_annotations(
276 276 | a: list[int] | None = [],
277 277 | b: Optional[Dict[int, int]] = {},
278 |- c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
278 |+ c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
279 279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 280 | ):
281 |+ if c is None:
282 |+ c = set()
281 283 | pass
282 284 |
283 285 |
271 271 | def mutable_annotations(
272 272 | a: list[int] | None = [],
273 273 | b: Optional[Dict[int, int]] = {},
274 |- c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
274 |+ c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
275 275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 276 | ):
277 |+ if c is None:
278 |+ c = set()
277 279 | pass
278 280 |
279 281 |
B006_B008.py:279:80: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:275:80: B006 [*] Do not use mutable data structures for argument defaults
|
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
273 | b: Optional[Dict[int, int]] = {},
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ B006
280 | ):
281 | pass
276 | ):
277 | pass
|
= help: Replace with `None`; initialize within function
Possible fix
276 276 | a: list[int] | None = [],
277 277 | b: Optional[Dict[int, int]] = {},
278 278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 |- d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 |+ d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
280 280 | ):
281 |+ if d is None:
282 |+ d = set()
281 283 | pass
272 272 | a: list[int] | None = [],
273 273 | b: Optional[Dict[int, int]] = {},
274 274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 |- d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 |+ d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
276 276 | ):
277 |+ if d is None:
278 |+ d = set()
277 279 | pass
278 280 |
279 281 |
B006_B008.py:280:52: B006 [*] Do not use mutable data structures for argument defaults
|
280 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
281 | """Docstring"""
|
= help: Replace with `None`; initialize within function
Possible fix
277 277 | pass
278 278 |
279 279 |
280 |-def single_line_func_wrong(value: dict[str, str] = {}):
280 |+def single_line_func_wrong(value: dict[str, str] = None):
281 281 | """Docstring"""
282 |+ if value is None:
283 |+ value = {}
282 284 |
283 285 |
284 286 | def single_line_func_wrong(value: dict[str, str] = {}):
B006_B008.py:284:52: B006 [*] Do not use mutable data structures for argument defaults
|
284 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
285 | """Docstring"""
286 | ...
|
= help: Replace with `None`; initialize within function
Possible fix
281 281 | pass
281 281 | """Docstring"""
282 282 |
283 283 |
284 |-def single_line_func_wrong(value: dict[str, str] = {}):
@@ -398,81 +420,59 @@ B006_B008.py:284:52: B006 [*] Do not use mutable data structures for argument de
285 285 | """Docstring"""
286 |+ if value is None:
287 |+ value = {}
286 288 |
286 288 | ...
287 289 |
288 290 | def single_line_func_wrong(value: dict[str, str] = {}):
288 290 |
B006_B008.py:288:52: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:289:52: B006 Do not use mutable data structures for argument defaults
|
288 | def single_line_func_wrong(value: dict[str, str] = {}):
289 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
289 | """Docstring"""
290 | ...
290 | """Docstring"""; ...
|
= help: Replace with `None`; initialize within function
Possible fix
285 285 | """Docstring"""
286 286 |
287 287 |
288 |-def single_line_func_wrong(value: dict[str, str] = {}):
288 |+def single_line_func_wrong(value: dict[str, str] = None):
289 289 | """Docstring"""
290 |+ if value is None:
291 |+ value = {}
290 292 | ...
291 293 |
292 294 |
B006_B008.py:293:52: B006 Do not use mutable data structures for argument defaults
|
293 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
294 | """Docstring"""; ...
294 | """Docstring"""; \
295 | ...
|
= help: Replace with `None`; initialize within function
B006_B008.py:297:52: B006 Do not use mutable data structures for argument defaults
B006_B008.py:298:52: B006 [*] Do not use mutable data structures for argument defaults
|
297 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
298 | """Docstring"""; \
299 | ...
|
= help: Replace with `None`; initialize within function
B006_B008.py:302:52: B006 [*] Do not use mutable data structures for argument defaults
|
302 | def single_line_func_wrong(value: dict[str, str] = {
298 | def single_line_func_wrong(value: dict[str, str] = {
| ____________________________________________________^
303 | | # This is a comment
304 | | }):
299 | | # This is a comment
300 | | }):
| |_^ B006
305 | """Docstring"""
301 | """Docstring"""
|
= help: Replace with `None`; initialize within function
Possible fix
299 299 | ...
300 300 |
301 301 |
302 |-def single_line_func_wrong(value: dict[str, str] = {
303 |- # This is a comment
304 |-}):
302 |+def single_line_func_wrong(value: dict[str, str] = None):
305 303 | """Docstring"""
304 |+ if value is None:
305 |+ value = {}
306 306 |
307 307 |
308 308 | def single_line_func_wrong(value: dict[str, str] = {}) \
295 295 | ...
296 296 |
297 297 |
298 |-def single_line_func_wrong(value: dict[str, str] = {
299 |- # This is a comment
300 |-}):
298 |+def single_line_func_wrong(value: dict[str, str] = None):
301 299 | """Docstring"""
300 |+ if value is None:
301 |+ value = {}
302 302 |
303 303 |
304 304 | def single_line_func_wrong(value: dict[str, str] = {}) \
B006_B008.py:308:52: B006 Do not use mutable data structures for argument defaults
B006_B008.py:304:52: B006 Do not use mutable data structures for argument defaults
|
308 | def single_line_func_wrong(value: dict[str, str] = {}) \
304 | def single_line_func_wrong(value: dict[str, str] = {}) \
| ^^ B006
309 | : \
310 | """Docstring"""
305 | : \
306 | """Docstring"""
|
= help: Replace with `None`; initialize within function

View File

@@ -46,38 +46,38 @@ B006_B008.py:134:30: B008 Do not perform function call in argument defaults
135 | ...
|
B006_B008.py:239:31: B008 Do not perform function call `dt.datetime.now` in argument defaults
B006_B008.py:235:31: B008 Do not perform function call `dt.datetime.now` in argument defaults
|
237 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008.
239 | def nested_combo(a=[float(3), dt.datetime.now()]):
233 | # B006 and B008
234 | # We should handle arbitrary nesting of these B008.
235 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^ B008
240 | pass
236 | pass
|
B006_B008.py:245:22: B008 Do not perform function call `map` in argument defaults
B006_B008.py:241:22: B008 Do not perform function call `map` in argument defaults
|
243 | # Don't flag nested B006 since we can't guarantee that
244 | # it isn't made mutable by the outer operation.
245 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
239 | # Don't flag nested B006 since we can't guarantee that
240 | # it isn't made mutable by the outer operation.
241 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B008
246 | pass
242 | pass
|
B006_B008.py:250:19: B008 Do not perform function call `random.randint` in argument defaults
B006_B008.py:246:19: B008 Do not perform function call `random.randint` in argument defaults
|
249 | # B008-ception.
250 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
245 | # B008-ception.
246 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B008
251 | pass
247 | pass
|
B006_B008.py:250:37: B008 Do not perform function call `dt.datetime.now` in argument defaults
B006_B008.py:246:37: B008 Do not perform function call `dt.datetime.now` in argument defaults
|
249 | # B008-ception.
250 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
245 | # B008-ception.
246 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
| ^^^^^^^^^^^^^^^^^ B008
251 | pass
247 | pass
|

View File

@@ -1,11 +1,11 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
---
B008_extended.py:24:51: B008 Do not perform function call `Depends` in argument defaults
B008_extended.py:19:51: B008 Do not perform function call `Depends` in argument defaults
|
24 | def error_due_to_missing_import(data: List[str] = Depends(None)):
19 | def error_due_to_missing_import(data: List[str] = Depends(None)):
| ^^^^^^^^^^^^^ B008
25 | ...
20 | ...
|

View File

@@ -1,22 +0,0 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
---
B006_extended.py:17:55: B006 [*] Do not use mutable data structures for argument defaults
|
17 | def error_due_to_missing_import(foo: ImmutableTypeA = []):
| ^^ B006
18 | ...
|
= help: Replace with `None`; initialize within function
Possible fix
14 14 | ...
15 15 |
16 16 |
17 |-def error_due_to_missing_import(foo: ImmutableTypeA = []):
17 |+def error_due_to_missing_import(foo: ImmutableTypeA = None):
18 |+ if foo is None:
19 |+ foo = []
18 20 | ...

View File

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

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Expr, Keyword, Ranged};
@@ -86,9 +86,8 @@ pub(crate) fn unnecessary_collection_call(
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_collection_call(checker, expr).map(Fix::suggested)
});
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| fixes::fix_unnecessary_collection_call(checker, expr));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Comprehension, Expr, Ranged};
@@ -63,9 +63,9 @@ fn add_diagnostic(checker: &mut Checker, expr: &Expr) {
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_comprehension(checker.locator(), checker.stylist(), expr)
.map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

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

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableKeyword;
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Ranged};
@@ -130,13 +130,13 @@ pub(crate) fn unnecessary_double_cast_or_process(
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_double_cast_or_process(
checker.locator(),
checker.stylist(),
expr,
)
.map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,7 +1,8 @@
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::rules::flake8_comprehensions::fixes;
@@ -58,8 +59,9 @@ pub(crate) fn unnecessary_generator_dict(
Expr::Tuple(ast::ExprTuple { elts, .. }) if elts.len() == 2 => {
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorDict, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_generator_dict(checker, expr).map(Fix::suggested)
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_generator_dict(checker, expr)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,6 +1,6 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
@@ -60,9 +60,9 @@ pub(crate) fn unnecessary_generator_list(
if let Expr::GeneratorExp(_) = argument {
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorList, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_generator_list(checker.locator(), checker.stylist(), expr)
.map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,6 +1,6 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
@@ -60,9 +60,9 @@ pub(crate) fn unnecessary_generator_set(
if let Expr::GeneratorExp(_) = argument {
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorSet, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_generator_set(checker, expr).map(Fix::suggested)
});
#[allow(deprecated)]
diagnostic
.try_set_fix_from_edit(|| fixes::fix_unnecessary_generator_set(checker, expr));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,6 +1,6 @@
use ruff_python_ast::{Expr, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
@@ -56,9 +56,9 @@ pub(crate) fn unnecessary_list_call(
}
let mut diagnostic = Diagnostic::new(UnnecessaryListCall, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_list_call(checker.locator(), checker.stylist(), expr)
.map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,7 +1,8 @@
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::rules::flake8_comprehensions::fixes;
@@ -65,8 +66,9 @@ pub(crate) fn unnecessary_list_comprehension_dict(
}
let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionDict, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_list_comprehension_dict(checker, expr).map(Fix::suggested)
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_list_comprehension_dict(checker, expr)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,6 +1,6 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
@@ -58,8 +58,9 @@ pub(crate) fn unnecessary_list_comprehension_set(
if argument.is_list_comp_expr() {
let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionSet, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_list_comprehension_set(checker, expr).map(Fix::suggested)
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_list_comprehension_set(checker, expr)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,7 +1,8 @@
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::rules::flake8_comprehensions::fixes;
@@ -80,8 +81,8 @@ pub(crate) fn unnecessary_literal_dict(
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic
.try_set_fix(|| fixes::fix_unnecessary_literal_dict(checker, expr).map(Fix::suggested));
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| fixes::fix_unnecessary_literal_dict(checker, expr));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,6 +1,6 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
@@ -75,8 +75,8 @@ pub(crate) fn unnecessary_literal_set(
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic
.try_set_fix(|| fixes::fix_unnecessary_literal_set(checker, expr).map(Fix::suggested));
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| fixes::fix_unnecessary_literal_set(checker, expr));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -2,7 +2,7 @@ use std::fmt;
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
@@ -91,13 +91,13 @@ pub(crate) fn unnecessary_literal_within_dict_call(
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_literal_within_dict_call(
checker.locator(),
checker.stylist(),
expr,
)
.map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,7 +1,8 @@
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::rules::flake8_comprehensions::fixes;
@@ -93,13 +94,13 @@ pub(crate) fn unnecessary_literal_within_list_call(
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_literal_within_list_call(
checker.locator(),
checker.stylist(),
expr,
)
.map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,6 +1,6 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
@@ -95,13 +95,13 @@ pub(crate) fn unnecessary_literal_within_tuple_call(
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_literal_within_tuple_call(
checker.locator(),
checker.stylist(),
expr,
)
.map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -103,17 +103,10 @@ pub(crate) fn unnecessary_map(
return;
};
if parameters.as_ref().is_some_and(|parameters| {
late_binding(parameters, body)
|| parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
.any(|param| param.default.is_some())
|| parameters.vararg.is_some()
|| parameters.kwarg.is_some()
}) {
if parameters
.as_ref()
.is_some_and(|parameters| late_binding(parameters, body))
{
return;
}
}
@@ -144,17 +137,10 @@ pub(crate) fn unnecessary_map(
return;
};
if parameters.as_ref().is_some_and(|parameters| {
late_binding(parameters, body)
|| parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
.any(|param| param.default.is_some())
|| parameters.vararg.is_some()
|| parameters.kwarg.is_some()
}) {
if parameters
.as_ref()
.is_some_and(|parameters| late_binding(parameters, body))
{
return;
}
}
@@ -191,21 +177,14 @@ pub(crate) fn unnecessary_map(
return;
}
if parameters.as_ref().is_some_and(|parameters| {
late_binding(parameters, body)
|| parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
.any(|param| param.default.is_some())
|| parameters.vararg.is_some()
|| parameters.kwarg.is_some()
}) {
if parameters
.as_ref()
.is_some_and(|parameters| late_binding(parameters, body))
{
return;
}
}
};
}
let mut diagnostic = Diagnostic::new(UnnecessaryMap { object_type }, expr.range());
if checker.patch(diagnostic.kind.rule()) {

View File

@@ -226,7 +226,7 @@ C417.py:15:8: C417 [*] Unnecessary `map` usage (rewrite using a `set` comprehens
15 |+_ = f"{ {x % 2 == 0 for x in nums} }"
16 16 | _ = f"{dict(map(lambda v: (v, v**2), nums))}"
17 17 |
18 18 | # False negatives.
18 18 | # Error, but unfixable.
C417.py:16:8: C417 [*] Unnecessary `map` usage (rewrite using a `dict` comprehension)
|
@@ -235,7 +235,7 @@ C417.py:16:8: C417 [*] Unnecessary `map` usage (rewrite using a `dict` comprehen
16 | _ = f"{dict(map(lambda v: (v, v**2), nums))}"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417
17 |
18 | # False negatives.
18 | # Error, but unfixable.
|
= help: Replace `map` with a `dict` comprehension
@@ -246,27 +246,33 @@ C417.py:16:8: C417 [*] Unnecessary `map` usage (rewrite using a `dict` comprehen
16 |-_ = f"{dict(map(lambda v: (v, v**2), nums))}"
16 |+_ = f"{ {v: v**2 for v in nums} }"
17 17 |
18 18 | # False negatives.
19 19 | map(lambda x=2, y=1: x + y, nums, nums)
18 18 | # Error, but unfixable.
19 19 | # For simple expressions, this could be: `(x if x else 1 for x in nums)`.
C417.py:34:1: C417 [*] Unnecessary `map` usage (rewrite using a generator expression)
C417.py:21:1: C417 Unnecessary `map` usage (rewrite using a generator expression)
|
33 | # Error: the `x` is overridden by the inner lambda.
34 | map(lambda x: lambda x: x, range(4))
19 | # For simple expressions, this could be: `(x if x else 1 for x in nums)`.
20 | # For more complex expressions, this would differ: `(x + 2 if x else 3 for x in nums)`.
21 | map(lambda x=1: x, nums)
| ^^^^^^^^^^^^^^^^^^^^^^^^ C417
22 |
23 | # False negatives.
|
= help: Replace `map` with a generator expression
C417.py:39:1: C417 [*] Unnecessary `map` usage (rewrite using a generator expression)
|
38 | # Error: the `x` is overridden by the inner lambda.
39 | map(lambda x: lambda x: x, range(4))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417
35 |
36 | # Ok because of the default parameters, and variadic arguments.
|
= help: Replace `map` with a generator expression
Suggested fix
31 31 | map(lambda x: lambda: x, range(4))
32 32 |
33 33 | # Error: the `x` is overridden by the inner lambda.
34 |-map(lambda x: lambda x: x, range(4))
34 |+(lambda x: x for x in range(4))
35 35 |
36 36 | # Ok because of the default parameters, and variadic arguments.
37 37 | map(lambda x=1: x, nums)
36 36 | map(lambda x: lambda: x, range(4))
37 37 |
38 38 | # Error: the `x` is overridden by the inner lambda.
39 |-map(lambda x: lambda x: x, range(4))
39 |+(lambda x: x for x in range(4))

View File

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

View File

@@ -5,49 +5,6 @@ use ruff_python_ast::{self as ast, Constant, Expr, Ranged};
use crate::checkers::ast::Checker;
use crate::rules::flake8_datetimez::rules::helpers::has_non_none_keyword;
/// ## What it does
/// Checks for uses of `datetime.datetime.strptime()` that lead to naive
/// datetime objects.
///
/// ## Why is this bad?
/// Python datetime objects can be naive or timezone-aware. While an aware
/// object represents a specific moment in time, a naive object does not
/// contain enough information to unambiguously locate itself relative to other
/// datetime objects. Since this can lead to errors, it is recommended to
/// always use timezone-aware objects.
///
/// `datetime.datetime.strptime()` without `%z` returns a naive datetime
/// object. Follow it with `.replace(tzinfo=)` or `.astimezone()`.
///
/// ## Example
/// ```python
/// import datetime
///
/// datetime.datetime.strptime("2022/01/31", "%Y/%m/%d")
/// ```
///
/// Instead, use `.replace(tzinfo=)`:
/// ```python
/// import datetime
///
/// datetime.datetime.strptime("2022/01/31", "%Y/%m/%d").replace(
/// tzinfo=datetime.timezone.utc
/// )
/// ```
///
/// Or, use `.astimezone()`:
/// ```python
/// import datetime
///
/// datetime.datetime.strptime("2022/01/31", "%Y/%m/%d").astimezone(datetime.timezone.utc)
/// ```
///
/// On Python 3.11 and later, `datetime.timezone.utc` can be replaced with
/// `datetime.UTC`.
///
/// ## References
/// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects)
/// - [Python documentation: `strftime()` and `strptime()` Behavior](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior)
#[violation]
pub struct CallDatetimeStrptimeWithoutZone;

View File

@@ -126,8 +126,8 @@ pub(crate) fn implicit(
}
fn concatenate_strings(a_range: TextRange, b_range: TextRange, locator: &Locator) -> Option<Fix> {
let a_text = locator.slice(a_range);
let b_text = locator.slice(b_range);
let a_text = &locator.contents()[a_range];
let b_text = &locator.contents()[b_range];
let a_leading_quote = leading_quote(a_text)?;
let b_leading_quote = leading_quote(b_text)?;

View File

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

View File

@@ -9,7 +9,6 @@ const CONVENTIONAL_ALIASES: &[(&str, &str)] = &[
("altair", "alt"),
("matplotlib", "mpl"),
("matplotlib.pyplot", "plt"),
("networkx", "nx"),
("numpy", "np"),
("pandas", "pd"),
("seaborn", "sns"),

View File

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

View File

@@ -21,19 +21,6 @@ use ruff_macros::{derive_message_formats, violation};
/// As an alternative to `extra`, passing values as arguments to the logging
/// method can also be used to defer string formatting until required.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -67,9 +54,6 @@ use ruff_macros::{derive_message_formats, violation};
/// logging.info("%s - Something happened", user)
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html)
/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization)
@@ -105,19 +89,6 @@ impl Violation for LoggingStringFormat {
/// As an alternative to `extra`, passing values as arguments to the logging
/// method can also be used to defer string formatting until required.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -151,9 +122,6 @@ impl Violation for LoggingStringFormat {
/// logging.info("%s - Something happened", user)
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html)
/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization)
@@ -188,19 +156,6 @@ impl Violation for LoggingPercentFormat {
/// As an alternative to `extra`, passing values as arguments to the logging
/// method can also be used to defer string formatting until required.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -234,9 +189,6 @@ impl Violation for LoggingPercentFormat {
/// logging.info("%s - Something happened", user)
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html)
/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization)
@@ -270,19 +222,6 @@ impl Violation for LoggingStringConcat {
/// As an alternative to `extra`, passing values as arguments to the logging
/// method can also be used to defer string formatting until required.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -316,9 +255,6 @@ impl Violation for LoggingStringConcat {
/// logging.info("%s - Something happened", user)
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html)
/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization)
@@ -340,19 +276,6 @@ impl Violation for LoggingFString {
/// `logging.warning` and `logging.Logger.warning`, which are functionally
/// equivalent.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -367,9 +290,6 @@ impl Violation for LoggingFString {
/// logging.warning("Something happened")
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging.warning`](https://docs.python.org/3/library/logging.html#logging.warning)
/// - [Python documentation: `logging.Logger.warning`](https://docs.python.org/3/library/logging.html#logging.Logger.warning)
@@ -400,19 +320,6 @@ impl AlwaysAutofixableViolation for LoggingWarn {
/// the `LogRecord` constructor will raise a `KeyError` when the `LogRecord` is
/// constructed.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -435,9 +342,6 @@ impl AlwaysAutofixableViolation for LoggingWarn {
/// logging.info("Something happened", extra=dict(user=username))
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: LogRecord attributes](https://docs.python.org/3/library/logging.html#logrecord-attributes)
#[violation]
@@ -461,19 +365,6 @@ impl Violation for LoggingExtraAttrClash {
/// `logging.exception`. Using `logging.exception` is more concise, more
/// readable, and conveys the intent of the logging statement more clearly.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -494,9 +385,6 @@ impl Violation for LoggingExtraAttrClash {
/// logging.exception("Exception occurred")
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging.exception`](https://docs.python.org/3/library/logging.html#logging.exception)
/// - [Python documentation: `exception`](https://docs.python.org/3/library/logging.html#logging.Logger.exception)
@@ -522,19 +410,6 @@ impl Violation for LoggingExcInfo {
/// Passing `exc_info=True` to `logging.exception` calls is redundant, as is
/// passing `exc_info=False` to `logging.error` calls.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -555,9 +430,6 @@ impl Violation for LoggingExcInfo {
/// logging.exception("Exception occurred")
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging.exception`](https://docs.python.org/3/library/logging.html#logging.exception)
/// - [Python documentation: `exception`](https://docs.python.org/3/library/logging.html#logging.Logger.exception)

View File

@@ -85,9 +85,7 @@ pub(crate) fn duplicate_class_field_definition(checker: &mut Checker, body: &[St
checker.locator(),
checker.indexer(),
);
diagnostic.set_fix(Fix::suggested(edit).isolate(Checker::isolation(Some(
checker.semantic().current_statement_id(),
))));
diagnostic.set_fix(Fix::suggested(edit).isolate(checker.statement_isolation()));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -50,12 +50,12 @@ pub(crate) fn duplicate_union_member<'a>(checker: &mut Checker, expr: &'a Expr)
// parent without the duplicate.
// If the parent node is not a `BinOp` we will not perform a fix
if let Some(parent @ Expr::BinOp(ast::ExprBinOp { left, right, .. })) = parent {
if let Some(Expr::BinOp(ast::ExprBinOp { left, right, .. })) = parent {
// Replace the parent with its non-duplicate child.
let child = if expr == left.as_ref() { right } else { left };
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
checker.locator().slice(child.range()).to_string(),
parent.range(),
parent.unwrap().range(),
)));
}
}

View File

@@ -69,9 +69,7 @@ pub(crate) fn ellipsis_in_non_empty_class_body(checker: &mut Checker, body: &[St
checker.locator(),
checker.indexer(),
);
diagnostic.set_fix(Fix::automatic(edit).isolate(Checker::isolation(Some(
checker.semantic().current_statement_id(),
))));
diagnostic.set_fix(Fix::automatic(edit).isolate(checker.statement_isolation()));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -36,9 +36,7 @@ pub(crate) fn pass_in_class_body(checker: &mut Checker, class_def: &ast::StmtCla
if checker.patch(diagnostic.kind.rule()) {
let edit =
autofix::edits::delete_stmt(stmt, Some(stmt), checker.locator(), checker.indexer());
diagnostic.set_fix(Fix::automatic(edit).isolate(Checker::isolation(Some(
checker.semantic().current_statement_id(),
))));
diagnostic.set_fix(Fix::automatic(edit).isolate(checker.statement_isolation()));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -99,9 +99,7 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) {
let stmt = checker.semantic().current_statement();
let parent = checker.semantic().current_statement_parent();
let edit = delete_stmt(stmt, parent, checker.locator(), checker.indexer());
diagnostic.set_fix(Fix::automatic(edit).isolate(Checker::isolation(
checker.semantic().current_statement_parent_id(),
)));
diagnostic.set_fix(Fix::automatic(edit).isolate(checker.parent_isolation()));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,7 +1,7 @@
use std::borrow::Cow;
use anyhow::bail;
use anyhow::Result;
use anyhow::{bail, Context};
use libcst_native::{
self, Assert, BooleanOp, CompoundStatement, Expression, ParenthesizableWhitespace,
ParenthesizedNode, SimpleStatementLine, SimpleWhitespace, SmallStatement, Statement,
@@ -635,8 +635,9 @@ fn parenthesize<'a>(expression: Expression<'a>, parent: &Expression<'a>) -> Expr
/// `assert a == "hello"` and `assert b == "world"`.
fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> Result<Edit> {
// Infer the indentation of the outer block.
let outer_indent =
whitespace::indentation(locator, stmt).context("Unable to fix multiline statement")?;
let Some(outer_indent) = whitespace::indentation(locator, stmt) else {
bail!("Unable to fix multiline statement");
};
// Extract the module text.
let contents = locator.lines(stmt.range());
@@ -671,11 +672,11 @@ fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) ->
&mut indented_block.body
};
let [Statement::Simple(simple_statement_line)] = statements.as_slice() else {
let [Statement::Simple(simple_statement_line)] = &statements[..] else {
bail!("Expected one simple statement")
};
let [SmallStatement::Assert(assert_statement)] = simple_statement_line.body.as_slice() else {
let [SmallStatement::Assert(assert_statement)] = &simple_statement_line.body[..] else {
bail!("Expected simple statement to be an assert")
};
@@ -753,13 +754,10 @@ pub(crate) fn composite_condition(
if matches!(composite, CompositionKind::Simple)
&& msg.is_none()
&& !checker.indexer().comment_ranges().intersects(stmt.range())
&& !checker
.indexer()
.in_multi_statement_line(stmt, checker.locator())
{
diagnostic.try_set_fix(|| {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fix_composite_condition(stmt, checker.locator(), checker.stylist())
.map(Fix::suggested)
});
}
}

View File

@@ -1,16 +1,18 @@
use rustc_hash::FxHashMap;
use std::hash::BuildHasherDefault;
use rustc_hash::FxHashMap;
use ruff_python_ast::{
self as ast, Arguments, Constant, Decorator, Expr, ExprContext, PySourceType, Ranged,
};
use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::node::AstNode;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{self as ast, Arguments, Constant, Decorator, Expr, ExprContext, Ranged};
use ruff_python_codegen::Generator;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{TextRange, TextSize};
use ruff_source_file::Locator;
use crate::checkers::ast::Checker;
use crate::registry::{AsRule, Rule};
@@ -72,7 +74,7 @@ use super::helpers::{is_pytest_parametrize, split_names};
/// - [`pytest` documentation: How to parametrize fixtures and test functions](https://docs.pytest.org/en/latest/how-to/parametrize.html#pytest-mark-parametrize)
#[violation]
pub struct PytestParametrizeNamesWrongType {
expected: types::ParametrizeNameType,
pub expected: types::ParametrizeNameType,
}
impl Violation for PytestParametrizeNamesWrongType {
@@ -157,8 +159,8 @@ impl Violation for PytestParametrizeNamesWrongType {
/// - [`pytest` documentation: How to parametrize fixtures and test functions](https://docs.pytest.org/en/latest/how-to/parametrize.html#pytest-mark-parametrize)
#[violation]
pub struct PytestParametrizeValuesWrongType {
values: types::ParametrizeValuesType,
row: types::ParametrizeValuesRowType,
pub values: types::ParametrizeValuesType,
pub row: types::ParametrizeValuesRowType,
}
impl Violation for PytestParametrizeValuesWrongType {
@@ -281,12 +283,34 @@ fn elts_to_csv(elts: &[Expr], generator: Generator) -> Option<String> {
fn get_parametrize_name_range(
decorator: &Decorator,
expr: &Expr,
source: &str,
) -> Option<TextRange> {
decorator
.expression
.as_call_expr()
.and_then(|call| parenthesized_range(expr.into(), call.arguments.as_any_node_ref(), source))
locator: &Locator,
source_type: PySourceType,
) -> TextRange {
let mut locations = Vec::new();
let mut name_range = None;
// The parenthesis are not part of the AST, so we need to tokenize the
// decorator to find them.
for (tok, range) in lexer::lex_starts_at(
locator.slice(decorator.range()),
source_type.as_mode(),
decorator.start(),
)
.flatten()
{
match tok {
Tok::Lpar => locations.push(range.start()),
Tok::Rpar => {
if let Some(start) = locations.pop() {
name_range = Some(TextRange::new(start, range.end()));
}
}
// Stop after the first argument.
Tok::Comma => break,
_ => (),
}
}
name_range.unwrap_or_else(|| expr.range())
}
/// PT006
@@ -305,9 +329,9 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
let name_range = get_parametrize_name_range(
decorator,
expr,
checker.locator().contents(),
)
.unwrap_or(expr.range());
checker.locator(),
checker.source_type,
);
let mut diagnostic = Diagnostic::new(
PytestParametrizeNamesWrongType {
expected: names_type,
@@ -340,9 +364,9 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
let name_range = get_parametrize_name_range(
decorator,
expr,
checker.locator().contents(),
)
.unwrap_or(expr.range());
checker.locator(),
checker.source_type,
);
let mut diagnostic = Diagnostic::new(
PytestParametrizeNamesWrongType {
expected: names_type,

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