Compare commits

...

40 Commits

Author SHA1 Message Date
konstin
5be2ec5ced Review 2023-09-26 14:21:03 +02:00
konsti
59c37d0cd8 Update crates/ruff_source_file/src/locator.rs
Co-authored-by: Micha Reiser <micha@reiser.io>
2023-09-26 14:05:04 +02:00
konstin
f39e8af8ae Add LSP utf-16 coordinates to byte offset conversion 2023-09-26 10:47:38 +02:00
konstin
dd8b1244fd Formatter and parser refactoring
I got confused and refactored a bit
2023-09-26 09:53:33 +02:00
Charlie Marsh
93b5d8a0fb Implement our own small-integer optimization (#7584)
## Summary

This is a follow-up to #7469 that attempts to achieve similar gains, but
without introducing malachite. Instead, this PR removes the `BigInt`
type altogether, instead opting for a simple enum that allows us to
store small integers directly and only allocate for values greater than
`i64`:

```rust
/// A Python integer literal. Represents both small (fits in an `i64`) and large integers.
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct Int(Number);

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Number {
    /// A "small" number that can be represented as an `i64`.
    Small(i64),
    /// A "large" number that cannot be represented as an `i64`.
    Big(Box<str>),
}

impl std::fmt::Display for Number {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Number::Small(value) => write!(f, "{value}"),
            Number::Big(value) => write!(f, "{value}"),
        }
    }
}
```

We typically don't care about numbers greater than `isize` -- our only
uses are comparisons against small constants (like `1`, `2`, `3`, etc.),
so there's no real loss of information, except in one or two rules where
we're now a little more conservative (with the worst-case being that we
don't flag, e.g., an `itertools.pairwise` that uses an extremely large
value for the slice start constant). For simplicity, a few diagnostics
now show a dedicated message when they see integers that are out of the
supported range (e.g., `outdated-version-block`).

An additional benefit here is that we get to remove a few dependencies,
especially `num-bigint`.

## Test Plan

`cargo test`
2023-09-25 15:13:21 +00:00
Charlie Marsh
65aebf127a Treat form feed as whitespace in SimpleTokenizer (#7626)
## Summary

This is whitespace as per `is_python_whitespace`, and right now it tends
to lead to panics in the formatter. Seems reasonable to treat it as
whitespace in the `SimpleTokenizer` too.

Closes .https://github.com/astral-sh/ruff/issues/7624.
2023-09-25 14:34:59 +00:00
Charlie Marsh
17ceb5dcb3 Preserve newlines after nested compound statements (#7608)
## Summary

Given:
```python
if True:
    if True:
        pass
    else:
        pass
        # a

        # b
        # c

else:
    pass
```

We want to preserve the newline after the `# c` (before the `else`).
However, the `last_node` ends at the `pass`, and the comments are
trailing comments on the `pass`, not trailing comments on the
`last_node` (the `if`). As such, when counting the trailing newlines on
the outer `if`, we abort as soon as we see the comment (`# a`).

This PR changes the logic to skip _all_ comments (even those with
newlines between them). This is safe as we know that there are no
"leading" comments on the `else`, so there's no risk of skipping those
accidentally.

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

## Test Plan

No change in compatibility.

Before:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1631 |
| django | 0.99983 | 2760 | 36 |
| transformers | 0.99963 | 2587 | 319 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99979 | 3496 | 22 |
| warehouse | 0.99967 | 648 | 15 |
| zulip | 0.99972 | 1437 | 21 |

After:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1631 |
| django | 0.99983 | 2760 | 36 |
| transformers | 0.99963 | 2587 | 319 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99967 | 648 | 15 |
| zulip | 0.99972 | 1437 | 21 |
2023-09-25 14:21:44 +00:00
Micha Reiser
8ce138760a Emit LexError for dedent to incorrect level (#7638) 2023-09-25 11:45:44 +01:00
dependabot[bot]
10e35e38d7 Bump semver from 1.0.18 to 1.0.19 (#7641)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-25 09:09:17 +00:00
dependabot[bot]
f169cb5d92 Bump wild from 2.1.0 to 2.2.0 (#7640)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-25 09:06:10 +00:00
Charlie Marsh
39ddad7454 Refactor FURB105 into explicit cases (#7634)
## Summary

I was having trouble keeping track of the various cases here, so opted
to refactor into a more explicit `match`.
2023-09-24 18:46:09 +00:00
Charlie Marsh
f32b0eef9c Flag FURB105 with starred kwargs (#7630) 2023-09-24 14:28:20 +00:00
Dhruv Manilawala
15813a65f3 Update return type for PT022 autofix (#7613)
## Summary

This PR fixes the autofix behavior for `PT022` to create an additional
edit for the return type if it's present. The edit will update the
return type from `Generator[T, ...]` to `T`. As per the [official
documentation](https://docs.python.org/3/library/typing.html?highlight=typing%20generator#typing.Generator),
the first position is the yield type, so we can ignore other positions.

```python
typing.Generator[YieldType, SendType, ReturnType]
```

## Test Plan

Add new test cases, `cargo test` and review the snapshots.

fixes: #7610
2023-09-24 06:39:47 +00:00
Tom Kuson
604cf521b5 [refurb] Implement print-empty-string (FURB105) (#7617)
## Summary

Implement
[`simplify-print`](https://github.com/dosisod/refurb/blob/master/refurb/checks/builtin/print.py)
as `print-empty-string` (`FURB105`).

Extends the original rule in that it also checks for multiple empty
string positional arguments with an empty string separator.

Related to #1348.

## Test Plan

`cargo test`
2023-09-24 04:10:36 +00:00
Charlie Marsh
865c89800e Avoid searching for bracketed comments in unparenthesized generators (#7627)
Similar to tuples, a generator _can_ be parenthesized or
unparenthesized. Only search for bracketed comments if it contains its
own parentheses.

Closes https://github.com/astral-sh/ruff/issues/7623.
2023-09-24 02:08:44 +00:00
Charlie Marsh
1a22eae98c Use deletion for D215 full-line removals (#7625)
Closes https://github.com/astral-sh/ruff/issues/7619.
2023-09-23 22:44:55 +00:00
Charlie Marsh
8ba8896a7f Skip BOM when inserting start-of-file imports (#7622)
See:
https://github.com/astral-sh/ruff/issues/7455#issuecomment-1732387485.
2023-09-23 19:36:50 +00:00
Charlie Marsh
b194f59aab Avoid flagging B009 and B010 on starred expressions (#7621)
See:
https://github.com/astral-sh/ruff/issues/7455#issuecomment-1732387247.
2023-09-23 19:08:19 +00:00
Chammika Mannakkara
e41b08f1d0 Fix typo in infinite (#7614) 2023-09-23 11:19:36 +00:00
Charlie Marsh
1a4f2a9baf Avoid reordering mixed-indent-level comments after branches (#7609)
## Summary

Given:

```python
if True:
    if True:
        if True:
            pass

        #a
            #b
        #c
else:
    pass
```

When determining the placement of the various comments, we compute the
indentation depth of each comment, and then compare it to the depth of
the previous statement. It turns out this can lead to reordering
comments, e.g., above, `#b` is assigned as a trailing comment of `pass`,
and so gets reordered above `#a`.

This PR modifies the logic such that when we compute the indentation
depth of `#b`, we limit it to at most the indentation depth of `#a`. In
other words, when analyzing comments at the end of branches, we don't
let successive comments go any _deeper_ than their preceding comments.

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

## Test Plan

`cargo test`

No change in similarity.

Before:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1631 |
| django | 0.99983 | 2760 | 36 |
| transformers | 0.99963 | 2587 | 319 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99979 | 3496 | 22 |
| warehouse | 0.99967 | 648 | 15 |
| zulip | 0.99972 | 1437 | 21 |

After:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1631 |
| django | 0.99983 | 2760 | 36 |
| transformers | 0.99963 | 2587 | 319 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99979 | 3496 | 22 |
| warehouse | 0.99967 | 648 | 15 |
| zulip | 0.99972 | 1437 | 21 |
2023-09-22 18:12:31 -04:00
konsti
19010f276e Fix line ending doc typo (#7611)
Fixes https://docs.astral.sh/ruff/settings/#format-quote-style
2023-09-22 20:16:41 +00:00
Charlie Marsh
5174e8c926 Ignore blank lines between comments when counting newlines-after-imports (#7607)
## Summary

Given:

```python
# -*- coding: utf-8 -*-
import random

# Defaults for arguments are defined here
# args.threshold = None;


logger = logging.getLogger("FastProject")
```

We want to count the number of newlines after `import random`, to ensure
that there's _at least one_, but up to two.

Previously, we used the end range of the statement (then skipped
trivia); instead, we need to use the end of the _last comment_. This is
similar to #7556.

Closes https://github.com/astral-sh/ruff/issues/7604.
2023-09-22 17:49:39 +00:00
Charlie Marsh
8bfe9bda41 Bump version to v0.0.291 (#7606) 2023-09-22 13:25:37 -04:00
Micha Reiser
01843af21a Support option group documentation (#7593) 2023-09-22 16:31:52 +00:00
Micha Reiser
2ecf59726f Refactor Options representation (#7591) 2023-09-22 18:19:58 +02:00
Charlie Marsh
f137819536 Improve B005 documentation to reflect duplicate-character behavior (#7601)
## Summary

B005 only flags `.strip()` calls for which the argument includes
duplicate characters. This is consistent with bugbear, but isn't
explained in the documentation.
2023-09-22 16:12:16 +00:00
Micha Reiser
9d16e46129 Add most formatter options to ruff.toml / pyproject.toml (#7566) 2023-09-22 15:47:57 +00:00
dependabot[bot]
82978ac9b5 Bump indicatif from 0.17.6 to 0.17.7 (#7592)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-22 08:54:48 +00:00
T-256
814403cdf7 Bump lint rules count to 700 (#7585) 2023-09-21 21:55:03 -04:00
Charlie Marsh
f254aaa847 Remove unwrap in os_error_alias.rs (#7583) 2023-09-21 21:19:32 +00:00
Charlie Marsh
a51b0b02f0 Treat os.error as an OSError alias (#7582)
Closes https://github.com/astral-sh/ruff/issues/7580.
2023-09-21 21:18:14 +00:00
Leiser Fernández Gallo
74dbd871f8 Make ruff format idempotent when using stdin input (#7581)
## Summary
Currently, this happens
```sh
$ echo "print()" | ruff format - 

#Notice that nothing went to stdout
```
Which does not match `ruff check --fix - ` behavior and deletes my code
every time I format it (more or less 5 times per minute 😄).

I just checked that my example works as the change was very
straightforward.
2023-09-21 16:50:23 -04:00
Charlie Marsh
d7508af48d Truncate to one empty line in stub files (#7558)
## Summary

This PR modifies a variety of sites in which we insert up to two empty
lines to instead truncate to at most one empty line in stub files. We
already enforce this in _some_ places, but not all.

## Test Plan

`cargo test`

No changes in similarity (as expected, since this only impacts
unformatted `.pyi` files).

Before:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1631 |
| django | 0.99983 | 2760 | 36 |
| transformers | 0.99963 | 2587 | 323 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99979 | 3496 | 22 |
| warehouse | 0.99967 | 648 | 15 |
| zulip | 0.99972 | 1437 | 21 |

After:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1631 |
| django | 0.99983 | 2760 | 36 |
| transformers | 0.99963 | 2587 | 323 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99979 | 3496 | 22 |
| warehouse | 0.99967 | 648 | 15 |
| zulip | 0.99972 | 1437 | 21 |
2023-09-21 16:24:42 -04:00
konsti
c3774e1255 Fix gitignore to not ignore files that are required (#7538)
It is apparently possible to add files to the git index, even if they
are part of the gitignore (see e.g.
https://stackoverflow.com/questions/45400361/why-is-gitignore-not-ignoring-my-files,
even though it's strange that the gitignore entries existed before the
files were added, i wouldn't know how to get them added in that case). I
ran
```
git rm -r --cached .
```
then change the gitignore not actually ignore those files with the
exception of
`crates/ruff_cli/resources/test/fixtures/cache_mutable/source.py`, which
is actually a generated file.
2023-09-21 21:33:09 +02:00
Charlie Marsh
887455c498 Use u8 to represent version segments (#7578) 2023-09-21 14:24:51 -04:00
Charlie Marsh
4d6f5ff0a7 Remove Int wrapper type from parser (#7577)
## Summary

This is only used for the `level` field in relative imports (e.g., `from
..foo import bar`). It seems unnecessary to use a wrapper here, so this
PR changes to a `u32` directly.
2023-09-21 17:01:44 +00:00
Micha Reiser
6c3378edb1 Fix the default indent style to tab (#7576) 2023-09-21 15:42:55 +00:00
Charlie Marsh
7f1456a2c9 Allow up to two newlines before trailing clause body comments (#7575)
## Summary

This is the peer to https://github.com/astral-sh/ruff/pull/7557, but for
"leading" clause comments, like:

```python
if True:
    pass


# comment
else:
    pass
```

In this case, we again want to allow up to two newlines at the top
level.

## Test Plan

`cargo test`

No changes.

Before:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1631 |
| django | 0.99983 | 2760 | 36 |
| transformers | 0.99963 | 2587 | 323 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99979 | 3496 | 22 |
| warehouse | 0.99967 | 648 | 15 |
| zulip | 0.99972 | 1437 | 21 |

After:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1631 |
| django | 0.99983 | 2760 | 36 |
| transformers | 0.99963 | 2587 | 323 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99979 | 3496 | 22 |
| warehouse | 0.99967 | 648 | 15 |
| zulip | 0.99972 | 1437 | 21 |
2023-09-21 14:52:38 +00:00
Charlie Marsh
2759db6604 Allow up to two newlines after trailing clause body comments (#7557)
## Summary

The number of newlines after a trailing comment in a clause body needs
to follow the usual rules -- so, up to two for top-level, up to one for
nested, etc.

For example, Black preserves both newlines after `# comment` here:

```python
if True:
    pass

    # comment


else:
    pass
```

But it truncates to one newline here:

```python
if True:
    if True:
        pass
        # comment


    else:
        pass
else:
    pass
```

## Test Plan

Significant improvement on `transformers`.

Before:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1631 |
| django | 0.99983 | 2760 | 36 |
| transformers | 0.99957 | 2587 | 402 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99979 | 3496 | 22 |
| warehouse | 0.99967 | 648 | 15 |
| zulip | 0.99972 | 1437 | 21 |


After:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1631 |
| django | 0.99983 | 2760 | 36 |
| **transformers** | **0.99963** | **2587** | **323** |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99979 | 3496 | 22 |
| warehouse | 0.99967 | 648 | 15 |
| zulip | 0.99972 | 1437 | 21 |
2023-09-21 14:04:49 +00:00
Charlie Marsh
124d95d246 Fix instability in trailing clause body comments (#7556)
## Summary

When we format the trailing comments on a clause body, we check if there
are any newlines after the last statement; if not, we insert one.

This logic didn't take into account that the last statement could itself
have trailing comments, as in:

```python
if True:
    pass

    # comment
else:
    pass
```

We were thus inserting a newline after the comment, like:

```python
if True:
    pass

    # comment

else:
    pass
```

In the context of function definitions, this led to an instability,
since we insert a newline _after_ a function, which would in turn lead
to the bug above appearing in the second formatting pass.

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

## Test Plan

`cargo test`

Small improvement in `transformers`, but no regressions.

Before:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1631 |
| django | 0.99983 | 2760 | 36 |
| transformers | 0.99956 | 2587 | 404 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99967 | 648 | 15 |
| zulip | 0.99972 | 1437 | 21 |

After:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1631 |
| django | 0.99983 | 2760 | 36 |
| **transformers** | **0.99957** | **2587** | **402** |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99967 | 648 | 15 |
| zulip | 0.99972 | 1437 | 21 |
2023-09-21 13:32:16 +00:00
153 changed files with 4608 additions and 1135 deletions

6
.gitignore vendored
View File

@@ -208,3 +208,9 @@ cython_debug/
# VIM
.*.sw?
.sw?
# Custom re-inclusions for the resolver test cases
!crates/ruff_python_resolver/resources/test/airflow/venv/
!crates/ruff_python_resolver/resources/test/airflow/venv/lib
!crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/_watchdog_fsevents.cpython-311-darwin.so
!crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so

50
Cargo.lock generated
View File

@@ -810,7 +810,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.290"
version = "0.0.291"
dependencies = [
"anyhow",
"clap",
@@ -1035,9 +1035,9 @@ dependencies = [
[[package]]
name = "indicatif"
version = "0.17.6"
version = "0.17.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b297dc40733f23a0e52728a58fa9489a5b7638a324932de16b41adc3ef80730"
checksum = "fb28741c9db9a713d93deb3bb9515c20788cef5815265bee4980e87bde7e0f25"
dependencies = [
"console",
"instant",
@@ -1454,27 +1454,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-bigint"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.16"
@@ -2051,7 +2030,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.290"
version = "0.0.291"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2187,7 +2166,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.0.290"
version = "0.0.291"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2206,8 +2185,6 @@ dependencies = [
"log",
"memchr",
"natord",
"num-bigint",
"num-traits",
"once_cell",
"path-absolutize",
"pathdiff",
@@ -2290,8 +2267,6 @@ dependencies = [
"is-macro",
"itertools 0.11.0",
"memchr",
"num-bigint",
"num-traits",
"once_cell",
"ruff_python_parser",
"ruff_python_trivia",
@@ -2336,6 +2311,7 @@ dependencies = [
"ruff_source_file",
"ruff_text_size",
"rustc-hash",
"schemars",
"serde",
"serde_json",
"similar",
@@ -2367,7 +2343,6 @@ dependencies = [
"is-macro",
"itertools 0.11.0",
"lexical-parse-float",
"num-traits",
"rand",
"unic-ucd-category",
]
@@ -2382,8 +2357,6 @@ dependencies = [
"itertools 0.11.0",
"lalrpop",
"lalrpop-util",
"num-bigint",
"num-traits",
"ruff_python_ast",
"ruff_text_size",
"rustc-hash",
@@ -2409,7 +2382,6 @@ version = "0.0.0"
dependencies = [
"bitflags 2.4.0",
"is-macro",
"num-traits",
"ruff_index",
"ruff_python_ast",
"ruff_python_parser",
@@ -2523,7 +2495,9 @@ dependencies = [
"ruff_formatter",
"ruff_linter",
"ruff_macros",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_source_file",
"rustc-hash",
"schemars",
"serde",
@@ -2669,9 +2643,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "semver"
version = "1.0.18"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0"
[[package]]
name = "serde"
@@ -3516,9 +3490,9 @@ dependencies = [
[[package]]
name = "wild"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b116685a6be0c52f5a103334cbff26db643826c7b3735fc0a3ba9871310a74"
checksum = "10d01931a94d5a115a53f95292f51d316856b68a035618eb831bbba593a30b67"
dependencies = [
"glob",
]

View File

@@ -26,8 +26,6 @@ is-macro = { version = "0.3.0" }
itertools = { version = "0.11.0" }
log = { version = "0.4.17" }
memchr = "2.6.3"
num-bigint = { version = "0.4.3" }
num-traits = { version = "0.2.15" }
once_cell = { version = "1.17.1" }
path-absolutize = { version = "3.1.1" }
proc-macro2 = { version = "1.0.67" }

View File

@@ -30,7 +30,7 @@ An extremely fast Python linter, written in Rust.
- 🤝 Python 3.11 compatibility
- 📦 Built-in caching, to avoid re-analyzing unchanged files
- 🔧 Autofix support, for automatic error correction (e.g., automatically remove unused imports)
- 📏 Over [600 built-in rules](https://docs.astral.sh/ruff/rules/)
- 📏 Over [700 built-in rules](https://docs.astral.sh/ruff/rules/)
- ⚖️ [Near-parity](https://docs.astral.sh/ruff/faq/#how-does-ruff-compare-to-flake8) with the
built-in Flake8 rule set
- 🔌 Native re-implementations of dozens of Flake8 plugins, like flake8-bugbear
@@ -140,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.290
rev: v0.0.291
hooks:
- id: ruff
```
@@ -233,7 +233,7 @@ linting command.
<!-- Begin section: Rules -->
**Ruff supports over 600 lint rules**, many of which are inspired by popular tools like Flake8,
**Ruff supports over 700 lint rules**, many of which are inspired by popular tools like Flake8,
isort, pyupgrade, and others. Regardless of the rule's origin, Ruff re-implements every rule in
Rust as a first-party feature.

View File

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

View File

@@ -4,7 +4,7 @@ use ruff_benchmark::criterion::{
criterion_group, criterion_main, BenchmarkId, Criterion, Throughput,
};
use ruff_benchmark::{TestCase, TestFile, TestFileDownloadError};
use ruff_python_formatter::{format_node, PyFormatOptions};
use ruff_python_formatter::{format_module_ast, PyFormatOptions};
use ruff_python_index::CommentRangesBuilder;
use ruff_python_parser::lexer::lex;
use ruff_python_parser::{parse_tokens, Mode};
@@ -65,13 +65,14 @@ fn benchmark_formatter(criterion: &mut Criterion) {
let comment_ranges = comment_ranges.finish();
// Parse the AST.
let python_ast = parse_tokens(tokens, Mode::Module, "<filename>")
let module = parse_tokens(tokens, Mode::Module, "<filename>")
.expect("Input to be a valid python program");
b.iter(|| {
let options = PyFormatOptions::from_extension(Path::new(case.name()));
let formatted = format_node(&python_ast, &comment_ranges, case.code(), options)
.expect("Formatting to succeed");
let formatted =
format_module_ast(&module, &comment_ranges, case.code(), options)
.expect("Formatting to succeed");
formatted.print().expect("Printing to succeed")
});

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.0.290"
version = "0.0.291"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -1,3 +0,0 @@
a = 1
__all__ = list(["a", "b"])

View File

@@ -1,12 +1,13 @@
use anyhow::{anyhow, Result};
use ruff_workspace::options::Options;
use ruff_workspace::options_base::OptionsMetadata;
#[allow(clippy::print_stdout)]
pub(crate) fn config(key: Option<&str>) -> Result<()> {
match key {
None => print!("{}", Options::metadata()),
Some(key) => match Options::metadata().get(key) {
Some(key) => match Options::metadata().find(key) {
None => {
return Err(anyhow!("Unknown option: {key}"));
}

View File

@@ -15,9 +15,9 @@ use ruff_linter::fs;
use ruff_linter::logging::LogLevel;
use ruff_linter::warn_user_once;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_python_formatter::{format_module, FormatModuleError, PyFormatOptions};
use ruff_source_file::{find_newline, LineEnding};
use ruff_python_formatter::{format_module_source, FormatModuleError};
use ruff_workspace::resolver::python_files_in_path;
use ruff_workspace::FormatterSettings;
use crate::args::{CliOverrides, FormatArguments};
use crate::panic::{catch_unwind, PanicError};
@@ -73,15 +73,17 @@ pub(crate) fn format(
};
let resolved_settings = resolver.resolve(path, &pyproject_config);
let options = resolved_settings.formatter.to_format_options(source_type);
debug!("Formatting {} with {:?}", path.display(), options);
Some(match catch_unwind(|| format_path(path, options, mode)) {
Ok(inner) => inner,
Err(error) => {
Err(FormatCommandError::Panic(Some(path.to_path_buf()), error))
}
})
Some(
match catch_unwind(|| {
format_path(path, &resolved_settings.formatter, source_type, mode)
}) {
Ok(inner) => inner,
Err(error) => {
Err(FormatCommandError::Panic(Some(path.to_path_buf()), error))
}
},
)
}
Err(err) => Some(Err(FormatCommandError::Ignore(err))),
}
@@ -139,21 +141,17 @@ pub(crate) fn format(
#[tracing::instrument(skip_all, fields(path = %path.display()))]
fn format_path(
path: &Path,
options: PyFormatOptions,
settings: &FormatterSettings,
source_type: PySourceType,
mode: FormatMode,
) -> Result<FormatCommandResult, FormatCommandError> {
let unformatted = std::fs::read_to_string(path)
.map_err(|err| FormatCommandError::Read(Some(path.to_path_buf()), err))?;
let line_ending = match find_newline(&unformatted) {
Some((_, LineEnding::Lf)) | None => ruff_formatter::printer::LineEnding::LineFeed,
Some((_, LineEnding::Cr)) => ruff_formatter::printer::LineEnding::CarriageReturn,
Some((_, LineEnding::CrLf)) => ruff_formatter::printer::LineEnding::CarriageReturnLineFeed,
};
let options = settings.to_format_options(source_type, &unformatted);
debug!("Formatting {} with {:?}", path.display(), options);
let options = options.with_line_ending(line_ending);
let formatted = format_module(&unformatted, options)
let formatted = format_module_source(&unformatted, options)
.map_err(|err| FormatCommandError::FormatModule(Some(path.to_path_buf()), err))?;
let formatted = formatted.as_code();

View File

@@ -5,8 +5,9 @@ use anyhow::Result;
use log::warn;
use ruff_python_ast::PySourceType;
use ruff_python_formatter::{format_module, PyFormatOptions};
use ruff_python_formatter::format_module_source;
use ruff_workspace::resolver::python_file_at_path;
use ruff_workspace::FormatterSettings;
use crate::args::{CliOverrides, FormatArguments};
use crate::commands::format::{FormatCommandError, FormatCommandResult, FormatMode};
@@ -37,12 +38,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
// Format the file.
let path = cli.stdin_filename.as_deref();
let options = pyproject_config
.settings
.formatter
.to_format_options(path.map(PySourceType::from).unwrap_or_default());
match format_source(path, options, mode) {
match format_source(path, &pyproject_config.settings.formatter, mode) {
Ok(result) => match mode {
FormatMode::Write => Ok(ExitStatus::Success),
FormatMode::Check => {
@@ -63,23 +59,30 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
/// Format source code read from `stdin`.
fn format_source(
path: Option<&Path>,
options: PyFormatOptions,
settings: &FormatterSettings,
mode: FormatMode,
) -> Result<FormatCommandResult, FormatCommandError> {
let unformatted = read_from_stdin()
.map_err(|err| FormatCommandError::Read(path.map(Path::to_path_buf), err))?;
let formatted = format_module(&unformatted, options)
let options = settings.to_format_options(
path.map(PySourceType::from).unwrap_or_default(),
&unformatted,
);
let formatted = format_module_source(&unformatted, options)
.map_err(|err| FormatCommandError::FormatModule(path.map(Path::to_path_buf), err))?;
let formatted = formatted.as_code();
if mode.is_write() {
stdout()
.lock()
.write_all(formatted.as_bytes())
.map_err(|err| FormatCommandError::Write(path.map(Path::to_path_buf), err))?;
}
if formatted.len() == unformatted.len() && formatted == unformatted {
Ok(FormatCommandResult::Unchanged)
} else {
if mode.is_write() {
stdout()
.lock()
.write_all(formatted.as_bytes())
.map_err(|err| FormatCommandError::Write(path.map(Path::to_path_buf), err))?;
}
Ok(FormatCommandResult::Formatted)
}
}

View File

@@ -0,0 +1,207 @@
#![cfg(not(target_family = "wasm"))]
use std::fs;
use std::process::Command;
use std::str;
use anyhow::Result;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use tempfile::TempDir;
const BIN_NAME: &str = "ruff";
#[test]
fn default_options() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--isolated"])
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print('Should\'t change quotes')
if condition:
print('Hy "Micha"') # Should not change quotes
"#), @r###"
success: true
exit_code: 0
----- stdout -----
def foo(
arg1,
arg2,
):
print("Should't change quotes")
if condition:
print('Hy "Micha"') # Should not change quotes
----- stderr -----
warning: `ruff format` is a work-in-progress, subject to change at any time, and intended only for experimentation.
"###);
}
#[test]
fn format_options() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[format]
indent-style = "tab"
quote-style = "single"
skip-magic-trailing-comma = true
line-ending = "cr-lf"
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--config"])
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print("Shouldn't change quotes")
if condition:
print("Should change quotes")
"#), @r###"
success: true
exit_code: 0
----- stdout -----
def foo(arg1, arg2):
print("Shouldn't change quotes")
if condition:
print('Should change quotes')
----- stderr -----
warning: `ruff format` is a work-in-progress, subject to change at any time, and intended only for experimentation.
"###);
Ok(())
}
#[test]
fn format_option_inheritance() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
let base_toml = tempdir.path().join("base.toml");
fs::write(
&ruff_toml,
r#"
extend = "base.toml"
[format]
quote-style = "single"
"#,
)?;
fs::write(
base_toml,
r#"
[format]
indent-style = "tab"
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--config"])
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print("Shouldn't change quotes")
if condition:
print("Should change quotes")
"#), @r###"
success: true
exit_code: 0
----- stdout -----
def foo(
arg1,
arg2,
):
print("Shouldn't change quotes")
if condition:
print('Should change quotes')
----- stderr -----
warning: `ruff format` is a work-in-progress, subject to change at any time, and intended only for experimentation.
"###);
Ok(())
}
/// Tests that the legacy `format` option continues to work but emits a warning.
#[test]
fn legacy_format_option() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
format = "json"
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--select", "F401", "--no-cache", "--config"])
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
import os
"#), @r###"
success: false
exit_code: 1
----- stdout -----
[
{
"code": "F401",
"end_location": {
"column": 10,
"row": 2
},
"filename": "-",
"fix": {
"applicability": "Automatic",
"edits": [
{
"content": "",
"end_location": {
"column": 1,
"row": 3
},
"location": {
"column": 1,
"row": 2
}
}
],
"message": "Remove unused import: `os`"
},
"location": {
"column": 8,
"row": 2
},
"message": "`os` imported but unused",
"noqa_row": 2,
"url": "https://docs.astral.sh/ruff/rules/unused-import"
}
]
----- stderr -----
warning: The option `format` has been deprecated to avoid ambiguity with Ruff's upcoming formatter. Use `output-format` instead.
"###);
Ok(())
}

View File

@@ -28,7 +28,7 @@ ruff_workspace = { path = "../ruff_workspace", features = ["schemars"]}
anyhow = { workspace = true }
clap = { workspace = true }
ignore = { workspace = true }
indicatif = "0.17.5"
indicatif = "0.17.7"
itertools = { workspace = true }
libcst = { workspace = true }
once_cell = { workspace = true }

View File

@@ -34,7 +34,7 @@ use ruff_formatter::{FormatError, LineWidth, PrintError};
use ruff_linter::logging::LogLevel;
use ruff_linter::settings::types::{FilePattern, FilePatternSet};
use ruff_python_formatter::{
format_module, FormatModuleError, MagicTrailingComma, PyFormatOptions,
format_module_source, FormatModuleError, MagicTrailingComma, PyFormatOptions,
};
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, Resolver};
@@ -549,7 +549,6 @@ fn format_dir_entry(
let settings = resolver.resolve(&path, pyproject_config);
// That's a bad way of doing this but it's not worth doing something better for format_dev
// TODO(micha) use formatter settings instead
if settings.formatter.line_width != LineWidth::default() {
options = options.with_line_width(settings.formatter.line_width);
}
@@ -800,7 +799,7 @@ fn format_dev_file(
let content = fs::read_to_string(input_path)?;
#[cfg(not(debug_assertions))]
let start = Instant::now();
let printed = match format_module(&content, options.clone()) {
let printed = match format_module_source(&content, options.clone()) {
Ok(printed) => printed,
Err(err @ (FormatModuleError::LexError(_) | FormatModuleError::ParseError(_))) => {
return Err(CheckFileError::SyntaxErrorInInput(err));
@@ -827,7 +826,7 @@ fn format_dev_file(
}
if stability_check {
let reformatted = match format_module(formatted, options) {
let reformatted = match format_module_source(formatted, options) {
Ok(reformatted) => reformatted,
Err(err @ (FormatModuleError::LexError(_) | FormatModuleError::ParseError(_))) => {
return Err(CheckFileError::SyntaxErrorInOutput {

View File

@@ -11,6 +11,7 @@ use strum::IntoEnumIterator;
use ruff_diagnostics::AutofixKind;
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
use ruff_workspace::options::Options;
use ruff_workspace::options_base::OptionsMetadata;
use crate::ROOT_DIR;
@@ -96,10 +97,7 @@ fn process_documentation(documentation: &str, out: &mut String) {
if let Some(rest) = line.strip_prefix("- `") {
let option = rest.trim_end().trim_end_matches('`');
assert!(
Options::metadata().get(option).is_some(),
"unknown option {option}"
);
assert!(Options::metadata().has(option), "unknown option {option}");
let anchor = option.replace('.', "-");
out.push_str(&format!("- [`{option}`][{option}]\n"));

View File

@@ -1,9 +1,74 @@
//! Generate a Markdown-compatible listing of configuration options for `pyproject.toml`.
//!
//! Used for <https://docs.astral.sh/ruff/settings/>.
use itertools::Itertools;
use std::fmt::Write;
use ruff_workspace::options::Options;
use ruff_workspace::options_base::{OptionEntry, OptionField};
use ruff_workspace::options_base::{OptionField, OptionSet, OptionsMetadata, Visit};
pub(crate) fn generate() -> String {
let mut output = String::new();
generate_set(&mut output, &Set::Toplevel(Options::metadata()));
output
}
fn generate_set(output: &mut String, set: &Set) {
writeln!(output, "### {title}\n", title = set.title()).unwrap();
if let Some(documentation) = set.metadata().documentation() {
output.push_str(documentation);
output.push('\n');
output.push('\n');
}
let mut visitor = CollectOptionsVisitor::default();
set.metadata().record(&mut visitor);
let (mut fields, mut sets) = (visitor.fields, visitor.groups);
fields.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2));
sets.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2));
// Generate the fields.
for (name, field) in &fields {
emit_field(output, name, field, set.name());
output.push_str("---\n\n");
}
// Generate all the sub-sets.
for (set_name, sub_set) in &sets {
generate_set(output, &Set::Named(set_name, *sub_set));
}
}
enum Set<'a> {
Toplevel(OptionSet),
Named(&'a str, OptionSet),
}
impl<'a> Set<'a> {
fn name(&self) -> Option<&'a str> {
match self {
Set::Toplevel(_) => None,
Set::Named(name, _) => Some(name),
}
}
fn title(&self) -> &'a str {
match self {
Set::Toplevel(_) => "Top-level",
Set::Named(name, _) => name,
}
}
fn metadata(&self) -> &OptionSet {
match self {
Set::Toplevel(set) => set,
Set::Named(_, set) => set,
}
}
}
fn emit_field(output: &mut String, name: &str, field: &OptionField, group_name: Option<&str>) {
// if there's a group name, we need to add it to the anchor
@@ -37,38 +102,18 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, group_name:
output.push('\n');
}
pub(crate) fn generate() -> String {
let mut output: String = "### Top-level\n\n".into();
let sorted_options: Vec<_> = Options::metadata()
.into_iter()
.sorted_by_key(|(name, _)| *name)
.collect();
// Generate all the top-level fields.
for (name, entry) in &sorted_options {
let OptionEntry::Field(field) = entry else {
continue;
};
emit_field(&mut output, name, field, None);
output.push_str("---\n\n");
}
// Generate all the sub-groups.
for (group_name, entry) in &sorted_options {
let OptionEntry::Group(fields) = entry else {
continue;
};
output.push_str(&format!("### {group_name}\n"));
output.push('\n');
for (name, entry) in fields.iter().sorted_by_key(|(name, _)| name) {
let OptionEntry::Field(field) = entry else {
continue;
};
emit_field(&mut output, name, field, Some(group_name));
output.push_str("---\n\n");
}
}
output
#[derive(Default)]
struct CollectOptionsVisitor {
groups: Vec<(String, OptionSet)>,
fields: Vec<(String, OptionField)>,
}
impl Visit for CollectOptionsVisitor {
fn record_set(&mut self, name: &str, group: OptionSet) {
self.groups.push((name.to_owned(), group));
}
fn record_field(&mut self, name: &str, field: OptionField) {
self.fields.push((name.to_owned(), field));
}
}

View File

@@ -9,6 +9,7 @@ use ruff_diagnostics::AutofixKind;
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
use ruff_linter::upstream_categories::UpstreamCategoryAndPrefix;
use ruff_workspace::options::Options;
use ruff_workspace::options_base::OptionsMetadata;
const FIX_SYMBOL: &str = "🛠️";
const PREVIEW_SYMBOL: &str = "🧪";
@@ -104,10 +105,7 @@ pub(crate) fn generate() -> String {
table_out.push('\n');
}
if Options::metadata()
.iter()
.any(|(name, _)| name == &linter.name())
{
if Options::metadata().has(linter.name()) {
table_out.push_str(&format!(
"For related settings, see [{}](settings.md#{}).",
linter.name(),

View File

@@ -55,7 +55,11 @@ use ruff_macros::CacheKey;
use ruff_text_size::{TextRange, TextSize};
#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, CacheKey)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "kebab-case")
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default)]
pub enum IndentStyle {

View File

@@ -1,5 +1,4 @@
use crate::{FormatOptions, IndentStyle, IndentWidth, LineWidth};
use ruff_macros::CacheKey;
/// Options that affect how the [`crate::Printer`] prints the format tokens
#[derive(Clone, Debug, Eq, PartialEq, Default)]
@@ -121,7 +120,7 @@ impl SourceMapGeneration {
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, CacheKey)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum LineEnding {
/// Line Feed only (\n), common on Linux and macOS as well as inside git repos

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.0.290"
version = "0.0.291"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -45,8 +45,6 @@ libcst = { workspace = true }
log = { workspace = true }
memchr = { workspace = true }
natord = { version = "1.0.9" }
num-bigint = { workspace = true }
num-traits = { workspace = true }
once_cell = { workspace = true }
path-absolutize = { workspace = true, features = [
"once_cell_cache",
@@ -60,7 +58,7 @@ regex = { workspace = true }
result-like = { version = "0.4.6" }
rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }
semver = { version = "1.0.16" }
semver = { version = "1.0.19" }
serde = { workspace = true }
serde_json = { workspace = true }
similar = { workspace = true }

View File

@@ -20,3 +20,4 @@ os.chmod(keyfile, stat.S_IRWXO | stat.S_IRWXG | stat.S_IRWXU) # Error
os.chmod("~/hidden_exec", stat.S_IXGRP) # Error
os.chmod("~/hidden_exec", stat.S_IXOTH) # OK
os.chmod("/etc/passwd", stat.S_IWOTH) # Error
os.chmod("/etc/passwd", 0o100000000) # Error

View File

@@ -56,3 +56,7 @@ setattr(foo.bar, r"baz", None)
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458885
assert getattr(func, '_rpc')is True
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1732387247
getattr(*foo, "bar")
setattr(*foo, "bar", None)

View File

@@ -15,3 +15,29 @@ def ok_complex_logic():
def error():
resource = acquire_resource()
yield resource
import typing
from typing import Generator
@pytest.fixture()
def ok_complex_logic() -> typing.Generator[Resource, None, None]:
if some_condition:
resource = acquire_resource()
yield resource
resource.release()
return
yield None
@pytest.fixture()
def error() -> typing.Generator[typing.Any, None, None]:
resource = acquire_resource()
yield resource
@pytest.fixture()
def error() -> Generator[Resource, None, None]:
resource = acquire_resource()
yield resource

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env python
try:
from __builtin__ import bytes, str, open, super, range, zip, round, int, pow, object, input
except ImportError: pass

View File

@@ -0,0 +1,4 @@
"""
TODO:
-
"""

View File

@@ -104,3 +104,12 @@ def get_owner_id_from_mac_address():
mac_address = get_primary_mac_address()
except(IOError, OSError) as ex:
msg = 'Unable to query URL to get Owner ID: {u}\n{e}'.format(u=owner_id_url, e=ex)
# Regression test for: https://github.com/astral-sh/ruff/issues/7580
import os
try:
pass
except os.error:
pass

View File

@@ -184,3 +184,15 @@ if sys.version_info < (3,12):
if sys.version_info <= (3,12):
print("py3")
if sys.version_info <= (3,12):
print("py3")
if sys.version_info == 10000000:
print("py3")
if sys.version_info < (3,10000000):
print("py3")
if sys.version_info <= (3,10000000):
print("py3")

View File

@@ -0,0 +1,33 @@
# Errors.
print("")
print("", sep=",")
print("", end="bar")
print("", sep=",", end="bar")
print(sep="")
print("", sep="")
print("", "", sep="")
print("", "", sep="", end="")
print("", "", sep="", end="bar")
print("", sep="", end="bar")
print(sep="", end="bar")
print("", "foo", sep="")
print("foo", "", sep="")
print("foo", "", "bar", sep="")
print("", *args)
print("", *args, sep="")
print("", **kwargs)
print(sep="\t")
# OK.
print()
print("foo")
print("", "")
print("", "foo")
print("foo", "")
print("", "", sep=",")
print("", "foo", sep=",")
print("foo", "", sep=",")
print("foo", "", "bar", "", sep=",")
print("", "", **kwargs)

View File

@@ -895,6 +895,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnsupportedMethodCallOnAll) {
flake8_pyi::rules::unsupported_method_call_on_all(checker, func);
}
if checker.enabled(Rule::PrintEmptyString) {
refurb::rules::print_empty_string(checker, call);
}
if checker.enabled(Rule::QuadraticListSummation) {
ruff::rules::quadratic_list_summation(checker, call);
}

View File

@@ -292,6 +292,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
stmt,
name,
parameters,
returns.as_deref(),
decorator_list,
body,
);
@@ -695,7 +696,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
},
) => {
let module = module.as_deref();
let level = level.map(|level| level.to_u32());
let level = *level;
if checker.enabled(Rule::ModuleImportNotAtTopOfFile) {
pycodestyle::rules::module_import_not_at_top_of_file(checker, stmt);
}

View File

@@ -358,7 +358,7 @@ where
range: _,
}) => {
let module = module.as_deref();
let level = level.map(|level| level.to_u32());
let level = *level;
for alias in names {
if let Some("__future__") = module {
let name = alias.asname.as_ref().unwrap_or(&alias.name);

View File

@@ -44,7 +44,7 @@ fn extract_import_map(path: &Path, package: Option<&Path>, blocks: &[&Block]) ->
level,
range: _,
}) => {
let level = level.map_or(0, |level| level.to_usize());
let level = level.unwrap_or_default() as usize;
let module = if let Some(module) = module {
let module: &String = module.as_ref();
if level == 0 {
@@ -95,6 +95,7 @@ pub(crate) fn check_imports(
tracker.visit_body(python_ast);
tracker
};
let blocks: Vec<&Block> = tracker.iter().collect();
// Enforce import rules.

View File

@@ -914,6 +914,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Slots, "002") => (RuleGroup::Unspecified, rules::flake8_slots::rules::NoSlotsInNamedtupleSubclass),
// refurb
(Refurb, "105") => (RuleGroup::Preview, rules::refurb::rules::PrintEmptyString),
#[allow(deprecated)]
(Refurb, "113") => (RuleGroup::Nursery, rules::refurb::rules::RepeatedAppend),
#[allow(deprecated)]

View File

@@ -64,7 +64,7 @@ impl<'a> Insertion<'a> {
// Otherwise, advance to the next row.
locator.full_line_end(location)
} else {
TextSize::default()
locator.contents_start()
};
// Skip over commented lines, with whitespace separation.

View File

@@ -308,7 +308,7 @@ impl<'a> Importer<'a> {
range: _,
}) = stmt
{
if level.map_or(true, |level| level.to_u32() == 0)
if level.map_or(true, |level| level == 0)
&& name.as_ref().is_some_and(|name| name == module)
&& names.iter().all(|alias| alias.name.as_str() != "*")
{

View File

@@ -1,8 +1,6 @@
use num_bigint::BigInt;
use ruff_python_ast::{self as ast, CmpOp, Constant, Expr};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, CmpOp, Constant, Expr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -237,7 +235,7 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[CmpOp], compara
..
}) = slice.as_ref()
{
if *i == BigInt::from(0) {
if *i == 0 {
if let (
[CmpOp::Eq | CmpOp::NotEq],
[Expr::Constant(ast::ExprConstant {
@@ -246,13 +244,13 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[CmpOp], compara
})],
) = (ops, comparators)
{
if *n == BigInt::from(3) && checker.enabled(Rule::SysVersionInfo0Eq3) {
if *n == 3 && checker.enabled(Rule::SysVersionInfo0Eq3) {
checker
.diagnostics
.push(Diagnostic::new(SysVersionInfo0Eq3, left.range()));
}
}
} else if *i == BigInt::from(1) {
} else if *i == 1 {
if let (
[CmpOp::Lt | CmpOp::LtE | CmpOp::Gt | CmpOp::GtE],
[Expr::Constant(ast::ExprConstant {

View File

@@ -1,8 +1,6 @@
use num_bigint::BigInt;
use ruff_python_ast::{self as ast, Constant, Expr};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Constant, Expr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -184,11 +182,11 @@ pub(crate) fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
..
}) = upper.as_ref()
{
if *i == BigInt::from(1) && checker.enabled(Rule::SysVersionSlice1) {
if *i == 1 && checker.enabled(Rule::SysVersionSlice1) {
checker
.diagnostics
.push(Diagnostic::new(SysVersionSlice1, value.range()));
} else if *i == BigInt::from(3) && checker.enabled(Rule::SysVersionSlice3) {
} else if *i == 3 && checker.enabled(Rule::SysVersionSlice3) {
checker
.diagnostics
.push(Diagnostic::new(SysVersionSlice3, value.range()));
@@ -200,11 +198,11 @@ pub(crate) fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
value: Constant::Int(i),
..
}) => {
if *i == BigInt::from(2) && checker.enabled(Rule::SysVersion2) {
if *i == 2 && checker.enabled(Rule::SysVersion2) {
checker
.diagnostics
.push(Diagnostic::new(SysVersion2, value.range()));
} else if *i == BigInt::from(0) && checker.enabled(Rule::SysVersion0) {
} else if *i == 0 && checker.enabled(Rule::SysVersion0) {
checker
.diagnostics
.push(Diagnostic::new(SysVersion0, value.range()));

View File

@@ -1,4 +1,4 @@
use num_traits::ToPrimitive;
use anyhow::Result;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -36,17 +36,28 @@ use crate::checkers::ast::Checker;
/// - [Common Weakness Enumeration: CWE-732](https://cwe.mitre.org/data/definitions/732.html)
#[violation]
pub struct BadFilePermissions {
mask: u16,
reason: Reason,
}
impl Violation for BadFilePermissions {
#[derive_message_formats]
fn message(&self) -> String {
let BadFilePermissions { mask } = self;
format!("`os.chmod` setting a permissive mask `{mask:#o}` on file or directory")
let BadFilePermissions { reason } = self;
match reason {
Reason::Permissive(mask) => {
format!("`os.chmod` setting a permissive mask `{mask:#o}` on file or directory")
}
Reason::Invalid => format!("`os.chmod` setting an invalid mask on file or directory"),
}
}
}
#[derive(Debug, PartialEq, Eq)]
enum Reason {
Permissive(u16),
Invalid,
}
/// S103
pub(crate) fn bad_file_permissions(checker: &mut Checker, call: &ast::ExprCall) {
if checker
@@ -55,10 +66,26 @@ pub(crate) fn bad_file_permissions(checker: &mut Checker, call: &ast::ExprCall)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["os", "chmod"]))
{
if let Some(mode_arg) = call.arguments.find_argument("mode", 1) {
if let Some(int_value) = int_value(mode_arg, checker.semantic()) {
if (int_value & WRITE_WORLD > 0) || (int_value & EXECUTE_GROUP > 0) {
match parse_mask(mode_arg, checker.semantic()) {
// The mask couldn't be determined (e.g., it's dynamic).
Ok(None) => {}
// The mask is a valid integer value -- check for overly permissive permissions.
Ok(Some(mask)) => {
if (mask & WRITE_WORLD > 0) || (mask & EXECUTE_GROUP > 0) {
checker.diagnostics.push(Diagnostic::new(
BadFilePermissions {
reason: Reason::Permissive(mask),
},
mode_arg.range(),
));
}
}
// The mask is an invalid integer value (i.e., it's out of range).
Err(_) => {
checker.diagnostics.push(Diagnostic::new(
BadFilePermissions { mask: int_value },
BadFilePermissions {
reason: Reason::Invalid,
},
mode_arg.range(),
));
}
@@ -113,28 +140,37 @@ fn py_stat(call_path: &CallPath) -> Option<u16> {
}
}
fn int_value(expr: &Expr, semantic: &SemanticModel) -> Option<u16> {
/// Return the mask value as a `u16`, if it can be determined. Returns an error if the mask is
/// an integer value, but that value is out of range.
fn parse_mask(expr: &Expr, semantic: &SemanticModel) -> Result<Option<u16>> {
match expr {
Expr::Constant(ast::ExprConstant {
value: Constant::Int(value),
value: Constant::Int(int),
..
}) => value.to_u16(),
Expr::Attribute(_) => semantic.resolve_call_path(expr).as_ref().and_then(py_stat),
}) => match int.as_u16() {
Some(value) => Ok(Some(value)),
None => anyhow::bail!("int value out of range"),
},
Expr::Attribute(_) => Ok(semantic.resolve_call_path(expr).as_ref().and_then(py_stat)),
Expr::BinOp(ast::ExprBinOp {
left,
op,
right,
range: _,
}) => {
let left_value = int_value(left, semantic)?;
let right_value = int_value(right, semantic)?;
match op {
let Some(left_value) = parse_mask(left, semantic)? else {
return Ok(None);
};
let Some(right_value) = parse_mask(right, semantic)? else {
return Ok(None);
};
Ok(match op {
Operator::BitAnd => Some(left_value & right_value),
Operator::BitOr => Some(left_value | right_value),
Operator::BitXor => Some(left_value ^ right_value),
_ => None,
}
})
}
_ => None,
_ => Ok(None),
}
}

View File

@@ -1,8 +1,6 @@
use num_traits::{One, Zero};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Constant, Expr};
use ruff_python_ast::{self as ast, Constant, Expr, Int};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -52,16 +50,16 @@ pub(crate) fn snmp_insecure_version(checker: &mut Checker, call: &ast::ExprCall)
})
{
if let Some(keyword) = call.arguments.find_keyword("mpModel") {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Int(value),
..
}) = &keyword.value
{
if value.is_zero() || value.is_one() {
checker
.diagnostics
.push(Diagnostic::new(SnmpInsecureVersion, keyword.range()));
}
if matches!(
keyword.value,
Expr::Constant(ast::ExprConstant {
value: Constant::Int(Int::ZERO | Int::ONE),
..
})
) {
checker
.diagnostics
.push(Diagnostic::new(SnmpInsecureVersion, keyword.range()));
}
}
}

View File

@@ -126,6 +126,15 @@ S103.py:22:25: S103 `os.chmod` setting a permissive mask `0o2` on file or direct
21 | os.chmod("~/hidden_exec", stat.S_IXOTH) # OK
22 | os.chmod("/etc/passwd", stat.S_IWOTH) # Error
| ^^^^^^^^^^^^ S103
23 | os.chmod("/etc/passwd", 0o100000000) # Error
|
S103.py:23:25: S103 `os.chmod` setting an invalid mask on file or directory
|
21 | os.chmod("~/hidden_exec", stat.S_IXOTH) # OK
22 | os.chmod("/etc/passwd", stat.S_IWOTH) # Error
23 | os.chmod("/etc/passwd", 0o100000000) # Error
| ^^^^^^^^^^^ S103
|

View File

@@ -64,6 +64,9 @@ pub(crate) fn getattr_with_constant(
let [obj, arg] = args else {
return;
};
if obj.is_starred_expr() {
return;
}
let Expr::Constant(ast::ExprConstant {
value: Constant::Str(ast::StringConstant { value, .. }),
..
@@ -77,6 +80,9 @@ pub(crate) fn getattr_with_constant(
if is_mangled_private(value) {
return;
}
if !checker.semantic().is_builtin("getattr") {
return;
}
let mut diagnostic = Diagnostic::new(GetAttrWithConstant, expr.range());
if checker.patch(diagnostic.kind.rule()) {

View File

@@ -78,6 +78,9 @@ pub(crate) fn setattr_with_constant(
let [obj, name, value] = args else {
return;
};
if obj.is_starred_expr() {
return;
}
let Expr::Constant(ast::ExprConstant {
value: Constant::Str(name),
..
@@ -91,6 +94,10 @@ pub(crate) fn setattr_with_constant(
if is_mangled_private(name) {
return;
}
if !checker.semantic().is_builtin("setattr") {
return;
}
// We can only replace a `setattr` call (which is an `Expr`) with an assignment
// (which is a `Stmt`) if the `Expr` is already being used as a `Stmt`
// (i.e., it's directly within an `Stmt::Expr`).

View File

@@ -21,14 +21,25 @@ use crate::checkers::ast::Checker;
/// `str.removesuffix` to remove an exact prefix or suffix from a string,
/// respectively, which should be preferred when possible.
///
/// ## Known problems
/// As a heuristic, this rule only flags multi-character strings that contain
/// duplicate characters. This allows usages like `.strip("xyz")`, which
/// removes all occurrences of the characters `x`, `y`, and `z` from the
/// leading and trailing ends of the string, but not `.strip("foo")`.
///
/// The use of unique, multi-character strings may be intentional and
/// consistent with the intent of `.strip()`, `.lstrip()`, or `.rstrip()`,
/// while the use of duplicate-character strings is very likely to be a
/// mistake.
///
/// ## Example
/// ```python
/// "abcba".strip("ab") # "c"
/// "text.txt".strip(".txt") # "ex"
/// ```
///
/// Use instead:
/// ```python
/// "abcba".removeprefix("ab").removesuffix("ba") # "c"
/// "text.txt".removesuffix(".txt") # "text"
/// ```
///
/// ## References
@@ -39,7 +50,7 @@ pub struct StripWithMultiCharacters;
impl Violation for StripWithMultiCharacters {
#[derive_message_formats]
fn message(&self) -> String {
format!("Using `.strip()` with multi-character strings is misleading the reader")
format!("Using `.strip()` with multi-character strings is misleading")
}
}
@@ -65,8 +76,7 @@ pub(crate) fn strip_with_multi_characters(
return;
};
let num_chars = value.chars().count();
if num_chars > 1 && num_chars != value.chars().unique().count() {
if value.chars().count() > 1 && !value.chars().all_unique() {
checker
.diagnostics
.push(Diagnostic::new(StripWithMultiCharacters, expr.range()));

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B005.py:4:1: B005 Using `.strip()` with multi-character strings is misleading the reader
B005.py:4:1: B005 Using `.strip()` with multi-character strings is misleading
|
2 | s.strip(s) # no warning
3 | s.strip("we") # no warning
@@ -11,7 +11,7 @@ B005.py:4:1: B005 Using `.strip()` with multi-character strings is misleading th
6 | s.strip("\n\t ") # no warning
|
B005.py:7:1: B005 Using `.strip()` with multi-character strings is misleading the reader
B005.py:7:1: B005 Using `.strip()` with multi-character strings is misleading
|
5 | s.strip("e") # no warning
6 | s.strip("\n\t ") # no warning
@@ -21,7 +21,7 @@ B005.py:7:1: B005 Using `.strip()` with multi-character strings is misleading th
9 | s.lstrip("we") # no warning
|
B005.py:10:1: B005 Using `.strip()` with multi-character strings is misleading the reader
B005.py:10:1: B005 Using `.strip()` with multi-character strings is misleading
|
8 | s.lstrip(s) # no warning
9 | s.lstrip("we") # no warning
@@ -31,7 +31,7 @@ B005.py:10:1: B005 Using `.strip()` with multi-character strings is misleading t
12 | s.lstrip("\n\t ") # no warning
|
B005.py:13:1: B005 Using `.strip()` with multi-character strings is misleading the reader
B005.py:13:1: B005 Using `.strip()` with multi-character strings is misleading
|
11 | s.lstrip("e") # no warning
12 | s.lstrip("\n\t ") # no warning
@@ -41,7 +41,7 @@ B005.py:13:1: B005 Using `.strip()` with multi-character strings is misleading t
15 | s.rstrip("we") # warning
|
B005.py:16:1: B005 Using `.strip()` with multi-character strings is misleading the reader
B005.py:16:1: B005 Using `.strip()` with multi-character strings is misleading
|
14 | s.rstrip(s) # no warning
15 | s.rstrip("we") # warning
@@ -51,7 +51,7 @@ B005.py:16:1: B005 Using `.strip()` with multi-character strings is misleading t
18 | s.rstrip("\n\t ") # no warning
|
B005.py:19:1: B005 Using `.strip()` with multi-character strings is misleading the reader
B005.py:19:1: B005 Using `.strip()` with multi-character strings is misleading
|
17 | s.rstrip("e") # no warning
18 | s.rstrip("\n\t ") # no warning
@@ -61,7 +61,7 @@ B005.py:19:1: B005 Using `.strip()` with multi-character strings is misleading t
21 | s.strip("あ") # no warning
|
B005.py:22:1: B005 Using `.strip()` with multi-character strings is misleading the reader
B005.py:22:1: B005 Using `.strip()` with multi-character strings is misleading
|
20 | s.strip("a") # no warning
21 | s.strip("あ") # no warning
@@ -71,7 +71,7 @@ B005.py:22:1: B005 Using `.strip()` with multi-character strings is misleading t
24 | s.strip("\u0074\u0065\u0073\u0074") # warning
|
B005.py:24:1: B005 Using `.strip()` with multi-character strings is misleading the reader
B005.py:24:1: B005 Using `.strip()` with multi-character strings is misleading
|
22 | s.strip("ああ") # warning
23 | s.strip("\ufeff") # no warning

View File

@@ -321,6 +321,8 @@ B009_B010.py:58:8: B009 [*] Do not call `getattr` with a constant attribute valu
57 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458885
58 | assert getattr(func, '_rpc')is True
| ^^^^^^^^^^^^^^^^^^^^^ B009
59 |
60 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1732387247
|
= help: Replace `getattr` with attribute access
@@ -330,5 +332,8 @@ B009_B010.py:58:8: B009 [*] Do not call `getattr` with a constant attribute valu
57 57 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458885
58 |-assert getattr(func, '_rpc')is True
58 |+assert func._rpc is True
59 59 |
60 60 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1732387247
61 61 | getattr(*foo, "bar")

View File

@@ -1,5 +1,3 @@
use num_bigint::BigInt;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Constant, Expr, UnaryOp};
@@ -93,7 +91,7 @@ pub(crate) fn unnecessary_subscript_reversal(
else {
return;
};
if *val != BigInt::from(1) {
if *val != 1 {
return;
};
checker.diagnostics.push(Diagnostic::new(

View File

@@ -1,5 +1,3 @@
use num_bigint::BigInt;
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::{AlwaysAutofixableViolation, Fix};
use ruff_macros::{derive_message_formats, violation};
@@ -75,7 +73,7 @@ pub(crate) fn unnecessary_range_start(checker: &mut Checker, call: &ast::ExprCal
else {
return;
};
if *value != BigInt::from(0) {
if *value != 0 {
return;
};

View File

@@ -1,10 +1,7 @@
use num_bigint::BigInt;
use num_traits::{One, Zero};
use ruff_python_ast::{self as ast, CmpOp, Constant, Expr};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::map_subscript;
use ruff_python_ast::{self as ast, CmpOp, Constant, Expr, Int};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -249,18 +246,18 @@ impl ExpectedComparator {
..
}) = upper.as_ref()
{
if *upper == BigInt::one() {
if *upper == 1 {
return Some(ExpectedComparator::MajorTuple);
}
if *upper == BigInt::from(2) {
if *upper == 2 {
return Some(ExpectedComparator::MajorMinorTuple);
}
}
}
Expr::Constant(ast::ExprConstant {
value: Constant::Int(n),
value: Constant::Int(Int::ZERO),
..
}) if n.is_zero() => {
}) => {
return Some(ExpectedComparator::MajorDigit);
}
_ => (),

View File

@@ -764,7 +764,13 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &D
}
/// PT004, PT005, PT022
fn check_fixture_returns(checker: &mut Checker, stmt: &Stmt, name: &str, body: &[Stmt]) {
fn check_fixture_returns(
checker: &mut Checker,
stmt: &Stmt,
name: &str,
body: &[Stmt],
returns: Option<&Expr>,
) {
let mut visitor = SkipFunctionsVisitor::default();
for stmt in body {
@@ -795,27 +801,50 @@ fn check_fixture_returns(checker: &mut Checker, stmt: &Stmt, name: &str, body: &
}
if checker.enabled(Rule::PytestUselessYieldFixture) {
if let Some(stmt) = body.last() {
if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt {
if value.is_yield_expr() {
if visitor.yield_statements.len() == 1 {
let mut diagnostic = Diagnostic::new(
PytestUselessYieldFixture {
name: name.to_string(),
},
stmt.range(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
"return".to_string(),
TextRange::at(stmt.start(), "yield".text_len()),
)));
}
checker.diagnostics.push(diagnostic);
}
let Some(stmt) = body.last() else {
return;
};
let Stmt::Expr(ast::StmtExpr { value, .. }) = stmt else {
return;
};
if !value.is_yield_expr() {
return;
}
if visitor.yield_statements.len() != 1 {
return;
}
let mut diagnostic = Diagnostic::new(
PytestUselessYieldFixture {
name: name.to_string(),
},
stmt.range(),
);
if checker.patch(diagnostic.kind.rule()) {
let yield_edit = Edit::range_replacement(
"return".to_string(),
TextRange::at(stmt.start(), "yield".text_len()),
);
let return_type_edit = returns.and_then(|returns| {
let ast::ExprSubscript { value, slice, .. } = returns.as_subscript_expr()?;
let ast::ExprTuple { elts, .. } = slice.as_tuple_expr()?;
let [first, ..] = elts.as_slice() else {
return None;
};
if !checker.semantic().match_typing_expr(value, "Generator") {
return None;
}
Some(Edit::range_replacement(
checker.generator().expr(first),
returns.range(),
))
});
if let Some(return_type_edit) = return_type_edit {
diagnostic.set_fix(Fix::automatic_edits(yield_edit, [return_type_edit]));
} else {
diagnostic.set_fix(Fix::automatic(yield_edit));
}
}
checker.diagnostics.push(diagnostic);
}
}
@@ -910,6 +939,7 @@ pub(crate) fn fixture(
stmt: &Stmt,
name: &str,
parameters: &Parameters,
returns: Option<&Expr>,
decorators: &[Decorator],
body: &[Stmt],
) {
@@ -933,7 +963,7 @@ pub(crate) fn fixture(
|| checker.enabled(Rule::PytestUselessYieldFixture))
&& !is_abstract(decorators, checker.semantic())
{
check_fixture_returns(checker, stmt, name, body);
check_fixture_returns(checker, stmt, name, body, returns);
}
if checker.enabled(Rule::PytestFixtureFinalizerCallback) {

View File

@@ -16,5 +16,49 @@ PT022.py:17:5: PT022 [*] No teardown in fixture `error`, use `return` instead of
16 16 | resource = acquire_resource()
17 |- yield resource
17 |+ return resource
18 18 |
19 19 |
20 20 | import typing
PT022.py:37:5: PT022 [*] No teardown in fixture `error`, use `return` instead of `yield`
|
35 | def error() -> typing.Generator[typing.Any, None, None]:
36 | resource = acquire_resource()
37 | yield resource
| ^^^^^^^^^^^^^^ PT022
|
= help: Replace `yield` with `return`
Fix
32 32 |
33 33 |
34 34 | @pytest.fixture()
35 |-def error() -> typing.Generator[typing.Any, None, None]:
35 |+def error() -> typing.Any:
36 36 | resource = acquire_resource()
37 |- yield resource
37 |+ return resource
38 38 |
39 39 |
40 40 | @pytest.fixture()
PT022.py:43:5: PT022 [*] No teardown in fixture `error`, use `return` instead of `yield`
|
41 | def error() -> Generator[Resource, None, None]:
42 | resource = acquire_resource()
43 | yield resource
| ^^^^^^^^^^^^^^ PT022
|
= help: Replace `yield` with `return`
Fix
38 38 |
39 39 |
40 40 | @pytest.fixture()
41 |-def error() -> Generator[Resource, None, None]:
41 |+def error() -> Resource:
42 42 | resource = acquire_resource()
43 |- yield resource
43 |+ return resource

View File

@@ -19,6 +19,7 @@ mod tests {
#[test_case(Rule::SuppressibleException, Path::new("SIM105_1.py"))]
#[test_case(Rule::SuppressibleException, Path::new("SIM105_2.py"))]
#[test_case(Rule::SuppressibleException, Path::new("SIM105_3.py"))]
#[test_case(Rule::SuppressibleException, Path::new("SIM105_4.py"))]
#[test_case(Rule::ReturnInTryExceptFinally, Path::new("SIM107.py"))]
#[test_case(Rule::IfElseBlockInsteadOfIfExp, Path::new("SIM108.py"))]
#[test_case(Rule::CompareWithTuple, Path::new("SIM109.py"))]

View File

@@ -0,0 +1,22 @@
---
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
---
SIM105_4.py:2:1: SIM105 [*] Use `contextlib.suppress(ImportError)` instead of `try`-`except`-`pass`
|
1 | #!/usr/bin/env python
2 | / try:
3 | | from __builtin__ import bytes, str, open, super, range, zip, round, int, pow, object, input
4 | | except ImportError: pass
| |___________________________^ SIM105
|
= help: Replace with `contextlib.suppress(ImportError)`
Suggested fix
1 1 | #!/usr/bin/env python
2 |-try:
2 |+import contextlib
3 |+with contextlib.suppress(ImportError):
3 4 | from __builtin__ import bytes, str, open, super, range, zip, round, int, pow, object, input
4 |-except ImportError: pass

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Identifier, Int, Stmt};
use ruff_python_ast::{self as ast, Identifier, Stmt};
use ruff_text_size::{Ranged, TextRange};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
@@ -99,7 +99,7 @@ fn fix_banned_relative_import(
TextRange::default(),
)),
names: names.clone(),
level: Some(Int::new(0)),
level: Some(0),
range: TextRange::default(),
};
let content = generator.stmt(&node.into());

View File

@@ -118,7 +118,7 @@ pub(crate) fn annotate_imports<'a>(
AnnotatedImport::ImportFrom {
module: module.as_deref(),
names: aliases,
level: level.map(|level| level.to_u32()),
level: *level,
trailing_comma: if split_on_trailing_comma {
trailing_comma(import, locator, source_type)
} else {

View File

@@ -75,7 +75,7 @@ fn includes_import(stmt: &Stmt, target: &AnyImport) -> bool {
return false;
};
module.as_deref() == target.module
&& level.map(|level| level.to_u32()) == target.level
&& *level == target.level
&& names.iter().any(|alias| {
&alias.name == target.name.name
&& alias.asname.as_deref() == target.name.as_name
@@ -166,7 +166,7 @@ pub(crate) fn add_required_imports(
name: name.name.as_str(),
as_name: name.asname.as_deref(),
},
level: level.map(|level| level.to_u32()),
level: *level,
}),
python_ast,
locator,

View File

@@ -1,9 +1,7 @@
use num_traits::One;
use ruff_python_ast::{self as ast, CmpOp, Constant, Expr};
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, CmpOp, Constant, Expr, Int};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -80,7 +78,13 @@ pub(crate) fn nunique_constant_series_check(
}
// Right should be the integer 1.
if !is_constant_one(right) {
if !matches!(
right,
Expr::Constant(ast::ExprConstant {
value: Constant::Int(Int::ONE),
range: _,
})
) {
return;
}
@@ -110,14 +114,3 @@ pub(crate) fn nunique_constant_series_check(
expr.range(),
));
}
/// Return `true` if an [`Expr`] is a constant `1`.
fn is_constant_one(expr: &Expr) -> bool {
match expr {
Expr::Constant(constant) => match &constant.value {
Constant::Int(int) => int.is_one(),
_ => false,
},
_ => false,
}
}

View File

@@ -79,6 +79,7 @@ mod tests {
#[test_case(Rule::SectionNameEndsInColon, Path::new("D.py"))]
#[test_case(Rule::SectionNotOverIndented, Path::new("sections.py"))]
#[test_case(Rule::SectionNotOverIndented, Path::new("D214_module.py"))]
#[test_case(Rule::SectionUnderlineNotOverIndented, Path::new("D215.py"))]
#[test_case(Rule::SectionUnderlineAfterName, Path::new("sections.py"))]
#[test_case(Rule::SectionUnderlineMatchesSectionLength, Path::new("sections.py"))]
#[test_case(Rule::SectionUnderlineNotOverIndented, Path::new("sections.py"))]

View File

@@ -1440,16 +1440,17 @@ fn blanks_and_section_underline(
docstring.range(),
);
if checker.patch(diagnostic.kind.rule()) {
// Replace the existing indentation with whitespace of the appropriate length.
let range = TextRange::at(
blank_lines_end,
leading_space.text_len() + TextSize::from(1),
);
// Replace the existing indentation with whitespace of the appropriate length.
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
clean_space(docstring.indentation),
range,
)));
let contents = clean_space(docstring.indentation);
diagnostic.set_fix(Fix::automatic(if contents.is_empty() {
Edit::range_deletion(range)
} else {
Edit::range_replacement(contents, range)
}));
};
checker.diagnostics.push(diagnostic);
}

View File

@@ -0,0 +1,21 @@
---
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
---
D215.py:1:1: D215 [*] Section underline is over-indented ("TODO")
|
1 | / """
2 | | TODO:
3 | | -
4 | | """
| |___^ D215
|
= help: Remove over-indentation from "TODO" underline
Fix
1 1 | """
2 2 | TODO:
3 |- -
3 |+
4 4 | """

View File

@@ -1,5 +1,5 @@
use itertools::Itertools;
use ruff_python_ast::{self as ast, Constant, Expr, UnaryOp};
use ruff_python_ast::{self as ast, Constant, Expr, Int, UnaryOp};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -83,7 +83,7 @@ fn is_magic_value(constant: &Constant, allowed_types: &[ConstantType]) -> bool {
Constant::Str(ast::StringConstant { value, .. }) => {
!matches!(value.as_str(), "" | "__main__")
}
Constant::Int(value) => !matches!(value.try_into(), Ok(0 | 1)),
Constant::Int(value) => !matches!(*value, Int::ZERO | Int::ONE),
Constant::Bytes(_) => true,
Constant::Float(_) => true,
Constant::Complex { .. } => true,

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Alias, Identifier, Int, Stmt};
use ruff_python_ast::{self as ast, Alias, Identifier, Stmt};
use ruff_text_size::{Ranged, TextRange};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
@@ -80,7 +80,7 @@ pub(crate) fn manual_from_import(
asname: None,
range: TextRange::default(),
}],
level: Some(Int::new(0)),
level: Some(0),
range: TextRange::default(),
};
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(

View File

@@ -71,7 +71,7 @@ pub(crate) fn deprecated_c_element_tree(checker: &mut Checker, stmt: &Stmt) {
level,
range: _,
}) => {
if level.is_some_and(|level| level.to_u32() > 0) {
if level.is_some_and(|level| level > 0) {
// Ex) `import .xml.etree.cElementTree as ET`
} else if let Some(module) = module {
if module == "xml.etree.cElementTree" {

View File

@@ -323,7 +323,7 @@ pub(crate) fn deprecated_mock_import(checker: &mut Checker, stmt: &Stmt) {
level,
..
}) => {
if level.is_some_and(|level| level.to_u32() > 0) {
if level.is_some_and(|level| level > 0) {
return;
}

View File

@@ -1,8 +1,6 @@
use std::fmt;
use std::str::FromStr;
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};
@@ -47,7 +45,7 @@ impl From<LiteralType> for Constant {
value: Vec::new(),
implicit_concatenated: false,
}),
LiteralType::Int => Constant::Int(BigInt::from(0)),
LiteralType::Int => Constant::Int(0.into()),
LiteralType::Float => Constant::Float(0.0),
LiteralType::Bool => Constant::Bool(false),
}

View File

@@ -61,7 +61,7 @@ fn is_alias(expr: &Expr, semantic: &SemanticModel) -> bool {
matches!(
call_path.as_slice(),
["", "EnvironmentError" | "IOError" | "WindowsError"]
| ["mmap" | "select" | "socket", "error"]
| ["mmap" | "select" | "socket" | "os", "error"]
)
})
}
@@ -93,16 +93,13 @@ fn atom_diagnostic(checker: &mut Checker, target: &Expr) {
}
/// Create a [`Diagnostic`] for a tuple of expressions.
fn tuple_diagnostic(checker: &mut Checker, target: &Expr, aliases: &[&Expr]) {
let mut diagnostic = Diagnostic::new(OSErrorAlias { name: None }, target.range());
fn tuple_diagnostic(checker: &mut Checker, tuple: &ast::ExprTuple, aliases: &[&Expr]) {
let mut diagnostic = Diagnostic::new(OSErrorAlias { name: None }, tuple.range());
if checker.patch(diagnostic.kind.rule()) {
if checker.semantic().is_builtin("OSError") {
let Expr::Tuple(ast::ExprTuple { elts, .. }) = target else {
panic!("Expected Expr::Tuple");
};
// Filter out any `OSErrors` aliases.
let mut remaining: Vec<Expr> = elts
let mut remaining: Vec<Expr> = tuple
.elts
.iter()
.filter_map(|elt| {
if aliases.contains(&elt) {
@@ -114,7 +111,11 @@ fn tuple_diagnostic(checker: &mut Checker, target: &Expr, aliases: &[&Expr]) {
.collect();
// If `OSError` itself isn't already in the tuple, add it.
if elts.iter().all(|elt| !is_os_error(elt, checker.semantic())) {
if tuple
.elts
.iter()
.all(|elt| !is_os_error(elt, checker.semantic()))
{
let node = ast::ExprName {
id: "OSError".into(),
ctx: ExprContext::Load,
@@ -135,8 +136,8 @@ fn tuple_diagnostic(checker: &mut Checker, target: &Expr, aliases: &[&Expr]) {
};
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
pad(content, target.range(), checker.locator()),
target.range(),
pad(content, tuple.range(), checker.locator()),
tuple.range(),
)));
}
}
@@ -156,16 +157,16 @@ pub(crate) fn os_error_alias_handlers(checker: &mut Checker, handlers: &[ExceptH
atom_diagnostic(checker, expr);
}
}
Expr::Tuple(ast::ExprTuple { elts, .. }) => {
Expr::Tuple(tuple) => {
// List of aliases to replace with `OSError`.
let mut aliases: Vec<&Expr> = vec![];
for elt in elts {
for elt in &tuple.elts {
if is_alias(elt, checker.semantic()) {
aliases.push(elt);
}
}
if !aliases.is_empty() {
tuple_diagnostic(checker, expr, &aliases);
tuple_diagnostic(checker, tuple, &aliases);
}
}
_ => {}

View File

@@ -1,12 +1,11 @@
use std::cmp::Ordering;
use num_bigint::{BigInt, Sign};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use anyhow::Result;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::stmt_if::{if_elif_branches, BranchKind, IfElifBranch};
use ruff_python_ast::whitespace::indentation;
use ruff_python_ast::{self as ast, CmpOp, Constant, ElifElseClause, Expr, StmtIf};
use ruff_python_ast::{self as ast, CmpOp, Constant, ElifElseClause, Expr, Int, StmtIf};
use ruff_text_size::{Ranged, TextLen, TextRange};
use crate::autofix::edits::delete_stmt;
@@ -47,19 +46,37 @@ use crate::settings::types::PythonVersion;
/// ## References
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct OutdatedVersionBlock;
pub struct OutdatedVersionBlock {
reason: Reason,
}
impl Violation for OutdatedVersionBlock {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
impl AlwaysAutofixableViolation for OutdatedVersionBlock {
#[derive_message_formats]
fn message(&self) -> String {
format!("Version block is outdated for minimum Python version")
let OutdatedVersionBlock { reason } = self;
match reason {
Reason::Outdated => format!("Version block is outdated for minimum Python version"),
Reason::Invalid => format!("Version specifier is invalid"),
}
}
fn autofix_title(&self) -> String {
"Remove outdated version block".to_string()
fn autofix_title(&self) -> Option<String> {
let OutdatedVersionBlock { reason } = self;
match reason {
Reason::Outdated => Some("Remove outdated version block".to_string()),
Reason::Invalid => None,
}
}
}
#[derive(Debug, PartialEq, Eq)]
enum Reason {
Outdated,
Invalid,
}
/// UP036
pub(crate) fn outdated_version_block(checker: &mut Checker, stmt_if: &StmtIf) {
for branch in if_elif_branches(stmt_if) {
@@ -88,44 +105,19 @@ pub(crate) fn outdated_version_block(checker: &mut Checker, stmt_if: &StmtIf) {
match comparison {
Expr::Tuple(ast::ExprTuple { elts, .. }) => match op {
CmpOp::Lt | CmpOp::LtE => {
let version = extract_version(elts);
let Some(version) = extract_version(elts) else {
return;
};
let target = checker.settings.target_version;
if compare_version(&version, target, op == &CmpOp::LtE) {
let mut diagnostic =
Diagnostic::new(OutdatedVersionBlock, branch.test.range());
if checker.patch(diagnostic.kind.rule()) {
if let Some(fix) = fix_always_false_branch(checker, stmt_if, &branch) {
diagnostic.set_fix(fix);
}
}
checker.diagnostics.push(diagnostic);
}
}
CmpOp::Gt | CmpOp::GtE => {
let version = extract_version(elts);
let target = checker.settings.target_version;
if compare_version(&version, target, op == &CmpOp::GtE) {
let mut diagnostic =
Diagnostic::new(OutdatedVersionBlock, branch.test.range());
if checker.patch(diagnostic.kind.rule()) {
if let Some(fix) = fix_always_true_branch(checker, stmt_if, &branch) {
diagnostic.set_fix(fix);
}
}
checker.diagnostics.push(diagnostic);
}
}
_ => {}
},
Expr::Constant(ast::ExprConstant {
value: Constant::Int(number),
..
}) => {
if op == &CmpOp::Eq {
match bigint_to_u32(number) {
2 => {
let mut diagnostic =
Diagnostic::new(OutdatedVersionBlock, branch.test.range());
match compare_version(&version, target, op == &CmpOp::LtE) {
Ok(false) => {}
Ok(true) => {
let mut diagnostic = Diagnostic::new(
OutdatedVersionBlock {
reason: Reason::Outdated,
},
branch.test.range(),
);
if checker.patch(diagnostic.kind.rule()) {
if let Some(fix) =
fix_always_false_branch(checker, stmt_if, &branch)
@@ -135,9 +127,30 @@ pub(crate) fn outdated_version_block(checker: &mut Checker, stmt_if: &StmtIf) {
}
checker.diagnostics.push(diagnostic);
}
3 => {
let mut diagnostic =
Diagnostic::new(OutdatedVersionBlock, branch.test.range());
Err(_) => {
checker.diagnostics.push(Diagnostic::new(
OutdatedVersionBlock {
reason: Reason::Invalid,
},
comparison.range(),
));
}
}
}
CmpOp::Gt | CmpOp::GtE => {
let Some(version) = extract_version(elts) else {
return;
};
let target = checker.settings.target_version;
match compare_version(&version, target, op == &CmpOp::GtE) {
Ok(false) => {}
Ok(true) => {
let mut diagnostic = Diagnostic::new(
OutdatedVersionBlock {
reason: Reason::Outdated,
},
branch.test.range(),
);
if checker.patch(diagnostic.kind.rule()) {
if let Some(fix) = fix_always_true_branch(checker, stmt_if, &branch)
{
@@ -146,6 +159,63 @@ pub(crate) fn outdated_version_block(checker: &mut Checker, stmt_if: &StmtIf) {
}
checker.diagnostics.push(diagnostic);
}
Err(_) => {
checker.diagnostics.push(Diagnostic::new(
OutdatedVersionBlock {
reason: Reason::Invalid,
},
comparison.range(),
));
}
}
}
_ => {}
},
Expr::Constant(ast::ExprConstant {
value: Constant::Int(int),
..
}) => {
if op == &CmpOp::Eq {
match int.as_u8() {
Some(2) => {
let mut diagnostic = Diagnostic::new(
OutdatedVersionBlock {
reason: Reason::Outdated,
},
branch.test.range(),
);
if checker.patch(diagnostic.kind.rule()) {
if let Some(fix) =
fix_always_false_branch(checker, stmt_if, &branch)
{
diagnostic.set_fix(fix);
}
}
checker.diagnostics.push(diagnostic);
}
Some(3) => {
let mut diagnostic = Diagnostic::new(
OutdatedVersionBlock {
reason: Reason::Outdated,
},
branch.test.range(),
);
if checker.patch(diagnostic.kind.rule()) {
if let Some(fix) = fix_always_true_branch(checker, stmt_if, &branch)
{
diagnostic.set_fix(fix);
}
}
checker.diagnostics.push(diagnostic);
}
None => {
checker.diagnostics.push(Diagnostic::new(
OutdatedVersionBlock {
reason: Reason::Invalid,
},
comparison.range(),
));
}
_ => {}
}
}
@@ -156,31 +226,42 @@ pub(crate) fn outdated_version_block(checker: &mut Checker, stmt_if: &StmtIf) {
}
/// Returns true if the `target_version` is always less than the [`PythonVersion`].
fn compare_version(target_version: &[u32], py_version: PythonVersion, or_equal: bool) -> bool {
fn compare_version(
target_version: &[Int],
py_version: PythonVersion,
or_equal: bool,
) -> Result<bool> {
let mut target_version_iter = target_version.iter();
let Some(if_major) = target_version_iter.next() else {
return false;
return Ok(false);
};
let Some(if_major) = if_major.as_u8() else {
return Err(anyhow::anyhow!("invalid major version: {if_major}"));
};
let (py_major, py_minor) = py_version.as_tuple();
match if_major.cmp(&py_major) {
Ordering::Less => true,
Ordering::Greater => false,
Ordering::Less => Ok(true),
Ordering::Greater => Ok(false),
Ordering::Equal => {
let Some(if_minor) = target_version_iter.next() else {
return true;
return Ok(true);
};
if or_equal {
let Some(if_minor) = if_minor.as_u8() else {
return Err(anyhow::anyhow!("invalid minor version: {if_minor}"));
};
Ok(if or_equal {
// Ex) `sys.version_info <= 3.8`. If Python 3.8 is the minimum supported version,
// the condition won't always evaluate to `false`, so we want to return `false`.
*if_minor < py_minor
if_minor < py_minor
} else {
// Ex) `sys.version_info < 3.8`. If Python 3.8 is the minimum supported version,
// the condition _will_ always evaluate to `false`, so we want to return `true`.
*if_minor <= py_minor
}
if_minor <= py_minor
})
}
}
}
@@ -353,31 +434,20 @@ fn fix_always_true_branch(
}
}
/// Converts a `BigInt` to a `u32`. If the number is negative, it will return 0.
fn bigint_to_u32(number: &BigInt) -> u32 {
let the_number = number.to_u32_digits();
match the_number.0 {
Sign::Minus | Sign::NoSign => 0,
Sign::Plus => *the_number.1.first().unwrap(),
}
}
/// Gets the version from the tuple
fn extract_version(elts: &[Expr]) -> Vec<u32> {
let mut version: Vec<u32> = vec![];
/// Return the version tuple as a sequence of [`Int`] values.
fn extract_version(elts: &[Expr]) -> Option<Vec<Int>> {
let mut version: Vec<Int> = vec![];
for elt in elts {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Int(item),
let Expr::Constant(ast::ExprConstant {
value: Constant::Int(int),
..
}) = &elt
{
let number = bigint_to_u32(item);
version.push(number);
} else {
return version;
}
else {
return None;
};
version.push(int.clone());
}
version
Some(version)
}
#[cfg(test)]
@@ -399,10 +469,13 @@ mod tests {
#[test_case(PythonVersion::Py310, &[3, 11], true, false; "compare-3.11")]
fn test_compare_version(
version: PythonVersion,
version_vec: &[u32],
target_versions: &[u8],
or_equal: bool,
expected: bool,
) {
assert_eq!(compare_version(version_vec, version, or_equal), expected);
) -> Result<()> {
let target_versions: Vec<_> = target_versions.iter().map(|int| Int::from(*int)).collect();
let actual = compare_version(&target_versions, version, or_equal)?;
assert_eq!(actual, expected);
Ok(())
}
}

View File

@@ -9,12 +9,6 @@ use crate::autofix::edits::{pad, remove_argument, Parentheses};
use crate::checkers::ast::Checker;
use crate::registry::Rule;
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Reason {
BytesLiteral,
DefaultArgument,
}
/// ## What it does
/// Checks for unnecessary calls to `encode` as UTF-8.
///
@@ -56,6 +50,12 @@ impl AlwaysAutofixableViolation for UnnecessaryEncodeUTF8 {
}
}
#[derive(Debug, PartialEq, Eq)]
enum Reason {
BytesLiteral,
DefaultArgument,
}
const UTF8_LITERALS: &[&str] = &["utf-8", "utf8", "utf_8", "u8", "utf", "cp65001"];
fn match_encoded_variable(func: &Expr) -> Option<&Expr> {

View File

@@ -281,5 +281,25 @@ UP024_0.py:105:11: UP024 [*] Replace aliased errors with `OSError`
105 |- except(IOError, OSError) as ex:
105 |+ except OSError as ex:
106 106 | msg = 'Unable to query URL to get Owner ID: {u}\n{e}'.format(u=owner_id_url, e=ex)
107 107 |
108 108 |
UP024_0.py:114:8: UP024 [*] Replace aliased errors with `OSError`
|
112 | try:
113 | pass
114 | except os.error:
| ^^^^^^^^ UP024
115 | pass
|
= help: Replace `os.error` with builtin `OSError`
Fix
111 111 |
112 112 | try:
113 113 | pass
114 |-except os.error:
114 |+except OSError:
115 115 | pass

View File

@@ -685,4 +685,31 @@ UP036_0.py:182:4: UP036 [*] Version block is outdated for minimum Python version
185 183 | if sys.version_info <= (3,12):
186 184 | print("py3")
UP036_0.py:191:24: UP036 Version specifier is invalid
|
189 | print("py3")
190 |
191 | if sys.version_info == 10000000:
| ^^^^^^^^ UP036
192 | print("py3")
|
UP036_0.py:194:23: UP036 Version specifier is invalid
|
192 | print("py3")
193 |
194 | if sys.version_info < (3,10000000):
| ^^^^^^^^^^^^ UP036
195 | print("py3")
|
UP036_0.py:197:24: UP036 Version specifier is invalid
|
195 | print("py3")
196 |
197 | if sys.version_info <= (3,10000000):
| ^^^^^^^^^^^^ UP036
198 | print("py3")
|

View File

@@ -20,6 +20,7 @@ mod tests {
#[test_case(Rule::ReimplementedStarmap, Path::new("FURB140.py"))]
#[test_case(Rule::SliceCopy, Path::new("FURB145.py"))]
#[test_case(Rule::UnnecessaryEnumerate, Path::new("FURB148.py"))]
#[test_case(Rule::PrintEmptyString, Path::new("FURB105.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -1,5 +1,6 @@
pub(crate) use check_and_remove_from_set::*;
pub(crate) use delete_full_slice::*;
pub(crate) use print_empty_string::*;
pub(crate) use reimplemented_starmap::*;
pub(crate) use repeated_append::*;
pub(crate) use slice_copy::*;
@@ -7,6 +8,7 @@ pub(crate) use unnecessary_enumerate::*;
mod check_and_remove_from_set;
mod delete_full_slice;
mod print_empty_string;
mod reimplemented_starmap;
mod repeated_append;
mod slice_copy;

View File

@@ -0,0 +1,229 @@
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Constant, Expr};
use ruff_python_codegen::Generator;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for `print` calls with an empty string as the only positional
/// argument.
///
/// ## Why is this bad?
/// Prefer calling `print` without any positional arguments, which is
/// equivalent and more concise.
///
/// ## Example
/// ```python
/// print("")
/// ```
///
/// Use instead:
/// ```python
/// print()
/// ```
///
/// ## References
/// - [Python documentation: `print`](https://docs.python.org/3/library/functions.html#print)
#[violation]
pub struct PrintEmptyString {
reason: Reason,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Reason {
/// Ex) `print("")`
EmptyArgument,
/// Ex) `print("foo", sep="\t")`
UselessSeparator,
/// Ex) `print("", sep="\t")`
Both,
}
impl Violation for PrintEmptyString {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let PrintEmptyString { reason } = self;
match reason {
Reason::EmptyArgument => format!("Unnecessary empty string passed to `print`"),
Reason::UselessSeparator => format!("Unnecessary separator passed to `print`"),
Reason::Both => format!("Unnecessary empty string and separator passed to `print`"),
}
}
fn autofix_title(&self) -> Option<String> {
let PrintEmptyString { reason } = self;
match reason {
Reason::EmptyArgument => Some("Remove empty string".to_string()),
Reason::UselessSeparator => Some("Remove separator".to_string()),
Reason::Both => Some("Remove empty string and separator".to_string()),
}
}
}
/// FURB105
pub(crate) fn print_empty_string(checker: &mut Checker, call: &ast::ExprCall) {
if !checker
.semantic()
.resolve_call_path(&call.func)
.as_ref()
.is_some_and(|call_path| matches!(call_path.as_slice(), ["", "print"]))
{
return;
}
match &call.arguments.args.as_slice() {
// Ex) `print(*args)` or `print(*args, sep="\t")`
[arg] if arg.is_starred_expr() => {}
// Ex) `print("")` or `print("", sep="\t")`
[arg] if is_empty_string(arg) => {
let reason = if call.arguments.find_keyword("sep").is_some() {
Reason::Both
} else {
Reason::EmptyArgument
};
let mut diagnostic = Diagnostic::new(PrintEmptyString { reason }, call.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::suggested(Edit::replacement(
generate_suggestion(call, Separator::Remove, checker.generator()),
call.start(),
call.end(),
)));
}
checker.diagnostics.push(diagnostic);
}
// Ex) `print(sep="\t")` or `print(obj, sep="\t")`
[] | [_] => {
// If there's a `sep` argument, remove it, regardless of what it is.
if call.arguments.find_keyword("sep").is_some() {
let mut diagnostic = Diagnostic::new(
PrintEmptyString {
reason: Reason::UselessSeparator,
},
call.range(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::suggested(Edit::replacement(
generate_suggestion(call, Separator::Remove, checker.generator()),
call.start(),
call.end(),
)));
}
checker.diagnostics.push(diagnostic);
}
}
// Ex) `print("foo", "", "bar", sep="")`
_ => {
// Ignore `**kwargs`.
let has_kwargs = call
.arguments
.keywords
.iter()
.any(|keyword| keyword.arg.is_none());
if has_kwargs {
return;
}
// Require an empty `sep` argument.
let empty_separator = call
.arguments
.find_keyword("sep")
.map_or(false, |keyword| is_empty_string(&keyword.value));
if !empty_separator {
return;
}
// Count the number of empty and non-empty arguments.
let empty_arguments = call
.arguments
.args
.iter()
.filter(|arg| is_empty_string(arg))
.count();
if empty_arguments == 0 {
return;
}
// If removing the arguments would leave us with one or fewer, then we can remove the
// separator too.
let separator = if call.arguments.args.len() - empty_arguments > 1
|| call.arguments.args.iter().any(Expr::is_starred_expr)
{
Separator::Retain
} else {
Separator::Remove
};
let mut diagnostic = Diagnostic::new(
PrintEmptyString {
reason: if separator == Separator::Retain {
Reason::EmptyArgument
} else {
Reason::Both
},
},
call.range(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::suggested(Edit::replacement(
generate_suggestion(call, separator, checker.generator()),
call.start(),
call.end(),
)));
}
checker.diagnostics.push(diagnostic);
}
}
}
/// Check if an expression is a constant empty string.
fn is_empty_string(expr: &Expr) -> bool {
matches!(
expr,
Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
..
}) if value.is_empty()
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Separator {
Remove,
Retain,
}
/// Generate a suggestion to remove the empty string positional argument and
/// the `sep` keyword argument, if it exists.
fn generate_suggestion(call: &ast::ExprCall, separator: Separator, generator: Generator) -> String {
let mut call = call.clone();
// Remove all empty string positional arguments.
call.arguments.args.retain(|arg| !is_empty_string(arg));
// Remove the `sep` keyword argument if it exists.
if separator == Separator::Remove {
call.arguments.keywords.retain(|keyword| {
keyword
.arg
.as_ref()
.map_or(true, |arg| arg.as_str() != "sep")
});
}
generator.expr(&call.into())
}

View File

@@ -1,11 +1,9 @@
use std::fmt;
use num_traits::Zero;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::{Arguments, Constant, Expr};
use ruff_python_ast::{Arguments, Constant, Expr, Int};
use ruff_python_codegen::Generator;
use ruff_text_size::{Ranged, TextRange};
@@ -160,15 +158,13 @@ pub(crate) fn unnecessary_enumerate(checker: &mut Checker, stmt_for: &ast::StmtF
// there's no clear fix.
let start = arguments.find_argument("start", 1);
if start.map_or(true, |start| {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Int(value),
..
}) = start
{
value.is_zero()
} else {
false
}
matches!(
start,
Expr::Constant(ast::ExprConstant {
value: Constant::Int(Int::ZERO),
..
})
)
}) {
let replace_iter = Edit::range_replacement(
generate_range_len_call(sequence, checker.generator()),

View File

@@ -0,0 +1,358 @@
---
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB105.py:3:1: FURB105 [*] Unnecessary empty string passed to `print`
|
1 | # Errors.
2 |
3 | print("")
| ^^^^^^^^^ FURB105
4 | print("", sep=",")
5 | print("", end="bar")
|
= help: Remove empty string
Suggested fix
1 1 | # Errors.
2 2 |
3 |-print("")
3 |+print()
4 4 | print("", sep=",")
5 5 | print("", end="bar")
6 6 | print("", sep=",", end="bar")
FURB105.py:4:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
3 | print("")
4 | print("", sep=",")
| ^^^^^^^^^^^^^^^^^^ FURB105
5 | print("", end="bar")
6 | print("", sep=",", end="bar")
|
= help: Remove empty string and separator
Suggested fix
1 1 | # Errors.
2 2 |
3 3 | print("")
4 |-print("", sep=",")
4 |+print()
5 5 | print("", end="bar")
6 6 | print("", sep=",", end="bar")
7 7 | print(sep="")
FURB105.py:5:1: FURB105 [*] Unnecessary empty string passed to `print`
|
3 | print("")
4 | print("", sep=",")
5 | print("", end="bar")
| ^^^^^^^^^^^^^^^^^^^^ FURB105
6 | print("", sep=",", end="bar")
7 | print(sep="")
|
= help: Remove empty string
Suggested fix
2 2 |
3 3 | print("")
4 4 | print("", sep=",")
5 |-print("", end="bar")
5 |+print(end="bar")
6 6 | print("", sep=",", end="bar")
7 7 | print(sep="")
8 8 | print("", sep="")
FURB105.py:6:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
4 | print("", sep=",")
5 | print("", end="bar")
6 | print("", sep=",", end="bar")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB105
7 | print(sep="")
8 | print("", sep="")
|
= help: Remove empty string and separator
Suggested fix
3 3 | print("")
4 4 | print("", sep=",")
5 5 | print("", end="bar")
6 |-print("", sep=",", end="bar")
6 |+print(end="bar")
7 7 | print(sep="")
8 8 | print("", sep="")
9 9 | print("", "", sep="")
FURB105.py:7:1: FURB105 [*] Unnecessary separator passed to `print`
|
5 | print("", end="bar")
6 | print("", sep=",", end="bar")
7 | print(sep="")
| ^^^^^^^^^^^^^ FURB105
8 | print("", sep="")
9 | print("", "", sep="")
|
= help: Remove separator
Suggested fix
4 4 | print("", sep=",")
5 5 | print("", end="bar")
6 6 | print("", sep=",", end="bar")
7 |-print(sep="")
7 |+print()
8 8 | print("", sep="")
9 9 | print("", "", sep="")
10 10 | print("", "", sep="", end="")
FURB105.py:8:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
6 | print("", sep=",", end="bar")
7 | print(sep="")
8 | print("", sep="")
| ^^^^^^^^^^^^^^^^^ FURB105
9 | print("", "", sep="")
10 | print("", "", sep="", end="")
|
= help: Remove empty string and separator
Suggested fix
5 5 | print("", end="bar")
6 6 | print("", sep=",", end="bar")
7 7 | print(sep="")
8 |-print("", sep="")
8 |+print()
9 9 | print("", "", sep="")
10 10 | print("", "", sep="", end="")
11 11 | print("", "", sep="", end="bar")
FURB105.py:9:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
7 | print(sep="")
8 | print("", sep="")
9 | print("", "", sep="")
| ^^^^^^^^^^^^^^^^^^^^^ FURB105
10 | print("", "", sep="", end="")
11 | print("", "", sep="", end="bar")
|
= help: Remove empty string and separator
Suggested fix
6 6 | print("", sep=",", end="bar")
7 7 | print(sep="")
8 8 | print("", sep="")
9 |-print("", "", sep="")
9 |+print()
10 10 | print("", "", sep="", end="")
11 11 | print("", "", sep="", end="bar")
12 12 | print("", sep="", end="bar")
FURB105.py:10:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
8 | print("", sep="")
9 | print("", "", sep="")
10 | print("", "", sep="", end="")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB105
11 | print("", "", sep="", end="bar")
12 | print("", sep="", end="bar")
|
= help: Remove empty string and separator
Suggested fix
7 7 | print(sep="")
8 8 | print("", sep="")
9 9 | print("", "", sep="")
10 |-print("", "", sep="", end="")
10 |+print(end="")
11 11 | print("", "", sep="", end="bar")
12 12 | print("", sep="", end="bar")
13 13 | print(sep="", end="bar")
FURB105.py:11:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
9 | print("", "", sep="")
10 | print("", "", sep="", end="")
11 | print("", "", sep="", end="bar")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB105
12 | print("", sep="", end="bar")
13 | print(sep="", end="bar")
|
= help: Remove empty string and separator
Suggested fix
8 8 | print("", sep="")
9 9 | print("", "", sep="")
10 10 | print("", "", sep="", end="")
11 |-print("", "", sep="", end="bar")
11 |+print(end="bar")
12 12 | print("", sep="", end="bar")
13 13 | print(sep="", end="bar")
14 14 | print("", "foo", sep="")
FURB105.py:12:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
10 | print("", "", sep="", end="")
11 | print("", "", sep="", end="bar")
12 | print("", sep="", end="bar")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB105
13 | print(sep="", end="bar")
14 | print("", "foo", sep="")
|
= help: Remove empty string and separator
Suggested fix
9 9 | print("", "", sep="")
10 10 | print("", "", sep="", end="")
11 11 | print("", "", sep="", end="bar")
12 |-print("", sep="", end="bar")
12 |+print(end="bar")
13 13 | print(sep="", end="bar")
14 14 | print("", "foo", sep="")
15 15 | print("foo", "", sep="")
FURB105.py:13:1: FURB105 [*] Unnecessary separator passed to `print`
|
11 | print("", "", sep="", end="bar")
12 | print("", sep="", end="bar")
13 | print(sep="", end="bar")
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB105
14 | print("", "foo", sep="")
15 | print("foo", "", sep="")
|
= help: Remove separator
Suggested fix
10 10 | print("", "", sep="", end="")
11 11 | print("", "", sep="", end="bar")
12 12 | print("", sep="", end="bar")
13 |-print(sep="", end="bar")
13 |+print(end="bar")
14 14 | print("", "foo", sep="")
15 15 | print("foo", "", sep="")
16 16 | print("foo", "", "bar", sep="")
FURB105.py:14:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
12 | print("", sep="", end="bar")
13 | print(sep="", end="bar")
14 | print("", "foo", sep="")
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB105
15 | print("foo", "", sep="")
16 | print("foo", "", "bar", sep="")
|
= help: Remove empty string and separator
Suggested fix
11 11 | print("", "", sep="", end="bar")
12 12 | print("", sep="", end="bar")
13 13 | print(sep="", end="bar")
14 |-print("", "foo", sep="")
14 |+print("foo")
15 15 | print("foo", "", sep="")
16 16 | print("foo", "", "bar", sep="")
17 17 | print("", *args)
FURB105.py:15:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
13 | print(sep="", end="bar")
14 | print("", "foo", sep="")
15 | print("foo", "", sep="")
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB105
16 | print("foo", "", "bar", sep="")
17 | print("", *args)
|
= help: Remove empty string and separator
Suggested fix
12 12 | print("", sep="", end="bar")
13 13 | print(sep="", end="bar")
14 14 | print("", "foo", sep="")
15 |-print("foo", "", sep="")
15 |+print("foo")
16 16 | print("foo", "", "bar", sep="")
17 17 | print("", *args)
18 18 | print("", *args, sep="")
FURB105.py:16:1: FURB105 [*] Unnecessary empty string passed to `print`
|
14 | print("", "foo", sep="")
15 | print("foo", "", sep="")
16 | print("foo", "", "bar", sep="")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB105
17 | print("", *args)
18 | print("", *args, sep="")
|
= help: Remove empty string
Suggested fix
13 13 | print(sep="", end="bar")
14 14 | print("", "foo", sep="")
15 15 | print("foo", "", sep="")
16 |-print("foo", "", "bar", sep="")
16 |+print("foo", "bar", sep="")
17 17 | print("", *args)
18 18 | print("", *args, sep="")
19 19 | print("", **kwargs)
FURB105.py:18:1: FURB105 [*] Unnecessary empty string passed to `print`
|
16 | print("foo", "", "bar", sep="")
17 | print("", *args)
18 | print("", *args, sep="")
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB105
19 | print("", **kwargs)
20 | print(sep="\t")
|
= help: Remove empty string
Suggested fix
15 15 | print("foo", "", sep="")
16 16 | print("foo", "", "bar", sep="")
17 17 | print("", *args)
18 |-print("", *args, sep="")
18 |+print(*args, sep="")
19 19 | print("", **kwargs)
20 20 | print(sep="\t")
21 21 |
FURB105.py:19:1: FURB105 [*] Unnecessary empty string passed to `print`
|
17 | print("", *args)
18 | print("", *args, sep="")
19 | print("", **kwargs)
| ^^^^^^^^^^^^^^^^^^^ FURB105
20 | print(sep="\t")
|
= help: Remove empty string
Suggested fix
16 16 | print("foo", "", "bar", sep="")
17 17 | print("", *args)
18 18 | print("", *args, sep="")
19 |-print("", **kwargs)
19 |+print(**kwargs)
20 20 | print(sep="\t")
21 21 |
22 22 | # OK.
FURB105.py:20:1: FURB105 [*] Unnecessary separator passed to `print`
|
18 | print("", *args, sep="")
19 | print("", **kwargs)
20 | print(sep="\t")
| ^^^^^^^^^^^^^^^ FURB105
21 |
22 | # OK.
|
= help: Remove separator
Suggested fix
17 17 | print("", *args)
18 18 | print("", *args, sep="")
19 19 | print("", **kwargs)
20 |-print(sep="\t")
20 |+print()
21 21 |
22 22 | # OK.
23 23 |

View File

@@ -1,8 +1,6 @@
use num_traits::ToPrimitive;
use ruff_python_ast::{self as ast, Constant, Expr, UnaryOp};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Constant, Expr, Int};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -45,26 +43,17 @@ impl Violation for PairwiseOverZipped {
#[derive(Debug)]
struct SliceInfo {
arg_name: String,
slice_start: Option<i64>,
id: String,
slice_start: Option<i32>,
}
impl SliceInfo {
pub(crate) fn new(arg_name: String, slice_start: Option<i64>) -> Self {
Self {
arg_name,
slice_start,
}
}
}
/// Return the argument name, lower bound, and upper bound for an expression, if it's a slice.
/// Return the argument name, lower bound, and upper bound for an expression, if it's a slice.
fn match_slice_info(expr: &Expr) -> Option<SliceInfo> {
let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr else {
return None;
};
let Expr::Name(ast::ExprName { id: arg_id, .. }) = value.as_ref() else {
let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else {
return None;
};
@@ -74,44 +63,40 @@ fn match_slice_info(expr: &Expr) -> Option<SliceInfo> {
// Avoid false positives for slices with a step.
if let Some(step) = step {
if let Some(step) = to_bound(step) {
if step != 1 {
return None;
}
} else {
if !matches!(
step.as_ref(),
Expr::Constant(ast::ExprConstant {
value: Constant::Int(Int::ONE),
..
})
) {
return None;
}
}
Some(SliceInfo::new(
arg_id.to_string(),
lower.as_ref().and_then(|expr| to_bound(expr)),
))
}
fn to_bound(expr: &Expr) -> Option<i64> {
match expr {
Expr::Constant(ast::ExprConstant {
value: Constant::Int(value),
..
}) => value.to_i64(),
Expr::UnaryOp(ast::ExprUnaryOp {
op: UnaryOp::USub | UnaryOp::Invert,
operand,
// If the slice start is a non-constant, we can't be sure that it's successive.
let slice_start = if let Some(lower) = lower.as_ref() {
let Expr::Constant(ast::ExprConstant {
value: Constant::Int(int),
range: _,
}) => {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Int(value),
..
}) = operand.as_ref()
{
value.to_i64().map(|v| -v)
} else {
None
}
}
_ => None,
}
}) = lower.as_ref()
else {
return None;
};
let Some(slice_start) = int.as_i32() else {
return None;
};
Some(slice_start)
} else {
None
};
Some(SliceInfo {
id: id.to_string(),
slice_start,
})
}
/// RUF007
@@ -121,9 +106,9 @@ pub(crate) fn pairwise_over_zipped(checker: &mut Checker, func: &Expr, args: &[E
};
// Require exactly two positional arguments.
if args.len() != 2 {
let [first, second] = args else {
return;
}
};
// Require the function to be the builtin `zip`.
if !(id == "zip" && checker.semantic().is_builtin(id)) {
@@ -132,25 +117,28 @@ pub(crate) fn pairwise_over_zipped(checker: &mut Checker, func: &Expr, args: &[E
// Allow the first argument to be a `Name` or `Subscript`.
let Some(first_arg_info) = ({
if let Expr::Name(ast::ExprName { id, .. }) = &args[0] {
Some(SliceInfo::new(id.to_string(), None))
if let Expr::Name(ast::ExprName { id, .. }) = first {
Some(SliceInfo {
id: id.to_string(),
slice_start: None,
})
} else {
match_slice_info(&args[0])
match_slice_info(first)
}
}) else {
return;
};
// Require second argument to be a `Subscript`.
if !args[1].is_subscript_expr() {
if !second.is_subscript_expr() {
return;
}
let Some(second_arg_info) = match_slice_info(&args[1]) else {
let Some(second_arg_info) = match_slice_info(second) else {
return;
};
// Verify that the arguments match the same name.
if first_arg_info.arg_name != second_arg_info.arg_name {
if first_arg_info.id != second_arg_info.id {
return;
}

View File

@@ -1,10 +1,8 @@
use std::borrow::Cow;
use num_traits::Zero;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Arguments, Comprehension, Constant, Expr};
use ruff_python_ast::{self as ast, Arguments, Comprehension, Constant, Expr, Int};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -110,15 +108,13 @@ pub(crate) fn unnecessary_iterable_allocation_for_first_element(
/// Check that the slice [`Expr`] is a slice of the first element (e.g., `x[0]`).
fn is_head_slice(expr: &Expr) -> bool {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Int(value),
..
}) = expr
{
value.is_zero()
} else {
false
}
matches!(
expr,
Expr::Constant(ast::ExprConstant {
value: Constant::Int(Int::ZERO),
..
})
)
}
#[derive(Debug)]

View File

@@ -11,7 +11,7 @@ use ruff_source_file::Locator;
///
/// A known type is either a builtin type, any object from the standard library,
/// or a type from the `typing_extensions` module.
fn is_known_type(call_path: &CallPath, minor_version: u32) -> bool {
fn is_known_type(call_path: &CallPath, minor_version: u8) -> bool {
match call_path.as_slice() {
["" | "typing_extensions", ..] => true,
[module, ..] => is_known_standard_library(minor_version, module),
@@ -72,7 +72,7 @@ impl<'a> TypingTarget<'a> {
expr: &'a Expr,
semantic: &SemanticModel,
locator: &Locator,
minor_version: u32,
minor_version: u8,
) -> Option<Self> {
match expr {
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
@@ -141,7 +141,7 @@ impl<'a> TypingTarget<'a> {
&self,
semantic: &SemanticModel,
locator: &Locator,
minor_version: u32,
minor_version: u8,
) -> bool {
match self {
TypingTarget::None
@@ -189,12 +189,7 @@ impl<'a> TypingTarget<'a> {
}
/// Check if the [`TypingTarget`] explicitly allows `Any`.
fn contains_any(
&self,
semantic: &SemanticModel,
locator: &Locator,
minor_version: u32,
) -> bool {
fn contains_any(&self, semantic: &SemanticModel, locator: &Locator, minor_version: u8) -> bool {
match self {
TypingTarget::Any => true,
// `Literal` cannot contain `Any` as it's a dynamic value.
@@ -242,7 +237,7 @@ pub(crate) fn type_hint_explicitly_allows_none<'a>(
annotation: &'a Expr,
semantic: &SemanticModel,
locator: &Locator,
minor_version: u32,
minor_version: u8,
) -> Option<&'a Expr> {
match TypingTarget::try_from_expr(annotation, semantic, locator, minor_version) {
None |
@@ -272,7 +267,7 @@ pub(crate) fn type_hint_resolves_to_any(
annotation: &Expr,
semantic: &SemanticModel,
locator: &Locator,
minor_version: u32,
minor_version: u8,
) -> bool {
match TypingTarget::try_from_expr(annotation, semantic, locator, minor_version) {
None |

View File

@@ -58,7 +58,7 @@ impl PythonVersion {
Self::Py312
}
pub const fn as_tuple(&self) -> (u32, u32) {
pub const fn as_tuple(&self) -> (u8, u8) {
match self {
Self::Py37 => (3, 7),
Self::Py38 => (3, 8),
@@ -69,11 +69,11 @@ impl PythonVersion {
}
}
pub const fn major(&self) -> u32 {
pub const fn major(&self) -> u8 {
self.as_tuple().0
}
pub const fn minor(&self) -> u32 {
pub const fn minor(&self) -> u8 {
self.as_tuple().1
}

View File

@@ -10,7 +10,12 @@ use syn::{
};
pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let DeriveInput { ident, data, .. } = input;
let DeriveInput {
ident,
data,
attrs: struct_attributes,
..
} = input;
match data {
Data::Struct(DataStruct {
@@ -50,15 +55,39 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenS
};
}
let options_len = output.len();
let docs: Vec<&Attribute> = struct_attributes
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.collect();
// Convert the list of `doc` attributes into a single string.
let doc = dedent(
&docs
.into_iter()
.map(parse_doc)
.collect::<syn::Result<Vec<_>>>()?
.join("\n"),
)
.trim_matches('\n')
.to_string();
let documentation = if doc.is_empty() {
None
} else {
Some(quote!(
fn documentation() -> Option<&'static str> {
Some(&#doc)
}
))
};
Ok(quote! {
impl #ident {
pub const fn metadata() -> crate::options_base::OptionGroup {
const OPTIONS: [(&'static str, crate::options_base::OptionEntry); #options_len] = [#(#output),*];
crate::options_base::OptionGroup::new(&OPTIONS)
impl crate::options_base::OptionsMetadata for #ident {
fn record(visit: &mut dyn crate::options_base::Visit) {
#(#output);*
}
#documentation
}
})
}
@@ -92,7 +121,7 @@ fn handle_option_group(field: &Field) -> syn::Result<proc_macro2::TokenStream> {
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
Ok(quote_spanned!(
ident.span() => (#kebab_name, crate::options_base::OptionEntry::Group(#path::metadata()))
ident.span() => (visit.record_set(#kebab_name, crate::options_base::OptionSet::of::<#path>()))
))
}
_ => Err(syn::Error::new(
@@ -150,12 +179,14 @@ fn handle_option(
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
Ok(quote_spanned!(
ident.span() => (#kebab_name, crate::options_base::OptionEntry::Field(crate::options_base::OptionField {
doc: &#doc,
default: &#default,
value_type: &#value_type,
example: &#example,
}))
ident.span() => {
visit.record_field(#kebab_name, crate::options_base::OptionField{
doc: &#doc,
default: &#default,
value_type: &#value_type,
example: &#example,
})
}
))
}

View File

@@ -21,8 +21,6 @@ bitflags = { workspace = true }
is-macro = { workspace = true }
itertools = { workspace = true }
memchr = { workspace = true }
num-bigint = { workspace = true }
num-traits = { workspace = true }
once_cell = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true, optional = true }

View File

@@ -15,8 +15,6 @@
//! an implicit concatenation of string literals, as these expressions are considered to
//! have the same shape in that they evaluate to the same value.
use num_bigint::BigInt;
use crate as ast;
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
@@ -334,7 +332,7 @@ pub enum ComparableConstant<'a> {
Bool(&'a bool),
Str { value: &'a str, unicode: bool },
Bytes(&'a [u8]),
Int(&'a BigInt),
Int(&'a ast::Int),
Tuple(Vec<ComparableConstant<'a>>),
Float(u64),
Complex { real: u64, imag: u64 },
@@ -1161,7 +1159,7 @@ pub struct StmtImport<'a> {
pub struct StmtImportFrom<'a> {
module: Option<&'a str>,
names: Vec<ComparableAlias<'a>>,
level: Option<ast::Int>,
level: Option<u32>,
}
#[derive(Debug, PartialEq, Eq, Hash)]

View File

@@ -1,7 +1,6 @@
use std::borrow::Cow;
use std::path::Path;
use num_traits::Zero;
use smallvec::SmallVec;
use ruff_text_size::{Ranged, TextRange};
@@ -1073,7 +1072,7 @@ impl Truthiness {
Constant::None => Some(false),
Constant::Str(ast::StringConstant { value, .. }) => Some(!value.is_empty()),
Constant::Bytes(bytes) => Some(!bytes.is_empty()),
Constant::Int(int) => Some(!int.is_zero()),
Constant::Int(int) => Some(*int != 0),
Constant::Float(float) => Some(*float != 0.0),
Constant::Complex { real, imag } => Some(*real != 0.0 || *imag != 0.0),
Constant::Ellipsis => Some(true),
@@ -1140,7 +1139,7 @@ mod tests {
use crate::helpers::{any_over_stmt, any_over_type_param, resolve_imported_module_path};
use crate::{
Constant, Expr, ExprConstant, ExprContext, ExprName, Identifier, Stmt, StmtTypeAlias,
Constant, Expr, ExprConstant, ExprContext, ExprName, Identifier, Int, Stmt, StmtTypeAlias,
TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, TypeParams,
};
@@ -1240,7 +1239,7 @@ mod tests {
assert!(!any_over_type_param(&type_var_no_bound, &|_expr| true));
let bound = Expr::Constant(ExprConstant {
value: Constant::Int(1.into()),
value: Constant::Int(Int::ONE),
range: TextRange::default(),
});

View File

@@ -0,0 +1,228 @@
use std::fmt::Debug;
use std::str::FromStr;
/// A Python integer literal. Represents both small (fits in an `i64`) and large integers.
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct Int(Number);
impl FromStr for Int {
type Err = std::num::ParseIntError;
/// Parse an [`Int`] from a string.
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.parse::<i64>() {
Ok(value) => Ok(Int::small(value)),
Err(err) => {
if matches!(
err.kind(),
std::num::IntErrorKind::PosOverflow | std::num::IntErrorKind::NegOverflow
) {
Ok(Int::big(s))
} else {
Err(err)
}
}
}
}
}
impl Int {
pub const ZERO: Int = Int(Number::Small(0));
pub const ONE: Int = Int(Number::Small(1));
/// Create an [`Int`] to represent a value that can be represented as an `i64`.
fn small(value: i64) -> Self {
Self(Number::Small(value))
}
/// Create an [`Int`] to represent a value that cannot be represented as an `i64`.
fn big(value: impl Into<Box<str>>) -> Self {
Self(Number::Big(value.into()))
}
/// Parse an [`Int`] from a string with a given radix.
pub fn from_str_radix(s: &str, radix: u32) -> Result<Self, std::num::ParseIntError> {
match i64::from_str_radix(s, radix) {
Ok(value) => Ok(Int::small(value)),
Err(err) => {
if matches!(
err.kind(),
std::num::IntErrorKind::PosOverflow | std::num::IntErrorKind::NegOverflow
) {
Ok(Int::big(s))
} else {
Err(err)
}
}
}
}
/// Return the [`Int`] as an u8, if it can be represented as that data type.
pub fn as_u8(&self) -> Option<u8> {
match &self.0 {
Number::Small(small) => u8::try_from(*small).ok(),
Number::Big(_) => None,
}
}
/// Return the [`Int`] as an u16, if it can be represented as that data type.
pub fn as_u16(&self) -> Option<u16> {
match &self.0 {
Number::Small(small) => u16::try_from(*small).ok(),
Number::Big(_) => None,
}
}
/// Return the [`Int`] as an u32, if it can be represented as that data type.
pub fn as_u32(&self) -> Option<u32> {
match &self.0 {
Number::Small(small) => u32::try_from(*small).ok(),
Number::Big(_) => None,
}
}
/// Return the [`Int`] as an i8, if it can be represented as that data type.
pub fn as_i8(&self) -> Option<i8> {
match &self.0 {
Number::Small(small) => i8::try_from(*small).ok(),
Number::Big(_) => None,
}
}
/// Return the [`Int`] as an i16, if it can be represented as that data type.
pub fn as_i16(&self) -> Option<i16> {
match &self.0 {
Number::Small(small) => i16::try_from(*small).ok(),
Number::Big(_) => None,
}
}
/// Return the [`Int`] as an i32, if it can be represented as that data type.
pub fn as_i32(&self) -> Option<i32> {
match &self.0 {
Number::Small(small) => i32::try_from(*small).ok(),
Number::Big(_) => None,
}
}
/// Return the [`Int`] as an i64, if it can be represented as that data type.
pub const fn as_i64(&self) -> Option<i64> {
match &self.0 {
Number::Small(small) => Some(*small),
Number::Big(_) => None,
}
}
}
impl std::fmt::Display for Int {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl Debug for Int {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
impl PartialEq<u8> for Int {
fn eq(&self, other: &u8) -> bool {
self.as_u8() == Some(*other)
}
}
impl PartialEq<u16> for Int {
fn eq(&self, other: &u16) -> bool {
self.as_u16() == Some(*other)
}
}
impl PartialEq<u32> for Int {
fn eq(&self, other: &u32) -> bool {
self.as_u32() == Some(*other)
}
}
impl PartialEq<i8> for Int {
fn eq(&self, other: &i8) -> bool {
self.as_i8() == Some(*other)
}
}
impl PartialEq<i16> for Int {
fn eq(&self, other: &i16) -> bool {
self.as_i16() == Some(*other)
}
}
impl PartialEq<i32> for Int {
fn eq(&self, other: &i32) -> bool {
self.as_i32() == Some(*other)
}
}
impl PartialEq<i64> for Int {
fn eq(&self, other: &i64) -> bool {
self.as_i64() == Some(*other)
}
}
impl From<u8> for Int {
fn from(value: u8) -> Self {
Self::small(i64::from(value))
}
}
impl From<u16> for Int {
fn from(value: u16) -> Self {
Self::small(i64::from(value))
}
}
impl From<u32> for Int {
fn from(value: u32) -> Self {
Self::small(i64::from(value))
}
}
impl From<i8> for Int {
fn from(value: i8) -> Self {
Self::small(i64::from(value))
}
}
impl From<i16> for Int {
fn from(value: i16) -> Self {
Self::small(i64::from(value))
}
}
impl From<i32> for Int {
fn from(value: i32) -> Self {
Self::small(i64::from(value))
}
}
impl From<i64> for Int {
fn from(value: i64) -> Self {
Self::small(value)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Number {
/// A "small" number that can be represented as an `i64`.
Small(i64),
/// A "large" number that cannot be represented as an `i64`.
Big(Box<str>),
}
impl std::fmt::Display for Number {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Number::Small(value) => write!(f, "{value}"),
Number::Big(value) => write!(f, "{value}"),
}
}
}

View File

@@ -1,6 +1,7 @@
use std::path::Path;
pub use expression::*;
pub use int::*;
pub use nodes::*;
pub mod all;
@@ -12,6 +13,7 @@ pub mod hashable;
pub mod helpers;
pub mod identifier;
pub mod imports;
mod int;
pub mod node;
mod nodes;
pub mod parenthesize;

View File

@@ -1,12 +1,12 @@
#![allow(clippy::derive_partial_eq_without_eq)]
use itertools::Itertools;
use std::fmt;
use std::fmt::Debug;
use std::ops::Deref;
use num_bigint::BigInt;
use crate::int;
use ruff_text_size::{Ranged, TextRange, TextSize};
/// See also [mod](https://docs.python.org/3/library/ast.html#ast.mod)
@@ -466,7 +466,7 @@ pub struct StmtImportFrom {
pub range: TextRange,
pub module: Option<Identifier>,
pub names: Vec<Alias>,
pub level: Option<Int>,
pub level: Option<u32>,
}
impl From<StmtImportFrom> for Stmt {
@@ -2578,42 +2578,13 @@ impl Ranged for Identifier {
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Int(u32);
impl Int {
pub fn new(i: u32) -> Self {
Self(i)
}
pub fn to_u32(&self) -> u32 {
self.0
}
pub fn to_usize(&self) -> usize {
self.0 as _
}
}
impl std::cmp::PartialEq<u32> for Int {
#[inline]
fn eq(&self, other: &u32) -> bool {
self.0 == *other
}
}
impl std::cmp::PartialEq<usize> for Int {
#[inline]
fn eq(&self, other: &usize) -> bool {
self.0 as usize == *other
}
}
#[derive(Clone, Debug, PartialEq, is_macro::Is)]
pub enum Constant {
None,
Bool(bool),
Str(StringConstant),
Bytes(BytesConstant),
Int(BigInt),
Int(int::Int),
Float(f64),
Complex { real: f64, imag: f64 },
Ellipsis,

View File

@@ -577,7 +577,9 @@ impl<'a> Generator<'a> {
statement!({
self.p("from ");
if let Some(level) = level {
self.p(&".".repeat(level.to_usize()));
for _ in 0..*level {
self.p(".");
}
}
if let Some(module) = module {
self.p_id(module);

View File

@@ -30,6 +30,7 @@ memchr = { workspace = true }
once_cell = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true, optional = true }
schemars = { workspace = true, optional = true }
smallvec = { workspace = true }
static_assertions = { workspace = true }
thiserror = { workspace = true }
@@ -52,4 +53,5 @@ required-features = ["serde"]
[features]
serde = ["dep:serde", "ruff_formatter/serde", "ruff_source_file/serde", "ruff_python_ast/serde"]
default = ["serde"]
schemars = ["dep:schemars", "ruff_formatter/schemars"]
default = []

View File

@@ -1,18 +1,18 @@
[
{
"indent_style": "Space",
"indent_style": "space",
"indent_width": 4
},
{
"indent_style": "Space",
"indent_style": "space",
"indent_width": 2
},
{
"indent_style": "Tab",
"indent_style": "tab",
"indent_width": 8
},
{
"indent_style": "Tab",
"indent_style": "tab",
"indent_width": 4
}
]

View File

@@ -366,7 +366,7 @@ if (
):
pass
z = (
z = (
a
+
# a: extracts this comment
@@ -377,7 +377,7 @@ if (
x and y
)
)
)
)
z = (
(

View File

@@ -52,3 +52,20 @@ a = (
aaaaaaaaaaaaaaaaaaaaa = (
o for o in self.registry.values if o.__class__ is not ModelAdmin
)
# Regression test for: https://github.com/astral-sh/ruff/issues/7623
tuple(
0 # comment
for x in y
)
tuple(
(0 # comment
for x in y)
)
tuple(
( # comment
0 for x in y
)
)

View File

@@ -1,10 +1,10 @@
[
{
"indent_style": "Space",
"indent_style": "space",
"indent_width": 4
},
{
"indent_style": "Space",
"indent_style": "space",
"indent_width": 2
}
]

View File

@@ -1,13 +1,13 @@
[
{
"indent_style": "Space",
"indent_style": "space",
"indent_width": 4
},
{
"indent_style": "Space",
"indent_style": "space",
"indent_width": 1
},
{
"indent_style": "Tab"
"indent_style": "tab"
}
]

View File

@@ -1,13 +1,13 @@
[
{
"indent_style": "Space",
"indent_style": "space",
"indent_width": 4
},
{
"indent_style": "Space",
"indent_style": "space",
"indent_width": 2
},
{
"indent_style": "Tab"
"indent_style": "tab"
}
]

View File

@@ -0,0 +1,6 @@
# Regression test for: https://github.com/astral-sh/ruff/issues/7624
if symbol is not None:
request["market"] = market["id"]
# "remaining_volume": "0.0",
else:
pass

View File

@@ -0,0 +1,161 @@
###
# Blank lines around functions
###
x = 1
# comment
def f():
pass
if True:
x = 1
# comment
def f():
pass
x = 1
# comment
def f():
pass
x = 1
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
# comment
def f():
pass
# comment
def f():
pass
# comment
###
# Blank lines around imports.
###
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x # comment
# comment
import y
def f(): pass # comment
# comment
x = 1
def f():
pass
# comment
x = 1

View File

@@ -376,3 +376,11 @@ def f( # first
def this_is_unusual() -> (please := no): ...
def this_is_unusual(x) -> (please := no): ...
# Regression test for: https://github.com/astral-sh/ruff/issues/7465
try:
def test():
pass
#comment
except ImportError:
pass

View File

@@ -104,6 +104,185 @@ if True: print("a") # 1
elif True: print("b") # 2
else: print("c") # 3
# Regression test for: https://github.com/astral-sh/ruff/issues/7465
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass # comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
if True:
pass
# comment
else:
pass
else:
pass
if True:
if True:
pass
# comment
else:
pass
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
if True:
pass
else:
pass
# a
# b
# c
else:
pass
if True:
if True:
pass
else:
pass
# b
# c
else:
pass
# Regression test for: https://github.com/astral-sh/ruff/issues/7602
if True:
if True:
if True:
pass
#a
#b
#c
else:
pass
if True:
if True:
if True:
pass
# b
# a
# c
else:
pass
# Same indent
if True:
if True:
if True:
pass
#a
#b
#c
else:
pass
if True:
if True:
if True:
pass
# a
# b
# c
else:
pass
# Regression test for https://github.com/astral-sh/ruff/issues/5337
if parent_body:
if current_body:

View File

@@ -56,3 +56,23 @@ def func():
x = 1
# Regression test for: https://github.com/astral-sh/ruff/issues/7604
import os
# Defaults for arguments are defined here
# args.threshold = None;
logger = logging.getLogger("FastProject")
# Regression test for: https://github.com/astral-sh/ruff/issues/7604
import os
# comment
# comment
# comment
x = 1

View File

@@ -2,17 +2,16 @@
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use anyhow::{format_err, Context, Result};
use clap::{command, Parser, ValueEnum};
use ruff_formatter::SourceCode;
use ruff_python_index::CommentRangesBuilder;
use ruff_python_parser::lexer::lex;
use ruff_python_parser::{parse_tokens, Mode};
use ruff_python_index::tokens_and_ranges;
use ruff_python_parser::{parse_ok_tokens, Mode};
use ruff_text_size::Ranged;
use crate::comments::collect_comments;
use crate::{format_node, PyFormatOptions};
use crate::{format_module_ast, PyFormatOptions};
#[derive(ValueEnum, Clone, Debug)]
pub enum Emit {
@@ -39,36 +38,25 @@ pub struct Cli {
pub print_comments: bool,
}
pub fn format_and_debug_print(input: &str, cli: &Cli, source_type: &Path) -> Result<String> {
let mut tokens = Vec::new();
let mut comment_ranges = CommentRangesBuilder::default();
for result in lex(input, Mode::Module) {
let (token, range) = match result {
Ok((token, range)) => (token, range),
Err(err) => bail!("Source contains syntax errors {err:?}"),
};
comment_ranges.visit_token(&token, range);
tokens.push(Ok((token, range)));
}
let comment_ranges = comment_ranges.finish();
pub fn format_and_debug_print(source: &str, cli: &Cli, source_type: &Path) -> Result<String> {
let (tokens, comment_ranges) = tokens_and_ranges(source)
.map_err(|err| format_err!("Source contains syntax errors {err:?}"))?;
// Parse the AST.
let python_ast =
parse_tokens(tokens, Mode::Module, "<filename>").context("Syntax error in input")?;
let module =
parse_ok_tokens(tokens, Mode::Module, "<filename>").context("Syntax error in input")?;
let options = PyFormatOptions::from_extension(source_type);
let formatted = format_node(&python_ast, &comment_ranges, input, options)
let source_code = SourceCode::new(source);
let formatted = format_module_ast(&module, &comment_ranges, source, options)
.context("Failed to format node")?;
if cli.print_ir {
println!("{}", formatted.document().display(SourceCode::new(input)));
println!("{}", formatted.document().display(source_code));
}
if cli.print_comments {
// Print preceding, following and enclosing nodes
let source_code = SourceCode::new(input);
let decorated_comments = collect_comments(&python_ast, source_code, &comment_ranges);
let decorated_comments = collect_comments(&module, source_code, &comment_ranges);
if !decorated_comments.is_empty() {
println!("# Comment decoration: Range, Preceding, Following, Enclosing, Comment");
}
@@ -86,13 +74,10 @@ pub fn format_and_debug_print(input: &str, cli: &Cli, source_type: &Path) -> Res
comment.enclosing_node().kind(),
comment.enclosing_node().range()
),
comment.slice().text(SourceCode::new(input)),
comment.slice().text(source_code),
);
}
println!(
"{:#?}",
formatted.context().comments().debug(SourceCode::new(input))
);
println!("{:#?}", formatted.context().comments().debug(source_code));
}
Ok(formatted
.print()

View File

@@ -86,17 +86,42 @@ impl Format<PyFormatContext<'_>> for FormatLeadingAlternateBranchComments<'_> {
if let Some(first_leading) = self.comments.first() {
// Leading comments only preserves the lines after the comment but not before.
// Insert the necessary lines.
if lines_before(first_leading.start(), f.context().source()) > 1 {
write!(f, [empty_line()])?;
}
write!(
f,
[empty_lines(lines_before(
first_leading.start(),
f.context().source()
))]
)?;
write!(f, [leading_comments(self.comments)])?;
} else if let Some(last_preceding) = self.last_node {
// The leading comments formatting ensures that it preserves the right amount of lines after
// We need to take care of this ourselves, if there's no leading `else` comment.
if lines_after_ignoring_trivia(last_preceding.end(), f.context().source()) > 1 {
write!(f, [empty_line()])?;
}
// The leading comments formatting ensures that it preserves the right amount of lines
// after We need to take care of this ourselves, if there's no leading `else` comment.
// Since the `last_node` could be a compound node, we need to skip _all_ trivia.
//
// For example, here, when formatting the `if` statement, the `last_node` (the `while`)
// would end at the end of `pass`, but we want to skip _all_ comments:
// ```python
// if True:
// while True:
// pass
// # comment
//
// # comment
// else:
// ...
// ```
//
// `lines_after_ignoring_trivia` is safe here, as we _know_ that the `else` doesn't
// have any leading comments.
write!(
f,
[empty_lines(lines_after_ignoring_trivia(
last_preceding.end(),
f.context().source()
))]
)?;
}
Ok(())
@@ -299,7 +324,14 @@ impl Format<PyFormatContext<'_>> for FormatEmptyLines {
NodeLevel::TopLevel => match self.lines {
0 | 1 => write!(f, [hard_line_break()]),
2 => write!(f, [empty_line()]),
_ => write!(f, [empty_line(), empty_line()]),
_ => match f.options().source_type() {
PySourceType::Stub => {
write!(f, [empty_line()])
}
PySourceType::Python | PySourceType::Ipynb => {
write!(f, [empty_line(), empty_line()])
}
},
},
NodeLevel::CompoundStatement => match self.lines {

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