Compare commits

...

35 Commits

Author SHA1 Message Date
Charlie Marsh
89d9f1e124 Use PatternMatchAs for mapping rest node 2023-08-24 00:44:50 -04:00
Harutaka Kawamura
205d234856 Format PatternMatchStar (#6653) 2023-08-24 01:58:05 +00:00
Charlie Marsh
4889b84338 Document logger-objects setting in flake8-logging-format rules (#6832)
Closes https://github.com/astral-sh/ruff/issues/6764.
2023-08-24 00:18:51 +00:00
Charlie Marsh
847432cacf Avoid attempting to fix PT018 in multi-statement lines (#6829)
## Summary

These fixes will _always_ fail, so we should avoid trying to construct
them in the first place.

Closes https://github.com/astral-sh/ruff/issues/6812.
2023-08-23 19:09:34 -04:00
Charlie Marsh
9b6e008cf1 Remove remaining usages of try_set_fix (#6828)
There are just a few remaining usages of this deprecated method.
2023-08-23 22:36:09 +00:00
Charlie Marsh
39c6665ff9 Remove remaining usage of set_fix_from_edit (#6827)
This method is deprecated; we have one last usage, so removing it.
2023-08-23 18:25:39 -04:00
konsti
19a87c220a Document ecosystem_all_check.sh input json (#6824)
Document the origin of `github_search.jsonl` in `ecosystem_all_check.sh`
2023-08-23 20:08:52 +00:00
Zanie Blue
0688883404 Fix uncessary-coding-comment fix when there's leading content (#6775)
Closes https://github.com/astral-sh/ruff/issues/6756

Including whitespace, code, and continuations.
2023-08-23 12:00:06 -05:00
Zanie Blue
3bb638875f Fix native-literals handling of int literal with attribute access (#6792)
Closes https://github.com/astral-sh/ruff/issues/6788 by special casing
integer literals with attribute access — either retaining parenthesis
for literals with values (e.g. `int(7).denominator` to
`(7).denominator)` or leaving calls without values (e.g.
`int().denominator`) unchanged.
2023-08-23 11:22:05 -05:00
Charlie Marsh
26e63ab137 Remove lexing from flake8-pytest-style (#6795)
## Summary

Another drive-by change to remove unnecessary custom lexing. We just
need to know the parenthesized range, so we can use...
`parenthesized_range`. I've also updated `parenthesized_range` to
support nested parentheses.

## Test Plan

`cargo test`
2023-08-23 15:54:11 +00:00
Zanie Blue
417a1d0717 Update mutable-argument-default (B006) to use extend-immutable-calls when determining if annotations are immutable (#6781)
Part of https://github.com/astral-sh/ruff/issues/3762
2023-08-23 15:44:35 +00:00
Micha Reiser
34b2ae73b4 Extend BestFitting with mode (#6814) 2023-08-23 17:23:45 +02:00
Charlie Marsh
71c25e4f9d Implement FormatPatternMatchValue (#6799)
## Summary

This is effectively #6608, but with additional tests.

We aren't properly handling parenthesized patterns, but that needs to be
dealt with separately as it's somewhat involved.

Closes #6555
2023-08-23 14:01:14 +00:00
Micha Reiser
4bdd99f882 Fix: Re-add missing node start positions (#6780) 2023-08-23 09:59:36 +02:00
Charlie Marsh
1e6d1182bf Improve comment handling around PatternMatchAs (#6797)
## Summary

Follows up on
https://github.com/astral-sh/ruff/pull/6652#discussion_r1300871033 with
some modifications to the `PatternMatchAs` comment handling.
Specifically, any comments between the `as` and the end are now
formatted as dangling, and we now insert some newlines in the
appropriate places.

## Test Plan

`cargo test`
2023-08-23 04:48:20 +00:00
Charlie Marsh
d08f697a04 Remove lexing for colon-matching use cases (#6803)
It's much simpler to just search ahead for the first colon.
2023-08-23 04:44:51 +00:00
Charlie Marsh
4bc5eddf91 Handle open-parenthesis comments on match case (#6798)
## Summary

Ensures that we retain the open-parenthesis comment in cases like:
```python
match pattern_comments:
    case (  # leading
        only_leading
    ):
        ...
```

Previously, this was treated as a leading comment on `only_leading`.

## Test Plan

`cargo test`
2023-08-23 00:40:18 -04:00
Charlie Marsh
5f5de52aba Confine repeated-equality-comparison-target to names and attributes (#6802)
Empirically, Pylint does this, so seems reasonable to follow.
2023-08-23 03:56:37 +00:00
Tom Kuson
1cb1bd731c Extend repeated-equality-comparison-target to check for mixed orderings and Yoda conditions. (#6691) 2023-08-23 03:45:30 +00:00
Dhruv Manilawala
db2e548f4f Simplify ANN204 autofix to use Parameters range (#6793)
## Summary

This PR fixes the bug where the decorator parentheses weren't being considered
when computing the autofix for `ANN204`. The existing logic would only look
for balanced parentheses and not multiple pairs of parentheses.

The solution is to remove the logic to generate the autofix and use the
`Parameters` end range directly which includes the parentheses as well.

## Test Plan

Add test case for `ANN204` with decorator being called

fixes: #6790
2023-08-23 09:05:46 +05:30
Harutaka Kawamura
94f5f18ddb Format PatternMatchSequence (#6676) 2023-08-23 00:44:33 +00:00
Luc Khai Hai
c34a342ab4 Format PatternMatchAs (#6652)
## Summary

Add formatting for `PatternMatchAs`.

This closes #6641.

## Test Plan

Add tests for comments.
2023-08-22 23:58:15 +00:00
Charlie Marsh
42ff833d00 Remove comment lexing from isort (#6794)
## Summary

No need to lex to find comments -- we already know their locations via
`Indexer`.
2023-08-22 21:26:38 +00:00
Konrad Listwan-Ciesielski
e1f4438498 Add docs for DTZ007 (#6757) 2023-08-22 21:12:50 +00:00
Charlie Marsh
1acdec3e29 Add a note on __future__ imports and keep-runtime-typing to pyupgrade rules (#6746)
Closes https://github.com/astral-sh/ruff/issues/6740.
2023-08-22 17:04:00 -04:00
Charlie Marsh
ca2bb20063 Fallback to end-of-file if ends in trailing continuation (#6789)
## Summary

Given:

```python
def end_of_file():
    if False:
        return 1
    x = 2 \

```

Then when searching for the end of the `x = 2` statement, we'd reach a
panic as we'd hit the last line (`\\`) and abort, since the universal
iterator doesn't return trailing newlines. Instead, we should just use
the end of the file as the fallback.

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

## Test Plan

`cargo test`
2023-08-22 15:12:26 -04:00
Dhruv Manilawala
2e00983762 Avoid C417 for lambda with default and variadic parameters (#6752)
## Summary

Avoid `C417` for `lambda` with default and variadic parameters.

## Test Plan

`cargo test` and checking if it generates any autofix errors as test
cases
for `lambda` with default parameters already exists.

fixes: #6715
2023-08-23 00:38:08 +05:30
Dhruv Manilawala
fb7caf43c8 Update lexer tests to use snapshots (#6658)
## Summary

This PR updates the lexer tests to use the snapshot testing framework.
It also
makes the following changes:
* Remove the use of macros in the lexer tests
* Use `test_case` for EOL tests

## Test Plan

```
cargo test --package ruff_python_parser --lib --all-features -- lexer::tests --no-capture
```
2023-08-22 18:23:19 +00:00
konsti
e53bf25616 Update formatter ecosystem checks revisions (#6770)
With https://github.com/django/django/pull/17181 merged, this removes an
odd edge case (tuple expression statements aka bogus trailing commas
after statements that turn them into a tuple without you noticing) that
we don't want to care about because the input code is ~wrong from the
similarity index. I've took this opportunity to update the revisions of
all projects we test.

main

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.75477          |
| django       | 0.99814          |
| transformers | 0.99621          |
| twine        | 0.99876          |
| typeshed     | 0.99953          |
| warehouse    | 0.99601          |
| zulip        | 0.99727          |

this PR

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.75996          |
| django       | 0.99819          |
| transformers | 0.99622          |
| twine        | 0.99876          |
| typeshed     | 0.99953          |
| warehouse    | 0.99607          |
| zulip        | 0.99729          |
2023-08-22 14:19:10 -04:00
Charlie Marsh
214eb707a6 Parenthesize expressions prior to LibCST parsing (#6742)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

This PR adds a utility for transforming expressions via LibCST that
automatically wraps the expression in parentheses, applies a
user-provided transformation, then strips the parentheses from the
generated code. LibCST can't parse arbitrary expression ranges, since
some expressions may require parenthesization in order to be parsed
properly. For example:

```python
option = (
    '{name}={value}'
    .format(nam=name, value=value)
)
```

In this case, the expression range is:

```python
'{name}={value}'
    .format(nam=name, value=value)
```

Which isn't valid on its own. So, instead, we add "fake" parentheses
around the expression.

We were already doing this in a few places, so this is mostly
formalizing and DRYing up that pattern.

Closes https://github.com/astral-sh/ruff/issues/6720.
2023-08-22 17:45:05 +00:00
Zanie Blue
5c1f7fd5dd Add networkx to conventional aliases (#6778)
Closes https://github.com/astral-sh/ruff/issues/6763
2023-08-22 11:49:04 -05:00
Charlie Marsh
cc278c24e2 Allow up to two empty lines after top-level imports (#6777)
## Summary

For imports, we enforce that there's _at least_ one empty line after an
import (assuming the next statement is _not_ an import), but allow up to
two at the module level.

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

## Test Plan

`cargo test`
2023-08-22 12:27:40 -04:00
Charlie Marsh
558b56f8a8 Avoid fixing D200 for docstrings that end in escapes (#6779)
Appease the fuzzers! Closes
https://github.com/astral-sh/ruff/issues/6755.
2023-08-22 16:25:37 +00:00
Charlie Marsh
749da6589a Fix isolation groups for unused imports (#6774)
## Summary

The isolation group for unused imports was relying on
`checker.semantic().current_statement()`, which isn't valid for that
rule, since it runs over the _scope_, not the statement. Instead, we
need to lookup the isolation group based on the `NodeId` of the
statement.

Our tests didn't catch this, because we mostly have cases that look like
this:

```python
if TYPE_CHECKING:
    import shelve
    import importlib
```

In this case, the two fixes to remove the two unused imports are
considered overlapping (since we delete the _full_ line, and the two
_full_ lines touch, and we consider exactly-adjacent fixes to be
overlapping), and so they don't run in a single pass due to the
non-overlapping-fixes requirement. That is: the isolation groups aren't
required for this case. They are, however, required for cases like:

```python
if TYPE_CHECKING:
    import shelve

    import importlib
```

...where the fixes don't overlap.

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

## Test Plan

`cargo test`
2023-08-22 11:55:27 -04:00
Charlie Marsh
d2eace3377 Prefer range_* edit methods (#6751) 2023-08-22 15:46:04 +00:00
173 changed files with 4149 additions and 2525 deletions

1
Cargo.lock generated
View File

@@ -2399,6 +2399,7 @@ dependencies = [
"ruff_text_size",
"rustc-hash",
"static_assertions",
"test-case",
"tiny-keccak",
"unic-emoji-char",
"unic-ucd-ident",

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ def unconventional():
import pandas
import seaborn
import tkinter
import networkx
def unconventional_aliases():
@@ -18,7 +19,7 @@ def unconventional_aliases():
import pandas as pdas
import seaborn as sbrn
import tkinter as tkr
import networkx as nxy
def conventional_aliases():
import altair as alt
@@ -27,3 +28,4 @@ def conventional_aliases():
import pandas as pd
import seaborn as sns
import tkinter as tk
import networkx as nx

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -52,7 +52,8 @@ use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind};
use ruff_python_semantic::analyze::{typing, visibility};
use ruff_python_semantic::{
BindingFlags, BindingId, BindingKind, Exceptions, Export, FromImport, Globals, Import, Module,
ModuleKind, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport, SubmoduleImport,
ModuleKind, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport,
SubmoduleImport,
};
use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS};
use ruff_source_file::Locator;
@@ -193,24 +194,6 @@ impl<'a> Checker<'a> {
}
}
/// Returns the [`IsolationLevel`] to isolate fixes for the current statement.
///
/// The primary use-case for fix isolation is to ensure that we don't delete all statements
/// in a given indented block, which would cause a syntax error. We therefore need to ensure
/// that we delete at most one statement per indented block per fixer pass. Fix isolation should
/// thus be applied whenever we delete a statement, but can otherwise be omitted.
pub(crate) fn statement_isolation(&self) -> IsolationLevel {
IsolationLevel::Group(self.semantic.current_statement_id().into())
}
/// Returns the [`IsolationLevel`] to isolate fixes in the current statement's parent.
pub(crate) fn parent_isolation(&self) -> IsolationLevel {
self.semantic
.current_statement_parent_id()
.map(|node_id| IsolationLevel::Group(node_id.into()))
.unwrap_or_default()
}
/// The [`Locator`] for the current file, which enables extraction of source code from byte
/// offsets.
pub(crate) const fn locator(&self) -> &'a Locator<'a> {
@@ -259,6 +242,18 @@ impl<'a> Checker<'a> {
pub(crate) const fn any_enabled(&self, rules: &[Rule]) -> bool {
self.settings.rules.any_enabled(rules)
}
/// Returns the [`IsolationLevel`] to isolate fixes for a given node.
///
/// The primary use-case for fix isolation is to ensure that we don't delete all statements
/// in a given indented block, which would cause a syntax error. We therefore need to ensure
/// that we delete at most one statement per indented block per fixer pass. Fix isolation should
/// thus be applied whenever we delete a statement, but can otherwise be omitted.
pub(crate) fn isolation(node_id: Option<NodeId>) -> IsolationLevel {
node_id
.map(|node_id| IsolationLevel::Group(node_id.into()))
.unwrap_or_default()
}
}
impl<'a, 'b> Visitor<'b> for Checker<'a>
@@ -1340,23 +1335,46 @@ where
fn visit_pattern(&mut self, pattern: &'b Pattern) {
// Step 1: Binding
if let Pattern::MatchAs(ast::PatternMatchAs {
name: Some(name), ..
})
| Pattern::MatchStar(ast::PatternMatchStar {
name: Some(name),
range: _,
})
| Pattern::MatchMapping(ast::PatternMatchMapping {
rest: Some(name), ..
}) = pattern
{
self.add_binding(
name,
name.range(),
BindingKind::Assignment,
BindingFlags::empty(),
);
match &pattern {
Pattern::MatchAs(ast::PatternMatchAs {
name: Some(name),
pattern: _,
range: _,
}) => {
self.add_binding(
name,
name.range(),
BindingKind::Assignment,
BindingFlags::empty(),
);
}
Pattern::MatchStar(ast::PatternMatchStar {
name: Some(name),
range: _,
}) => {
self.add_binding(
name,
name.range(),
BindingKind::Assignment,
BindingFlags::empty(),
);
}
Pattern::MatchMapping(ast::PatternMatchMapping {
rest: Some(rest), ..
}) => {
if let Pattern::MatchAs(ast::PatternMatchAs {
name: Some(name), ..
}) = rest.as_ref()
{
self.add_binding(
name,
name.range(),
BindingKind::Assignment,
BindingFlags::empty(),
);
}
}
_ => {}
}
// Step 2: Traversal

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()) {
#[allow(deprecated)]
diagnostic.set_fix_from_edit(delete_noqa(directive.range(), locator));
diagnostic
.set_fix(Fix::automatic(delete_noqa(directive.range(), locator)));
}
diagnostics.push(diagnostic);
}
@@ -175,12 +175,12 @@ pub(crate) fn check_noqa(
);
if settings.rules.should_fix(diagnostic.kind.rule()) {
if valid_codes.is_empty() {
#[allow(deprecated)]
diagnostic
.set_fix_from_edit(delete_noqa(directive.range(), locator));
diagnostic.set_fix(Fix::automatic(delete_noqa(
directive.range(),
locator,
)));
} else {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
format!("# noqa: {}", valid_codes.join(", ")),
directive.range(),
)));

View File

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

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.contents()[TextRange::new(prev_end, offset)]);
output.push_str(locator.slice(TextRange::new(prev_end, offset)));
let line = locator.full_line(offset);
@@ -619,7 +619,7 @@ fn add_noqa_inner(
prev_end = offset + line.text_len();
}
output.push_str(&locator.contents()[usize::from(prev_end)..]);
output.push_str(locator.after(prev_end));
(count, output)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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.contents()[a_range];
let b_text = &locator.contents()[b_range];
let a_text = locator.slice(a_range);
let b_text = locator.slice(b_range);
let a_leading_quote = leading_quote(a_text)?;
let b_leading_quote = leading_quote(b_text)?;

View File

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

View File

@@ -72,7 +72,7 @@ defaults.py:9:12: ICN001 [*] `pandas` should be imported as `pd`
9 |+ import pandas as pd
10 10 | import seaborn
11 11 | import tkinter
12 12 |
12 12 | import networkx
defaults.py:10:12: ICN001 [*] `seaborn` should be imported as `sns`
|
@@ -81,6 +81,7 @@ defaults.py:10:12: ICN001 [*] `seaborn` should be imported as `sns`
10 | import seaborn
| ^^^^^^^ ICN001
11 | import tkinter
12 | import networkx
|
= help: Alias `seaborn` to `sns`
@@ -91,7 +92,7 @@ defaults.py:10:12: ICN001 [*] `seaborn` should be imported as `sns`
10 |- import seaborn
10 |+ import seaborn as sns
11 11 | import tkinter
12 12 |
12 12 | import networkx
13 13 |
defaults.py:11:12: ICN001 [*] `tkinter` should be imported as `tk`
@@ -100,6 +101,7 @@ defaults.py:11:12: ICN001 [*] `tkinter` should be imported as `tk`
10 | import seaborn
11 | import tkinter
| ^^^^^^^ ICN001
12 | import networkx
|
= help: Alias `tkinter` to `tk`
@@ -109,130 +111,172 @@ defaults.py:11:12: ICN001 [*] `tkinter` should be imported as `tk`
10 10 | import seaborn
11 |- import tkinter
11 |+ import tkinter as tk
12 12 |
12 12 | import networkx
13 13 |
14 14 | def unconventional_aliases():
14 14 |
defaults.py:15:22: ICN001 [*] `altair` should be imported as `alt`
defaults.py:12:12: ICN001 [*] `networkx` should be imported as `nx`
|
14 | def unconventional_aliases():
15 | import altair as altr
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
16 | import matplotlib.pyplot as plot
17 | import numpy as nmp
17 | import matplotlib.pyplot as plot
18 | import numpy as nmp
|
= help: Alias `altair` to `alt`
Suggested fix
12 12 |
13 13 |
14 14 | def unconventional_aliases():
15 |- import altair as altr
15 |+ import altair as alt
16 16 | import matplotlib.pyplot as plot
17 17 | import numpy as nmp
18 18 | import pandas as pdas
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:16:33: ICN001 [*] `matplotlib.pyplot` should be imported as `plt`
defaults.py:17:33: ICN001 [*] `matplotlib.pyplot` should be imported as `plt`
|
14 | def unconventional_aliases():
15 | import altair as altr
16 | import matplotlib.pyplot as plot
15 | def unconventional_aliases():
16 | import altair as altr
17 | import matplotlib.pyplot as plot
| ^^^^ ICN001
17 | import numpy as nmp
18 | import pandas as pdas
18 | import numpy as nmp
19 | import pandas as pdas
|
= help: Alias `matplotlib.pyplot` to `plt`
Suggested fix
13 13 |
14 14 | def unconventional_aliases():
15 15 | import altair as altr
16 |- import matplotlib.pyplot as plot
16 |+ import matplotlib.pyplot as plt
17 17 | import numpy as nmp
18 18 | import pandas as pdas
19 19 | import seaborn as sbrn
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:17:21: ICN001 [*] `numpy` should be imported as `np`
defaults.py:18:21: ICN001 [*] `numpy` should be imported as `np`
|
15 | import altair as altr
16 | import matplotlib.pyplot as plot
17 | import numpy as nmp
16 | import altair as altr
17 | import matplotlib.pyplot as plot
18 | import numpy as nmp
| ^^^ ICN001
18 | import pandas as pdas
19 | import seaborn as sbrn
19 | import pandas as pdas
20 | import seaborn as sbrn
|
= help: Alias `numpy` to `np`
Suggested fix
14 14 | def unconventional_aliases():
15 15 | import altair as altr
16 16 | import matplotlib.pyplot as plot
17 |- import numpy as nmp
17 |+ import numpy as np
18 18 | import pandas as pdas
19 19 | import seaborn as sbrn
20 20 | import tkinter as tkr
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:18:22: ICN001 [*] `pandas` should be imported as `pd`
defaults.py:19:22: ICN001 [*] `pandas` should be imported as `pd`
|
16 | import matplotlib.pyplot as plot
17 | import numpy as nmp
18 | import pandas as pdas
17 | import matplotlib.pyplot as plot
18 | import numpy as nmp
19 | import pandas as pdas
| ^^^^ ICN001
19 | import seaborn as sbrn
20 | import tkinter as tkr
20 | import seaborn as sbrn
21 | import tkinter as tkr
|
= help: Alias `pandas` to `pd`
Suggested fix
15 15 | import altair as altr
16 16 | import matplotlib.pyplot as plot
17 17 | import numpy as nmp
18 |- import pandas as pdas
18 |+ import pandas as pd
19 19 | import seaborn as sbrn
20 20 | import tkinter as tkr
21 21 |
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:19:23: ICN001 [*] `seaborn` should be imported as `sns`
defaults.py:20:23: ICN001 [*] `seaborn` should be imported as `sns`
|
17 | import numpy as nmp
18 | import pandas as pdas
19 | import seaborn as sbrn
18 | import numpy as nmp
19 | import pandas as pdas
20 | import seaborn as sbrn
| ^^^^ ICN001
20 | import tkinter as tkr
21 | import tkinter as tkr
22 | import networkx as nxy
|
= help: Alias `seaborn` to `sns`
Suggested fix
16 16 | import matplotlib.pyplot as plot
17 17 | import numpy as nmp
18 18 | import pandas as pdas
19 |- import seaborn as sbrn
19 |+ import seaborn as sns
20 20 | import tkinter as tkr
21 21 |
22 22 |
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:20:23: ICN001 [*] `tkinter` should be imported as `tk`
defaults.py:21:23: ICN001 [*] `tkinter` should be imported as `tk`
|
18 | import pandas as pdas
19 | import seaborn as sbrn
20 | import tkinter as tkr
19 | import pandas as pdas
20 | import seaborn as sbrn
21 | import tkinter as tkr
| ^^^ ICN001
22 | import networkx as nxy
|
= help: Alias `tkinter` to `tk`
Suggested fix
17 17 | import numpy as nmp
18 18 | import pandas as pdas
19 19 | import seaborn as sbrn
20 |- import tkinter as tkr
20 |+ import tkinter as tk
21 21 |
22 22 |
23 23 | def conventional_aliases():
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,6 +21,19 @@ use ruff_macros::{derive_message_formats, violation};
/// As an alternative to `extra`, passing values as arguments to the logging
/// method can also be used to defer string formatting until required.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -54,6 +67,9 @@ use ruff_macros::{derive_message_formats, violation};
/// logging.info("%s - Something happened", user)
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html)
/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization)
@@ -89,6 +105,19 @@ impl Violation for LoggingStringFormat {
/// As an alternative to `extra`, passing values as arguments to the logging
/// method can also be used to defer string formatting until required.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -122,6 +151,9 @@ impl Violation for LoggingStringFormat {
/// logging.info("%s - Something happened", user)
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html)
/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization)
@@ -156,6 +188,19 @@ impl Violation for LoggingPercentFormat {
/// As an alternative to `extra`, passing values as arguments to the logging
/// method can also be used to defer string formatting until required.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -189,6 +234,9 @@ impl Violation for LoggingPercentFormat {
/// logging.info("%s - Something happened", user)
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html)
/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization)
@@ -222,6 +270,19 @@ impl Violation for LoggingStringConcat {
/// As an alternative to `extra`, passing values as arguments to the logging
/// method can also be used to defer string formatting until required.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -255,6 +316,9 @@ impl Violation for LoggingStringConcat {
/// logging.info("%s - Something happened", user)
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html)
/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization)
@@ -276,6 +340,19 @@ impl Violation for LoggingFString {
/// `logging.warning` and `logging.Logger.warning`, which are functionally
/// equivalent.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -290,6 +367,9 @@ impl Violation for LoggingFString {
/// logging.warning("Something happened")
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging.warning`](https://docs.python.org/3/library/logging.html#logging.warning)
/// - [Python documentation: `logging.Logger.warning`](https://docs.python.org/3/library/logging.html#logging.Logger.warning)
@@ -320,6 +400,19 @@ impl AlwaysAutofixableViolation for LoggingWarn {
/// the `LogRecord` constructor will raise a `KeyError` when the `LogRecord` is
/// constructed.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -342,6 +435,9 @@ impl AlwaysAutofixableViolation for LoggingWarn {
/// logging.info("Something happened", extra=dict(user=username))
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: LogRecord attributes](https://docs.python.org/3/library/logging.html#logrecord-attributes)
#[violation]
@@ -365,6 +461,19 @@ impl Violation for LoggingExtraAttrClash {
/// `logging.exception`. Using `logging.exception` is more concise, more
/// readable, and conveys the intent of the logging statement more clearly.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -385,6 +494,9 @@ impl Violation for LoggingExtraAttrClash {
/// logging.exception("Exception occurred")
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging.exception`](https://docs.python.org/3/library/logging.html#logging.exception)
/// - [Python documentation: `exception`](https://docs.python.org/3/library/logging.html#logging.Logger.exception)
@@ -410,6 +522,19 @@ impl Violation for LoggingExcInfo {
/// Passing `exc_info=True` to `logging.exception` calls is redundant, as is
/// passing `exc_info=False` to `logging.error` calls.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -430,6 +555,9 @@ impl Violation for LoggingExcInfo {
/// logging.exception("Exception occurred")
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging.exception`](https://docs.python.org/3/library/logging.html#logging.exception)
/// - [Python documentation: `exception`](https://docs.python.org/3/library/logging.html#logging.Logger.exception)

View File

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

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(Expr::BinOp(ast::ExprBinOp { left, right, .. })) = parent {
if let Some(parent @ Expr::BinOp(ast::ExprBinOp { left, right, .. })) = parent {
// Replace the parent with its non-duplicate child.
let child = if expr == left.as_ref() { right } else { left };
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
checker.locator().slice(child.range()).to_string(),
parent.unwrap().range(),
parent.range(),
)));
}
}

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -299,6 +299,8 @@ PT018.py:44:1: PT018 [*] Assertion should be broken down into multiple parts
44 |+assert something
45 |+assert something_else
45 46 | assert something and something_else and something_third # Error
46 47 |
47 48 |
PT018.py:45:1: PT018 [*] Assertion should be broken down into multiple parts
|
@@ -316,5 +318,37 @@ PT018.py:45:1: PT018 [*] Assertion should be broken down into multiple parts
45 |-assert something and something_else and something_third # Error
45 |+assert something and something_else
46 |+assert something_third
46 47 |
47 48 |
48 49 | def test_multiline():
PT018.py:49:5: PT018 Assertion should be broken down into multiple parts
|
48 | def test_multiline():
49 | assert something and something_else; x = 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
50 |
51 | x = 1; assert something and something_else
|
= help: Break down assertion into multiple parts
PT018.py:51:12: PT018 Assertion should be broken down into multiple parts
|
49 | assert something and something_else; x = 1
50 |
51 | x = 1; assert something and something_else
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
52 |
53 | x = 1; \
|
= help: Break down assertion into multiple parts
PT018.py:54:9: PT018 Assertion should be broken down into multiple parts
|
53 | x = 1; \
54 | assert something and something_else
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
|
= help: Break down assertion into multiple parts

View File

@@ -25,19 +25,11 @@ pub(super) fn result_exists(returns: &[&ast::StmtReturn]) -> bool {
/// This method assumes that the statement is the last statement in its body; specifically, that
/// the statement isn't followed by a semicolon, followed by a multi-line statement.
pub(super) fn end_of_last_statement(stmt: &Stmt, locator: &Locator) -> TextSize {
if stmt.end() == locator.text_len() {
// End-of-file, so just return the end of the statement.
stmt.end()
} else {
// Otherwise, find the end of the last line that's "part of" the statement.
let contents = locator.after(stmt.end());
for line in contents.universal_newlines() {
if !line.ends_with('\\') {
return stmt.end() + line.end();
}
// Find the end of the last line that's "part of" the statement.
for line in locator.after(stmt.end()).universal_newlines() {
if !line.ends_with('\\') {
return stmt.end() + line.end();
}
unreachable!("Expected to find end-of-statement")
}
locator.text_len()
}

View File

@@ -380,5 +380,24 @@ RET503.py:320:9: RET503 [*] Missing explicit `return` at the end of function abl
321 321 | return "" \
322 322 | ; # type: ignore
323 |+ return None
323 324 |
324 325 |
325 326 | def end_of_file():
RET503.py:328:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
326 | if False:
327 | return 1
328 | x = 2 \
| ^^^^^ RET503
|
= help: Add explicit `return` statement
Suggested fix
326 326 | if False:
327 327 | return 1
328 328 | x = 2 \
329 |+
330 |+ return None

View File

@@ -11,8 +11,8 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::{ComparableConstant, ComparableExpr, ComparableStmt};
use ruff_python_ast::helpers::{any_over_expr, contains_effect};
use ruff_python_ast::stmt_if::{if_elif_branches, IfElifBranch};
use ruff_python_parser::first_colon_range;
use ruff_python_semantic::SemanticModel;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::{Locator, UniversalNewlines};
use crate::checkers::ast::Checker;
@@ -369,16 +369,10 @@ pub(crate) fn nested_if_statements(
};
// Find the deepest nested if-statement, to inform the range.
let Some((test, first_stmt)) = find_last_nested_if(body) else {
let Some((test, _first_stmt)) = find_last_nested_if(body) else {
return;
};
let colon = first_colon_range(
TextRange::new(test.end(), first_stmt.start()),
checker.locator().contents(),
checker.source_type.is_jupyter(),
);
// Check if the parent is already emitting a larger diagnostic including this if statement
if let Some(Stmt::If(stmt_if)) = parent {
if let Some((body, _range, _is_elif)) = nested_if_body(stmt_if) {
@@ -392,10 +386,14 @@ pub(crate) fn nested_if_statements(
}
}
let mut diagnostic = Diagnostic::new(
CollapsibleIf,
colon.map_or(range, |colon| TextRange::new(range.start(), colon.end())),
);
let Some(colon) = SimpleTokenizer::starts_at(test.end(), checker.locator().contents())
.skip_trivia()
.find(|token| token.kind == SimpleTokenKind::Colon)
else {
return;
};
let mut diagnostic = Diagnostic::new(CollapsibleIf, TextRange::new(range.start(), colon.end()));
if checker.patch(diagnostic.kind.rule()) {
// The fixer preserves comments in the nested body, but removes comments between
// the outer and inner if statements.
@@ -721,10 +719,16 @@ pub(crate) fn if_with_same_arms(checker: &mut Checker, locator: &Locator, stmt_i
// ...and the same comments
let first_comments = checker
.indexer()
.comments_in_range(body_range(&current_branch, locator), locator);
.comment_ranges()
.comments_in_range(body_range(&current_branch, locator))
.iter()
.map(|range| locator.slice(*range));
let second_comments = checker
.indexer()
.comments_in_range(body_range(following_branch, locator), locator);
.comment_ranges()
.comments_in_range(body_range(following_branch, locator))
.iter()
.map(|range| locator.slice(*range));
if !first_comments.eq(second_comments) {
continue;
}

View File

@@ -1,12 +1,12 @@
use log::error;
use ruff_python_ast::{self as ast, Ranged, Stmt, WithItem};
use ruff_text_size::TextRange;
use ruff_diagnostics::{AutofixKind, Violation};
use ruff_diagnostics::{Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_parser::first_colon_range;
use ruff_python_ast::{self as ast, Ranged, Stmt, WithItem};
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::UniversalNewlines;
use ruff_text_size::TextRange;
use crate::checkers::ast::Checker;
use crate::line_width::LineWidth;
@@ -106,32 +106,24 @@ pub(crate) fn multiple_with_statements(
}
}
if let Some((is_async, items, body)) = next_with(&with_stmt.body) {
if let Some((is_async, items, _body)) = next_with(&with_stmt.body) {
if is_async != with_stmt.is_async {
// One of the statements is an async with, while the other is not,
// we can't merge those statements.
return;
}
let last_item = items.last().expect("Expected items to be non-empty");
let colon = first_colon_range(
TextRange::new(
last_item
.optional_vars
.as_ref()
.map_or(last_item.context_expr.end(), |v| v.end()),
body.first().expect("Expected body to be non-empty").start(),
),
checker.locator().contents(),
checker.source_type.is_jupyter(),
);
let Some(colon) = items.last().and_then(|item| {
SimpleTokenizer::starts_at(item.end(), checker.locator().contents())
.skip_trivia()
.find(|token| token.kind == SimpleTokenKind::Colon)
}) else {
return;
};
let mut diagnostic = Diagnostic::new(
MultipleWithStatements,
colon.map_or_else(
|| with_stmt.range(),
|colon| TextRange::new(with_stmt.start(), colon.end()),
),
TextRange::new(with_stmt.start(), colon.end()),
);
if checker.patch(diagnostic.kind.rule()) {
if !checker

View File

@@ -8,10 +8,9 @@ use ruff_python_codegen::Stylist;
use ruff_python_stdlib::str::{self};
use ruff_source_file::Locator;
use crate::autofix::codemods::CodegenStylist;
use crate::autofix::snippet::SourceCodeSnippet;
use crate::checkers::ast::Checker;
use crate::cst::matchers::{match_comparison, match_expression};
use crate::cst::matchers::{match_comparison, transform_expression};
use crate::registry::AsRule;
/// ## What it does
@@ -96,68 +95,69 @@ fn is_constant_like(expr: &Expr) -> bool {
/// Generate a fix to reverse a comparison.
fn reverse_comparison(expr: &Expr, locator: &Locator, stylist: &Stylist) -> Result<String> {
let range = expr.range();
let contents = locator.slice(range);
let source_code = locator.slice(range);
let mut expression = match_expression(contents)?;
let comparison = match_comparison(&mut expression)?;
transform_expression(source_code, stylist, |mut expression| {
let comparison = match_comparison(&mut expression)?;
let left = (*comparison.left).clone();
let left = (*comparison.left).clone();
// Copy the right side to the left side.
comparison.left = Box::new(comparison.comparisons[0].comparator.clone());
// Copy the right side to the left side.
comparison.left = Box::new(comparison.comparisons[0].comparator.clone());
// Copy the left side to the right side.
comparison.comparisons[0].comparator = left;
// Copy the left side to the right side.
comparison.comparisons[0].comparator = left;
// Reverse the operator.
let op = comparison.comparisons[0].operator.clone();
comparison.comparisons[0].operator = match op {
CompOp::LessThan {
whitespace_before,
whitespace_after,
} => CompOp::GreaterThan {
whitespace_before,
whitespace_after,
},
CompOp::GreaterThan {
whitespace_before,
whitespace_after,
} => CompOp::LessThan {
whitespace_before,
whitespace_after,
},
CompOp::LessThanEqual {
whitespace_before,
whitespace_after,
} => CompOp::GreaterThanEqual {
whitespace_before,
whitespace_after,
},
CompOp::GreaterThanEqual {
whitespace_before,
whitespace_after,
} => CompOp::LessThanEqual {
whitespace_before,
whitespace_after,
},
CompOp::Equal {
whitespace_before,
whitespace_after,
} => CompOp::Equal {
whitespace_before,
whitespace_after,
},
CompOp::NotEqual {
whitespace_before,
whitespace_after,
} => CompOp::NotEqual {
whitespace_before,
whitespace_after,
},
_ => panic!("Expected comparison operator"),
};
// Reverse the operator.
let op = comparison.comparisons[0].operator.clone();
comparison.comparisons[0].operator = match op {
CompOp::LessThan {
whitespace_before,
whitespace_after,
} => CompOp::GreaterThan {
whitespace_before,
whitespace_after,
},
CompOp::GreaterThan {
whitespace_before,
whitespace_after,
} => CompOp::LessThan {
whitespace_before,
whitespace_after,
},
CompOp::LessThanEqual {
whitespace_before,
whitespace_after,
} => CompOp::GreaterThanEqual {
whitespace_before,
whitespace_after,
},
CompOp::GreaterThanEqual {
whitespace_before,
whitespace_after,
} => CompOp::LessThanEqual {
whitespace_before,
whitespace_after,
},
CompOp::Equal {
whitespace_before,
whitespace_after,
} => CompOp::Equal {
whitespace_before,
whitespace_after,
},
CompOp::NotEqual {
whitespace_before,
whitespace_after,
} => CompOp::NotEqual {
whitespace_before,
whitespace_after,
},
_ => panic!("Expected comparison operator"),
};
Ok(expression.codegen_stylist(stylist))
Ok(expression)
})
}
/// SIM300

View File

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

View File

@@ -236,7 +236,8 @@ fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) ->
)?;
Ok(
Fix::suggested_edits(remove_import_edit, add_import_edit.into_edits())
.isolate(checker.parent_isolation()),
Fix::suggested_edits(remove_import_edit, add_import_edit.into_edits()).isolate(
Checker::isolation(checker.semantic().parent_statement_id(node_id)),
),
)
}

View File

@@ -486,7 +486,8 @@ fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) ->
)?;
Ok(
Fix::suggested_edits(remove_import_edit, add_import_edit.into_edits())
.isolate(checker.parent_isolation()),
Fix::suggested_edits(remove_import_edit, add_import_edit.into_edits()).isolate(
Checker::isolation(checker.semantic().parent_statement_id(node_id)),
),
)
}

View File

@@ -1,7 +1,7 @@
use std::borrow::Cow;
use ruff_python_ast::{PySourceType, Ranged};
use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_python_ast::Ranged;
use ruff_python_index::Indexer;
use ruff_source_file::Locator;
use ruff_text_size::TextRange;
@@ -21,20 +21,15 @@ impl Ranged for Comment<'_> {
pub(crate) fn collect_comments<'a>(
range: TextRange,
locator: &'a Locator,
source_type: PySourceType,
indexer: &'a Indexer,
) -> Vec<Comment<'a>> {
let contents = locator.slice(range);
lexer::lex_starts_at(contents, source_type.as_mode(), range.start())
.flatten()
.filter_map(|(tok, range)| {
if let Tok::Comment(value) = tok {
Some(Comment {
value: value.into(),
range,
})
} else {
None
}
indexer
.comment_ranges()
.comments_in_range(range)
.iter()
.map(|range| Comment {
value: locator.slice(*range).into(),
range: *range,
})
.collect()
}

View File

@@ -1,16 +1,16 @@
use std::path::Path;
use itertools::{EitherOrBoth, Itertools};
use ruff_python_ast::{PySourceType, Ranged, Stmt};
use ruff_text_size::TextRange;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::whitespace::{followed_by_multi_statement_line, trailing_lines_end};
use ruff_python_ast::whitespace::trailing_lines_end;
use ruff_python_ast::{PySourceType, Ranged, Stmt};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_trivia::{leading_indentation, textwrap::indent, PythonWhitespace};
use ruff_source_file::{Locator, UniversalNewlines};
use ruff_text_size::TextRange;
use crate::line_width::LineWidth;
use crate::registry::AsRule;
@@ -97,7 +97,7 @@ pub(crate) fn organize_imports(
// Special-cases: there's leading or trailing content in the import block. These
// are too hard to get right, and relatively rare, so flag but don't fix.
if indexer.preceded_by_multi_statement_line(block.imports.first().unwrap(), locator)
|| followed_by_multi_statement_line(block.imports.last().unwrap(), locator)
|| indexer.followed_by_multi_statement_line(block.imports.last().unwrap(), locator)
{
return Some(Diagnostic::new(UnsortedImports, range));
}
@@ -106,7 +106,7 @@ pub(crate) fn organize_imports(
let comments = comments::collect_comments(
TextRange::new(range.start(), locator.full_line_end(range.end())),
locator,
source_type,
indexer,
);
let trailing_line_end = if block.trailer.is_none() {

View File

@@ -39,6 +39,7 @@ mod tests {
#[test_case(Rule::NewLineAfterLastParagraph, Path::new("D.py"))]
#[test_case(Rule::NewLineAfterSectionName, Path::new("sections.py"))]
#[test_case(Rule::NoBlankLineAfterFunction, Path::new("D.py"))]
#[test_case(Rule::FitsOnOneLine, Path::new("D200.py"))]
#[test_case(Rule::NoBlankLineAfterFunction, Path::new("D202.py"))]
#[test_case(Rule::BlankLineBeforeClass, Path::new("D.py"))]
#[test_case(Rule::NoBlankLineBeforeFunction, Path::new("D.py"))]

View File

@@ -74,7 +74,8 @@ pub(crate) fn one_liner(checker: &mut Checker, docstring: &Docstring) {
// characters, avoid applying the fix.
let body = docstring.body();
let trimmed = body.trim();
if !trimmed.ends_with(trailing.chars().last().unwrap())
if trimmed.chars().rev().take_while(|c| *c == '\\').count() % 2 == 0
&& !trimmed.ends_with(trailing.chars().last().unwrap())
&& !trimmed.starts_with(leading.chars().last().unwrap())
{
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(

View File

@@ -0,0 +1,45 @@
---
source: crates/ruff/src/rules/pydocstyle/mod.rs
---
D200.py:2:5: D200 One-line docstring should fit on one line
|
1 | def func():
2 | """\
| _____^
3 | | """
| |_______^ D200
|
= help: Reformat to one line
D200.py:7:5: D200 [*] One-line docstring should fit on one line
|
6 | def func():
7 | """\\
| _____^
8 | | """
| |_______^ D200
|
= help: Reformat to one line
Suggested fix
4 4 |
5 5 |
6 6 | def func():
7 |- """\\
8 |- """
7 |+ """\\"""
9 8 |
10 9 |
11 10 | def func():
D200.py:12:5: D200 One-line docstring should fit on one line
|
11 | def func():
12 | """\ \
| _____^
13 | | """
| |_______^ D200
|
= help: Reformat to one line

View File

@@ -1,93 +1,88 @@
use anyhow::{Context, Ok, Result};
use ruff_python_ast::{Expr, Ranged};
use ruff_text_size::TextRange;
use ruff_diagnostics::Edit;
use ruff_python_ast::{self as ast, Ranged};
use ruff_python_codegen::Stylist;
use ruff_python_semantic::Binding;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::Locator;
use crate::autofix::codemods::CodegenStylist;
use crate::cst::matchers::{match_call_mut, match_dict, match_expression};
use crate::cst::matchers::{match_call_mut, match_dict, transform_expression};
/// Generate a [`Edit`] to remove unused keys from format dict.
pub(super) fn remove_unused_format_arguments_from_dict(
unused_arguments: &[usize],
stmt: &Expr,
dict: &ast::ExprDict,
locator: &Locator,
stylist: &Stylist,
) -> Result<Edit> {
let module_text = locator.slice(stmt.range());
let mut tree = match_expression(module_text)?;
let dict = match_dict(&mut tree)?;
let source_code = locator.slice(dict.range());
transform_expression(source_code, stylist, |mut expression| {
let dict = match_dict(&mut expression)?;
// Remove the elements at the given indexes.
let mut index = 0;
dict.elements.retain(|_| {
let is_unused = unused_arguments.contains(&index);
index += 1;
!is_unused
});
// Remove the elements at the given indexes.
let mut index = 0;
dict.elements.retain(|_| {
let is_unused = unused_arguments.contains(&index);
index += 1;
!is_unused
});
Ok(Edit::range_replacement(
tree.codegen_stylist(stylist),
stmt.range(),
))
Ok(expression)
})
.map(|output| Edit::range_replacement(output, dict.range()))
}
/// Generate a [`Edit`] to remove unused keyword arguments from a `format` call.
pub(super) fn remove_unused_keyword_arguments_from_format_call(
unused_arguments: &[usize],
location: TextRange,
call: &ast::ExprCall,
locator: &Locator,
stylist: &Stylist,
) -> Result<Edit> {
let module_text = locator.slice(location);
let mut tree = match_expression(module_text)?;
let call = match_call_mut(&mut tree)?;
let source_code = locator.slice(call.range());
transform_expression(source_code, stylist, |mut expression| {
let call = match_call_mut(&mut expression)?;
// Remove the keyword arguments at the given indexes.
let mut index = 0;
call.args.retain(|arg| {
if arg.keyword.is_none() {
return true;
}
// Remove the keyword arguments at the given indexes.
let mut index = 0;
call.args.retain(|arg| {
if arg.keyword.is_none() {
return true;
}
let is_unused = unused_arguments.contains(&index);
index += 1;
!is_unused
});
let is_unused = unused_arguments.contains(&index);
index += 1;
!is_unused
});
Ok(Edit::range_replacement(
tree.codegen_stylist(stylist),
location,
))
Ok(expression)
})
.map(|output| Edit::range_replacement(output, call.range()))
}
/// Generate a [`Edit`] to remove unused positional arguments from a `format` call.
pub(crate) fn remove_unused_positional_arguments_from_format_call(
unused_arguments: &[usize],
location: TextRange,
call: &ast::ExprCall,
locator: &Locator,
stylist: &Stylist,
) -> Result<Edit> {
let module_text = locator.slice(location);
let mut tree = match_expression(module_text)?;
let call = match_call_mut(&mut tree)?;
let source_code = locator.slice(call.range());
transform_expression(source_code, stylist, |mut expression| {
let call = match_call_mut(&mut expression)?;
// Remove any unused arguments.
let mut index = 0;
call.args.retain(|_| {
let is_unused = unused_arguments.contains(&index);
index += 1;
!is_unused
});
// Remove any unused arguments.
let mut index = 0;
call.args.retain(|_| {
let is_unused = unused_arguments.contains(&index);
index += 1;
!is_unused
});
Ok(Edit::range_replacement(
tree.codegen_stylist(stylist),
location,
))
Ok(expression)
})
.map(|output| Edit::range_replacement(output, call.range()))
}
/// Generate a [`Edit`] to remove the binding from an exception handler.

View File

@@ -62,8 +62,7 @@ fn find_useless_f_strings<'a>(
kind: StringKind::FString | StringKind::RawFString,
..
} => {
let first_char =
&locator.contents()[TextRange::at(range.start(), TextSize::from(1))];
let first_char = locator.slice(TextRange::at(range.start(), TextSize::from(1)));
// f"..." => f_position = 0
// fr"..." => f_position = 0
// rf"..." => f_position = 1

View File

@@ -1,6 +1,6 @@
use std::string::ToString;
use ruff_python_ast::{self as ast, Constant, Expr, Identifier, Keyword};
use ruff_python_ast::{self as ast, Constant, Expr, Identifier, Keyword, Ranged};
use ruff_text_size::TextRange;
use rustc_hash::FxHashSet;
@@ -570,15 +570,16 @@ pub(crate) fn percent_format_extra_named_arguments(
if summary.num_positional > 0 {
return;
}
let Expr::Dict(ast::ExprDict { keys, .. }) = &right else {
let Expr::Dict(dict) = &right else {
return;
};
// If any of the keys are spread, abort.
if keys.iter().any(Option::is_none) {
if dict.keys.iter().any(Option::is_none) {
return;
}
let missing: Vec<(usize, &str)> = keys
let missing: Vec<(usize, &str)> = dict
.keys
.iter()
.enumerate()
.filter_map(|(index, key)| match key {
@@ -613,7 +614,7 @@ pub(crate) fn percent_format_extra_named_arguments(
diagnostic.try_set_fix(|| {
let edit = remove_unused_format_arguments_from_dict(
&indexes,
right,
dict,
checker.locator(),
checker.stylist(),
)?;
@@ -739,9 +740,9 @@ pub(crate) fn percent_format_star_requires_sequence(
/// F522
pub(crate) fn string_dot_format_extra_named_arguments(
checker: &mut Checker,
call: &ast::ExprCall,
summary: &FormatSummary,
keywords: &[Keyword],
location: TextRange,
) {
// If there are any **kwargs, abort.
if has_star_star_kwargs(keywords) {
@@ -773,14 +774,14 @@ pub(crate) fn string_dot_format_extra_named_arguments(
.collect();
let mut diagnostic = Diagnostic::new(
StringDotFormatExtraNamedArguments { missing: names },
location,
call.range(),
);
if checker.patch(diagnostic.kind.rule()) {
let indexes: Vec<usize> = missing.iter().map(|(index, _)| *index).collect();
diagnostic.try_set_fix(|| {
let edit = remove_unused_keyword_arguments_from_format_call(
&indexes,
location,
call,
checker.locator(),
checker.stylist(),
)?;
@@ -793,9 +794,9 @@ pub(crate) fn string_dot_format_extra_named_arguments(
/// F523
pub(crate) fn string_dot_format_extra_positional_arguments(
checker: &mut Checker,
call: &ast::ExprCall,
summary: &FormatSummary,
args: &[Expr],
location: TextRange,
) {
let missing: Vec<usize> = args
.iter()
@@ -817,7 +818,7 @@ pub(crate) fn string_dot_format_extra_positional_arguments(
.map(ToString::to_string)
.collect::<Vec<String>>(),
},
location,
call.range(),
);
if checker.patch(diagnostic.kind.rule()) {
// We can only fix if the positional arguments we're removing don't require re-indexing
@@ -849,7 +850,7 @@ pub(crate) fn string_dot_format_extra_positional_arguments(
diagnostic.try_set_fix(|| {
let edit = remove_unused_positional_arguments_from_format_call(
&missing,
location,
call,
checker.locator(),
checker.stylist(),
)?;
@@ -863,10 +864,10 @@ pub(crate) fn string_dot_format_extra_positional_arguments(
/// F524
pub(crate) fn string_dot_format_missing_argument(
checker: &mut Checker,
call: &ast::ExprCall,
summary: &FormatSummary,
args: &[Expr],
keywords: &[Keyword],
location: TextRange,
) {
if has_star_args(args) || has_star_star_kwargs(keywords) {
return;
@@ -898,7 +899,7 @@ pub(crate) fn string_dot_format_missing_argument(
if !missing.is_empty() {
checker.diagnostics.push(Diagnostic::new(
StringDotFormatMissingArguments { missing },
location,
call.range(),
));
}
}
@@ -906,12 +907,13 @@ pub(crate) fn string_dot_format_missing_argument(
/// F525
pub(crate) fn string_dot_format_mixing_automatic(
checker: &mut Checker,
call: &ast::ExprCall,
summary: &FormatSummary,
location: TextRange,
) {
if !(summary.autos.is_empty() || summary.indices.is_empty()) {
checker
.diagnostics
.push(Diagnostic::new(StringDotFormatMixingAutomatic, location));
checker.diagnostics.push(Diagnostic::new(
StringDotFormatMixingAutomatic,
call.range(),
));
}
}

View File

@@ -217,6 +217,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
}
/// An unused import with its surrounding context.
#[derive(Debug)]
struct ImportBinding<'a> {
/// The qualified name of the import (e.g., `typing.List` for `from typing import List`).
import: AnyImport<'a>,
@@ -251,5 +252,7 @@ fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) ->
checker.stylist(),
checker.indexer(),
)?;
Ok(Fix::automatic(edit).isolate(checker.parent_isolation()))
Ok(Fix::automatic(edit).isolate(Checker::isolation(
checker.semantic().parent_statement_id(node_id),
)))
}

View File

@@ -1,6 +1,6 @@
use itertools::Itertools;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, IsolationLevel, Violation};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::contains_effect;
use ruff_python_ast::{self as ast, PySourceType, Ranged, Stmt};
@@ -206,11 +206,7 @@ fn remove_unused_variable(binding: &Binding, checker: &Checker) -> Option<Fix> {
let node_id = binding.source?;
let statement = checker.semantic().statement(node_id);
let parent = checker.semantic().parent_statement(node_id);
let isolation = checker
.semantic()
.parent_statement_id(node_id)
.map(|node_id| IsolationLevel::Group(node_id.into()))
.unwrap_or_default();
let isolation = Checker::isolation(checker.semantic().parent_statement_id(node_id));
// First case: simple assignment (`x = 1`)
if let Stmt::Assign(ast::StmtAssign { targets, value, .. }) = statement {

View File

@@ -190,5 +190,78 @@ F401_0.py:99:8: F401 [*] `foo.bar.baz` imported but unused
99 |-import foo.bar.baz
100 99 |
101 100 | print(bop.baz.read_csv("test.csv"))
102 101 |
F401_0.py:105:12: F401 [*] `a1` imported but unused
|
103 | # Test: isolated deletions.
104 | if TYPE_CHECKING:
105 | import a1
| ^^ F401
106 |
107 | import a2
|
= help: Remove unused import: `a1`
Fix
102 102 |
103 103 | # Test: isolated deletions.
104 104 | if TYPE_CHECKING:
105 |- import a1
106 105 |
107 106 | import a2
108 107 |
F401_0.py:107:12: F401 [*] `a2` imported but unused
|
105 | import a1
106 |
107 | import a2
| ^^ F401
|
= help: Remove unused import: `a2`
Fix
104 104 | if TYPE_CHECKING:
105 105 | import a1
106 106 |
107 |- import a2
108 107 |
109 108 |
110 109 | match *0, 1, *2:
F401_0.py:112:16: F401 [*] `b1` imported but unused
|
110 | match *0, 1, *2:
111 | case 0,:
112 | import b1
| ^^ F401
113 |
114 | import b2
|
= help: Remove unused import: `b1`
Fix
109 109 |
110 110 | match *0, 1, *2:
111 111 | case 0,:
112 |- import b1
113 112 |
114 113 | import b2
F401_0.py:114:16: F401 [*] `b2` imported but unused
|
112 | import b1
113 |
114 | import b2
| ^^ F401
|
= help: Remove unused import: `b2`
Fix
111 111 | case 0,:
112 112 | import b1
113 113 |
114 |- import b2

View File

@@ -33,7 +33,7 @@ F522.py:2:1: F522 [*] `.format` call has unused named argument(s): spam
2 |+"{bar}{}".format(1, bar=2, ) # F522
3 3 | "{bar:{spam}}".format(bar=2, spam=3) # No issues
4 4 | "{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
5 5 | # Not fixable
5 5 | (''
F522.py:4:1: F522 [*] `.format` call has unused named argument(s): eggs, ham
|
@@ -41,8 +41,8 @@ F522.py:4:1: F522 [*] `.format` call has unused named argument(s): eggs, ham
3 | "{bar:{spam}}".format(bar=2, spam=3) # No issues
4 | "{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F522
5 | # Not fixable
6 | (''
5 | (''
6 | .format(x=2)) # F522
|
= help: Remove extra named arguments: eggs, ham
@@ -52,19 +52,25 @@ F522.py:4:1: F522 [*] `.format` call has unused named argument(s): eggs, ham
3 3 | "{bar:{spam}}".format(bar=2, spam=3) # No issues
4 |-"{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
4 |+"{bar:{spam}}".format(bar=2, spam=3, ) # F522
5 5 | # Not fixable
6 6 | (''
7 7 | .format(x=2))
5 5 | (''
6 6 | .format(x=2)) # F522
F522.py:6:2: F522 `.format` call has unused named argument(s): x
F522.py:5:2: F522 [*] `.format` call has unused named argument(s): x
|
3 | "{bar:{spam}}".format(bar=2, spam=3) # No issues
4 | "{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
5 | # Not fixable
6 | (''
5 | (''
| __^
7 | | .format(x=2))
6 | | .format(x=2)) # F522
| |_____________^ F522
|
= help: Remove extra named arguments: x
Fix
3 3 | "{bar:{spam}}".format(bar=2, spam=3) # No issues
4 4 | "{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
5 5 | (''
6 |- .format(x=2)) # F522
6 |+ .format()) # F522

View File

@@ -243,13 +243,13 @@ F523.py:29:1: F523 `.format` call has unused arguments at position(s): 0
29 | "{1} {8}".format(0, 1) # F523, # F524
| ^^^^^^^^^^^^^^^^^^^^^^ F523
30 |
31 | # Not fixable
31 | # Multiline
|
= help: Remove extra positional arguments at position(s): 0
F523.py:32:2: F523 `.format` call has unused arguments at position(s): 0
F523.py:32:2: F523 [*] `.format` call has unused arguments at position(s): 0
|
31 | # Not fixable
31 | # Multiline
32 | (''
| __^
33 | | .format(2))
@@ -257,4 +257,11 @@ F523.py:32:2: F523 `.format` call has unused arguments at position(s): 0
|
= help: Remove extra positional arguments at position(s): 0
Fix
30 30 |
31 31 | # Multiline
32 32 | (''
33 |-.format(2))
33 |+.format())

View File

@@ -601,5 +601,76 @@ F841_3.py:155:17: F841 [*] Local variable `e` is assigned to but never used
155 |- except A as e :
155 |+ except A:
156 156 | print("oh no!")
157 157 |
158 158 |
F841_3.py:160:5: F841 [*] Local variable `x` is assigned to but never used
|
159 | def f():
160 | x = 1
| ^ F841
161 | y = 2
|
= help: Remove assignment to unused variable `x`
Suggested fix
157 157 |
158 158 |
159 159 | def f():
160 |- x = 1
161 160 | y = 2
162 161 |
163 162 |
F841_3.py:161:5: F841 [*] Local variable `y` is assigned to but never used
|
159 | def f():
160 | x = 1
161 | y = 2
| ^ F841
|
= help: Remove assignment to unused variable `y`
Suggested fix
158 158 |
159 159 | def f():
160 160 | x = 1
161 |- y = 2
162 161 |
163 162 |
164 163 | def f():
F841_3.py:165:5: F841 [*] Local variable `x` is assigned to but never used
|
164 | def f():
165 | x = 1
| ^ F841
166 |
167 | y = 2
|
= help: Remove assignment to unused variable `x`
Suggested fix
162 162 |
163 163 |
164 164 | def f():
165 |- x = 1
166 165 |
167 166 | y = 2
F841_3.py:167:5: F841 [*] Local variable `y` is assigned to but never used
|
165 | x = 1
166 |
167 | y = 2
| ^ F841
|
= help: Remove assignment to unused variable `y`
Suggested fix
164 164 | def f():
165 165 | x = 1
166 166 |
167 |- y = 2

View File

@@ -2,15 +2,16 @@ use std::hash::BuildHasherDefault;
use std::ops::Deref;
use itertools::{any, Itertools};
use ruff_python_ast::{BoolOp, CmpOp, Expr, ExprBoolOp, ExprCompare, Ranged};
use rustc_hash::FxHashMap;
use crate::autofix::snippet::SourceCodeSnippet;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::hashable::HashableExpr;
use ruff_python_ast::{self as ast, BoolOp, CmpOp, Expr, Ranged};
use ruff_source_file::Locator;
use crate::autofix::snippet::SourceCodeSnippet;
use crate::checkers::ast::Checker;
/// ## What it does
@@ -63,7 +64,10 @@ impl Violation for RepeatedEqualityComparisonTarget {
}
/// PLR1714
pub(crate) fn repeated_equality_comparison_target(checker: &mut Checker, bool_op: &ExprBoolOp) {
pub(crate) fn repeated_equality_comparison_target(
checker: &mut Checker,
bool_op: &ast::ExprBoolOp,
) {
if bool_op
.values
.iter()
@@ -72,27 +76,49 @@ pub(crate) fn repeated_equality_comparison_target(checker: &mut Checker, bool_op
return;
}
let mut left_to_comparators: FxHashMap<HashableExpr, (usize, Vec<&Expr>)> =
FxHashMap::with_capacity_and_hasher(bool_op.values.len(), BuildHasherDefault::default());
let mut value_to_comparators: FxHashMap<HashableExpr, (usize, Vec<&Expr>)> =
FxHashMap::with_capacity_and_hasher(
bool_op.values.len() * 2,
BuildHasherDefault::default(),
);
for value in &bool_op.values {
if let Expr::Compare(ExprCompare {
// Enforced via `is_allowed_value`.
let Expr::Compare(ast::ExprCompare {
left, comparators, ..
}) = value
{
let (count, matches) = left_to_comparators
else {
return;
};
// Enforced via `is_allowed_value`.
let [right] = comparators.as_slice() else {
return;
};
if matches!(left.as_ref(), Expr::Name(_) | Expr::Attribute(_)) {
let (left_count, left_matches) = value_to_comparators
.entry(left.deref().into())
.or_insert_with(|| (0, Vec::new()));
*count += 1;
matches.extend(comparators);
*left_count += 1;
left_matches.push(right);
}
if matches!(right, Expr::Name(_) | Expr::Attribute(_)) {
let (right_count, right_matches) = value_to_comparators
.entry(right.into())
.or_insert_with(|| (0, Vec::new()));
*right_count += 1;
right_matches.push(left);
}
}
for (left, (count, comparators)) in left_to_comparators {
for (value, (count, comparators)) in value_to_comparators {
if count > 1 {
checker.diagnostics.push(Diagnostic::new(
RepeatedEqualityComparisonTarget {
expression: SourceCodeSnippet::new(merged_membership_test(
left.as_expr(),
value.as_expr(),
bool_op.op,
&comparators,
checker.locator(),
@@ -108,7 +134,7 @@ pub(crate) fn repeated_equality_comparison_target(checker: &mut Checker, bool_op
/// E.g., `==` operators can be joined with `or` and `!=` operators can be
/// joined with `and`.
fn is_allowed_value(bool_op: BoolOp, value: &Expr) -> bool {
let Expr::Compare(ExprCompare {
let Expr::Compare(ast::ExprCompare {
left,
ops,
comparators,
@@ -130,6 +156,14 @@ fn is_allowed_value(bool_op: BoolOp, value: &Expr) -> bool {
return false;
}
// Ignore self-comparisons, e.g., `foo == foo`.
let [right] = comparators.as_slice() else {
return false;
};
if ComparableExpr::from(left) == ComparableExpr::from(right) {
return false;
}
if left.is_call_expr() {
return false;
}

View File

@@ -109,7 +109,9 @@ pub(crate) fn useless_return(
checker.locator(),
checker.indexer(),
);
diagnostic.set_fix(Fix::automatic(edit).isolate(checker.statement_isolation()));
diagnostic.set_fix(Fix::automatic(edit).isolate(Checker::isolation(Some(
checker.semantic().current_statement_id(),
))));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -47,7 +47,97 @@ repeated_equality_comparison_target.py:10:1: PLR1714 Consider merging multiple c
10 | foo == a or foo == "b" or foo == 3 # Mixed types.
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714
11 |
12 | # False negatives (the current implementation doesn't support Yoda conditions).
12 | "a" == foo or "b" == foo or "c" == foo
|
repeated_equality_comparison_target.py:12:1: PLR1714 Consider merging multiple comparisons: `foo in ("a", "b", "c")`. Use a `set` if the elements are hashable.
|
10 | foo == a or foo == "b" or foo == 3 # Mixed types.
11 |
12 | "a" == foo or "b" == foo or "c" == foo
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714
13 |
14 | "a" != foo and "b" != foo and "c" != foo
|
repeated_equality_comparison_target.py:14:1: PLR1714 Consider merging multiple comparisons: `foo not in ("a", "b", "c")`. Use a `set` if the elements are hashable.
|
12 | "a" == foo or "b" == foo or "c" == foo
13 |
14 | "a" != foo and "b" != foo and "c" != foo
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714
15 |
16 | "a" == foo or foo == "b" or "c" == foo
|
repeated_equality_comparison_target.py:16:1: PLR1714 Consider merging multiple comparisons: `foo in ("a", "b", "c")`. Use a `set` if the elements are hashable.
|
14 | "a" != foo and "b" != foo and "c" != foo
15 |
16 | "a" == foo or foo == "b" or "c" == foo
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714
17 |
18 | foo == bar or baz == foo or qux == foo
|
repeated_equality_comparison_target.py:18:1: PLR1714 Consider merging multiple comparisons: `foo in (bar, baz, qux)`. Use a `set` if the elements are hashable.
|
16 | "a" == foo or foo == "b" or "c" == foo
17 |
18 | foo == bar or baz == foo or qux == foo
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714
19 |
20 | foo == "a" or "b" == foo or foo == "c"
|
repeated_equality_comparison_target.py:20:1: PLR1714 Consider merging multiple comparisons: `foo in ("a", "b", "c")`. Use a `set` if the elements are hashable.
|
18 | foo == bar or baz == foo or qux == foo
19 |
20 | foo == "a" or "b" == foo or foo == "c"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714
21 |
22 | foo != "a" and "b" != foo and foo != "c"
|
repeated_equality_comparison_target.py:22:1: PLR1714 Consider merging multiple comparisons: `foo not in ("a", "b", "c")`. Use a `set` if the elements are hashable.
|
20 | foo == "a" or "b" == foo or foo == "c"
21 |
22 | foo != "a" and "b" != foo and foo != "c"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714
23 |
24 | foo == "a" or foo == "b" or "c" == bar or "d" == bar # Multiple targets
|
repeated_equality_comparison_target.py:24:1: PLR1714 Consider merging multiple comparisons: `foo in ("a", "b")`. Use a `set` if the elements are hashable.
|
22 | foo != "a" and "b" != foo and foo != "c"
23 |
24 | foo == "a" or foo == "b" or "c" == bar or "d" == bar # Multiple targets
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714
25 |
26 | foo.bar == "a" or foo.bar == "b" # Attributes.
|
repeated_equality_comparison_target.py:24:1: PLR1714 Consider merging multiple comparisons: `bar in ("c", "d")`. Use a `set` if the elements are hashable.
|
22 | foo != "a" and "b" != foo and foo != "c"
23 |
24 | foo == "a" or foo == "b" or "c" == bar or "d" == bar # Multiple targets
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714
25 |
26 | foo.bar == "a" or foo.bar == "b" # Attributes.
|
repeated_equality_comparison_target.py:26:1: PLR1714 Consider merging multiple comparisons: `foo.bar in ("a", "b")`. Use a `set` if the elements are hashable.
|
24 | foo == "a" or foo == "b" or "c" == bar or "d" == bar # Multiple targets
25 |
26 | foo.bar == "a" or foo.bar == "b" # Attributes.
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714
27 |
28 | # OK
|

View File

@@ -67,6 +67,11 @@ mod tests {
#[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_3.py"))]
#[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_4.py"))]
#[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_5.py"))]
#[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_6.py"))]
#[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_7.py"))]
#[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_8.py"))]
#[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_9.py"))]
#[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_10.py"))]
#[test_case(Rule::UnicodeKindPrefix, Path::new("UP025.py"))]
#[test_case(Rule::UnnecessaryBuiltinImport, Path::new("UP029.py"))]
#[test_case(Rule::UnnecessaryClassParentheses, Path::new("UP039.py"))]

View File

@@ -335,10 +335,10 @@ pub(crate) fn deprecated_mock_import(checker: &mut Checker, stmt: &Stmt) {
);
if checker.patch(diagnostic.kind.rule()) {
if let Some(indent) = indentation(checker.locator(), stmt) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
diagnostic.try_set_fix(|| {
format_import_from(stmt, indent, checker.locator(), checker.stylist())
.map(|content| Edit::range_replacement(content, stmt.range()))
.map(Fix::suggested)
});
}
}

View File

@@ -1,18 +1,18 @@
use std::borrow::Cow;
use anyhow::{Context, Result};
use ruff_python_ast::{self as ast, Arguments, Constant, Expr, Keyword, Ranged};
use ruff_python_literal::format::{
FieldName, FieldNamePart, FieldType, FormatPart, FormatString, FromTemplate,
};
use ruff_python_parser::{lexer, Mode, Tok};
use ruff_text_size::TextRange;
use rustc_hash::FxHashMap;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::str::{leading_quote, trailing_quote};
use ruff_python_ast::{self as ast, Constant, Expr, Keyword, Ranged};
use ruff_python_literal::format::{
FieldName, FieldNamePart, FieldType, FormatPart, FormatString, FromTemplate,
};
use ruff_python_parser::{lexer, Mode, Tok};
use ruff_source_file::Locator;
use ruff_text_size::TextRange;
use crate::checkers::ast::Checker;
use crate::line_width::LineLength;
@@ -67,39 +67,34 @@ struct FormatSummaryValues<'a> {
}
impl<'a> FormatSummaryValues<'a> {
fn try_from_expr(expr: &'a Expr, locator: &'a Locator) -> Option<Self> {
fn try_from_call(call: &'a ast::ExprCall, locator: &'a Locator) -> Option<Self> {
let mut extracted_args: Vec<&Expr> = Vec::new();
let mut extracted_kwargs: FxHashMap<&str, &Expr> = FxHashMap::default();
if let Expr::Call(ast::ExprCall {
arguments: Arguments { args, keywords, .. },
..
}) = expr
{
for arg in args {
if matches!(arg, Expr::Starred(..))
|| contains_quotes(locator.slice(arg.range()))
|| locator.contains_line_break(arg.range())
{
return None;
}
extracted_args.push(arg);
for arg in &call.arguments.args {
if matches!(arg, Expr::Starred(..))
|| contains_quotes(locator.slice(arg.range()))
|| locator.contains_line_break(arg.range())
{
return None;
}
for keyword in keywords {
let Keyword {
arg,
value,
range: _,
} = keyword;
let Some(key) = arg else {
return None;
};
if contains_quotes(locator.slice(value.range()))
|| locator.contains_line_break(value.range())
{
return None;
}
extracted_kwargs.insert(key, value);
extracted_args.push(arg);
}
for keyword in &call.arguments.keywords {
let Keyword {
arg,
value,
range: _,
} = keyword;
let Some(key) = arg else {
return None;
};
if contains_quotes(locator.slice(value.range()))
|| locator.contains_line_break(value.range())
{
return None;
}
extracted_kwargs.insert(key, value);
}
if extracted_args.is_empty() && extracted_kwargs.is_empty() {
@@ -309,8 +304,8 @@ fn try_convert_to_f_string(
/// UP032
pub(crate) fn f_strings(
checker: &mut Checker,
call: &ast::ExprCall,
summary: &FormatSummary,
expr: &Expr,
template: &Expr,
line_length: LineLength,
) {
@@ -318,14 +313,7 @@ pub(crate) fn f_strings(
return;
}
let Expr::Call(ast::ExprCall {
func, arguments, ..
}) = expr
else {
return;
};
let Expr::Attribute(ast::ExprAttribute { value, .. }) = func.as_ref() else {
let Expr::Attribute(ast::ExprAttribute { value, .. }) = call.func.as_ref() else {
return;
};
@@ -339,14 +327,14 @@ pub(crate) fn f_strings(
return;
};
let Some(mut summary) = FormatSummaryValues::try_from_expr(expr, checker.locator()) else {
let Some(mut summary) = FormatSummaryValues::try_from_call(call, checker.locator()) else {
return;
};
let mut patches: Vec<(TextRange, String)> = vec![];
let mut lex = lexer::lex_starts_at(
checker.locator().slice(func.range()),
checker.locator().slice(call.func.range()),
Mode::Expression,
expr.start(),
call.start(),
)
.flatten();
let end = loop {
@@ -384,8 +372,8 @@ pub(crate) fn f_strings(
return;
}
let mut contents = String::with_capacity(checker.locator().slice(expr.range()).len());
let mut prev_end = expr.start();
let mut contents = String::with_capacity(checker.locator().slice(call.range()).len());
let mut prev_end = call.start();
for (range, fstring) in patches {
contents.push_str(
checker
@@ -415,7 +403,7 @@ pub(crate) fn f_strings(
// If necessary, add a space between any leading keyword (`return`, `yield`, `assert`, etc.)
// and the string. For example, `return"foo"` is valid, but `returnf"foo"` is not.
let existing = checker.locator().slice(TextRange::up_to(expr.start()));
let existing = checker.locator().slice(TextRange::up_to(call.start()));
if existing
.chars()
.last()
@@ -424,7 +412,7 @@ pub(crate) fn f_strings(
contents.insert(0, ' ');
}
let mut diagnostic = Diagnostic::new(FString, expr.range());
let mut diagnostic = Diagnostic::new(FString, call.range());
// Avoid autofix if there are comments within the call:
// ```
@@ -436,11 +424,11 @@ pub(crate) fn f_strings(
&& !checker
.indexer()
.comment_ranges()
.intersects(arguments.range())
.intersects(call.arguments.range())
{
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
contents,
expr.range(),
call.range(),
)));
};
checker.diagnostics.push(diagnostic);

View File

@@ -2,16 +2,18 @@ use anyhow::{anyhow, Result};
use libcst_native::{Arg, Expression};
use once_cell::sync::Lazy;
use regex::Regex;
use ruff_python_ast::{self as ast, Expr, Ranged};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Ranged};
use ruff_python_codegen::Stylist;
use ruff_source_file::Locator;
use crate::autofix::codemods::CodegenStylist;
use crate::checkers::ast::Checker;
use crate::cst::matchers::{match_attribute, match_call_mut, match_expression};
use crate::cst::matchers::{
match_attribute, match_call_mut, match_expression, transform_expression_text,
};
use crate::registry::AsRule;
use crate::rules::pyflakes::format::FormatSummary;
@@ -58,8 +60,8 @@ impl Violation for FormatLiterals {
/// UP030
pub(crate) fn format_literals(
checker: &mut Checker,
summary: &FormatSummary,
call: &ast::ExprCall,
summary: &FormatSummary,
) {
// The format we expect is, e.g.: `"{0} {1}".format(...)`
if summary.has_nested_parts {
@@ -112,10 +114,8 @@ pub(crate) fn format_literals(
let mut diagnostic = Diagnostic::new(FormatLiterals, call.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
Ok(Fix::suggested(Edit::range_replacement(
generate_call(call, arguments, checker.locator(), checker.stylist())?,
call.range(),
)))
generate_call(call, arguments, checker.locator(), checker.stylist())
.map(|suggestion| Fix::suggested(Edit::range_replacement(suggestion, call.range())))
});
}
checker.diagnostics.push(diagnostic);
@@ -165,7 +165,7 @@ fn remove_specifiers<'a>(value: &mut Expression<'a>, arena: &'a typed_arena::Are
}
/// Return the corrected argument vector.
fn generate_arguments<'a>(arguments: &[Arg<'a>], order: &'a [usize]) -> Result<Vec<Arg<'a>>> {
fn generate_arguments<'a>(arguments: &[Arg<'a>], order: &[usize]) -> Result<Vec<Arg<'a>>> {
let mut new_arguments: Vec<Arg> = Vec::with_capacity(arguments.len());
for (idx, given) in order.iter().enumerate() {
// We need to keep the formatting in the same order but move the values.
@@ -205,28 +205,27 @@ fn generate_call(
locator: &Locator,
stylist: &Stylist,
) -> Result<String> {
let content = locator.slice(call.range());
let parenthesized_content = format!("({content})");
let mut expression = match_expression(&parenthesized_content)?;
let source_code = locator.slice(call.range());
// Fix the call arguments.
let call = match_call_mut(&mut expression)?;
if let Arguments::Reorder(order) = arguments {
call.args = generate_arguments(&call.args, order)?;
}
let output = transform_expression_text(source_code, |source_code| {
let mut expression = match_expression(&source_code)?;
// Fix the string itself.
let item = match_attribute(&mut call.func)?;
let arena = typed_arena::Arena::new();
remove_specifiers(&mut item.value, &arena);
// Fix the call arguments.
let call = match_call_mut(&mut expression)?;
if let Arguments::Reorder(order) = arguments {
call.args = generate_arguments(&call.args, order)?;
}
// Remove the parentheses (first and last characters).
let mut output = expression.codegen_stylist(stylist);
output.remove(0);
output.pop();
// Fix the string itself.
let item = match_attribute(&mut call.func)?;
let arena = typed_arena::Arena::new();
remove_specifiers(&mut item.value, &arena);
Ok(expression.codegen_stylist(stylist))
})?;
// Ex) `'{' '0}'.format(1)`
if output == content {
if output == source_code {
return Err(anyhow!("Unable to identify format literals"));
}

View File

@@ -78,10 +78,7 @@ pub(crate) fn lru_cache_without_parameters(checker: &mut Checker, decorator_list
TextRange::new(func.end(), decorator.end()),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::automatic(Edit::deletion(
arguments.start(),
arguments.end(),
)));
diagnostic.set_fix(Fix::automatic(Edit::range_deletion(arguments.range())));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -5,7 +5,7 @@ use num_bigint::BigInt;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Constant, Expr, Keyword, Ranged};
use ruff_python_ast::{self as ast, Constant, Expr, Ranged};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -129,12 +129,21 @@ impl AlwaysAutofixableViolation for NativeLiterals {
/// UP018
pub(crate) fn native_literals(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
call: &ast::ExprCall,
parent_expr: Option<&ast::Expr>,
) {
let Expr::Name(ast::ExprName { id, .. }) = func else {
let ast::ExprCall {
func,
arguments:
ast::Arguments {
args,
keywords,
range: _,
},
range: _,
} = call;
let Expr::Name(ast::ExprName { ref id, .. }) = func.as_ref() else {
return;
};
@@ -165,13 +174,20 @@ pub(crate) fn native_literals(
match args.get(0) {
None => {
let mut diagnostic = Diagnostic::new(NativeLiterals { literal_type }, expr.range());
let mut diagnostic = Diagnostic::new(NativeLiterals { literal_type }, call.range());
// Do not suggest fix for attribute access on an int like `int().attribute`
// Ex) `int().denominator` is valid but `0.denominator` is not
if literal_type == LiteralType::Int && matches!(parent_expr, Some(Expr::Attribute(_))) {
return;
}
if checker.patch(diagnostic.kind.rule()) {
let constant = Constant::from(literal_type);
let content = checker.generator().constant(&constant);
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
content,
expr.range(),
call.range(),
)));
}
checker.diagnostics.push(diagnostic);
@@ -196,11 +212,20 @@ pub(crate) fn native_literals(
let arg_code = checker.locator().slice(arg.range());
let mut diagnostic = Diagnostic::new(NativeLiterals { literal_type }, expr.range());
// Attribute access on an integer requires the integer to be parenthesized to disambiguate from a float
// Ex) `(7).denominator` is valid but `7.denominator` is not
// Note that floats do not have this problem
// Ex) `(1.0).real` is valid and `1.0.real` is too
let content = match (parent_expr, value) {
(Some(Expr::Attribute(_)), Constant::Int(_)) => format!("({arg_code})"),
_ => arg_code.to_string(),
};
let mut diagnostic = Diagnostic::new(NativeLiterals { literal_type }, call.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
arg_code.to_string(),
expr.range(),
content,
call.range(),
)));
}
checker.diagnostics.push(diagnostic);

View File

@@ -135,7 +135,9 @@ pub(crate) fn unnecessary_builtin_import(
checker.stylist(),
checker.indexer(),
)?;
Ok(Fix::suggested(edit).isolate(checker.parent_isolation()))
Ok(Fix::suggested(edit).isolate(Checker::isolation(
checker.semantic().current_statement_parent_id(),
)))
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -5,6 +5,7 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_index::Indexer;
use ruff_source_file::Locator;
use ruff_text_size::TextRange;
use crate::registry::AsRule;
use crate::settings::Settings;
@@ -55,20 +56,45 @@ pub(crate) fn unnecessary_coding_comment(
) {
// The coding comment must be on one of the first two lines. Since each comment spans at least
// one line, we only need to check the first two comments at most.
for range in indexer.comment_ranges().iter().take(2) {
let line = locator.slice(*range);
if CODING_COMMENT_REGEX.is_match(line) {
for comment_range in indexer.comment_ranges().iter().take(2) {
// If leading content is not whitespace then it's not a valid coding comment e.g.
// ```
// print(x) # coding=utf8
// ```
let line_range = locator.full_line_range(comment_range.start());
if !locator
.slice(TextRange::new(line_range.start(), comment_range.start()))
.trim()
.is_empty()
{
continue;
}
// If the line is after a continuation then it's not a valid coding comment e.g.
// ```
// x = 1 \
// # coding=utf8
// x = 2
// ```
if indexer
.preceded_by_continuations(line_range.start(), locator)
.is_some()
{
continue;
}
if CODING_COMMENT_REGEX.is_match(locator.slice(line_range)) {
#[allow(deprecated)]
let line = locator.compute_line_index(range.start());
if line.to_zero_indexed() > 1 {
let index = locator.compute_line_index(line_range.start());
if index.to_zero_indexed() > 1 {
continue;
}
let mut diagnostic = Diagnostic::new(UTF8EncodingDeclaration, *range);
let mut diagnostic = Diagnostic::new(UTF8EncodingDeclaration, *comment_range);
if settings.rules.should_fix(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::automatic(Edit::deletion(
range.start(),
locator.full_line_end(range.end()),
line_range.start(),
line_range.end(),
)));
}
diagnostics.push(diagnostic);

View File

@@ -124,7 +124,9 @@ pub(crate) fn unnecessary_future_import(checker: &mut Checker, stmt: &Stmt, name
checker.stylist(),
checker.indexer(),
)?;
Ok(Fix::suggested(edit).isolate(checker.parent_isolation()))
Ok(Fix::suggested(edit).isolate(Checker::isolation(
checker.semantic().current_statement_parent_id(),
)))
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -20,8 +20,16 @@ use crate::registry::AsRule;
///
/// When available, the [PEP 585] syntax should be used instead of importing
/// members from the `typing` module, as it's more concise and readable.
/// Importing those members from `typing` is considered deprecated as of PEP
/// 585.
/// Importing those members from `typing` is considered deprecated as of [PEP
/// 585].
///
/// This rule is enabled when targeting Python 3.9 or later (see:
/// [`target-version`]). By default, it's _also_ enabled for earlier Python
/// versions if `from __future__ import annotations` is present, as
/// `__future__` annotations are not evaluated at runtime. If your code relies
/// on runtime type annotations (either directly or via a library like
/// Pydantic), you can disable this behavior for Python versions prior to 3.9
/// by setting [`pyupgrade.keep-runtime-typing`] to `true`.
///
/// ## Example
/// ```python

View File

@@ -18,6 +18,14 @@ use crate::registry::AsRule;
/// `|` operator. This syntax is more concise and readable than the previous
/// `typing.Union` and `typing.Optional` syntaxes.
///
/// This rule is enabled when targeting Python 3.10 or later (see:
/// [`target-version`]). By default, it's _also_ enabled for earlier Python
/// versions if `from __future__ import annotations` is present, as
/// `__future__` annotations are not evaluated at runtime. If your code relies
/// on runtime type annotations (either directly or via a library like
/// Pydantic), you can disable this behavior for Python versions prior to 3.10
/// by setting [`pyupgrade.keep-runtime-typing`] to `true`.
///
/// ## Example
/// ```python
/// from typing import Union

View File

@@ -66,7 +66,9 @@ pub(crate) fn useless_metaclass_type(
let stmt = checker.semantic().current_statement();
let parent = checker.semantic().current_statement_parent();
let edit = autofix::edits::delete_stmt(stmt, parent, checker.locator(), checker.indexer());
diagnostic.set_fix(Fix::automatic(edit).isolate(checker.parent_isolation()));
diagnostic.set_fix(Fix::automatic(edit).isolate(Checker::isolation(
checker.semantic().current_statement_parent_id(),
)));
}
checker.diagnostics.push(diagnostic);
}

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