Compare commits

...

70 Commits

Author SHA1 Message Date
Charlie Marsh
8d9912a83a Bump version to v0.1.7 (#8999) 2023-12-04 16:28:23 -05:00
Charlie Marsh
93258e8d5b Default max-positional-args to max-args (#8998) 2023-12-04 19:02:10 +00:00
Philipp A
b90027d037 [pylint] Implement too-many-positional (PLR0917) (#8995)
## Summary

Adds a rule that bans too many positional (i.e. not keyword-only)
parameters in function definitions.

Fixes https://github.com/astral-sh/ruff/issues/8946

Rule ID code taken from https://github.com/pylint-dev/pylint/pull/9278

## Test Plan
1. fixtures file checking multiple OKs/fails
2. parametrized test file
2023-12-04 18:03:09 +00:00
Dhruv Manilawala
060a25df09 Rename semantic model flag LITERAL to TYPING_LITERAL (#8997)
This PR renames the semantic model flag `LITERAL` to `TYPING_LITERAL` to
better reflect its purpose. The main motivation behind this change is to
avoid any confusion with the "literal" terminology used in the AST for
literal nodes like string, bytes, numbers, etc.
2023-12-04 11:28:09 -06:00
dependabot[bot]
f5d4676c13 Bump ureq from 2.8.0 to 2.9.1 (#8993) 2023-12-04 09:53:25 -06:00
dependabot[bot]
df69dc9f8d Bump url from 2.4.1 to 2.5.0 (#8994) 2023-12-04 10:16:39 -05:00
dependabot[bot]
cb6c37abd9 Bump js-sys from 0.3.65 to 0.3.66 (#8992) 2023-12-04 19:50:08 +09:00
dependabot[bot]
54de990621 Bump fs-err from 2.10.0 to 2.11.0 (#8991) 2023-12-04 19:49:34 +09:00
dependabot[bot]
b91b09b961 Bump schemars from 0.8.15 to 0.8.16 (#8990)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-04 09:24:53 +00:00
Micha Reiser
0bda1913d1 Create dedicated is_*_enabled functions for each preview style (#8988) 2023-12-04 05:38:54 +00:00
Micha Reiser
7e390d3772 Move ParenthesizedExpr to ruff_python_parser (#8987) 2023-12-04 05:36:28 +00:00
Micha Reiser
0bf0aa28ac Inline trailing comments for type alias similar to assignments (#8941) 2023-12-04 05:27:04 +00:00
Micha Reiser
8088c5367a Refactor the comment handling of a statement's last expression (#8920) 2023-12-04 05:12:12 +00:00
Charlie Marsh
6fe8f8a272 Avoid unstable formatting in ellipsis-only body with trailing comment (#8984)
## Summary

We should avoid inlining the ellipsis in:

```python
def h():
    ...
    # bye
```

Just as we omit the ellipsis in:

```python
def h():
    # bye
    ...
```

Closes https://github.com/astral-sh/ruff/issues/8905.
2023-12-03 19:15:40 -05:00
Charlie Marsh
bfae1f1412 Convert over-indentation rule to use number of characters (#8983)
Closes https://github.com/astral-sh/ruff/issues/8978.
2023-12-03 20:45:30 +00:00
Charlie Marsh
b358cbf398 Fix start >= end error in over-indentation (#8982)
Closes https://github.com/astral-sh/ruff/issues/8977.
2023-12-03 20:19:43 +00:00
Charlie Marsh
17c8817695 Avoid off-by-one error in stripping noqa following multi-byte char (#8979)
Closes https://github.com/astral-sh/ruff/issues/8976.
2023-12-03 11:01:58 -05:00
Charlie Marsh
1dda669f9a Avoid syntax error via invalid ur string prefix (#8971)
## Summary

If a string has a Unicode prefix, we can't add the `r` prefix on top of
that -- we need to remove and replace it. (The Unicode prefix is
redundant anyway in Python 3.)

Closes https://github.com/astral-sh/ruff/issues/8967.
2023-12-02 18:37:49 +00:00
Tom Kuson
3fbabfe126 [flake8-pyi] Check PEP 695 type aliases for snake-case-type-alias and t-suffixed-type-alias (#8966)
## Summary

Check PEP 695 type alias definitions for `snake-case-type-alias`
(`PYI042`) and `t-suffixed-type-alias` (`PYI043`)

Related to #8771.

## Test Plan

`cargo test`
2023-12-02 13:26:43 -05:00
Charlie Marsh
20ab14e354 Avoid unnecessary index diagnostics when value is modified (#8970)
Closes https://github.com/astral-sh/ruff/issues/8969.
2023-12-02 18:17:17 +00:00
Charlie Marsh
22d8a989d4 Avoid underflow in get_model matching (#8965)
Closes https://github.com/astral-sh/ruff/issues/8962.
2023-12-02 13:56:57 +00:00
Tom Kuson
35082b28cd Fix error in t-suffixed-type-alias (PYI043) example (#8963)
## Summary

For `t-suffixed-type-alias` to trigger, the type alias needs to be
marked as such using the `typing.TypeAlias` annotation and the name of
the alias must be marked as private using a leading underscore. The
documentation example was of an unannotated type alias that was not
marked as private, which was misleading.

## Test Plan

The current example doesn't trigger the rule; the example in this merge
request does.
2023-12-02 08:52:50 -05:00
Micha Reiser
5aaf99b856 Implement the fix_power_op_line_length preview style (#8947) 2023-12-02 09:35:34 +09:00
Charlie Marsh
58bf6f5762 Remove todo branches from control-flow graph (#8960) 2023-12-01 23:46:50 +00:00
Charlie Marsh
277cd80175 Add erroneous for-loop test case for CFG (#8957) 2023-12-01 23:11:42 +00:00
Charlie Marsh
20a40771a5 Consider more wildcards in control flow graph matches (#8956) 2023-12-01 17:58:32 -05:00
Michael Essiet
4af3f43e5e Added the command to run ruff using pkgx to the installation.md (#8955)
## Summary

This PR adds the command to run ruff using [pkgx](https://pkgx.sh).

<!-- What's the purpose of the change? What does it do, and why? -->

It's just showing that ruff is supported in one more package manager.

## Test Plan

You can run `pkgx ruff` if you have pkgx installed or run `sh <(curl
https://pkgx.sh) +github.com/charliermarsh/ruff sh
`
2023-12-01 20:43:01 +00:00
Andrew Gallant
0b1a36f8c8 ruff_python_formatter: light refactoring of code snippet formatting in docstrings (#8950)
In the source of working on #8859, I made a number of smallish refactors
to how code snippet formatting works. Most or all of these were
motivated by writing in support for reStructuredText blocks. They have
some fundamentally different requirements than doctests, and there are a
lot more ways for reStructuredText blocks to become invalid.

(Commit-by-commit review is recommended as the commit messages provide
further context on each change. I split this off from ongoing work to
make review more manageable.)
2023-12-01 14:46:39 -05:00
qdegraaf
64c2535e28 [pylint] Add add_argument utility and autofix for PLW1514 (#8928)
## Summary

- Adds `add_argument` similar to existing `remove_argument` utility to
safely add arguments to functions.
- Adds autofix for `PLW1514` as per specs requested in
https://github.com/astral-sh/ruff/issues/8883 as a test

## Test Plan

Checks on existing fixtures as well as additional test and fixture for
Python 3.9 and lower fix

## Issue Link

Closes: https://github.com/astral-sh/ruff/issues/8883
2023-12-01 18:23:56 +00:00
Charlie Marsh
5510a6131e Ignore @overload and @override methods for too-many-arguments checks (#8954)
Closes https://github.com/astral-sh/ruff/issues/8945.
2023-12-01 18:22:53 +00:00
Charlie Marsh
e5db72459e Detect implicit returns in auto-return-types (#8952)
## Summary

Adds detection for branches without a `return` or `raise`, so that we
can properly `Optional` the return types. I'd like to remove this and
replace it with our code graph analysis from the `unreachable.rs` rule,
but it at least fixes the worst offenders.

Closes #8942.
2023-12-01 12:35:01 -05:00
Tom Kuson
d66063bb33 [flake8-pyi] Check for kwarg and vararg NoReturn type annotations (#8948)
## Summary

Triggers `no-return-argument-annotation-in-stub` (`PYI050`) for vararg
and kwarg `NoReturn` type annotations.

Related to #8771.

## Test Plan

`cargo test`
2023-12-01 12:18:52 -05:00
Micha Reiser
506be68782 Enable Preview mode for formatter benchmarks (#8944) 2023-12-01 10:02:59 +00:00
Steve C
cb1d3df085 [pylint] Implement unnecessary-dict-index-lookup (PLR1733) (#8036)
## Summary

Add
[R1733](https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/unnecessary-dict-index-lookup.html)
and autofix!

See #970 

## Test Plan

`cargo test` and manually
2023-12-01 05:09:50 +00:00
Charlie Marsh
69dfe0a207 Fix doc formatting for zero-sleep-call (#8937) 2023-11-30 22:34:09 -05:00
Charlie Marsh
46a174a22e Use full arguments range for zero-sleep-call (#8936) 2023-12-01 03:09:18 +00:00
Charlie Marsh
912c39ce2a Add support for @functools.singledispatch (#8934)
## Summary

When a function uses `@functools.singledispatch`, we need to treat the
first argument of any implementations as runtime-required.

Closes https://github.com/astral-sh/ruff/issues/6849.
2023-12-01 03:04:58 +00:00
Charlie Marsh
b2638c62a5 Update formatter fixtures (#8935)
I merged a branch that wasn't up-to-date, which left us with test
failures on `main`.
2023-12-01 02:57:05 +00:00
Charlie Marsh
eaa310429f Insert trailing comma when function breaks with single argument (#8921)
## Summary

Given:

```python
def _example_function_xxxxxxx(
    variable: Optional[List[str]]
) -> List[example.ExampleConfig]:
    pass
```

We should be inserting a trailing comma after the argument (as long as
it's a single-argument function). This was an inconsistency with Black,
but also led to some internal inconsistencies, whereby we added the
comma if the argument contained a trailing end-of-line comment, but not
otherwise.

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

## Test Plan

`cargo test`

Before:

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

|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75804 | 1799 | 1648 |
| django | 0.99984 | 2772 | 34 |
| home-assistant | 0.99963 | 10596 | 146 |
| poetry | 0.99925 | 317 | 12 |
| transformers | 0.99967 | 2657 | 322 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99980 | 3669 | 18 |
| warehouse | 0.99977 | 654 | 13 |
| zulip | 0.99970 | 1459 | 21 |

After:

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

|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75804 | 1799 | 1648 |
| django | 0.99984 | 2772 | 34 |
| home-assistant | 0.99955 | 10596 | 213 |
| poetry | 0.99917 | 317 | 13 |
| transformers | 0.99967 | 2657 | 324 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99980 | 3669 | 18 |
| warehouse | 0.99976 | 654 | 14 |
| zulip | 0.99957 | 1459 | 36 |
2023-11-30 21:49:28 -05:00
Charlie Marsh
019d9aebe9 Implement multiline dictionary and list hugging for preview style (#8293)
## Summary

This PR implement's Black's new single-argument hugging for lists, sets,
and dictionaries under preview style.

For example, this:

```python
foo(
    [
        1,
        2,
        3,
    ]
)
```

Would instead now be formatted as:

```python
foo([
    1,
    2,
    3,
])
```

A couple notes:

- This doesn't apply when the argument has a magic trailing comma.
- This _does_ apply when the argument is starred or double-starred.
- We don't apply this when there are comments before or after the
argument, though Black does in some cases (and moves the comments
outside the call parentheses).

It doesn't say it in the originating PR
(https://github.com/psf/black/pull/3964), but I think this also applies
to parenthesized expressions? At least, it does in my testing of preview
vs. stable, though it's possible that behavior predated the linked PR.

See: #8279.

## Test Plan

Before:

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

|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75804 | 1799 | 1648 |
| django | 0.99984 | 2772 | 34 |
| home-assistant | 0.99963 | 10596 | 146 |
| poetry | 0.99925 | 317 | 12 |
| transformers | 0.99967 | 2657 | 322 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99980 | 3669 | 18 |
| warehouse | 0.99977 | 654 | 13 |
| zulip | 0.99970 | 1459 | 21 |

After:

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

|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75804 | 1799 | 1648 |
| django | 0.99984 | 2772 | 34 |
| home-assistant | 0.99963 | 10596 | 146 |
| poetry | 0.96215 | 317 | 34 |
| transformers | 0.99967 | 2657 | 322 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99980 | 3669 | 18 |
| warehouse | 0.99977 | 654 | 13 |
| zulip | 0.99970 | 1459 | 21 |
2023-11-30 21:11:14 -05:00
Dhruv Manilawala
f06c5dc896 Use correct range for TRIO115 fix (#8933)
## Summary

This PR fixes the bug where the autofix for `TRIO115` was taking the
entire arguments range for the fix which included the parenthesis as
well. This means that the fix would remove the arguments and the
parenthesis. The fix is to use the correct range.

fixes: #8713 

## Test Plan

Update existing snapshots :)
2023-12-01 01:42:46 +00:00
Charlie Marsh
c1dc4a60be Apply some minor changes to unnecessary-list-index-lookup (#8932)
## Summary

I was late in reviewing this but found a few things I wanted to tweak.
No functional changes.
2023-12-01 00:53:26 +00:00
Steve C
70febb1862 [pylint] - add unnecessary-list-index-lookup (PLR1736) + autofix (#7999)
## Summary

Add
[R1736](https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/unnecessary-list-index-lookup.html)
along with the autofix

See #970 

## Test Plan

`cargo test` and manually

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
2023-11-30 17:45:12 -06:00
Steve C
4212b41796 [pylint] - implement R0202 and R0203 with autofixes (#8335)
## Summary

Implements
[`no-classmethod-decorator`/`R0202`](https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/no-classmethod-decorator.html)
and
[`no-staticmethod-decorator`/`R0203`](https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/no-staticmethod-decorator.html)
with autofixes.

They're similar enough that all code is reusable for both.

See: #970 

## Test Plan

`cargo test`
2023-11-30 16:18:09 -06:00
Steve C
bbad4b4c93 Add autofix for PYI030 (#7934)
## Summary

Part 2 of implementing the reverted autofix for `PYI030`

Also handles `typing.Union` and `typing_extensions.Literal` etc, uses
the first subscript name it finds for each offensive line.

## Test Plan

<!-- How was it tested? -->

`cargo test` and manually

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
2023-11-30 22:16:57 +00:00
Steve C
3ee1ec70cc Fix up some types in the ecosystem code (#8898)
## Summary

Fixes up the type annotations to make type analyzers a little happier 😄 

## Test Plan

N/A
2023-11-30 16:02:20 -06:00
Charlie Marsh
ee5d95f751 Remove duplicate imports from os-stat documentation (#8930)
Closes https://github.com/astral-sh/ruff/issues/8799.
2023-11-30 20:13:29 +00:00
Charlie Marsh
d674e7946d Ignore underlines when determining docstring logical lines (#8929) 2023-11-30 14:27:04 -05:00
Charlie Marsh
20782ab02c Support type alias statements in simple statement positions (#8916)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

Our `SoftKeywordTokenizer` only respected soft keywords in compound
statement positions -- for example, at the start of a logical line:

```python
type X = int
```

However, type aliases can also appear in simple statement positions,
like:

```python
class Class: type X = int
```

(Note that `match` and `case` are _not_ valid keywords in such
positions.)

This PR upgrades the tokenizer to track both kinds of valid positions.

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

## Test Plan

`cargo test`
2023-11-30 19:15:19 +00:00
Charlie Marsh
073eddb1d9 Use Python version to determine typing rewrite safety (#8919)
## Summary

These rewrites are only (potentially) unsafe on Python versions that
predate their introduction into the standard library and grammar, so it
seems correct to mark them as safe on those later versions.
2023-11-29 22:22:04 -05:00
Charlie Marsh
e8da95d09c Document fix safety for flake8-comprehensions and some pyupgrade rules (#8918)
See: https://github.com/astral-sh/ruff/issues/7993.
2023-11-29 20:51:23 -05:00
Charlie Marsh
c324cb6202 [pep8-naming] Allow Django model loads in non-lowercase-variable-in-function (N806) (#8917)
## Summary

Allows assignments of the form, e.g., `Attachment =
apps.get_model("zerver", "Attachment")`, for better compatibility with
Django.

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

## Test Plan

`cargo test`
2023-11-29 20:43:40 -05:00
Charlie Marsh
774c77adae Avoid off-by-one error in with-item named expressions (#8915)
## Summary

Given `with (a := b): pass`, we truncate the `WithItem` range by one on
both sides such that the parentheses are part of the statement, rather
than the item. However, for `with (a := b) as x: pass`, we want to avoid
this trick.

Closes https://github.com/astral-sh/ruff/issues/8913.
2023-11-30 00:11:04 +00:00
Micha Reiser
fd70cd789f Update Black tests (#8901) 2023-11-30 00:09:55 +00:00
Andrey
08f3110f1e Optimize workflow run (#8225) 2023-11-30 00:09:33 +00:00
Micha Reiser
2314b9aaca Add benchmark for running all rules including preview rules (#8865) 2023-11-29 04:26:25 +00:00
Micha Reiser
cddc696896 Stop at the first resolved parent configuration (#8864) 2023-11-29 04:21:07 +00:00
Charlie Marsh
6435e4e4aa Enable auto-return-type involving Optional and Union annotations (#8885)
## Summary

Previously, this was only supported for Python 3.10 and later, since we
always use the PEP 604-style unions.
2023-11-28 18:35:55 -08:00
Dhruv Manilawala
ec7456bac0 Rename as_str to to_str (#8886)
This PR renames the method on `StringLiteralValue` from `as_str` to
`to_str`. The main motivation is to follow the naming convention as
described in the [Rust API
Guidelines](https://rust-lang.github.io/api-guidelines/naming.html#ad-hoc-conversions-follow-as_-to_-into_-conventions-c-conv).
This method can perform a string allocation in case the string is
implicitly concatenated.
2023-11-28 18:50:42 -06:00
Dhruv Manilawala
b28556d739 Update E402 to work at cell level for notebooks (#8872)
## Summary

This PR updates the `E402` rule to work at cell level for Jupyter
notebooks. This is enabled only in preview to gather feedback.

The implementation basically resets the import boundary flag on the
semantic model when we encounter the first statement in a cell.

Another potential solution is to introduce `E403` rule that is
specifically for notebooks that works at cell level while `E402` will be
disabled for notebooks.

## Test Plan

Add a notebook with imports in multiple cells and verify that the rule
works as expected.

resolves: #8669
2023-11-29 00:32:35 +00:00
Andrew Gallant
4957d94beb ruff_python_formatter: small cleanups in doctest formatting (#8871)
This PR contains a few small clean-ups that are responses to
@MichaReiser's review of my #8811 PR.
2023-11-28 18:43:07 -05:00
Charlie Marsh
5d554edace Allow booleans in @override methods (#8882)
Closes #8867.
2023-11-28 13:42:31 -08:00
Charlie Marsh
412688826c Avoid filtering out un-representable types in return annotation (#8881)
## Summary

Given `Union[Dict, None]` (in our internal representation), we were
filtering out `Dict` since we treat it as un-representable (i.e., we
can't convert it to an expression), returning just `None` as the type
annotation. We should require that all members of the union are
representable.

Closes https://github.com/astral-sh/ruff/issues/8879.
2023-11-28 21:10:42 +00:00
Dhruv Manilawala
47d80f29a7 Lexer start of line is false only for Mode::Expression (#8880)
## Summary

This PR fixes the bug in the lexer where the `Mode::Ipython` wasn't
being considered when initializing the soft keyword transformer which
wraps the lexer. This means that if the source code starts with either
`match` or `type` keyword, then the keywords were being considered as
name tokens instead. For example,

```python
match foo:
    case bar:
        pass
```

This would transform the `match` keyword into an identifier if the mode
is `Ipython`.

The fix is to reverse the condition in the soft keyword initializer so
that any new modes are by default considered as the lexer being at start
of line.

## Test Plan

Add a new test case for `Mode::Ipython` and verify the snapshot.

fixes: #8870
2023-11-28 20:38:25 +00:00
Dhruv Manilawala
9dee1883ce Update ruff-dev to use SourceKind (#8878)
Just a small quality of life improvement to be able to pass in the
Jupyter Notebook to `ruff-dev` CLI.
2023-11-28 14:27:35 -06:00
Andrew Gallant
f585e3e2dc remove several uses of unsafe (#8600)
This PR removes several uses of `unsafe`. I generally limited myself to
low hanging fruit that I could see. There are still a few remaining uses
of `unsafe` that looked a bit more difficult to remove (if possible at
all). But this gets rid of a good chunk of them.

I put each `unsafe` removal into its own commit with a justification for
why I did it. So I would encourage reviewing this PR commit-by-commit.
That way, we can legislate them independently. It's no problem to drop a
commit if we feel the `unsafe` should stay in that case.
2023-11-28 09:50:03 -05:00
Joffrey Bluthé
578ddf1bb1 [isort] Add support for length-sort settings (#8841)
## Summary

Closes #1567.

Add both `length-sort` and `length-sort-straight` settings for isort.

Here are a few notable points:
- The length is determined using the
[`unicode_width`](https://crates.io/crates/unicode-width) crate, i.e. we
are talking about displayed length (this is explicitly mentioned in the
description of the setting)
- The dots are taken into account in the length to be compatible with
the original isort
- I had to reorder a few fields of the module key struct for it all to
make sense (notably the `force_to_top` field is now the first one)

## Test Plan

I added tests for the following cases:
- Basic tests for length-sort with ASCII characters only
- Tests with non-ASCII characters
- Tests with relative imports
- Tests for length-sort-straight
2023-11-28 06:00:37 +00:00
Charlie Marsh
ed14fd9163 [pydocstyle] Avoid non-character breaks in over-indentation (D208) (#8866)
Closes https://github.com/astral-sh/ruff/issues/8844.
2023-11-27 21:47:35 -08:00
Tom Kuson
60eb11fa50 [refurb] Implement redundant-log-base (FURB163) (#8842)
## Summary

Implement
[`simplify-math-log`](https://github.com/dosisod/refurb/blob/master/refurb/checks/math/simplify_log.py)
as `redundant-log-base` (`FURB163`).

Auto-fixes

```python
import math

math.log(2, 2)
```

to

```python
import math

math.log2(2)
```

Related to #1348.

## Test Plan

`cargo test`
2023-11-27 23:57:00 +00:00
Andrew Gallant
33caa2ab1c ruff_python_formatter: move docstring handling to a submodule (#8861)
This turns `string` into a parent module with a `docstring` sub-module.
I arranged things this way because there are parts of the `string`
module that the `docstring` module wants to know about (such as a
`NormalizedString`). The alternative I think would be to make
`docstring` a sibling module and expose more of `string`'s internals.

I think I overall like this change because it gives docstring handling a
bit more room to breath. It has grown quite a bit with the addition of
code snippet formatting.

[This was suggested by
@charliermarsh.](https://github.com/astral-sh/ruff/pull/8811#discussion_r1401169531)
2023-11-27 13:32:26 -05:00
696 changed files with 33996 additions and 6136 deletions

View File

@@ -23,8 +23,13 @@ jobs:
name: "Determine changes"
runs-on: ubuntu-latest
outputs:
# Flag that is raised when any code that affects linter is changed
linter: ${{ steps.changed.outputs.linter_any_changed }}
# Flag that is raised when any code that affects formatter is changed
formatter: ${{ steps.changed.outputs.formatter_any_changed }}
# Flag that is raised when any code is changed
# This is superset of the linter and formatter
code: ${{ steps.changed.outputs.code_any_changed }}
steps:
- uses: actions/checkout@v4
with:
@@ -62,6 +67,12 @@ jobs:
- python/**
- .github/workflows/ci.yaml
code:
- "*/**"
- "!**/*.md"
- "!docs/**"
- "!assets/**"
cargo-fmt:
name: "cargo fmt"
runs-on: ubuntu-latest
@@ -74,6 +85,8 @@ jobs:
cargo-clippy:
name: "cargo clippy"
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: "Install Rust toolchain"
@@ -88,6 +101,8 @@ jobs:
cargo-test-linux:
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
name: "cargo test (linux)"
steps:
- uses: actions/checkout@v4
@@ -112,6 +127,8 @@ jobs:
cargo-test-windows:
runs-on: windows-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
name: "cargo test (windows)"
steps:
- uses: actions/checkout@v4
@@ -129,6 +146,8 @@ jobs:
cargo-test-wasm:
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
name: "cargo test (wasm)"
steps:
- uses: actions/checkout@v4
@@ -148,6 +167,8 @@ jobs:
cargo-fuzz:
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
name: "cargo fuzz"
steps:
- uses: actions/checkout@v4
@@ -165,6 +186,8 @@ jobs:
scripts:
name: "test scripts"
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: "Install Rust toolchain"
@@ -188,8 +211,7 @@ jobs:
# Only runs on pull requests, since that is the only we way we can find the base version for comparison.
# Ecosystem check needs linter and/or formatter changes.
if: github.event_name == 'pull_request' && ${{
needs.determine_changes.outputs.linter == 'true' ||
needs.determine_changes.outputs.formatter == 'true'
needs.determine_changes.outputs.code == 'true'
}}
steps:
- uses: actions/checkout@v4
@@ -298,6 +320,8 @@ jobs:
cargo-udeps:
name: "cargo udeps"
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: "Install nightly Rust toolchain"
@@ -417,7 +441,10 @@ jobs:
check-ruff-lsp:
name: "test ruff-lsp"
runs-on: ubuntu-latest
needs: cargo-test-linux
needs:
- cargo-test-linux
- determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
steps:
- uses: extractions/setup-just@v1
env:
@@ -455,6 +482,8 @@ jobs:
benchmarks:
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
steps:
- name: "Checkout Branch"
uses: actions/checkout@v4

View File

@@ -1,5 +1,99 @@
# Changelog
## 0.1.7
### Preview features
- Implement multiline dictionary and list hugging for preview style ([#8293](https://github.com/astral-sh/ruff/pull/8293))
- Implement the `fix_power_op_line_length` preview style ([#8947](https://github.com/astral-sh/ruff/pull/8947))
- Use Python version to determine typing rewrite safety ([#8919](https://github.com/astral-sh/ruff/pull/8919))
- \[`flake8-annotations`\] Enable auto-return-type involving `Optional` and `Union` annotations ([#8885](https://github.com/astral-sh/ruff/pull/8885))
- \[`flake8-bandit`\] Implement `django-raw-sql` (`S611`) ([#8651](https://github.com/astral-sh/ruff/pull/8651))
- \[`flake8-bandit`\] Implement `tarfile-unsafe-members` (`S202`) ([#8829](https://github.com/astral-sh/ruff/pull/8829))
- \[`flake8-pyi`\] Implement fix for `unnecessary-literal-union` (`PYI030`) ([#7934](https://github.com/astral-sh/ruff/pull/7934))
- \[`flake8-simplify`\] Extend `dict-get-with-none-default` (`SIM910`) to non-literals ([#8762](https://github.com/astral-sh/ruff/pull/8762))
- \[`pylint`\] - add `unnecessary-list-index-lookup` (`PLR1736`) + autofix ([#7999](https://github.com/astral-sh/ruff/pull/7999))
- \[`pylint`\] - implement R0202 and R0203 with autofixes ([#8335](https://github.com/astral-sh/ruff/pull/8335))
- \[`pylint`\] Implement `repeated-keyword` (`PLe1132`) ([#8706](https://github.com/astral-sh/ruff/pull/8706))
- \[`pylint`\] Implement `too-many-positional` (`PLR0917`) ([#8995](https://github.com/astral-sh/ruff/pull/8995))
- \[`pylint`\] Implement `unnecessary-dict-index-lookup` (`PLR1733`) ([#8036](https://github.com/astral-sh/ruff/pull/8036))
- \[`refurb`\] Implement `redundant-log-base` (`FURB163`) ([#8842](https://github.com/astral-sh/ruff/pull/8842))
### Rule changes
- \[`flake8-boolean-trap`\] Allow booleans in `@override` methods ([#8882](https://github.com/astral-sh/ruff/pull/8882))
- \[`flake8-bugbear`\] Avoid `B015`,`B018` for last expression in a cell ([#8815](https://github.com/astral-sh/ruff/pull/8815))
- \[`flake8-pie`\] Allow ellipses for enum values in stub files ([#8825](https://github.com/astral-sh/ruff/pull/8825))
- \[`flake8-pyi`\] Check PEP 695 type aliases for `snake-case-type-alias` and `t-suffixed-type-alias` ([#8966](https://github.com/astral-sh/ruff/pull/8966))
- \[`flake8-pyi`\] Check for kwarg and vararg `NoReturn` type annotations ([#8948](https://github.com/astral-sh/ruff/pull/8948))
- \[`flake8-simplify`\] Omit select context managers from `SIM117` ([#8801](https://github.com/astral-sh/ruff/pull/8801))
- \[`pep8-naming`\] Allow Django model loads in `non-lowercase-variable-in-function` (`N806`) ([#8917](https://github.com/astral-sh/ruff/pull/8917))
- \[`pycodestyle`\] Avoid `E703` for last expression in a cell ([#8821](https://github.com/astral-sh/ruff/pull/8821))
- \[`pycodestyle`\] Update `E402` to work at cell level for notebooks ([#8872](https://github.com/astral-sh/ruff/pull/8872))
- \[`pydocstyle`\] Avoid `D100` for Jupyter Notebooks ([#8816](https://github.com/astral-sh/ruff/pull/8816))
- \[`pylint`\] Implement fix for `unspecified-encoding` (`PLW1514`) ([#8928](https://github.com/astral-sh/ruff/pull/8928))
### Formatter
- Avoid unstable formatting in ellipsis-only body with trailing comment ([#8984](https://github.com/astral-sh/ruff/pull/8984))
- Inline trailing comments for type alias similar to assignments ([#8941](https://github.com/astral-sh/ruff/pull/8941))
- Insert trailing comma when function breaks with single argument ([#8921](https://github.com/astral-sh/ruff/pull/8921))
### CLI
- Update `ruff check` and `ruff format` to default to the current directory ([#8791](https://github.com/astral-sh/ruff/pull/8791))
- Stop at the first resolved parent configuration ([#8864](https://github.com/astral-sh/ruff/pull/8864))
### Configuration
- \[`pylint`\] Default `max-positional-args` to `max-args` ([#8998](https://github.com/astral-sh/ruff/pull/8998))
- \[`pylint`\] Add `allow-dunder-method-names` setting for `bad-dunder-method-name` (`PLW3201`) ([#8812](https://github.com/astral-sh/ruff/pull/8812))
- \[`isort`\] Add support for `from-first` setting ([#8663](https://github.com/astral-sh/ruff/pull/8663))
- \[`isort`\] Add support for `length-sort` settings ([#8841](https://github.com/astral-sh/ruff/pull/8841))
### Bug fixes
- Add support for `@functools.singledispatch` ([#8934](https://github.com/astral-sh/ruff/pull/8934))
- Avoid off-by-one error in stripping noqa following multi-byte char ([#8979](https://github.com/astral-sh/ruff/pull/8979))
- Avoid off-by-one error in with-item named expressions ([#8915](https://github.com/astral-sh/ruff/pull/8915))
- Avoid syntax error via invalid ur string prefix ([#8971](https://github.com/astral-sh/ruff/pull/8971))
- Avoid underflow in `get_model` matching ([#8965](https://github.com/astral-sh/ruff/pull/8965))
- Avoid unnecessary index diagnostics when value is modified ([#8970](https://github.com/astral-sh/ruff/pull/8970))
- Convert over-indentation rule to use number of characters ([#8983](https://github.com/astral-sh/ruff/pull/8983))
- Detect implicit returns in auto-return-types ([#8952](https://github.com/astral-sh/ruff/pull/8952))
- Fix start >= end error in over-indentation ([#8982](https://github.com/astral-sh/ruff/pull/8982))
- Ignore `@overload` and `@override` methods for too-many-arguments checks ([#8954](https://github.com/astral-sh/ruff/pull/8954))
- Lexer start of line is false only for `Mode::Expression` ([#8880](https://github.com/astral-sh/ruff/pull/8880))
- Mark `pydantic_settings.BaseSettings` as having default copy semantics ([#8793](https://github.com/astral-sh/ruff/pull/8793))
- Respect dictionary unpacking in `NamedTuple` assignments ([#8810](https://github.com/astral-sh/ruff/pull/8810))
- Respect local subclasses in `flake8-type-checking` ([#8768](https://github.com/astral-sh/ruff/pull/8768))
- Support type alias statements in simple statement positions ([#8916](https://github.com/astral-sh/ruff/pull/8916))
- \[`flake8-annotations`\] Avoid filtering out un-representable types in return annotation ([#8881](https://github.com/astral-sh/ruff/pull/8881))
- \[`flake8-pie`\] Retain extra ellipses in protocols and abstract methods ([#8769](https://github.com/astral-sh/ruff/pull/8769))
- \[`flake8-pyi`\] Respect local enum subclasses in `simple-defaults` (`PYI052`) ([#8767](https://github.com/astral-sh/ruff/pull/8767))
- \[`flake8-trio`\] Use correct range for `TRIO115` fix ([#8933](https://github.com/astral-sh/ruff/pull/8933))
- \[`flake8-trio`\] Use full arguments range for zero-sleep-call ([#8936](https://github.com/astral-sh/ruff/pull/8936))
- \[`isort`\] fix: mark `__main__` as first-party import ([#8805](https://github.com/astral-sh/ruff/pull/8805))
- \[`pep8-naming`\] Avoid `N806` errors for type alias statements ([#8785](https://github.com/astral-sh/ruff/pull/8785))
- \[`perflint`\] Avoid `PERF101` if there's an append in loop body ([#8809](https://github.com/astral-sh/ruff/pull/8809))
- \[`pycodestyle`\] Allow space-before-colon after end-of-slice ([#8838](https://github.com/astral-sh/ruff/pull/8838))
- \[`pydocstyle`\] Avoid non-character breaks in `over-indentation` (`D208`) ([#8866](https://github.com/astral-sh/ruff/pull/8866))
- \[`pydocstyle`\] Ignore underlines when determining docstring logical lines ([#8929](https://github.com/astral-sh/ruff/pull/8929))
- \[`pylint`\] Extend `self-assigning-variable` to multi-target assignments ([#8839](https://github.com/astral-sh/ruff/pull/8839))
- \[`tryceratops`\] Avoid repeated triggers in nested `tryceratops` diagnostics ([#8772](https://github.com/astral-sh/ruff/pull/8772))
### Documentation
- Add advice for fixing RUF008 when mutability is not desired ([#8853](https://github.com/astral-sh/ruff/pull/8853))
- Added the command to run ruff using pkgx to the installation.md ([#8955](https://github.com/astral-sh/ruff/pull/8955))
- Document fix safety for flake8-comprehensions and some pyupgrade rules ([#8918](https://github.com/astral-sh/ruff/pull/8918))
- Fix doc formatting for zero-sleep-call ([#8937](https://github.com/astral-sh/ruff/pull/8937))
- Remove duplicate imports from os-stat documentation ([#8930](https://github.com/astral-sh/ruff/pull/8930))
- Replace generated reference to MkDocs ([#8806](https://github.com/astral-sh/ruff/pull/8806))
- Update Arch Linux package URL in installation.md ([#8802](https://github.com/astral-sh/ruff/pull/8802))
- \[`flake8-pyi`\] Fix error in `t-suffixed-type-alias` (`PYI043`) example ([#8963](https://github.com/astral-sh/ruff/pull/8963))
- \[`flake8-pyi`\] Improve motivation for `custom-type-var-return-type` (`PYI019`) ([#8766](https://github.com/astral-sh/ruff/pull/8766))
## 0.1.6
### Preview features

64
Cargo.lock generated
View File

@@ -808,7 +808,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.1.6"
version = "0.1.7"
dependencies = [
"anyhow",
"clap",
@@ -848,18 +848,18 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.2.0"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
"percent-encoding",
]
[[package]]
name = "fs-err"
version = "2.10.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5fd9bcbe8b1087cbd395b51498c01bc997cef73e778a80b77a811af5e2d29f"
checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41"
dependencies = [
"autocfg",
]
@@ -987,9 +987,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
@@ -1170,9 +1170,9 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "js-sys"
version = "0.3.65"
version = "0.3.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8"
checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca"
dependencies = [
"wasm-bindgen",
]
@@ -1622,9 +1622,9 @@ dependencies = [
[[package]]
name = "percent-encoding"
version = "2.3.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "petgraph"
@@ -2062,7 +2062,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.1.6"
version = "0.1.7"
dependencies = [
"annotate-snippets 0.9.2",
"anyhow",
@@ -2198,7 +2198,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.1.6"
version = "0.1.7"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2450,7 +2450,7 @@ dependencies = [
[[package]]
name = "ruff_shrinking"
version = "0.1.6"
version = "0.1.7"
dependencies = [
"anyhow",
"clap",
@@ -2618,9 +2618,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "0.8.15"
version = "0.8.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c"
checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29"
dependencies = [
"dyn-clone",
"schemars_derive",
@@ -2630,9 +2630,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "0.8.15"
version = "0.8.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c"
checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967"
dependencies = [
"proc-macro2",
"quote",
@@ -3334,9 +3334,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "ureq"
version = "2.8.0"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5ccd538d4a604753ebc2f17cd9946e89b77bf87f6a8e2309667c6f2e87855e3"
checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97"
dependencies = [
"base64",
"flate2",
@@ -3350,9 +3350,9 @@ dependencies = [
[[package]]
name = "url"
version = "2.4.1"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
dependencies = [
"form_urlencoded",
"idna",
@@ -3461,9 +3461,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.88"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce"
checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
@@ -3471,9 +3471,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.88"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217"
checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826"
dependencies = [
"bumpalo",
"log",
@@ -3498,9 +3498,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.88"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2"
checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -3508,9 +3508,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.88"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907"
checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [
"proc-macro2",
"quote",
@@ -3521,9 +3521,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.88"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b"
checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
[[package]]
name = "wasm-bindgen-test"

View File

@@ -33,7 +33,7 @@ proc-macro2 = { version = "1.0.70" }
quote = { version = "1.0.23" }
regex = { version = "1.10.2" }
rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.15" }
schemars = { version = "0.8.16" }
serde = { version = "1.0.190", features = ["derive"] }
serde_json = { version = "1.0.108" }
shellexpand = { version = "3.0.0" }

View File

@@ -150,7 +150,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.6
rev: v0.1.7
hooks:
# Run the linter.
- id: ruff

View File

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

View File

@@ -34,8 +34,8 @@ harness = false
once_cell.workspace = true
serde.workspace = true
serde_json.workspace = true
url = "2.3.1"
ureq = "2.8.0"
url = "2.5.0"
ureq = "2.9.1"
criterion = { version = "0.5.1", default-features = false }
codspeed-criterion-compat = { version="2.3.3", default-features = false, optional = true}

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_module_ast, PyFormatOptions};
use ruff_python_formatter::{format_module_ast, PreviewMode, PyFormatOptions};
use ruff_python_index::CommentRangesBuilder;
use ruff_python_parser::lexer::lex;
use ruff_python_parser::{parse_tokens, Mode};
@@ -69,7 +69,8 @@ fn benchmark_formatter(criterion: &mut Criterion) {
.expect("Input to be a valid python program");
b.iter(|| {
let options = PyFormatOptions::from_extension(Path::new(case.name()));
let options = PyFormatOptions::from_extension(Path::new(case.name()))
.with_preview(PreviewMode::Enabled);
let formatted =
format_module_ast(&module, &comment_ranges, case.code(), options)
.expect("Formatting to succeed");

View File

@@ -3,7 +3,9 @@ use ruff_benchmark::criterion::{
};
use ruff_benchmark::{TestCase, TestFile, TestFileDownloadError};
use ruff_linter::linter::lint_only;
use ruff_linter::rule_selector::PreviewOptions;
use ruff_linter::settings::rule_table::RuleTable;
use ruff_linter::settings::types::PreviewMode;
use ruff_linter::settings::{flags, LinterSettings};
use ruff_linter::source_kind::SourceKind;
use ruff_linter::{registry::Rule, RuleSelector};
@@ -78,12 +80,21 @@ fn benchmark_default_rules(criterion: &mut Criterion) {
benchmark_linter(group, &LinterSettings::default());
}
fn benchmark_all_rules(criterion: &mut Criterion) {
let mut rules: RuleTable = RuleSelector::All.all_rules().collect();
// Disable IO based rules because it is a source of flakiness
/// Disables IO based rules because they are a source of flakiness
fn disable_io_rules(rules: &mut RuleTable) {
rules.disable(Rule::ShebangMissingExecutableFile);
rules.disable(Rule::ShebangNotExecutable);
}
fn benchmark_all_rules(criterion: &mut Criterion) {
let mut rules: RuleTable = RuleSelector::All
.rules(&PreviewOptions {
mode: PreviewMode::Disabled,
require_explicit: false,
})
.collect();
disable_io_rules(&mut rules);
let settings = LinterSettings {
rules,
@@ -94,6 +105,22 @@ fn benchmark_all_rules(criterion: &mut Criterion) {
benchmark_linter(group, &settings);
}
fn benchmark_preview_rules(criterion: &mut Criterion) {
let mut rules: RuleTable = RuleSelector::All.all_rules().collect();
disable_io_rules(&mut rules);
let settings = LinterSettings {
rules,
preview: PreviewMode::Enabled,
..LinterSettings::default()
};
let group = criterion.benchmark_group("linter/all-with-preview-rules");
benchmark_linter(group, &settings);
}
criterion_group!(default_rules, benchmark_default_rules);
criterion_group!(all_rules, benchmark_all_rules);
criterion_main!(default_rules, all_rules);
criterion_group!(preview_rules, benchmark_preview_rules);
criterion_main!(default_rules, all_rules, preview_rules);

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.1.6"
version = "0.1.7"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -69,7 +69,7 @@ insta = { workspace = true, features = ["filters", "json"] }
insta-cmd = { version = "0.4.0" }
tempfile = "3.8.1"
test-case = { workspace = true }
ureq = { version = "2.8.0", features = [] }
ureq = { version = "2.9.1", features = [] }
[target.'cfg(target_os = "windows")'.dependencies]
mimalloc = "0.1.39"

View File

@@ -396,3 +396,43 @@ if __name__ == "__main__":
"###);
Ok(())
}
/// Regression test for [#8858](https://github.com/astral-sh/ruff/issues/8858)
#[test]
fn parent_configuration_override() -> Result<()> {
let tempdir = TempDir::new()?;
let root_ruff = tempdir.path().join("ruff.toml");
fs::write(
root_ruff,
r#"
[lint]
select = ["ALL"]
"#,
)?;
let sub_dir = tempdir.path().join("subdirectory");
fs::create_dir(&sub_dir)?;
let subdirectory_ruff = sub_dir.join("ruff.toml");
fs::write(
subdirectory_ruff,
r#"
[lint]
ignore = ["D203", "D212"]
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(sub_dir)
.arg("check")
.args(STDIN_BASE_OPTIONS)
, @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: No Python files found under the given path(s)
"###);
Ok(())
}

View File

@@ -1,30 +1,34 @@
//! Print the AST for a given Python file.
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::fs;
use std::path::PathBuf;
use anyhow::Result;
use ruff_python_parser::{parse, Mode};
use ruff_linter::source_kind::SourceKind;
use ruff_python_ast::PySourceType;
use ruff_python_parser::{parse, AsMode};
#[derive(clap::Args)]
pub(crate) struct Args {
/// Python file for which to generate the AST.
#[arg(required = true)]
file: PathBuf,
/// Run in Jupyter mode i.e., allow line magics.
#[arg(long)]
jupyter: bool,
}
pub(crate) fn main(args: &Args) -> Result<()> {
let contents = fs::read_to_string(&args.file)?;
let mode = if args.jupyter {
Mode::Ipython
} else {
Mode::Module
};
let python_ast = parse(&contents, mode, &args.file.to_string_lossy())?;
let source_type = PySourceType::from(&args.file);
let source_kind = SourceKind::from_path(&args.file, source_type)?.ok_or_else(|| {
anyhow::anyhow!(
"Could not determine source kind for file: {}",
args.file.display()
)
})?;
let python_ast = parse(
source_kind.source_code(),
source_type.as_mode(),
&args.file.to_string_lossy(),
)?;
println!("{python_ast:#?}");
Ok(())
}

View File

@@ -1,30 +1,30 @@
//! Print the token stream for a given Python file.
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::fs;
use std::path::PathBuf;
use anyhow::Result;
use ruff_python_parser::{lexer, Mode};
use ruff_linter::source_kind::SourceKind;
use ruff_python_ast::PySourceType;
use ruff_python_parser::{lexer, AsMode};
#[derive(clap::Args)]
pub(crate) struct Args {
/// Python file for which to generate the AST.
#[arg(required = true)]
file: PathBuf,
/// Run in Jupyter mode i.e., allow line magics (`%`, `!`, `?`, `/`, `,`, `;`).
#[arg(long)]
jupyter: bool,
}
pub(crate) fn main(args: &Args) -> Result<()> {
let contents = fs::read_to_string(&args.file)?;
let mode = if args.jupyter {
Mode::Ipython
} else {
Mode::Module
};
for (tok, range) in lexer::lex(&contents, mode).flatten() {
let source_type = PySourceType::from(&args.file);
let source_kind = SourceKind::from_path(&args.file, source_type)?.ok_or_else(|| {
anyhow::anyhow!(
"Could not determine source kind for file: {}",
args.file.display()
)
})?;
for (tok, range) in lexer::lex(source_kind.source_code(), source_type.as_mode()).flatten() {
println!(
"{start:#?} {tok:#?} {end:#?}",
start = range.start(),

View File

@@ -1,23 +1,9 @@
use super::{Buffer, Format, Formatter};
use crate::FormatResult;
use std::ffi::c_void;
use std::marker::PhantomData;
/// Mono-morphed type to format an object. Used by the [`crate::format`!], [`crate::format_args`!], and
/// [`crate::write`!] macros.
///
/// This struct is similar to a dynamic dispatch (using `dyn Format`) because it stores a pointer to the value.
/// However, it doesn't store the pointer to `dyn Format`'s vtable, instead it statically resolves the function
/// pointer of `Format::format` and stores it in `formatter`.
/// A convenience wrapper for representing a formattable argument.
pub struct Argument<'fmt, Context> {
/// The value to format stored as a raw pointer where `lifetime` stores the value's lifetime.
value: *const c_void,
/// Stores the lifetime of the value. To get the most out of our dear borrow checker.
lifetime: PhantomData<&'fmt ()>,
/// The function pointer to `value`'s `Format::format` method
formatter: fn(*const c_void, &mut Formatter<'_, Context>) -> FormatResult<()>,
value: &'fmt dyn Format<Context>,
}
impl<Context> Clone for Argument<'_, Context> {
@@ -28,32 +14,19 @@ impl<Context> Clone for Argument<'_, Context> {
impl<Context> Copy for Argument<'_, Context> {}
impl<'fmt, Context> Argument<'fmt, Context> {
/// Called by the [ruff_formatter::format_args] macro. Creates a mono-morphed value for formatting
/// an object.
/// Called by the [ruff_formatter::format_args] macro.
#[doc(hidden)]
#[inline]
pub fn new<F: Format<Context>>(value: &'fmt F) -> Self {
#[inline]
fn formatter<F: Format<Context>, Context>(
ptr: *const c_void,
fmt: &mut Formatter<Context>,
) -> FormatResult<()> {
// SAFETY: Safe because the 'fmt lifetime is captured by the 'lifetime' field.
#[allow(unsafe_code)]
F::fmt(unsafe { &*ptr.cast::<F>() }, fmt)
}
Self {
value: (value as *const F).cast::<std::ffi::c_void>(),
lifetime: PhantomData,
formatter: formatter::<F, Context>,
}
Self { value }
}
/// Formats the value stored by this argument using the given formatter.
#[inline]
// Seems to only be triggered on wasm32 and looks like a false positive?
#[allow(clippy::trivially_copy_pass_by_ref)]
pub(super) fn format(&self, f: &mut Formatter<Context>) -> FormatResult<()> {
(self.formatter)(self.value, f)
self.value.fmt(f)
}
}

View File

@@ -2555,17 +2555,17 @@ pub struct BestFitting<'a, Context> {
}
impl<'a, Context> BestFitting<'a, Context> {
/// Creates a new best fitting IR with the given variants. The method itself isn't unsafe
/// but it is to discourage people from using it because the printer will panic if
/// the slice doesn't contain at least the least and most expanded variants.
/// Creates a new best fitting IR with the given variants.
///
/// Callers are required to ensure that the number of variants given
/// is at least 2.
///
/// You're looking for a way to create a `BestFitting` object, use the `best_fitting![least_expanded, most_expanded]` macro.
///
/// ## Safety
/// The slice must contain at least two variants.
#[allow(unsafe_code)]
pub unsafe fn from_arguments_unchecked(variants: Arguments<'a, Context>) -> Self {
/// # Panics
///
/// When the slice contains less than two variants.
pub fn from_arguments_unchecked(variants: Arguments<'a, Context>) -> Self {
assert!(
variants.0.len() >= 2,
"Requires at least the least expanded and most expanded variants"
@@ -2696,14 +2696,12 @@ impl<Context> Format<Context> for BestFitting<'_, Context> {
buffer.write_element(FormatElement::Tag(EndBestFittingEntry));
}
// SAFETY: The constructor guarantees that there are always at least two variants. It's, therefore,
// safe to call into the unsafe `from_vec_unchecked` function
#[allow(unsafe_code)]
let element = unsafe {
FormatElement::BestFitting {
variants: BestFittingVariants::from_vec_unchecked(buffer.into_vec()),
mode: self.mode,
}
// OK because the constructor guarantees that there are always at
// least two variants.
let variants = BestFittingVariants::from_vec_unchecked(buffer.into_vec());
let element = FormatElement::BestFitting {
variants,
mode: self.mode,
};
f.write_element(element);

View File

@@ -332,17 +332,14 @@ pub enum BestFittingMode {
pub struct BestFittingVariants(Box<[FormatElement]>);
impl BestFittingVariants {
/// Creates a new best fitting IR with the given variants. The method itself isn't unsafe
/// but it is to discourage people from using it because the printer will panic if
/// the slice doesn't contain at least the least and most expanded variants.
/// Creates a new best fitting IR with the given variants.
///
/// Callers are required to ensure that the number of variants given
/// is at least 2 when using `most_expanded` or `most_flag`.
///
/// You're looking for a way to create a `BestFitting` object, use the `best_fitting![least_expanded, most_expanded]` macro.
///
/// ## Safety
/// The slice must contain at least two variants.
#[doc(hidden)]
#[allow(unsafe_code)]
pub unsafe fn from_vec_unchecked(variants: Vec<FormatElement>) -> Self {
pub fn from_vec_unchecked(variants: Vec<FormatElement>) -> Self {
debug_assert!(
variants
.iter()
@@ -351,12 +348,23 @@ impl BestFittingVariants {
>= 2,
"Requires at least the least expanded and most expanded variants"
);
Self(variants.into_boxed_slice())
}
/// Returns the most expanded variant
///
/// # Panics
///
/// When the number of variants is less than two.
pub fn most_expanded(&self) -> &[FormatElement] {
assert!(
self.as_slice()
.iter()
.filter(|element| matches!(element, FormatElement::Tag(Tag::StartBestFittingEntry)))
.count()
>= 2,
"Requires at least the least expanded and most expanded variants"
);
self.into_iter().last().unwrap()
}
@@ -365,7 +373,19 @@ impl BestFittingVariants {
}
/// Returns the least expanded variant
///
/// # Panics
///
/// When the number of variants is less than two.
pub fn most_flat(&self) -> &[FormatElement] {
assert!(
self.as_slice()
.iter()
.filter(|element| matches!(element, FormatElement::Tag(Tag::StartBestFittingEntry)))
.count()
>= 2,
"Requires at least the least expanded and most expanded variants"
);
self.into_iter().next().unwrap()
}
}

View File

@@ -329,10 +329,8 @@ macro_rules! format {
#[macro_export]
macro_rules! best_fitting {
($least_expanded:expr, $($tail:expr),+ $(,)?) => {{
#[allow(unsafe_code)]
unsafe {
$crate::BestFitting::from_arguments_unchecked($crate::format_args!($least_expanded, $($tail),+))
}
// OK because the macro syntax requires at least two variants.
$crate::BestFitting::from_arguments_unchecked($crate::format_args!($least_expanded, $($tail),+))
}}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.1.6"
version = "0.1.7"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -39,3 +39,18 @@ def func():
for i in range(1110):
if True:
break
# TODO(charlie): The `pass` here does not get properly redirected to the top of the
# loop, unlike below.
def func():
for i in range(5):
pass
else:
return 1
def func():
for i in range(5):
pass
else:
return 1
x = 1

View File

@@ -129,3 +129,11 @@ def func():
print("Grass is green")
case Color.BLUE:
print("I'm feeling the blues :(")
def func(point):
match point:
case (0, 0):
print("Origin")
case foo:
raise ValueError("oops")

View File

@@ -30,3 +30,120 @@ def func(x: int):
def func(x: int):
return 1 + 2.5 if x > 0 else 1.5 or "str"
def func(x: int):
if not x:
return None
return {"foo": 1}
def func(x: int):
return {"foo": 1}
def func(x: int):
if not x:
return 1
else:
return True
def func(x: int):
if not x:
return 1
else:
return None
def func(x: int):
if not x:
return 1
elif x > 5:
return "str"
else:
return None
def func(x: int):
if x:
return 1
def func():
x = 1
def func(x: int):
if x > 0:
return 1
def func(x: int):
match x:
case [1, 2, 3]:
return 1
case 4 as y:
return "foo"
def func(x: int):
for i in range(5):
if i > 0:
return 1
def func(x: int):
for i in range(5):
if i > 0:
return 1
else:
return 4
def func(x: int):
for i in range(5):
if i > 0:
break
else:
return 4
def func(x: int):
try:
pass
except:
return 1
def func(x: int):
try:
pass
except:
return 1
finally:
return 2
def func(x: int):
try:
pass
except:
return 1
else:
return 2
def func(x: int):
try:
return 1
except:
return 2
else:
pass
def func(x: int):
while x > 0:
break
return 1

View File

@@ -106,3 +106,11 @@ def func(x: bool | str):
def func(x: int | str):
pass
from typing import override
@override
def func(x: bool):
pass

View File

@@ -36,3 +36,54 @@ field10: (Literal[1] | str) | Literal[2] # Error
# Should emit for union in generic parent type.
field11: dict[Literal[1] | Literal[2], str] # Error
# Should emit for unions with more than two cases
field12: Literal[1] | Literal[2] | Literal[3] # Error
field13: Literal[1] | Literal[2] | Literal[3] | Literal[4] # Error
# Should emit for unions with more than two cases, even if not directly adjacent
field14: Literal[1] | Literal[2] | str | Literal[3] # Error
# Should emit for unions with mixed literal internal types
field15: Literal[1] | Literal["foo"] | Literal[True] # Error
# Shouldn't emit for duplicate field types with same value; covered by Y016
field16: Literal[1] | Literal[1] # OK
# Shouldn't emit if in new parent type
field17: Literal[1] | dict[Literal[2], str] # OK
# Shouldn't emit if not in a union parent
field18: dict[Literal[1], Literal[2]] # OK
# Should respect name of literal type used
field19: typing.Literal[1] | typing.Literal[2] # Error
# Should emit in cases with newlines
field20: typing.Union[
Literal[
1 # test
],
Literal[2],
] # Error, newline and comment will not be emitted in message
# Should handle multiple unions with multiple members
field21: Literal[1, 2] | Literal[3, 4] # Error
# Should emit in cases with `typing.Union` instead of `|`
field22: typing.Union[Literal[1], Literal[2]] # Error
# Should emit in cases with `typing_extensions.Literal`
field23: typing_extensions.Literal[1] | typing_extensions.Literal[2] # Error
# Should emit in cases with nested `typing.Union`
field24: typing.Union[Literal[1], typing.Union[Literal[2], str]] # Error
# Should emit in cases with mixed `typing.Union` and `|`
field25: typing.Union[Literal[1], Literal[2] | str] # Error
# Should emit only once in cases with multiple nested `typing.Union`
field24: typing.Union[Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]]] # Error
# Should use the first literal subscript attribute when fixing
field25: typing.Union[typing_extensions.Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]], str] # Error

View File

@@ -84,3 +84,6 @@ field25: typing.Union[Literal[1], Literal[2] | str] # Error
# Should emit only once in cases with multiple nested `typing.Union`
field24: typing.Union[Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]]] # Error
# Should use the first literal subscript attribute when fixing
field25: typing.Union[typing_extensions.Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]], str] # Error

View File

@@ -22,3 +22,7 @@ Snake_case_alias: TypeAlias = int | float # PYI042, since not camel case
# check that this edge case doesn't crash
_: TypeAlias = str | int
# PEP 695
type foo_bar = int | str
type FooBar = int | str

View File

@@ -22,3 +22,7 @@ Snake_case_alias: TypeAlias = int | float # PYI042, since not camel case
# check that this edge case doesn't crash
_: TypeAlias = str | int
# PEP 695
type foo_bar = int | str
type FooBar = int | str

View File

@@ -21,3 +21,7 @@ _PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
# check that this edge case doesn't crash
_: TypeAlias = str | int
# PEP 695
type _FooT = str | int
type Foo = str | int

View File

@@ -21,3 +21,7 @@ _PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
# check that this edge case doesn't crash
_: TypeAlias = str | int
# PEP 695
type _FooT = str | int
type Foo = str | int

View File

@@ -10,3 +10,14 @@ def foo_no_return_typing_extensions(
def foo_no_return_kwarg(arg: int, *, arg2: NoReturn): ... # Error: PYI050
def foo_no_return_pos_only(arg: int, /, arg2: NoReturn): ... # Error: PYI050
def foo_never(arg: Never): ...
def foo_args(*args: NoReturn): ... # Error: PYI050
def foo_kwargs(**kwargs: NoReturn): ... # Error: PYI050
def foo_args_kwargs(*args: NoReturn, **kwargs: NoReturn): ... # Error: PYI050
def foo_int_args(*args: int): ...
def foo_int_kwargs(**kwargs: int): ...
def foo_int_args_kwargs(*args: int, **kwargs: int): ...
def foo_int_args_no_return(*args: int, **kwargs: NoReturn): ... # Error: PYI050
def foo_int_kwargs_no_return(*args: NoReturn, **kwargs: int): ... # Error: PYI050
def foo_args_never(*args: Never): ...
def foo_kwargs_never(**kwargs: Never): ...
def foo_args_kwargs_never(*args: Never, **kwargs: Never): ...

View File

@@ -26,5 +26,10 @@ def func():
from trio import Event, sleep
def func():
sleep(0) # TRIO115
async def func():
await sleep(seconds=0) # TRIO115

View File

@@ -0,0 +1,34 @@
"""Test module."""
from __future__ import annotations
from functools import singledispatch
from typing import TYPE_CHECKING
from numpy import asarray
from numpy.typing import ArrayLike
from scipy.sparse import spmatrix
from pandas import DataFrame
if TYPE_CHECKING:
from numpy import ndarray
@singledispatch
def to_array_or_mat(a: ArrayLike | spmatrix) -> ndarray | spmatrix:
"""Convert arg to array or leaves it as sparse matrix."""
msg = f"Unhandled type {type(a)}"
raise NotImplementedError(msg)
@to_array_or_mat.register
def _(a: ArrayLike) -> ndarray:
return asarray(a)
@to_array_or_mat.register
def _(a: spmatrix) -> spmatrix:
return a
def _(a: DataFrame) -> DataFrame:
return a

View File

@@ -0,0 +1,3 @@
from mediuuuuuuuuuuum import a
from short import b
from loooooooooooooooooooooog import c

View File

@@ -0,0 +1,11 @@
from module1 import (
loooooooooooooong,
σηορτ,
mediuuuuum,
shoort,
looooooooooooooong,
μεδιυυυυυμ,
short,
mediuuuuuum,
λοοοοοοοοοοοοοονγ,
)

View File

@@ -0,0 +1,9 @@
import loooooooooooooong
import mediuuuuuum
import short
import σηορτ
import shoort
import mediuuuuum
import λοοοοοοοοοοοοοονγ
import μεδιυυυυυμ
import looooooooooooooong

View File

@@ -0,0 +1,6 @@
import mediuuuuuum
import short
import looooooooooooooooong
from looooooooooooooong import a
from mediuuuum import c
from short import b

View File

@@ -0,0 +1,4 @@
import mediuuuuuumb
import short
import looooooooooooooooong
import mediuuuuuuma

View File

@@ -0,0 +1,7 @@
from ..looooooooooooooong import a
from ...mediuuuum import b
from .short import c
from ....short import c
from . import d
from .mediuuuum import a
from ......short import b

View File

@@ -0,0 +1,3 @@
from looooooooooooooong import a
from mediuuuum import *
from short import *

View File

@@ -1,6 +1,6 @@
import collections
from collections import namedtuple
from typing import TypeAlias, TypeVar, NewType, NamedTuple, TypedDict
from typing import Type, TypeAlias, TypeVar, NewType, NamedTuple, TypedDict
GLOBAL: str = "foo"
@@ -40,3 +40,18 @@ def loop_assign():
global CURRENT_PORT
for CURRENT_PORT in range(5):
pass
def model_assign() -> None:
Bad = apps.get_model("zerver", "Stream") # N806
Attachment = apps.get_model("zerver", "Attachment") # OK
Recipient = apps.get_model("zerver", model_name="Recipient") # OK
Address: Type = apps.get_model("zerver", "Address") # OK
from django.utils.module_loading import import_string
Bad = import_string("django.core.exceptions.ValidationError") # N806
ValidationError = import_string("django.core.exceptions.ValidationError") # OK
Bad = apps.get_model() # N806
Bad = apps.get_model(model_name="Stream") # N806

View File

@@ -0,0 +1,113 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "33faf7ad-a3fd-4ac4-a0c3-52e507ed49df",
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"\n",
"sys.path"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1331140f-2741-4661-9086-0764368710c9",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "a4113383-725d-4f04-80b8-a3080b2b8c4b",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"os.path\n",
"\n",
"import pathlib"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a5d2ef63-ae60-4311-bae3-42e845afba3f",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "79599475-a5ee-4f60-80d1-6efa77693da0",
"metadata": {},
"outputs": [],
"source": [
"import a\n",
"\n",
"try:\n",
" import b\n",
"except ImportError:\n",
" pass\n",
"else:\n",
" pass\n",
"\n",
"__some__magic = 1\n",
"\n",
"import c"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "863dcc35-5c8d-4d05-8b4a-91059e944112",
"metadata": {},
"outputs": [],
"source": [
"import ok\n",
"\n",
"\n",
"def foo() -> None:\n",
" import e\n",
"\n",
"\n",
"import no_ok"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6b2377d0-b814-4057-83ec-d443d8e19401",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff-playground)",
"language": "python",
"name": "ruff-playground"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -43,3 +43,6 @@ regex = '''
''' # noqa
regex = '\\\_'
#: W605:1:7
u'foo\ bar'

View File

@@ -1,40 +1,54 @@
# Same as `W605_0.py` but using f-strings instead.
#: W605:1:10
regex = '\.png$'
regex = f'\.png$'
#: W605:2:1
regex = '''
regex = f'''
\.png$
'''
#: W605:2:6
f(
'\_'
f'\_'
)
#: W605:4:6
"""
f"""
multi-line
literal
with \_ somewhere
in the middle
"""
#: W605:1:38
value = f'new line\nand invalid escape \_ here'
def f():
#: W605:1:11
return'\.png$'
#: Okay
regex = r'\.png$'
regex = '\\.png$'
regex = r'''
regex = fr'\.png$'
regex = f'\\.png$'
regex = fr'''
\.png$
'''
regex = r'''
regex = fr'''
\\.png$
'''
s = '\\'
regex = '\w' # noqa
regex = '''
s = f'\\'
regex = f'\w' # noqa
regex = f'''
\w
''' # noqa
regex = f'\\\_'
value = f'\{{1}}'
value = f'\{1}'
value = f'{1:\}'
value = f"{f"\{1}"}"
value = rf"{f"\{1}"}"
# Okay
value = rf'\{{1}}'
value = rf'\{1}'
value = rf'{1:\}'
value = f"{rf"\{1}"}"

View File

@@ -1,54 +0,0 @@
# Same as `W605_0.py` but using f-strings instead.
#: W605:1:10
regex = f'\.png$'
#: W605:2:1
regex = f'''
\.png$
'''
#: W605:2:6
f(
f'\_'
)
#: W605:4:6
f"""
multi-line
literal
with \_ somewhere
in the middle
"""
#: W605:1:38
value = f'new line\nand invalid escape \_ here'
#: Okay
regex = fr'\.png$'
regex = f'\\.png$'
regex = fr'''
\.png$
'''
regex = fr'''
\\.png$
'''
s = f'\\'
regex = f'\w' # noqa
regex = f'''
\w
''' # noqa
regex = f'\\\_'
value = f'\{{1}}'
value = f'\{1}'
value = f'{1:\}'
value = f"{f"\{1}"}"
value = rf"{f"\{1}"}"
# Okay
value = rf'\{{1}}'
value = rf'\{1}'
value = rf'{1:\}'
value = f"{rf"\{1}"}"

View File

@@ -0,0 +1,16 @@
"""
Author
"""
class Platform:
""" Remove sampler
Args:
    Returns:
"""
def memory_test():
"""
   参数含义precision:精确到小数点后几位
"""

View File

@@ -91,3 +91,12 @@ def f(rounds: list[int], number: int) -> bool:
bool: was the round played?
"""
return number in rounds
def f():
"""
My example
==========
My example explanation
"""

View File

@@ -0,0 +1,19 @@
from random import choice
class Fruit:
COLORS = []
def __init__(self, color):
self.color = color
def pick_colors(cls, *args): # [no-classmethod-decorator]
"""classmethod to pick fruit colors"""
cls.COLORS = args
pick_colors = classmethod(pick_colors)
def pick_one_color(): # [no-staticmethod-decorator]
"""staticmethod to pick one fruit color"""
return choice(Fruit.COLORS)
pick_one_color = staticmethod(pick_one_color)

View File

@@ -32,3 +32,16 @@ def f(x, y, z, *, u, v, w): # Too many arguments (6/5)
def f(x, y, z, a, b, c, *, u, v, w): # Too many arguments (9/5)
pass
from typing import override, overload
@override
def f(x, y, z, a, b, c, *, u, v, w): # OK
pass
@overload
def f(x, y, z, a, b, c, *, u, v, w): # OK
pass

View File

@@ -0,0 +1,30 @@
def f(x, y, z, t, u, v, w, r): # Too many positional arguments (8/3)
pass
def f(x): # OK
pass
def f(x, y, z, _t, _u, _v, _w, r): # OK (underscore-prefixed names are ignored
pass
def f(x, y, z, *, u=1, v=1, r=1): # OK
pass
def f(x=1, y=1, z=1): # OK
pass
def f(x, y, z, /, u, v, w): # Too many positional arguments (6/3)
pass
def f(x, y, z, *, u, v, w): # OK
pass
def f(x, y, z, a, b, c, *, u, v, w): # Too many positional arguments (6/3)
pass

View File

@@ -0,0 +1,10 @@
# Too many positional arguments (7/4) for max_positional=4
# OK for dummy_variable_rgx ~ "skip_.*"
def f(w, x, y, z, skip_t, skip_u, skip_v):
pass
# Too many positional arguments (7/4) for max_args=4
# Too many positional arguments (7/3) for dummy_variable_rgx ~ "skip_.*"
def f(w, x, y, z, t, u, v):
pass

View File

@@ -0,0 +1,40 @@
FRUITS = {"apple": 1, "orange": 10, "berry": 22}
def fix_these():
[FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()] # PLR1733
{FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
{fruit_name: FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
for fruit_name, fruit_count in FRUITS.items():
print(FRUITS[fruit_name]) # PLR1733
blah = FRUITS[fruit_name] # PLR1733
assert FRUITS[fruit_name] == "pear" # PLR1733
def dont_fix_these():
# once there is an assignment to the dict[index], we stop emitting diagnostics
for fruit_name, fruit_count in FRUITS.items():
FRUITS[fruit_name] = 0 # OK
assert FRUITS[fruit_name] == 0 # OK
# once there is an assignment to the key, we stop emitting diagnostics
for fruit_name, fruit_count in FRUITS.items():
fruit_name = 0 # OK
assert FRUITS[fruit_name] == 0 # OK
# once there is an assignment to the value, we stop emitting diagnostics
for fruit_name, fruit_count in FRUITS.items():
if fruit_count < 5:
fruit_count = -fruit_count
assert FRUITS[fruit_name] == 0 # OK
def value_intentionally_unused():
[FRUITS[fruit_name] for fruit_name, _ in FRUITS.items()] # OK
{FRUITS[fruit_name] for fruit_name, _ in FRUITS.items()} # OK
{fruit_name: FRUITS[fruit_name] for fruit_name, _ in FRUITS.items()} # OK
for fruit_name, _ in FRUITS.items():
print(FRUITS[fruit_name]) # OK
blah = FRUITS[fruit_name] # OK
assert FRUITS[fruit_name] == "pear" # OK

View File

@@ -0,0 +1,64 @@
import builtins
letters = ["a", "b", "c"]
def fix_these():
[letters[index] for index, letter in enumerate(letters)] # PLR1736
{letters[index] for index, letter in enumerate(letters)} # PLR1736
{letter: letters[index] for index, letter in enumerate(letters)} # PLR1736
for index, letter in enumerate(letters):
print(letters[index]) # PLR1736
blah = letters[index] # PLR1736
assert letters[index] == "d" # PLR1736
for index, letter in builtins.enumerate(letters):
print(letters[index]) # PLR1736
blah = letters[index] # PLR1736
assert letters[index] == "d" # PLR1736
def dont_fix_these():
# once there is an assignment to the sequence[index], we stop emitting diagnostics
for index, letter in enumerate(letters):
letters[index] = "d" # OK
letters[index] += "e" # OK
assert letters[index] == "de" # OK
# once there is an assignment to the index, we stop emitting diagnostics
for index, letter in enumerate(letters):
index += 1 # OK
print(letters[index]) # OK
# once there is an assignment to the sequence, we stop emitting diagnostics
for index, letter in enumerate(letters):
letters = ["d", "e", "f"] # OK
print(letters[index]) # OK
# once there is an assignment to the value, we stop emitting diagnostics
for index, letter in enumerate(letters):
letter = "d"
print(letters[index]) # OK
# once there is an deletion from or of the sequence or index, we stop emitting diagnostics
for index, letter in enumerate(letters):
del letters[index] # OK
print(letters[index]) # OK
for index, letter in enumerate(letters):
del letters # OK
print(letters[index]) # OK
for index, letter in enumerate(letters):
del index # OK
print(letters[index]) # OK
def value_intentionally_unused():
[letters[index] for index, _ in enumerate(letters)] # OK
{letters[index] for index, _ in enumerate(letters)} # OK
{index: letters[index] for index, _ in enumerate(letters)} # OK
for index, _ in enumerate(letters):
print(letters[index]) # OK
blah = letters[index] # OK
letters[index] = "d" # OK

View File

@@ -42,3 +42,30 @@ tempfile.SpooledTemporaryFile(0, "w", encoding="utf-8")
tempfile.SpooledTemporaryFile(0, "w", -1, "utf-8")
tempfile.SpooledTemporaryFile(0, "wb")
tempfile.SpooledTemporaryFile(0, )
open("test.txt",)
open()
open(
"test.txt", # comment
)
open(
"test.txt",
# comment
)
open(("test.txt"),)
open(
("test.txt"), # comment
)
open(
("test.txt"),
# comment
)
open((("test.txt")),)
open(
(("test.txt")), # comment
)
open(
(("test.txt")),
# comment
)

View File

@@ -0,0 +1,47 @@
import math
from math import e as special_e
from math import log as special_log
# Errors.
math.log(1, 2)
math.log(1, 10)
math.log(1, math.e)
foo = ...
math.log(foo, 2)
math.log(foo, 10)
math.log(foo, math.e)
math.log(1, special_e)
special_log(1, 2)
special_log(1, 10)
special_log(1, math.e)
special_log(1, special_e)
# Ok.
math.log2(1)
math.log10(1)
math.log(1)
math.log(1, 3)
math.log(1, math.pi)
two = 2
math.log(1, two)
ten = 10
math.log(1, ten)
e = math.e
math.log(1, e)
math.log2(1, 10) # math.log2 takes only one argument.
math.log10(1, 2) # math.log10 takes only one argument.
math.log(1, base=2) # math.log does not accept keyword arguments.
def log(*args):
print(f"Logging: {args}")
log(1, 2)
log(1, 10)
log(1, math.e)

View File

@@ -23,3 +23,6 @@ print(a) # noqa: E501, F821 # comment
print(a) # noqa: E501, F821 # comment
print(a) # noqa: E501, F821 comment
print(a) # noqa: E501, F821 comment
print(a) # comment with unicode µ # noqa: E501
print(a) # comment with unicode µ # noqa: E501, F821

View File

@@ -205,19 +205,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
ExprContext::Store => {
if checker.enabled(Rule::NonLowercaseVariableInFunction) {
if checker.semantic.current_scope().kind.is_function() {
// Ignore globals.
if !checker
.semantic
.current_scope()
.get(id)
.is_some_and(|binding_id| {
checker.semantic.binding(binding_id).is_global()
})
{
pep8_naming::rules::non_lowercase_variable_in_function(
checker, expr, id,
);
}
pep8_naming::rules::non_lowercase_variable_in_function(
checker, expr, id,
);
}
}
if checker.enabled(Rule::MixedCaseVariableInClassScope) {
@@ -380,13 +370,13 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flynt::rules::static_join_to_fstring(
checker,
expr,
string_value.as_str(),
string_value.to_str(),
);
}
} else if attr == "format" {
// "...".format(...) call
let location = expr.range();
match pyflakes::format::FormatSummary::try_from(string_value.as_str()) {
match pyflakes::format::FormatSummary::try_from(string_value.to_str()) {
Err(e) => {
if checker.enabled(Rule::StringDotFormatInvalidFormat) {
checker.diagnostics.push(Diagnostic::new(
@@ -432,7 +422,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::BadStringFormatCharacter) {
pylint::rules::bad_string_format_character::call(
checker,
string_value.as_str(),
string_value.to_str(),
location,
);
}
@@ -928,6 +918,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::PrintEmptyString) {
refurb::rules::print_empty_string(checker, call);
}
if checker.enabled(Rule::RedundantLogBase) {
refurb::rules::redundant_log_base(checker, call);
}
if checker.enabled(Rule::QuadraticListSummation) {
ruff::rules::quadratic_list_summation(checker, call);
}
@@ -1042,7 +1035,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Rule::PercentFormatUnsupportedFormatCharacter,
]) {
let location = expr.range();
match pyflakes::cformat::CFormatSummary::try_from(value.as_str()) {
match pyflakes::cformat::CFormatSummary::try_from(value.to_str()) {
Err(CFormatError {
typ: CFormatErrorType::UnsupportedFormatChar(c),
..
@@ -1337,6 +1330,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
range: _,
},
) => {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryDictIndexLookup) {
pylint::rules::unnecessary_dict_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryComprehension) {
flake8_comprehensions::rules::unnecessary_list_set_comprehension(
checker, expr, elt, generators,
@@ -1361,6 +1360,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
range: _,
},
) => {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryDictIndexLookup) {
pylint::rules::unnecessary_dict_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryComprehension) {
flake8_comprehensions::rules::unnecessary_list_set_comprehension(
checker, expr, elt, generators,
@@ -1384,6 +1389,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
generators,
range: _,
}) => {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryDictIndexLookup) {
pylint::rules::unnecessary_dict_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryComprehension) {
flake8_comprehensions::rules::unnecessary_dict_comprehension(
checker, expr, key, value, generators,
@@ -1408,6 +1419,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
range: _,
},
) => {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryDictIndexLookup) {
pylint::rules::unnecessary_dict_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::FunctionUsesLoopVariable) {
flake8_bugbear::rules::function_uses_loop_variable(checker, &Node::Expr(expr));
}

View File

@@ -248,7 +248,10 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::property_with_parameters(checker, stmt, decorator_list, parameters);
}
if checker.enabled(Rule::TooManyArguments) {
pylint::rules::too_many_arguments(checker, parameters, stmt);
pylint::rules::too_many_arguments(checker, function_def);
}
if checker.enabled(Rule::TooManyPositional) {
pylint::rules::too_many_positional(checker, parameters, stmt);
}
if checker.enabled(Rule::TooManyReturnStatements) {
if let Some(diagnostic) = pylint::rules::too_many_return_statements(
@@ -384,6 +387,12 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
range: _,
},
) => {
if checker.enabled(Rule::NoClassmethodDecorator) {
pylint::rules::no_classmethod_decorator(checker, class_def);
}
if checker.enabled(Rule::NoStaticmethodDecorator) {
pylint::rules::no_staticmethod_decorator(checker, class_def);
}
if checker.enabled(Rule::DjangoNullableModelStringField) {
flake8_django::rules::nullable_model_string_field(checker, body);
}
@@ -1271,6 +1280,12 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryListCast) {
perflint::rules::unnecessary_list_cast(checker, iter, body);
}
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup(checker, for_stmt);
}
if checker.enabled(Rule::UnnecessaryDictIndexLookup) {
pylint::rules::unnecessary_dict_index_lookup(checker, for_stmt);
}
if !is_async {
if checker.enabled(Rule::ReimplementedBuiltin) {
flake8_simplify::rules::convert_for_loop_to_any_all(checker, stmt);
@@ -1522,6 +1537,14 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
}
Stmt::TypeAlias(ast::StmtTypeAlias { name, .. }) => {
if checker.enabled(Rule::SnakeCaseTypeAlias) {
flake8_pyi::rules::snake_case_type_alias(checker, name);
}
if checker.enabled(Rule::TSuffixedTypeAlias) {
flake8_pyi::rules::t_suffixed_type_alias(checker, name);
}
}
Stmt::Delete(delete @ ast::StmtDelete { targets, range: _ }) => {
if checker.enabled(Rule::GlobalStatement) {
for target in targets {

View File

@@ -107,6 +107,8 @@ pub(crate) struct Checker<'a> {
pub(crate) diagnostics: Vec<Diagnostic>,
/// The list of names already seen by flake8-bugbear diagnostics, to avoid duplicate violations..
pub(crate) flake8_bugbear_seen: Vec<TextRange>,
/// The end offset of the last visited statement.
last_stmt_end: TextSize,
}
impl<'a> Checker<'a> {
@@ -142,6 +144,7 @@ impl<'a> Checker<'a> {
diagnostics: Vec::default(),
flake8_bugbear_seen: Vec::default(),
cell_offsets,
last_stmt_end: TextSize::default(),
}
}
}
@@ -268,6 +271,18 @@ where
// Step 0: Pre-processing
self.semantic.push_node(stmt);
// For Jupyter Notebooks, we'll reset the `IMPORT_BOUNDARY` flag when
// we encounter a cell boundary.
if self.source_type.is_ipynb()
&& self.semantic.at_top_level()
&& self.semantic.seen_import_boundary()
&& self.cell_offsets.is_some_and(|cell_offsets| {
cell_offsets.has_cell_boundary(TextRange::new(self.last_stmt_end, stmt.start()))
})
{
self.semantic.flags -= SemanticModelFlags::IMPORT_BOUNDARY;
}
// Track whether we've seen docstrings, non-imports, etc.
match stmt {
Stmt::ImportFrom(ast::StmtImportFrom { module, names, .. }) => {
@@ -477,6 +492,13 @@ where
// are enabled.
let runtime_annotation = !self.semantic.future_annotations();
// The first parameter may be a single dispatch.
let mut singledispatch =
flake8_type_checking::helpers::is_singledispatch_implementation(
function_def,
self.semantic(),
);
self.semantic.push_scope(ScopeKind::Type);
if let Some(type_params) = type_params {
@@ -490,7 +512,7 @@ where
.chain(&parameters.kwonlyargs)
{
if let Some(expr) = &parameter_with_default.parameter.annotation {
if runtime_annotation {
if runtime_annotation || singledispatch {
self.visit_runtime_annotation(expr);
} else {
self.visit_annotation(expr);
@@ -499,6 +521,7 @@ where
if let Some(expr) = &parameter_with_default.default {
self.visit_expr(expr);
}
singledispatch = false;
}
if let Some(arg) = &parameters.vararg {
if let Some(expr) = &arg.annotation {
@@ -655,23 +678,24 @@ where
// available at runtime.
// See: https://docs.python.org/3/reference/simple_stmts.html#annotated-assignment-statements
let runtime_annotation = if self.semantic.future_annotations() {
if self.semantic.current_scope().kind.is_class() {
let baseclasses = &self
.settings
.flake8_type_checking
.runtime_evaluated_base_classes;
let decorators = &self
.settings
.flake8_type_checking
.runtime_evaluated_decorators;
flake8_type_checking::helpers::runtime_evaluated(
baseclasses,
decorators,
&self.semantic,
)
} else {
false
}
self.semantic
.current_scope()
.kind
.as_class()
.is_some_and(|class_def| {
flake8_type_checking::helpers::runtime_evaluated_class(
class_def,
&self
.settings
.flake8_type_checking
.runtime_evaluated_base_classes,
&self
.settings
.flake8_type_checking
.runtime_evaluated_decorators,
&self.semantic,
)
})
} else {
matches!(
self.semantic.current_scope().kind,
@@ -779,6 +803,7 @@ where
self.semantic.flags = flags_snapshot;
self.semantic.pop_node();
self.last_stmt_end = stmt.end();
}
fn visit_annotation(&mut self, expr: &'b Expr) {
@@ -791,7 +816,7 @@ where
fn visit_expr(&mut self, expr: &'b Expr) {
// Step 0: Pre-processing
if !self.semantic.in_f_string()
&& !self.semantic.in_literal()
&& !self.semantic.in_typing_literal()
&& !self.semantic.in_deferred_type_definition()
&& self.semantic.in_type_definition()
&& self.semantic.future_annotations()
@@ -799,7 +824,7 @@ where
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = expr {
self.deferred.string_type_definitions.push((
expr.range(),
value.as_str(),
value.to_str(),
self.semantic.snapshot(),
));
} else {
@@ -1173,7 +1198,7 @@ where
) {
// Ex) Literal["Class"]
Some(typing::SubscriptKind::Literal) => {
self.semantic.flags |= SemanticModelFlags::LITERAL;
self.semantic.flags |= SemanticModelFlags::TYPING_LITERAL;
self.visit_expr(slice);
self.visit_expr_context(ctx);
@@ -1214,12 +1239,12 @@ where
}
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
if self.semantic.in_type_definition()
&& !self.semantic.in_literal()
&& !self.semantic.in_typing_literal()
&& !self.semantic.in_f_string()
{
self.deferred.string_type_definitions.push((
expr.range(),
value.as_str(),
value.to_str(),
self.semantic.snapshot(),
));
}

View File

@@ -3,10 +3,10 @@
use std::path::Path;
use itertools::Itertools;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ruff_text_size::{Ranged, TextLen, TextRange};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_python_trivia::CommentRanges;
use ruff_python_trivia::{CommentRanges, PythonWhitespace};
use ruff_source_file::Locator;
use crate::noqa;
@@ -200,17 +200,11 @@ fn delete_noqa(range: TextRange, locator: &Locator) -> Edit {
// Compute the leading space.
let prefix = locator.slice(TextRange::new(line_range.start(), range.start()));
let leading_space = prefix
.rfind(|c: char| !c.is_whitespace())
.map_or(prefix.len(), |i| prefix.len() - i - 1);
let leading_space_len = TextSize::try_from(leading_space).unwrap();
let leading_space_len = prefix.text_len() - prefix.trim_whitespace_end().text_len();
// Compute the trailing space.
let suffix = locator.slice(TextRange::new(range.end(), line_range.end()));
let trailing_space = suffix
.find(|c: char| !c.is_whitespace())
.map_or(suffix.len(), |i| i);
let trailing_space_len = TextSize::try_from(trailing_space).unwrap();
let trailing_space_len = suffix.text_len() - suffix.trim_whitespace_start().text_len();
// Ex) `# noqa`
if line_range

View File

@@ -245,6 +245,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "E2515") => (RuleGroup::Stable, rules::pylint::rules::InvalidCharacterZeroWidthSpace),
(Pylint, "R0124") => (RuleGroup::Stable, rules::pylint::rules::ComparisonWithItself),
(Pylint, "R0133") => (RuleGroup::Stable, rules::pylint::rules::ComparisonOfConstant),
(Pylint, "R0202") => (RuleGroup::Preview, rules::pylint::rules::NoClassmethodDecorator),
(Pylint, "R0203") => (RuleGroup::Preview, rules::pylint::rules::NoStaticmethodDecorator),
(Pylint, "R0206") => (RuleGroup::Stable, rules::pylint::rules::PropertyWithParameters),
(Pylint, "R0402") => (RuleGroup::Stable, rules::pylint::rules::ManualFromImport),
(Pylint, "R0911") => (RuleGroup::Stable, rules::pylint::rules::TooManyReturnStatements),
@@ -252,12 +254,15 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R0913") => (RuleGroup::Stable, rules::pylint::rules::TooManyArguments),
(Pylint, "R0915") => (RuleGroup::Stable, rules::pylint::rules::TooManyStatements),
(Pylint, "R0916") => (RuleGroup::Preview, rules::pylint::rules::TooManyBooleanExpressions),
(Pylint, "R0917") => (RuleGroup::Preview, rules::pylint::rules::TooManyPositional),
(Pylint, "R1701") => (RuleGroup::Stable, rules::pylint::rules::RepeatedIsinstanceCalls),
(Pylint, "R1704") => (RuleGroup::Preview, rules::pylint::rules::RedefinedArgumentFromLocal),
(Pylint, "R1711") => (RuleGroup::Stable, rules::pylint::rules::UselessReturn),
(Pylint, "R1714") => (RuleGroup::Stable, rules::pylint::rules::RepeatedEqualityComparison),
(Pylint, "R1706") => (RuleGroup::Preview, rules::pylint::rules::AndOrTernary),
(Pylint, "R1722") => (RuleGroup::Stable, rules::pylint::rules::SysExitAlias),
(Pylint, "R1733") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryDictIndexLookup),
(Pylint, "R1736") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryListIndexLookup),
(Pylint, "R2004") => (RuleGroup::Stable, rules::pylint::rules::MagicValueComparison),
(Pylint, "R5501") => (RuleGroup::Stable, rules::pylint::rules::CollapsibleElseIf),
(Pylint, "R6201") => (RuleGroup::Preview, rules::pylint::rules::LiteralMembership),
@@ -955,6 +960,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Refurb, "145") => (RuleGroup::Preview, rules::refurb::rules::SliceCopy),
(Refurb, "148") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryEnumerate),
(Refurb, "152") => (RuleGroup::Preview, rules::refurb::rules::MathConstant),
(Refurb, "163") => (RuleGroup::Preview, rules::refurb::rules::RedundantLogBase),
(Refurb, "168") => (RuleGroup::Preview, rules::refurb::rules::IsinstanceTypeNone),
(Refurb, "169") => (RuleGroup::Preview, rules::refurb::rules::TypeNoneComparison),
(Refurb, "171") => (RuleGroup::Preview, rules::refurb::rules::SingleItemMembershipTest),

View File

@@ -3,12 +3,14 @@
use anyhow::{Context, Result};
use ruff_diagnostics::Edit;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Stmt};
use ruff_python_ast::{AnyNodeRef, ArgOrKeyword};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_trivia::{
has_leading_content, is_python_whitespace, PythonWhitespace, SimpleTokenKind, SimpleTokenizer,
has_leading_content, is_python_whitespace, CommentRanges, PythonWhitespace, SimpleTokenKind,
SimpleTokenizer,
};
use ruff_source_file::{Locator, NewlineWithTrailingNewline, UniversalNewlines};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
@@ -138,6 +140,32 @@ pub(crate) fn remove_argument<T: Ranged>(
}
}
/// Generic function to add arguments or keyword arguments to function calls.
pub(crate) fn add_argument(
argument: &str,
arguments: &Arguments,
comment_ranges: &CommentRanges,
source: &str,
) -> Edit {
if let Some(last) = arguments.arguments_source_order().last() {
// Case 1: existing arguments, so append after the last argument.
let last = parenthesized_range(
match last {
ArgOrKeyword::Arg(arg) => arg.into(),
ArgOrKeyword::Keyword(keyword) => (&keyword.value).into(),
},
arguments.into(),
comment_ranges,
source,
)
.unwrap_or(last.range());
Edit::insertion(format!(", {argument}"), last.end())
} else {
// Case 2: no arguments. Add argument, without any trailing comma.
Edit::insertion(argument.to_string(), arguments.start() + TextSize::from(1))
}
}
/// Determine if a vector contains only one, specific element.
fn is_only<T: PartialEq>(vec: &[T], value: &T) -> bool {
vec.len() == 1 && vec[0] == *value

View File

@@ -1,13 +1,18 @@
use itertools::Itertools;
use rustc_hash::FxHashSet;
use ruff_python_ast::helpers::{pep_604_union, ReturnStatementVisitor};
use ruff_diagnostics::Edit;
use ruff_python_ast::helpers::{
implicit_return, pep_604_union, typing_optional, typing_union, ReturnStatementVisitor,
};
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Expr, ExprContext};
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Definition, SemanticModel};
use ruff_text_size::TextRange;
use ruff_text_size::{TextRange, TextSize};
use crate::importer::{ImportRequest, Importer};
use crate::settings::types::PythonVersion;
/// Return the name of the function, if it's overloaded.
@@ -38,22 +43,24 @@ pub(crate) fn is_overload_impl(
}
/// Given a function, guess its return type.
pub(crate) fn auto_return_type(
function: &ast::StmtFunctionDef,
target_version: PythonVersion,
) -> Option<Expr> {
pub(crate) fn auto_return_type(function: &ast::StmtFunctionDef) -> Option<AutoPythonType> {
// Collect all the `return` statements.
let returns = {
let mut visitor = ReturnStatementVisitor::default();
visitor.visit_body(&function.body);
// Ignore generators.
if visitor.is_generator {
return None;
}
visitor.returns
};
// Determine the return type of the first `return` statement.
let (return_statement, returns) = returns.split_first()?;
let Some((return_statement, returns)) = returns.split_first() else {
return Some(AutoPythonType::Atom(PythonType::None));
};
let mut return_type = return_statement.value.as_deref().map_or(
ResolvedPythonType::Atom(PythonType::None),
ResolvedPythonType::from,
@@ -67,25 +74,105 @@ pub(crate) fn auto_return_type(
));
}
match return_type {
ResolvedPythonType::Atom(python_type) => type_expr(python_type),
ResolvedPythonType::Union(python_types) if target_version >= PythonVersion::Py310 => {
// Aggregate all the individual types (e.g., `int`, `float`).
let names = python_types
.iter()
.sorted_unstable()
.filter_map(|python_type| type_expr(*python_type))
.collect::<Vec<_>>();
// If the function has an implicit return, union with `None`, as in:
// ```python
// def func(x: int):
// if x > 0:
// return 1
// ```
if implicit_return(function) {
return_type = return_type.union(ResolvedPythonType::Atom(PythonType::None));
}
// Wrap in a bitwise union (e.g., `int | float`).
Some(pep_604_union(&names))
}
ResolvedPythonType::Union(_) => None,
match return_type {
ResolvedPythonType::Atom(python_type) => Some(AutoPythonType::Atom(python_type)),
ResolvedPythonType::Union(python_types) => Some(AutoPythonType::Union(python_types)),
ResolvedPythonType::Unknown => None,
ResolvedPythonType::TypeError => None,
}
}
#[derive(Debug)]
pub(crate) enum AutoPythonType {
Atom(PythonType),
Union(FxHashSet<PythonType>),
}
impl AutoPythonType {
/// Convert an [`AutoPythonType`] into an [`Expr`].
///
/// If the [`Expr`] relies on importing any external symbols, those imports will be returned as
/// additional edits.
pub(crate) fn into_expression(
self,
importer: &Importer,
at: TextSize,
semantic: &SemanticModel,
target_version: PythonVersion,
) -> Option<(Expr, Vec<Edit>)> {
match self {
AutoPythonType::Atom(python_type) => {
let expr = type_expr(python_type)?;
Some((expr, vec![]))
}
AutoPythonType::Union(python_types) => {
if target_version >= PythonVersion::Py310 {
// Aggregate all the individual types (e.g., `int`, `float`).
let names = python_types
.iter()
.sorted_unstable()
.map(|python_type| type_expr(*python_type))
.collect::<Option<Vec<_>>>()?;
// Wrap in a bitwise union (e.g., `int | float`).
let expr = pep_604_union(&names);
Some((expr, vec![]))
} else {
let python_types = python_types
.into_iter()
.sorted_unstable()
.collect::<Vec<_>>();
match python_types.as_slice() {
[python_type, PythonType::None] | [PythonType::None, python_type] => {
let element = type_expr(*python_type)?;
// Ex) `Optional[int]`
let (optional_edit, binding) = importer
.get_or_import_symbol(
&ImportRequest::import_from("typing", "Optional"),
at,
semantic,
)
.ok()?;
let expr = typing_optional(element, binding);
Some((expr, vec![optional_edit]))
}
_ => {
let elements = python_types
.into_iter()
.map(type_expr)
.collect::<Option<Vec<_>>>()?;
// Ex) `Union[int, str]`
let (union_edit, binding) = importer
.get_or_import_symbol(
&ImportRequest::import_from("typing", "Union"),
at,
semantic,
)
.ok()?;
let expr = typing_union(&elements, binding);
Some((expr, vec![union_edit]))
}
}
}
}
}
}
}
/// Given a [`PythonType`], return an [`Expr`] that resolves to that type.
fn type_expr(python_type: PythonType) -> Option<Expr> {
fn name(name: &str) -> Expr {

View File

@@ -11,6 +11,7 @@ mod tests {
use crate::assert_messages;
use crate::registry::Rule;
use crate::settings::types::PythonVersion;
use crate::settings::LinterSettings;
use crate::test::test_path;
@@ -128,6 +129,25 @@ mod tests {
Ok(())
}
#[test]
fn auto_return_type_py38() -> Result<()> {
let diagnostics = test_path(
Path::new("flake8_annotations/auto_return_type.py"),
&LinterSettings {
target_version: PythonVersion::Py38,
..LinterSettings::for_rules(vec![
Rule::MissingReturnTypeUndocumentedPublicFunction,
Rule::MissingReturnTypePrivateFunction,
Rule::MissingReturnTypeSpecialMethod,
Rule::MissingReturnTypeStaticMethod,
Rule::MissingReturnTypeClassMethod,
])
},
)?;
assert_messages!(diagnostics);
Ok(())
}
#[test]
fn suppress_none_returning() -> Result<()> {
let diagnostics = test_path(

View File

@@ -508,7 +508,7 @@ fn check_dynamically_typed<F>(
if let Expr::StringLiteral(ast::ExprStringLiteral { range, value }) = annotation {
// Quoted annotations
if let Ok((parsed_annotation, _)) =
parse_type_annotation(value.as_str(), *range, checker.locator().contents())
parse_type_annotation(value.to_str(), *range, checker.locator().contents())
{
if type_hint_resolves_to_any(
&parsed_annotation,
@@ -725,39 +725,55 @@ pub(crate) fn definition(
) {
if is_method && visibility::is_classmethod(decorator_list, checker.semantic()) {
if checker.enabled(Rule::MissingReturnTypeClassMethod) {
let return_type = auto_return_type(function, checker.settings.target_version)
.map(|return_type| checker.generator().expr(&return_type));
let return_type = auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits));
let mut diagnostic = Diagnostic::new(
MissingReturnTypeClassMethod {
name: name.to_string(),
annotation: return_type.clone(),
annotation: return_type.clone().map(|(return_type, ..)| return_type),
},
function.identifier(),
);
if let Some(return_type) = return_type {
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
format!(" -> {return_type}"),
function.parameters.range().end(),
)));
if let Some((return_type, edits)) = return_type {
diagnostic.set_fix(Fix::unsafe_edits(
Edit::insertion(format!(" -> {return_type}"), function.parameters.end()),
edits,
));
}
diagnostics.push(diagnostic);
}
} else if is_method && visibility::is_staticmethod(decorator_list, checker.semantic()) {
if checker.enabled(Rule::MissingReturnTypeStaticMethod) {
let return_type = auto_return_type(function, checker.settings.target_version)
.map(|return_type| checker.generator().expr(&return_type));
let return_type = auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits));
let mut diagnostic = Diagnostic::new(
MissingReturnTypeStaticMethod {
name: name.to_string(),
annotation: return_type.clone(),
annotation: return_type.clone().map(|(return_type, ..)| return_type),
},
function.identifier(),
);
if let Some(return_type) = return_type {
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
format!(" -> {return_type}"),
function.parameters.range().end(),
)));
if let Some((return_type, edits)) = return_type {
diagnostic.set_fix(Fix::unsafe_edits(
Edit::insertion(format!(" -> {return_type}"), function.parameters.end()),
edits,
));
}
diagnostics.push(diagnostic);
}
@@ -775,7 +791,7 @@ pub(crate) fn definition(
);
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
" -> None".to_string(),
function.parameters.range().end(),
function.parameters.end(),
)));
diagnostics.push(diagnostic);
}
@@ -793,7 +809,7 @@ pub(crate) fn definition(
if let Some(return_type) = return_type {
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
format!(" -> {return_type}"),
function.parameters.range().end(),
function.parameters.end(),
)));
}
diagnostics.push(diagnostic);
@@ -802,42 +818,70 @@ pub(crate) fn definition(
match visibility {
visibility::Visibility::Public => {
if checker.enabled(Rule::MissingReturnTypeUndocumentedPublicFunction) {
let return_type =
auto_return_type(function, checker.settings.target_version)
.map(|return_type| checker.generator().expr(&return_type));
let return_type = auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| {
(checker.generator().expr(&return_type), edits)
});
let mut diagnostic = Diagnostic::new(
MissingReturnTypeUndocumentedPublicFunction {
name: name.to_string(),
annotation: return_type.clone(),
annotation: return_type
.clone()
.map(|(return_type, ..)| return_type),
},
function.identifier(),
);
if let Some(return_type) = return_type {
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
format!(" -> {return_type}"),
function.parameters.range().end(),
)));
if let Some((return_type, edits)) = return_type {
diagnostic.set_fix(Fix::unsafe_edits(
Edit::insertion(
format!(" -> {return_type}"),
function.parameters.end(),
),
edits,
));
}
diagnostics.push(diagnostic);
}
}
visibility::Visibility::Private => {
if checker.enabled(Rule::MissingReturnTypePrivateFunction) {
let return_type =
auto_return_type(function, checker.settings.target_version)
.map(|return_type| checker.generator().expr(&return_type));
let return_type = auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| {
(checker.generator().expr(&return_type), edits)
});
let mut diagnostic = Diagnostic::new(
MissingReturnTypePrivateFunction {
name: name.to_string(),
annotation: return_type.clone(),
annotation: return_type
.clone()
.map(|(return_type, ..)| return_type),
},
function.identifier(),
);
if let Some(return_type) = return_type {
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
format!(" -> {return_type}"),
function.parameters.range().end(),
)));
if let Some((return_type, edits)) = return_type {
diagnostic.set_fix(Fix::unsafe_edits(
Edit::insertion(
format!(" -> {return_type}"),
function.parameters.end(),
),
edits,
));
}
diagnostics.push(diagnostic);
}

View File

@@ -123,5 +123,308 @@ auto_return_type.py:31:5: ANN201 [*] Missing return type annotation for public f
31 |-def func(x: int):
31 |+def func(x: int) -> str | float:
32 32 | return 1 + 2.5 if x > 0 else 1.5 or "str"
33 33 |
34 34 |
auto_return_type.py:35:5: ANN201 Missing return type annotation for public function `func`
|
35 | def func(x: int):
| ^^^^ ANN201
36 | if not x:
37 | return None
|
= help: Add return type annotation
auto_return_type.py:41:5: ANN201 Missing return type annotation for public function `func`
|
41 | def func(x: int):
| ^^^^ ANN201
42 | return {"foo": 1}
|
= help: Add return type annotation
auto_return_type.py:45:5: ANN201 [*] Missing return type annotation for public function `func`
|
45 | def func(x: int):
| ^^^^ ANN201
46 | if not x:
47 | return 1
|
= help: Add return type annotation: `int`
Unsafe fix
42 42 | return {"foo": 1}
43 43 |
44 44 |
45 |-def func(x: int):
45 |+def func(x: int) -> int:
46 46 | if not x:
47 47 | return 1
48 48 | else:
auto_return_type.py:52:5: ANN201 [*] Missing return type annotation for public function `func`
|
52 | def func(x: int):
| ^^^^ ANN201
53 | if not x:
54 | return 1
|
= help: Add return type annotation: `int | None`
Unsafe fix
49 49 | return True
50 50 |
51 51 |
52 |-def func(x: int):
52 |+def func(x: int) -> int | None:
53 53 | if not x:
54 54 | return 1
55 55 | else:
auto_return_type.py:59:5: ANN201 [*] Missing return type annotation for public function `func`
|
59 | def func(x: int):
| ^^^^ ANN201
60 | if not x:
61 | return 1
|
= help: Add return type annotation: `str | int | None`
Unsafe fix
56 56 | return None
57 57 |
58 58 |
59 |-def func(x: int):
59 |+def func(x: int) -> str | int | None:
60 60 | if not x:
61 61 | return 1
62 62 | elif x > 5:
auto_return_type.py:68:5: ANN201 [*] Missing return type annotation for public function `func`
|
68 | def func(x: int):
| ^^^^ ANN201
69 | if x:
70 | return 1
|
= help: Add return type annotation: `int | None`
Unsafe fix
65 65 | return None
66 66 |
67 67 |
68 |-def func(x: int):
68 |+def func(x: int) -> int | None:
69 69 | if x:
70 70 | return 1
71 71 |
auto_return_type.py:73:5: ANN201 [*] Missing return type annotation for public function `func`
|
73 | def func():
| ^^^^ ANN201
74 | x = 1
|
= help: Add return type annotation: `None`
Unsafe fix
70 70 | return 1
71 71 |
72 72 |
73 |-def func():
73 |+def func() -> None:
74 74 | x = 1
75 75 |
76 76 |
auto_return_type.py:77:5: ANN201 [*] Missing return type annotation for public function `func`
|
77 | def func(x: int):
| ^^^^ ANN201
78 | if x > 0:
79 | return 1
|
= help: Add return type annotation: `int | None`
Unsafe fix
74 74 | x = 1
75 75 |
76 76 |
77 |-def func(x: int):
77 |+def func(x: int) -> int | None:
78 78 | if x > 0:
79 79 | return 1
80 80 |
auto_return_type.py:82:5: ANN201 [*] Missing return type annotation for public function `func`
|
82 | def func(x: int):
| ^^^^ ANN201
83 | match x:
84 | case [1, 2, 3]:
|
= help: Add return type annotation: `str | int`
Unsafe fix
79 79 | return 1
80 80 |
81 81 |
82 |-def func(x: int):
82 |+def func(x: int) -> str | int:
83 83 | match x:
84 84 | case [1, 2, 3]:
85 85 | return 1
auto_return_type.py:90:5: ANN201 [*] Missing return type annotation for public function `func`
|
90 | def func(x: int):
| ^^^^ ANN201
91 | for i in range(5):
92 | if i > 0:
|
= help: Add return type annotation: `int | None`
Unsafe fix
87 87 | return "foo"
88 88 |
89 89 |
90 |-def func(x: int):
90 |+def func(x: int) -> int | None:
91 91 | for i in range(5):
92 92 | if i > 0:
93 93 | return 1
auto_return_type.py:96:5: ANN201 [*] Missing return type annotation for public function `func`
|
96 | def func(x: int):
| ^^^^ ANN201
97 | for i in range(5):
98 | if i > 0:
|
= help: Add return type annotation: `int`
Unsafe fix
93 93 | return 1
94 94 |
95 95 |
96 |-def func(x: int):
96 |+def func(x: int) -> int:
97 97 | for i in range(5):
98 98 | if i > 0:
99 99 | return 1
auto_return_type.py:104:5: ANN201 [*] Missing return type annotation for public function `func`
|
104 | def func(x: int):
| ^^^^ ANN201
105 | for i in range(5):
106 | if i > 0:
|
= help: Add return type annotation: `int | None`
Unsafe fix
101 101 | return 4
102 102 |
103 103 |
104 |-def func(x: int):
104 |+def func(x: int) -> int | None:
105 105 | for i in range(5):
106 106 | if i > 0:
107 107 | break
auto_return_type.py:112:5: ANN201 [*] Missing return type annotation for public function `func`
|
112 | def func(x: int):
| ^^^^ ANN201
113 | try:
114 | pass
|
= help: Add return type annotation: `int | None`
Unsafe fix
109 109 | return 4
110 110 |
111 111 |
112 |-def func(x: int):
112 |+def func(x: int) -> int | None:
113 113 | try:
114 114 | pass
115 115 | except:
auto_return_type.py:119:5: ANN201 [*] Missing return type annotation for public function `func`
|
119 | def func(x: int):
| ^^^^ ANN201
120 | try:
121 | pass
|
= help: Add return type annotation: `int`
Unsafe fix
116 116 | return 1
117 117 |
118 118 |
119 |-def func(x: int):
119 |+def func(x: int) -> int:
120 120 | try:
121 121 | pass
122 122 | except:
auto_return_type.py:128:5: ANN201 [*] Missing return type annotation for public function `func`
|
128 | def func(x: int):
| ^^^^ ANN201
129 | try:
130 | pass
|
= help: Add return type annotation: `int`
Unsafe fix
125 125 | return 2
126 126 |
127 127 |
128 |-def func(x: int):
128 |+def func(x: int) -> int:
129 129 | try:
130 130 | pass
131 131 | except:
auto_return_type.py:137:5: ANN201 [*] Missing return type annotation for public function `func`
|
137 | def func(x: int):
| ^^^^ ANN201
138 | try:
139 | return 1
|
= help: Add return type annotation: `int`
Unsafe fix
134 134 | return 2
135 135 |
136 136 |
137 |-def func(x: int):
137 |+def func(x: int) -> int:
138 138 | try:
139 139 | return 1
140 140 | except:
auto_return_type.py:146:5: ANN201 [*] Missing return type annotation for public function `func`
|
146 | def func(x: int):
| ^^^^ ANN201
147 | while x > 0:
148 | break
|
= help: Add return type annotation: `int | None`
Unsafe fix
143 143 | pass
144 144 |
145 145 |
146 |-def func(x: int):
146 |+def func(x: int) -> int | None:
147 147 | while x > 0:
148 148 | break
149 149 | return 1

View File

@@ -0,0 +1,485 @@
---
source: crates/ruff_linter/src/rules/flake8_annotations/mod.rs
---
auto_return_type.py:1:5: ANN201 [*] Missing return type annotation for public function `func`
|
1 | def func():
| ^^^^ ANN201
2 | return 1
|
= help: Add return type annotation: `int`
Unsafe fix
1 |-def func():
1 |+def func() -> int:
2 2 | return 1
3 3 |
4 4 |
auto_return_type.py:5:5: ANN201 [*] Missing return type annotation for public function `func`
|
5 | def func():
| ^^^^ ANN201
6 | return 1.5
|
= help: Add return type annotation: `float`
Unsafe fix
2 2 | return 1
3 3 |
4 4 |
5 |-def func():
5 |+def func() -> float:
6 6 | return 1.5
7 7 |
8 8 |
auto_return_type.py:9:5: ANN201 [*] Missing return type annotation for public function `func`
|
9 | def func(x: int):
| ^^^^ ANN201
10 | if x > 0:
11 | return 1
|
= help: Add return type annotation: `float`
Unsafe fix
6 6 | return 1.5
7 7 |
8 8 |
9 |-def func(x: int):
9 |+def func(x: int) -> float:
10 10 | if x > 0:
11 11 | return 1
12 12 | else:
auto_return_type.py:16:5: ANN201 [*] Missing return type annotation for public function `func`
|
16 | def func():
| ^^^^ ANN201
17 | return True
|
= help: Add return type annotation: `bool`
Unsafe fix
13 13 | return 1.5
14 14 |
15 15 |
16 |-def func():
16 |+def func() -> bool:
17 17 | return True
18 18 |
19 19 |
auto_return_type.py:20:5: ANN201 [*] Missing return type annotation for public function `func`
|
20 | def func(x: int):
| ^^^^ ANN201
21 | if x > 0:
22 | return None
|
= help: Add return type annotation: `None`
Unsafe fix
17 17 | return True
18 18 |
19 19 |
20 |-def func(x: int):
20 |+def func(x: int) -> None:
21 21 | if x > 0:
22 22 | return None
23 23 | else:
auto_return_type.py:27:5: ANN201 [*] Missing return type annotation for public function `func`
|
27 | def func(x: int):
| ^^^^ ANN201
28 | return 1 or 2.5 if x > 0 else 1.5 or "str"
|
= help: Add return type annotation: `Union[str | float]`
Unsafe fix
1 |+from typing import Union
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
24 25 | return
25 26 |
26 27 |
27 |-def func(x: int):
28 |+def func(x: int) -> Union[str | float]:
28 29 | return 1 or 2.5 if x > 0 else 1.5 or "str"
29 30 |
30 31 |
auto_return_type.py:31:5: ANN201 [*] Missing return type annotation for public function `func`
|
31 | def func(x: int):
| ^^^^ ANN201
32 | return 1 + 2.5 if x > 0 else 1.5 or "str"
|
= help: Add return type annotation: `Union[str | float]`
Unsafe fix
1 |+from typing import Union
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
28 29 | return 1 or 2.5 if x > 0 else 1.5 or "str"
29 30 |
30 31 |
31 |-def func(x: int):
32 |+def func(x: int) -> Union[str | float]:
32 33 | return 1 + 2.5 if x > 0 else 1.5 or "str"
33 34 |
34 35 |
auto_return_type.py:35:5: ANN201 Missing return type annotation for public function `func`
|
35 | def func(x: int):
| ^^^^ ANN201
36 | if not x:
37 | return None
|
= help: Add return type annotation
auto_return_type.py:41:5: ANN201 Missing return type annotation for public function `func`
|
41 | def func(x: int):
| ^^^^ ANN201
42 | return {"foo": 1}
|
= help: Add return type annotation
auto_return_type.py:45:5: ANN201 [*] Missing return type annotation for public function `func`
|
45 | def func(x: int):
| ^^^^ ANN201
46 | if not x:
47 | return 1
|
= help: Add return type annotation: `int`
Unsafe fix
42 42 | return {"foo": 1}
43 43 |
44 44 |
45 |-def func(x: int):
45 |+def func(x: int) -> int:
46 46 | if not x:
47 47 | return 1
48 48 | else:
auto_return_type.py:52:5: ANN201 [*] Missing return type annotation for public function `func`
|
52 | def func(x: int):
| ^^^^ ANN201
53 | if not x:
54 | return 1
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
49 50 | return True
50 51 |
51 52 |
52 |-def func(x: int):
53 |+def func(x: int) -> Optional[int]:
53 54 | if not x:
54 55 | return 1
55 56 | else:
auto_return_type.py:59:5: ANN201 [*] Missing return type annotation for public function `func`
|
59 | def func(x: int):
| ^^^^ ANN201
60 | if not x:
61 | return 1
|
= help: Add return type annotation: `Union[str | int | None]`
Unsafe fix
1 |+from typing import Union
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
56 57 | return None
57 58 |
58 59 |
59 |-def func(x: int):
60 |+def func(x: int) -> Union[str | int | None]:
60 61 | if not x:
61 62 | return 1
62 63 | elif x > 5:
auto_return_type.py:68:5: ANN201 [*] Missing return type annotation for public function `func`
|
68 | def func(x: int):
| ^^^^ ANN201
69 | if x:
70 | return 1
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
65 66 | return None
66 67 |
67 68 |
68 |-def func(x: int):
69 |+def func(x: int) -> Optional[int]:
69 70 | if x:
70 71 | return 1
71 72 |
auto_return_type.py:73:5: ANN201 [*] Missing return type annotation for public function `func`
|
73 | def func():
| ^^^^ ANN201
74 | x = 1
|
= help: Add return type annotation: `None`
Unsafe fix
70 70 | return 1
71 71 |
72 72 |
73 |-def func():
73 |+def func() -> None:
74 74 | x = 1
75 75 |
76 76 |
auto_return_type.py:77:5: ANN201 [*] Missing return type annotation for public function `func`
|
77 | def func(x: int):
| ^^^^ ANN201
78 | if x > 0:
79 | return 1
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
74 75 | x = 1
75 76 |
76 77 |
77 |-def func(x: int):
78 |+def func(x: int) -> Optional[int]:
78 79 | if x > 0:
79 80 | return 1
80 81 |
auto_return_type.py:82:5: ANN201 [*] Missing return type annotation for public function `func`
|
82 | def func(x: int):
| ^^^^ ANN201
83 | match x:
84 | case [1, 2, 3]:
|
= help: Add return type annotation: `Union[str | int]`
Unsafe fix
1 |+from typing import Union
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
79 80 | return 1
80 81 |
81 82 |
82 |-def func(x: int):
83 |+def func(x: int) -> Union[str | int]:
83 84 | match x:
84 85 | case [1, 2, 3]:
85 86 | return 1
auto_return_type.py:90:5: ANN201 [*] Missing return type annotation for public function `func`
|
90 | def func(x: int):
| ^^^^ ANN201
91 | for i in range(5):
92 | if i > 0:
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
87 88 | return "foo"
88 89 |
89 90 |
90 |-def func(x: int):
91 |+def func(x: int) -> Optional[int]:
91 92 | for i in range(5):
92 93 | if i > 0:
93 94 | return 1
auto_return_type.py:96:5: ANN201 [*] Missing return type annotation for public function `func`
|
96 | def func(x: int):
| ^^^^ ANN201
97 | for i in range(5):
98 | if i > 0:
|
= help: Add return type annotation: `int`
Unsafe fix
93 93 | return 1
94 94 |
95 95 |
96 |-def func(x: int):
96 |+def func(x: int) -> int:
97 97 | for i in range(5):
98 98 | if i > 0:
99 99 | return 1
auto_return_type.py:104:5: ANN201 [*] Missing return type annotation for public function `func`
|
104 | def func(x: int):
| ^^^^ ANN201
105 | for i in range(5):
106 | if i > 0:
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
101 102 | return 4
102 103 |
103 104 |
104 |-def func(x: int):
105 |+def func(x: int) -> Optional[int]:
105 106 | for i in range(5):
106 107 | if i > 0:
107 108 | break
auto_return_type.py:112:5: ANN201 [*] Missing return type annotation for public function `func`
|
112 | def func(x: int):
| ^^^^ ANN201
113 | try:
114 | pass
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
109 110 | return 4
110 111 |
111 112 |
112 |-def func(x: int):
113 |+def func(x: int) -> Optional[int]:
113 114 | try:
114 115 | pass
115 116 | except:
auto_return_type.py:119:5: ANN201 [*] Missing return type annotation for public function `func`
|
119 | def func(x: int):
| ^^^^ ANN201
120 | try:
121 | pass
|
= help: Add return type annotation: `int`
Unsafe fix
116 116 | return 1
117 117 |
118 118 |
119 |-def func(x: int):
119 |+def func(x: int) -> int:
120 120 | try:
121 121 | pass
122 122 | except:
auto_return_type.py:128:5: ANN201 [*] Missing return type annotation for public function `func`
|
128 | def func(x: int):
| ^^^^ ANN201
129 | try:
130 | pass
|
= help: Add return type annotation: `int`
Unsafe fix
125 125 | return 2
126 126 |
127 127 |
128 |-def func(x: int):
128 |+def func(x: int) -> int:
129 129 | try:
130 130 | pass
131 131 | except:
auto_return_type.py:137:5: ANN201 [*] Missing return type annotation for public function `func`
|
137 | def func(x: int):
| ^^^^ ANN201
138 | try:
139 | return 1
|
= help: Add return type annotation: `int`
Unsafe fix
134 134 | return 2
135 135 |
136 136 |
137 |-def func(x: int):
137 |+def func(x: int) -> int:
138 138 | try:
139 139 | return 1
140 140 | except:
auto_return_type.py:146:5: ANN201 [*] Missing return type annotation for public function `func`
|
146 | def func(x: int):
| ^^^^ ANN201
147 | while x > 0:
148 | break
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
143 144 | pass
144 145 |
145 146 |
146 |-def func(x: int):
147 |+def func(x: int) -> Optional[int]:
147 148 | while x > 0:
148 149 | break
149 150 | return 1

View File

@@ -1,14 +1,24 @@
---
source: crates/ruff_linter/src/rules/flake8_annotations/mod.rs
---
annotation_presence.py:5:5: ANN201 Missing return type annotation for public function `foo`
annotation_presence.py:5:5: ANN201 [*] Missing return type annotation for public function `foo`
|
4 | # Error
5 | def foo(a, b):
| ^^^ ANN201
6 | pass
|
= help: Add return type annotation
= help: Add return type annotation: `None`
Unsafe fix
2 2 | from typing_extensions import override
3 3 |
4 4 | # Error
5 |-def foo(a, b):
5 |+def foo(a, b) -> None:
6 6 | pass
7 7 |
8 8 |
annotation_presence.py:5:9: ANN001 Missing type annotation for function argument `a`
|
@@ -26,14 +36,24 @@ annotation_presence.py:5:12: ANN001 Missing type annotation for function argumen
6 | pass
|
annotation_presence.py:10:5: ANN201 Missing return type annotation for public function `foo`
annotation_presence.py:10:5: ANN201 [*] Missing return type annotation for public function `foo`
|
9 | # Error
10 | def foo(a: int, b):
| ^^^ ANN201
11 | pass
|
= help: Add return type annotation
= help: Add return type annotation: `None`
Unsafe fix
7 7 |
8 8 |
9 9 | # Error
10 |-def foo(a: int, b):
10 |+def foo(a: int, b) -> None:
11 11 | pass
12 12 |
13 13 |
annotation_presence.py:10:17: ANN001 Missing type annotation for function argument `b`
|
@@ -51,23 +71,43 @@ annotation_presence.py:15:17: ANN001 Missing type annotation for function argume
16 | pass
|
annotation_presence.py:20:5: ANN201 Missing return type annotation for public function `foo`
annotation_presence.py:20:5: ANN201 [*] Missing return type annotation for public function `foo`
|
19 | # Error
20 | def foo(a: int, b: int):
| ^^^ ANN201
21 | pass
|
= help: Add return type annotation
= help: Add return type annotation: `None`
annotation_presence.py:25:5: ANN201 Missing return type annotation for public function `foo`
Unsafe fix
17 17 |
18 18 |
19 19 | # Error
20 |-def foo(a: int, b: int):
20 |+def foo(a: int, b: int) -> None:
21 21 | pass
22 22 |
23 23 |
annotation_presence.py:25:5: ANN201 [*] Missing return type annotation for public function `foo`
|
24 | # Error
25 | def foo():
| ^^^ ANN201
26 | pass
|
= help: Add return type annotation
= help: Add return type annotation: `None`
Unsafe fix
22 22 |
23 23 |
24 24 | # Error
25 |-def foo():
25 |+def foo() -> None:
26 26 | pass
27 27 |
28 28 |
annotation_presence.py:45:12: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|

View File

@@ -1,13 +1,23 @@
---
source: crates/ruff_linter/src/rules/flake8_annotations/mod.rs
---
ignore_fully_untyped.py:24:5: ANN201 Missing return type annotation for public function `error_partially_typed_1`
ignore_fully_untyped.py:24:5: ANN201 [*] Missing return type annotation for public function `error_partially_typed_1`
|
24 | def error_partially_typed_1(a: int, b):
| ^^^^^^^^^^^^^^^^^^^^^^^ ANN201
25 | pass
|
= help: Add return type annotation
= help: Add return type annotation: `None`
Unsafe fix
21 21 | pass
22 22 |
23 23 |
24 |-def error_partially_typed_1(a: int, b):
24 |+def error_partially_typed_1(a: int, b) -> None:
25 25 | pass
26 26 |
27 27 |
ignore_fully_untyped.py:24:37: ANN001 Missing type annotation for function argument `b`
|
@@ -23,15 +33,25 @@ ignore_fully_untyped.py:28:37: ANN001 Missing type annotation for function argum
29 | pass
|
ignore_fully_untyped.py:32:5: ANN201 Missing return type annotation for public function `error_partially_typed_3`
ignore_fully_untyped.py:32:5: ANN201 [*] Missing return type annotation for public function `error_partially_typed_3`
|
32 | def error_partially_typed_3(a: int, b: int):
| ^^^^^^^^^^^^^^^^^^^^^^^ ANN201
33 | pass
|
= help: Add return type annotation
= help: Add return type annotation: `None`
ignore_fully_untyped.py:43:9: ANN201 Missing return type annotation for public function `error_typed_self`
Unsafe fix
29 29 | pass
30 30 |
31 31 |
32 |-def error_partially_typed_3(a: int, b: int):
32 |+def error_partially_typed_3(a: int, b: int) -> None:
33 33 | pass
34 34 |
35 35 |
ignore_fully_untyped.py:43:9: ANN201 [*] Missing return type annotation for public function `error_typed_self`
|
41 | pass
42 |
@@ -39,6 +59,14 @@ ignore_fully_untyped.py:43:9: ANN201 Missing return type annotation for public f
| ^^^^^^^^^^^^^^^^ ANN201
44 | pass
|
= help: Add return type annotation
= help: Add return type annotation: `None`
Unsafe fix
40 40 | def ok_untyped_method(self):
41 41 | pass
42 42 |
43 |- def error_typed_self(self: X):
43 |+ def error_typed_self(self: X) -> None:
44 44 | pass

View File

@@ -41,14 +41,24 @@ mypy_init_return.py:11:9: ANN204 [*] Missing return type annotation for special
13 13 |
14 14 |
mypy_init_return.py:40:5: ANN202 Missing return type annotation for private function `__init__`
mypy_init_return.py:40:5: ANN202 [*] Missing return type annotation for private function `__init__`
|
39 | # Error
40 | def __init__(self, foo: int):
| ^^^^^^^^ ANN202
41 | ...
|
= help: Add return type annotation
= help: Add return type annotation: `None`
Unsafe fix
37 37 |
38 38 |
39 39 | # Error
40 |-def __init__(self, foo: int):
40 |+def __init__(self, foo: int) -> None:
41 41 | ...
42 42 |
43 43 |
mypy_init_return.py:47:9: ANN204 [*] Missing return type annotation for special method `__init__`
|

View File

@@ -10,7 +10,7 @@ static PASSWORD_CANDIDATE_REGEX: Lazy<Regex> = Lazy::new(|| {
pub(super) fn string_literal(expr: &Expr) -> Option<&str> {
match expr {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => Some(value.as_str()),
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => Some(value.to_str()),
_ => None,
}
}

View File

@@ -35,7 +35,7 @@ impl Violation for HardcodedBindAllInterfaces {
/// S104
pub(crate) fn hardcoded_bind_all_interfaces(string: &ExprStringLiteral) -> Option<Diagnostic> {
if string.value.as_str() == "0.0.0.0" {
if string.value.to_str() == "0.0.0.0" {
Some(Diagnostic::new(HardcodedBindAllInterfaces, string.range))
} else {
None

View File

@@ -55,7 +55,7 @@ fn password_target(target: &Expr) -> Option<&str> {
Expr::Name(ast::ExprName { id, .. }) => id.as_str(),
// d["password"] = "s3cr3t"
Expr::Subscript(ast::ExprSubscript { slice, .. }) => match slice.as_ref() {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.as_str(),
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.to_str(),
_ => return None,
},
// obj.password = "s3cr3t"

View File

@@ -93,7 +93,7 @@ pub(crate) fn hardcoded_sql_expression(checker: &mut Checker, expr: &Expr) {
let Some(string) = left.as_string_literal_expr() else {
return;
};
string.value.as_str().escape_default().to_string()
string.value.to_str().escape_default().to_string()
}
Expr::Call(ast::ExprCall { func, .. }) => {
let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func.as_ref() else {
@@ -106,7 +106,7 @@ pub(crate) fn hardcoded_sql_expression(checker: &mut Checker, expr: &Expr) {
let Some(string) = value.as_string_literal_expr() else {
return;
};
string.value.as_str().escape_default().to_string()
string.value.to_str().escape_default().to_string()
}
// f"select * from table where val = {val}"
Expr::FString(f_string) => concatenated_f_string(f_string, checker.locator()),

View File

@@ -57,7 +57,7 @@ pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, string: &ast::ExprS
.flake8_bandit
.hardcoded_tmp_directory
.iter()
.any(|prefix| string.value.as_str().starts_with(prefix))
.any(|prefix| string.value.to_str().starts_with(prefix))
{
return;
}

View File

@@ -855,7 +855,7 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
// If the `url` argument is a string literal, allow `http` and `https` schemes.
if call.arguments.args.iter().all(|arg| !arg.is_starred_expr()) && call.arguments.keywords.iter().all(|keyword| keyword.arg.is_some()) {
if let Some(Expr::StringLiteral(ast::ExprStringLiteral { value, .. })) = &call.arguments.find_argument("url", 0) {
let url = value.as_str().trim_start();
let url = value.to_str().trim_start();
if url.starts_with("http://") || url.starts_with("https://") {
return None;
}

View File

@@ -60,7 +60,7 @@ pub(crate) fn tarfile_unsafe_members(checker: &mut Checker, call: &ast::ExprCall
.arguments
.find_keyword("filter")
.and_then(|keyword| keyword.value.as_string_literal_expr())
.is_some_and(|value| matches!(value.value.as_str(), "data" | "tar"))
.is_some_and(|value| matches!(value.value.to_str(), "data" | "tar"))
{
return;
}

View File

@@ -2,6 +2,7 @@ use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::collect_call_path;
use ruff_python_ast::{Decorator, ParameterWithDefault, Parameters};
use ruff_python_semantic::analyze::visibility;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -94,23 +95,18 @@ impl Violation for BooleanDefaultValuePositionalArgument {
}
}
/// FBT002
pub(crate) fn boolean_default_value_positional_argument(
checker: &mut Checker,
name: &str,
decorator_list: &[Decorator],
parameters: &Parameters,
) {
// Allow Boolean defaults in explicitly-allowed functions.
if is_allowed_func_def(name) {
return;
}
if decorator_list.iter().any(|decorator| {
collect_call_path(&decorator.expression)
.is_some_and(|call_path| call_path.as_slice() == [name, "setter"])
}) {
return;
}
for ParameterWithDefault {
parameter,
default,
@@ -121,6 +117,20 @@ pub(crate) fn boolean_default_value_positional_argument(
.as_ref()
.is_some_and(|default| default.is_boolean_literal_expr())
{
// Allow Boolean defaults in setters.
if decorator_list.iter().any(|decorator| {
collect_call_path(&decorator.expression)
.is_some_and(|call_path| call_path.as_slice() == [name, "setter"])
}) {
return;
}
// Allow Boolean defaults in `@override` methods, since they're required to adhere to
// the parent signature.
if visibility::is_override(decorator_list, checker.semantic()) {
return;
}
checker.diagnostics.push(Diagnostic::new(
BooleanDefaultValuePositionalArgument,
parameter.name.range(),

View File

@@ -4,6 +4,7 @@ use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::collect_call_path;
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
@@ -109,17 +110,11 @@ pub(crate) fn boolean_type_hint_positional_argument(
decorator_list: &[Decorator],
parameters: &Parameters,
) {
// Allow Boolean type hints in explicitly-allowed functions.
if is_allowed_func_def(name) {
return;
}
if decorator_list.iter().any(|decorator| {
collect_call_path(&decorator.expression)
.is_some_and(|call_path| call_path.as_slice() == [name, "setter"])
}) {
return;
}
for ParameterWithDefault {
parameter,
default: _,
@@ -138,9 +133,26 @@ pub(crate) fn boolean_type_hint_positional_argument(
continue;
}
}
// Allow Boolean type hints in setters.
if decorator_list.iter().any(|decorator| {
collect_call_path(&decorator.expression)
.is_some_and(|call_path| call_path.as_slice() == [name, "setter"])
}) {
return;
}
// Allow Boolean defaults in `@override` methods, since they're required to adhere to
// the parent signature.
if visibility::is_override(decorator_list, checker.semantic()) {
return;
}
// If `bool` isn't actually a reference to the `bool` built-in, return.
if !checker.semantic().is_builtin("bool") {
return;
}
checker.diagnostics.push(Diagnostic::new(
BooleanTypeHintPositionalArgument,
parameter.name.range(),

View File

@@ -69,10 +69,10 @@ pub(crate) fn getattr_with_constant(
let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = arg else {
return;
};
if !is_identifier(value.as_str()) {
if !is_identifier(value.to_str()) {
return;
}
if is_mangled_private(value.as_str()) {
if is_mangled_private(value.to_str()) {
return;
}
if !checker.semantic().is_builtin("getattr") {

View File

@@ -83,10 +83,10 @@ pub(crate) fn setattr_with_constant(
let Expr::StringLiteral(ast::ExprStringLiteral { value: name, .. }) = name else {
return;
};
if !is_identifier(name.as_str()) {
if !is_identifier(name.to_str()) {
return;
}
if is_mangled_private(name.as_str()) {
if is_mangled_private(name.to_str()) {
return;
}
if !checker.semantic().is_builtin("setattr") {
@@ -104,7 +104,7 @@ pub(crate) fn setattr_with_constant(
if expr == child.as_ref() {
let mut diagnostic = Diagnostic::new(SetAttrWithConstant, expr.range());
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
assignment(obj, name.as_str(), value, checker.generator()),
assignment(obj, name.to_str(), value, checker.generator()),
expr.range(),
)));
checker.diagnostics.push(diagnostic);

View File

@@ -30,6 +30,14 @@ use crate::rules::flake8_comprehensions::fixes;
/// ```python
/// sorted(iterable, reverse=True)
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as `reversed` and `reverse=True` will
/// yield different results in the event of custom sort keys or equality
/// functions. Specifically, `reversed` will reverse the order of the
/// collection, while `sorted` with `reverse=True` will perform a stable
/// reverse sort, which will preserve the order of elements that compare as
/// equal.
#[violation]
pub struct UnnecessaryCallAroundSorted {
func: String,

View File

@@ -32,6 +32,10 @@ use crate::rules::flake8_comprehensions::settings::Settings;
/// []
/// ()
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryCollectionCall {
obj_type: String,

View File

@@ -29,6 +29,11 @@ use crate::rules::flake8_comprehensions::fixes;
/// list(iterable)
/// set(iterable)
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the comprehension. In most cases, though, comments will be
/// preserved.
#[violation]
pub struct UnnecessaryComprehension {
obj_type: String,

View File

@@ -40,6 +40,11 @@ use crate::rules::flake8_comprehensions::fixes;
/// any(x.id for x in bar)
/// all(x.id for x in bar)
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the comprehension. In most cases, though, comments will be
/// preserved.
#[violation]
pub struct UnnecessaryComprehensionAnyAll;

View File

@@ -43,6 +43,10 @@ use crate::rules::flake8_comprehensions::fixes;
/// - Instead of `sorted(tuple(iterable))`, use `sorted(iterable)`.
/// - Instead of `sorted(sorted(iterable))`, use `sorted(iterable)`.
/// - Instead of `sorted(reversed(iterable))`, use `sorted(iterable)`.
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryDoubleCastOrProcess {
inner: String,

View File

@@ -27,6 +27,10 @@ use super::helpers;
/// ```python
/// {x: f(x) for x in foo}
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryGeneratorDict;

View File

@@ -28,6 +28,10 @@ use super::helpers;
/// ```python
/// [f(x) for x in foo]
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryGeneratorList;

View File

@@ -28,6 +28,10 @@ use super::helpers;
/// ```python
/// {f(x) for x in foo}
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryGeneratorSet;

View File

@@ -25,6 +25,10 @@ use super::helpers;
/// ```python
/// [f(x) for x in foo]
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryListCall;

View File

@@ -25,6 +25,10 @@ use super::helpers;
/// ```python
/// {x: f(x) for x in foo}
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryListComprehensionDict;

View File

@@ -26,6 +26,10 @@ use super::helpers;
/// ```python
/// {f(x) for x in foo}
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryListComprehensionSet;

View File

@@ -29,6 +29,10 @@ use super::helpers;
/// {1: 2, 3: 4}
/// {}
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryLiteralDict {
obj_type: String,

View File

@@ -31,6 +31,10 @@ use super::helpers;
/// {1, 2}
/// set()
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryLiteralSet {
obj_type: String,

View File

@@ -46,6 +46,10 @@ impl fmt::Display for DictKind {
/// {}
/// {"a": 1}
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryLiteralWithinDictCall {
kind: DictKind,

View File

@@ -32,6 +32,10 @@ use super::helpers;
/// [1, 2]
/// [1, 2]
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryLiteralWithinListCall {
literal: String,

View File

@@ -33,6 +33,10 @@ use super::helpers;
/// (1, 2)
/// (1, 2)
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryLiteralWithinTupleCall {
literal: String,

View File

@@ -22,6 +22,16 @@ use super::helpers;
/// using a generator expression or a comprehension, as the latter approach
/// avoids the function call overhead, in addition to being more readable.
///
/// This rule also applies to `map` calls within `list`, `set`, and `dict`
/// calls. For example:
///
/// - Instead of `list(map(lambda num: num * 2, nums))`, use
/// `[num * 2 for num in nums]`.
/// - Instead of `set(map(lambda num: num % 2 == 0, nums))`, use
/// `{num % 2 == 0 for num in nums}`.
/// - Instead of `dict(map(lambda v: (v, v ** 2), values))`, use
/// `{v: v ** 2 for v in values}`.
///
/// ## Examples
/// ```python
/// map(lambda x: x + 1, iterable)
@@ -32,15 +42,9 @@ use super::helpers;
/// (x + 1 for x in iterable)
/// ```
///
/// This rule also applies to `map` calls within `list`, `set`, and `dict`
/// calls. For example:
///
/// - Instead of `list(map(lambda num: num * 2, nums))`, use
/// `[num * 2 for num in nums]`.
/// - Instead of `set(map(lambda num: num % 2 == 0, nums))`, use
/// `{num % 2 == 0 for num in nums}`.
/// - Instead of `dict(map(lambda v: (v, v ** 2), values))`, use
/// `{v: v ** 2 for v in values}`.
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryMap {
object_type: ObjectType,

View File

@@ -78,7 +78,7 @@ pub(crate) fn call_datetime_strptime_without_zone(checker: &mut Checker, call: &
if let Some(Expr::StringLiteral(ast::ExprStringLiteral { value: format, .. })) =
call.arguments.args.get(1).as_ref()
{
if format.as_str().contains("%z") {
if format.to_str().contains("%z") {
return;
}
};

View File

@@ -93,7 +93,7 @@ fn check_log_record_attr_clash(checker: &mut Checker, extra: &Keyword) {
for key in keys {
if let Some(key) = &key {
if let Expr::StringLiteral(ast::ExprStringLiteral { value: attr, .. }) = key {
if is_reserved_attr(attr.as_str()) {
if is_reserved_attr(attr.to_str()) {
checker.diagnostics.push(Diagnostic::new(
LoggingExtraAttrClash(attr.to_string()),
key.range(),

View File

@@ -110,8 +110,8 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs
/// Return `Some` if a key is a valid keyword argument name, or `None` otherwise.
fn as_kwarg(key: &Expr) -> Option<&str> {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = key {
if is_identifier(value.as_str()) {
return Some(value.as_str());
if is_identifier(value.to_str()) {
return Some(value.to_str());
}
}
None

View File

@@ -2,7 +2,7 @@ use std::fmt;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::Parameters;
use ruff_python_ast::{Expr, Parameters};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -51,6 +51,9 @@ impl Violation for NoReturnArgumentAnnotationInStub {
/// PYI050
pub(crate) fn no_return_argument_annotation(checker: &mut Checker, parameters: &Parameters) {
// Ex) def func(arg: NoReturn): ...
// Ex) def func(arg: NoReturn, /): ...
// Ex) def func(*, arg: NoReturn): ...
for annotation in parameters
.posonlyargs
.iter()
@@ -58,19 +61,37 @@ pub(crate) fn no_return_argument_annotation(checker: &mut Checker, parameters: &
.chain(&parameters.kwonlyargs)
.filter_map(|arg| arg.parameter.annotation.as_ref())
{
if checker.semantic().match_typing_expr(annotation, "NoReturn") {
checker.diagnostics.push(Diagnostic::new(
NoReturnArgumentAnnotationInStub {
module: if checker.settings.target_version >= Py311 {
TypingModule::Typing
} else {
TypingModule::TypingExtensions
},
},
annotation.range(),
));
check_no_return_argument_annotation(checker, annotation);
}
// Ex) def func(*args: NoReturn): ...
if let Some(arg) = &parameters.vararg {
if let Some(annotation) = &arg.annotation {
check_no_return_argument_annotation(checker, annotation);
}
}
// Ex) def func(**kwargs: NoReturn): ...
if let Some(arg) = &parameters.kwarg {
if let Some(annotation) = &arg.annotation {
check_no_return_argument_annotation(checker, annotation);
}
}
}
fn check_no_return_argument_annotation(checker: &mut Checker, annotation: &Expr) {
if checker.semantic().match_typing_expr(annotation, "NoReturn") {
checker.diagnostics.push(Diagnostic::new(
NoReturnArgumentAnnotationInStub {
module: if checker.settings.target_version >= Py311 {
TypingModule::Typing
} else {
TypingModule::TypingExtensions
},
},
annotation.range(),
));
}
}
#[derive(Debug, PartialEq, Eq)]

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