Compare commits

...

63 Commits

Author SHA1 Message Date
David Peter
048182635a [ty] Use full project names in good.txt 2025-09-24 09:09:47 +02:00
Alex Waygood
09f570af92 Bump mypy_primer pin (#20540) 2025-09-23 19:35:49 +00:00
Shunsuke Shibayama
722f1a7d7a [ty] fix stack overflow when comparing recursive NamedTuple types with is_disjoint_from (#20538)
## Summary

I found this bug while working on #20528.
The minimum reproducible code is:

```python
from __future__ import annotations

from typing import NamedTuple
from ty_extensions import is_disjoint_from, static_assert

class Path(NamedTuple):
    prev: Path | None
    key: str

static_assert(not is_disjoint_from(Path, Path))
```

A stack overflow occurs when a nominal instance type inherits from
`NamedTuple` and is defined recursively.
This PR fixes this bug.

## Test Plan

mdtest updated
2025-09-23 19:29:03 +02:00
ShikChen
dbc5983503 Update import path to ruff-wasm-web (#20539) 2025-09-23 16:57:26 +00:00
Dan Parizher
46decd4feb [pyupgrade] Fix UP008 to not apply when __class__ is a local variable (UP008) (#20497)
## Summary

Fixes #20491
2025-09-23 10:56:39 -04:00
Renkai Ge
bf38e69870 [ty] Rename "possibly unbound" diagnostics to "possibly missing" (#20492)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-09-23 14:26:55 +00:00
fgiacome
4ed8c65d29 [ty] Add positional-only-parameter-as-kwarg error (#20495) 2025-09-23 15:10:45 +01:00
Dan Parizher
2c916562ba [playground] Allow hover quick fixes to appear for overlapping diagnostics (#20527) 2025-09-23 15:15:31 +02:00
Pieter Cardillo Kwok
edb920b4d5 [flake8-async] Implement blocking-path-method (ASYNC240) (#20264)
## Summary
Adds a new rule to find and report use of `os.path` or `pathlib.Path` in
async functions.

Issue: #8451

## Test Plan

Using `cargo insta test`
2025-09-23 08:30:47 -04:00
Dan Parizher
346842f003 [pyflakes] Fix false positives for __annotate__ (Py3.14+) and __warningregistry__ (F821) (#20154)
## Summary

Fixes #19970

---------

Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-09-23 08:16:00 -04:00
David Peter
742f8a4ee6 [ty] Use C[T] instead of C[Unknown] for the upper bound of Self (#20479)
### Summary

This PR includes two changes, both of which are necessary to resolve
https://github.com/astral-sh/ty/issues/1196:

* For a generic class `C[T]`, we previously used `C[Unknown]` as the
upper bound of the `Self` type variable. There were two problems with
this. For one, when `Self` appeared in contravariant position, we would
materialize its upper bound to `Bottom[C[Unknown]]` (which might
simplify to `C[Never]` if `C` is covariant in `T`) when accessing
methods on `Top[C[Unknown]]`. This would result in `invalid-argument`
errors on the `self` parameter. Also, using an upper bound of
`C[Unknown]` would mean that inside methods, references to `T` would be
treated as `Unknown`. This could lead to false negatives. To fix this,
we now use `C[T]` (with a "nested" typevar) as the upper bound for
`Self` on `C[T]`.
* In order to make this work, we needed to allow assignability/subtyping
of inferable typevars to other types, since we now check assignability
of e.g. `C[int]` to `C[T]` (when checking assignability to the upper
bound of `Self`) when calling an instance-method on `C[int]` whose
`self` parameter is annotated as `self: Self` (or implicitly `Self`,
following https://github.com/astral-sh/ruff/pull/18007).

closes https://github.com/astral-sh/ty/issues/1196
closes https://github.com/astral-sh/ty/issues/1208


### Test Plan

Regression tests for both issues.
2025-09-23 14:02:25 +02:00
Matthew Mckee
fd5c48c539 [ty] Add support for inlay hints on attribute assignment (#20485) 2025-09-23 13:14:46 +02:00
justin
ef4df34652 [ty] implement auto() for StrEnum (#20524)
## Summary
see discussion here:
https://github.com/astral-sh/ty/issues/876#issuecomment-3310130167

https://docs.python.org/3/library/enum.html#enum.StrEnum

> Note Using
[auto](https://docs.python.org/3/library/enum.html#enum.auto) with
[StrEnum](https://docs.python.org/3/library/enum.html#enum.StrEnum)
results in the lower-cased member name as the value.

## Test Plan
- new mdtest
- also, added a test to assert the (already correct) behavior for
`IntEnum`

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-09-23 12:22:59 +02:00
Manuel Mendez
036f3616a1 [ty] Add PYTHONPATH to EnvVars and fix on Windows (#20490)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-09-23 08:27:05 +00:00
Matthew Mckee
68ae9c8a15 [ty] Fix class literal subtyping with object fallback (#20521)
## Summary

@ibraheemdev notes this example failed

```py
from typing import Callable

class X:
    ...

def f(callable: Callable[[], X]) -> X:
    return callable()

x = f(X)
```

Resolves https://github.com/astral-sh/ty/issues/1210

The issue was that we set the `Self` to the class type instead of the
instance type of the class.

## Test Plan

Fix tests in `is_subtype_of.md`
2025-09-22 17:26:25 -07:00
Dan Parizher
094bf70a60 [flake8-bultins] Detect class-scope builtin shadowing in decorators, default args, and attribute initializers (A003) (#20178)
## Summary
Fix #20171

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-09-22 18:12:45 -04:00
Ibraheem Ahmed
32d00cd569 update get-size2 to 0.7.0 2025-09-22 17:37:46 -04:00
David Peter
00a9e65d00 Fix 'cargo shear' runs (#20514)
## Summary

Previous error:

```
▶ cargo shear
Analyzing /home/shark/ruff

ruff_diagnostics -- crates/ruff_diagnostics/Cargo.toml:
  get-size2

ruff_index -- crates/ruff_index/Cargo.toml:
  get-size2

ruff_source_file -- crates/ruff_source_file/Cargo.toml:
  get-size2

ruff_text_size -- crates/ruff_text_size/Cargo.toml:
  get-size2

ty_ide -- crates/ty_ide/Cargo.toml:
  get-size2

ty_project -- crates/ty_project/Cargo.toml:
  get-size2


cargo-shear may have detected unused dependencies incorrectly due to its limitations.
They can be ignored by adding the crate name to the package's Cargo.toml:

[package.metadata.cargo-shear]
ignored = ["crate-name"]

or in the workspace Cargo.toml:

[workspace.metadata.cargo-shear]
ignored = ["crate-name"]
```
2025-09-22 16:04:56 +02:00
Micha Reiser
0c7cfd2a8d Update transitive dependencies (#20513) 2025-09-22 12:50:53 +02:00
renovate[bot]
61bb2a8245 Update Rust crate anyhow to v1.0.100 (#20499)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-09-22 09:51:52 +02:00
Alex Waygood
f1aacd0f2c [ty] The runtime object typing.Protocol is an instance of _ProtocolMeta (#20488)
## Summary

Fixes https://github.com/astral-sh/ty/issues/1218.

This bug doesn't currently cause us any real-world issues, because we
don't yet understand the signatures typeshed gives us for `isinstance()`
and `issubclass()` (typeshed's annotations there use PEP-613 type
aliases). #20107 demonstrates that this will start causing us issues as
soon as we add support for PEP-613 aliases, however, so it makes sense
to fix it now.

## Test Plan

Added mdtests
2025-09-22 08:29:03 +01:00
renovate[bot]
3033d1e5a5 Update Rust crate wasm-bindgen-test to v0.3.53 (#20506)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[wasm-bindgen-test](https://redirect.github.com/wasm-bindgen/wasm-bindgen)
| workspace.dependencies | patch | `0.3.51` -> `0.3.53` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 09:06:31 +02:00
renovate[bot]
d96d40ef42 Update Rust crate toml to v0.9.7 (#20505)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [toml](https://redirect.github.com/toml-rs/toml) |
workspace.dependencies | patch | `0.9.5` -> `0.9.7` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>toml-rs/toml (toml)</summary>

###
[`v0.9.7`](https://redirect.github.com/toml-rs/toml/compare/toml-v0.9.6...toml-v0.9.7)

[Compare
Source](https://redirect.github.com/toml-rs/toml/compare/toml-v0.9.6...toml-v0.9.7)

###
[`v0.9.6`](https://redirect.github.com/toml-rs/toml/compare/toml-v0.9.5...toml-v0.9.6)

[Compare
Source](https://redirect.github.com/toml-rs/toml/compare/toml-v0.9.5...toml-v0.9.6)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [x] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 09:04:58 +02:00
renovate[bot]
79224cc53d Update Rust crate clap to v4.5.48 (#20500)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [clap](https://redirect.github.com/clap-rs/clap) |
workspace.dependencies | patch | `4.5.47` -> `4.5.48` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>clap-rs/clap (clap)</summary>

###
[`v4.5.48`](https://redirect.github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#4548---2025-09-19)

[Compare
Source](https://redirect.github.com/clap-rs/clap/compare/v4.5.47...v4.5.48)

##### Documentation

- Add a new CLI Concepts document as another way of framing clap
- Expand the `typed_derive` cookbook entry

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 09:03:53 +02:00
renovate[bot]
fe01a5e032 Update Rust crate serde to v1.0.226 (#20503)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde](https://serde.rs)
([source](https://redirect.github.com/serde-rs/serde)) |
workspace.dependencies | patch | `1.0.223` -> `1.0.226` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>serde-rs/serde (serde)</summary>

###
[`v1.0.226`](https://redirect.github.com/serde-rs/serde/releases/tag/v1.0.226)

[Compare
Source](https://redirect.github.com/serde-rs/serde/compare/v1.0.225...v1.0.226)

- Deduplicate variant matching logic inside generated Deserialize impl
for adjacently tagged enums
([#&#8203;2935](https://redirect.github.com/serde-rs/serde/issues/2935),
thanks [@&#8203;Mingun](https://redirect.github.com/Mingun))

###
[`v1.0.225`](https://redirect.github.com/serde-rs/serde/releases/tag/v1.0.225)

[Compare
Source](https://redirect.github.com/serde-rs/serde/compare/v1.0.224...v1.0.225)

- Avoid triggering a deprecation warning in derived Serialize and
Deserialize impls for a data structure that contains its own
deprecations
([#&#8203;2879](https://redirect.github.com/serde-rs/serde/issues/2879),
thanks [@&#8203;rcrisanti](https://redirect.github.com/rcrisanti))

###
[`v1.0.224`](https://redirect.github.com/serde-rs/serde/releases/tag/v1.0.224)

[Compare
Source](https://redirect.github.com/serde-rs/serde/compare/v1.0.223...v1.0.224)

- Remove private types being suggested in rustc diagnostics
([#&#8203;2979](https://redirect.github.com/serde-rs/serde/issues/2979))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 09:03:28 +02:00
renovate[bot]
740425d39d Update Rust crate serde_with to v3.14.1 (#20504)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde_with](https://redirect.github.com/jonasbb/serde_with) |
workspace.dependencies | patch | `3.14.0` -> `3.14.1` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>jonasbb/serde_with (serde_with)</summary>

###
[`v3.14.1`](https://redirect.github.com/jonasbb/serde_with/releases/tag/v3.14.1):
serde_with v3.14.1

[Compare
Source](https://redirect.github.com/jonasbb/serde_with/compare/v3.14.0...v3.14.1)

##### Fixed

- Show macro expansion in the docs.rs generated rustdoc.
Since macros are used to generate trait implementations, this is useful
to understand the exact generated code.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 09:03:10 +02:00
renovate[bot]
4fddd373aa Update Rust crate ordermap to v0.5.12 (#20502)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [ordermap](https://redirect.github.com/indexmap-rs/ordermap) |
workspace.dependencies | patch | `0.5.10` -> `0.5.12` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>indexmap-rs/ordermap (ordermap)</summary>

###
[`v0.5.12`](https://redirect.github.com/indexmap-rs/ordermap/blob/HEAD/RELEASES.md#0512-2025-09-15)

[Compare
Source](https://redirect.github.com/indexmap-rs/ordermap/compare/0.5.11...0.5.12)

- Make the minimum `serde` version only apply when "serde" is enabled.

###
[`v0.5.11`](https://redirect.github.com/indexmap-rs/ordermap/blob/HEAD/RELEASES.md#0511-2025-09-15)

[Compare
Source](https://redirect.github.com/indexmap-rs/ordermap/compare/0.5.10...0.5.11)

- Switched the "serde" feature to depend on `serde_core`, improving
build
parallelism in cases where other dependents have enabled "serde/derive".

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 09:02:58 +02:00
renovate[bot]
0b1e12f086 Update Rust crate indexmap to v2.11.4 (#20501) 2025-09-22 09:02:43 +02:00
renovate[bot]
d12324f06e Update dependency ruff to v0.13.1 (#20498) 2025-09-22 09:02:05 +02:00
Manuel Mendez
2c6c3e78f6 [ty] Search PYTHONPATH to find modules (#20441)
Co-authored-by: Nate Lust <natelust@linux.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-09-20 13:40:10 +02:00
Micha Reiser
3ffe56d19d [ty] Remove unnecessary FileScopeId to ScopeId conversion (#20481) 2025-09-20 11:20:10 +00:00
GF
eb354608d2 [ty] Add LSP debug information command (#20379)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-09-20 11:15:13 +00:00
Ibraheem Ahmed
12086dfa69 re-infer RHS of annotated assignments in isolation for assignability diagnostics 2025-09-19 17:00:37 -04:00
Ibraheem Ahmed
5f294f9f2e use type context for inference of generic function calls 2025-09-19 17:00:37 -04:00
Gary Yendell
44fc87f491 [ruff] Add logging-eager-conversion (RUF065) (#19942)
<!--
Thank you for contributing to Ruff/ty! 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? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

Fixes #12734

I have started with simply checking if any arguments that are providing
extra values to the log message are calls to `str` or `repr`, as
suggested in the linked issue. There was a concern that this could cause
false positives and the check should be more explicit. I am happy to
look into that if I have some further examples to work with.

If this is the accepted solution then there are more cases to add to the
test and it should possibly also do test for the same behavior via the
`extra` keyword.

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

## Test Plan

I have added a new test case and python file to flake8_logging_format
with examples of this anti-pattern.

<!-- How was it tested? -->
2025-09-19 16:43:44 -04:00
Takayuki Maeda
43cda2dfe9 [ruff] Fix B004 to skip invalid hasattr/getattr calls (#20486)
## Summary

Fixes #20440

Fix B004 to skip invalid hasattr/getattr calls

- Add argument validation for `hasattr` and `getattr`
- Skip B004 rule when function calls have invalid argument patterns
2025-09-19 13:44:42 -05:00
Takayuki Maeda
bd5b3e4f6e Deduplicate input paths (#20105)
<!--
Thank you for contributing to Ruff/ty! 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? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

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

Fixes #20035, fixes #19395

This is for deduplicating input paths to avoid processing the same file
multiple times.

This is my first contribution, so I'm sorry if I miss something. Please
tell me if this is needed for this feature.

## Test Plan

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

I just added a test `find_python_files_deduplicated` in
eee1020e32/crates/ruff_workspace/src/resolver.rs (L1017)
. This pull request adds changes to `WalkPythonFilesState::finish`,
which is used in `python_files_in_path`, so they affect some commands
such as `analyze`, `format`, `check` and so on. I will add snapshot
tests for them if necessary.

I’ve already confirmed that the same thing happens with ruff check as
well.

```
$ echo "x   = 1" > example/foo.py
$ uvx ruff check example example/foo.py
I002 [*] Missing required import: `from __future__ import annotations`
--> /path/to/example/foo.py:1:1
help: Insert required import: `from __future__ import annotations`

I002 [*] Missing required import: `from __future__ import annotations`
--> /path/to/example/foo.py:1:1
help: Insert required import: `from __future__ import annotations`

Found 2 errors.
[*] 2 fixable with the `--fix` option.
```
2025-09-19 14:40:23 -04:00
Andrew Gallant
3bf4dae452 [ty] Make auto-import work in the playground
It turned out that we weren't quite funneling the new completion data
all the way through.

I followed the docs for [`CompletionItem`] for the Monaco editor. It's
similar, but not identical, to the LSP protocol specification.

[`CompletionItem`]: https://microsoft.github.io/monaco-editor/typedoc/interfaces/languages.CompletionItem.html
2025-09-19 14:35:51 -04:00
Takayuki Maeda
8eeca023d6 [ruff] FURB164 Replace -nan with nan when using the value to construct Decimal (#20391)
## Summary

Fixes #19699

Normalize `Decimal(float("-nan"))` to `Decimal("nan")`.

The same handling is implemented in:

c3e873dd82/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs (L165)
2025-09-19 13:04:29 -05:00
Dan Parizher
c94ddb590f [flake8-bugbear] Add B912: map() without an explicit strict= parameter (#20429)
## Summary

Implements new rule `B912` that requires the `strict=` argument for
`map(...)` calls with two or more iterables on Python 3.14+, following
the same pattern as `B905` for `zip()`.

Closes #20057

---------

Co-authored-by: dylwil3 <dylwil3@gmail.com>
2025-09-19 12:54:44 -05:00
renovate[bot]
bae8ddfb8a Update Rust crate hashbrown to 0.16.0 (#20399)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [hashbrown](https://redirect.github.com/rust-lang/hashbrown) |
workspace.dependencies | minor | `0.15.0` -> `0.16.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>rust-lang/hashbrown (hashbrown)</summary>

###
[`v0.16.0`](https://redirect.github.com/rust-lang/hashbrown/blob/HEAD/CHANGELOG.md#0160---2025-08-28)

[Compare
Source](https://redirect.github.com/rust-lang/hashbrown/compare/v0.15.5...v0.16.0)

##### Changed

- Bump foldhash, the default hasher, to 0.2.0.
- Replaced `DefaultHashBuilder` with a newtype wrapper around `foldhash`
instead
  of re-exporting it directly.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: David Peter <mail@david-peter.de>
Co-authored-by: Ibraheem Ahmed <ibraheem@ibraheem.ca>
2025-09-19 13:24:45 -04:00
Dan Parizher
c0fb235a70 [flake8-comprehensions] Preserve trailing commas for single-element lists (C409) (#19571)
## Summary

Fixes #19568
2025-09-19 09:27:14 -04:00
Andrew Gallant
b5a3503a58 [ty] Enable auto-import for completions in WASM builds by default
Now that imports are actually inserted, this should give us some
valuable dog-fooding experience.

Note that we don't currently do any ranking on completions, so until
that is improved, even in-scope completions could suffer. With that
said, this shouldn't have any impact at all in several scenarios (like
completions for attributes on objects).
2025-09-19 08:11:59 -04:00
Andrew Gallant
d45209f425 [ty] Add some tricky test cases for the auto-import importer
We don't attempt to fix these yet. I think there are bigger fish to fry.

I came up with these based on this discussion:
https://github.com/astral-sh/ruff/pull/20439#discussion_r2357769518

Here's one example:

```
if ...:
    from foo import MAGIC
else:
    from bar import MAGIC

MAG<CURSOR>
```

Now in this example, completions will include `MAGIC` from the local
scope. That is, auto-import is involved with that completion. But at
present, auto-import will suggest importing `foo` and `bar` because we
haven't de-duplicated completions yet. Which is fine.

Here's another example:

```
if ...:
    import foo as fubar
else:
    import bar as fubar

MAG<CURSOR>
```

Now here, there is no `MAGIC` symbol in scope. So auto-import is in
play. Let's assume that the user selects `MAGIC` from `foo` in this
example. (`bar` also has `MAGIC`.)

Since we currently ignore the declaration site for symbols with
multiple possible bindings, the importer today doesn't know that
`fubar` _could_ contain `MAGIC`. But even if it did, what would we do
with that information? Should we do this?

```
if ...:
    import foo as fubar
    from foo import MAGIC
else:
    import bar as fubar

MAGIC
```

Or could we reason that `bar` also has `MAGIC`?

```
if ...:
    import foo as fubar
else:
    import bar as fubar

fubar.MAGIC
```

But if we did that, we're making an assumption of user intent, since
they *selected* `foo.MAGIC` but not `bar.MAGIC`.

Anyway, I don't think we need to settle on an answer today, but I
wanted to capture some of these tricky cases in tests at the very
least.
2025-09-19 07:54:07 -04:00
Micha Reiser
5d1cd85662 Shard instrumented benchmark (#20437) 2025-09-19 10:25:04 +02:00
Dhruv Manilawala
902b0b4ce9 [ty] Add support for **kwargs (#20430)
## Summary

This PR adds support for unpacking `**kwargs` argument.

This can be matched against any standard (positional or keyword),
keyword-only, or keyword variadic parameter that haven't been matched
yet.

This PR also takes care of special casing `TypedDict` because the key
names and the corresponding value type is known, so we can be more
precise in our matching and type checking step. In the future, this
special casing would be extended to include `ParamSpec` as well.

Part of astral-sh/ty#247

## Test Plan

Add test cases for various scenarios.
2025-09-19 05:00:30 +00:00
Amethyst Reese
6f2b60708e Exclude snapshots from vscode search results (#20457)
Makes ⌘-T file search ignore snapshot files, so you can actually fuzzy
match "ruff cache" to "ruff/src/cache.rs" without looking/scrolling past
dozens of snapshot files in the search results.
2025-09-18 21:10:42 -07:00
Frazer McLean
bc89d0394c [flake8-simplify] Fix incorrect fix for positive maxsplit without separator (SIM905) (#20056)
<!--
Thank you for contributing to Ruff/ty! 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? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

Resolves #20033

## Test Plan

unit tests added to the new split function, existing snapshot test
updated.

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-09-18 20:56:34 +00:00
Dylan
706be0a6e7 Add pyproject.toml to rooster config version_files and bump to 0.13.1 (#20475)
It looks like the new `rooster` does not automatically bump
`pyproject.toml`.

This should fix the following failure for the release action:


https://github.com/astral-sh/ruff/actions/runs/17839256763/job/50724254795
2025-09-18 14:37:29 -05:00
Dylan
7b40428b6a Bump 0.13.1 (#20473) 2025-09-18 19:25:17 +00:00
Dylan
b9b5755368 Upgrade to the latest rooster version and include contributors in CHANGELOG (#20472)
What it says on the tin!

Manually tested the release script and it appears to produce the right
thing.
2025-09-18 13:43:39 -05:00
Takayuki Maeda
b4b5d67a4a [flynt] Use triple quotes for joined raw strings with newlines (FLY002) (#20197)
<!--
Thank you for contributing to Ruff/ty! 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? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

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

Fixes #19887

- flynt(FLY002): When joining only string constants, upgrade raw
single-quoted strings to raw triple-quoted if the resulting
content contains a newline.
- Choose a safe triple-quote delimiter by switching to the opposite
quote style if the preferred triple appears inside the
content.
- Update FLY002 snapshot to include the `\n'.join([r'line1','line2'])`
case.

## Test Plan

I've added one test case to FLY002.py.

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

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-09-18 13:18:29 -04:00
Micha Reiser
0b60584b7e Bump MSRV to Rust 1.88 (#20470) 2025-09-18 17:52:37 +02:00
Takayuki Maeda
821b2f8b2e [refurb] Mark single-item-membership-test fix as always unsafe (FURB171) (#20279)
<!--
Thank you for contributing to Ruff/ty! 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? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

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

Fixes #20255

Mark single-item-membership-test fixes as always unsafe

- Always set `Applicability::Unsafe` for FURB171 fixes
- Update “Fix safety” docs to reflect always-unsafe behavior
- Expand tests (not in, nested set/frozenset, commented args)

## Test Plan

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

I have added new test cases to
`crates/ruff_linter/resources/test/fixtures/refurb/FURB171_0.py` and
`crates/ruff_linter/resources/test/fixtures/refurb/FURB171_1.py`.

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-09-18 15:24:15 +00:00
Micha Reiser
1758f26d94 Update rust toolchain to 1.90 (#20469) 2025-09-18 16:54:49 +02:00
Eric Mark Martin
2502ff7638 [ty] Make TypeIs invariant in its type argument (#20428)
## Summary

What it says on the tin. See the [typing
spec](https://docs.python.org/3/library/typing.html#typing.TypeIs) for
justification.

## Test Plan

Add more tests to PEP 695 `variance.md` suite.
2025-09-18 07:53:13 -07:00
chiri
144373fb3c [flake8-use-pathlib] Fix PTH101, PTH104, PTH105, PTH121 fixes (#20143)
## Summary
Fixes https://github.com/astral-sh/ruff/issues/20134

## Test Plan
`cargo nextest run flake8_use_pathlib`

---------

Co-authored-by: Dan Parizher <danparizher@users.noreply.github.com>
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-09-18 14:17:54 +00:00
Dan Parizher
91995aa516 [pyupgrade] Fix false positive when class name is shadowed by local variable (UP008) (#20427)
## Summary

Fixes #20422

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-09-18 14:05:05 +00:00
Andrew Gallant
1ebbe73a1d [ty] Swap detail and description fields for CompletionItemLabelDetails
This seems to be more consistent with how other LSPs work (like
`rust-analyzer`), and also I think is more consistent with how
`CompletionItem.detail` is itself rendered. Namely, in VS Code, it
is right-aligned. And it's also where we put the type signature.
But `CompletionItemLabelDetails.detail` is left-aligned where as
`CompletionItemLabelDetails.description` is right-aligned. So let's
swap them such that type signatures go in the latter and not the
former.

This also adds a space before the module name and contextualizes
it with `(import <name>)` to help aide the end user in figuring out
selecting the completion will do.

Fixes #1200
2025-09-18 09:11:17 -04:00
Shahar Naveh
48ada2d359 Generator preferred quote style (#20434) 2025-09-18 12:57:21 +02:00
David Peter
50bd3943da [ty] Faster iteration on mdtests (#20465)
## Summary

This change reduces MD test compilation time from 6s to 3s on my laptop.
We don't need to build the unit tests and the corpus tests when we're
only interested in Markdown-based tests.

## Test Plan

local benchmarks
2025-09-18 10:48:52 +00:00
Dan Parizher
5707958dad [flake8-simplify] Fix diagnostic to show correct method name for rsplit calls (SIM905) (#20459)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-09-18 07:52:08 +00:00
Nikolas Hearp
c4d359306b Add fixes to output-format=sarif (#20300)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-09-18 09:37:04 +02:00
210 changed files with 5747 additions and 1127 deletions

View File

@@ -88,7 +88,6 @@ jobs:
':!crates/ruff_python_formatter/**' \
':!crates/ruff_formatter/**' \
':!crates/ruff_dev/**' \
':!crates/ruff_db/**' \
':scripts/*' \
':python/**' \
':.github/workflows/ci.yaml' \
@@ -907,10 +906,13 @@ jobs:
run: npm run fmt:check
working-directory: playground
benchmarks-instrumented:
benchmarks-instrumented-ruff:
name: "benchmarks instrumented (ruff)"
runs-on: ubuntu-24.04
needs: determine_changes
if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
if: |
github.ref == 'refs/heads/main' ||
(needs.determine_changes.outputs.formatter == 'true' || needs.determine_changes.outputs.linter == 'true')
timeout-minutes: 20
steps:
- name: "Checkout Branch"
@@ -930,7 +932,42 @@ jobs:
tool: cargo-codspeed
- name: "Build benchmarks"
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark formatter lexer linter parser
- name: "Run benchmarks"
uses: CodSpeedHQ/action@653fdc30e6c40ffd9739e40c8a0576f4f4523ca1 # v4.0.1
with:
mode: instrumentation
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
benchmarks-instrumented-ty:
name: "benchmarks instrumented (ty)"
runs-on: ubuntu-24.04
needs: determine_changes
if: |
github.ref == 'refs/heads/main' ||
needs.determine_changes.outputs.ty == 'true'
timeout-minutes: 20
steps:
- name: "Checkout Branch"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3
with:
tool: cargo-codspeed
- name: "Build benchmarks"
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark ty
- name: "Run benchmarks"
uses: CodSpeedHQ/action@653fdc30e6c40ffd9739e40c8a0576f4f4523ca1 # v4.0.1

View File

@@ -49,7 +49,7 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@27dd66d9e397d986ef9c631119ee09556eab8af9"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@fc0f612798710b0dd69bb7528bc9b361dc60bd43"
ecosystem-analyzer \
--verbose \

View File

@@ -3,4 +3,7 @@
"--all-features"
],
"rust-analyzer.check.command": "clippy",
}
"search.exclude": {
"**/*.snap": true
}
}

View File

@@ -1,5 +1,65 @@
# Changelog
## 0.13.1
Released on 2025-09-18.
### Preview features
- \[`flake8-simplify`\] Detect unnecessary `None` default for additional key expression types (`SIM910`) ([#20343](https://github.com/astral-sh/ruff/pull/20343))
- \[`flake8-use-pathlib`\] Add fix for `PTH123` ([#20169](https://github.com/astral-sh/ruff/pull/20169))
- \[`flake8-use-pathlib`\] Fix `PTH101`, `PTH104`, `PTH105`, `PTH121` fixes ([#20143](https://github.com/astral-sh/ruff/pull/20143))
- \[`flake8-use-pathlib`\] Make `PTH111` fix unsafe because it can change behavior ([#20215](https://github.com/astral-sh/ruff/pull/20215))
- \[`pycodestyle`\] Fix `E301` to only trigger for functions immediately within a class ([#19768](https://github.com/astral-sh/ruff/pull/19768))
- \[`refurb`\] Mark `single-item-membership-test` fix as always unsafe (`FURB171`) ([#20279](https://github.com/astral-sh/ruff/pull/20279))
### Bug fixes
- Handle t-strings for token-based rules and suppression comments ([#20357](https://github.com/astral-sh/ruff/pull/20357))
- \[`flake8-bandit`\] Fix truthiness: dict-only `**` displays not truthy for `shell` (`S602`, `S604`, `S609`) ([#20177](https://github.com/astral-sh/ruff/pull/20177))
- \[`flake8-simplify`\] Fix diagnostic to show correct method name for `str.rsplit` calls (`SIM905`) ([#20459](https://github.com/astral-sh/ruff/pull/20459))
- \[`flynt`\] Use triple quotes for joined raw strings with newlines (`FLY002`) ([#20197](https://github.com/astral-sh/ruff/pull/20197))
- \[`pyupgrade`\] Fix false positive when class name is shadowed by local variable (`UP008`) ([#20427](https://github.com/astral-sh/ruff/pull/20427))
- \[`pyupgrade`\] Prevent infinite loop with `I002` and `UP026` ([#20327](https://github.com/astral-sh/ruff/pull/20327))
- \[`ruff`\] Recognize t-strings, generators, and lambdas in `invalid-index-type` (`RUF016`) ([#20213](https://github.com/astral-sh/ruff/pull/20213))
### Rule changes
- \[`RUF102`\] Respect rule redirects in invalid rule code detection ([#20245](https://github.com/astral-sh/ruff/pull/20245))
- \[`flake8-bugbear`\] Mark the fix for `unreliable-callable-check` as always unsafe (`B004`) ([#20318](https://github.com/astral-sh/ruff/pull/20318))
- \[`ruff`\] Allow dataclass attribute value instantiation from nested frozen dataclass (`RUF009`) ([#20352](https://github.com/astral-sh/ruff/pull/20352))
### CLI
- Add fixes to `output-format=sarif` ([#20300](https://github.com/astral-sh/ruff/pull/20300))
- Treat panics as fatal diagnostics, sort panics last ([#20258](https://github.com/astral-sh/ruff/pull/20258))
### Documentation
- \[`ruff`\] Add `analyze.string-imports-min-dots` to settings ([#20375](https://github.com/astral-sh/ruff/pull/20375))
- Update README.md with Albumentations new repository URL ([#20415](https://github.com/astral-sh/ruff/pull/20415))
### Other changes
- Bump MSRV to Rust 1.88 ([#20470](https://github.com/astral-sh/ruff/pull/20470))
- Enable inline noqa for multiline strings in playground ([#20442](https://github.com/astral-sh/ruff/pull/20442))
### Contributors
- [@chirizxc](https://github.com/chirizxc)
- [@danparizher](https://github.com/danparizher)
- [@IDrokin117](https://github.com/IDrokin117)
- [@amyreese](https://github.com/amyreese)
- [@AlexWaygood](https://github.com/AlexWaygood)
- [@dylwil3](https://github.com/dylwil3)
- [@njhearp](https://github.com/njhearp)
- [@woodruffw](https://github.com/woodruffw)
- [@dcreager](https://github.com/dcreager)
- [@TaKO8Ki](https://github.com/TaKO8Ki)
- [@BurntSushi](https://github.com/BurntSushi)
- [@salahelfarissi](https://github.com/salahelfarissi)
- [@MichaReiser](https://github.com/MichaReiser)
## 0.13.0
Check out the [blog post](https://astral.sh/blog/ruff-v0.13.0) for a migration

376
Cargo.lock generated
View File

@@ -23,12 +23,6 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -104,9 +98,9 @@ dependencies = [
[[package]]
name = "anstyle-svg"
version = "0.1.10"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc03a770ef506fe1396c0e476120ac0e6523cf14b74218dd5f18cd6833326fa9"
checksum = "26b9ec8c976eada1b0f9747a3d7cc4eae3bef10613e443746e7487f26c872fde"
dependencies = [
"anstyle",
"anstyle-lossy",
@@ -128,9 +122,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.99"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "approx"
@@ -284,9 +278,9 @@ dependencies = [
[[package]]
name = "boxcar"
version = "0.2.13"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26c4925bc979b677330a8c7fe7a8c94af2dbb4a2d37b4a20a80d884400f46baa"
checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e"
[[package]]
name = "bstr"
@@ -346,10 +340,11 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.31"
version = "1.2.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
@@ -357,9 +352,9 @@ dependencies = [
[[package]]
name = "cfg-if"
version = "1.0.1"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "cfg_aliases"
@@ -369,14 +364,13 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.41"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"windows-link 0.1.3",
"windows-link 0.2.0",
]
[[package]]
@@ -408,9 +402,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.47"
version = "4.5.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931"
checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae"
dependencies = [
"clap_builder",
"clap_derive",
@@ -418,9 +412,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.47"
version = "4.5.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9"
dependencies = [
"anstream",
"anstyle",
@@ -431,9 +425,9 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.5.55"
version = "4.5.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a"
checksum = "75bf0b32ad2e152de789bb635ea4d3078f6b838ad7974143e99b99f45a04af4a"
dependencies = [
"clap",
]
@@ -650,15 +644,15 @@ dependencies = [
[[package]]
name = "console"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d"
checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width 0.2.1",
"windows-sys 0.60.2",
"windows-sys 0.61.0",
]
[[package]]
@@ -837,9 +831,9 @@ dependencies = [
[[package]]
name = "darling"
version = "0.20.11"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
dependencies = [
"darling_core",
"darling_macro",
@@ -847,9 +841,9 @@ dependencies = [
[[package]]
name = "darling_core"
version = "0.20.11"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
dependencies = [
"fnv",
"ident_case",
@@ -861,9 +855,9 @@ dependencies = [
[[package]]
name = "darling_macro"
version = "0.20.11"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
dependencies = [
"darling_core",
"quote",
@@ -956,7 +950,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.60.2",
"windows-sys 0.61.0",
]
[[package]]
@@ -1037,12 +1031,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.13"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.61.0",
]
[[package]]
@@ -1053,12 +1047,11 @@ checksum = "5692dd7b5a1978a5aeb0ce83b7655c58ca8efdcb79d21036ea249da95afec2c6"
[[package]]
name = "escargot"
version = "0.5.14"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83f351750780493fc33fa0ce8ba3c7d61f9736cfa3b3bb9ee2342643ffe40211"
checksum = "11c3aea32bc97b500c9ca6a72b768a26e558264303d101d3409cf6d57a9ed0cf"
dependencies = [
"log",
"once_cell",
"serde",
"serde_json",
]
@@ -1101,6 +1094,12 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959"
[[package]]
name = "flate2"
version = "1.1.2"
@@ -1168,9 +1167,9 @@ dependencies = [
[[package]]
name = "get-size-derive2"
version = "0.6.2"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75a17a226478b2e8294ded60782c03efe54476aa8cd1371d0e5ad9d1071e74e0"
checksum = "e3814abc7da8ab18d2fd820f5b540b5e39b6af0a32de1bdd7c47576693074843"
dependencies = [
"attribute-derive",
"quote",
@@ -1179,21 +1178,21 @@ dependencies = [
[[package]]
name = "get-size2"
version = "0.6.2"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5697765925a05c9d401dd04a93dfd662d336cc25fdcc3301220385a1ffcfdde5"
checksum = "5dfe2cec5b5ce8fb94dcdb16a1708baa4d0609cc3ce305ca5d3f6f2ffb59baed"
dependencies = [
"compact_str",
"get-size-derive2",
"hashbrown 0.15.5",
"hashbrown 0.16.0",
"smallvec",
]
[[package]]
name = "getopts"
version = "0.2.23"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1"
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
dependencies = [
"unicode-width 0.2.1",
]
@@ -1219,7 +1218,7 @@ dependencies = [
"js-sys",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
"wasi 0.14.7+wasi-0.2.4",
"wasm-bindgen",
]
@@ -1280,6 +1279,15 @@ dependencies = [
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
dependencies = [
"equivalent",
]
[[package]]
name = "hashlink"
version = "0.10.0"
@@ -1321,9 +1329,9 @@ dependencies = [
[[package]]
name = "iana-time-zone"
version = "0.1.63"
version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [
"android_system_properties",
"core-foundation-sys",
@@ -1493,13 +1501,14 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.11.1"
version = "2.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
dependencies = [
"equivalent",
"hashbrown 0.15.5",
"hashbrown 0.16.0",
"serde",
"serde_core",
]
[[package]]
@@ -1508,7 +1517,7 @@ version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd"
dependencies = [
"console 0.16.0",
"console 0.16.1",
"portable-atomic",
"unicode-width 0.2.1",
"unit-prefix",
@@ -1588,9 +1597,9 @@ dependencies = [
[[package]]
name = "inventory"
version = "0.3.20"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83"
checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e"
dependencies = [
"rustversion",
]
@@ -1719,9 +1728,9 @@ dependencies = [
[[package]]
name = "jobserver"
version = "0.1.33"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.3",
"libc",
@@ -1735,9 +1744,9 @@ checksum = "a037eddb7d28de1d0fc42411f501b53b75838d313908078d6698d064f3029b24"
[[package]]
name = "js-sys"
version = "0.3.78"
version = "0.3.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738"
checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -1812,9 +1821,9 @@ dependencies = [
[[package]]
name = "libredox"
version = "0.1.9"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
dependencies = [
"bitflags 2.9.4",
"libc",
@@ -1835,9 +1844,9 @@ dependencies = [
[[package]]
name = "linux-raw-sys"
version = "0.9.4"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "litemap"
@@ -2133,12 +2142,13 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordermap"
version = "0.5.10"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dcd63f1ae4b091e314a26627c467dd8810d674ba798abc0e566679955776c63"
checksum = "b100f7dd605611822d30e182214d3c02fdefce2d801d23993f6b6ba6ca1392af"
dependencies = [
"indexmap",
"serde",
"serde_core",
]
[[package]]
@@ -2289,9 +2299,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pest"
version = "2.8.1"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8"
dependencies = [
"memchr",
"thiserror 2.0.16",
@@ -2300,9 +2310,9 @@ dependencies = [
[[package]]
name = "pest_derive"
version = "2.8.1"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc"
checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663"
dependencies = [
"pest",
"pest_generator",
@@ -2310,9 +2320,9 @@ dependencies = [
[[package]]
name = "pest_generator"
version = "2.8.1"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966"
checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f"
dependencies = [
"pest",
"pest_meta",
@@ -2323,9 +2333,9 @@ dependencies = [
[[package]]
name = "pest_meta"
version = "2.8.1"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5"
checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420"
dependencies = [
"pest",
"sha2",
@@ -2398,9 +2408,9 @@ dependencies = [
[[package]]
name = "potential_utf"
version = "0.1.2"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
dependencies = [
"zerovec",
]
@@ -2453,9 +2463,9 @@ dependencies = [
[[package]]
name = "proc-macro-crate"
version = "3.3.0"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
dependencies = [
"toml_edit",
]
@@ -2705,15 +2715,15 @@ dependencies = [
[[package]]
name = "regex-lite"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30"
[[package]]
name = "regex-syntax"
version = "0.8.5"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]]
name = "ron"
@@ -2728,7 +2738,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"anyhow",
"argfile",
@@ -2984,7 +2994,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"aho-corasick",
"anyhow",
@@ -2994,7 +3004,7 @@ dependencies = [
"fern",
"glob",
"globset",
"hashbrown 0.15.5",
"hashbrown 0.16.0",
"imperative",
"insta",
"is-macro",
@@ -3338,7 +3348,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3427,22 +3437,22 @@ checksum = "781442f29170c5c93b7185ad559492601acdc71d5bb0706f5868094f45cfcd08"
[[package]]
name = "rustix"
version = "1.0.8"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
dependencies = [
"bitflags 2.9.4",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
"windows-sys 0.61.0",
]
[[package]]
name = "rustversion"
version = "1.0.21"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
@@ -3537,9 +3547,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "serde"
version = "1.0.223"
version = "1.0.226"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a505d71960adde88e293da5cb5eda57093379f64e61cf77bf0e6a63af07a7bac"
checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd"
dependencies = [
"serde_core",
"serde_derive",
@@ -3558,18 +3568,18 @@ dependencies = [
[[package]]
name = "serde_core"
version = "1.0.223"
version = "1.0.226"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20f57cbd357666aa7b3ac84a90b4ea328f1d4ddb6772b430caa5d9e1309bb9e9"
checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.223"
version = "1.0.226"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d428d07faf17e306e699ec1e91996e5a165ba5d6bce5b5155173e91a8a01a56"
checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33"
dependencies = [
"proc-macro2",
"quote",
@@ -3613,11 +3623,11 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "1.0.0"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee"
dependencies = [
"serde",
"serde_core",
]
[[package]]
@@ -3631,9 +3641,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.14.0"
version = "3.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5"
checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e"
dependencies = [
"serde",
"serde_derive",
@@ -3642,9 +3652,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.14.0"
version = "3.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f"
checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e"
dependencies = [
"darling",
"proc-macro2",
@@ -3830,7 +3840,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.61.0",
]
[[package]]
@@ -3844,12 +3854,12 @@ dependencies = [
[[package]]
name = "terminal_size"
version = "0.4.2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed"
checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
dependencies = [
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -4009,9 +4019,9 @@ dependencies = [
[[package]]
name = "tinyvec"
version = "1.9.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
dependencies = [
"tinyvec_macros",
]
@@ -4024,14 +4034,14 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
version = "0.9.5"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0"
dependencies = [
"indexmap",
"serde",
"serde_core",
"serde_spanned",
"toml_datetime 0.7.0",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
@@ -4039,44 +4049,39 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.6.11"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
[[package]]
name = "toml_datetime"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1"
dependencies = [
"serde",
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
version = "0.23.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b"
dependencies = [
"indexmap",
"toml_datetime 0.6.11",
"toml_datetime",
"toml_parser",
"winnow",
]
[[package]]
name = "toml_parser"
version = "1.0.2"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.2"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109"
[[package]]
name = "tracing"
@@ -4286,6 +4291,7 @@ dependencies = [
"tracing",
"ty_combine",
"ty_python_semantic",
"ty_static",
"ty_vendored",
]
@@ -4303,7 +4309,7 @@ dependencies = [
"drop_bomb",
"get-size2",
"glob",
"hashbrown 0.15.5",
"hashbrown 0.16.0",
"indexmap",
"insta",
"itertools 0.14.0",
@@ -4513,9 +4519,9 @@ dependencies = [
[[package]]
name = "unicode-id"
version = "0.3.5"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561"
checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580"
[[package]]
name = "unicode-ident"
@@ -4740,18 +4746,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
version = "0.14.7+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c"
dependencies = [
"wit-bindgen-rt",
"wasip2",
]
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.101"
version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b"
checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819"
dependencies = [
"cfg-if",
"once_cell",
@@ -4762,9 +4777,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.101"
version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb"
checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c"
dependencies = [
"bumpalo",
"log",
@@ -4776,9 +4791,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.51"
version = "0.4.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe"
checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67"
dependencies = [
"cfg-if",
"js-sys",
@@ -4789,9 +4804,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.101"
version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d"
checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -4799,9 +4814,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.101"
version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa"
checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32"
dependencies = [
"proc-macro2",
"quote",
@@ -4812,18 +4827,18 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.101"
version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1"
checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-bindgen-test"
version = "0.3.51"
version = "0.3.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80cc7f8a4114fdaa0c58383caf973fc126cf004eba25c9dc639bccd3880d55ad"
checksum = "aee0a0f5343de9221a0d233b04520ed8dc2e6728dce180b1dcd9288ec9d9fa3c"
dependencies = [
"js-sys",
"minicov",
@@ -4834,9 +4849,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-macro"
version = "0.3.51"
version = "0.3.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5ada2ab788d46d4bda04c9d567702a79c8ced14f51f221646a16ed39d0e6a5d"
checksum = "a369369e4360c2884c3168d22bded735c43cccae97bbc147586d4b480edd138d"
dependencies = [
"proc-macro2",
"quote",
@@ -4845,9 +4860,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.78"
version = "0.3.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12"
checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -4885,22 +4900,22 @@ dependencies = [
[[package]]
name = "winapi-util"
version = "0.1.9"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.61.0",
]
[[package]]
name = "windows-core"
version = "0.61.2"
version = "0.62.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link 0.1.3",
"windows-link 0.2.0",
"windows-result",
"windows-strings",
]
@@ -4941,20 +4956,20 @@ checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
[[package]]
name = "windows-result"
version = "0.3.4"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f"
dependencies = [
"windows-link 0.1.3",
"windows-link 0.2.0",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda"
dependencies = [
"windows-link 0.1.3",
"windows-link 0.2.0",
]
[[package]]
@@ -5124,9 +5139,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.7.12"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
dependencies = [
"memchr",
]
@@ -5138,13 +5153,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
name = "wit-bindgen"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.4",
]
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "writeable"
@@ -5193,18 +5205,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.26"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.26"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
@@ -5299,9 +5311,9 @@ dependencies = [
[[package]]
name = "zstd-sys"
version = "2.0.15+zstd.1.5.7"
version = "2.0.16+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
dependencies = [
"cc",
"pkg-config",

View File

@@ -5,7 +5,7 @@ resolver = "2"
[workspace.package]
# Please update rustfmt.toml when bumping the Rust edition
edition = "2024"
rust-version = "1.87"
rust-version = "1.88"
homepage = "https://docs.astral.sh/ruff"
documentation = "https://docs.astral.sh/ruff"
repository = "https://github.com/astral-sh/ruff"
@@ -86,7 +86,7 @@ etcetera = { version = "0.10.0" }
fern = { version = "0.7.0" }
filetime = { version = "0.2.23" }
getrandom = { version = "0.3.1" }
get-size2 = { version = "0.6.2", features = [
get-size2 = { version = "0.7.0", features = [
"derive",
"smallvec",
"hashbrown",
@@ -95,7 +95,7 @@ get-size2 = { version = "0.6.2", features = [
glob = { version = "0.3.1" }
globset = { version = "0.4.14" }
globwalk = { version = "0.9.1" }
hashbrown = { version = "0.15.0", default-features = false, features = [
hashbrown = { version = "0.16.0", default-features = false, features = [
"raw-entry",
"equivalent",
"inline-more",
@@ -203,7 +203,7 @@ wild = { version = "2" }
zip = { version = "0.6.6", default-features = false }
[workspace.metadata.cargo-shear]
ignored = ["getrandom", "ruff_options_metadata", "uuid"]
ignored = ["getrandom", "ruff_options_metadata", "uuid", "get-size2"]
[workspace.lints.rust]

View File

@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.13.0/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.13.0/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.13.1/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.13.1/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.13.0
rev: v0.13.1
hooks:
# Run the linter.
- id: ruff-check

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.13.0"
version = "0.13.1"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -500,6 +500,35 @@ OTHER = "OTHER"
Ok(())
}
/// Regression test for <https://github.com/astral-sh/ruff/issues/20035>
#[test]
fn deduplicate_directory_and_explicit_file() -> Result<()> {
let tempdir = TempDir::new()?;
let root = tempdir.path();
let main = root.join("main.py");
fs::write(&main, "x = 1\n")?;
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.current_dir(root)
.args(["format", "--no-cache", "--check"])
.arg(".")
.arg("main.py"),
@r"
success: false
exit_code: 1
----- stdout -----
Would reformat: main.py
1 file would be reformatted
----- stderr -----
"
);
Ok(())
}
#[test]
fn syntax_error() -> Result<()> {
let tempdir = TempDir::new()?;

View File

@@ -271,6 +271,50 @@ OTHER = "OTHER"
Ok(())
}
/// Regression test for <https://github.com/astral-sh/ruff/issues/20035>
#[test]
fn deduplicate_directory_and_explicit_file() -> Result<()> {
let tempdir = TempDir::new()?;
let root = tempdir.path();
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint]
exclude = ["main.py"]
"#,
)?;
let main = root.join("main.py");
fs::write(&main, "import os\n")?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.current_dir(root)
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.arg(".")
// Explicitly pass main.py, should be linted regardless of it being excluded by lint.exclude
.arg("main.py"),
@r"
success: false
exit_code: 1
----- stdout -----
main.py:1:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"
);
});
Ok(())
}
#[test]
fn exclude_stdin() -> Result<()> {
let tempdir = TempDir::new()?;

View File

@@ -22,6 +22,30 @@ exit_code: 1
{
"results": [
{
"fixes": [
{
"artifactChanges": [
{
"artifactLocation": {
"uri": "[TMP]/input.py"
},
"replacements": [
{
"deletedRegion": {
"endColumn": 1,
"endLine": 2,
"startColumn": 1,
"startLine": 1
}
}
]
}
],
"description": {
"text": "Remove unused import: `os`"
}
}
],
"level": "error",
"locations": [
{

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="182px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="164px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,7 +1,7 @@
<svg width="1356px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,7 +1,7 @@
<svg width="869px" height="236px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.fg-yellow { fill: #AA5500 }

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.fg-yellow { fill: #AA5500 }

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,7 +1,7 @@
<svg width="911px" height="236px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.fg-yellow { fill: #AA5500 }

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,7 +1,7 @@
<svg width="768px" height="290px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="146px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,7 +1,7 @@
<svg width="1196px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="182px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.fg-yellow { fill: #AA5500 }

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,7 +1,7 @@
<svg width="1196px" height="164px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -232,7 +232,7 @@ static STATIC_FRAME: std::sync::LazyLock<Benchmark<'static>> = std::sync::LazyLo
max_dep_date: "2025-08-09",
python_version: PythonVersion::PY311,
},
500,
600,
)
});

View File

@@ -58,10 +58,11 @@ impl<'a> FullRenderer<'a> {
writeln!(f, "{}", renderer.render(diag.to_annotate()))?;
}
if self.config.show_fix_diff && diag.has_applicable_fix(self.config) {
if let Some(diff) = Diff::from_diagnostic(diag, &stylesheet, self.resolver) {
write!(f, "{diff}")?;
}
if self.config.show_fix_diff
&& diag.has_applicable_fix(self.config)
&& let Some(diff) = Diff::from_diagnostic(diag, &stylesheet, self.resolver)
{
write!(f, "{diff}")?;
}
writeln!(f)?;

View File

@@ -504,8 +504,8 @@ impl ToOwned for SystemPath {
pub struct SystemPathBuf(#[cfg_attr(feature = "schemars", schemars(with = "String"))] Utf8PathBuf);
impl get_size2::GetSize for SystemPathBuf {
fn get_heap_size(&self) -> usize {
self.0.capacity()
fn get_heap_size_with_tracker<T: get_size2::GetSizeTracker>(&self, tracker: T) -> (usize, T) {
(self.0.capacity(), tracker)
}
}

View File

@@ -92,8 +92,8 @@ impl ToOwned for VendoredPath {
pub struct VendoredPathBuf(Utf8PathBuf);
impl get_size2::GetSize for VendoredPathBuf {
fn get_heap_size(&self) -> usize {
self.0.capacity()
fn get_heap_size_with_tracker<T: get_size2::GetSizeTracker>(&self, tracker: T) -> (usize, T) {
(self.0.capacity(), tracker)
}
}

View File

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

View File

@@ -0,0 +1,104 @@
import os
from typing import Optional
from pathlib import Path
## Valid cases:
def os_path_in_foo():
file = "file.txt"
os.path.abspath(file) # OK
os.path.exists(file) # OK
os.path.split() # OK
async def non_io_os_path_methods():
os.path.split() # OK
os.path.dirname() # OK
os.path.basename() # OK
os.path.join() # OK
def pathlib_path_in_foo():
path = Path("src/my_text.txt") # OK
path.exists() # OK
with path.open() as f: # OK
...
path = Path("src/my_text.txt").open() # OK
async def non_io_pathlib_path_methods():
path = Path("src/my_text.txt")
path.is_absolute() # OK
path.is_relative_to() # OK
path.as_posix() # OK
path.relative_to() # OK
def inline_path_method_call():
Path("src/my_text.txt").open() # OK
Path("src/my_text.txt").open().flush() # OK
with Path("src/my_text.txt").open() as f: # OK
...
async def trio_path_in_foo():
from trio import Path
path = Path("src/my_text.txt") # OK
await path.absolute() # OK
await path.exists() # OK
with Path("src/my_text.txt").open() as f: # OK
...
async def anyio_path_in_foo():
from anyio import Path
path = Path("src/my_text.txt") # OK
await path.absolute() # OK
await path.exists() # OK
with Path("src/my_text.txt").open() as f: # OK
...
async def path_open_in_foo():
path = Path("src/my_text.txt") # OK
path.open() # OK, covered by ASYNC230
## Invalid cases:
async def os_path_in_foo():
file = "file.txt"
os.path.abspath(file) # ASYNC240
os.path.exists(file) # ASYNC240
async def pathlib_path_in_foo():
path = Path("src/my_text.txt")
path.exists() # ASYNC240
async def pathlib_path_in_foo():
import pathlib
path = pathlib.Path("src/my_text.txt")
path.exists() # ASYNC240
async def inline_path_method_call():
Path("src/my_text.txt").exists() # ASYNC240
Path("src/my_text.txt").absolute().exists() # ASYNC240
async def aliased_path_in_foo():
from pathlib import Path as PathAlias
path = PathAlias("src/my_text.txt")
path.exists() # ASYNC240
global_path = Path("src/my_text.txt")
async def global_path_in_foo():
global_path.exists() # ASYNC240
async def path_as_simple_parameter_type(path: Path):
path.exists() # ASYNC240
async def path_as_union_parameter_type(path: Path | None):
path.exists() # ASYNC240
async def path_as_optional_parameter_type(path: Optional[Path]):
path.exists() # ASYNC240

View File

@@ -52,3 +52,21 @@ class A:
assert hasattr(A(), "__call__")
assert callable(A()) is False
# https://github.com/astral-sh/ruff/issues/20440
def test_invalid_hasattr_calls():
hasattr(0, "__call__", 0) # 3 args - invalid
hasattr(0, "__call__", x=0) # keyword arg - invalid
hasattr(0, "__call__", 0, x=0) # 3 args + keyword - invalid
hasattr() # no args - invalid
hasattr(0) # 1 arg - invalid
hasattr(*(), "__call__", "extra") # unpacking - invalid
hasattr(*()) # unpacking - invalid
def test_invalid_getattr_calls():
getattr(0, "__call__", None, "extra") # 4 args - invalid
getattr(0, "__call__", default=None) # keyword arg - invalid
getattr() # no args - invalid
getattr(0) # 1 arg - invalid
getattr(*(), "__call__", None, "extra") # unpacking - invalid
getattr(*()) # unpacking - invalid

View File

@@ -0,0 +1,33 @@
from itertools import count, cycle, repeat
# Errors
map(lambda x: x, [1, 2, 3])
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
# Errors (limited iterators).
map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1))
map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=4))
import builtins
# Still an error even though it uses the qualified name
builtins.map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
# OK
map(lambda x: x, [1, 2, 3], strict=True)
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], strict=True)
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], strict=False)
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=True)
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True), strict=True)
# OK (single iterable - no strict required)
map(lambda x: x, [1, 2, 3])
# OK (infinite iterators)
map(lambda x, y: x + y, [1, 2, 3], cycle([1, 2, 3]))
map(lambda x, y: x + y, [1, 2, 3], repeat(1))
map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=None))
map(lambda x, y: x + y, [1, 2, 3], count())

View File

@@ -19,3 +19,18 @@ class MyClass:
def attribute_usage(self) -> id:
pass
class C:
@staticmethod
def property(f):
return f
id = 1
@[property][0]
def f(self, x=[id]):
return x
bin = 2
foo = [bin]

View File

@@ -42,3 +42,6 @@ tuple(
x for x in [1,2,3]
}
)
t9 = tuple([1],)
t10 = tuple([1, 2],)

View File

@@ -170,3 +170,4 @@ print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
# leading/trailing whitespace should not count towards maxsplit
" a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
" a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
"a b".split(maxsplit=1) # ["a", "b"]

View File

@@ -125,3 +125,15 @@ os.makedirs("name", 0o777, False)
os.makedirs(name="name", mode=0o777, exist_ok=False)
os.makedirs("name", unknown_kwarg=True)
# https://github.com/astral-sh/ruff/issues/20134
os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
# Only diagnostic
os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)

View File

@@ -16,7 +16,9 @@ nok4 = "a".join([a, a, *a]) # Not OK (not a static length)
nok5 = "a".join([choice("flarp")]) # Not OK (not a simple call)
nok6 = "a".join(x for x in "feefoofum") # Not OK (generator)
nok7 = "a".join([f"foo{8}", "bar"]) # Not OK (contains an f-string)
# https://github.com/astral-sh/ruff/issues/19887
nok8 = '\n'.join([r'line1','line2'])
nok9 = '\n'.join([r"raw string", '<""">', "<'''>"]) # Not OK (both triple-quote delimiters appear; should bail)
# Regression test for: https://github.com/astral-sh/ruff/issues/7197
def create_file_public_url(url, filename):

View File

@@ -271,3 +271,35 @@ class ChildI9(ParentI):
if False: super
if False: __class__
builtins.super(ChildI9, self).f()
# See: https://github.com/astral-sh/ruff/issues/20422
# UP008 should not apply when the class variable is shadowed
class A:
def f(self):
return 1
class B(A):
def f(self):
return 2
class C(B):
def f(self):
C = B # Local variable C shadows the class name
return super(C, self).f() # Should NOT trigger UP008
# See: https://github.com/astral-sh/ruff/issues/20491
# UP008 should not apply when __class__ is a local variable
class A:
def f(self):
return 1
class B(A):
def f(self):
return 2
class C(B):
def f(self):
__class__ = B # Local variable __class__ shadows the implicit __class__
return super(__class__, self).f() # Should NOT trigger UP008

View File

@@ -117,3 +117,31 @@ def _():
# After
] and \
0 < 1: ...
# https://github.com/astral-sh/ruff/issues/20255
import math
# NaN behavior differences
if math.nan in [math.nan]:
print("This is True")
if math.nan in (math.nan,):
print("This is True")
if math.nan in {math.nan}:
print("This is True")
# Potential type differences with custom __eq__ methods
class CustomEq:
def __eq__(self, other):
return "custom"
obj = CustomEq()
if obj in [CustomEq()]:
pass
if obj in (CustomEq(),):
pass
if obj in {CustomEq()}:
pass

View File

@@ -51,3 +51,13 @@ if 1 in set(1,2):
if 1 in set((x for x in range(2))):
pass
# https://github.com/astral-sh/ruff/issues/20255
import math
# set() and frozenset() with NaN
if math.nan in set([math.nan]):
print("This should be marked unsafe")
if math.nan in frozenset([math.nan]):
print("This should be marked unsafe")

View File

@@ -0,0 +1,39 @@
import logging
# %s + str()
logging.info("Hello %s", str("World!"))
logging.log(logging.INFO, "Hello %s", str("World!"))
# %s + repr()
logging.info("Hello %s", repr("World!"))
logging.log(logging.INFO, "Hello %s", repr("World!"))
# %r + str()
logging.info("Hello %r", str("World!"))
logging.log(logging.INFO, "Hello %r", str("World!"))
# %r + repr()
logging.info("Hello %r", repr("World!"))
logging.log(logging.INFO, "Hello %r", repr("World!"))
from logging import info, log
# %s + str()
info("Hello %s", str("World!"))
log(logging.INFO, "Hello %s", str("World!"))
# %s + repr()
info("Hello %s", repr("World!"))
log(logging.INFO, "Hello %s", repr("World!"))
# %r + str()
info("Hello %r", str("World!"))
log(logging.INFO, "Hello %r", str("World!"))
# %r + repr()
info("Hello %r", repr("World!"))
log(logging.INFO, "Hello %r", repr("World!"))
def str(s): return f"str = {s}"
# Don't flag this
logging.info("Hello %s", str("World!"))

View File

@@ -669,6 +669,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::BlockingOpenCallInAsyncFunction) {
flake8_async::rules::blocking_open_call(checker, call);
}
if checker.is_rule_enabled(Rule::BlockingPathMethodInAsyncFunction) {
flake8_async::rules::blocking_os_path(checker, call);
}
if checker.any_rule_enabled(&[
Rule::CreateSubprocessInAsyncFunction,
Rule::RunProcessInAsyncFunction,
@@ -717,7 +720,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
flake8_bugbear::rules::re_sub_positional_args(checker, call);
}
if checker.is_rule_enabled(Rule::UnreliableCallableCheck) {
flake8_bugbear::rules::unreliable_callable_check(checker, expr, func, args);
flake8_bugbear::rules::unreliable_callable_check(
checker, expr, func, args, keywords,
);
}
if checker.is_rule_enabled(Rule::StripWithMultiCharacters) {
flake8_bugbear::rules::strip_with_multi_characters(checker, expr, func, args);
@@ -741,6 +746,11 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
flake8_bugbear::rules::zip_without_explicit_strict(checker, call);
}
}
if checker.is_rule_enabled(Rule::MapWithoutExplicitStrict) {
if checker.target_version() >= PythonVersion::PY314 {
flake8_bugbear::rules::map_without_explicit_strict(checker, call);
}
}
if checker.is_rule_enabled(Rule::NoExplicitStacklevel) {
flake8_bugbear::rules::no_explicit_stacklevel(checker, call);
}
@@ -1279,6 +1289,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::UnnecessaryEmptyIterableWithinDequeCall) {
ruff::rules::unnecessary_literal_within_deque_call(checker, call);
}
if checker.is_rule_enabled(Rule::LoggingEagerConversion) {
ruff::rules::logging_eager_conversion(checker, call);
}
if checker.is_rule_enabled(Rule::StarmapZip) {
ruff::rules::starmap_zip(checker, call);
}

View File

@@ -56,7 +56,7 @@ use ruff_python_semantic::{
Import, Module, ModuleKind, ModuleSource, NodeId, ScopeId, ScopeKind, SemanticModel,
SemanticModelFlags, StarImport, SubmoduleImport,
};
use ruff_python_stdlib::builtins::{MAGIC_GLOBALS, python_builtins};
use ruff_python_stdlib::builtins::{python_builtins, python_magic_globals};
use ruff_python_trivia::CommentRanges;
use ruff_source_file::{OneIndexed, SourceFile, SourceFileBuilder, SourceRow};
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -2550,7 +2550,7 @@ impl<'a> Checker<'a> {
for builtin in standard_builtins {
bind_builtin(builtin);
}
for builtin in MAGIC_GLOBALS {
for builtin in python_magic_globals(target_version.minor) {
bind_builtin(builtin);
}
for builtin in &settings.builtins {

View File

@@ -341,6 +341,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Async, "221") => (RuleGroup::Stable, rules::flake8_async::rules::RunProcessInAsyncFunction),
(Flake8Async, "222") => (RuleGroup::Stable, rules::flake8_async::rules::WaitForProcessInAsyncFunction),
(Flake8Async, "230") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingOpenCallInAsyncFunction),
(Flake8Async, "240") => (RuleGroup::Preview, rules::flake8_async::rules::BlockingPathMethodInAsyncFunction),
(Flake8Async, "250") => (RuleGroup::Preview, rules::flake8_async::rules::BlockingInputInAsyncFunction),
(Flake8Async, "251") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingSleepInAsyncFunction),
@@ -394,6 +395,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Bugbear, "905") => (RuleGroup::Stable, rules::flake8_bugbear::rules::ZipWithoutExplicitStrict),
(Flake8Bugbear, "909") => (RuleGroup::Preview, rules::flake8_bugbear::rules::LoopIteratorMutation),
(Flake8Bugbear, "911") => (RuleGroup::Stable, rules::flake8_bugbear::rules::BatchedWithoutExplicitStrict),
(Flake8Bugbear, "912") => (RuleGroup::Preview, rules::flake8_bugbear::rules::MapWithoutExplicitStrict),
// flake8-blind-except
(Flake8BlindExcept, "001") => (RuleGroup::Stable, rules::flake8_blind_except::rules::BlindExcept),
@@ -1051,6 +1053,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "061") => (RuleGroup::Preview, rules::ruff::rules::LegacyFormPytestRaises),
(Ruff, "063") => (RuleGroup::Preview, rules::ruff::rules::AccessAnnotationsFromClassDict),
(Ruff, "064") => (RuleGroup::Preview, rules::ruff::rules::NonOctalPermissions),
(Ruff, "065") => (RuleGroup::Preview, rules::ruff::rules::LoggingEagerConversion),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),
(Ruff, "102") => (RuleGroup::Preview, rules::ruff::rules::InvalidRuleCode),

View File

@@ -65,7 +65,7 @@ pub(crate) fn remove_imports<'a>(
if member == "*" {
found_star = true;
} else {
bail!("Expected \"*\" for unused import (got: \"{}\")", member);
bail!("Expected \"*\" for unused import (got: \"{member}\")");
}
}
if !found_star {

View File

@@ -2,17 +2,24 @@ use std::collections::HashSet;
use std::io::Write;
use anyhow::Result;
use log::warn;
use serde::{Serialize, Serializer};
use serde_json::json;
use ruff_db::diagnostic::{Diagnostic, SecondaryCode};
use ruff_source_file::OneIndexed;
use ruff_source_file::{OneIndexed, SourceFile};
use ruff_text_size::{Ranged, TextRange};
use crate::VERSION;
use crate::fs::normalize_path;
use crate::message::{Emitter, EmitterContext};
use crate::registry::{Linter, RuleNamespace};
/// An emitter for producing SARIF 2.1.0-compliant JSON output.
///
/// Static Analysis Results Interchange Format (SARIF) is a standard format
/// for static analysis results. For full specfification, see:
/// [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html)
pub struct SarifEmitter;
impl Emitter for SarifEmitter {
@@ -29,7 +36,7 @@ impl Emitter for SarifEmitter {
let unique_rules: HashSet<_> = results
.iter()
.filter_map(|result| result.code.as_secondary_code())
.filter_map(|result| result.rule_id.as_secondary_code())
.collect();
let mut rules: Vec<SarifRule> = unique_rules.into_iter().map(SarifRule::from).collect();
rules.sort_by(|a, b| a.code.cmp(b.code));
@@ -134,6 +141,15 @@ impl RuleCode<'_> {
}
}
impl Serialize for RuleCode<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'a> From<&'a Diagnostic> for RuleCode<'a> {
fn from(code: &'a Diagnostic) -> Self {
match code.secondary_code() {
@@ -143,12 +159,83 @@ impl<'a> From<&'a Diagnostic> for RuleCode<'a> {
}
}
#[derive(Debug)]
/// Represents a single result in a SARIF 2.1.0 report.
///
/// See the SARIF 2.1.0 specification for details:
/// [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html)
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifResult<'a> {
code: RuleCode<'a>,
rule_id: RuleCode<'a>,
level: String,
message: String,
message: SarifMessage,
locations: Vec<SarifLocation>,
#[serde(skip_serializing_if = "Vec::is_empty")]
fixes: Vec<SarifFix>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifMessage {
text: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifPhysicalLocation {
artifact_location: SarifArtifactLocation,
region: SarifRegion,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifLocation {
physical_location: SarifPhysicalLocation,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifFix {
description: RuleDescription,
artifact_changes: Vec<SarifArtifactChange>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct RuleDescription {
text: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifArtifactChange {
artifact_location: SarifArtifactLocation,
replacements: Vec<SarifReplacement>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifArtifactLocation {
uri: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifReplacement {
deleted_region: SarifRegion,
#[serde(skip_serializing_if = "Option::is_none")]
inserted_content: Option<InsertedContent>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct InsertedContent {
text: String,
}
#[derive(Debug, Serialize, Clone, Copy)]
#[serde(rename_all = "camelCase")]
struct SarifRegion {
start_line: OneIndexed,
start_column: OneIndexed,
end_line: OneIndexed,
@@ -156,70 +243,107 @@ struct SarifResult<'a> {
}
impl<'a> SarifResult<'a> {
#[cfg(not(target_arch = "wasm32"))]
fn from_message(message: &'a Diagnostic) -> Result<Self> {
let start_location = message.ruff_start_location().unwrap_or_default();
let end_location = message.ruff_end_location().unwrap_or_default();
let path = normalize_path(&*message.expect_ruff_filename());
Ok(Self {
code: RuleCode::from(message),
level: "error".to_string(),
message: message.body().to_string(),
uri: url::Url::from_file_path(&path)
.map_err(|()| anyhow::anyhow!("Failed to convert path to URL: {}", path.display()))?
.to_string(),
fn range_to_sarif_region(source_file: &SourceFile, range: TextRange) -> SarifRegion {
let source_code = source_file.to_source_code();
let start_location = source_code.line_column(range.start());
let end_location = source_code.line_column(range.end());
SarifRegion {
start_line: start_location.line,
start_column: start_location.column,
end_line: end_location.line,
end_column: end_location.column,
})
}
}
#[cfg(target_arch = "wasm32")]
#[expect(clippy::unnecessary_wraps)]
fn from_message(message: &'a Diagnostic) -> Result<Self> {
let start_location = message.ruff_start_location().unwrap_or_default();
let end_location = message.ruff_end_location().unwrap_or_default();
let path = normalize_path(&*message.expect_ruff_filename());
Ok(Self {
code: RuleCode::from(message),
level: "error".to_string(),
message: message.body().to_string(),
uri: path.display().to_string(),
start_line: start_location.line,
start_column: start_location.column,
end_line: end_location.line,
end_column: end_location.column,
})
}
}
fn fix(diagnostic: &'a Diagnostic, uri: &str) -> Option<SarifFix> {
let fix = diagnostic.fix()?;
impl Serialize for SarifResult<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
json!({
"level": self.level,
"message": {
"text": self.message,
},
"locations": [{
"physicalLocation": {
"artifactLocation": {
"uri": self.uri,
},
"region": {
"startLine": self.start_line,
"startColumn": self.start_column,
"endLine": self.end_line,
"endColumn": self.end_column,
}
let Some(source_file) = diagnostic.ruff_source_file() else {
debug_assert!(
false,
"Omitting the fix for diagnostic with id `{}` because the source file is missing. This is a bug in Ruff, please report an issue.",
diagnostic.id()
);
warn!(
"Omitting the fix for diagnostic with id `{}` because the source file is missing. This is a bug in Ruff, please report an issue.",
diagnostic.id()
);
return None;
};
let fix_description = diagnostic
.first_help_text()
.map(std::string::ToString::to_string);
let replacements: Vec<SarifReplacement> = fix
.edits()
.iter()
.map(|edit| {
let range = edit.range();
let deleted_region = Self::range_to_sarif_region(source_file, range);
SarifReplacement {
deleted_region,
inserted_content: edit.content().map(|content| InsertedContent {
text: content.to_string(),
}),
}
}],
"ruleId": self.code.as_str(),
})
.collect();
let artifact_changes = vec![SarifArtifactChange {
artifact_location: SarifArtifactLocation {
uri: uri.to_string(),
},
replacements,
}];
Some(SarifFix {
description: RuleDescription {
text: fix_description,
},
artifact_changes,
})
}
#[allow(clippy::unnecessary_wraps)]
fn uri(diagnostic: &Diagnostic) -> Result<String> {
let path = normalize_path(&*diagnostic.expect_ruff_filename());
#[cfg(not(target_arch = "wasm32"))]
return url::Url::from_file_path(&path)
.map_err(|()| anyhow::anyhow!("Failed to convert path to URL: {}", path.display()))
.map(|u| u.to_string());
#[cfg(target_arch = "wasm32")]
return Ok(format!("file://{}", path.display()));
}
fn from_message(diagnostic: &'a Diagnostic) -> Result<Self> {
let start_location = diagnostic.ruff_start_location().unwrap_or_default();
let end_location = diagnostic.ruff_end_location().unwrap_or_default();
let region = SarifRegion {
start_line: start_location.line,
start_column: start_location.column,
end_line: end_location.line,
end_column: end_location.column,
};
let uri = Self::uri(diagnostic)?;
Ok(Self {
rule_id: RuleCode::from(diagnostic),
level: "error".to_string(),
message: SarifMessage {
text: diagnostic.body().to_string(),
},
fixes: Self::fix(diagnostic, &uri).into_iter().collect(),
locations: vec![SarifLocation {
physical_location: SarifPhysicalLocation {
artifact_location: SarifArtifactLocation { uri },
region,
},
}],
})
.serialize(serializer)
}
}
@@ -256,6 +380,7 @@ mod tests {
insta::assert_json_snapshot!(value, {
".runs[0].tool.driver.version" => "[VERSION]",
".runs[0].results[].locations[].physicalLocation.artifactLocation.uri" => "[URI]",
".runs[0].results[].fixes[].artifactChanges[].artifactLocation.uri" => "[URI]",
});
}
}

View File

@@ -8,6 +8,30 @@ expression: value
{
"results": [
{
"fixes": [
{
"artifactChanges": [
{
"artifactLocation": {
"uri": "[URI]"
},
"replacements": [
{
"deletedRegion": {
"endColumn": 1,
"endLine": 2,
"startColumn": 1,
"startLine": 1
}
}
]
}
],
"description": {
"text": "Remove unused import: `os`"
}
}
],
"level": "error",
"locations": [
{
@@ -30,6 +54,30 @@ expression: value
"ruleId": "F401"
},
{
"fixes": [
{
"artifactChanges": [
{
"artifactLocation": {
"uri": "[URI]"
},
"replacements": [
{
"deletedRegion": {
"endColumn": 10,
"endLine": 6,
"startColumn": 5,
"startLine": 6
}
}
]
}
],
"description": {
"text": "Remove assignment to unused variable `x`"
}
}
],
"level": "error",
"locations": [
{

View File

@@ -228,3 +228,10 @@ pub(crate) const fn is_sim910_expanded_key_support_enabled(settings: &LinterSett
pub(crate) const fn is_fix_builtin_open_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/20178
pub(crate) const fn is_a003_class_scope_shadowing_expansion_enabled(
settings: &LinterSettings,
) -> bool {
settings.preview.is_enabled()
}

View File

@@ -487,7 +487,6 @@ impl<'a> Iterator for PathParamIterator<'a> {
let param_name_end = param_content.find(':').unwrap_or(param_content.len());
let param_name = &param_content[..param_name_end];
#[expect(clippy::range_plus_one)]
return Some((param_name, start..end + 1));
}
}

View File

@@ -28,6 +28,7 @@ mod tests {
#[test_case(Rule::RunProcessInAsyncFunction, Path::new("ASYNC22x.py"))]
#[test_case(Rule::WaitForProcessInAsyncFunction, Path::new("ASYNC22x.py"))]
#[test_case(Rule::BlockingOpenCallInAsyncFunction, Path::new("ASYNC230.py"))]
#[test_case(Rule::BlockingPathMethodInAsyncFunction, Path::new("ASYNC240.py"))]
#[test_case(Rule::BlockingInputInAsyncFunction, Path::new("ASYNC250.py"))]
#[test_case(Rule::BlockingSleepInAsyncFunction, Path::new("ASYNC251.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {

View File

@@ -0,0 +1,246 @@
use crate::Violation;
use crate::checkers::ast::Checker;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Expr, ExprCall};
use ruff_python_semantic::analyze::typing::{TypeChecker, check_type, traverse_union_and_optional};
use ruff_text_size::Ranged;
/// ## What it does
/// Checks that async functions do not call blocking `os.path` or `pathlib.Path`
/// methods.
///
/// ## Why is this bad?
/// Calling some `os.path` or `pathlib.Path` methods in an async function will block
/// the entire event loop, preventing it from executing other tasks while waiting
/// for the operation. This negates the benefits of asynchronous programming.
///
/// Instead, use the methods' async equivalents from `trio.Path` or `anyio.Path`.
///
/// ## Example
/// ```python
/// import os
///
///
/// async def func():
/// path = "my_file.txt"
/// file_exists = os.path.exists(path)
/// ```
///
/// Use instead:
/// ```python
/// import trio
///
///
/// async def func():
/// path = trio.Path("my_file.txt")
/// file_exists = await path.exists()
/// ```
///
/// Non-blocking methods are OK to use:
/// ```python
/// import pathlib
///
///
/// async def func():
/// path = pathlib.Path("my_file.txt")
/// file_dirname = path.dirname()
/// new_path = os.path.join("/tmp/src/", path)
/// ```
#[derive(ViolationMetadata)]
pub(crate) struct BlockingPathMethodInAsyncFunction {
path_library: String,
}
impl Violation for BlockingPathMethodInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Async functions should not use {path_library} methods, use trio.Path or anyio.path",
path_library = self.path_library
)
}
}
/// ASYNC240
pub(crate) fn blocking_os_path(checker: &Checker, call: &ExprCall) {
let semantic = checker.semantic();
if !semantic.in_async_context() {
return;
}
// Check if an expression is calling I/O related os.path method.
// Just initializing pathlib.Path object is OK, we can return
// early in that scenario.
if let Some(qualified_name) = semantic.resolve_qualified_name(call.func.as_ref()) {
let segments = qualified_name.segments();
if !matches!(segments, ["os", "path", _]) {
return;
}
let Some(os_path_method) = segments.last() else {
return;
};
if maybe_calling_io_operation(os_path_method) {
checker.report_diagnostic(
BlockingPathMethodInAsyncFunction {
path_library: "os.path".to_string(),
},
call.func.range(),
);
}
return;
}
let Some(ast::ExprAttribute { value, attr, .. }) = call.func.as_attribute_expr() else {
return;
};
if !maybe_calling_io_operation(attr.id.as_str()) {
return;
}
// Check if an expression is a pathlib.Path constructor that directly
// calls an I/O method.
if PathlibPathChecker::match_initializer(value, semantic) {
checker.report_diagnostic(
BlockingPathMethodInAsyncFunction {
path_library: "pathlib.Path".to_string(),
},
call.func.range(),
);
return;
}
// Lastly, check if a variable is a pathlib.Path instance and it's
// calling an I/O method.
let Some(name) = value.as_name_expr() else {
return;
};
let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else {
return;
};
if check_type::<PathlibPathChecker>(binding, semantic) {
checker.report_diagnostic(
BlockingPathMethodInAsyncFunction {
path_library: "pathlib.Path".to_string(),
},
call.func.range(),
);
}
}
struct PathlibPathChecker;
impl PathlibPathChecker {
fn is_pathlib_path_constructor(
semantic: &ruff_python_semantic::SemanticModel,
expr: &Expr,
) -> bool {
let Some(qualified_name) = semantic.resolve_qualified_name(expr) else {
return false;
};
matches!(
qualified_name.segments(),
[
"pathlib",
"Path"
| "PosixPath"
| "PurePath"
| "PurePosixPath"
| "PureWindowsPath"
| "WindowsPath"
]
)
}
}
impl TypeChecker for PathlibPathChecker {
fn match_annotation(annotation: &Expr, semantic: &ruff_python_semantic::SemanticModel) -> bool {
if Self::is_pathlib_path_constructor(semantic, annotation) {
return true;
}
let mut found = false;
traverse_union_and_optional(
&mut |inner_expr, _| {
if Self::is_pathlib_path_constructor(semantic, inner_expr) {
found = true;
}
},
semantic,
annotation,
);
found
}
fn match_initializer(
initializer: &Expr,
semantic: &ruff_python_semantic::SemanticModel,
) -> bool {
let Expr::Call(ast::ExprCall { func, .. }) = initializer else {
return false;
};
Self::is_pathlib_path_constructor(semantic, func)
}
}
fn maybe_calling_io_operation(attr: &str) -> bool {
// ".open()" is added to the allow list to let ASYNC 230 handle
// that case.
!matches!(
attr,
"ALLOW_MISSING"
| "altsep"
| "anchor"
| "as_posix"
| "as_uri"
| "basename"
| "commonpath"
| "commonprefix"
| "curdir"
| "defpath"
| "devnull"
| "dirname"
| "drive"
| "expandvars"
| "extsep"
| "genericpath"
| "is_absolute"
| "is_relative_to"
| "is_reserved"
| "isabs"
| "join"
| "joinpath"
| "match"
| "name"
| "normcase"
| "os"
| "open"
| "pardir"
| "parent"
| "parents"
| "parts"
| "pathsep"
| "relative_to"
| "root"
| "samestat"
| "sep"
| "split"
| "splitdrive"
| "splitext"
| "splitroot"
| "stem"
| "suffix"
| "suffixes"
| "supports_unicode_filenames"
| "sys"
| "with_name"
| "with_segments"
| "with_stem"
| "with_suffix"
)
}

View File

@@ -5,6 +5,7 @@ pub(crate) use blocking_http_call::*;
pub(crate) use blocking_http_call_httpx::*;
pub(crate) use blocking_input::*;
pub(crate) use blocking_open_call::*;
pub(crate) use blocking_path_methods::*;
pub(crate) use blocking_process_invocation::*;
pub(crate) use blocking_sleep::*;
pub(crate) use cancel_scope_no_checkpoint::*;
@@ -18,6 +19,7 @@ mod blocking_http_call;
mod blocking_http_call_httpx;
mod blocking_input;
mod blocking_open_call;
mod blocking_path_methods;
mod blocking_process_invocation;
mod blocking_sleep;
mod cancel_scope_no_checkpoint;

View File

@@ -0,0 +1,111 @@
---
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
---
ASYNC240 Async functions should not use os.path methods, use trio.Path or anyio.path
--> ASYNC240.py:67:5
|
65 | file = "file.txt"
66 |
67 | os.path.abspath(file) # ASYNC240
| ^^^^^^^^^^^^^^^
68 | os.path.exists(file) # ASYNC240
|
ASYNC240 Async functions should not use os.path methods, use trio.Path or anyio.path
--> ASYNC240.py:68:5
|
67 | os.path.abspath(file) # ASYNC240
68 | os.path.exists(file) # ASYNC240
| ^^^^^^^^^^^^^^
69 |
70 | async def pathlib_path_in_foo():
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:72:5
|
70 | async def pathlib_path_in_foo():
71 | path = Path("src/my_text.txt")
72 | path.exists() # ASYNC240
| ^^^^^^^^^^^
73 |
74 | async def pathlib_path_in_foo():
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:78:5
|
77 | path = pathlib.Path("src/my_text.txt")
78 | path.exists() # ASYNC240
| ^^^^^^^^^^^
79 |
80 | async def inline_path_method_call():
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:81:5
|
80 | async def inline_path_method_call():
81 | Path("src/my_text.txt").exists() # ASYNC240
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
82 | Path("src/my_text.txt").absolute().exists() # ASYNC240
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:82:5
|
80 | async def inline_path_method_call():
81 | Path("src/my_text.txt").exists() # ASYNC240
82 | Path("src/my_text.txt").absolute().exists() # ASYNC240
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
83 |
84 | async def aliased_path_in_foo():
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:88:5
|
87 | path = PathAlias("src/my_text.txt")
88 | path.exists() # ASYNC240
| ^^^^^^^^^^^
89 |
90 | global_path = Path("src/my_text.txt")
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:93:5
|
92 | async def global_path_in_foo():
93 | global_path.exists() # ASYNC240
| ^^^^^^^^^^^^^^^^^^
94 |
95 | async def path_as_simple_parameter_type(path: Path):
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:96:5
|
95 | async def path_as_simple_parameter_type(path: Path):
96 | path.exists() # ASYNC240
| ^^^^^^^^^^^
97 |
98 | async def path_as_union_parameter_type(path: Path | None):
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:99:5
|
98 | async def path_as_union_parameter_type(path: Path | None):
99 | path.exists() # ASYNC240
| ^^^^^^^^^^^
100 |
101 | async def path_as_optional_parameter_type(path: Optional[Path]):
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:102:5
|
101 | async def path_as_optional_parameter_type(path: Optional[Path]):
102 | path.exists() # ASYNC240
| ^^^^^^^^^^^
|

View File

@@ -4,6 +4,7 @@ use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextRange};
use crate::Locator;
use ruff_python_ast::{self as ast, Arguments, Expr};
/// Return `true` if the statement containing the current expression is the last
/// top-level expression in the cell. This assumes that the source is a Jupyter
@@ -27,3 +28,54 @@ pub(super) fn at_last_top_level_expression_in_cell(
.all(|token| token.kind() == SimpleTokenKind::Semi || token.kind().is_trivia())
})
}
/// Return `true` if the [`Expr`] appears to be an infinite iterator (e.g., a call to
/// `itertools.cycle` or similar).
pub(crate) fn is_infinite_iterable(arg: &Expr, semantic: &SemanticModel) -> bool {
let Expr::Call(ast::ExprCall {
func,
arguments: Arguments { args, keywords, .. },
..
}) = &arg
else {
return false;
};
semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| match qualified_name.segments() {
["itertools", "cycle" | "count"] => true,
["itertools", "repeat"] => {
// Ex) `itertools.repeat(1)`
if keywords.is_empty() && args.len() == 1 {
return true;
}
// Ex) `itertools.repeat(1, None)`
if args.len() == 2 && args[1].is_none_literal_expr() {
return true;
}
// Ex) `itertools.repeat(1, times=None)`
for keyword in keywords {
if keyword.arg.as_ref().is_some_and(|name| name == "times")
&& keyword.value.is_none_literal_expr()
{
return true;
}
}
false
}
_ => false,
})
}
/// Return `true` if any expression in the iterator appears to be an infinite iterator.
pub(crate) fn any_infinite_iterables<'a>(
iter: impl IntoIterator<Item = &'a Expr>,
semantic: &SemanticModel,
) -> bool {
iter.into_iter()
.any(|arg| is_infinite_iterable(arg, semantic))
}

View File

@@ -16,6 +16,7 @@ mod tests {
use crate::settings::LinterSettings;
use crate::test::test_path;
use crate::settings::types::PreviewMode;
use ruff_python_ast::PythonVersion;
#[test_case(Rule::AbstractBaseClassWithoutAbstractMethod, Path::new("B024.py"))]
@@ -81,11 +82,35 @@ mod tests {
Ok(())
}
#[test_case(Rule::MapWithoutExplicitStrict, Path::new("B912.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,
unresolved_target_version: PythonVersion::PY314.into(),
..LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(
Rule::ClassAsDataStructure,
Path::new("class_as_data_structure.py"),
PythonVersion::PY39
)]
#[test_case(
Rule::MapWithoutExplicitStrict,
Path::new("B912.py"),
PythonVersion::PY313
)]
fn rules_with_target_version(
rule_code: Rule,
path: &Path,

View File

@@ -3,7 +3,7 @@ use ruff_python_ast::ExprCall;
use ruff_python_ast::PythonVersion;
use crate::checkers::ast::Checker;
use crate::rules::flake8_bugbear::rules::is_infinite_iterable;
use crate::rules::flake8_bugbear::helpers::is_infinite_iterable;
use crate::{FixAvailability, Violation};
/// ## What it does

View File

@@ -0,0 +1,89 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::edits::add_argument;
use crate::rules::flake8_bugbear::helpers::any_infinite_iterables;
use crate::{AlwaysFixableViolation, Applicability, Fix};
/// ## What it does
/// Checks for `map` calls without an explicit `strict` parameter when called with two or more iterables.
///
/// This rule applies to Python 3.14 and later, where `map` accepts a `strict` keyword
/// argument. For details, see: [Whats New in Python 3.14](https://docs.python.org/dev/whatsnew/3.14.html).
///
/// ## Why is this bad?
/// By default, if the iterables passed to `map` are of different lengths, the
/// resulting iterator will be silently truncated to the length of the shortest
/// iterable. This can lead to subtle bugs.
///
/// Pass `strict=True` to raise a `ValueError` if the iterables are of
/// non-uniform length. Alternatively, if the iterables are deliberately of
/// different lengths, pass `strict=False` to make the intention explicit.
///
/// ## Example
/// ```python
/// map(f, a, b)
/// ```
///
/// Use instead:
/// ```python
/// map(f, a, b, strict=True)
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe for `map` calls that contain
/// `**kwargs`, as adding a `strict` keyword argument to such a call may lead
/// to a duplicate keyword argument error.
///
/// ## References
/// - [Python documentation: `map`](https://docs.python.org/3/library/functions.html#map)
/// - [Whats New in Python 3.14](https://docs.python.org/dev/whatsnew/3.14.html)
#[derive(ViolationMetadata)]
pub(crate) struct MapWithoutExplicitStrict;
impl AlwaysFixableViolation for MapWithoutExplicitStrict {
#[derive_message_formats]
fn message(&self) -> String {
"`map()` without an explicit `strict=` parameter".to_string()
}
fn fix_title(&self) -> String {
"Add explicit value for parameter `strict=`".to_string()
}
}
/// B912
pub(crate) fn map_without_explicit_strict(checker: &Checker, call: &ast::ExprCall) {
let semantic = checker.semantic();
if semantic.match_builtin_expr(&call.func, "map")
&& call.arguments.find_keyword("strict").is_none()
&& call.arguments.args.len() >= 3 // function + at least 2 iterables
&& !any_infinite_iterables(call.arguments.args.iter().skip(1), semantic)
{
checker
.report_diagnostic(MapWithoutExplicitStrict, call.range())
.set_fix(Fix::applicable_edit(
add_argument(
"strict=False",
&call.arguments,
checker.comment_ranges(),
checker.locator().contents(),
),
// If the function call contains `**kwargs`, mark the fix as unsafe.
if call
.arguments
.keywords
.iter()
.any(|keyword| keyword.arg.is_none())
{
Applicability::Unsafe
} else {
Applicability::Safe
},
));
}
}

View File

@@ -16,6 +16,7 @@ pub(crate) use getattr_with_constant::*;
pub(crate) use jump_statement_in_finally::*;
pub(crate) use loop_iterator_mutation::*;
pub(crate) use loop_variable_overrides_iterator::*;
pub(crate) use map_without_explicit_strict::*;
pub(crate) use mutable_argument_default::*;
pub(crate) use mutable_contextvar_default::*;
pub(crate) use no_explicit_stacklevel::*;
@@ -56,6 +57,7 @@ mod getattr_with_constant;
mod jump_statement_in_finally;
mod loop_iterator_mutation;
mod loop_variable_overrides_iterator;
mod map_without_explicit_strict;
mod mutable_argument_default;
mod mutable_contextvar_default;
mod no_explicit_stacklevel;

View File

@@ -90,7 +90,11 @@ pub(crate) fn unreliable_callable_check(
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[ast::Keyword],
) {
if !keywords.is_empty() {
return;
}
let [obj, attr, ..] = args else {
return;
};
@@ -103,7 +107,21 @@ pub(crate) fn unreliable_callable_check(
let Some(builtins_function) = checker.semantic().resolve_builtin_symbol(func) else {
return;
};
if !matches!(builtins_function, "hasattr" | "getattr") {
// Validate function arguments based on function name
let valid_args = match builtins_function {
"hasattr" => {
// hasattr should have exactly 2 positional arguments and no keywords
args.len() == 2
}
"getattr" => {
// getattr should have 2 or 3 positional arguments and no keywords
args.len() == 2 || args.len() == 3
}
_ => return,
};
if !valid_args {
return;
}

View File

@@ -1,11 +1,11 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Arguments, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_python_ast::{self as ast};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::edits::add_argument;
use crate::rules::flake8_bugbear::helpers::any_infinite_iterables;
use crate::{AlwaysFixableViolation, Applicability, Fix};
/// ## What it does
@@ -57,11 +57,7 @@ pub(crate) fn zip_without_explicit_strict(checker: &Checker, call: &ast::ExprCal
if semantic.match_builtin_expr(&call.func, "zip")
&& call.arguments.find_keyword("strict").is_none()
&& !call
.arguments
.args
.iter()
.any(|arg| is_infinite_iterable(arg, semantic))
&& !any_infinite_iterables(call.arguments.args.iter(), semantic)
{
checker
.report_diagnostic(ZipWithoutExplicitStrict, call.range())
@@ -86,47 +82,3 @@ pub(crate) fn zip_without_explicit_strict(checker: &Checker, call: &ast::ExprCal
));
}
}
/// Return `true` if the [`Expr`] appears to be an infinite iterator (e.g., a call to
/// `itertools.cycle` or similar).
pub(crate) fn is_infinite_iterable(arg: &Expr, semantic: &SemanticModel) -> bool {
let Expr::Call(ast::ExprCall {
func,
arguments: Arguments { args, keywords, .. },
..
}) = &arg
else {
return false;
};
semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| {
match qualified_name.segments() {
["itertools", "cycle" | "count"] => true,
["itertools", "repeat"] => {
// Ex) `itertools.repeat(1)`
if keywords.is_empty() && args.len() == 1 {
return true;
}
// Ex) `itertools.repeat(1, None)`
if args.len() == 2 && args[1].is_none_literal_expr() {
return true;
}
// Ex) `iterools.repeat(1, times=None)`
for keyword in keywords {
if keyword.arg.as_ref().is_some_and(|name| name == "times") {
if keyword.value.is_none_literal_expr() {
return true;
}
}
}
false
}
_ => false,
}
})
}

View File

@@ -156,4 +156,6 @@ help: Replace with `callable()`
- assert hasattr(A(), "__call__")
53 + assert callable(A())
54 | assert callable(A()) is False
55 |
56 | # https://github.com/astral-sh/ruff/issues/20440
note: This is an unsafe fix and may change runtime behavior

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---

View File

@@ -0,0 +1,141 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:5:1
|
3 | # Errors
4 | map(lambda x: x, [1, 2, 3])
5 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6 | map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
|
help: Add explicit value for parameter `strict=`
2 |
3 | # Errors
4 | map(lambda x: x, [1, 2, 3])
- map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
5 + map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], strict=False)
6 | map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
8 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:6:1
|
4 | map(lambda x: x, [1, 2, 3])
5 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
6 | map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
8 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
|
help: Add explicit value for parameter `strict=`
3 | # Errors
4 | map(lambda x: x, [1, 2, 3])
5 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
- map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
6 + map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9], strict=False)
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
8 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
9 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:7:1
|
5 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
6 | map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
9 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
|
help: Add explicit value for parameter `strict=`
4 | map(lambda x: x, [1, 2, 3])
5 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
6 | map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
- map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
8 + map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
9 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
10 |
11 | # Errors (limited iterators).
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:9:1
|
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
8 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
9 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
10 |
11 | # Errors (limited iterators).
|
help: Add explicit value for parameter `strict=`
6 | map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
8 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
- map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
9 + map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True), strict=False)
10 |
11 | # Errors (limited iterators).
12 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1))
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:12:1
|
11 | # Errors (limited iterators).
12 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
13 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=4))
|
help: Add explicit value for parameter `strict=`
9 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
10 |
11 | # Errors (limited iterators).
- map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1))
12 + map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1), strict=False)
13 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=4))
14 |
15 | import builtins
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:13:1
|
11 | # Errors (limited iterators).
12 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1))
13 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=4))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
14 |
15 | import builtins
|
help: Add explicit value for parameter `strict=`
10 |
11 | # Errors (limited iterators).
12 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1))
- map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=4))
13 + map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=4), strict=False)
14 |
15 | import builtins
16 | # Still an error even though it uses the qualified name
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:17:1
|
15 | import builtins
16 | # Still an error even though it uses the qualified name
17 | builtins.map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
18 |
19 | # OK
|
help: Add explicit value for parameter `strict=`
14 |
15 | import builtins
16 | # Still an error even though it uses the qualified name
- builtins.map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
17 + builtins.map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], strict=False)
18 |
19 | # OK
20 | map(lambda x: x, [1, 2, 3], strict=True)

View File

@@ -14,6 +14,7 @@ mod tests {
use crate::registry::Rule;
use crate::rules::flake8_builtins;
use crate::settings::LinterSettings;
use crate::settings::types::PreviewMode;
use crate::test::{test_path, test_resource_path};
use ruff_python_ast::PythonVersion;
@@ -63,6 +64,28 @@ mod tests {
Ok(())
}
#[test_case(Rule::BuiltinAttributeShadowing, Path::new("A003.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_builtins").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Enabled,
flake8_builtins: flake8_builtins::settings::Settings {
strict_checking: true,
..Default::default()
},
..LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(
Rule::StdlibModuleShadowing,
Path::new("A005/modules/utils/logging.py"),

View File

@@ -6,6 +6,7 @@ use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::preview::is_a003_class_scope_shadowing_expansion_enabled;
use crate::rules::flake8_builtins::helpers::shadows_builtin;
/// ## What it does
@@ -123,16 +124,26 @@ pub(crate) fn builtin_attribute_shadowing(
// def repeat(value: int, times: int) -> list[int]:
// return [value] * times
// ```
// In stable, only consider references whose first non-type parent scope is the class
// scope (e.g., decorators, default args, and attribute initializers).
// In preview, also consider references from within the class scope.
let consider_reference = |reference_scope_id: ScopeId| {
if is_a003_class_scope_shadowing_expansion_enabled(checker.settings()) {
if reference_scope_id == scope_id {
return true;
}
}
checker
.semantic()
.first_non_type_parent_scope_id(reference_scope_id)
== Some(scope_id)
};
for reference in binding
.references
.iter()
.map(|reference_id| checker.semantic().reference(*reference_id))
.filter(|reference| {
checker
.semantic()
.first_non_type_parent_scope_id(reference.scope_id())
== Some(scope_id)
})
.filter(|reference| consider_reference(reference.scope_id()))
{
checker.report_diagnostic(
BuiltinAttributeShadowing {

View File

@@ -0,0 +1,50 @@
---
source: crates/ruff_linter/src/rules/flake8_builtins/mod.rs
---
A003 Python builtin is shadowed by method `str` from line 14
--> A003.py:17:31
|
15 | pass
16 |
17 | def method_usage(self) -> str:
| ^^^
18 | pass
|
A003 Python builtin is shadowed by class attribute `id` from line 3
--> A003.py:20:34
|
18 | pass
19 |
20 | def attribute_usage(self) -> id:
| ^^
21 | pass
|
A003 Python builtin is shadowed by method `property` from line 26
--> A003.py:31:7
|
29 | id = 1
30 |
31 | @[property][0]
| ^^^^^^^^
32 | def f(self, x=[id]):
33 | return x
|
A003 Python builtin is shadowed by class attribute `id` from line 29
--> A003.py:32:20
|
31 | @[property][0]
32 | def f(self, x=[id]):
| ^^
33 | return x
|
A003 Python builtin is shadowed by class attribute `bin` from line 35
--> A003.py:36:12
|
35 | bin = 2
36 | foo = [bin]
| ^^^
|

View File

@@ -124,7 +124,7 @@ pub(crate) fn unnecessary_literal_within_tuple_call(
let needs_trailing_comma = if let [item] = elts.as_slice() {
SimpleTokenizer::new(
checker.locator().contents(),
TextRange::new(item.end(), call.end()),
TextRange::new(item.end(), argument.end()),
)
.all(|token| token.kind != SimpleTokenKind::Comma)
} else {

View File

@@ -247,3 +247,36 @@ help: Rewrite as a tuple literal
28 | tuple([x for x in range(5)])
29 | tuple({x for x in range(10)})
note: This is an unsafe fix and may change runtime behavior
C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal)
--> C409.py:46:6
|
44 | )
45 |
46 | t9 = tuple([1],)
| ^^^^^^^^^^^
47 | t10 = tuple([1, 2],)
|
help: Rewrite as a tuple literal
43 | }
44 | )
45 |
- t9 = tuple([1],)
46 + t9 = (1,)
47 | t10 = tuple([1, 2],)
note: This is an unsafe fix and may change runtime behavior
C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal)
--> C409.py:47:7
|
46 | t9 = tuple([1],)
47 | t10 = tuple([1, 2],)
| ^^^^^^^^^^^^^^
|
help: Rewrite as a tuple literal
44 | )
45 |
46 | t9 = tuple([1],)
- t10 = tuple([1, 2],)
47 + t10 = (1, 2)
note: This is an unsafe fix and may change runtime behavior

View File

@@ -344,3 +344,36 @@ help: Rewrite as a generator
42 | x for x in [1,2,3]
43 | }
note: This is an unsafe fix and may change runtime behavior
C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal)
--> C409.py:46:6
|
44 | )
45 |
46 | t9 = tuple([1],)
| ^^^^^^^^^^^
47 | t10 = tuple([1, 2],)
|
help: Rewrite as a tuple literal
43 | }
44 | )
45 |
- t9 = tuple([1],)
46 + t9 = (1,)
47 | t10 = tuple([1, 2],)
note: This is an unsafe fix and may change runtime behavior
C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal)
--> C409.py:47:7
|
46 | t9 = tuple([1],)
47 | t10 = tuple([1, 2],)
| ^^^^^^^^^^^^^^
|
help: Rewrite as a tuple literal
44 | )
45 |
46 | t9 = tuple([1],)
- t10 = tuple([1, 2],)
47 + t10 = (1, 2)
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1,8 +1,9 @@
use ruff_python_ast::InterpolatedStringElement;
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Operator, StringFlags};
use ruff_python_semantic::analyze::logging;
use ruff_python_stdlib::logging::LoggingLevel;
use ruff_text_size::Ranged;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::preview::is_fix_f_string_logging_enabled;
@@ -198,7 +199,7 @@ fn check_log_record_attr_clash(checker: &Checker, extra: &Keyword) {
}
#[derive(Debug, Copy, Clone)]
enum LoggingCallType {
pub(crate) enum LoggingCallType {
/// Logging call with a level method, e.g., `logging.info`.
LevelCall(LoggingLevel),
/// Logging call with an integer level as an argument, e.g., `logger.log(level, ...)`.
@@ -215,39 +216,41 @@ impl LoggingCallType {
}
}
/// Check logging calls for violations.
pub(crate) fn logging_call(checker: &Checker, call: &ast::ExprCall) {
pub(crate) fn find_logging_call(
checker: &Checker,
call: &ast::ExprCall,
) -> Option<(LoggingCallType, TextRange)> {
// Determine the call type (e.g., `info` vs. `exception`) and the range of the attribute.
let (logging_call_type, range) = match call.func.as_ref() {
match call.func.as_ref() {
Expr::Attribute(ast::ExprAttribute { value: _, attr, .. }) => {
let Some(call_type) = LoggingCallType::from_attribute(attr.as_str()) else {
return;
};
let call_type = LoggingCallType::from_attribute(attr.as_str())?;
if !logging::is_logger_candidate(
&call.func,
checker.semantic(),
&checker.settings().logger_objects,
) {
return;
return None;
}
(call_type, attr.range())
Some((call_type, attr.range()))
}
Expr::Name(_) => {
let Some(qualified_name) = checker
let qualified_name = checker
.semantic()
.resolve_qualified_name(call.func.as_ref())
else {
return;
};
.resolve_qualified_name(call.func.as_ref())?;
let ["logging", attribute] = qualified_name.segments() else {
return;
return None;
};
let Some(call_type) = LoggingCallType::from_attribute(attribute) else {
return;
};
(call_type, call.func.range())
let call_type = LoggingCallType::from_attribute(attribute)?;
Some((call_type, call.func.range()))
}
_ => return,
_ => None,
}
}
/// Check logging calls for violations.
pub(crate) fn logging_call(checker: &Checker, call: &ast::ExprCall) {
let Some((logging_call_type, range)) = find_logging_call(checker, call) else {
return;
};
// G001, G002, G003, G004

View File

@@ -1,5 +1,3 @@
use std::cmp::Ordering;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::StringFlags;
use ruff_python_ast::{
@@ -7,6 +5,8 @@ use ruff_python_ast::{
StringLiteralValue, UnaryOp, str::TripleQuotes,
};
use ruff_text_size::{Ranged, TextRange};
use std::cmp::Ordering;
use std::fmt::{Display, Formatter};
use crate::checkers::ast::Checker;
use crate::preview::is_maxsplit_without_separator_fix_enabled;
@@ -47,14 +47,40 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
/// ## References
/// - [Python documentation: `str.split`](https://docs.python.org/3/library/stdtypes.html#str.split)
#[derive(ViolationMetadata)]
pub(crate) struct SplitStaticString;
pub(crate) struct SplitStaticString {
method: Method,
}
#[derive(Copy, Clone, Debug)]
enum Method {
Split,
RSplit,
}
impl Method {
fn is_rsplit(self) -> bool {
matches!(self, Method::RSplit)
}
}
impl Display for Method {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Method::Split => f.write_str("split"),
Method::RSplit => f.write_str("rsplit"),
}
}
}
impl Violation for SplitStaticString {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"Consider using a list literal instead of `str.split`".to_string()
format!(
"Consider using a list literal instead of `str.{}`",
self.method
)
}
fn fix_title(&self) -> Option<String> {
@@ -77,26 +103,21 @@ pub(crate) fn split_static_string(
};
// `split` vs `rsplit`.
let direction = if attr == "split" {
Direction::Left
let method = if attr == "split" {
Method::Split
} else {
Direction::Right
Method::RSplit
};
let sep_arg = arguments.find_argument_value("sep", 0);
let split_replacement = if let Some(sep) = sep_arg {
match sep {
Expr::NoneLiteral(_) => {
split_default(str_value, maxsplit_value, direction, checker.settings())
split_default(str_value, maxsplit_value, method, checker.settings())
}
Expr::StringLiteral(sep_value) => {
let sep_value_str = sep_value.value.to_str();
Some(split_sep(
str_value,
sep_value_str,
maxsplit_value,
direction,
))
Some(split_sep(str_value, sep_value_str, maxsplit_value, method))
}
// Ignore names until type inference is available.
_ => {
@@ -104,10 +125,10 @@ pub(crate) fn split_static_string(
}
}
} else {
split_default(str_value, maxsplit_value, direction, checker.settings())
split_default(str_value, maxsplit_value, method, checker.settings())
};
let mut diagnostic = checker.report_diagnostic(SplitStaticString, call.range());
let mut diagnostic = checker.report_diagnostic(SplitStaticString { method }, call.range());
if let Some(ref replacement_expr) = split_replacement {
diagnostic.set_fix(Fix::applicable_edit(
Edit::range_replacement(checker.generator().expr(replacement_expr), call.range()),
@@ -177,68 +198,22 @@ fn construct_replacement(elts: &[&str], flags: StringLiteralFlags) -> Expr {
fn split_default(
str_value: &StringLiteralValue,
max_split: i32,
direction: Direction,
method: Method,
settings: &LinterSettings,
) -> Option<Expr> {
// From the Python documentation:
// > If sep is not specified or is None, a different splitting algorithm is applied: runs of
// > consecutive whitespace are regarded as a single separator, and the result will contain
// > no empty strings at the start or end if the string has leading or trailing whitespace.
// > Consequently, splitting an empty string or a string consisting of just whitespace with
// > a None separator returns [].
// https://docs.python.org/3/library/stdtypes.html#str.split
let string_val = str_value.to_str();
match max_split.cmp(&0) {
Ordering::Greater => {
if !is_maxsplit_without_separator_fix_enabled(settings) {
return None;
}
Ordering::Greater if !is_maxsplit_without_separator_fix_enabled(settings) => None,
Ordering::Greater | Ordering::Equal => {
let Ok(max_split) = usize::try_from(max_split) else {
return None;
};
let list_items: Vec<&str> = if direction == Direction::Left {
string_val
.trim_start_matches(py_unicode_is_whitespace)
.splitn(max_split + 1, py_unicode_is_whitespace)
.filter(|s| !s.is_empty())
.collect()
} else {
let mut items: Vec<&str> = string_val
.trim_end_matches(py_unicode_is_whitespace)
.rsplitn(max_split + 1, py_unicode_is_whitespace)
.filter(|s| !s.is_empty())
.collect();
items.reverse();
items
};
let list_items = split_whitespace_with_maxsplit(string_val, max_split, method);
Some(construct_replacement(
&list_items,
str_value.first_literal_flags(),
))
}
Ordering::Equal => {
// Behavior for maxsplit = 0 when sep is None:
// - If the string is empty or all whitespace, result is [].
// - Otherwise:
// - " x ".split(maxsplit=0) -> ['x ']
// - " x ".rsplit(maxsplit=0) -> [' x']
// - "".split(maxsplit=0) -> []
// - " ".split(maxsplit=0) -> []
let processed_str = if direction == Direction::Left {
string_val.trim_start_matches(py_unicode_is_whitespace)
} else {
string_val.trim_end_matches(py_unicode_is_whitespace)
};
let list_items: &[_] = if processed_str.is_empty() {
&[]
} else {
&[processed_str]
};
Some(construct_replacement(
list_items,
str_value.first_literal_flags(),
))
}
Ordering::Less => {
let list_items: Vec<&str> = string_val
.split(py_unicode_is_whitespace)
@@ -256,22 +231,22 @@ fn split_sep(
str_value: &StringLiteralValue,
sep_value: &str,
max_split: i32,
direction: Direction,
method: Method,
) -> Expr {
let value = str_value.to_str();
let list_items: Vec<&str> = if let Ok(split_n) = usize::try_from(max_split) {
match direction {
Direction::Left => value.splitn(split_n + 1, sep_value).collect(),
Direction::Right => {
match method {
Method::Split => value.splitn(split_n + 1, sep_value).collect(),
Method::RSplit => {
let mut items: Vec<&str> = value.rsplitn(split_n + 1, sep_value).collect();
items.reverse();
items
}
}
} else {
match direction {
Direction::Left => value.split(sep_value).collect(),
Direction::Right => {
match method {
Method::Split => value.split(sep_value).collect(),
Method::RSplit => {
let mut items: Vec<&str> = value.rsplit(sep_value).collect();
items.reverse();
items
@@ -316,12 +291,6 @@ fn get_maxsplit_value(arg: Option<&Expr>) -> Option<i32> {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Direction {
Left,
Right,
}
/// Like [`char::is_whitespace`] but with Python's notion of whitespace.
///
/// <https://github.com/astral-sh/ruff/issues/19845>
@@ -352,3 +321,107 @@ const fn py_unicode_is_whitespace(ch: char) -> bool {
| '\u{3000}'
)
}
struct WhitespaceMaxSplitIterator<'a> {
remaining: &'a str,
max_split: usize,
splits: usize,
method: Method,
}
impl<'a> WhitespaceMaxSplitIterator<'a> {
fn new(s: &'a str, max_split: usize, method: Method) -> Self {
let remaining = match method {
Method::Split => s.trim_start_matches(py_unicode_is_whitespace),
Method::RSplit => s.trim_end_matches(py_unicode_is_whitespace),
};
Self {
remaining,
max_split,
splits: 0,
method,
}
}
}
impl<'a> Iterator for WhitespaceMaxSplitIterator<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
if self.remaining.is_empty() {
return None;
}
if self.splits >= self.max_split {
let result = self.remaining;
self.remaining = "";
return Some(result);
}
self.splits += 1;
match self.method {
Method::Split => match self.remaining.split_once(py_unicode_is_whitespace) {
Some((s, remaining)) => {
self.remaining = remaining.trim_start_matches(py_unicode_is_whitespace);
Some(s)
}
None => Some(std::mem::take(&mut self.remaining)),
},
Method::RSplit => match self.remaining.rsplit_once(py_unicode_is_whitespace) {
Some((remaining, s)) => {
self.remaining = remaining.trim_end_matches(py_unicode_is_whitespace);
Some(s)
}
None => Some(std::mem::take(&mut self.remaining)),
},
}
}
}
// From the Python documentation:
// > If sep is not specified or is None, a different splitting algorithm is applied: runs of
// > consecutive whitespace are regarded as a single separator, and the result will contain
// > no empty strings at the start or end if the string has leading or trailing whitespace.
// > Consequently, splitting an empty string or a string consisting of just whitespace with
// > a None separator returns [].
// https://docs.python.org/3/library/stdtypes.html#str.split
fn split_whitespace_with_maxsplit(s: &str, max_split: usize, method: Method) -> Vec<&str> {
let mut result: Vec<_> = WhitespaceMaxSplitIterator::new(s, max_split, method).collect();
if method.is_rsplit() {
result.reverse();
}
result
}
#[cfg(test)]
mod tests {
use super::{Method, split_whitespace_with_maxsplit};
use test_case::test_case;
#[test_case(" ", 1, &[])]
#[test_case("a b", 1, &["a", "b"])]
#[test_case("a b", 2, &["a", "b"])]
#[test_case(" a b c d ", 2, &["a", "b", "c d "])]
#[test_case(" a b c ", 1, &["a", "b c "])]
#[test_case(" x ", 0, &["x "])]
#[test_case(" ", 0, &[])]
#[test_case("a\u{3000}b", 1, &["a", "b"])]
fn test_split_whitespace_with_maxsplit(s: &str, max_split: usize, expected: &[&str]) {
let parts = split_whitespace_with_maxsplit(s, max_split, Method::Split);
assert_eq!(parts, expected);
}
#[test_case(" ", 1, &[])]
#[test_case("a b", 1, &["a", "b"])]
#[test_case("a b", 2, &["a", "b"])]
#[test_case(" a b c d ", 2, &[" a b", "c", "d"])]
#[test_case(" a b c ", 1, &[" a b", "c"])]
#[test_case(" x ", 0, &[" x"])]
#[test_case(" ", 0, &[])]
#[test_case("a\u{3000}b", 1, &["a", "b"])]
fn test_rsplit_whitespace_with_maxsplit(s: &str, max_split: usize, expected: &[&str]) {
let parts = split_whitespace_with_maxsplit(s, max_split, Method::RSplit);
assert_eq!(parts, expected);
}
}

View File

@@ -846,7 +846,7 @@ help: Replace with list literal
105 | # https://github.com/astral-sh/ruff/issues/18042
106 | print("a,b".rsplit(","))
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:111:7
|
110 | # https://github.com/astral-sh/ruff/issues/18042
@@ -864,7 +864,7 @@ help: Replace with list literal
113 |
114 | # https://github.com/astral-sh/ruff/issues/18069
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:112:7
|
110 | # https://github.com/astral-sh/ruff/issues/18042
@@ -1043,7 +1043,7 @@ help: Replace with list literal
125 | print("".rsplit(sep=None, maxsplit=0))
126 | print(" ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:124:7
|
122 | print(" x ".split(maxsplit=0))
@@ -1063,7 +1063,7 @@ help: Replace with list literal
126 | print(" ".rsplit(maxsplit=0))
127 | print(" ".rsplit(sep=None, maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:125:7
|
123 | print(" x ".split(sep=None, maxsplit=0))
@@ -1083,7 +1083,7 @@ help: Replace with list literal
127 | print(" ".rsplit(sep=None, maxsplit=0))
128 | print(" x ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:126:7
|
124 | print("".rsplit(maxsplit=0))
@@ -1103,7 +1103,7 @@ help: Replace with list literal
128 | print(" x ".rsplit(maxsplit=0))
129 | print(" x ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:127:7
|
125 | print("".rsplit(sep=None, maxsplit=0))
@@ -1123,7 +1123,7 @@ help: Replace with list literal
129 | print(" x ".rsplit(maxsplit=0))
130 | print(" x ".rsplit(sep=None, maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:128:7
|
126 | print(" ".rsplit(maxsplit=0))
@@ -1143,7 +1143,7 @@ help: Replace with list literal
130 | print(" x ".rsplit(sep=None, maxsplit=0))
131 | print(" x ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:129:7
|
127 | print(" ".rsplit(sep=None, maxsplit=0))
@@ -1163,7 +1163,7 @@ help: Replace with list literal
131 | print(" x ".rsplit(maxsplit=0))
132 | print(" x ".rsplit(sep=None, maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:130:7
|
128 | print(" x ".rsplit(maxsplit=0))
@@ -1183,7 +1183,7 @@ help: Replace with list literal
132 | print(" x ".rsplit(sep=None, maxsplit=0))
133 |
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:131:7
|
129 | print(" x ".rsplit(maxsplit=0))
@@ -1202,7 +1202,7 @@ help: Replace with list literal
133 |
134 | # https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:132:7
|
130 | print(" x ".rsplit(sep=None, maxsplit=0))
@@ -1345,7 +1345,7 @@ help: Replace with list literal
169 |
170 | # leading/trailing whitespace should not count towards maxsplit
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:168:7
|
166 | print("S\x1cP\x1dL\x1eI\x1fT".split())
@@ -1372,15 +1372,27 @@ SIM905 Consider using a list literal instead of `str.split`
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
173 | "a b".split(maxsplit=1) # ["a", "b"]
|
help: Replace with list literal
SIM905 Consider using a list literal instead of `str.split`
SIM905 Consider using a list literal instead of `str.rsplit`
--> SIM905.py:172:1
|
170 | # leading/trailing whitespace should not count towards maxsplit
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
173 | "a b".split(maxsplit=1) # ["a", "b"]
|
help: Replace with list literal
SIM905 Consider using a list literal instead of `str.split`
--> SIM905.py:173:1
|
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
173 | "a b".split(maxsplit=1) # ["a", "b"]
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Replace with list literal

View File

@@ -894,7 +894,7 @@ help: Replace with list literal
105 | # https://github.com/astral-sh/ruff/issues/18042
106 | print("a,b".rsplit(","))
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:111:7
|
110 | # https://github.com/astral-sh/ruff/issues/18042
@@ -912,7 +912,7 @@ help: Replace with list literal
113 |
114 | # https://github.com/astral-sh/ruff/issues/18069
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:112:7
|
110 | # https://github.com/astral-sh/ruff/issues/18042
@@ -1091,7 +1091,7 @@ help: Replace with list literal
125 | print("".rsplit(sep=None, maxsplit=0))
126 | print(" ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:124:7
|
122 | print(" x ".split(maxsplit=0))
@@ -1111,7 +1111,7 @@ help: Replace with list literal
126 | print(" ".rsplit(maxsplit=0))
127 | print(" ".rsplit(sep=None, maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:125:7
|
123 | print(" x ".split(sep=None, maxsplit=0))
@@ -1131,7 +1131,7 @@ help: Replace with list literal
127 | print(" ".rsplit(sep=None, maxsplit=0))
128 | print(" x ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:126:7
|
124 | print("".rsplit(maxsplit=0))
@@ -1151,7 +1151,7 @@ help: Replace with list literal
128 | print(" x ".rsplit(maxsplit=0))
129 | print(" x ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:127:7
|
125 | print("".rsplit(sep=None, maxsplit=0))
@@ -1171,7 +1171,7 @@ help: Replace with list literal
129 | print(" x ".rsplit(maxsplit=0))
130 | print(" x ".rsplit(sep=None, maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:128:7
|
126 | print(" ".rsplit(maxsplit=0))
@@ -1191,7 +1191,7 @@ help: Replace with list literal
130 | print(" x ".rsplit(sep=None, maxsplit=0))
131 | print(" x ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:129:7
|
127 | print(" ".rsplit(sep=None, maxsplit=0))
@@ -1211,7 +1211,7 @@ help: Replace with list literal
131 | print(" x ".rsplit(maxsplit=0))
132 | print(" x ".rsplit(sep=None, maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:130:7
|
128 | print(" x ".rsplit(maxsplit=0))
@@ -1231,7 +1231,7 @@ help: Replace with list literal
132 | print(" x ".rsplit(sep=None, maxsplit=0))
133 |
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:131:7
|
129 | print(" x ".rsplit(maxsplit=0))
@@ -1250,7 +1250,7 @@ help: Replace with list literal
133 |
134 | # https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:132:7
|
130 | print(" x ".rsplit(sep=None, maxsplit=0))
@@ -1393,7 +1393,7 @@ help: Replace with list literal
169 |
170 | # leading/trailing whitespace should not count towards maxsplit
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:168:7
|
166 | print("S\x1cP\x1dL\x1eI\x1fT".split())
@@ -1420,6 +1420,7 @@ SIM905 [*] Consider using a list literal instead of `str.split`
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
173 | "a b".split(maxsplit=1) # ["a", "b"]
|
help: Replace with list literal
168 | print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
@@ -1428,14 +1429,16 @@ help: Replace with list literal
- " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
171 + ["a", "b", "c d "] # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
173 | "a b".split(maxsplit=1) # ["a", "b"]
SIM905 [*] Consider using a list literal instead of `str.split`
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:172:1
|
170 | # leading/trailing whitespace should not count towards maxsplit
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
173 | "a b".split(maxsplit=1) # ["a", "b"]
|
help: Replace with list literal
169 |
@@ -1443,3 +1446,19 @@ help: Replace with list literal
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
- " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
172 + [" a b", "c", "d"] # [" a b", "c", "d"]
173 | "a b".split(maxsplit=1) # ["a", "b"]
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:173:1
|
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
173 | "a b".split(maxsplit=1) # ["a", "b"]
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Replace with list literal
170 | # leading/trailing whitespace should not count towards maxsplit
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
- "a b".split(maxsplit=1) # ["a", "b"]
173 + ["a", "b"] # ["a", "b"]

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Expr, ExprCall};
use ruff_python_ast::{self as ast, Arguments, Expr, ExprCall};
use ruff_python_semantic::{SemanticModel, analyze::typing};
use ruff_text_size::Ranged;
@@ -6,7 +6,7 @@ use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::{Applicability, Edit, Fix, Violation};
pub(crate) fn is_keyword_only_argument_non_default(arguments: &ast::Arguments, name: &str) -> bool {
pub(crate) fn is_keyword_only_argument_non_default(arguments: &Arguments, name: &str) -> bool {
arguments
.find_keyword(name)
.is_some_and(|keyword| !keyword.value.is_none_literal_expr())
@@ -24,10 +24,7 @@ pub(crate) fn is_pathlib_path_call(checker: &Checker, expr: &Expr) -> bool {
/// Check if the given segments represent a pathlib Path subclass or `PackagePath` with preview mode support.
/// In stable mode, only checks for `Path` and `PurePath`. In preview mode, also checks for
/// `PosixPath`, `PurePosixPath`, `WindowsPath`, `PureWindowsPath`, and `PackagePath`.
pub(crate) fn is_pure_path_subclass_with_preview(
checker: &crate::checkers::ast::Checker,
segments: &[&str],
) -> bool {
pub(crate) fn is_pure_path_subclass_with_preview(checker: &Checker, segments: &[&str]) -> bool {
let is_core_pathlib = matches!(segments, ["pathlib", "Path" | "PurePath"]);
if is_core_pathlib {
@@ -193,7 +190,7 @@ pub(crate) fn check_os_pathlib_two_arg_calls(
}
pub(crate) fn has_unknown_keywords_or_starred_expr(
arguments: &ast::Arguments,
arguments: &Arguments,
allowed: &[&str],
) -> bool {
if arguments.args.iter().any(Expr::is_starred_expr) {
@@ -207,11 +204,7 @@ pub(crate) fn has_unknown_keywords_or_starred_expr(
}
/// Returns `true` if argument `name` is set to a non-default `None` value.
pub(crate) fn is_argument_non_default(
arguments: &ast::Arguments,
name: &str,
position: usize,
) -> bool {
pub(crate) fn is_argument_non_default(arguments: &Arguments, name: &str, position: usize) -> bool {
arguments
.find_argument_value(name, position)
.is_some_and(|expr| !expr.is_none_literal_expr())

View File

@@ -1,11 +1,16 @@
use ruff_diagnostics::{Applicability, Edit, Fix};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{ArgOrKeyword, ExprCall};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::preview::is_fix_os_chmod_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_two_arg_calls, is_file_descriptor, is_keyword_only_argument_non_default,
has_unknown_keywords_or_starred_expr, is_file_descriptor, is_keyword_only_argument_non_default,
is_pathlib_path_call,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.chmod`.
@@ -73,22 +78,80 @@ pub(crate) fn os_chmod(checker: &Checker, call: &ExprCall, segments: &[&str]) {
// 0 1 2 3
// os.chmod(path, mode, *, dir_fd=None, follow_symlinks=True)
// ```
if call
.arguments
.find_argument_value("path", 0)
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
let path_arg = call.arguments.find_argument_value("path", 0);
if path_arg.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
|| is_keyword_only_argument_non_default(&call.arguments, "dir_fd")
{
return;
}
check_os_pathlib_two_arg_calls(
checker,
call,
"chmod",
"path",
"mode",
is_fix_os_chmod_enabled(checker.settings()),
OsChmod,
);
let range = call.range();
let mut diagnostic = checker.report_diagnostic(OsChmod, call.func.range());
if !is_fix_os_chmod_enabled(checker.settings()) {
return;
}
if call.arguments.len() < 2 {
return;
}
if has_unknown_keywords_or_starred_expr(
&call.arguments,
&["path", "mode", "dir_fd", "follow_symlinks"],
) {
return;
}
let (Some(path_arg), Some(_)) = (path_arg, call.arguments.find_argument_value("mode", 1))
else {
return;
};
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
call.start(),
checker.semantic(),
)?;
let locator = checker.locator();
let path_code = locator.slice(path_arg.range());
let args = |arg: ArgOrKeyword| match arg {
ArgOrKeyword::Arg(expr) if expr.range() != path_arg.range() => {
Some(locator.slice(expr.range()))
}
ArgOrKeyword::Keyword(kw)
if matches!(kw.arg.as_deref(), Some("mode" | "follow_symlinks")) =>
{
Some(locator.slice(kw.range()))
}
_ => None,
};
let chmod_args = itertools::join(
call.arguments.arguments_source_order().filter_map(args),
", ",
);
let replacement = if is_pathlib_path_call(checker, path_arg) {
format!("{path_code}.chmod({chmod_args})")
} else {
format!("{binding}({path_code}).chmod({chmod_args})")
};
let applicability = if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
Ok(Fix::applicable_edits(
Edit::range_replacement(replacement, range),
[import_edit],
applicability,
))
});
}

View File

@@ -1,6 +1,8 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_samefile_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_two_arg_calls;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -65,13 +67,16 @@ pub(crate) fn os_path_samefile(checker: &Checker, call: &ExprCall, segments: &[&
return;
}
let fix_enabled = is_fix_os_path_samefile_enabled(checker.settings())
&& !has_unknown_keywords_or_starred_expr(&call.arguments, &["f1", "f2"]);
check_os_pathlib_two_arg_calls(
checker,
call,
"samefile",
"f1",
"f2",
is_fix_os_path_samefile_enabled(checker.settings()),
fix_enabled,
OsPathSamefile,
);
}

View File

@@ -1,7 +1,8 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_rename_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_two_arg_calls, is_keyword_only_argument_non_default,
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
@@ -79,13 +80,11 @@ pub(crate) fn os_rename(checker: &Checker, call: &ExprCall, segments: &[&str]) {
return;
}
check_os_pathlib_two_arg_calls(
checker,
call,
"rename",
"src",
"dst",
is_fix_os_rename_enabled(checker.settings()),
OsRename,
);
let fix_enabled = is_fix_os_rename_enabled(checker.settings())
&& !has_unknown_keywords_or_starred_expr(
&call.arguments,
&["src", "dst", "src_dir_fd", "dst_dir_fd"],
);
check_os_pathlib_two_arg_calls(checker, call, "rename", "src", "dst", fix_enabled, OsRename);
}

View File

@@ -1,7 +1,8 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_replace_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_two_arg_calls, is_keyword_only_argument_non_default,
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
@@ -82,13 +83,19 @@ pub(crate) fn os_replace(checker: &Checker, call: &ExprCall, segments: &[&str])
return;
}
let fix_enabled = is_fix_os_replace_enabled(checker.settings())
&& !has_unknown_keywords_or_starred_expr(
&call.arguments,
&["src", "dst", "src_dir_fd", "dst_dir_fd"],
);
check_os_pathlib_two_arg_calls(
checker,
call,
"replace",
"src",
"dst",
is_fix_os_replace_enabled(checker.settings()),
fix_enabled,
OsReplace,
);
}

View File

@@ -104,14 +104,6 @@ pub(crate) fn os_symlink(checker: &Checker, call: &ExprCall, segments: &[&str])
return;
};
let target_is_directory_arg = call.arguments.find_argument_value("target_is_directory", 2);
if let Some(expr) = &target_is_directory_arg {
if expr.as_boolean_literal_expr().is_none() {
return;
}
}
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
@@ -129,7 +121,9 @@ pub(crate) fn os_symlink(checker: &Checker, call: &ExprCall, segments: &[&str])
let src_code = locator.slice(src.range());
let dst_code = locator.slice(dst.range());
let target_is_directory = target_is_directory_arg
let target_is_directory = call
.arguments
.find_argument_value("target_is_directory", 2)
.and_then(|expr| {
let code = locator.slice(expr.range());
expr.as_boolean_literal_expr()

View File

@@ -500,5 +500,72 @@ PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
126 |
127 | os.makedirs("name", unknown_kwarg=True)
| ^^^^^^^^^^^
128 |
129 | # https://github.com/astral-sh/ruff/issues/20134
|
help: Replace with `Path(...).mkdir(parents=True)`
PTH101 `os.chmod()` should be replaced by `Path.chmod()`
--> full_name.py:130:1
|
129 | # https://github.com/astral-sh/ruff/issues/20134
130 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
| ^^^^^^^^
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
|
help: Replace with `Path(...).chmod(...)`
PTH101 `os.chmod()` should be replaced by `Path.chmod()`
--> full_name.py:131:1
|
129 | # https://github.com/astral-sh/ruff/issues/20134
130 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
| ^^^^^^^^
132 |
133 | # Only diagnostic
|
help: Replace with `Path(...).chmod(...)`
PTH101 `os.chmod()` should be replaced by `Path.chmod()`
--> full_name.py:134:1
|
133 | # Only diagnostic
134 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^
135 |
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
|
help: Replace with `Path(...).chmod(...)`
PTH104 `os.rename()` should be replaced by `Path.rename()`
--> full_name.py:136:1
|
134 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
135 |
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^^
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
|
help: Replace with `Path(...).rename(...)`
PTH105 `os.replace()` should be replaced by `Path.replace()`
--> full_name.py:137:1
|
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^^^
138 |
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
|
help: Replace with `Path(...).replace(...)`
PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
--> full_name.py:139:1
|
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
138 |
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^^^^^^^^^
|
help: Replace with `Path(...).samefile()`

View File

@@ -931,6 +931,7 @@ help: Replace with `Path(...).mkdir(parents=True)`
126 + pathlib.Path("name").mkdir(mode=0o777, exist_ok=False, parents=True)
127 |
128 | os.makedirs("name", unknown_kwarg=True)
129 |
PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
--> full_name.py:127:1
@@ -939,5 +940,102 @@ PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
126 |
127 | os.makedirs("name", unknown_kwarg=True)
| ^^^^^^^^^^^
128 |
129 | # https://github.com/astral-sh/ruff/issues/20134
|
help: Replace with `Path(...).mkdir(parents=True)`
PTH101 [*] `os.chmod()` should be replaced by `Path.chmod()`
--> full_name.py:130:1
|
129 | # https://github.com/astral-sh/ruff/issues/20134
130 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
| ^^^^^^^^
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
|
help: Replace with `Path(...).chmod(...)`
1 | import os
2 | import os.path
3 + import pathlib
4 |
5 | p = "/foo"
6 | q = "bar"
--------------------------------------------------------------------------------
128 | os.makedirs("name", unknown_kwarg=True)
129 |
130 | # https://github.com/astral-sh/ruff/issues/20134
- os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
131 + pathlib.Path("pth1_link").chmod(mode=0o600, follow_symlinks= False)
132 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
133 |
134 | # Only diagnostic
PTH101 [*] `os.chmod()` should be replaced by `Path.chmod()`
--> full_name.py:131:1
|
129 | # https://github.com/astral-sh/ruff/issues/20134
130 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
| ^^^^^^^^
132 |
133 | # Only diagnostic
|
help: Replace with `Path(...).chmod(...)`
1 | import os
2 | import os.path
3 + import pathlib
4 |
5 | p = "/foo"
6 | q = "bar"
--------------------------------------------------------------------------------
129 |
130 | # https://github.com/astral-sh/ruff/issues/20134
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
- os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
132 + pathlib.Path("pth1_link").chmod(mode=0o600, follow_symlinks=True)
133 |
134 | # Only diagnostic
135 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
PTH101 `os.chmod()` should be replaced by `Path.chmod()`
--> full_name.py:134:1
|
133 | # Only diagnostic
134 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^
135 |
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
|
help: Replace with `Path(...).chmod(...)`
PTH104 `os.rename()` should be replaced by `Path.rename()`
--> full_name.py:136:1
|
134 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
135 |
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^^
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
|
help: Replace with `Path(...).rename(...)`
PTH105 `os.replace()` should be replaced by `Path.replace()`
--> full_name.py:137:1
|
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^^^
138 |
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
|
help: Replace with `Path(...).replace(...)`
PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
--> full_name.py:139:1
|
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
138 |
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^^^^^^^^^
|
help: Replace with `Path(...).samefile()`

View File

@@ -2,7 +2,7 @@ use ast::FStringFlags;
use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Arguments, Expr};
use ruff_python_ast::{self as ast, Arguments, Expr, StringFlags};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
@@ -72,24 +72,42 @@ fn is_static_length(elts: &[Expr]) -> bool {
fn build_fstring(joiner: &str, joinees: &[Expr], flags: FStringFlags) -> Option<Expr> {
// If all elements are string constants, join them into a single string.
if joinees.iter().all(Expr::is_string_literal_expr) {
let mut flags = None;
let node = ast::StringLiteral {
value: joinees
.iter()
.filter_map(|expr| {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = expr {
if flags.is_none() {
// take the flags from the first Expr
flags = Some(value.first_literal_flags());
}
Some(value.to_str())
} else {
None
let mut flags: Option<ast::StringLiteralFlags> = None;
let content = joinees
.iter()
.filter_map(|expr| {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = expr {
if flags.is_none() {
// Take the flags from the first Expr
flags = Some(value.first_literal_flags());
}
})
.join(joiner)
.into_boxed_str(),
flags: flags?,
Some(value.to_str())
} else {
None
}
})
.join(joiner);
let mut flags = flags?;
// If the result is a raw string and contains a newline, use triple quotes.
if flags.prefix().is_raw() && content.contains(['\n', '\r']) {
flags = flags.with_triple_quotes(ruff_python_ast::str::TripleQuotes::Yes);
// Prefer a delimiter that doesn't occur in the content; if both occur, bail.
if content.contains(flags.quote_str()) {
flags = flags.with_quote_style(flags.quote_style().opposite());
if content.contains(flags.quote_str()) {
// Both "'''" and "\"\"\"" are present in content; avoid emitting
// an invalid raw triple-quoted literal (or escaping). Bail on the fix.
return None;
}
}
}
let node = ast::StringLiteral {
value: content.into_boxed_str(),
flags,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};

View File

@@ -125,18 +125,39 @@ help: Replace with `f"{secrets.token_urlsafe()}a{secrets.token_hex()}"`
13 | nok2 = a.join(["1", "2", "3"]) # Not OK (not a static joiner)
note: This is an unsafe fix and may change runtime behavior
FLY002 [*] Consider `f"{url}{filename}"` instead of string join
--> FLY002.py:23:11
FLY002 [*] Consider f-string instead of string join
--> FLY002.py:20:8
|
21 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
22 | def create_file_public_url(url, filename):
23 | return''.join([url, filename])
18 | nok7 = "a".join([f"foo{8}", "bar"]) # Not OK (contains an f-string)
19 | # https://github.com/astral-sh/ruff/issues/19887
20 | nok8 = '\n'.join([r'line1','line2'])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
21 | nok9 = '\n'.join([r"raw string", '<""">', "<'''>"]) # Not OK (both triple-quote delimiters appear; should bail)
|
help: Replace with f-string
17 | nok6 = "a".join(x for x in "feefoofum") # Not OK (generator)
18 | nok7 = "a".join([f"foo{8}", "bar"]) # Not OK (contains an f-string)
19 | # https://github.com/astral-sh/ruff/issues/19887
- nok8 = '\n'.join([r'line1','line2'])
20 + nok8 = r'''line1
21 + line2'''
22 | nok9 = '\n'.join([r"raw string", '<""">', "<'''>"]) # Not OK (both triple-quote delimiters appear; should bail)
23 |
24 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
note: This is an unsafe fix and may change runtime behavior
FLY002 [*] Consider `f"{url}{filename}"` instead of string join
--> FLY002.py:25:11
|
23 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
24 | def create_file_public_url(url, filename):
25 | return''.join([url, filename])
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Replace with `f"{url}{filename}"`
20 |
21 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
22 | def create_file_public_url(url, filename):
22 |
23 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
24 | def create_file_public_url(url, filename):
- return''.join([url, filename])
23 + return f"{url}{filename}"
25 + return f"{url}{filename}"
note: This is an unsafe fix and may change runtime behavior

View File

@@ -269,7 +269,7 @@ pub(crate) fn indentation(
range: TextRange,
context: &LintContext,
) {
if indent_level % indent_size != 0 {
if !indent_level.is_multiple_of(indent_size) {
if logical_line.is_comment_only() {
context.report_diagnostic_if_enabled(
IndentationWithInvalidMultipleComment {

View File

@@ -920,6 +920,42 @@ mod tests {
flakes("__annotations__", &[]);
}
#[test]
fn module_warningregistry() {
// Using __warningregistry__ should not be considered undefined.
flakes("__warningregistry__", &[]);
}
#[test]
fn module_annotate_py314_available() {
// __annotate__ is available starting in Python 3.14.
let diagnostics = crate::test::test_snippet(
"__annotate__",
&crate::settings::LinterSettings {
unresolved_target_version: ruff_python_ast::PythonVersion::PY314.into(),
..crate::settings::LinterSettings::for_rules(vec![
crate::codes::Rule::UndefinedName,
])
},
);
assert!(diagnostics.is_empty());
}
#[test]
fn module_annotate_pre_py314_undefined() {
// __annotate__ is not available before Python 3.14.
let diagnostics = crate::test::test_snippet(
"__annotate__",
&crate::settings::LinterSettings {
unresolved_target_version: ruff_python_ast::PythonVersion::PY313.into(),
..crate::settings::LinterSettings::for_rules(vec![
crate::codes::Rule::UndefinedName,
])
},
);
assert_eq!(diagnostics.len(), 1);
}
#[test]
fn magic_globals_file() {
// Use of the C{__file__} magic global should not emit an undefined name

View File

@@ -139,7 +139,11 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall
return;
};
if !((first_arg_id == "__class__" || first_arg_id == parent_name.as_str())
// The `super(__class__, self)` and `super(ParentClass, self)` patterns are redundant in Python 3
// when the first argument refers to the implicit `__class__` cell or to the enclosing class.
// Avoid triggering if a local variable shadows either name.
if !(((first_arg_id == "__class__") || (first_arg_id == parent_name.as_str()))
&& !checker.semantic().current_scope().has(first_arg_id)
&& second_arg_id == parent_arg.name().as_str())
{
return;

View File

@@ -6,7 +6,7 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::edits::pad;
use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
use crate::{Edit, Fix, FixAvailability, Violation};
/// ## What it does
/// Checks for membership tests against single-item containers.
@@ -27,13 +27,16 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
/// ```
///
/// ## Fix safety
/// The fix is always marked as unsafe.
///
/// When the right-hand side is a string, the fix is marked as unsafe.
/// This is because `c in "a"` is true both when `c` is `"a"` and when `c` is the empty string,
/// so the fix can change the behavior of your program in these cases.
/// When the right-hand side is a string, this fix can change the behavior of your program.
/// This is because `c in "a"` is true both when `c` is `"a"` and when `c` is the empty string.
///
/// Additionally, if there are comments within the fix's range,
/// it will also be marked as unsafe.
/// Additionally, converting `in`/`not in` against a single-item container to `==`/`!=` can
/// change runtime behavior: `in` may consider identity (e.g., `NaN`) and always
/// yields a `bool`.
///
/// Comments within the replacement range will also be removed.
///
/// ## References
/// - [Python documentation: Comparisons](https://docs.python.org/3/reference/expressions.html#comparisons)
@@ -100,14 +103,8 @@ pub(crate) fn single_item_membership_test(
expr.range(),
);
let applicability =
if right.is_string_literal_expr() || checker.comment_ranges().intersects(expr.range()) {
Applicability::Unsafe
} else {
Applicability::Safe
};
let fix = Fix::applicable_edit(edit, applicability);
// All supported cases can change runtime behavior; mark as unsafe.
let fix = Fix::unsafe_edit(edit);
checker
.report_diagnostic(SingleItemMembershipTest { membership_test }, expr.range())

View File

@@ -293,7 +293,19 @@ fn handle_non_finite_float_special_case(
return None;
};
let normalized = as_non_finite_float_string_literal(float_arg)?;
let replacement_text = format!(r#"{constructor_name}("{normalized}")"#);
let replacement_text = format!(
r#"{constructor_name}("{}")"#,
// `Decimal.from_float(float(" -nan")) == Decimal("nan")`
if normalized == "-nan" {
// Here we do not attempt to remove just the '-' character.
// It may have been encoded (e.g. as '\N{hyphen-minus}')
// in the original source slice, and the added complexity
// does not make sense for this edge case.
"nan"
} else {
normalized
}
);
Some(Edit::range_replacement(replacement_text, call.range()))
}

View File

@@ -363,7 +363,7 @@ help: Replace with `Decimal` constructor
21 | _ = Decimal.from_float(float("-Infinity"))
22 | _ = Decimal.from_float(float("nan"))
- _ = Decimal.from_float(float("-NaN"))
23 + _ = Decimal("-nan")
23 + _ = Decimal("nan")
24 | _ = Decimal.from_float(float(" \n+nan \t"))
25 | _ = Decimal.from_float(float(" iNf\n\t "))
26 | _ = Decimal.from_float(float(" -inF\n \t"))
@@ -655,7 +655,7 @@ help: Replace with `Decimal` constructor
62 |
63 | # Cases with non-finite floats - should produce safe fixes
- _ = Decimal.from_float(float("-nan"))
64 + _ = Decimal("-nan")
64 + _ = Decimal("nan")
65 | _ = Decimal.from_float(float("\x2dnan"))
66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
@@ -673,7 +673,7 @@ help: Replace with `Decimal` constructor
63 | # Cases with non-finite floats - should produce safe fixes
64 | _ = Decimal.from_float(float("-nan"))
- _ = Decimal.from_float(float("\x2dnan"))
65 + _ = Decimal("-nan")
65 + _ = Decimal("nan")
66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
FURB164 [*] Verbose method `from_float` in `Decimal` construction
@@ -689,4 +689,4 @@ help: Replace with `Decimal` constructor
64 | _ = Decimal.from_float(float("-nan"))
65 | _ = Decimal.from_float(float("\x2dnan"))
- _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
66 + _ = Decimal("-nan")
66 + _ = Decimal("nan")

View File

@@ -18,6 +18,7 @@ help: Convert to equality test
4 | print("Single-element tuple")
5 |
6 | if 1 in [1]:
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:6:4
@@ -37,6 +38,7 @@ help: Convert to equality test
7 | print("Single-element list")
8 |
9 | if 1 in {1}:
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:9:4
@@ -56,6 +58,7 @@ help: Convert to equality test
10 | print("Single-element set")
11 |
12 | if "a" in "a":
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:12:4
@@ -95,6 +98,7 @@ help: Convert to inequality test
16 | print("Check `not in` membership test")
17 |
18 | if not 1 in (1,):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:18:8
@@ -114,6 +118,7 @@ help: Convert to equality test
19 | print("Check the negated membership test")
20 |
21 | # Non-errors.
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:52:5
@@ -344,4 +349,122 @@ help: Convert to inequality test
- ] and \
113 + if foo != bar and \
114 | 0 < 1: ...
115 |
116 | # https://github.com/astral-sh/ruff/issues/20255
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:125:4
|
124 | # NaN behavior differences
125 | if math.nan in [math.nan]:
| ^^^^^^^^^^^^^^^^^^^^^^
126 | print("This is True")
|
help: Convert to equality test
122 | import math
123 |
124 | # NaN behavior differences
- if math.nan in [math.nan]:
125 + if math.nan == math.nan:
126 | print("This is True")
127 |
128 | if math.nan in (math.nan,):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:128:4
|
126 | print("This is True")
127 |
128 | if math.nan in (math.nan,):
| ^^^^^^^^^^^^^^^^^^^^^^^
129 | print("This is True")
|
help: Convert to equality test
125 | if math.nan in [math.nan]:
126 | print("This is True")
127 |
- if math.nan in (math.nan,):
128 + if math.nan == math.nan:
129 | print("This is True")
130 |
131 | if math.nan in {math.nan}:
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:131:4
|
129 | print("This is True")
130 |
131 | if math.nan in {math.nan}:
| ^^^^^^^^^^^^^^^^^^^^^^
132 | print("This is True")
|
help: Convert to equality test
128 | if math.nan in (math.nan,):
129 | print("This is True")
130 |
- if math.nan in {math.nan}:
131 + if math.nan == math.nan:
132 | print("This is True")
133 |
134 | # Potential type differences with custom __eq__ methods
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:140:4
|
139 | obj = CustomEq()
140 | if obj in [CustomEq()]:
| ^^^^^^^^^^^^^^^^^^^
141 | pass
|
help: Convert to equality test
137 | return "custom"
138 |
139 | obj = CustomEq()
- if obj in [CustomEq()]:
140 + if obj == CustomEq():
141 | pass
142 |
143 | if obj in (CustomEq(),):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:143:4
|
141 | pass
142 |
143 | if obj in (CustomEq(),):
| ^^^^^^^^^^^^^^^^^^^^
144 | pass
|
help: Convert to equality test
140 | if obj in [CustomEq()]:
141 | pass
142 |
- if obj in (CustomEq(),):
143 + if obj == CustomEq():
144 | pass
145 |
146 | if obj in {CustomEq()}:
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:146:4
|
144 | pass
145 |
146 | if obj in {CustomEq()}:
| ^^^^^^^^^^^^^^^^^^^
147 | pass
|
help: Convert to equality test
143 | if obj in (CustomEq(),):
144 | pass
145 |
- if obj in {CustomEq()}:
146 + if obj == CustomEq():
147 | pass
note: This is an unsafe fix and may change runtime behavior

View File

@@ -18,6 +18,7 @@ help: Convert to equality test
4 | print("Single-element set")
5 |
6 | if 1 in set((1,)):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:6:4
@@ -37,6 +38,7 @@ help: Convert to equality test
7 | print("Single-element set")
8 |
9 | if 1 in set({1}):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:9:4
@@ -56,6 +58,7 @@ help: Convert to equality test
10 | print("Single-element set")
11 |
12 | if 1 in frozenset([1]):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:12:4
@@ -75,6 +78,7 @@ help: Convert to equality test
13 | print("Single-element set")
14 |
15 | if 1 in frozenset((1,)):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:15:4
@@ -94,6 +98,7 @@ help: Convert to equality test
16 | print("Single-element set")
17 |
18 | if 1 in frozenset({1}):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:18:4
@@ -113,6 +118,7 @@ help: Convert to equality test
19 | print("Single-element set")
20 |
21 | if 1 in set(set([1])):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:21:4
@@ -131,4 +137,42 @@ help: Convert to equality test
21 + if 1 == 1:
22 | print('Recursive solution')
23 |
24 |
24 |
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:59:4
|
58 | # set() and frozenset() with NaN
59 | if math.nan in set([math.nan]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
60 | print("This should be marked unsafe")
|
help: Convert to equality test
56 | import math
57 |
58 | # set() and frozenset() with NaN
- if math.nan in set([math.nan]):
59 + if math.nan == math.nan:
60 | print("This should be marked unsafe")
61 |
62 | if math.nan in frozenset([math.nan]):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:62:4
|
60 | print("This should be marked unsafe")
61 |
62 | if math.nan in frozenset([math.nan]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
63 | print("This should be marked unsafe")
|
help: Convert to equality test
59 | if math.nan in set([math.nan]):
60 | print("This should be marked unsafe")
61 |
- if math.nan in frozenset([math.nan]):
62 + if math.nan == math.nan:
63 | print("This should be marked unsafe")
note: This is an unsafe fix and may change runtime behavior

View File

@@ -112,6 +112,7 @@ mod tests {
#[test_case(Rule::LegacyFormPytestRaises, Path::new("RUF061_warns.py"))]
#[test_case(Rule::LegacyFormPytestRaises, Path::new("RUF061_deprecated_call.py"))]
#[test_case(Rule::NonOctalPermissions, Path::new("RUF064.py"))]
#[test_case(Rule::LoggingEagerConversion, Path::new("RUF065.py"))]
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_0.py"))]
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_1.py"))]
#[test_case(Rule::InvalidRuleCode, Path::new("RUF102.py"))]

View File

@@ -0,0 +1,131 @@
use std::str::FromStr;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_literal::cformat::{CFormatPart, CFormatString, CFormatType};
use ruff_python_literal::format::FormatConversion;
use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::rules::flake8_logging_format::rules::{LoggingCallType, find_logging_call};
/// ## What it does
/// Checks for eager string conversion of arguments to `logging` calls.
///
/// ## Why is this bad?
/// Arguments to `logging` calls will be formatted as strings automatically, so it
/// is unnecessary and less efficient to eagerly format the arguments before passing
/// them in.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`lint.logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`lint.logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
///
/// logging.basicConfig(format="%(message)s", level=logging.INFO)
///
/// user = "Maria"
///
/// logging.info("%s - Something happened", str(user))
/// ```
///
/// Use instead:
/// ```python
/// import logging
///
/// logging.basicConfig(format="%(message)s", level=logging.INFO)
///
/// user = "Maria"
///
/// logging.info("%s - Something happened", user)
/// ```
///
/// ## Options
/// - `lint.logger-objects`
///
/// ## References
/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html)
/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization)
#[derive(ViolationMetadata)]
pub(crate) struct LoggingEagerConversion {
pub(crate) format_conversion: FormatConversion,
}
impl Violation for LoggingEagerConversion {
#[derive_message_formats]
fn message(&self) -> String {
let LoggingEagerConversion { format_conversion } = self;
let (format_str, call_arg) = match format_conversion {
FormatConversion::Str => ("%s", "str()"),
FormatConversion::Repr => ("%r", "repr()"),
FormatConversion::Ascii => ("%a", "ascii()"),
FormatConversion::Bytes => ("%b", "bytes()"),
};
format!("Unnecessary `{call_arg}` conversion when formatting with `{format_str}`")
}
}
/// RUF065
pub(crate) fn logging_eager_conversion(checker: &Checker, call: &ast::ExprCall) {
let Some((logging_call_type, _range)) = find_logging_call(checker, call) else {
return;
};
let msg_pos = match logging_call_type {
LoggingCallType::LevelCall(_) => 0,
LoggingCallType::LogCall => 1,
};
// Extract a format string from the logging statement msg argument
let Some(Expr::StringLiteral(string_literal)) =
call.arguments.find_argument_value("msg", msg_pos)
else {
return;
};
let Ok(format_string) = CFormatString::from_str(string_literal.value.to_str()) else {
return;
};
// Iterate over % placeholders in format string and zip with logging statement arguments
for (spec, arg) in format_string
.iter()
.filter_map(|(_, part)| {
if let CFormatPart::Spec(spec) = part {
Some(spec)
} else {
None
}
})
.zip(call.arguments.args.iter().skip(msg_pos + 1))
{
// Check if the argument is a call to eagerly format a value
if let Expr::Call(ast::ExprCall { func, .. }) = arg {
let CFormatType::String(format_conversion) = spec.format_type else {
continue;
};
// Check for use of %s with str() or %r with repr()
if checker.semantic().match_builtin_expr(func.as_ref(), "str")
&& matches!(format_conversion, FormatConversion::Str)
|| checker.semantic().match_builtin_expr(func.as_ref(), "repr")
&& matches!(format_conversion, FormatConversion::Repr)
{
checker
.report_diagnostic(LoggingEagerConversion { format_conversion }, arg.range());
}
}
}
}

View File

@@ -23,6 +23,7 @@ pub(crate) use invalid_index_type::*;
pub(crate) use invalid_pyproject_toml::*;
pub(crate) use invalid_rule_code::*;
pub(crate) use legacy_form_pytest_raises::*;
pub(crate) use logging_eager_conversion::*;
pub(crate) use map_int_version_parsing::*;
pub(crate) use missing_fstring_syntax::*;
pub(crate) use mutable_class_default::*;
@@ -86,6 +87,7 @@ mod invalid_index_type;
mod invalid_pyproject_toml;
mod invalid_rule_code;
mod legacy_form_pytest_raises;
mod logging_eager_conversion;
mod map_int_version_parsing;
mod missing_fstring_syntax;
mod mutable_class_default;

View File

@@ -0,0 +1,83 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
assertion_line: 124
---
RUF065 Unnecessary `str()` conversion when formatting with `%s`
--> RUF065.py:4:26
|
3 | # %s + str()
4 | logging.info("Hello %s", str("World!"))
| ^^^^^^^^^^^^^
5 | logging.log(logging.INFO, "Hello %s", str("World!"))
|
RUF065 Unnecessary `str()` conversion when formatting with `%s`
--> RUF065.py:5:39
|
3 | # %s + str()
4 | logging.info("Hello %s", str("World!"))
5 | logging.log(logging.INFO, "Hello %s", str("World!"))
| ^^^^^^^^^^^^^
6 |
7 | # %s + repr()
|
RUF065 Unnecessary `repr()` conversion when formatting with `%r`
--> RUF065.py:16:26
|
15 | # %r + repr()
16 | logging.info("Hello %r", repr("World!"))
| ^^^^^^^^^^^^^^
17 | logging.log(logging.INFO, "Hello %r", repr("World!"))
|
RUF065 Unnecessary `repr()` conversion when formatting with `%r`
--> RUF065.py:17:39
|
15 | # %r + repr()
16 | logging.info("Hello %r", repr("World!"))
17 | logging.log(logging.INFO, "Hello %r", repr("World!"))
| ^^^^^^^^^^^^^^
18 |
19 | from logging import info, log
|
RUF065 Unnecessary `str()` conversion when formatting with `%s`
--> RUF065.py:22:18
|
21 | # %s + str()
22 | info("Hello %s", str("World!"))
| ^^^^^^^^^^^^^
23 | log(logging.INFO, "Hello %s", str("World!"))
|
RUF065 Unnecessary `str()` conversion when formatting with `%s`
--> RUF065.py:23:31
|
21 | # %s + str()
22 | info("Hello %s", str("World!"))
23 | log(logging.INFO, "Hello %s", str("World!"))
| ^^^^^^^^^^^^^
24 |
25 | # %s + repr()
|
RUF065 Unnecessary `repr()` conversion when formatting with `%r`
--> RUF065.py:34:18
|
33 | # %r + repr()
34 | info("Hello %r", repr("World!"))
| ^^^^^^^^^^^^^^
35 | log(logging.INFO, "Hello %r", repr("World!"))
|
RUF065 Unnecessary `repr()` conversion when formatting with `%r`
--> RUF065.py:35:31
|
33 | # %r + repr()
34 | info("Hello %r", repr("World!"))
35 | log(logging.INFO, "Hello %r", repr("World!"))
| ^^^^^^^^^^^^^^
36 |
37 | def str(s): return f"str = {s}"
|

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