Compare commits

...

65 Commits

Author SHA1 Message Date
Dhruv Manilawala
9cb191c496 Update rule docs for preview feature 2024-05-20 12:52:47 +05:30
Dhruv Manilawala
ff2bb6867c Move logic behind preview 2024-05-20 12:52:47 +05:30
Dhruv Manilawala
574843049e Add test case 2024-05-20 12:52:47 +05:30
Dhruv Manilawala
9e7821f4a6 Flag B018 for strings and f-strings which aren't docstrings 2024-05-20 12:52:47 +05:30
Dhruv Manilawala
403f0dccd8 Consider soft keywords for E27 rules (#11446)
## Summary

This is a follow-up PR to #11445 update the `E27` rules to consider soft
keywords as well.

## Test Plan

Add test cases consisting of soft keywords and update the snapshot.
2024-05-20 05:38:06 +00:00
Zanie Blue
46fcd19ca6 Fix division by zero error in ecosystem check (#11469)
e.g.
https://github.com/astral-sh/ruff/actions/runs/9144809516/job/25143076896?pr=11468

<img width="1388" alt="Screenshot 2024-05-19 at 12 02 15 AM"
src="https://github.com/astral-sh/ruff/assets/2586601/0df7cbcd-712c-4ea9-96f5-73f871570525">
2024-05-19 09:08:10 -05:00
Charlie Marsh
d9ec3d56b0 Add some new projects to the ecosystem CI (#11468)
Co-authored-by: Zanie Blue <contact@zanie.dev>
2024-05-19 08:08:38 -05:00
Auguste Lalande
cd87b787d9 Fix windows-ci failure (#11470)
<!--
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

The recent issues with the windows CI seem to be caused by
https://github.com/nextest-rs/nextest/issues/1493. With this
https://github.com/nextest-rs/nextest/issues/1493#issuecomment-2106331574
as a fix.

(Let's see if it works)
2024-05-19 07:25:06 -05:00
Charlie Marsh
dd6d411026 Remove comma from ecosystem checks (#11466)
## Summary

Something's up with this repo -- they added a post-checkout hook? So
let's just remove it for now. We should go through and add a new batch
of repositories some time.
2024-05-18 23:37:56 -04:00
Charlie Marsh
cfceb437a8 Treat escaped newline as valid sequence (#11465)
## Summary

We weren't treating the escaped newline as a valid condition to trigger
the safer fix (add an extra backslash before each invalid escape
sequence).

Closes https://github.com/astral-sh/ruff/issues/11461.
2024-05-19 03:32:32 +00:00
Charlie Marsh
48b0660228 Respect operator precedence in FURB110 (#11464)
## Summary

Ensures that we parenthesize expressions (if necessary) to preserve
operator precedence in `FURB110`.

Closes https://github.com/astral-sh/ruff/issues/11398.
2024-05-19 03:17:11 +00:00
Charlie Marsh
24899efe50 Remove example from tab-indentation (#11462)
## Summary

I think the example is more confusing than helpful, since there's no
visual difference between the tab and space here (even if it rendered
properly).

Closes
https://github.com/astral-sh/ruff/issues/11460#issuecomment-2118397278.
2024-05-17 17:49:16 -04:00
Dhruv Manilawala
83152fff92 Include soft keywords for is_keyword check (#11445)
## Summary

This PR updates the `TokenKind::is_keyword` check to include soft
keywords. To account for this change, it adds a new
`is_non_soft_keyword` method.

The usage in logical line rules were updated to use the
`is_non_soft_keyword` method but it'll be updated to use `is_keyword` in
a follow-up PR (#11446).

While, the parser usages were kept as is. And because of that, the
snapshots for two test cases were updated in a better direction.

## Test Plan

`cargo insta test`
2024-05-17 10:26:48 +05:30
Charlie Marsh
43e8147eaf Sort edits prior to deduplicating in quotation fix (#11452)
## Summary

We already have handling for "references that get quoted within our
quoted references", but we were assuming a specific ordering in the way
edits were generated.

Closes https://github.com/astral-sh/ruff/issues/11449.
2024-05-16 12:13:09 -04:00
Jason R. Coombs
42b655b24f Locate ruff executable in 'bin' directory as installed by 'pip install --target'. (#11450)
Fixes #11246

## Summary

This change adds an intermediate additional search path for
`find_ruff_bin`.

I would have added this path as the last one, except that the last one
is the one reported to the user, so I made this one second to last.

## Test Plan

It's shown to work with this command:

```
 ~ @ pip-run git+https://github.com/jaraco/ruff@feature/honor-install-target-bin -- -m ruff --version
ruff 0.4.4
```

I tried running the same command on Windows, which should work in
theory, but building ruff from source on Windows is complicated. Even
after installing Rust, ruff fails to build when `libmimalloc-sys` fails
to build because `gcc` isn't installed (and the error message points to
a [broken
anchor](https://github.com/rust-lang/cc-rs#compile-time-requirements)).
I was really hoping Rust would get us away from the Windows as
second-class-citizen model :(.
2024-05-16 16:07:22 +00:00
Dhruv Manilawala
f67c02c837 Remove leftover marker tokens (#11444)
## Summary

This PR removes the leftover marker tokens from the LALRPOP to
hand-written parser migration.
2024-05-16 11:39:05 +00:00
Charlie Marsh
4436dec1d9 Fix broken comment in too-many-branches (#11440) 2024-05-16 02:25:20 +00:00
Tim Hatch
27da223e9f Add --output-format to ruff config CLI (#11438)
This is useful for extracting the defaults in order to construct
equivalent configs by external scripts. This is my first non-hello-world
rust code, comments and suggested tests appreciated.

## Summary

We already have `ruff linter --output-format json`, this provides `ruff
config x --output-format json` as well. I plan to use this to construct
an equivalent config snippet to include in some managed repos, so when
we update their version of ruff and it adds new lints, they get a PR
that includes the commented-out new lints.

Note that the no-args form of `ruff config` ignores output-format
currently, but probably should obey it (although array-of-strings
doesn't seem that useful, looking for input on format).

## Test Plan

I could use a hand coming up with a typical way to write automated tests
for this.

```sh-session
(.venv) [timhatch:ruff ]$ ./target/debug/ruff config lint.select
A list of rule codes or prefixes to enable. Prefixes can specify exact
rules (like `F841`), entire categories (like `F`), or anything in
between.

When breaking ties between enabled and disabled rules (via `select` and
`ignore`, respectively), more specific prefixes override less
specific prefixes.

Default value: ["E4", "E7", "E9", "F"]
Type: list[RuleSelector]
Example usage:
``toml
# On top of the defaults (`E4`, E7`, `E9`, and `F`), enable flake8-bugbear (`B`) and flake8-quotes (`Q`).
select = ["E4", "E7", "E9", "F", "B", "Q"]
``
(.venv) [timhatch:ruff ]$ ./target/debug/ruff config lint.select --output-format json
{
  "Field": {
    "doc": "A list of rule codes or prefixes to enable. Prefixes can specify exact\nrules (like `F841`), entire categories (like `F`), or anything in\nbetween.\n\nWhen breaking ties between enabled and disabled rules (via `select` and\n`ignore`, respectively), more specific prefixes override less\nspecific prefixes.",
    "default": "[\"E4\", \"E7\", \"E9\", \"F\"]",
    "value_type": "list[RuleSelector]",
    "scope": null,
    "example": "# On top of the defaults (`E4`, E7`, `E9`, and `F`), enable flake8-bugbear (`B`) and flake8-quotes (`Q`).\nselect = [\"E4\", \"E7\", \"E9\", \"F\", \"B\", \"Q\"]",
    "deprecated": null
  }
}
```
2024-05-15 22:17:33 -04:00
Jaap Roes
b3e4d39f64 Clearly indicate what is counted as a branch (#11423)
## Summary

As discussed in issue #11408, PLR0912 has a broader definition of
"branches" than I expected. This updates the documentation to include
this definition.

I also updated the example to include several different types of
branches, while still maintaining dictionary lookup as an alternative
solution. (Crafting a realistic example was quite a challenge 😅).

Closes https://github.com/astral-sh/ruff/issues/11408.
2024-05-15 22:17:05 -04:00
Tim Hatch
d05347cfcb Regenerate sys.rs with stdlibs==2024.5.15 (#11437)
## Summary

Now that 3.13.0 b1 is out some of the stdlib modules have changed names.

## Test Plan

Wait for CI to run, expected to be pretty safe.
2024-05-15 22:17:32 +00:00
Léopold Mebazaa
7ac9cabbff Update CONTRIBUTING.md to reflect the new parser (#11434)
## Summary

CONTRIBUTING.md says that `cargo dev print-ast` uses the old RuffPython
parser, even though, as far as I can tell, it uses the shiny new parser.
This PR fixes this.

## Test Plan

CI jobs should do the trick -- I didn't modify any code.
2024-05-15 14:36:28 +00:00
Alex Waygood
6963f75a14 Move string-prefix enumerations to a separate submodule (#11425)
## Summary

This moves the string-prefix enumerations in `ruff_python_ast` to a
separate submodule. I think this helps clarify that these prefixes are
purely abstract: they only depend on each other, and do not depend on
any of the other code in `nodes.rs` in any way. Moreover, while various
AST nodes _use_ them, they're not really nodes themselves, so they feel
slightly out of place in `nodes.rs`.

I considered moving all of them to `str.rs`, but it felt like enough
code that it could be a separate submodule.

## Test Plan

`cargo test`
2024-05-15 07:40:27 -04:00
github-actions[bot]
effe3ad4ef Sync vendored typeshed stubs (#11428)
Co-authored-by: typeshedbot <>
2024-05-15 00:46:41 +00:00
Alex Waygood
bdc15a7cb9 Add automation for updating our vendored typeshed stubs (#11427) 2024-05-14 20:39:30 -04:00
plredmond
da882b6657 F401 - Recommend adding unused import bindings to __all__ (#11314)
Followup on #11168 and resolve #10391

# User facing changes

* F401 now recommends a fix to add unused import bindings to to
`__all__` if a single `__all__` list or tuple is found in `__init__.py`.
* If there are no `__all__` found in the file, fall back to recommending
redundant-aliases.
* If there are multiple `__all__` or only one but of the wrong type (non
list or tuple) then diagnostics are generated without fixes.
* `fix_title` is updated to reflect what the fix/recommendation is.

Subtlety: For a renamed import such as `import foo as bees`, we can
generate a fix to add `bees` to `__all__` but cannot generate a fix to
produce a redundant import (because that would break uses of the binding
`bees`).

# Implementation changes

* Add `name` field to `ImportBinding` to contain the name of the
_binding_ we want to add to `__all__` (important for the `import foo as
bees` case). It previously only contained the `AnyImport` which can give
us information about the import but not the binding.
* Add `binding` field to `UnusedImport` to contain the same. (Naming
note: the field `name` field already existed on `UnusedImport` and
contains the qualified name of the imported symbol/module)
* Change `fix_by_reexporting` to branch on the size of `dunder_all:
Vec<&Expr>`
* For length 0 call the edit-producing function `make_redundant_alias`.
  * For length 1 call edit-producing function `add_to_dunder_all`.
  * Otherwise, produce no fix.
* Implement the edit-producing function `add_to_dunder_all` and add unit
tests.
* Implement several fixture tests: empty `__all__ = []`, nonempty
`__all__ = ["foo"]`, mis-typed `__all__ = None`, plus-eq `__all__ +=
["foo"]`
* `UnusedImportContext::Init` variant now has two fields: whether the
fix is in `__init__.py` and how many `__all__` were found.

# Other changes

* Remove a spurious pattern match and instead use field lookups b/c the
addition of a field would have required changing the unrelated pattern.
* Tweak input type of `make_redundant_alias`

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-05-14 17:02:33 -07:00
Dhruv Manilawala
96f6288622 Move UP034 to use TokenKind instead of Tok (#11424)
## Summary

This PR follows up from #11420 to move `UP034` to use `TokenKind`
instead of `Tok`.

The main reason to have a separate PR is so that the reviewing is easy.
This required a lot more updates because the rule used an index (`i`) to
keep track of the current position in the token vector. Now, as it's
just an iterator, we just use `next` to move the iterator forward and
extract the relevant information.

This is part of https://github.com/astral-sh/ruff/issues/11401

## Test Plan

`cargo test`
2024-05-14 17:28:04 +00:00
Dhruv Manilawala
bb1c107afd Move most of token-based rules to use TokenKind (#11420)
## Summary

This PR moves the following rules to use `TokenKind` instead of `Tok`:
* `PLE2510`, `PLE2512`, `PLE2513`, `PLE2514`, `PLE2515`
* `E701`, `E702`, `E703`
* `ISC001`, `ISC002`
* `COM812`, `COM818`, `COM819`
* `W391`

I've paused here because the next set of rules
(`pyupgrade::rules::extraneous_parentheses`) indexes into the token
slice but we only have an iterator implementation. So, I want to isolate
that change to make sure the logic is still the same when I move to
using the iterator approach.

This is part of #11401 

## Test Plan

`cargo test`
2024-05-14 17:16:42 +00:00
Dhruv Manilawala
c17193b5f8 Use TokenKind in blank lines checker (#11419)
## Summary

This PR updates the blank line rules checker to use `TokenKind` instead
of `Tok`.

This is part of #11401 

## Test Plan

`cargo test`
2024-05-14 17:07:35 +00:00
Dhruv Manilawala
a33763170e Use TokenKind in doc_lines_from_tokens (#11418)
## Summary

This PR updates the `doc_lines_from_tokens` function to use `TokenKind`
instead of `Tok`.

This is part of #11401 

## Test Plan

`cargo test`
2024-05-14 16:56:14 +00:00
Dhruv Manilawala
025768d303 Add Tokens newtype wrapper, TokenKind iterator (#11361)
## Summary

Alternative to #11237 

This PR adds a new `Tokens` struct which is a newtype wrapper around a
vector of lexer output. This allows us to add a `kinds` method which
returns an iterator over the corresponding `TokenKind`. This iterator is
implemented as a separate `TokenKindIter` struct to allow using the type
and provide additional methods like `peek` directly on the iterator.

This exposes the linter to access the stream of `TokenKind` instead of
`Tok`.

Edit: I've made the necessary downstream changes and plan to merge the
entire stack at once.
2024-05-14 16:45:04 +00:00
Dhruv Manilawala
50f14d017e Use tokenize for linter benchmark (#11417)
## Summary

This PR updates the linter benchmark to use the `tokenize` function
instead of the lexer.

The linter expects the token list to be up to and including the first
error which is what the `ruff_python_parser::tokenize` function returns.

This was not a problem before because the benchmarks only uses valid
Python code.
2024-05-14 10:28:40 -04:00
Alex Waygood
aceb182db6 Improve the update_schemastore script (#11353) 2024-05-13 17:06:54 +00:00
Charlie Marsh
6ed2482e27 Add Python 3.13 to list of allowed Python versions (#11411)
## Summary

I believe we're already "Python 3.13-ready"? The main Ruff-impacting
change I see in https://docs.python.org/3.13/whatsnew/3.13.html is [PEP
696](https://peps.python.org/pep-0696/) which Jelle added in
https://github.com/astral-sh/ruff/pull/11120.
2024-05-13 16:35:41 +00:00
Charlie Marsh
dc5c44ccc4 Remove some hardcoded modules from generate_known_standard_library.py (#11409)
See feedback in: https://github.com/astral-sh/ruff/pull/11374
2024-05-13 12:27:34 -04:00
Dhruv Manilawala
c3c87e86ef Implement IntoIterator for FStringElements (#11410)
A change which I lost somewhere when I force pushed in
https://github.com/astral-sh/ruff/pull/11400
2024-05-13 16:24:49 +00:00
Dhruv Manilawala
ca99e9e2f0 Move W605 to the AST checker (#11402)
## Summary

This PR moves the `W605` rule to the AST checker.

This is part of #11401

## Test Plan

`cargo test`
2024-05-13 16:13:06 +00:00
Dhruv Manilawala
4b41e4de7f Create a newtype wrapper around Vec<FStringElement> (#11400)
## Summary

This PR adds a newtype wrapper around `Vec<FStringElement>` that derefs
to a `&Vec<FStringElement>`.

Both f-string and format specifier are made up of `Vec<FStringElement>`.
By creating a newtype wrapper around it, we can share the methods for
both parent types.
2024-05-13 16:04:04 +00:00
Dhruv Manilawala
0dc130e841 Add Iterator impl for StringLike parts (#11399)
## Summary

This PR adds support to iterate over each part of a string-like
expression.

This similar to the one in the formatter:


128414cd95/crates/ruff_python_formatter/src/string/any.rs (L121-L125)

Although I don't think it's a 1-1 replacement in the formatter because
the one implemented in the formatter has another information for certain
variants (as can be seen for `FString`).

The main motivation for this is to avoid duplication for rules which
work only on the parts of the string and doesn't require any information
from the parent node. Here, the parent node being the expression node
which could be an implicitly concatenated string.

This PR also updates certain rule implementation to make use of this and
avoids logic duplication.
2024-05-13 15:52:03 +00:00
Dhruv Manilawala
10b85a0f07 Avoid lexer usage in PLE1300 and PLE1307 (#11406)
## Summary

This PR updates `PLE1300` and `PLE1307` to avoid using the lexer.

This is part of #11401 

## Test Plan

`cargo test`
2024-05-13 10:48:44 -04:00
Charlie Marsh
af60d539ab Move sub-crates to workspace dependencies (#11407)
## Summary

This matches the setup we use in `uv` and allows for consistency in the
`Cargo.toml` files.
2024-05-13 14:37:50 +00:00
Charlie Marsh
b371713591 Add a note on --preview to the README (#11395) 2024-05-13 14:27:29 +00:00
Dimitri Papadopoulos Orfanos
3b0584449d Fix a few typos found by codespell (#11404)
## Summary

Just fix typos.

## Test Plan

CI jobs.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2024-05-13 13:22:35 +00:00
Dhruv Manilawala
6ecb4776de Rename AnyStringKind -> AnyStringFlags (#11405)
## Summary

This PR renames `AnyStringKind` to `AnyStringFlags` and `AnyStringFlags`
to `AnyStringFlagsInner`.

The main motivation is to have consistent usage of "kind" and "flags".
For each string kind, it's "flags" like `StringLiteralFlags`,
`BytesLiteralFlags`, and `FStringFlags` but it was `AnyStringKind` for
the "any" variant.
2024-05-13 13:18:07 +00:00
Charlie Marsh
be0ccabbaa Add cargo shear to CI (#11393) 2024-05-12 22:23:36 -04:00
Charlie Marsh
6cec82fff8 Get cargo shear passing (#11392)
## Summary

Remove some unused dependencies, add a few ignores.
2024-05-13 01:56:24 +00:00
Tom Kuson
5ab4cc86c2 Reword future-rewritable-type-annotation (FA100) message (#11381)
## Summary

Changes `future-rewritable-type-annotation` (`FA100`) message to be less
confusing. Uses phrasing from the rule documentation to be consistent.
For example,

```
from_typing_import.py:5:13: FA100 Add `from __future__ import annotations` to rewrite `typing.List` more succinctly
```

Closes #10573.

## Test Plan

`cargo nextest run`
2024-05-13 01:38:49 +00:00
renovate[bot]
bc7856e899 Update pre-commit dependencies (#11391) 2024-05-12 21:22:04 -04:00
Rahul Modpur
6a28f3448e Migrate sys.rs generation to stdlibs (#11374)
## Summary

Closes #11347
2024-05-12 21:21:51 -04:00
renovate[bot]
7c824faa88 Update Rust crate thiserror to v1.0.60 (#11390) 2024-05-13 00:36:08 +00:00
renovate[bot]
12da5968a0 Update Rust crate serde_json to v1.0.117 (#11388) 2024-05-13 00:35:46 +00:00
renovate[bot]
a747b3f2a1 Update Rust crate syn to v2.0.63 (#11389) 2024-05-13 00:35:23 +00:00
renovate[bot]
01a0e6cc7e Update Rust crate serde to v1.0.201 (#11387) 2024-05-13 00:34:34 +00:00
renovate[bot]
a8b06537c7 Update Rust crate anyhow to v1.0.83 (#11384) 2024-05-13 00:34:00 +00:00
renovate[bot]
7b8fe25d32 Update Rust crate schemars to v0.8.19 (#11386) 2024-05-13 00:33:29 +00:00
renovate[bot]
a50416a6d7 Update Rust crate proc-macro2 to v1.0.82 (#11385) 2024-05-13 00:33:05 +00:00
renovate[bot]
41e53d59ab Update NPM Development dependencies (#11383) 2024-05-13 00:30:58 +00:00
Dhruv Manilawala
0fc6cf9bee Avoid PLE0237 for property with setter (#11377)
## Summary

Should this consider the decorator only if the name is actually a
property or is the logic in this PR correct?

fixes: #11358

## Test Plan

Add test case.
2024-05-12 20:23:00 -04:00
Dhruv Manilawala
d835b3e218 Avoid TCH005 for if stmt with elif/else block (#11376)
## Summary

This PR fixes a bug where the auto-fix for `TCH005` would delete the
entire `if` statement.

The fix in this PR is to not consider it a violation if there are any
`elif`/`else` blocks. This also matches the behavior of the original
plugin.

fixes: #11368 

## Test plan

Add test cases.
2024-05-12 20:22:25 -04:00
Jane Lewis
d7f093ef9e ruff server: Support noqa comment code action (#11276)
## Summary

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

Code actions to disable a diagnostic via `noqa` comment are now
available.


https://github.com/astral-sh/ruff/assets/19577865/6d3bcf11-a9d9-499b-8c7f-a10cd39cfbba

`DiagnosticFix` has been changed so that `noqa` code actions appear even
for diagnostics with no available quick fix. It can contain quick fix
edits, `noqa` comment edits, or both.

## Test Plan

The scenarios that need to be tested are as follows:
* A code action to disable a diagnostic should be available for every
diagnostic.
* Using this code action should append to the appropriate line with the
diagnostic, or modify an existing `noqa` comment.
* Adding a `noqa` comment manually should make a diagnostic disappear
* `Fix all auto-fixable problems` should not add `noqa` comments
* Removing a code from a `noqa` comment should make the diagnostic
re-appear
2024-05-12 14:39:46 -07:00
Charlie Marsh
4b330b11c6 [flake8-pie] Preserve parentheses in unnecessary-dict-kwargs (#11372)
## Summary

Closes https://github.com/astral-sh/ruff/issues/11371.
2024-05-11 18:04:54 -04:00
Jane Lewis
890cc325d5 Split add_noqa process into distinctive edit generation and edit application stages (#11265)
## Summary

`--add-noqa` now runs in two stages: first, the linter finds all
diagnostics that need noqa comments and generate edits on a per-line
basis. Second, these edits are applied, in order, to the document.

A public-facing function, `generate_noqa_edits`, has also been
introduced, which returns noqa edits generated on a per-diagnostic
basis. This will be used by `ruff server` for noqa comment quick-fixes.

## Test Plan

Unit tests have been updated.
2024-05-10 23:16:52 +00:00
Douglas Thor
0726e82342 [pyflakes] Update docs to describe WAI behavior (F541) (#11362)
Addresses this comment:
https://github.com/astral-sh/ruff/issues/11357#issuecomment-2104714029


## Summary

The docs for F541 did not mention some surprising, but WAI, behavior
regarding implicit string concatenation. Update the docs to describe the
behavior.

Here's how things rendered for me locally:


![image](https://github.com/astral-sh/ruff/assets/5386897/32067121-b190-4268-b987-ff37df11a618)
2024-05-10 19:10:34 +00:00
Dhruv Manilawala
f79c980e17 Add support for attribute docstring in the semantic model (#11315)
## Summary

This PR adds updates the semantic model to detect attribute docstring.

Refer to [PEP 258](https://peps.python.org/pep-0258/#attribute-docstrings) 
for the definition of an attribute docstring.

This PR doesn't add full support for it but only considers string
literals as attribute docstring for the following cases:
1. A string literal following an assignment statement in the **global
scope**.
2. A global class attribute

For an assignment statement, it's considered an attribute docstring only
if the target expression is a name expression (`x = 1`). So, chained
assignment, multiple assignment or unpacking, and starred expression,
which are all valid in the target position, aren't considered here.

In `__init__` method, an assignment to the `self` variable like `self.x = 1`
is also a candidate for an attribute docstring. **This PR does not
support this position.**

## Test Plan

I used the following source code along with a print statement to verify
that the attribute docstring detection is correct.

Refer to the PR description for the code snippet.

I'll add this in the follow-up PR
(https://github.com/astral-sh/ruff/pull/11302) which uses this method.
2024-05-10 20:27:56 +05:30
Charlie Marsh
35ba3c91ce Use u64 instead of i64 in Int type (#11356)
## Summary

I believe the value here is always unsigned, since we represent `-42` as
a unary operator on `42`.
2024-05-10 13:35:15 +00:00
konsti
1f794077ec Allow clippy map-unwrap-or (#11354)
`map_or` is harder too read than the `.map().unwrap()` version.

See also https://github.com/astral-sh/uv/pull/3498
2024-05-09 21:22:09 +00:00
248 changed files with 4711 additions and 2032 deletions

View File

@@ -167,6 +167,9 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: "Run tests"
shell: bash
env:
# Workaround for <https://github.com/nextest-rs/nextest/issues/1493>.
RUSTUP_WINDOWS_PATH_ADD_BIN: 1
run: |
cargo nextest run --all-features --profile ci
cargo test --all-features --doc
@@ -395,22 +398,16 @@ jobs:
name: ecosystem-result
path: ecosystem-result
cargo-udeps:
name: "cargo udeps"
cargo-shear:
name: "cargo shear"
runs-on: ubuntu-latest
needs: determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: "Install nightly Rust toolchain"
# Only pinned to make caching work, update freely
run: rustup toolchain install nightly-2023-10-15
- uses: Swatinem/rust-cache@v2
- name: "Install cargo-udeps"
uses: taiki-e/install-action@cargo-udeps
- name: "Run cargo-udeps"
run: cargo +nightly-2023-10-15 udeps
- uses: cargo-bins/cargo-binstall@main
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
python-package:
name: "python package"

80
.github/workflows/sync_typeshed.yaml vendored Normal file
View File

@@ -0,0 +1,80 @@
name: Sync typeshed
on:
workflow_dispatch:
schedule:
# Run on the 1st and the 15th of every month:
- cron: "0 0 1,15 * *"
env:
FORCE_COLOR: 1
GH_TOKEN: ${{ github.token }}
jobs:
sync:
name: Sync typeshed
runs-on: ubuntu-latest
timeout-minutes: 20
# Don't run the cron job on forks:
if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
name: Checkout Ruff
with:
path: ruff
- uses: actions/checkout@v4
name: Checkout typeshed
with:
repository: python/typeshed
path: typeshed
- name: Setup git
run: |
git config --global user.name typeshedbot
git config --global user.email '<>'
- name: Sync typeshed
id: sync
run: |
rm -rf ruff/crates/red_knot/vendor/typeshed
mkdir ruff/crates/red_knot/vendor/typeshed
cp typeshed/README.md ruff/crates/red_knot/vendor/typeshed
cp typeshed/LICENSE ruff/crates/red_knot/vendor/typeshed
cp -r typeshed/stdlib ruff/crates/red_knot/vendor/typeshed/stdlib
rm -rf ruff/crates/red_knot/vendor/typeshed/stdlib/@tests
git -C typeshed rev-parse HEAD > ruff/crates/red_knot/vendor/typeshed/source_commit.txt
- name: Commit the changes
id: commit
if: ${{ steps.sync.outcome == 'success' }}
run: |
cd ruff
git checkout -b typeshedbot/sync-typeshed
git add .
git diff --staged --quiet || git commit -m "Sync typeshed. Source commit: https://github.com/python/typeshed/commit/$(git -C ../typeshed rev-parse HEAD)"
- name: Create a PR
if: ${{ steps.sync.outcome == 'success' && steps.commit.outcome == 'success' }}
run: |
cd ruff
git push --force origin typeshedbot/sync-typeshed
gh pr list --repo $GITHUB_REPOSITORY --head typeshedbot/sync-typeshed --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
gh pr create --title "Sync vendored typeshed stubs" --body "Close and reopen this PR to trigger CI" --label "internal"
create-issue-on-failure:
name: Create an issue if the typeshed sync failed
runs-on: ubuntu-latest
needs: [sync]
if: ${{ github.repository == 'astral-sh/ruff' && always() && github.event_name == 'schedule' && needs.sync.result == 'failure' }}
permissions:
issues: write
steps:
- uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
await github.rest.issues.create({
owner: "astral-sh",
repo: "ruff",
title: `Automated typeshed sync failed on ${new Date().toDateString()}`,
body: "Runs are listed here: https://github.com/astral-sh/ruff/actions/workflows/sync_typeshed.yaml",
})

View File

@@ -14,7 +14,7 @@ exclude: |
repos:
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.16
rev: v0.17
hooks:
- id: validate-pyproject
@@ -56,7 +56,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.3
rev: v0.4.4
hooks:
- id: ruff-format
- id: ruff

View File

@@ -637,11 +637,11 @@ Otherwise, follow the instructions from the linux section.
`cargo dev` is a shortcut for `cargo run --package ruff_dev --bin ruff_dev`. You can run some useful
utils with it:
- `cargo dev print-ast <file>`: Print the AST of a python file using the
[RustPython parser](https://github.com/astral-sh/ruff/tree/main/crates/ruff_python_parser) that is
mainly used in Ruff. For `if True: pass # comment`, you can see the syntax tree, the byte offsets
for start and stop of each node and also how the `:` token, the comment and whitespace are not
represented anymore:
- `cargo dev print-ast <file>`: Print the AST of a python file using Ruff's
[Python parser](https://github.com/astral-sh/ruff/tree/main/crates/ruff_python_parser).
For `if True: pass # comment`, you can see the syntax tree, the byte offsets for start and
stop of each node and also how the `:` token, the comment and whitespace are not represented
anymore:
```text
[

45
Cargo.lock generated
View File

@@ -129,9 +129,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.82"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
[[package]]
name = "argfile"
@@ -1707,9 +1707,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.81"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
dependencies = [
"unicode-ident",
]
@@ -1821,7 +1821,6 @@ dependencies = [
"dashmap",
"hashbrown 0.14.5",
"indexmap",
"log",
"notify",
"parking_lot",
"rayon",
@@ -1829,10 +1828,8 @@ dependencies = [
"ruff_notebook",
"ruff_python_ast",
"ruff_python_parser",
"ruff_python_trivia",
"ruff_text_size",
"rustc-hash",
"smallvec",
"smol_str",
"tempfile",
"textwrap",
@@ -2200,7 +2197,6 @@ version = "0.0.0"
dependencies = [
"aho-corasick",
"bitflags 2.5.0",
"insta",
"is-macro",
"itertools 0.12.1",
"once_cell",
@@ -2348,7 +2344,6 @@ dependencies = [
name = "ruff_python_trivia"
version = "0.0.0"
dependencies = [
"insta",
"itertools 0.12.1",
"ruff_source_file",
"ruff_text_size",
@@ -2560,9 +2555,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "0.8.17"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f55c82c700538496bdc329bb4918a81f87cc8888811bd123cf325a0f2f8d309"
checksum = "fc6e7ed6919cb46507fb01ff1654309219f62b4d603822501b0b80d42f6f21ef"
dependencies = [
"dyn-clone",
"schemars_derive",
@@ -2572,9 +2567,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "0.8.17"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83263746fe5e32097f06356968a077f96089739c927a61450efa069905eec108"
checksum = "185f2b7aa7e02d418e453790dde16890256bbd2bcd04b7dc5348811052b53f49"
dependencies = [
"proc-macro2",
"quote",
@@ -2602,9 +2597,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "serde"
version = "1.0.200"
version = "1.0.201"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f"
checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c"
dependencies = [
"serde_derive",
]
@@ -2622,9 +2617,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.200"
version = "1.0.201"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb"
checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865"
dependencies = [
"proc-macro2",
"quote",
@@ -2644,9 +2639,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.116"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
dependencies = [
"itoa",
"ryu",
@@ -2819,9 +2814,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "syn"
version = "2.0.60"
version = "2.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704"
dependencies = [
"proc-macro2",
"quote",
@@ -2909,18 +2904,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.59"
version = "1.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa"
checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.59"
version = "1.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66"
checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -12,6 +12,28 @@ authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
license = "MIT"
[workspace.dependencies]
ruff = { path = "crates/ruff" }
ruff_cache = { path = "crates/ruff_cache" }
ruff_diagnostics = { path = "crates/ruff_diagnostics" }
ruff_formatter = { path = "crates/ruff_formatter" }
ruff_index = { path = "crates/ruff_index" }
ruff_linter = { path = "crates/ruff_linter" }
ruff_macros = { path = "crates/ruff_macros" }
ruff_notebook = { path = "crates/ruff_notebook" }
ruff_python_ast = { path = "crates/ruff_python_ast" }
ruff_python_codegen = { path = "crates/ruff_python_codegen" }
ruff_python_formatter = { path = "crates/ruff_python_formatter" }
ruff_python_index = { path = "crates/ruff_python_index" }
ruff_python_literal = { path = "crates/ruff_python_literal" }
ruff_python_parser = { path = "crates/ruff_python_parser" }
ruff_python_semantic = { path = "crates/ruff_python_semantic" }
ruff_python_stdlib = { path = "crates/ruff_python_stdlib" }
ruff_python_trivia = { path = "crates/ruff_python_trivia" }
ruff_server = { path = "crates/ruff_server" }
ruff_source_file = { path = "crates/ruff_source_file" }
ruff_text_size = { path = "crates/ruff_text_size" }
ruff_workspace = { path = "crates/ruff_workspace" }
aho-corasick = { version = "1.1.3" }
annotate-snippets = { version = "0.9.2", features = ["color"] }
anyhow = { version = "1.0.80" }
@@ -37,7 +59,6 @@ drop_bomb = { version = "0.1.5" }
env_logger = { version = "0.11.0" }
fern = { version = "0.6.1" }
filetime = { version = "0.2.23" }
fs-err = { version = "2.11.0" }
glob = { version = "0.3.1" }
globset = { version = "0.4.14" }
hashbrown = "0.14.3"
@@ -130,6 +151,7 @@ char_lit_as_u8 = "allow"
collapsible_else_if = "allow"
collapsible_if = "allow"
implicit_hasher = "allow"
map_unwrap_or = "allow"
match_same_arms = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"

View File

@@ -266,6 +266,11 @@ The remaining configuration options can be provided through a catch-all `--confi
ruff check --config "lint.per-file-ignores = {'some_file.py' = ['F841']}"
```
To opt in to the latest lint rules, formatter style changes, interface updates, and more, enable
[preview mode](https://docs.astral.sh/ruff/rules/) by setting `preview = true` in your configuration
file or passing `--preview` on the command line. Preview mode enables a collection of unstable
features that may change prior to stabilization.
See `ruff help` for more on Ruff's top-level commands, or `ruff help check` and `ruff help format`
for more on the linting and formatting commands, respectively.

View File

@@ -12,33 +12,30 @@ license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ruff_python_parser = { path = "../ruff_python_parser" }
ruff_python_ast = { path = "../ruff_python_ast" }
ruff_python_trivia = { path = "../ruff_python_trivia" }
ruff_text_size = { path = "../ruff_text_size" }
ruff_index = { path = "../ruff_index" }
ruff_notebook = { path = "../ruff_notebook" }
ruff_python_parser = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_text_size = { workspace = true }
ruff_index = { workspace = true }
ruff_notebook = { workspace = true }
anyhow = { workspace = true }
bitflags = { workspace = true }
ctrlc = "3.4.4"
crossbeam = { workspace = true }
ctrlc = { version = "3.4.4" }
dashmap = { workspace = true }
hashbrown = { workspace = true }
indexmap = { workspace = true }
log = { workspace = true }
notify = { workspace = true }
parking_lot = { workspace = true }
rayon = { workspace = true }
rustc-hash = { workspace = true }
smallvec = { workspace = true }
smol_str = "0.2.1"
smol_str = { version = "0.2.1" }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-tree = { workspace = true }
[dev-dependencies]
textwrap = "0.16.1"
textwrap = { version = "0.16.1" }
tempfile = { workspace = true }
[lints]

View File

@@ -6,13 +6,4 @@ The Red Knot crate contains code working towards multifile analysis, type infere
Red Knot vendors [typeshed](https://github.com/python/typeshed)'s stubs for the standard library. The vendored stubs can be found in `crates/red_knot/vendor/typeshed`. The file `crates/red_knot/vendor/typeshed/source_commit.txt` tells you the typeshed commit that our vendored stdlib stubs currently correspond to.
Updating the vendored stubs is currently done manually. On a Unix machine, follow the following steps (if you have a typeshed clone in a `typeshed` directory, and a Ruff clone in a `ruff` directory):
```shell
rm -rf ruff/crates/red_knot/vendor/typeshed
mkdir ruff/crates/red_knot/vendor/typeshed
cp typeshed/README.md ruff/crates/red_knot/vendor/typeshed
cp typeshed/LICENSE ruff/crates/red_knot/vendor/typeshed
cp -r typeshed/stdlib ruff/crates/red_knot/vendor/typeshed/stdlib
git -C typeshed rev-parse HEAD > ruff/crates/red_knot/vendor/typeshed/source_commit.txt
```
The typeshed stubs are updated every two weeks via an automated PR using the `sync_typeshed.yaml` workflow in the `.github/workflows` directory. This workflow can also be triggered at any time via [workflow dispatch](https://docs.github.com/en/actions/using-workflows/manually-running-a-workflow#running-a-workflow).

View File

@@ -1 +1 @@
2d33fe212221a05661c0db5215a91cf3d7b7f072
a9d7e861f7a46ae7acd56569326adef302e10f29

View File

@@ -0,0 +1,18 @@
# Implicit protocols used in importlib.
# We intentionally omit deprecated and optional methods.
from collections.abc import Sequence
from importlib.machinery import ModuleSpec
from types import ModuleType
from typing import Protocol
__all__ = ["LoaderProtocol", "MetaPathFinderProtocol", "PathEntryFinderProtocol"]
class LoaderProtocol(Protocol):
def load_module(self, fullname: str, /) -> ModuleType: ...
class MetaPathFinderProtocol(Protocol):
def find_spec(self, fullname: str, path: Sequence[str] | None, target: ModuleType | None = ..., /) -> ModuleSpec | None: ...
class PathEntryFinderProtocol(Protocol):
def find_spec(self, fullname: str, target: ModuleType | None = ..., /) -> ModuleSpec | None: ...

View File

@@ -31,7 +31,7 @@ from _typeshed import (
)
from collections.abc import Awaitable, Callable, Iterable, Iterator, MutableSet, Reversible, Set as AbstractSet, Sized
from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWrapper
from types import CodeType, TracebackType, _Cell
from types import CellType, CodeType, TracebackType
# mypy crashes if any of {ByteString, Sequence, MutableSequence, Mapping, MutableMapping} are imported from collections.abc in builtins.pyi
from typing import ( # noqa: Y022
@@ -951,7 +951,7 @@ class tuple(Sequence[_T_co]):
class function:
# Make sure this class definition stays roughly in line with `types.FunctionType`
@property
def __closure__(self) -> tuple[_Cell, ...] | None: ...
def __closure__(self) -> tuple[CellType, ...] | None: ...
__code__: CodeType
__defaults__: tuple[Any, ...] | None
__dict__: dict[str, Any]
@@ -1333,7 +1333,7 @@ if sys.version_info >= (3, 11):
locals: Mapping[str, object] | None = None,
/,
*,
closure: tuple[_Cell, ...] | None = None,
closure: tuple[CellType, ...] | None = None,
) -> None: ...
else:
@@ -1794,7 +1794,7 @@ def __import__(
fromlist: Sequence[str] = (),
level: int = 0,
) -> types.ModuleType: ...
def __build_class__(func: Callable[[], _Cell | Any], name: str, /, *bases: Any, metaclass: Any = ..., **kwds: Any) -> Any: ...
def __build_class__(func: Callable[[], CellType | Any], name: str, /, *bases: Any, metaclass: Any = ..., **kwds: Any) -> Any: ...
if sys.version_info >= (3, 10):
from types import EllipsisType

View File

@@ -1,3 +1,5 @@
import sys
from _typeshed import StrOrBytesPath
from collections.abc import Iterator, MutableMapping
from types import TracebackType
from typing import Literal
@@ -91,5 +93,10 @@ class _error(Exception): ...
error: tuple[type[_error], type[OSError]]
def whichdb(filename: str) -> str | None: ...
def open(file: str, flag: _TFlags = "r", mode: int = 0o666) -> _Database: ...
if sys.version_info >= (3, 11):
def whichdb(filename: StrOrBytesPath) -> str | None: ...
def open(file: StrOrBytesPath, flag: _TFlags = "r", mode: int = 0o666) -> _Database: ...
else:
def whichdb(filename: str) -> str | None: ...
def open(file: str, flag: _TFlags = "r", mode: int = 0o666) -> _Database: ...

View File

@@ -1,3 +1,5 @@
import sys
from _typeshed import StrOrBytesPath
from collections.abc import Iterator, MutableMapping
from types import TracebackType
from typing_extensions import Self, TypeAlias
@@ -28,4 +30,8 @@ class _Database(MutableMapping[_KeyType, bytes]):
self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
) -> None: ...
def open(file: str, flag: str = "c", mode: int = 0o666) -> _Database: ...
if sys.version_info >= (3, 11):
def open(file: StrOrBytesPath, flag: str = "c", mode: int = 0o666) -> _Database: ...
else:
def open(file: str, flag: str = "c", mode: int = 0o666) -> _Database: ...

View File

@@ -1,5 +1,5 @@
import sys
from _typeshed import ReadOnlyBuffer
from _typeshed import ReadOnlyBuffer, StrOrBytesPath
from types import TracebackType
from typing import TypeVar, overload
from typing_extensions import Self, TypeAlias
@@ -38,4 +38,7 @@ if sys.platform != "win32":
__new__: None # type: ignore[assignment]
__init__: None # type: ignore[assignment]
def open(filename: str, flags: str = "r", mode: int = 0o666, /) -> _gdbm: ...
if sys.version_info >= (3, 11):
def open(filename: StrOrBytesPath, flags: str = "r", mode: int = 0o666, /) -> _gdbm: ...
else:
def open(filename: str, flags: str = "r", mode: int = 0o666, /) -> _gdbm: ...

View File

@@ -1,5 +1,5 @@
import sys
from _typeshed import ReadOnlyBuffer
from _typeshed import ReadOnlyBuffer, StrOrBytesPath
from types import TracebackType
from typing import TypeVar, overload
from typing_extensions import Self, TypeAlias
@@ -34,4 +34,7 @@ if sys.platform != "win32":
__new__: None # type: ignore[assignment]
__init__: None # type: ignore[assignment]
def open(filename: str, flags: str = "r", mode: int = 0o666, /) -> _dbm: ...
if sys.version_info >= (3, 11):
def open(filename: StrOrBytesPath, flags: str = "r", mode: int = 0o666, /) -> _dbm: ...
else:
def open(filename: str, flags: str = "r", mode: int = 0o666, /) -> _dbm: ...

View File

@@ -64,7 +64,7 @@ class SourceLoader(ResourceLoader, ExecutionLoader, metaclass=ABCMeta):
# The base classes differ starting in 3.10:
if sys.version_info >= (3, 10):
# Please keep in sync with sys._MetaPathFinder
# Please keep in sync with _typeshed.importlib.MetaPathFinderProtocol
class MetaPathFinder(metaclass=ABCMeta):
if sys.version_info < (3, 12):
def find_module(self, fullname: str, path: Sequence[str] | None) -> Loader | None: ...
@@ -85,7 +85,7 @@ if sys.version_info >= (3, 10):
def find_spec(self, fullname: str, target: types.ModuleType | None = ...) -> ModuleSpec | None: ...
else:
# Please keep in sync with sys._MetaPathFinder
# Please keep in sync with _typeshed.importlib.MetaPathFinderProtocol
class MetaPathFinder(Finder):
def find_module(self, fullname: str, path: Sequence[str] | None) -> Loader | None: ...
def invalidate_caches(self) -> None: ...

View File

@@ -3,6 +3,7 @@ import importlib.machinery
import sys
import types
from _typeshed import ReadableBuffer, StrOrBytesPath
from _typeshed.importlib import LoaderProtocol
from collections.abc import Callable
from typing import Any
from typing_extensions import ParamSpec
@@ -23,13 +24,13 @@ def source_from_cache(path: str) -> str: ...
def decode_source(source_bytes: ReadableBuffer) -> str: ...
def find_spec(name: str, package: str | None = None) -> importlib.machinery.ModuleSpec | None: ...
def spec_from_loader(
name: str, loader: importlib.abc.Loader | None, *, origin: str | None = None, is_package: bool | None = None
name: str, loader: LoaderProtocol | None, *, origin: str | None = None, is_package: bool | None = None
) -> importlib.machinery.ModuleSpec | None: ...
def spec_from_file_location(
name: str,
location: StrOrBytesPath | None = None,
*,
loader: importlib.abc.Loader | None = None,
loader: LoaderProtocol | None = None,
submodule_search_locations: list[str] | None = ...,
) -> importlib.machinery.ModuleSpec | None: ...
def module_from_spec(spec: importlib.machinery.ModuleSpec) -> types.ModuleType: ...

View File

@@ -1,7 +1,7 @@
import sys
from _typeshed import SupportsRead
from _typeshed.importlib import LoaderProtocol, MetaPathFinderProtocol, PathEntryFinderProtocol
from collections.abc import Callable, Iterable, Iterator
from importlib.abc import Loader, MetaPathFinder, PathEntryFinder
from typing import IO, Any, NamedTuple, TypeVar
from typing_extensions import deprecated
@@ -23,7 +23,7 @@ if sys.version_info < (3, 12):
_PathT = TypeVar("_PathT", bound=Iterable[str])
class ModuleInfo(NamedTuple):
module_finder: MetaPathFinder | PathEntryFinder
module_finder: MetaPathFinderProtocol | PathEntryFinderProtocol
name: str
ispkg: bool
@@ -37,11 +37,11 @@ if sys.version_info < (3, 12):
def __init__(self, fullname: str, file: IO[str], filename: str, etc: tuple[str, str, int]) -> None: ...
@deprecated("Use importlib.util.find_spec() instead. Will be removed in Python 3.14.")
def find_loader(fullname: str) -> Loader | None: ...
def get_importer(path_item: str) -> PathEntryFinder | None: ...
def find_loader(fullname: str) -> LoaderProtocol | None: ...
def get_importer(path_item: str) -> PathEntryFinderProtocol | None: ...
@deprecated("Use importlib.util.find_spec() instead. Will be removed in Python 3.14.")
def get_loader(module_or_name: str) -> Loader | None: ...
def iter_importers(fullname: str = "") -> Iterator[MetaPathFinder | PathEntryFinder]: ...
def get_loader(module_or_name: str) -> LoaderProtocol | None: ...
def iter_importers(fullname: str = "") -> Iterator[MetaPathFinderProtocol | PathEntryFinderProtocol]: ...
def iter_modules(path: Iterable[str] | None = None, prefix: str = "") -> Iterator[ModuleInfo]: ...
def read_code(stream: SupportsRead[bytes]) -> Any: ... # undocumented
def walk_packages(

View File

@@ -1,3 +1,5 @@
import sys
from _typeshed import StrOrBytesPath
from collections.abc import Iterator, MutableMapping
from dbm import _TFlags
from types import TracebackType
@@ -41,6 +43,17 @@ class BsdDbShelf(Shelf[_VT]):
def last(self) -> tuple[str, _VT]: ...
class DbfilenameShelf(Shelf[_VT]):
def __init__(self, filename: str, flag: _TFlags = "c", protocol: int | None = None, writeback: bool = False) -> None: ...
if sys.version_info >= (3, 11):
def __init__(
self, filename: StrOrBytesPath, flag: _TFlags = "c", protocol: int | None = None, writeback: bool = False
) -> None: ...
else:
def __init__(self, filename: str, flag: _TFlags = "c", protocol: int | None = None, writeback: bool = False) -> None: ...
def open(filename: str, flag: _TFlags = "c", protocol: int | None = None, writeback: bool = False) -> Shelf[Any]: ...
if sys.version_info >= (3, 11):
def open(
filename: StrOrBytesPath, flag: _TFlags = "c", protocol: int | None = None, writeback: bool = False
) -> Shelf[Any]: ...
else:
def open(filename: str, flag: _TFlags = "c", protocol: int | None = None, writeback: bool = False) -> Shelf[Any]: ...

View File

@@ -474,6 +474,13 @@ if sys.version_info >= (3, 12):
ETHERTYPE_VLAN as ETHERTYPE_VLAN,
)
if sys.platform == "linux":
from _socket import ETH_P_ALL as ETH_P_ALL
if sys.platform != "linux" and sys.platform != "win32" and sys.platform != "darwin":
# FreeBSD >= 14.0
from _socket import PF_DIVERT as PF_DIVERT
# Re-exported from errno
EBADF: int
EAGAIN: int
@@ -525,6 +532,9 @@ class AddressFamily(IntEnum):
AF_BLUETOOTH = 32
if sys.platform == "win32" and sys.version_info >= (3, 12):
AF_HYPERV = 34
if sys.platform != "linux" and sys.platform != "win32" and sys.platform != "darwin" and sys.version_info >= (3, 12):
# FreeBSD >= 14.0
AF_DIVERT = 44
AF_INET = AddressFamily.AF_INET
AF_INET6 = AddressFamily.AF_INET6
@@ -577,6 +587,9 @@ if sys.platform != "win32" or sys.version_info >= (3, 9):
if sys.platform == "win32" and sys.version_info >= (3, 12):
AF_HYPERV = AddressFamily.AF_HYPERV
if sys.platform != "linux" and sys.platform != "win32" and sys.platform != "darwin" and sys.version_info >= (3, 12):
# FreeBSD >= 14.0
AF_DIVERT = AddressFamily.AF_DIVERT
class SocketKind(IntEnum):
SOCK_STREAM = 1

View File

@@ -1,9 +1,8 @@
import sys
from _typeshed import OptExcInfo, ProfileFunction, TraceFunction, structseq
from _typeshed.importlib import MetaPathFinderProtocol, PathEntryFinderProtocol
from builtins import object as _object
from collections.abc import AsyncGenerator, Callable, Sequence
from importlib.abc import PathEntryFinder
from importlib.machinery import ModuleSpec
from io import TextIOWrapper
from types import FrameType, ModuleType, TracebackType
from typing import Any, Final, Literal, NoReturn, Protocol, TextIO, TypeVar, final
@@ -15,10 +14,6 @@ _T = TypeVar("_T")
_ExitCode: TypeAlias = str | int | None
_OptExcInfo: TypeAlias = OptExcInfo # noqa: Y047 # TODO: obsolete, remove fall 2022 or later
# Intentionally omits one deprecated and one optional method of `importlib.abc.MetaPathFinder`
class _MetaPathFinder(Protocol):
def find_spec(self, fullname: str, path: Sequence[str] | None, target: ModuleType | None = ..., /) -> ModuleSpec | None: ...
# ----- sys variables -----
if sys.platform != "win32":
abiflags: str
@@ -44,13 +39,13 @@ if sys.version_info >= (3, 12):
last_exc: BaseException # or undefined.
maxsize: int
maxunicode: int
meta_path: list[_MetaPathFinder]
meta_path: list[MetaPathFinderProtocol]
modules: dict[str, ModuleType]
if sys.version_info >= (3, 10):
orig_argv: list[str]
path: list[str]
path_hooks: list[Callable[[str], PathEntryFinder]]
path_importer_cache: dict[str, PathEntryFinder | None]
path_hooks: list[Callable[[str], PathEntryFinderProtocol]]
path_importer_cache: dict[str, PathEntryFinderProtocol | None]
platform: str
if sys.version_info >= (3, 9):
platlibdir: str

View File

@@ -374,7 +374,11 @@ class SpooledTemporaryFile(IO[AnyStr], _SpooledTemporaryFileBase):
def readlines(self, hint: int = ..., /) -> list[AnyStr]: ... # type: ignore[override]
def seek(self, offset: int, whence: int = ...) -> int: ...
def tell(self) -> int: ...
def truncate(self, size: int | None = None) -> None: ... # type: ignore[override]
if sys.version_info >= (3, 11):
def truncate(self, size: int | None = None) -> int: ...
else:
def truncate(self, size: int | None = None) -> None: ... # type: ignore[override]
@overload
def write(self: SpooledTemporaryFile[str], s: str) -> int: ...
@overload

View File

@@ -1,5 +1,6 @@
import sys
from _typeshed import SupportsKeysAndGetItem
from _typeshed.importlib import LoaderProtocol
from collections.abc import (
AsyncGenerator,
Awaitable,
@@ -16,7 +17,7 @@ from collections.abc import (
from importlib.machinery import ModuleSpec
# pytype crashes if types.MappingProxyType inherits from collections.abc.Mapping instead of typing.Mapping
from typing import Any, ClassVar, Literal, Mapping, Protocol, TypeVar, final, overload # noqa: Y022
from typing import Any, ClassVar, Literal, Mapping, TypeVar, final, overload # noqa: Y022
from typing_extensions import ParamSpec, Self, TypeVarTuple, deprecated
__all__ = [
@@ -64,18 +65,11 @@ _T2 = TypeVar("_T2")
_KT = TypeVar("_KT")
_VT_co = TypeVar("_VT_co", covariant=True)
@final
class _Cell:
def __new__(cls, contents: object = ..., /) -> Self: ...
def __eq__(self, value: object, /) -> bool: ...
__hash__: ClassVar[None] # type: ignore[assignment]
cell_contents: Any
# Make sure this class definition stays roughly in line with `builtins.function`
@final
class FunctionType:
@property
def __closure__(self) -> tuple[_Cell, ...] | None: ...
def __closure__(self) -> tuple[CellType, ...] | None: ...
__code__: CodeType
__defaults__: tuple[Any, ...] | None
__dict__: dict[str, Any]
@@ -98,7 +92,7 @@ class FunctionType:
globals: dict[str, Any],
name: str | None = ...,
argdefs: tuple[object, ...] | None = ...,
closure: tuple[_Cell, ...] | None = ...,
closure: tuple[CellType, ...] | None = ...,
) -> Self: ...
def __call__(self, *args: Any, **kwargs: Any) -> Any: ...
@overload
@@ -318,15 +312,12 @@ class SimpleNamespace:
def __setattr__(self, name: str, value: Any, /) -> None: ...
def __delattr__(self, name: str, /) -> None: ...
class _LoaderProtocol(Protocol):
def load_module(self, fullname: str, /) -> ModuleType: ...
class ModuleType:
__name__: str
__file__: str | None
@property
def __dict__(self) -> dict[str, Any]: ... # type: ignore[override]
__loader__: _LoaderProtocol | None
__loader__: LoaderProtocol | None
__package__: str | None
__path__: MutableSequence[str]
__spec__: ModuleSpec | None
@@ -336,6 +327,12 @@ class ModuleType:
# using `builtins.__import__` or `importlib.import_module` less painful
def __getattr__(self, name: str) -> Any: ...
@final
class CellType:
def __new__(cls, contents: object = ..., /) -> Self: ...
__hash__: ClassVar[None] # type: ignore[assignment]
cell_contents: Any
_YieldT_co = TypeVar("_YieldT_co", covariant=True)
_SendT_contra = TypeVar("_SendT_contra", contravariant=True)
_ReturnT_co = TypeVar("_ReturnT_co", covariant=True)
@@ -405,7 +402,7 @@ class CoroutineType(Coroutine[_YieldT_co, _SendT_contra, _ReturnT_co]):
@final
class MethodType:
@property
def __closure__(self) -> tuple[_Cell, ...] | None: ... # inherited from the added function
def __closure__(self) -> tuple[CellType, ...] | None: ... # inherited from the added function
@property
def __defaults__(self) -> tuple[Any, ...] | None: ... # inherited from the added function
@property
@@ -570,8 +567,6 @@ def coroutine(func: Callable[_P, Generator[Any, Any, _R]]) -> Callable[_P, Await
@overload
def coroutine(func: _Fn) -> _Fn: ...
CellType = _Cell
if sys.version_info >= (3, 9):
class GenericAlias:
@property

View File

@@ -8,7 +8,6 @@ import typing_extensions
from _collections_abc import dict_items, dict_keys, dict_values
from _typeshed import IdentityFunction, ReadableBuffer, SupportsKeysAndGetItem
from abc import ABCMeta, abstractmethod
from contextlib import AbstractAsyncContextManager, AbstractContextManager
from re import Match as Match, Pattern as Pattern
from types import (
BuiltinFunctionType,
@@ -24,10 +23,10 @@ from types import (
)
from typing_extensions import Never as _Never, ParamSpec as _ParamSpec
if sys.version_info >= (3, 10):
from types import UnionType
if sys.version_info >= (3, 9):
from types import GenericAlias
if sys.version_info >= (3, 10):
from types import UnionType
__all__ = [
"AbstractSet",
@@ -402,8 +401,8 @@ class Reversible(Iterable[_T_co], Protocol[_T_co]):
def __reversed__(self) -> Iterator[_T_co]: ...
_YieldT_co = TypeVar("_YieldT_co", covariant=True)
_SendT_contra = TypeVar("_SendT_contra", contravariant=True)
_ReturnT_co = TypeVar("_ReturnT_co", covariant=True)
_SendT_contra = TypeVar("_SendT_contra", contravariant=True, default=None)
_ReturnT_co = TypeVar("_ReturnT_co", covariant=True, default=None)
class Generator(Iterator[_YieldT_co], Generic[_YieldT_co, _SendT_contra, _ReturnT_co]):
def __next__(self) -> _YieldT_co: ...
@@ -428,24 +427,28 @@ class Generator(Iterator[_YieldT_co], Generic[_YieldT_co, _SendT_contra, _Return
@property
def gi_yieldfrom(self) -> Generator[Any, Any, Any] | None: ...
# NOTE: Technically we would like this to be able to accept a second parameter as well, just
# like it's counterpart in contextlib, however `typing._SpecialGenericAlias` enforces the
# correct number of arguments at runtime, so we would be hiding runtime errors.
@runtime_checkable
class ContextManager(AbstractContextManager[_T_co, bool | None], Protocol[_T_co]): ...
# NOTE: Prior to Python 3.13 these aliases are lacking the second _ExitT_co parameter
if sys.version_info >= (3, 13):
from contextlib import AbstractAsyncContextManager as AsyncContextManager, AbstractContextManager as ContextManager
else:
from contextlib import AbstractAsyncContextManager, AbstractContextManager
# NOTE: Technically we would like this to be able to accept a second parameter as well, just
# like it's counterpart in contextlib, however `typing._SpecialGenericAlias` enforces the
# correct number of arguments at runtime, so we would be hiding runtime errors.
@runtime_checkable
class AsyncContextManager(AbstractAsyncContextManager[_T_co, bool | None], Protocol[_T_co]): ...
@runtime_checkable
class ContextManager(AbstractContextManager[_T_co, bool | None], Protocol[_T_co]): ...
@runtime_checkable
class AsyncContextManager(AbstractAsyncContextManager[_T_co, bool | None], Protocol[_T_co]): ...
@runtime_checkable
class Awaitable(Protocol[_T_co]):
@abstractmethod
def __await__(self) -> Generator[Any, Any, _T_co]: ...
class Coroutine(Awaitable[_ReturnT_co], Generic[_YieldT_co, _SendT_contra, _ReturnT_co]):
# Non-default variations to accommodate couroutines, and `AwaitableGenerator` having a 4th type parameter.
_SendT_contra_nd = TypeVar("_SendT_contra_nd", contravariant=True)
_ReturnT_co_nd = TypeVar("_ReturnT_co_nd", covariant=True)
class Coroutine(Awaitable[_ReturnT_co_nd], Generic[_YieldT_co, _SendT_contra_nd, _ReturnT_co_nd]):
__name__: str
__qualname__: str
@property
@@ -457,7 +460,7 @@ class Coroutine(Awaitable[_ReturnT_co], Generic[_YieldT_co, _SendT_contra, _Retu
@property
def cr_running(self) -> bool: ...
@abstractmethod
def send(self, value: _SendT_contra, /) -> _YieldT_co: ...
def send(self, value: _SendT_contra_nd, /) -> _YieldT_co: ...
@overload
@abstractmethod
def throw(
@@ -473,9 +476,9 @@ class Coroutine(Awaitable[_ReturnT_co], Generic[_YieldT_co, _SendT_contra, _Retu
# The parameters correspond to Generator, but the 4th is the original type.
@type_check_only
class AwaitableGenerator(
Awaitable[_ReturnT_co],
Generator[_YieldT_co, _SendT_contra, _ReturnT_co],
Generic[_YieldT_co, _SendT_contra, _ReturnT_co, _S],
Awaitable[_ReturnT_co_nd],
Generator[_YieldT_co, _SendT_contra_nd, _ReturnT_co_nd],
Generic[_YieldT_co, _SendT_contra_nd, _ReturnT_co_nd, _S],
metaclass=ABCMeta,
): ...

View File

@@ -13,17 +13,17 @@ readme = "../../README.md"
default-run = "ruff"
[dependencies]
ruff_cache = { path = "../ruff_cache" }
ruff_diagnostics = { path = "../ruff_diagnostics" }
ruff_linter = { path = "../ruff_linter", features = ["clap"] }
ruff_macros = { path = "../ruff_macros" }
ruff_notebook = { path = "../ruff_notebook" }
ruff_python_ast = { path = "../ruff_python_ast" }
ruff_python_formatter = { path = "../ruff_python_formatter" }
ruff_server = { path = "../ruff_server" }
ruff_source_file = { path = "../ruff_source_file" }
ruff_text_size = { path = "../ruff_text_size" }
ruff_workspace = { path = "../ruff_workspace" }
ruff_cache = { workspace = true }
ruff_diagnostics = { workspace = true }
ruff_linter = { workspace = true, features = ["clap"] }
ruff_macros = { workspace = true }
ruff_notebook = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_formatter = { workspace = true }
ruff_server = { workspace = true }
ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }
ruff_workspace = { workspace = true }
anyhow = { workspace = true }
argfile = { workspace = true }
@@ -60,7 +60,7 @@ wild = { workspace = true }
[dev-dependencies]
# Enable test rules during development
ruff_linter = { path = "../ruff_linter", features = ["clap", "test-rules"] }
ruff_linter = { workspace = true, features = ["clap", "test-rules"] }
# Avoid writing colored snapshots when running tests from the terminal
colored = { workspace = true, features = ["no-color"] }
insta = { workspace = true, features = ["filters", "json"] }
@@ -68,6 +68,10 @@ insta-cmd = { workspace = true }
tempfile = { workspace = true }
test-case = { workspace = true }
[package.metadata.cargo-shear]
# Used via macro expansion.
ignored = ["chrono"]
[target.'cfg(target_os = "windows")'.dependencies]
mimalloc = { workspace = true }

View File

@@ -111,7 +111,13 @@ pub enum Command {
output_format: HelpFormat,
},
/// List or describe the available configuration options.
Config { option: Option<String> },
Config {
/// Config key to show
option: Option<String>,
/// Output format
#[arg(long, value_enum, default_value = "text")]
output_format: HelpFormat,
},
/// List all supported upstream linters.
Linter {
/// Output format

View File

@@ -1,19 +1,38 @@
use anyhow::{anyhow, Result};
use crate::args::HelpFormat;
use ruff_workspace::options::Options;
use ruff_workspace::options_base::OptionsMetadata;
#[allow(clippy::print_stdout)]
pub(crate) fn config(key: Option<&str>) -> Result<()> {
pub(crate) fn config(key: Option<&str>, format: HelpFormat) -> Result<()> {
match key {
None => print!("{}", Options::metadata()),
None => {
let metadata = Options::metadata();
match format {
HelpFormat::Text => {
println!("{metadata}");
}
HelpFormat::Json => {
println!("{}", &serde_json::to_string_pretty(&metadata)?);
}
}
}
Some(key) => match Options::metadata().find(key) {
None => {
return Err(anyhow!("Unknown option: {key}"));
}
Some(entry) => {
print!("{entry}");
}
Some(entry) => match format {
HelpFormat::Text => {
print!("{entry}");
}
HelpFormat::Json => {
println!("{}", &serde_json::to_string_pretty(&entry)?);
}
},
},
}
Ok(())

View File

@@ -180,8 +180,11 @@ pub fn run(
}
Ok(ExitStatus::Success)
}
Command::Config { option } => {
commands::config::config(option.as_deref())?;
Command::Config {
option,
output_format,
} => {
commands::config::config(option.as_deref(), output_format)?;
Ok(ExitStatus::Success)
}
Command::Linter { output_format } => {

View File

@@ -0,0 +1,55 @@
//! Tests for the `ruff config` subcommand.
use std::process::Command;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
const BIN_NAME: &str = "ruff";
#[test]
fn lint_select() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME)).arg("config").arg("lint.select"), @r###"
success: true
exit_code: 0
----- stdout -----
A list of rule codes or prefixes to enable. Prefixes can specify exact
rules (like `F841`), entire categories (like `F`), or anything in
between.
When breaking ties between enabled and disabled rules (via `select` and
`ignore`, respectively), more specific prefixes override less
specific prefixes.
Default value: ["E4", "E7", "E9", "F"]
Type: list[RuleSelector]
Example usage:
```toml
# On top of the defaults (`E4`, E7`, `E9`, and `F`), enable flake8-bugbear (`B`) and flake8-quotes (`Q`).
select = ["E4", "E7", "E9", "F", "B", "Q"]
```
----- stderr -----
"###
);
}
#[test]
fn lint_select_json() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME)).arg("config").arg("lint.select").arg("--output-format").arg("json"), @r###"
success: true
exit_code: 0
----- stdout -----
{
"doc": "A list of rule codes or prefixes to enable. Prefixes can specify exact\nrules (like `F841`), entire categories (like `F`), or anything in\nbetween.\n\nWhen breaking ties between enabled and disabled rules (via `select` and\n`ignore`, respectively), more specific prefixes override less\nspecific prefixes.",
"default": "[\"E4\", \"E7\", \"E9\", \"F\"]",
"value_type": "list[RuleSelector]",
"scope": null,
"example": "# On top of the defaults (`E4`, E7`, `E9`, and `F`), enable flake8-bugbear (`B`) and flake8-quotes (`Q`).\nselect = [\"E4\", \"E7\", \"E9\", \"F\", \"B\", \"Q\"]",
"deprecated": null
}
----- stderr -----
"###
);
}

View File

@@ -1553,3 +1553,68 @@ def unused(x): # noqa: ANN001, ARG001, D103
Ok(())
}
#[test]
fn add_noqa_multiline_comment() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint]
select = ["UP031"]
"#,
)?;
let test_path = tempdir.path().join("noqa.py");
fs::write(
&test_path,
r#"
print(
"""First line
second line
third line
%s"""
% name
)
"#,
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.arg(&test_path)
.arg("--preview")
.args(["--add-noqa"])
.arg("-")
.pass_stdin(r#"
"#), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Added 1 noqa directive.
"###);
});
let test_code = std::fs::read_to_string(&test_path).expect("should read test file");
insta::assert_snapshot!(test_code, @r###"
print(
"""First line
second line
third line
%s""" # noqa: UP031
% name
)
"###);
Ok(())
}

View File

@@ -38,14 +38,14 @@ serde_json = { workspace = true }
url = { workspace = true }
ureq = { workspace = true }
criterion = { workspace = true, default-features = false }
codspeed-criterion-compat = { workspace = true, default-features = false, optional = true}
codspeed-criterion-compat = { workspace = true, default-features = false, optional = true }
[dev-dependencies]
ruff_linter = { path = "../ruff_linter" }
ruff_python_ast = { path = "../ruff_python_ast" }
ruff_python_formatter = { path = "../ruff_python_formatter" }
ruff_python_index = { path = "../ruff_python_index" }
ruff_python_parser = { path = "../ruff_python_parser" }
ruff_linter = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_formatter = { workspace = true }
ruff_python_index = { workspace = true }
ruff_python_parser = { workspace = true }
[lints]
workspace = true

View File

@@ -10,7 +10,7 @@ use ruff_linter::settings::{flags, LinterSettings};
use ruff_linter::source_kind::SourceKind;
use ruff_linter::{registry::Rule, RuleSelector};
use ruff_python_ast::PySourceType;
use ruff_python_parser::{lexer, parse_program_tokens, Mode};
use ruff_python_parser::{parse_program_tokens, tokenize, Mode};
#[cfg(target_os = "windows")]
#[global_allocator]
@@ -55,7 +55,7 @@ fn benchmark_linter(mut group: BenchmarkGroup, settings: &LinterSettings) {
&case,
|b, case| {
// Tokenize the source.
let tokens: Vec<_> = lexer::lex(case.code(), Mode::Module).collect();
let tokens = tokenize(case.code(), Mode::Module);
// Parse the source.
let ast = parse_program_tokens(tokens.clone(), case.code(), false).unwrap();

View File

@@ -19,7 +19,7 @@ filetime = { workspace = true }
seahash = { workspace = true }
[dev-dependencies]
ruff_macros = { path = "../ruff_macros" }
ruff_macros = { workspace = true }
[lints]
workspace = true

View File

@@ -11,18 +11,18 @@ repository = { workspace = true }
license = { workspace = true }
[dependencies]
ruff = { path = "../ruff" }
ruff_diagnostics = { path = "../ruff_diagnostics" }
ruff_formatter = { path = "../ruff_formatter" }
ruff_linter = { path = "../ruff_linter", features = ["schemars"] }
ruff_notebook = { path = "../ruff_notebook" }
ruff_python_ast = { path = "../ruff_python_ast" }
ruff_python_codegen = { path = "../ruff_python_codegen" }
ruff_python_formatter = { path = "../ruff_python_formatter" }
ruff_python_parser = { path = "../ruff_python_parser" }
ruff_python_stdlib = { path = "../ruff_python_stdlib" }
ruff_python_trivia = { path = "../ruff_python_trivia" }
ruff_workspace = { path = "../ruff_workspace", features = ["schemars"] }
ruff = { workspace = true }
ruff_diagnostics = { workspace = true }
ruff_formatter = { workspace = true }
ruff_linter = { workspace = true, features = ["schemars"] }
ruff_notebook = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_codegen = { workspace = true }
ruff_python_formatter = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_python_stdlib = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_workspace = { workspace = true, features = ["schemars"] }
anyhow = { workspace = true }
clap = { workspace = true, features = ["wrap_help"] }

View File

@@ -14,7 +14,7 @@ license = { workspace = true }
doctest = false
[dependencies]
ruff_text_size = { path = "../ruff_text_size" }
ruff_text_size = { workspace = true }
anyhow = { workspace = true }
log = { workspace = true }

View File

@@ -11,9 +11,9 @@ repository = { workspace = true }
license = { workspace = true }
[dependencies]
ruff_cache = { path = "../ruff_cache" }
ruff_macros = { path = "../ruff_macros" }
ruff_text_size = { path = "../ruff_text_size" }
ruff_cache = { workspace = true }
ruff_macros = { workspace = true }
ruff_text_size = { workspace = true }
drop_bomb = { workspace = true }
rustc-hash = { workspace = true }
@@ -25,6 +25,10 @@ unicode-width = { workspace = true }
[dev-dependencies]
[package.metadata.cargo-shear]
# Used via `CacheKey` macro expansion.
ignored = ["ruff_cache"]
[features]
serde = ["dep:serde", "ruff_text_size/serde"]
schemars = ["dep:schemars", "ruff_text_size/schemars"]

View File

@@ -14,7 +14,7 @@ license = { workspace = true }
doctest = false
[dependencies]
ruff_macros = { path = "../ruff_macros" }
ruff_macros = { workspace = true }
[dev-dependencies]
static_assertions = { workspace = true }

View File

@@ -13,20 +13,20 @@ license = { workspace = true }
[lib]
[dependencies]
ruff_cache = { path = "../ruff_cache" }
ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] }
ruff_notebook = { path = "../ruff_notebook" }
ruff_macros = { path = "../ruff_macros" }
ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] }
ruff_python_codegen = { path = "../ruff_python_codegen" }
ruff_python_index = { path = "../ruff_python_index" }
ruff_python_literal = { path = "../ruff_python_literal" }
ruff_python_semantic = { path = "../ruff_python_semantic" }
ruff_python_stdlib = { path = "../ruff_python_stdlib" }
ruff_python_trivia = { path = "../ruff_python_trivia" }
ruff_python_parser = { path = "../ruff_python_parser" }
ruff_source_file = { path = "../ruff_source_file", features = ["serde"] }
ruff_text_size = { path = "../ruff_text_size" }
ruff_cache = { workspace = true }
ruff_diagnostics = { workspace = true, features = ["serde"] }
ruff_notebook = { workspace = true }
ruff_macros = { workspace = true }
ruff_python_ast = { workspace = true, features = ["serde"] }
ruff_python_codegen = { workspace = true }
ruff_python_index = { workspace = true }
ruff_python_literal = { workspace = true }
ruff_python_semantic = { workspace = true }
ruff_python_stdlib = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_source_file = { workspace = true, features = ["serde"] }
ruff_text_size = { workspace = true }
aho-corasick = { workspace = true }
annotate-snippets = { workspace = true, features = ["color"] }

View File

@@ -6,8 +6,8 @@ class Foo2:
"""abc"""
a = 2
"str" # Str (no raise)
f"{int}" # JoinedStr (no raise)
"str" # StringLiteral
f"{int}" # FString
1j # Number (complex)
1 # Number (int)
1.0 # Number (float)
@@ -34,8 +34,8 @@ def foo1():
def foo2():
"""my docstring"""
a = 2
"str" # Str (no raise)
f"{int}" # JoinedStr (no raise)
"str" # StringLiteral
f"{int}" # FString
1j # Number (complex)
1 # Number (int)
1.0 # Number (float)

View File

@@ -0,0 +1,93 @@
# These test cases not only check for `B018` but also verifies that the semantic model
# correctly identifies certain strings as attribute docstring. And, by way of not
# raising the `B018` violation, it can be verified.
a: int
"a: docstring"
b = 1
"b: docstring" " continue"
"b: not a docstring"
c: int = 1
"c: docstring"
_a: int
"_a: docstring"
if True:
d = 1
"d: not a docstring"
(e := 1)
"e: not a docstring"
f = 0
f += 1
"f: not a docstring"
g.h = 1
"g.h: not a docstring"
(i) = 1
"i: docstring"
(j): int = 1
"j: docstring"
(k): int
"k: docstring"
l = m = 1
"l m: not a docstring"
n.a = n.b = n.c = 1
"n.*: not a docstring"
(o, p) = (1, 2)
"o p: not a docstring"
[q, r] = [1, 2]
"q r: not a docstring"
*s = 1
"s: not a docstring"
class Foo:
a = 1
"Foo.a: docstring"
b: int
"Foo.b: docstring"
"Foo.b: not a docstring"
c: int = 1
"Foo.c: docstring"
def __init__(self) -> None:
# This is actually a docstring but we currently don't detect it.
self.x = 1
"self.x: not a docstring"
t = 2
"t: not a docstring"
def random(self):
self.y = 2
"self.y: not a docstring"
u = 2
"u: not a docstring"
def add(self, y: int):
self.x += y
def function():
v = 2
"v: not a docstring"
function.a = 1
"function.a: not a docstring"

View File

@@ -24,3 +24,7 @@ foo(**{},)
# Duplicated key names won't be fixed, to avoid syntax errors.
abc(**{'a': b}, **{'a': c}) # PIE804
abc(a=1, **{'a': c}, **{'b': c}) # PIE804
# Some values need to be parenthesized.
abc(foo=1, **{'bar': (bar := 1)}) # PIE804
abc(foo=1, **{'bar': (yield 1)}) # PIE804

View File

@@ -43,3 +43,13 @@ from typing_extensions import TYPE_CHECKING
if TYPE_CHECKING:
pass # TCH005
# https://github.com/astral-sh/ruff/issues/11368
if TYPE_CHECKING:
pass
else:
pass
if TYPE_CHECKING:
pass
elif test:
pass

View File

@@ -90,3 +90,10 @@ def f():
def func() -> DataFrame[[DataFrame[_P, _R]], DataFrame[_P, _R]]:
...
def f():
from pandas import DataFrame, Series
def func(self) -> DataFrame | list[Series]:
pass

View File

@@ -63,3 +63,16 @@ if (a and
#: Okay
def f():
return 1
# Soft keywords
#: E271
type Number = int
#: E273
type Number = int
#: E275
match(foo):
case(1):
pass

View File

@@ -46,3 +46,15 @@ regex = '\\\_'
#: W605:1:7
u'foo\ bar'
#: W605:1:13
(
"foo \
bar \. baz"
)
#: W605:1:6
"foo \. bar \t"
#: W605:1:13
"foo \t bar \."

View File

@@ -1,4 +1,4 @@
"""__init__.py with __all__
"""__init__.py with nonempty __all__
Unused stdlib and third party imports are unsafe removals
@@ -33,10 +33,10 @@ from . import aliased as aliased # Ok: is redundant alias
from . import exported # Ok: is exported in __all__
# from . import unused # F401: add to __all__
from . import unused # F401: add to __all__
# from . import renamed as bees # F401: add to __all__
from . import renamed as bees # F401: add to __all__
__all__ = ["argparse", "exported"]

View File

@@ -0,0 +1,11 @@
"""__init__.py with empty __all__
"""
from . import unused # F401: add to __all__
from . import renamed as bees # F401: add to __all__
__all__ = []

View File

@@ -0,0 +1 @@
# empty module imported by __init__.py for test fixture

View File

@@ -0,0 +1 @@
# empty module imported by __init__.py for test fixture

View File

@@ -0,0 +1,11 @@
"""__init__.py with mis-typed __all__
"""
from . import unused # F401: recommend add to all w/o fix
from . import renamed as bees # F401: recommend add to all w/o fix
__all__ = None

View File

@@ -0,0 +1 @@
# empty module imported by __init__.py for test fixture

View File

@@ -0,0 +1 @@
# empty module imported by __init__.py for test fixture

View File

@@ -0,0 +1,8 @@
"""__init__.py with multiple imports added to all in one edit
"""
from . import unused, renamed as bees # F401: add to __all__
__all__ = [];

View File

@@ -0,0 +1 @@
# empty module imported by __init__.py for test fixture

View File

@@ -0,0 +1 @@
# empty module imported by __init__.py for test fixture

View File

@@ -0,0 +1,16 @@
"""__init__.py with __all__ populated by conditional plus-eq
multiple __all__ so cannot offer a fix to add to them
"""
import sys
from . import unused, exported, renamed as bees
if sys.version_info > (3, 9):
from . import also_exported
__all__ = ["exported"]
if sys.version_info >= (3, 9):
__all__ += ["also_exported"]

View File

@@ -0,0 +1 @@
# empty module imported by __init__.py for test fixture

View File

@@ -0,0 +1 @@
# empty module imported by __init__.py for test fixture

View File

@@ -0,0 +1 @@
# empty module imported by __init__.py for test fixture

View File

@@ -0,0 +1 @@
# empty module imported by __init__.py for test fixture

View File

@@ -66,3 +66,19 @@ class StudentF(object):
def setup(self):
pass
# https://github.com/astral-sh/ruff/issues/11358
class Foo:
__slots__ = ("bar",)
def __init__(self):
self.qux = 2
@property
def qux(self):
return self.bar * 2
@qux.setter
def qux(self, value):
self.bar = value / 2

View File

@@ -38,3 +38,12 @@ z = (
else
y
)
# FURB110
z = (
x
if x
else y
if y > 0
else None
)

View File

@@ -103,7 +103,7 @@ def f():
def f():
# Invalid - nonexistant error code with multibyte character
# Invalid - nonexistent error code with multibyte character
d = 1 # …noqa: F841, E50
e = 1 # …noqa: E50

View File

@@ -1065,13 +1065,17 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pyflakes::rules::invalid_print_syntax(checker, left);
}
}
Expr::BinOp(ast::ExprBinOp {
left,
op: Operator::Mod,
right,
range: _,
}) => {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = left.as_ref() {
Expr::BinOp(
bin_op @ ast::ExprBinOp {
left,
op: Operator::Mod,
right,
range: _,
},
) => {
if let Expr::StringLiteral(format_string @ ast::ExprStringLiteral { value, .. }) =
left.as_ref()
{
if checker.any_enabled(&[
Rule::PercentFormatInvalidFormat,
Rule::PercentFormatExpectedMapping,
@@ -1151,10 +1155,14 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pyupgrade::rules::printf_string_formatting(checker, expr, right);
}
if checker.enabled(Rule::BadStringFormatCharacter) {
pylint::rules::bad_string_format_character::percent(checker, expr);
pylint::rules::bad_string_format_character::percent(
checker,
expr,
format_string,
);
}
if checker.enabled(Rule::BadStringFormatType) {
pylint::rules::bad_string_format_type(checker, expr, right);
pylint::rules::bad_string_format_type(checker, bin_op, format_string);
}
if checker.enabled(Rule::HardcodedSQLExpression) {
flake8_bandit::rules::hardcoded_sql_expression(checker, expr);

View File

@@ -2,7 +2,6 @@ use ruff_diagnostics::Diagnostic;
use ruff_python_ast::helpers;
use ruff_python_ast::types::Node;
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_semantic::analyze::typing;
use ruff_python_semantic::ScopeKind;
use ruff_text_size::Ranged;
@@ -1098,9 +1097,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::too_many_nested_blocks(checker, stmt);
}
if checker.enabled(Rule::EmptyTypeCheckingBlock) {
if typing::is_type_checking_block(if_, &checker.semantic) {
flake8_type_checking::rules::empty_type_checking_block(checker, if_);
}
flake8_type_checking::rules::empty_type_checking_block(checker, if_);
}
if checker.enabled(Rule::IfTuple) {
pyflakes::rules::if_tuple(checker, if_);

View File

@@ -2,7 +2,7 @@ use ruff_python_ast::StringLike;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_bandit, flake8_pyi, flake8_quotes, ruff};
use crate::rules::{flake8_bandit, flake8_pyi, flake8_quotes, pycodestyle, ruff};
/// Run lint rules over a [`StringLike`] syntax nodes.
pub(crate) fn string_like(string_like: StringLike, checker: &mut Checker) {
@@ -36,4 +36,7 @@ pub(crate) fn string_like(string_like: StringLike, checker: &mut Checker) {
if checker.enabled(Rule::AvoidableEscapedQuote) && checker.settings.flake8_quotes.avoid_escape {
flake8_quotes::rules::avoidable_escaped_quote(checker, string_like);
}
if checker.enabled(Rule::InvalidEscapeSequence) {
pycodestyle::rules::invalid_escape_sequence(checker, string_like);
}
}

View File

@@ -73,7 +73,7 @@ mod annotation;
mod deferred;
/// State representing whether a docstring is expected or not for the next statement.
#[derive(Default, Debug, Copy, Clone, PartialEq)]
#[derive(Debug, Copy, Clone, PartialEq)]
enum DocstringState {
/// The next statement is expected to be a docstring, but not necessarily so.
///
@@ -92,15 +92,84 @@ enum DocstringState {
/// For `Foo`, the state is expected when the checker is visiting the class
/// body but isn't going to be present. While, for `bar` function, the docstring
/// is expected and present.
#[default]
Expected,
Expected(ExpectedDocstringKind),
Other,
}
impl Default for DocstringState {
/// Returns the default docstring state which is to expect a module-level docstring.
fn default() -> Self {
Self::Expected(ExpectedDocstringKind::Module)
}
}
impl DocstringState {
/// Returns `true` if the next statement is expected to be a docstring.
const fn is_expected(self) -> bool {
matches!(self, DocstringState::Expected)
/// Returns the docstring kind if the state is expecting a docstring.
const fn expected_kind(self) -> Option<ExpectedDocstringKind> {
match self {
DocstringState::Expected(kind) => Some(kind),
DocstringState::Other => None,
}
}
}
/// The kind of an expected docstring.
#[derive(Debug, Copy, Clone, PartialEq)]
enum ExpectedDocstringKind {
/// A module-level docstring.
///
/// For example,
/// ```python
/// """This is a module-level docstring."""
///
/// a = 1
/// ```
Module,
/// A class-level docstring.
///
/// For example,
/// ```python
/// class Foo:
/// """This is the docstring for `Foo` class."""
///
/// def __init__(self) -> None:
/// ...
/// ```
Class,
/// A function-level docstring.
///
/// For example,
/// ```python
/// def foo():
/// """This is the docstring for `foo` function."""
/// pass
/// ```
Function,
/// An attribute-level docstring.
///
/// For example,
/// ```python
/// a = 1
/// """This is the docstring for `a` variable."""
///
///
/// class Foo:
/// b = 1
/// """This is the docstring for `Foo.b` class variable."""
/// ```
Attribute,
}
impl ExpectedDocstringKind {
/// Returns the semantic model flag that represents the current docstring state.
const fn as_flag(self) -> SemanticModelFlags {
match self {
ExpectedDocstringKind::Attribute => SemanticModelFlags::ATTRIBUTE_DOCSTRING,
_ => SemanticModelFlags::PEP_257_DOCSTRING,
}
}
}
@@ -383,9 +452,9 @@ impl<'a> Visitor<'a> for Checker<'a> {
// Update the semantic model if it is in a docstring. This should be done after the
// flags snapshot to ensure that it gets reset once the statement is analyzed.
if self.docstring_state.is_expected() {
if let Some(kind) = self.docstring_state.expected_kind() {
if is_docstring_stmt(stmt) {
self.semantic.flags |= SemanticModelFlags::DOCSTRING;
self.semantic.flags |= kind.as_flag();
}
// Reset the state irrespective of whether the statement is a docstring or not.
self.docstring_state = DocstringState::Other;
@@ -709,7 +778,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
}
// Set the docstring state before visiting the class body.
self.docstring_state = DocstringState::Expected;
self.docstring_state = DocstringState::Expected(ExpectedDocstringKind::Class);
self.visit_body(body);
let scope_id = self.semantic.scope_id;
@@ -874,6 +943,24 @@ impl<'a> Visitor<'a> for Checker<'a> {
_ => visitor::walk_stmt(self, stmt),
};
if self.semantic().at_top_level() || self.semantic().current_scope().kind.is_class() {
match stmt {
Stmt::Assign(ast::StmtAssign { targets, .. }) => {
if let [Expr::Name(_)] = targets.as_slice() {
self.docstring_state =
DocstringState::Expected(ExpectedDocstringKind::Attribute);
}
}
Stmt::AnnAssign(ast::StmtAnnAssign { target, .. }) => {
if target.is_name_expr() {
self.docstring_state =
DocstringState::Expected(ExpectedDocstringKind::Attribute);
}
}
_ => {}
}
}
// Step 3: Clean-up
// Step 4: Analysis
@@ -2122,7 +2209,7 @@ impl<'a> Checker<'a> {
self.visit_parameters(parameters);
// Set the docstring state before visiting the function body.
self.docstring_state = DocstringState::Expected;
self.docstring_state = DocstringState::Expected(ExpectedDocstringKind::Function);
self.visit_body(body);
}
}

View File

@@ -5,13 +5,13 @@ use std::path::Path;
use ruff_notebook::CellOffsets;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
use ruff_diagnostics::Diagnostic;
use ruff_python_index::Indexer;
use ruff_source_file::Locator;
use crate::directives::TodoComment;
use crate::linter::TokenSource;
use crate::registry::{AsRule, Rule};
use crate::rules::pycodestyle::rules::BlankLinesChecker;
use crate::rules::{
@@ -22,7 +22,7 @@ use crate::settings::LinterSettings;
#[allow(clippy::too_many_arguments)]
pub(crate) fn check_tokens(
tokens: &[LexResult],
tokens: &TokenSource,
path: &Path,
locator: &Locator,
indexer: &Indexer,
@@ -42,7 +42,7 @@ pub(crate) fn check_tokens(
Rule::BlankLinesBeforeNestedDefinition,
]) {
BlankLinesChecker::new(locator, stylist, settings, source_type, cell_offsets)
.check_lines(tokens, &mut diagnostics);
.check_lines(tokens.kinds(), &mut diagnostics);
}
if settings.rules.enabled(Rule::BlanketTypeIgnore) {
@@ -75,18 +75,6 @@ pub(crate) fn check_tokens(
pyupgrade::rules::unnecessary_coding_comment(&mut diagnostics, locator, indexer);
}
if settings.rules.enabled(Rule::InvalidEscapeSequence) {
for (tok, range) in tokens.iter().flatten() {
pycodestyle::rules::invalid_escape_sequence(
&mut diagnostics,
locator,
indexer,
tok,
*range,
);
}
}
if settings.rules.enabled(Rule::TabIndentation) {
pycodestyle::rules::tab_indentation(&mut diagnostics, locator, indexer);
}
@@ -98,8 +86,8 @@ pub(crate) fn check_tokens(
Rule::InvalidCharacterNul,
Rule::InvalidCharacterZeroWidthSpace,
]) {
for (tok, range) in tokens.iter().flatten() {
pylint::rules::invalid_string_characters(&mut diagnostics, tok, *range, locator);
for (token, range) in tokens.kinds() {
pylint::rules::invalid_string_characters(&mut diagnostics, token, range, locator);
}
}
@@ -110,7 +98,7 @@ pub(crate) fn check_tokens(
]) {
pycodestyle::rules::compound_statements(
&mut diagnostics,
tokens,
tokens.kinds(),
locator,
indexer,
source_type,
@@ -124,7 +112,7 @@ pub(crate) fn check_tokens(
]) {
flake8_implicit_str_concat::rules::implicit(
&mut diagnostics,
tokens,
tokens.kinds(),
settings,
locator,
indexer,
@@ -136,11 +124,11 @@ pub(crate) fn check_tokens(
Rule::TrailingCommaOnBareTuple,
Rule::ProhibitedTrailingComma,
]) {
flake8_commas::rules::trailing_commas(&mut diagnostics, tokens, locator, indexer);
flake8_commas::rules::trailing_commas(&mut diagnostics, tokens.kinds(), locator, indexer);
}
if settings.rules.enabled(Rule::ExtraneousParentheses) {
pyupgrade::rules::extraneous_parentheses(&mut diagnostics, tokens, locator);
pyupgrade::rules::extraneous_parentheses(&mut diagnostics, tokens.kinds(), locator);
}
if source_type.is_stub() && settings.rules.enabled(Rule::TypeCommentInStub) {
@@ -184,7 +172,7 @@ pub(crate) fn check_tokens(
}
if settings.rules.enabled(Rule::TooManyNewlinesAtEndOfFile) {
pycodestyle::rules::too_many_newlines_at_end_of_file(&mut diagnostics, tokens);
pycodestyle::rules::too_many_newlines_at_end_of_file(&mut diagnostics, tokens.kinds());
}
diagnostics.retain(|diagnostic| settings.rules.enabled(diagnostic.kind.rule()));

View File

@@ -131,7 +131,7 @@ fn extract_noqa_line_for(lxr: &[LexResult], locator: &Locator, indexer: &Indexer
// For multi-line strings, we expect `noqa` directives on the last line of the
// string.
Tok::String { kind, .. } if kind.is_triple_quoted() => {
Tok::String { flags, .. } if flags.is_triple_quoted() => {
if locator.contains_line_break(*range) {
string_mappings.push(TextRange::new(
locator.line_start(range.start()),

View File

@@ -4,27 +4,26 @@
use std::iter::FusedIterator;
use ruff_python_ast::{self as ast, Stmt, Suite};
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_python_parser::{TokenKind, TokenKindIter};
use ruff_text_size::{Ranged, TextSize};
use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor};
use ruff_source_file::{Locator, UniversalNewlineIterator};
/// Extract doc lines (standalone comments) from a token sequence.
pub(crate) fn doc_lines_from_tokens(lxr: &[LexResult]) -> DocLines {
DocLines::new(lxr)
pub(crate) fn doc_lines_from_tokens(tokens: TokenKindIter) -> DocLines {
DocLines::new(tokens)
}
pub(crate) struct DocLines<'a> {
inner: std::iter::Flatten<core::slice::Iter<'a, LexResult>>,
inner: TokenKindIter<'a>,
prev: TextSize,
}
impl<'a> DocLines<'a> {
fn new(lxr: &'a [LexResult]) -> Self {
fn new(tokens: TokenKindIter<'a>) -> Self {
Self {
inner: lxr.iter().flatten(),
inner: tokens,
prev: TextSize::default(),
}
}
@@ -39,15 +38,15 @@ impl Iterator for DocLines<'_> {
let (tok, range) = self.inner.next()?;
match tok {
Tok::Comment(..) => {
TokenKind::Comment => {
if at_start_of_line {
break Some(range.start());
}
}
Tok::Newline | Tok::NonLogicalNewline => {
TokenKind::Newline | TokenKind::NonLogicalNewline => {
at_start_of_line = true;
}
Tok::Indent | Tok::Dedent => {
TokenKind::Indent | TokenKind::Dedent => {
// ignore
}
_ => {

View File

@@ -1,10 +1,12 @@
//! Interface for generating fix edits from higher-level actions (e.g., "remove an argument").
use std::borrow::Cow;
use anyhow::{Context, Result};
use ruff_diagnostics::Edit;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Stmt};
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, ExprList, Stmt};
use ruff_python_ast::{AnyNodeRef, ArgOrKeyword};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
@@ -124,7 +126,7 @@ pub(crate) fn remove_unused_imports<'a>(
/// Edits to make the specified imports explicit, e.g. change `import x` to `import x as x`.
pub(crate) fn make_redundant_alias<'a>(
member_names: impl Iterator<Item = &'a str>,
member_names: impl Iterator<Item = Cow<'a, str>>,
stmt: &Stmt,
) -> Vec<Edit> {
let aliases = match stmt {
@@ -144,6 +146,53 @@ pub(crate) fn make_redundant_alias<'a>(
.collect()
}
/// Fix to add the specified imports to the `__all__` export list.
pub(crate) fn add_to_dunder_all<'a>(
names: impl Iterator<Item = &'a str>,
expr: &Expr,
stylist: &Stylist,
) -> Vec<Edit> {
let (insertion_point, export_prefix_length) = match expr {
Expr::List(ExprList { elts, range, .. }) => (
elts.last()
.map_or(range.end() - "]".text_len(), Ranged::end),
elts.len(),
),
Expr::Tuple(tup) if tup.parenthesized => (
tup.elts
.last()
.map_or(tup.end() - ")".text_len(), Ranged::end),
tup.elts.len(),
),
Expr::Tuple(tup) if !tup.parenthesized => (
tup.elts
.last()
.expect("unparenthesized empty tuple is not possible")
.range()
.end(),
tup.elts.len(),
),
_ => {
// we don't know how to insert into this expression
return vec![];
}
};
let quote = stylist.quote();
let mut edits: Vec<_> = names
.enumerate()
.map(|(offset, name)| match export_prefix_length + offset {
0 => Edit::insertion(format!("{quote}{name}{quote}"), insertion_point),
_ => Edit::insertion(format!(", {quote}{name}{quote}"), insertion_point),
})
.collect();
if let Expr::Tuple(tup) = expr {
if tup.parenthesized && export_prefix_length + edits.len() == 1 {
edits.push(Edit::insertion(",".to_string(), insertion_point));
}
}
edits
}
#[derive(Debug, Copy, Clone)]
pub(crate) enum Parentheses {
/// Remove parentheses, if the removed argument is the only argument left.
@@ -477,14 +526,20 @@ fn all_lines_fit(
#[cfg(test)]
mod tests {
use anyhow::Result;
use anyhow::{anyhow, Result};
use std::borrow::Cow;
use test_case::test_case;
use ruff_diagnostics::Edit;
use ruff_python_parser::parse_suite;
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_python_codegen::Stylist;
use ruff_python_parser::{lexer, parse_expression, parse_suite, Mode};
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::fix::edits::{make_redundant_alias, next_stmt_break, trailing_semicolon};
use crate::fix::apply_fixes;
use crate::fix::edits::{
add_to_dunder_all, make_redundant_alias, next_stmt_break, trailing_semicolon,
};
#[test]
fn find_semicolon() -> Result<()> {
@@ -562,7 +617,7 @@ x = 1 \
let program = parse_suite(contents).unwrap();
let stmt = program.first().unwrap();
assert_eq!(
make_redundant_alias(["x"].into_iter(), stmt),
make_redundant_alias(["x"].into_iter().map(Cow::from), stmt),
vec![Edit::range_replacement(
String::from("x as x"),
TextRange::new(TextSize::new(7), TextSize::new(8)),
@@ -570,7 +625,7 @@ x = 1 \
"make just one item redundant"
);
assert_eq!(
make_redundant_alias(vec!["x", "y"].into_iter(), stmt),
make_redundant_alias(vec!["x", "y"].into_iter().map(Cow::from), stmt),
vec![Edit::range_replacement(
String::from("x as x"),
TextRange::new(TextSize::new(7), TextSize::new(8)),
@@ -578,7 +633,7 @@ x = 1 \
"the second item is already a redundant alias"
);
assert_eq!(
make_redundant_alias(vec!["x", "z"].into_iter(), stmt),
make_redundant_alias(vec!["x", "z"].into_iter().map(Cow::from), stmt),
vec![Edit::range_replacement(
String::from("x as x"),
TextRange::new(TextSize::new(7), TextSize::new(8)),
@@ -586,4 +641,47 @@ x = 1 \
"the third item is already aliased to something else"
);
}
#[test_case("()", &["x", "y"], r#"("x", "y")"# ; "2 into empty tuple")]
#[test_case("()", &["x"], r#"("x",)"# ; "1 into empty tuple adding a trailing comma")]
#[test_case("[]", &["x", "y"], r#"["x", "y"]"# ; "2 into empty list")]
#[test_case("[]", &["x"], r#"["x"]"# ; "1 into empty list")]
#[test_case(r#""a", "b""#, &["x", "y"], r#""a", "b", "x", "y""# ; "2 into unparenthesized tuple")]
#[test_case(r#""a", "b""#, &["x"], r#""a", "b", "x""# ; "1 into unparenthesized tuple")]
#[test_case(r#""a", "b","#, &["x", "y"], r#""a", "b", "x", "y","# ; "2 into unparenthesized tuple w/trailing comma")]
#[test_case(r#""a", "b","#, &["x"], r#""a", "b", "x","# ; "1 into unparenthesized tuple w/trailing comma")]
#[test_case(r#"("a", "b")"#, &["x", "y"], r#"("a", "b", "x", "y")"# ; "2 into nonempty tuple")]
#[test_case(r#"("a", "b")"#, &["x"], r#"("a", "b", "x")"# ; "1 into nonempty tuple")]
#[test_case(r#"("a", "b",)"#, &["x", "y"], r#"("a", "b", "x", "y",)"# ; "2 into nonempty tuple w/trailing comma")]
#[test_case(r#"("a", "b",)"#, &["x"], r#"("a", "b", "x",)"# ; "1 into nonempty tuple w/trailing comma")]
#[test_case(r#"["a", "b",]"#, &["x", "y"], r#"["a", "b", "x", "y",]"# ; "2 into nonempty list w/trailing comma")]
#[test_case(r#"["a", "b",]"#, &["x"], r#"["a", "b", "x",]"# ; "1 into nonempty list w/trailing comma")]
#[test_case(r#"["a", "b"]"#, &["x", "y"], r#"["a", "b", "x", "y"]"# ; "2 into nonempty list")]
#[test_case(r#"["a", "b"]"#, &["x"], r#"["a", "b", "x"]"# ; "1 into nonempty list")]
fn add_to_dunder_all_test(raw: &str, names: &[&str], expect: &str) -> Result<()> {
let locator = Locator::new(raw);
let edits = {
let expr = parse_expression(raw)?;
let stylist = Stylist::from_tokens(
&lexer::lex(raw, Mode::Expression).collect::<Vec<_>>(),
&locator,
);
// SUT
add_to_dunder_all(names.iter().copied(), &expr, &stylist)
};
let diag = {
use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile;
let mut iter = edits.into_iter();
Diagnostic::new(
MissingNewlineAtEndOfFile, // The choice of rule here is arbitrary.
TextRange::default(),
)
.with_fix(Fix::safe_edits(
iter.next().ok_or(anyhow!("expected edits nonempty"))?,
iter,
))
};
assert_eq!(apply_fixes([diag].iter(), &locator).code, expect);
Ok(())
}
}

View File

@@ -321,7 +321,6 @@ mod tests {
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::{parse_suite, Mode};
use ruff_source_file::{LineEnding, Locator};
use ruff_text_size::TextSize;
@@ -332,7 +331,7 @@ mod tests {
fn start_of_file() -> Result<()> {
fn insert(contents: &str) -> Result<Insertion> {
let program = parse_suite(contents)?;
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, Mode::Module);
let tokens = ruff_python_parser::tokenize(contents, Mode::Module);
let locator = Locator::new(contents);
let stylist = Stylist::from_tokens(&tokens, &locator);
Ok(Insertion::start_of_file(&program, &locator, &stylist))
@@ -443,7 +442,7 @@ x = 1
#[test]
fn start_of_block() {
fn insert(contents: &str, offset: TextSize) -> Insertion {
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, Mode::Module);
let tokens = ruff_python_parser::tokenize(contents, Mode::Module);
let locator = Locator::new(contents);
let stylist = Stylist::from_tokens(&tokens, &locator);
Insertion::start_of_block(offset, &locator, &stylist, PySourceType::default())

View File

@@ -5,6 +5,7 @@
//!
//! [Ruff]: https://github.com/astral-sh/ruff
pub use noqa::generate_noqa_edits;
#[cfg(feature = "clap")]
pub use registry::clap_completion::RuleParser;
#[cfg(feature = "clap")]

View File

@@ -14,7 +14,7 @@ use ruff_python_ast::{PySourceType, Suite};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::{AsMode, ParseError};
use ruff_python_parser::{AsMode, ParseError, TokenKindIter, Tokens};
use ruff_source_file::{Locator, SourceFileBuilder};
use ruff_text_size::Ranged;
@@ -93,7 +93,7 @@ pub fn check_path(
let use_doc_lines = settings.rules.enabled(Rule::DocLineTooLong);
let mut doc_lines = vec![];
if use_doc_lines {
doc_lines.extend(doc_lines_from_tokens(&tokens));
doc_lines.extend(doc_lines_from_tokens(tokens.kinds()));
}
// Run the token-based rules.
@@ -353,7 +353,7 @@ pub fn add_noqa_to_path(
let contents = source_kind.source_code();
// Tokenize once.
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, source_type.as_mode());
let tokens = ruff_python_parser::tokenize(contents, source_type.as_mode());
// Map row and column locations to byte slices (lazily).
let locator = Locator::new(contents);
@@ -518,8 +518,7 @@ pub fn lint_fix<'a>(
// Continuously fix until the source code stabilizes.
loop {
// Tokenize once.
let tokens: Vec<LexResult> =
ruff_python_parser::tokenize(transformed.source_code(), source_type.as_mode());
let tokens = ruff_python_parser::tokenize(transformed.source_code(), source_type.as_mode());
// Map row and column locations to byte slices (lazily).
let locator = Locator::new(transformed.source_code());
@@ -715,7 +714,7 @@ impl<'a> ParseSource<'a> {
#[derive(Debug, Clone)]
pub enum TokenSource<'a> {
/// Use the precomputed tokens to generate the AST.
Tokens(Vec<LexResult>),
Tokens(Tokens),
/// Use the precomputed tokens and AST.
Precomputed {
tokens: &'a [LexResult],
@@ -723,6 +722,18 @@ pub enum TokenSource<'a> {
},
}
impl TokenSource<'_> {
/// Returns an iterator over the [`TokenKind`] and the corresponding range.
///
/// [`TokenKind`]: ruff_python_parser::TokenKind
pub fn kinds(&self) -> TokenKindIter {
match self {
TokenSource::Tokens(tokens) => tokens.kinds(),
TokenSource::Precomputed { tokens, .. } => TokenKindIter::new(tokens),
}
}
}
impl Deref for TokenSource<'_> {
type Target = [LexResult];

View File

@@ -1,6 +1,6 @@
use std::collections::BTreeMap;
use std::error::Error;
use std::fmt::{Display, Write};
use std::fmt::Display;
use std::fs;
use std::ops::Add;
use std::path::Path;
@@ -10,7 +10,7 @@ use itertools::Itertools;
use log::warn;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::{Diagnostic, Edit};
use ruff_python_trivia::{indentation_at_offset, CommentRanges};
use ruff_source_file::{LineEnding, Locator};
@@ -19,6 +19,27 @@ use crate::fs::relativize_path;
use crate::registry::{AsRule, Rule, RuleSet};
use crate::rule_redirects::get_redirect_target;
/// Generates an array of edits that matches the length of `diagnostics`.
/// Each potential edit in the array is paired, in order, with the associated diagnostic.
/// Each edit will add a `noqa` comment to the appropriate line in the source to hide
/// the diagnostic. These edits may conflict with each other and should not be applied
/// simultaneously.
pub fn generate_noqa_edits(
path: &Path,
diagnostics: &[Diagnostic],
locator: &Locator,
comment_ranges: &CommentRanges,
external: &[String],
noqa_line_for: &NoqaMapping,
line_ending: LineEnding,
) -> Vec<Option<Edit>> {
let exemption =
FileExemption::try_extract(locator.contents(), comment_ranges, external, path, locator);
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, path, locator);
let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for);
build_noqa_edits_by_diagnostic(comments, locator, line_ending)
}
/// A directive to ignore a set of rules for a given line of Python source code (e.g.,
/// `# noqa: F401, F841`).
#[derive(Debug)]
@@ -511,6 +532,7 @@ pub(crate) fn add_noqa(
noqa_line_for,
line_ending,
);
fs::write(path, output)?;
Ok(count)
}
@@ -524,9 +546,7 @@ fn add_noqa_inner(
noqa_line_for: &NoqaMapping,
line_ending: LineEnding,
) -> (usize, String) {
// Map of line start offset to set of (non-ignored) diagnostic codes that are triggered on that line.
let mut matches_by_line: BTreeMap<TextSize, (RuleSet, Option<&Directive>)> =
BTreeMap::default();
let mut count = 0;
// Whether the file is exempted from all checks.
// Codes that are globally exempted (within the current file).
@@ -534,16 +554,117 @@ fn add_noqa_inner(
FileExemption::try_extract(locator.contents(), comment_ranges, external, path, locator);
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, path, locator);
let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for);
let edits = build_noqa_edits_by_line(comments, locator, line_ending);
let contents = locator.contents();
let mut output = String::with_capacity(contents.len());
let mut last_append = TextSize::default();
for (_, edit) in edits {
output.push_str(&contents[TextRange::new(last_append, edit.start())]);
edit.write(&mut output);
count += 1;
last_append = edit.end();
}
output.push_str(&contents[TextRange::new(last_append, TextSize::of(contents))]);
(count, output)
}
fn build_noqa_edits_by_diagnostic(
comments: Vec<Option<NoqaComment>>,
locator: &Locator,
line_ending: LineEnding,
) -> Vec<Option<Edit>> {
let mut edits = Vec::default();
for comment in comments {
match comment {
Some(comment) => {
if let Some(noqa_edit) = generate_noqa_edit(
comment.directive,
comment.line,
RuleSet::from_rule(comment.diagnostic.kind.rule()),
locator,
line_ending,
) {
edits.push(Some(noqa_edit.into_edit()));
}
}
None => edits.push(None),
}
}
edits
}
fn build_noqa_edits_by_line<'a>(
comments: Vec<Option<NoqaComment<'a>>>,
locator: &Locator,
line_ending: LineEnding,
) -> BTreeMap<TextSize, NoqaEdit<'a>> {
let mut comments_by_line = BTreeMap::default();
for comment in comments.into_iter().flatten() {
comments_by_line
.entry(comment.line)
.or_insert_with(Vec::default)
.push(comment);
}
let mut edits = BTreeMap::default();
for (offset, matches) in comments_by_line {
let Some(first_match) = matches.first() else {
continue;
};
let directive = first_match.directive;
if let Some(edit) = generate_noqa_edit(
directive,
offset,
matches
.into_iter()
.map(|NoqaComment { diagnostic, .. }| diagnostic.kind.rule())
.collect(),
locator,
line_ending,
) {
edits.insert(offset, edit);
}
}
edits
}
struct NoqaComment<'a> {
line: TextSize,
diagnostic: &'a Diagnostic,
directive: Option<&'a Directive<'a>>,
}
fn find_noqa_comments<'a>(
diagnostics: &'a [Diagnostic],
locator: &'a Locator,
exemption: &Option<FileExemption>,
directives: &'a NoqaDirectives,
noqa_line_for: &NoqaMapping,
) -> Vec<Option<NoqaComment<'a>>> {
// List of noqa comments, ordered to match up with `diagnostics`
let mut comments_by_line: Vec<Option<NoqaComment<'a>>> = vec![];
// Mark any non-ignored diagnostics.
for diagnostic in diagnostics {
match &exemption {
Some(FileExemption::All) => {
// If the file is exempted, don't add any noqa directives.
comments_by_line.push(None);
continue;
}
Some(FileExemption::Codes(codes)) => {
// If the diagnostic is ignored by a global exemption, don't add a noqa directive.
if codes.contains(&diagnostic.kind.rule().noqa_code()) {
comments_by_line.push(None);
continue;
}
}
@@ -557,10 +678,12 @@ fn add_noqa_inner(
{
match &directive_line.directive {
Directive::All(_) => {
comments_by_line.push(None);
continue;
}
Directive::Codes(codes) => {
if codes.includes(diagnostic.kind.rule()) {
comments_by_line.push(None);
continue;
}
}
@@ -574,18 +697,17 @@ fn add_noqa_inner(
if let Some(directive_line) = directives.find_line_with_directive(noqa_offset) {
match &directive_line.directive {
Directive::All(_) => {
comments_by_line.push(None);
continue;
}
Directive::Codes(codes) => {
directive @ Directive::Codes(codes) => {
let rule = diagnostic.kind.rule();
if !codes.includes(rule) {
matches_by_line
.entry(directive_line.start())
.or_insert_with(|| {
(RuleSet::default(), Some(&directive_line.directive))
})
.0
.insert(rule);
comments_by_line.push(Some(NoqaComment {
line: directive_line.start(),
diagnostic,
directive: Some(directive),
}));
}
continue;
}
@@ -593,87 +715,106 @@ fn add_noqa_inner(
}
// There's no existing noqa directive that suppresses the diagnostic.
matches_by_line
.entry(locator.line_start(noqa_offset))
.or_insert_with(|| (RuleSet::default(), None))
.0
.insert(diagnostic.kind.rule());
comments_by_line.push(Some(NoqaComment {
line: locator.line_start(noqa_offset),
diagnostic,
directive: None,
}));
}
let mut count = 0;
let mut output = String::with_capacity(locator.len());
let mut prev_end = TextSize::default();
comments_by_line
}
for (offset, (rules, directive)) in matches_by_line {
output.push_str(locator.slice(TextRange::new(prev_end, offset)));
struct NoqaEdit<'a> {
edit_range: TextRange,
rules: RuleSet,
codes: Option<&'a Codes<'a>>,
line_ending: LineEnding,
}
let line = locator.full_line(offset);
impl<'a> NoqaEdit<'a> {
fn into_edit(self) -> Edit {
let mut edit_content = String::new();
self.write(&mut edit_content);
match directive {
None => {
// Add existing content.
output.push_str(line.trim_end());
Edit::range_replacement(edit_content, self.edit_range)
}
// Add `noqa` directive.
output.push_str(" # noqa: ");
// Add codes.
push_codes(&mut output, rules.iter().map(|rule| rule.noqa_code()));
output.push_str(&line_ending);
count += 1;
}
Some(Directive::All(_)) => {
// Does not get inserted into the map.
}
Some(Directive::Codes(codes)) => {
// Reconstruct the line based on the preserved rule codes.
// This enables us to tally the number of edits.
let output_start = output.len();
// Add existing content.
output.push_str(
locator
.slice(TextRange::new(offset, codes.start()))
.trim_end(),
);
// Add `noqa` directive.
output.push_str(" # noqa: ");
// Add codes.
fn write(&self, writer: &mut impl std::fmt::Write) {
write!(writer, " # noqa: ").unwrap();
match self.codes {
Some(codes) => {
push_codes(
&mut output,
rules
writer,
self.rules
.iter()
.map(|rule| rule.noqa_code().to_string())
.chain(codes.iter().map(ToString::to_string))
.sorted_unstable(),
);
// Only count if the new line is an actual edit.
if &output[output_start..] != line.trim_end() {
count += 1;
}
output.push_str(&line_ending);
}
None => {
push_codes(
writer,
self.rules.iter().map(|rule| rule.noqa_code().to_string()),
);
}
}
prev_end = offset + line.text_len();
write!(writer, "{}", self.line_ending.as_str()).unwrap();
}
output.push_str(locator.after(prev_end));
(count, output)
}
fn push_codes<I: Display>(str: &mut String, codes: impl Iterator<Item = I>) {
impl<'a> Ranged for NoqaEdit<'a> {
fn range(&self) -> TextRange {
self.edit_range
}
}
fn generate_noqa_edit<'a>(
directive: Option<&'a Directive>,
offset: TextSize,
rules: RuleSet,
locator: &Locator,
line_ending: LineEnding,
) -> Option<NoqaEdit<'a>> {
let line_range = locator.full_line_range(offset);
let edit_range;
let codes;
// Add codes.
match directive {
None => {
let trimmed_line = locator.slice(line_range).trim_end();
edit_range = TextRange::new(TextSize::of(trimmed_line), line_range.len()) + offset;
codes = None;
}
Some(Directive::Codes(existing_codes)) => {
// find trimmed line without the noqa
let trimmed_line = locator
.slice(TextRange::new(line_range.start(), existing_codes.start()))
.trim_end();
edit_range = TextRange::new(TextSize::of(trimmed_line), line_range.len()) + offset;
codes = Some(existing_codes);
}
Some(Directive::All(_)) => return None,
};
Some(NoqaEdit {
edit_range,
rules,
codes,
line_ending,
})
}
fn push_codes<I: Display>(writer: &mut dyn std::fmt::Write, codes: impl Iterator<Item = I>) {
let mut first = true;
for code in codes {
if !first {
str.push_str(", ");
write!(writer, ", ").unwrap();
}
write!(str, "{code}").unwrap();
write!(writer, "{code}").unwrap();
first = false;
}
}
@@ -846,13 +987,15 @@ mod tests {
use insta::assert_debug_snapshot;
use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::{Diagnostic, Edit};
use ruff_python_trivia::CommentRanges;
use ruff_source_file::{LineEnding, Locator};
use crate::generate_noqa_edits;
use crate::noqa::{add_noqa_inner, Directive, NoqaMapping, ParsedFileExemption};
use crate::rules::pycodestyle::rules::AmbiguousVariableName;
use crate::rules::pyflakes::rules::UnusedVariable;
use crate::rules::pyupgrade::rules::PrintfStringFormatting;
#[test]
fn noqa_all() {
@@ -1130,4 +1273,41 @@ mod tests {
assert_eq!(count, 0);
assert_eq!(output, "x = 1 # noqa");
}
#[test]
fn multiline_comment() {
let path = Path::new("/tmp/foo.txt");
let source = r#"
print(
"""First line
second line
third line
%s"""
% name
)
"#;
let noqa_line_for = [TextRange::new(8.into(), 68.into())].into_iter().collect();
let diagnostics = [Diagnostic::new(
PrintfStringFormatting,
TextRange::new(12.into(), 79.into()),
)];
let comment_ranges = CommentRanges::default();
let edits = generate_noqa_edits(
path,
&diagnostics,
&Locator::new(source),
&comment_ranges,
&[],
&noqa_line_for,
LineEnding::Lf,
);
assert_eq!(
edits,
vec![Some(Edit::replacement(
" # noqa: UP031\n".to_string(),
68.into(),
69.into()
))]
);
}
}

View File

@@ -270,7 +270,6 @@ impl Rule {
| Rule::InvalidCharacterNul
| Rule::InvalidCharacterSub
| Rule::InvalidCharacterZeroWidthSpace
| Rule::InvalidEscapeSequence
| Rule::InvalidTodoCapitalization
| Rule::InvalidTodoTag
| Rule::LineContainsFixme

View File

@@ -57,7 +57,7 @@ pub(crate) fn hardcoded_bind_all_interfaces(checker: &mut Checker, string: Strin
}
}
ast::FStringPart::FString(f_string) => {
for literal in f_string.literals() {
for literal in f_string.elements.literals() {
if &**literal == "0.0.0.0" {
checker.diagnostics.push(Diagnostic::new(
HardcodedBindAllInterfaces,

View File

@@ -64,7 +64,7 @@ pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, string: StringLike)
check(checker, literal, literal.range());
}
ast::FStringPart::FString(f_string) => {
for literal in f_string.literals() {
for literal in f_string.elements.literals() {
check(checker, literal, literal.range());
}
}

View File

@@ -13,6 +13,7 @@ mod tests {
use crate::assert_messages;
use crate::registry::Rule;
use crate::settings::types::PreviewMode;
use crate::settings::LinterSettings;
use crate::test::test_path;
@@ -62,6 +63,7 @@ mod tests {
#[test_case(Rule::UselessContextlibSuppress, Path::new("B022.py"))]
#[test_case(Rule::UselessExpression, Path::new("B018.ipynb"))]
#[test_case(Rule::UselessExpression, Path::new("B018.py"))]
#[test_case(Rule::UselessExpression, Path::new("B018_attribute_docstring.py"))]
#[test_case(Rule::LoopIteratorMutation, Path::new("B909.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
@@ -73,6 +75,26 @@ mod tests {
Ok(())
}
#[test_case(Rule::UselessExpression, Path::new("B018.ipynb"))]
#[test_case(Rule::UselessExpression, Path::new("B018.py"))]
#[test_case(Rule::UselessExpression, Path::new("B018_attribute_docstring.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_bugbear").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Enabled,
..LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test]
fn zip_without_explicit_strict() -> Result<()> {
let snapshot = "B905.py";

View File

@@ -16,6 +16,9 @@ use super::super::helpers::at_last_top_level_expression_in_cell;
/// by mistake. Assign a useless expression to a variable, or remove it
/// entirely.
///
/// In [preview mode], this rule will also flag string literals and f-strings that
/// are not used as a docstring or an attribute docstring.
///
/// ## Example
/// ```python
/// 1 + 1
@@ -45,6 +48,8 @@ use super::super::helpers::at_last_top_level_expression_in_cell;
/// with errors.ExceptionRaisedContext():
/// _ = obj.attribute
/// ```
///
/// [preview]: https://docs.astral.sh/ruff/preview/
#[violation]
pub struct UselessExpression {
kind: Kind,
@@ -69,15 +74,16 @@ impl Violation for UselessExpression {
/// B018
pub(crate) fn useless_expression(checker: &mut Checker, value: &Expr) {
// Ignore comparisons, as they're handled by `useless_comparison`.
if value.is_compare_expr() {
if matches!(value, Expr::Compare(_) | Expr::EllipsisLiteral(_)) {
return;
}
// Ignore strings, to avoid false positives with docstrings.
if matches!(
value,
Expr::FString(_) | Expr::StringLiteral(_) | Expr::EllipsisLiteral(_)
) {
if checker.settings.preview.is_enabled() {
if checker.semantic().in_pep_257_docstring() || checker.semantic().in_attribute_docstring()
{
return;
}
} else if matches!(value, Expr::StringLiteral(_) | Expr::FString(_)) {
return;
}

View File

@@ -3,8 +3,8 @@ source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B018.py:11:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
9 | "str" # Str (no raise)
10 | f"{int}" # JoinedStr (no raise)
9 | "str" # StringLiteral
10 | f"{int}" # FString
11 | 1j # Number (complex)
| ^^ B018
12 | 1 # Number (int)
@@ -13,7 +13,7 @@ B018.py:11:5: B018 Found useless expression. Either assign it to a variable or r
B018.py:12:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
10 | f"{int}" # JoinedStr (no raise)
10 | f"{int}" # FString
11 | 1j # Number (complex)
12 | 1 # Number (int)
| ^ B018
@@ -117,8 +117,8 @@ B018.py:27:5: B018 Found useless expression. Either assign it to a variable or r
B018.py:39:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
37 | "str" # Str (no raise)
38 | f"{int}" # JoinedStr (no raise)
37 | "str" # StringLiteral
38 | f"{int}" # FString
39 | 1j # Number (complex)
| ^^ B018
40 | 1 # Number (int)
@@ -127,7 +127,7 @@ B018.py:39:5: B018 Found useless expression. Either assign it to a variable or r
B018.py:40:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
38 | f"{int}" # JoinedStr (no raise)
38 | f"{int}" # FString
39 | 1j # Number (complex)
40 | 1 # Number (int)
| ^ B018
@@ -254,5 +254,3 @@ B018.py:65:5: B018 Found useless expression. Either assign it to a variable or r
65 | "foo" + "bar" # BinOp (raise)
| ^^^^^^^^^^^^^ B018
|

View File

@@ -0,0 +1,11 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B018_attribute_docstring.py:22:2: B018 Found useless expression. Either assign it to a variable or remove it.
|
20 | "d: not a docstring"
21 |
22 | (e := 1)
| ^^^^^^ B018
23 | "e: not a docstring"
|

View File

@@ -0,0 +1,32 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B018.ipynb:5:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
3 | x
4 | # Only skip the last expression
5 | x # B018
| ^ B018
6 | x
7 | # Nested expressions isn't relevant
|
B018.ipynb:9:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
7 | # Nested expressions isn't relevant
8 | if True:
9 | x
| ^ B018
10 | # Semicolons shouldn't affect the output
11 | x;
|
B018.ipynb:13:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
11 | x;
12 | # Semicolons with multiple expressions
13 | x; x
| ^ B018
14 | # Comments, newlines and whitespace
15 | x # comment
|

View File

@@ -0,0 +1,295 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B018.py:10:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
8 | a = 2
9 | "str" # StringLiteral
10 | f"{int}" # FString
| ^^^^^^^^ B018
11 | 1j # Number (complex)
12 | 1 # Number (int)
|
B018.py:11:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
9 | "str" # StringLiteral
10 | f"{int}" # FString
11 | 1j # Number (complex)
| ^^ B018
12 | 1 # Number (int)
13 | 1.0 # Number (float)
|
B018.py:12:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
10 | f"{int}" # FString
11 | 1j # Number (complex)
12 | 1 # Number (int)
| ^ B018
13 | 1.0 # Number (float)
14 | b"foo" # Binary
|
B018.py:13:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
11 | 1j # Number (complex)
12 | 1 # Number (int)
13 | 1.0 # Number (float)
| ^^^ B018
14 | b"foo" # Binary
15 | True # NameConstant (True)
|
B018.py:14:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
12 | 1 # Number (int)
13 | 1.0 # Number (float)
14 | b"foo" # Binary
| ^^^^^^ B018
15 | True # NameConstant (True)
16 | False # NameConstant (False)
|
B018.py:15:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
13 | 1.0 # Number (float)
14 | b"foo" # Binary
15 | True # NameConstant (True)
| ^^^^ B018
16 | False # NameConstant (False)
17 | None # NameConstant (None)
|
B018.py:16:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
14 | b"foo" # Binary
15 | True # NameConstant (True)
16 | False # NameConstant (False)
| ^^^^^ B018
17 | None # NameConstant (None)
18 | [1, 2] # list
|
B018.py:17:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
15 | True # NameConstant (True)
16 | False # NameConstant (False)
17 | None # NameConstant (None)
| ^^^^ B018
18 | [1, 2] # list
19 | {1, 2} # set
|
B018.py:18:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
16 | False # NameConstant (False)
17 | None # NameConstant (None)
18 | [1, 2] # list
| ^^^^^^ B018
19 | {1, 2} # set
20 | {"foo": "bar"} # dict
|
B018.py:19:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
17 | None # NameConstant (None)
18 | [1, 2] # list
19 | {1, 2} # set
| ^^^^^^ B018
20 | {"foo": "bar"} # dict
|
B018.py:20:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
18 | [1, 2] # list
19 | {1, 2} # set
20 | {"foo": "bar"} # dict
| ^^^^^^^^^^^^^^ B018
|
B018.py:24:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
23 | class Foo3:
24 | 123
| ^^^ B018
25 | a = 2
26 | "str"
|
B018.py:27:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
25 | a = 2
26 | "str"
27 | 1
| ^ B018
|
B018.py:37:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
35 | """my docstring"""
36 | a = 2
37 | "str" # StringLiteral
| ^^^^^ B018
38 | f"{int}" # FString
39 | 1j # Number (complex)
|
B018.py:38:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
36 | a = 2
37 | "str" # StringLiteral
38 | f"{int}" # FString
| ^^^^^^^^ B018
39 | 1j # Number (complex)
40 | 1 # Number (int)
|
B018.py:39:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
37 | "str" # StringLiteral
38 | f"{int}" # FString
39 | 1j # Number (complex)
| ^^ B018
40 | 1 # Number (int)
41 | 1.0 # Number (float)
|
B018.py:40:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
38 | f"{int}" # FString
39 | 1j # Number (complex)
40 | 1 # Number (int)
| ^ B018
41 | 1.0 # Number (float)
42 | b"foo" # Binary
|
B018.py:41:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
39 | 1j # Number (complex)
40 | 1 # Number (int)
41 | 1.0 # Number (float)
| ^^^ B018
42 | b"foo" # Binary
43 | True # NameConstant (True)
|
B018.py:42:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
40 | 1 # Number (int)
41 | 1.0 # Number (float)
42 | b"foo" # Binary
| ^^^^^^ B018
43 | True # NameConstant (True)
44 | False # NameConstant (False)
|
B018.py:43:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
41 | 1.0 # Number (float)
42 | b"foo" # Binary
43 | True # NameConstant (True)
| ^^^^ B018
44 | False # NameConstant (False)
45 | None # NameConstant (None)
|
B018.py:44:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
42 | b"foo" # Binary
43 | True # NameConstant (True)
44 | False # NameConstant (False)
| ^^^^^ B018
45 | None # NameConstant (None)
46 | [1, 2] # list
|
B018.py:45:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
43 | True # NameConstant (True)
44 | False # NameConstant (False)
45 | None # NameConstant (None)
| ^^^^ B018
46 | [1, 2] # list
47 | {1, 2} # set
|
B018.py:46:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
44 | False # NameConstant (False)
45 | None # NameConstant (None)
46 | [1, 2] # list
| ^^^^^^ B018
47 | {1, 2} # set
48 | {"foo": "bar"} # dict
|
B018.py:47:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
45 | None # NameConstant (None)
46 | [1, 2] # list
47 | {1, 2} # set
| ^^^^^^ B018
48 | {"foo": "bar"} # dict
|
B018.py:48:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
46 | [1, 2] # list
47 | {1, 2} # set
48 | {"foo": "bar"} # dict
| ^^^^^^^^^^^^^^ B018
|
B018.py:52:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
51 | def foo3():
52 | 123
| ^^^ B018
53 | a = 2
54 | "str"
|
B018.py:54:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
52 | 123
53 | a = 2
54 | "str"
| ^^^^^ B018
55 | 3
|
B018.py:55:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
53 | a = 2
54 | "str"
55 | 3
| ^ B018
|
B018.py:63:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
62 | def foo5():
63 | foo.bar # Attribute (raise)
| ^^^^^^^ B018
64 | object().__class__ # Attribute (raise)
65 | "foo" + "bar" # BinOp (raise)
|
B018.py:64:5: B018 Found useless attribute access. Either assign it to a variable or remove it.
|
62 | def foo5():
63 | foo.bar # Attribute (raise)
64 | object().__class__ # Attribute (raise)
| ^^^^^^^^^^^^^^^^^^ B018
65 | "foo" + "bar" # BinOp (raise)
|
B018.py:65:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
63 | foo.bar # Attribute (raise)
64 | object().__class__ # Attribute (raise)
65 | "foo" + "bar" # BinOp (raise)
| ^^^^^^^^^^^^^ B018
|

View File

@@ -0,0 +1,165 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B018_attribute_docstring.py:10:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
8 | b = 1
9 | "b: docstring" " continue"
10 | "b: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
11 |
12 | c: int = 1
|
B018_attribute_docstring.py:20:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
18 | if True:
19 | d = 1
20 | "d: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
21 |
22 | (e := 1)
|
B018_attribute_docstring.py:22:2: B018 Found useless expression. Either assign it to a variable or remove it.
|
20 | "d: not a docstring"
21 |
22 | (e := 1)
| ^^^^^^ B018
23 | "e: not a docstring"
|
B018_attribute_docstring.py:23:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
22 | (e := 1)
23 | "e: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
24 |
25 | f = 0
|
B018_attribute_docstring.py:27:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
25 | f = 0
26 | f += 1
27 | "f: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
28 |
29 | g.h = 1
|
B018_attribute_docstring.py:30:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
29 | g.h = 1
30 | "g.h: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^ B018
31 |
32 | (i) = 1
|
B018_attribute_docstring.py:42:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
41 | l = m = 1
42 | "l m: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^ B018
43 |
44 | n.a = n.b = n.c = 1
|
B018_attribute_docstring.py:45:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
44 | n.a = n.b = n.c = 1
45 | "n.*: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^ B018
46 |
47 | (o, p) = (1, 2)
|
B018_attribute_docstring.py:48:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
47 | (o, p) = (1, 2)
48 | "o p: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^ B018
49 |
50 | [q, r] = [1, 2]
|
B018_attribute_docstring.py:51:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
50 | [q, r] = [1, 2]
51 | "q r: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^ B018
52 |
53 | *s = 1
|
B018_attribute_docstring.py:54:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
53 | *s = 1
54 | "s: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
|
B018_attribute_docstring.py:63:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
61 | b: int
62 | "Foo.b: docstring"
63 | "Foo.b: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^^^ B018
64 |
65 | c: int = 1
|
B018_attribute_docstring.py:71:9: B018 Found useless expression. Either assign it to a variable or remove it.
|
69 | # This is actually a docstring but we currently don't detect it.
70 | self.x = 1
71 | "self.x: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B018
72 |
73 | t = 2
|
B018_attribute_docstring.py:74:9: B018 Found useless expression. Either assign it to a variable or remove it.
|
73 | t = 2
74 | "t: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
75 |
76 | def random(self):
|
B018_attribute_docstring.py:78:9: B018 Found useless expression. Either assign it to a variable or remove it.
|
76 | def random(self):
77 | self.y = 2
78 | "self.y: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B018
79 |
80 | u = 2
|
B018_attribute_docstring.py:81:9: B018 Found useless expression. Either assign it to a variable or remove it.
|
80 | u = 2
81 | "u: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
82 |
83 | def add(self, y: int):
|
B018_attribute_docstring.py:89:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
87 | def function():
88 | v = 2
89 | "v: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
|
B018_attribute_docstring.py:93:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
92 | function.a = 1
93 | "function.a: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B018
|

View File

@@ -2,8 +2,7 @@ use ruff_diagnostics::{AlwaysFixableViolation, Violation};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_index::Indexer;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_python_parser::{TokenKind, TokenKindIter};
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange};
@@ -52,26 +51,26 @@ impl Token {
}
}
impl From<(&Tok, TextRange)> for Token {
fn from((tok, range): (&Tok, TextRange)) -> Self {
impl From<(TokenKind, TextRange)> for Token {
fn from((tok, range): (TokenKind, TextRange)) -> Self {
let ty = match tok {
Tok::Name { .. } => TokenType::Named,
Tok::String { .. } => TokenType::String,
Tok::Newline => TokenType::Newline,
Tok::NonLogicalNewline => TokenType::NonLogicalNewline,
Tok::Lpar => TokenType::OpeningBracket,
Tok::Rpar => TokenType::ClosingBracket,
Tok::Lsqb => TokenType::OpeningSquareBracket,
Tok::Rsqb => TokenType::ClosingBracket,
Tok::Colon => TokenType::Colon,
Tok::Comma => TokenType::Comma,
Tok::Lbrace => TokenType::OpeningCurlyBracket,
Tok::Rbrace => TokenType::ClosingBracket,
Tok::Def => TokenType::Def,
Tok::For => TokenType::For,
Tok::Lambda => TokenType::Lambda,
TokenKind::Name => TokenType::Named,
TokenKind::String => TokenType::String,
TokenKind::Newline => TokenType::Newline,
TokenKind::NonLogicalNewline => TokenType::NonLogicalNewline,
TokenKind::Lpar => TokenType::OpeningBracket,
TokenKind::Rpar => TokenType::ClosingBracket,
TokenKind::Lsqb => TokenType::OpeningSquareBracket,
TokenKind::Rsqb => TokenType::ClosingBracket,
TokenKind::Colon => TokenType::Colon,
TokenKind::Comma => TokenType::Comma,
TokenKind::Lbrace => TokenType::OpeningCurlyBracket,
TokenKind::Rbrace => TokenType::ClosingBracket,
TokenKind::Def => TokenType::Def,
TokenKind::For => TokenType::For,
TokenKind::Lambda => TokenType::Lambda,
// Import treated like a function.
Tok::Import => TokenType::Named,
TokenKind::Import => TokenType::Named,
_ => TokenType::Irrelevant,
};
#[allow(clippy::inconsistent_struct_constructor)]
@@ -227,27 +226,23 @@ impl AlwaysFixableViolation for ProhibitedTrailingComma {
/// COM812, COM818, COM819
pub(crate) fn trailing_commas(
diagnostics: &mut Vec<Diagnostic>,
tokens: &[LexResult],
tokens: TokenKindIter,
locator: &Locator,
indexer: &Indexer,
) {
let mut fstrings = 0u32;
let tokens = tokens.iter().filter_map(|result| {
let Ok((tok, tok_range)) = result else {
return None;
};
match tok {
let tokens = tokens.filter_map(|(token, tok_range)| {
match token {
// Completely ignore comments -- they just interfere with the logic.
Tok::Comment(_) => None,
TokenKind::Comment => None,
// F-strings are handled as `String` token type with the complete range
// of the outermost f-string. This means that the expression inside the
// f-string is not checked for trailing commas.
Tok::FStringStart(_) => {
TokenKind::FStringStart => {
fstrings = fstrings.saturating_add(1);
None
}
Tok::FStringEnd => {
TokenKind::FStringEnd => {
fstrings = fstrings.saturating_sub(1);
if fstrings == 0 {
indexer
@@ -260,7 +255,7 @@ pub(crate) fn trailing_commas(
}
_ => {
if fstrings == 0 {
Some(Token::from((tok, *tok_range)))
Some(Token::from((token, tok_range)))
} else {
None
}

View File

@@ -118,7 +118,11 @@ pub(crate) fn call_datetime_strptime_without_zone(checker: &mut Checker, call: &
}
}
ast::FStringPart::FString(f_string) => {
if f_string.literals().any(|literal| literal.contains("%z")) {
if f_string
.elements
.literals()
.any(|literal| literal.contains("%z"))
{
return;
}
}

View File

@@ -72,7 +72,7 @@ impl Violation for FutureRewritableTypeAnnotation {
#[derive_message_formats]
fn message(&self) -> String {
let FutureRewritableTypeAnnotation { name } = self;
format!("Missing `from __future__ import annotations`, but uses `{name}`")
format!("Add `from __future__ import annotations` to simplify `{name}`")
}
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
---
edge_case.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List`
edge_case.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List`
|
5 | def main(_: List[int]) -> None:
| ^^^^ FA100
@@ -9,12 +9,10 @@ edge_case.py:5:13: FA100 Missing `from __future__ import annotations`, but uses
7 | a_list.append("hello")
|
edge_case.py:6:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List`
edge_case.py:6:13: FA100 Add `from __future__ import annotations` to simplify `typing.List`
|
5 | def main(_: List[int]) -> None:
6 | a_list: t.List[str] = []
| ^^^^^^ FA100
7 | a_list.append("hello")
|

View File

@@ -1,12 +1,10 @@
---
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
---
from_typing_import.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List`
from_typing_import.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List`
|
4 | def main() -> None:
5 | a_list: List[str] = []
| ^^^^ FA100
6 | a_list.append("hello")
|

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
---
from_typing_import_many.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List`
from_typing_import_many.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List`
|
4 | def main() -> None:
5 | a_list: List[Optional[str]] = []
@@ -10,7 +10,7 @@ from_typing_import_many.py:5:13: FA100 Missing `from __future__ import annotatio
7 | a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {})
|
from_typing_import_many.py:5:18: FA100 Missing `from __future__ import annotations`, but uses `typing.Optional`
from_typing_import_many.py:5:18: FA100 Add `from __future__ import annotations` to simplify `typing.Optional`
|
4 | def main() -> None:
5 | a_list: List[Optional[str]] = []
@@ -18,5 +18,3 @@ from_typing_import_many.py:5:18: FA100 Missing `from __future__ import annotatio
6 | a_list.append("hello")
7 | a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {})
|

View File

@@ -1,12 +1,10 @@
---
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
---
import_typing.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List`
import_typing.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List`
|
4 | def main() -> None:
5 | a_list: typing.List[str] = []
| ^^^^^^^^^^^ FA100
6 | a_list.append("hello")
|

View File

@@ -1,12 +1,10 @@
---
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
---
import_typing_as.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List`
import_typing_as.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List`
|
4 | def main() -> None:
5 | a_list: t.List[str] = []
| ^^^^^^ FA100
6 | a_list.append("hello")
|

View File

@@ -4,10 +4,9 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::str::{leading_quote, trailing_quote};
use ruff_python_index::Indexer;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_python_parser::{TokenKind, TokenKindIter};
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange};
use ruff_text_size::TextRange;
use crate::settings::LinterSettings;
@@ -93,36 +92,34 @@ impl Violation for MultiLineImplicitStringConcatenation {
/// ISC001, ISC002
pub(crate) fn implicit(
diagnostics: &mut Vec<Diagnostic>,
tokens: &[LexResult],
tokens: TokenKindIter,
settings: &LinterSettings,
locator: &Locator,
indexer: &Indexer,
) {
for ((a_tok, a_range), (b_tok, b_range)) in tokens
.iter()
.flatten()
.filter(|(tok, _)| {
!tok.is_comment()
.filter(|(token, _)| {
*token != TokenKind::Comment
&& (settings.flake8_implicit_str_concat.allow_multiline
|| !tok.is_non_logical_newline())
|| *token != TokenKind::NonLogicalNewline)
})
.tuple_windows()
{
let (a_range, b_range) = match (a_tok, b_tok) {
(Tok::String { .. }, Tok::String { .. }) => (*a_range, *b_range),
(Tok::String { .. }, Tok::FStringStart(_)) => {
(TokenKind::String, TokenKind::String) => (a_range, b_range),
(TokenKind::String, TokenKind::FStringStart) => {
match indexer.fstring_ranges().innermost(b_range.start()) {
Some(b_range) => (*a_range, b_range),
Some(b_range) => (a_range, b_range),
None => continue,
}
}
(Tok::FStringEnd, Tok::String { .. }) => {
(TokenKind::FStringEnd, TokenKind::String) => {
match indexer.fstring_ranges().innermost(a_range.start()) {
Some(a_range) => (a_range, *b_range),
Some(a_range) => (a_range, b_range),
None => continue,
}
}
(Tok::FStringEnd, Tok::FStringStart(_)) => {
(TokenKind::FStringEnd, TokenKind::FStringStart) => {
match (
indexer.fstring_ranges().innermost(a_range.start()),
indexer.fstring_ranges().innermost(b_range.start()),

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